Binding a Silverlight DataGrid to dynamic data via IDictionary

摘要:Binding a Silverlight DataGrid to dynamic data via IDictionary

Binding a Silverlight DataGrid to dynamic data

This post demonstrates a technique for binding a Silverlight DataGrid to dynamic data, the structure of which is not know at compile-time …

Update: I have extended this technique to add include change notification so that the DataGrid can be made editable. Read all about it in part two.

With Silverlight binding data to a DataGrid is a very straightforward process. With an XML datasource, you can simply parse your data via Linq to SQL into an anonymous type then bind the resulting collection to the grid. Just a few simple lines of code. But, what if the data you want to bind to your grid is dynamic? That is, at compile time you do not know how many columns are required, or what their contents is. This is a common problem that Silverlight users have faced again, and again, and again and again!

The most obvious solution to this problem is to create a dictionary for each row of your DataGrid, and bind your columns to your values via the dictionary’s string indexer:

Binding="{Binding Path=[Name]}"

However, unfortunately the PropertyPath syntax used for binding does not understand indexers, making it impossible to bind to a dictionary.

Vladimir Bodurov presents an ingenious solution to this problem by dynamically generating a new Type based on the values present within the dictionary, i.e. If the dictionary contains the keys “Name” and “Age”, a Type will be generated that has properties of Name and Age. Crazy stuff! Here I would like to show an alternative method that does not use intermediate Language or other black magic!

We will start with a simple example of a class which can be used to store ‘dynamic’ data for rendering in our DataGrid:


{
  private Dictionary<string, object> _data = new Dictionary<string, object>();
  public object this [string index]
  {
    get { return _data[index]; }
    set { _data[index] = value; }
  }
}

We can populate a collection of these objects and associate them with a DataGrid as follows:


Random rand = new Random();
 
var rows = new ObservableCollection<Row>();
for (int i = 0; i < 200; i++)
{
  Row row = new Row();
  row["Forename"] = s_names[rand.Next(s_names.Length)];
  row["Surname"] = s_surnames[rand.Next(s_surnames.Length)];
  row["Age"] = rand.Next(40) + 10;
  row["Shoesize"] = rand.Next(10) + 5;
  rows.Add(row);
}
 
_dataGrid.ItemsSource = rows;

However, as mentioned earlier, we cannot create a binding path that accesses our Rows indexer, so just how do we bind the columns to our data?

The classic .NET solution to this problem would be to create a custom property descriptor. When databinding, the properties of an object are not accessed directly, rather they are accessed via their associated property descriptor. A custom property descriptor can be supplied for our Row class, via the ICustomTypeDescriptor interface for example, that exposes properties which when accessed invoke our indexer. This is how the .NET DataRowView exposes its properties. Unfortunately there is one small snag here … Silverlight does not include the required interfaces to create custom properties.

A simple workaround to this problem is to use a binding that binds each row directly to each Row item rather than a specific property of the item, then use a value converter to access the indexer and extract the required value. Here is an example:


  <data:DataGrid.Columns>
    <data:DataGridTextColumn Header="Forename" Binding="{Binding Converter={StaticResource RowIndexConverter}, ConverterParameter=Forename}"/> 
    <data:DataGridTextColumn Header="Surname" Binding="{Binding Converter={StaticResource RowIndexConverter}, ConverterParameter=Surname}"/> 
    <data:DataGridTextColumn Header="Age" Binding="{Binding Converter={StaticResource RowIndexConverter}, ConverterParameter=Age}"/> 
    <data:DataGridTextColumn Header="Shoesize" Binding="{Binding Converter={StaticResource RowIndexConverter}, ConverterParameter=Shoesize}"/> 
  </data:DataGrid.Columns>
</data:DataGrid>

And here is the value converter:

IndexConverter : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
    Row row = value as Row;
    string index = parameter as string;
    return row[index];
  }
 
  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  {
    throw new NotImplementedException();
  }
}

In the above XAML, the binding expression does not have a Path specified, therefore, the binding source is our Row instance rather than a property of the Row. The RowIndexConverter value converter simply uses the supplied ConverterParameter for each column to access the Row indexer and extract the correct value. This works quite nicely, and we are able to see our data within the grid:

 

However, there is one problem here, if you click on the column headers the grid does not sort the data. The columns of a DataGrid have CanUserSort and SortMemberPath properties, however setting these will not help because the DataGrid will expect the bound object to have a property with the given name which of course it does not! To solve this problem (without resorting to dynamically generated types) we need to delve a little deeper into the way in which the DataGrid binds to the data.

A lot of insight can be gained from the documentation of ICollectionView:

The DataGrid control uses this interface to access the indicated functionality in the data source assigned to its ItemsSource property. If the ItemsSource implements IList, but does not implement ICollectionView, the DataGrid wraps the ItemsSource in an internal ICollectionView implementation.

The ICollectionView interface is responsible for filtering, sorting and grouping of the data bound to the DataGrid. WPF also has the ICollectionViewinterface, you can read a good overview of its features on Marlon Grech’s blog. However, whereas WPF wraps the DataContext itself in a view which is shared across multiple controls, it would appear that Silverlight restricts its usage to the DataGrid. This causes problems if, for example, you want to synchronize the current item between controls (However, Laurent Bugnion has a novel solution for emulating this behaviour).

So, it is the ICollectionView which is responsible for sorting our data. The internal implementation of this interface which the DataGrid creates will expects our object to expose the bound properties, which explains why it does not work. However, if we supply our own ICollectionView interface, we can take control of sorting and implement it ourselves, accessing our ‘property’ values via the Row’s string indexer.

The ICollectionView interface has a lot of methods, events and properties, fortunately I was able to find a suitable implementation of this interface on Manish Dalal’s blog. He had implemented this interface, on a class which extends ObservableCollection, in order to bind a DataGrid to a collection where the data from the server is being paged, hence sorting must be done server side. This gives us pretty much everything we need here, a collection class which is able to manage sorting itself. The only change required is to the ICollectiomView.Refresh() method which is responsible for refreshing the view after the SortDescriptions have changed.

The implementation of this method is as follows:

public class SortableCollectionView : ObservableCollection<Row>, ICollectionView
{
 
  ...
  public void Refresh()
  {
      IEnumerable<Row> rows = this;
      IOrderedEnumerable<Row> orderedRows = null;
 
      // use the OrderBy and ThenBy LINQ extension methods to
      // sort our data
      bool firstSort = true;
      for (int sortIndex = 0; sortIndex < _sort.Count; sortIndex++)
      {
          SortDescription sort = _sort[sortIndex];
          Func<Row, object> function = row => row[sort.PropertyName];
          if (firstSort)
          {
              orderedRows = sort.Direction == ListSortDirection.Ascending ?
                  rows.OrderBy(function) : rows.OrderByDescending(function);
 
              firstSort = false;
          }
          else
          {
              orderedRows = sort.Direction == ListSortDirection.Ascending ?
                  orderedRows.ThenBy(function) : orderedRows.ThenByDescending(function);
          }
      }
 
      _suppressCollectionChanged = true;
 
      // re-order this collection based on the result if the above
      int index = 0;
      foreach (var row in orderedRows)
      {
          this[index++] = row;
      }
 
      _suppressCollectionChanged = false;
 
      // raise the required notification
      this.OnCollectionChanged(
          new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
   }
 
  ...
}

When the user clicks on a grid view header, it modifies the bounds ICollectionView.SortDescription to reflect this change in state. The implementation of ICollectiomView from Manish’s blog invokes the Refresh method whenever the ICollectionView.SortDescription collection changes. In the above implementation we use the OrderBy and ThenBy LINQ extension methods to order the data. Interestingly, OrderBy is a method on IEnumerable, whereas ThenBy is a methods on the IOrderedEnumerable, hence the funny looking logic involving ‘firstSort’. Once the sorting has been performed, the underlying collection is re-ordered to match. The only subtle part is that we use a boolean field, _suppressCollectionChanged to suppress the numerous CollectionChanged events that would be fired by ObservableCollection during this process. Finally we raise NotifyCollectionChanged, resulting in the DataGrid updating to reflect the sort order.

Putting it all together, we simply populate our SortableCollectionView and bind this to the DataGrid, modifying the XAML to explicitly inform the grid that it can sort and which property each column sorts on (usually this is inferred from the Binding Path):


    <data:DataGrid.Columns>
        <data:DataGridTextColumn Header="Forename" CanUserSort="True" SortMemberPath="Forename" 
                                  Binding="{Binding Converter={StaticResource RowIndexConverter},
                                    ConverterParameter=Forename}"/>
        <data:DataGridTextColumn Header="Surname" CanUserSort="True" SortMemberPath="Surname" 
                                 Binding="{Binding Converter={StaticResource RowIndexConverter},
                                    ConverterParameter=Surname}"/>
        <data:DataGridTextColumn Header="Age" CanUserSort="True" SortMemberPath="Age" 
                                 Binding="{Binding Converter={StaticResource RowIndexConverter},
                                    ConverterParameter=Age}"/>
        <data:DataGridTextColumn Header="Shoesize" CanUserSort="True" SortMemberPath="Shoesize" 
                                 Binding="{Binding Converter={StaticResource RowIndexConverter},
                                    ConverterParameter=Shoesize}"/>                
    </data:DataGrid.Columns>
</data:DataGrid>

You can download the complete solution here: silverlightdynamicbinding.zip.

Regards,
Colin E.

Editable data and INotifyPropertyChanged

In my previous blog post I described a method for solving the commonly faced problem of binding a Silverlight DataGrid to dynamic data, the form of which is not know at compile time. This blog post extends on the method previously described, adding change notification, allowing the DataGrid to synchronise the UI with changes to the bound data and to allow the user to edit the DataGrid’s contents.

To briefly recap my previous post, the dynamic data is copied to a collection of Rows:


{
  private Dictionary<string, object> _data = new Dictionary<string, object>();
 
  public object this [string index]
  {
    get { return _data[index]; }
    set { _data[index] = value; }
  }
}

Where the row ‘properties’ which are implemented via the string indexer bound to the grid via a ValueConverter. The XAML for the DataGrid looks like the following:


    <data:DataGrid.Columns>
        <data:DataGridTextColumn Header="Forename"
                                 Binding="{Binding Converter={StaticResource RowIndexConverter},
                                    ConverterParameter=Forename}"/>
        <data:DataGridTextColumn Header="Surname" 
                                 Binding="{Binding Converter={StaticResource RowIndexConverter},
                                    ConverterParameter=Surname}"/>
        ...               
    </data:DataGrid.Columns>
</data:DataGrid>

Where the ValueConverter takes the ConverterParameter and uses it to index the bound Row instance:

IndexConverter : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
    Row row = value as Row;
    string index = parameter as string;
    return row[index];
  } 
}

It is important to note that the Binding does not have a Path defined. As a result, the binding source is the Row instance itself rather than a property of the Row as would more normally be the case when binding data. The rest of the post described how to create a collection view which sorts the collection via the Row’s string indexer. You can read about this in full in my previous blog post.

Now, one question a few people have asked me is how to implement INotifyPropertyChanged on the Row class in order to make the DataGrid editable. Simply implementing INotifyPropertyChanged on the Row class, with events raised from the index setter, will not make the grid editable or synchronised with the bound data. The underlying problem here is that the Binding does not have a Path specified, therefore, the binding framework does not know which property name to look out for when handling PropertyChanged events. Interestingly I would expect the above bindings to be updated if a PropertyChanged event were raised with a null or empty PropertyName string, however they do not.

So if the Binding framework needs a Path for the binding in order for change notification to work, we will have to fabricate one for this purpose! We can modify the Row class as follows, adding a Data property which is simply a reference to the Row instance itself:

 : INotifyPropertyChanged
{
    private Dictionary<string, object> _data = new Dictionary<string, object>();
 
    public object this [string index]
    {
        get
        {
            return _data[index];
        }
        set
        {
            _data[index] = value;
 
            OnPropertyChanged("Data");
        }
    }
 
    public object Data
    {
        get
        {
            return this;
        }
    }
 
    public event PropertyChangedEventHandler PropertyChanged;
 
    protected void OnPropertyChanged(string property)
    {
        if (PropertyChanged!=null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }
}

In order to bind this class we modify our bindings so that the source Path points to the Data property:


...
<data:DataGridTextColumn Header="Forename"
                           Binding="{Binding Path=Data, Converter={StaticResource RowIndexConverter},
                           ConverterParameter=Forename}"/>
<data:DataGridTextColumn Header="Forename"
                           Binding="{Binding Path=Data, Converter={StaticResource RowIndexConverter},
                           ConverterParameter=Surname}"/>
...

With this change in place, all of our bindings are bound to the Row’s Data property. Again, the Row’s indexer is being accessed via our value converter. However, if any ‘property’ change via the string indexer setter will result in all of our bindings being updated for that Row. I know, it is not the most efficient solution – but at least it work!

The next problem is making the DataGrid editable. The problem we face here is that all of our bindings are to the same property, Data. When a cell is edited, the binding framework will quite happily call the Data properties setter, with the updated value. However, how do we know which property of the Row was really being set? The only place we hold this information is in the ConverterParameter which is fed to our RowIndexConverter value converter. Fortunately, there is a solution to this problem, when the binding framework updates the bound object, it will first invoke the ConvertBack method on our value converter. This gives us the opportunity to ‘capture’ the converter parameter and feed it into the setter of our Data property, as shown below:

IndexConverter : IValueConverter
{
    ...
 
    public object ConvertBack(object value, Type targetType,
        object parameter, CultureInfo culture)
    {
        return new PropertyValueChange(parameter as string, value);
    }
}

Where PropertyValueChanged is a simple value object:

public class PropertyValueChange
{
    private string _propertyName;
    private object _value;
 
    public object Value
    {
        get { return _value; }
    }
 
    public string PropertyName
    {
        get { return _propertyName; }
    }
 
    public PropertyValueChange(string propertyName, object value)
    {
        _propertyName = propertyName;
        _value = value;
    }
}

The setter for the Data property will now be invoked with an instance of PropertyValueChanged, giving it all the information it requires to invoke the string indexer with the correct ‘property’ name:

 : INotifyPropertyChanged
{
    ...
 
    public object Data
    {
        get { return this; }
        set
        {
            PropertyValueChange setter = value as PropertyValueChange;
            _data[setter.PropertyName] = setter.Value;
        }
    }
    ...
}

With this change in place, we the user is able to edit the data bound to the DataGrid, and the binding ensures that the DataGrid UI is synchronised with any changes to the bound data that may happen elsewhere in our code.

One final thought that struck me is that this technique uses the Binding’s associated ValueConverter to access the Row’s string indexer, but what if we wanted to use a ValueConverter for something else like formatting? Fortunately it is a pretty straightforward process to nest one value converter inside another. For details, see the attached project. For now, have quick play with the grid below, where all the columns are editable and the button click event sets the Row’s Age property demonstrating how the DataGrid is synchronised:

private void Button_Click(object sender, RoutedEventArgs e)
{
    // demonstrate the property changes from code-behind still work.
    Random rand = new Random();
    foreach (var row in _rows)
    {
        row["Age"] = rand.Next(10) + 5;
    }
}

Also, the Age column has a value converter associated …

So there you have it, binding to dynamic data with INotifyPropertyChanged implemented and an editable DataGrid. The only reservation I have with this technique is that I am really bending the binding framework to achieve the results, having a Data property which expects different types for the getter and setter is a little ugly … use with caution!

You can download the project sourcecode: silverlightdynamicbinding2.zip