12

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.

Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
Manoj
  • 5,011
  • 12
  • 52
  • 76
  • 1
    DataGrid simply doesn't support this. If you need dynamic columns there are other ways. – H H Jan 05 '13 at 11:06
  • @HenkHolterman I do need dynamic columns. Can you please point me to other ways of doing this? – Manoj Jan 05 '13 at 11:09
  • 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 Jan 05 '13 at 13:20
  • @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 Jan 05 '13 at 13:52
  • Would you really need different classes for different tests? Could you update your post by adding an example of such a class? – Sphinxxx Jan 05 '13 at 14:03
  • @Sphinxxx Yes the kind of data generated by different types of test would be unique to that particular type of test. We have a On/Off test which measures the time taken for the device to come to usable state - measured over many cycle. Then we test the functionality of the device which measures completely different characteristics according to the functionality being tested. – Manoj Jan 05 '13 at 15:05

2 Answers2

24

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) });            
        }            
    }
weston
  • 54,145
  • 21
  • 145
  • 203
  • 1
    BindableDynamicDictionary is perfect for my requirement. Thanks for the example with comments. – Manoj Jan 05 '13 at 15:13
  • Why do you use local variables `propChange` in `RaisePropertyChanged` and `names` in `dataGrid1_AutoGeneratedColumns()` ? – Sinatr Sep 22 '14 at 11:43
  • @Sinatr There is no requirement for them. I think I thought they made it more readable at the time. – weston Sep 22 '14 at 13:02
  • Just wondering if simply changing Dictionary key type from object to some type which implements IPropertyChanged wouldn't solve this? So each item will be wired via IPropertyChanged – sll Feb 19 '15 at 10:17
  • @sll Well a key that changes value while inside a dictionary is a bad idea, so I'm not sure how that would work. – weston Feb 19 '15 at 10:41
  • @weston I'm trying to follow the same steps you provided, but my datagrid is not showing the data only the columns and it's also showing an extra column called "Items", any idea what i could be doing wrong? – user3340627 Jun 17 '15 at 09:17
  • @user3340627 no sorry. You could ask question. – weston Jun 17 '15 at 09:35
1

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.