0

I'm trying to bind to a datagrid a collection of object where as this property will change depending on the requirement/s.

In my VM, I have this property:

private ObservableCollection<object> _productData;

        public ObservableCollection<object> ProductData
        {
            get { return _productData; }
            set { _productData = value; }
        }

And in my View:

<DataGrid CanUserAddRows="False"
                  ItemsSource="{Binding ProductData, UpdateSourceTrigger=PropertyChanged}"
                  IsReadOnly="True"
                  AutoGenerateColumns="True"
                  Margin="0 2 0 0" />

Is it possible to auto generate columns based on the "object" supplied?

Ex:

ProductData = new ObservableCollection<object>(SomethingThatReturnsClassAList());
Jack Frost
  • 318
  • 4
  • 13
  • If all objects in this collection are always of the same class, you should use templated class to store the collection – Krzysztof Skowronek Apr 25 '18 at 12:16
  • Actually, they're not.. They don't even have the same interface... – Jack Frost Apr 25 '18 at 12:19
  • then how do you imagine having them in a datagrid? That's lots of empty columns and cells – Krzysztof Skowronek Apr 25 '18 at 12:44
  • I tried looking into ExpandoObject and DynamicObject but I'm not sure how I will implement it. I thought of getting all the properties after ProductData = new ObservableCollection(SomethingThatReturnsClassAList()); by ProductDat[0].GetType() but I don't know what to do after that. – Jack Frost Apr 25 '18 at 12:50
  • So you want the generate columns based on the type of the first element in the source collection? – mm8 Apr 25 '18 at 14:25
  • Yes, because once the collection is generated, it will be all the same. The SomethingThatReturnsClassAList() can be also SomethingThatReturnsClassBList() and etc. So, everything will be the same type once the method returns a list... – Jack Frost Apr 26 '18 at 03:54
  • Short answer, you can't do it using this method. See https://stackoverflow.com/questions/882214/data-binding-dynamic-data – Adam Vincent Apr 26 '18 at 19:45

1 Answers1

1

The way DataGrid Auto Generates Columns is pretty robust, but unfortunately it does not work the way you want it to. One way or another, you need to tell it what columns to expect. If you give it an object type, it isn't going to reflect over what type is assigned to the 'object', it's just going to reflect over 'System.object', which will net you 0 columns auto generated. I'm not smart enough to explain this whole debacle. I just want to jump into the solution I came up with, which should work well for your purposes.

I actually surprised myself by making a working example. There's a few things going on here that need some 'splaining.

I left your XAML alone, it's still

<DataGrid CanUserAddRows="False"
              ItemsSource="{Binding ProductData, UpdateSourceTrigger=PropertyChanged}"
              IsReadOnly="True"
              AutoGenerateColumns="True"
              Margin="0 2 0 0" />

However, in the ViewModel, I've made a decision that hopefully you can work with, to changed ProducData from an ObservableCollection<> to a DataTable. The binding will still work the same, and the UI will update appropriately.

Your original intent was to make the ObservableCollection of type object to make things more generic, but here I've implemented the Factory Pattern to show a way to create multiple types without making things too complicated. You can take it or leave it for what it's worth.

ViewModel (I'm using Prism boilerplate, replace BindableBase with your implementation of INotifyPropertyChanged


public class ViewAViewModel : BindableBase
{
    private DataTable _productData;
    private IDataFactory _dataFactory;

    public ViewAViewModel(IDataFactory dataFactory)
    {
        _dataFactory = dataFactory;
    }

    public DataTable ProductData
    {
        get { return _productData; }
        set { _productData = value; OnPropertyChanged(); }
    }

    public void Load()
    {
        ProductData = _dataFactory.Create(typeof(FooData));
    }
}

DataFactory


public interface IDataFactory
{
    DataTable Create(Type t);
}

public class DataFactory : IDataFactory
{
    public DataTable Create(Type t)
    {

        if (t == typeof(FooData))
        {
            return new List<FooData>()
            {
                new FooData() {Id = 0, AlbumName = "Greatest Hits", IsPlatinum = true},
                new FooData() {Id = 1, AlbumName = "Worst Hits", IsPlatinum = false}
            }.ToDataTable();
        }

        if (t == typeof(BarData))
        {
            return new List<BarData>()
            {
                new BarData() {Id = 1, PenPointSize = 0.7m, InkColor = "Blue"},
                new BarData() {Id = 2, PenPointSize = 0.5m, InkColor = "Red"}
            }.ToDataTable();
        }

        return new List<dynamic>().ToDataTable();
    }
}

Base class and Data Models


public abstract class  ProductData
{
    public int Id { get; set; }
} 

public class FooData : ProductData
{
    public string AlbumName { get; set; }
    public bool IsPlatinum { get; set; }
}

public class BarData : ProductData
{
    public decimal PenPointSize { get; set; }
    public string InkColor { get; set; }
}

So for usage, you can swap FooData with BarData, or any type derived of ProductData

public void LoadFooData()
{
    ProductData = _dataFactory.Create(typeof(FooData));
}

Last but not least, I found this little gem somewhere on SO (If I find it again, will credit the author) This is an extension method to generate the DataTable from an IList<T> with column names based on the properties of T.

public static class DataFactoryExtensions
{
    public static DataTable ToDataTable<T>(this IList<T> data)
    {
        PropertyDescriptorCollection properties = 
            TypeDescriptor.GetProperties(typeof(T));
        DataTable table = new DataTable();
        foreach (PropertyDescriptor prop in properties)
            table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
        foreach (T item in data)
        {
            DataRow row = table.NewRow();
            foreach (PropertyDescriptor prop in properties)
                row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
            table.Rows.Add(row);
        }
        return table;
    }
}


public void LoadBarData()
{
    ProductData = _dataFactory.Create(typeof(BarData));
}

And in either case, INPC will update your UI.

Update based on your addition to the post

To implement this using a method that returns a list like your example; SomethingThatReturnsClassAList()

Just update factory like so. (Notice how easy it is to change your code to meet new requirements when you use the Factory Pattern? =)

public class DataFactory : IDataFactory
{
    public DataTable Create(Type t)
    {

        if (t == typeof(FooData))
        {
            return SomethingThatReturnsClassAList().ToDataTable();
        }

        if (t == typeof(BarData))
        {
            return SomethingThatReturnsClassBList().ToDataTable();
        }

        return new List<dynamic>().ToDataTable();
    }
}
Adam Vincent
  • 3,281
  • 14
  • 38
  • Hi Adam! This looks great! Though I managed to get away with it by adding OnPropertyChanged on ProductData. It's not tested yet on how will I save the changes on the datagrid, if its not possible, I will definitely try this one! For now, I will upvote it. Thanks!! – Jack Frost Apr 27 '18 at 03:43