8

I am looking for a way to display data in a DataGrid from types that are unknown at compile-time.

I have the following base class

public abstract class Entity
{
    // Some implementation of methods ...
}

In run-time, I load a plug-in DLL and use reflection to get a list of all the types derived from Entity. For example:

public class A : Entity
{
    public LocalAddress Address{ get; set; }
}

public class B : Entity
{
    public Vendor Vendor { get; set; }

    public string Name { get; set; }
}

Then I retreive a list of their instances from DB

public IEnumerable<Entity> Entities { get; set; } // A list of instances of type A for example

Entities is the DataGrid's ItemsSource, But what's the best way I can bind the properties to the DataGrid? Since the properties can be complex, I also need to be able to bind to a specific path, for example Address.HomeNum ...

Clarifications

  1. I only need to show a one grid of a type's instances at a time. The complete scenario is this:

    1. I get a list of types that derive from Entity from the plug-in DLL through reflection
    2. I show their names in a List. (in this example that list will contain A and B
    3. When the user clicks on a specific item, let's say A, I get a list of A instances from DB - so far so good.
    4. I want to display that list of A's instances in a DataGrid.
    5. When the user selects another item from the list (meaning another type, lets say B), I get a list of B's instances from DB and need to display those in the grid and so on ...
  2. The plug-in DLL is a class library with no xamls (also my users are the ones making this plug-ins and I don't want them to have to write DataTemplates for their entities. I also can't make predifned DataTemplates as I don't know the types I'll need to display until run-time. Each type can have different types and amount of properties. All I know in complie-time is that they all derived from Entity.

  3. The grid should also be editable.
Omri Btian
  • 6,499
  • 4
  • 39
  • 65
  • Create specific DataGrids for each entity type and use a `ContentPresenter` to switch Views at runtime. keep it simple. – Federico Berasategui Oct 30 '13 at 21:24
  • I am not sure if you need to update properties in the types derived from Entity, and whether those updates need to be shown in the ItemsControl. If you do, you will either need to implement INotifyPropertyChanged change notifications for those properties, or turn them into Dependency properties. –  Oct 30 '13 at 21:30
  • If you actually want them in one grid all at the same time, you could make your view model property be of type IEnumerable. – Millie Smith Oct 30 '13 at 23:15
  • @HighCore I made some updates to the question can you take a look? So far the best idea I came up with is iterate the type's properties through reflection and programatically create it's columns. Is there an easier way? – Omri Btian Oct 31 '13 at 07:43
  • @MillieSmith I do not need to display multiple types in one grid, only one type at a time. I've updated my question with clarifications ... – Omri Btian Oct 31 '13 at 07:45
  • 1
    Why not set `AutoGenerateColumns` property to `True` and let datagrid create columns for you based on properties exposed by your object? – Rohit Vats Nov 04 '13 at 19:12
  • @RohitVats because entity may contain complex properties that contain their own properties I might want bind f.e `Employee.Devision.IsNew` and I cannot simply override the ToString method of Devision because I want the column to be a Checkbox since IsNew property is boolean ... – Omri Btian Nov 04 '13 at 22:37
  • I have to ask, is Entity a DependencyObject or does it implement INotifyPropertyChanged or neither? – Bizhan Nov 07 '13 at 10:46
  • @Bizz neither at the moment. But I guess I could make it implement `INotifyPropertyChanged` ... – Omri Btian Nov 07 '13 at 11:04
  • It would be impossible or too difficult if neither is used. but I've edited my answer, if that's what you're looking for. – Bizhan Nov 07 '13 at 11:45
  • Would the answer to this question help? http://stackoverflow.com/questions/3065758/wpf-mvvm-datagrid-dynamic-columns – BanksySan Nov 08 '13 at 18:04
  • Does the answer to this question help? http://stackoverflow.com/questions/3065758/wpf-mvvm-datagrid-dynamic-columns – BanksySan Nov 08 '13 at 18:05

3 Answers3

5

A DataGrid seems inappropriate in this case. If your list was bound to two separate entities, it would break badly.

A better option would potentially be to use some other ItemsControl and set up a DataTemplate for each type of Entity. This would allow you to build custom editors per entity, and have a "list" of them to edit.

If you know the entities will always be of a single type, I'd instead build the collection of that specific type, and bind to it.

Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • Thanks for the answer. I made some updates to the question can you take a look? So far the best idea I came up with is iterate the type's properties through reflection and programatically create it's columns. Is there an easier way? – Omri Btian Oct 31 '13 at 07:43
  • @Omribitan Having a fixed type - ie: bind to an `ObservableCollection` instead of `OC`, and just build a data template for each one... No reflection needed, then. – Reed Copsey Oct 31 '13 at 17:54
  • 1
    That kind of works against the OP though in that the users are creating types in assemblies. Creating a template should either be up to the user (which the OP has said they don't want the users to have to do) or a dynamic grid should be used. This is all possible just using programmatic bindings/columns. – Charleh Nov 05 '13 at 04:05
4

Since you don't know the property names of the Entities beforehand, I think your best option is to keep your DataGrid in Xaml but move the defintion and the Bindings of its DataGridColumns to the code behind.

AddColumnsForProperty(PropertyInfo property, string parentPath = "")
{
     var title = property.Name;
     var path = parentPath + (parentPath=="" ? "" : ".") + property.Name;

     if(property.PropertyType == typeof(string))
     {
        var column = new DataGridTextColumn();
        column.Header = title;
        column.Binding = new Binding(path);
        dataGrid.Columns.Add(column);
     }
     else if(property.PropertyType == typeof(bool))
     {
        //use DataGridCheckBoxColumn and so on
     }
     else
     {
          //...
     }

     var properties = property.GetProperties();
     foreach(var item in properties)
     {
          AddColumnsForProperty(item, path);
     }
}

Now if you execute these you'll have your dataGrid columns filled. and by adding all instances of the desired type in an observable collection and bind it to ItemsSource of the DataGrid it should work. selectedItem should be an instance of one the classes derived from Entity. The listbox contains new A() and new B() (or any existing instances of A and B) so selectedItem can be used in the following statement.

var propertyList = selectedItem.GetType().GetProperties();
foreach (var property in propertyList) 
    AddColumnsForProperty(PropertyInfo property);

how to write DataGridColumnTemplate in code


Edit:

Member can't be used in this scenario because INotifyPropertyChanged should get involved, so I replaced members with properties.

Bizhan
  • 16,157
  • 9
  • 63
  • 101
0

I would use attributes to specify what exactly is bindable (including composite object):

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public abstract class EntityAttribute : Attribute
{
    internal abstract IEnumerable<EntityColumn> GetColumns(object instance, PropertyInfo property);
}

This attribute supports plain properties as well as composite structures. You should simply inherit and implement the method.

EntityColumn represents single value. Simplified version can be implemented like this:

public class EntityColumn
{
    private readonly Action<object> _setMethod;
    private readonly Func<object> _getMethod;

    public string Caption { get; private set; }

    public object Value
    {
        get { return _getMethod(); }
        set { _setMethod(value);}
    }

    internal EntityColumn(string caption, Action<object> setMethod, Func<object> getMethod)
    {
        _getMethod = getMethod;
        _setMethod = setMethod;
        Caption = caption;
    }
}

Later you can create single DataTemplate for EntityColumn and use it for all properties for all possible entities. Entity Object will contain additional method to return all EntityColumn relevant to it:

 public IList<EntityColumn> GetColumns()
    {
        var objectType = GetType();
        var properties = objectType.GetProperties();
        return properties.SelectMany(
            p => p.GetCustomAttributes<EntityAttribute>().SelectMany(a => a.GetColumns(this, p))).ToList();
    }

For collection of Entities you can introduce EntityCollection which will absorb column information and provide structure similar to DataSet. This implementation gives you flexibility of dynamic structure and keeps almost everything strongly typed. You can even extend attributes and EntityColumn to support validation.

As of displaying object, you'd rather use ItemsControl or even self written control inherited from ItemsControl to take advantage of knowing about Entity and EntityCollection classes.

agrinevsky
  • 31
  • 1