Click here to Skip to main content
15,881,803 members
Articles / Silverlight

Binding a Silverlight DataGrid to dynamic data via IDictionary

Rate me:
Please Sign up or sign in to vote.
4.90/5 (8 votes)
17 May 2009CPOL6 min read 98.7K   18   10
This technical blog post shows how to bind a DataGrid to dynamic data where the properties are not known at design / compile-time.

This post demonstrates a technique for binding a Silverlight DataGrid to dynamic data, the structure of which is not known 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 are. 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:

C#
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:

C#
public class Row
{
  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:

C#
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:

XML
<data:DataGrid Name="_dataGrid" AutoGenerateColumns="False" 
	Height="300" IsReadOnly="False">
  <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:

C#
public class RowIndexConverter : 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:

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 ICollectionView interface, 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 expect 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:

C#
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 method 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):

XML
<data:DataGrid Name="_dataGrid" AutoGenerateColumns="False"
	IsReadOnly="False" Margin="5">
    <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>

And here it is in action:

[See the grid in action on my blog.]

You can download the complete solution from here.

Regards,
Colin E.

This article was originally posted at http://www.scottlogic.co.uk/blog/wpf?p=251

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Scott Logic
United Kingdom United Kingdom
I am CTO at ShinobiControls, a team of iOS developers who are carefully crafting iOS charts, grids and controls for making your applications awesome.

I am a Technical Architect for Visiblox which have developed the world's fastest WPF / Silverlight and WP7 charts.

I am also a Technical Evangelist at Scott Logic, a provider of bespoke financial software and consultancy for the retail and investment banking, stockbroking, asset management and hedge fund communities.

Visit my blog - Colin Eberhardt's Adventures in .NET.

Follow me on Twitter - @ColinEberhardt

-

Comments and Discussions

 
NewsNow works with Silverlight 3 Pin
Colin Eberhardt26-Mar-10 20:03
Colin Eberhardt26-Mar-10 20:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.