12
votes

I have a ObservableCollection<Dictionary> and want to bind it to a DataGrid.

ObservableDictionary<String,Object> NewRecord1 = new ObservableDictionary<string,object>();

Dictionary<String,Object> Record1 = new Dictionary<string,object>();
Record1.Add("FirstName", "FName1");
Record1.Add("LastName", "LName1");
Record1.Add("Age", "32");

DictRecords.Add(Record1);

Dictionary<String, Object> Record2 = new Dictionary<string, object>();
NewRecord2.Add("FirstName", "FName2");
NewRecord2.Add("LastName", "LName2");
NewRecord2.Add("Age", "42");

DictRecords.Add(Record2);

I wanted the keys to become the header of the DataGrid and the values of each Dictionary item to be the rows. Setting the ItemsSource does not work.

2
DataGrid simply doesn't support this. If you need dynamic columns there are other ways. - Henk Holterman
@HenkHolterman I do need dynamic columns. Can you please point me to other ways of doing this? - Manoj
In your example, it looks like you're adding persons to the grid. I assume you need dynamic columns because you'll need to display other things than persons in the same grid some other time? If so, would it be acceptable to make a Person class, and similar classes for all other items you need to display (instead of using Dictionary<>)? - Sphinxxx
@Sphinxxx Yes you are right. Actually it would be test results - multiple tests are run in sequence and currently running test result would be shown in the data grid. I considered adding a result class for every test result. I am confused at how I would update the data binding based on the test being run. I did not want to add a select statement in the view to select the appropriate object to bind - as this would require me to update it every time we add a new test. i thought this was not a good design. Any better way of handling this? - Manoj
Would you really need different classes for different tests? Could you update your post by adding an example of such a class? - Sphinxxx

2 Answers

24
votes

You could use a bindable dynamic dictionary. This will expose each dictionary entry as a property.

/// <summary>
/// Bindable dynamic dictionary.
/// </summary>
public sealed class BindableDynamicDictionary : DynamicObject, INotifyPropertyChanged
{
    /// <summary>
    /// The internal dictionary.
    /// </summary>
    private readonly Dictionary<string, object> _dictionary;

    /// <summary>
    /// Creates a new BindableDynamicDictionary with an empty internal dictionary.
    /// </summary>
    public BindableDynamicDictionary()
    {
        _dictionary = new Dictionary<string, object>();
    }

    /// <summary>
    /// Copies the contents of the given dictionary to initilize the internal dictionary.
    /// </summary>
    /// <param name="source"></param>
    public BindableDynamicDictionary(IDictionary<string, object> source)
    {
        _dictionary = new Dictionary<string, object>(source);
    }
    /// <summary>
    /// You can still use this as a dictionary.
    /// </summary>
    /// <param name="key"></param>
    /// <returns></returns>
    public object this[string key]
    {
        get
        {
            return _dictionary[key];
        }
        set
        {
            _dictionary[key] = value;
            RaisePropertyChanged(key);
        }
    }

    /// <summary>
    /// This allows you to get properties dynamically.
    /// </summary>
    /// <param name="binder"></param>
    /// <param name="result"></param>
    /// <returns></returns>
    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        return _dictionary.TryGetValue(binder.Name, out result);
    }

    /// <summary>
    /// This allows you to set properties dynamically.
    /// </summary>
    /// <param name="binder"></param>
    /// <param name="value"></param>
    /// <returns></returns>
    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        _dictionary[binder.Name] = value;
        RaisePropertyChanged(binder.Name);
        return true;
    }

    /// <summary>
    /// This is used to list the current dynamic members.
    /// </summary>
    /// <returns></returns>
    public override IEnumerable<string> GetDynamicMemberNames()
    {
        return _dictionary.Keys;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void RaisePropertyChanged(string propertyName)
    {
        var propChange = PropertyChanged;
        if (propChange == null) return;
        propChange(this, new PropertyChangedEventArgs(propertyName));
    }
}

Then you can use it like this:

    private void testButton1_Click(object sender, RoutedEventArgs e)
    {
        // Creating a dynamic dictionary.
        var dd = new BindableDynamicDictionary();

        //access like any dictionary
        dd["Age"] = 32;

        //or as a dynamic
        dynamic person = dd;

        // Adding new dynamic properties.  
        // The TrySetMember method is called.
        person.FirstName = "Alan";
        person.LastName = "Evans";

        //hacky for short example, should have a view model and use datacontext
        var collection = new ObservableCollection<object>();
        collection.Add(person);
        dataGrid1.ItemsSource = collection;
    }

Datagrid needs custom code for building the columns up:

XAML:

<DataGrid AutoGenerateColumns="True" Name="dataGrid1" AutoGeneratedColumns="dataGrid1_AutoGeneratedColumns" />

AutoGeneratedColumns event:

    private void dataGrid1_AutoGeneratedColumns(object sender, EventArgs e)
    {
        var dg = sender as DataGrid;
        var first = dg.ItemsSource.Cast<object>().FirstOrDefault() as DynamicObject;
        if (first == null) return;
        var names = first.GetDynamicMemberNames();
        foreach(var name in names)
        {
            dg.Columns.Add(new DataGridTextColumn { Header = name, Binding = new Binding(name) });            
        }            
    }
1
votes

Based on westons answer i came up with another solution without using a custom BindableDynamicDictionary class.

There is a class called ExpandoObject in the namespace System.Dynamic(which is heavily used in ASP.NET).

It basically does the same thing as westons BindableDynamicDictionary with the drawback of not having the index operator available since it explicitly implements the interface IDictionary<string, object>

private void MyDataGrid_AutoGeneratedColumns(object sender, EventArgs e)
{
  var dg = sender as DataGrid;
  dg.Columns.Clear();
  var first = dg.ItemsSource.Cast<object>().FirstOrDefault() as IDictionary<string, object>;
  if (first == null) return;
  var names = first.Keys;
  foreach (var name in names)
  {
    dg.Columns.Add(new DataGridTextColumn { Header = name, Binding = new Binding(name) });
  }
}

Notice that the only difference here is that you have to cast the ExpandoObject to IDictionary<string, object> to access/add values or properties via the index operator.