Colin Eberhardt's Adventures in .NET

Using a Grid as the Panel for an ItemsControl

November 15th, 2010

In this blog post I look at how to use a Grid as the ItemsPanel for an ItemsControl, solving a few of the issues that crop up along the way.

The Grid is probably the most useful of Silverlight and WPF’s panels (panels are elements which provide a mechanism for laying out their children). The ItemsControl provides a mechanism for generating multiple instances of some template based on the data that is bound to it. The ItemsControl ‘concept’ is highlight versatile, and is used as the foundation for many of the framework controls, I find myself using it all the time.

Isn’t it a shame that Grid and ItemsControl don’t play together nicely?

So what do I mean by this? I am sure that if you have ever tried to generate Grid a layout using an ItemsControl you will know what I am talking about, but if not, here’s a very brief summary of the problem. An ItemsControl manages a number of child elements, you can configure the type of panel the ItemsControl adds children to via its ItemsPanel property. If I have a list of strings which I want to render using a Grid, I might expect the following to work … in XAML:

<ItemsControl ItemsSource="{Binding}">
  <!-- host the items generated by this ItemsControl in a grid -->
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <Grid/>
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>
  <!-- render each bound item using a TextBlock-->
  <ItemsControl.ItemTemplate>
    <DataTemplate>
      <TextBlock Text="{Binding}"/>
    </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>

And in code-behind:

this.DataContext = new string[]
  { "one", "two", "three", "four", "five"  };

So what is wrong with the above? There are a couple of problems:

  1. When using a Grid you must add the required number of rows / columns via the RowDefinitions ColumnDefinitions properties. In the above example what we really want is for the grid rows to be added dynamically based on the number of items in the bound data. Unfortunately this is not supported either by the Grid or the ItemsControl.
  2. The second problem is that items within a grid must declare the row / column that they occupy via the Grid.Row and Grid.Column attached properties. You might think that you could add a Grid.Row property to the TextBlock in the above example, binding it to some other property of the bound collection. However, this will not work because the TextBlock elements are not added into the grid directly; each one is added as the content of a ContentPresenter which is generated for us.

The above two problems mean that it is not possible to use a Grid as the panel within an ItemsControl. In this blog post I will show a solution to these problems …

Dynamically adding RowDefinitions

The first problem to overcome is how to dynamically add the required number of RowDefinitions to our Grid. To support this I added an attached property ItemsPerRow, which upon attachment handles the LayoutUpdated event. This event is fired when items are added to the grid, this enables us to compute the number of RowDefinitions that are required, and also to assign a row index to each of the Grid’s children. The following code is the property changed handler for the attached ItemsPerRow property:

/// <summary>
/// Handles property changed event for the ItemsPerRow property, constructing
/// the required ItemsPerRow elements on the grid which this property is attached to.
/// </summary>
private static void OnItemsPerRowPropertyChanged(DependencyObject d,
                    DependencyPropertyChangedEventArgs e)
{
  Grid grid = d as Grid;
  int itemsPerRow = (int)e.NewValue;
 
  // construct the required row definitions
  grid.LayoutUpdated += (s, e2) =>
    {
      var childCount = grid.Children.Count;
 
      // add the required number of row definitions
      int rowsToAdd = (childCount - grid.RowDefinitions.Count) / itemsPerRow;
      for (int row = 0; row < rowsToAdd; row++)
      {
        grid.RowDefinitions.Add(new RowDefinition());
      }
 
      // set the row property for each chid
      for (int i = 0; i < childCount; i++)
      {
        var child = grid.Children[i] as FrameworkElement;
        Grid.SetRow(child, i / itemsPerRow);
      }
    };
}

Note the use of a lambda expression for the event handler in order to ‘capture’ a reference to the Grid (as described in my previous blog post on binding the ScrollViewers offset properties).

Changing our markup to use the above attached property:

<ItemsControl ItemsSource="{Binding}">  
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <!-- use the ItemsPerRow attached property to dynamically add rows -->
      <Grid local:GridUtils.ItemsPerRow="1"
          ShowGridLines="True"/>
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>
  <ItemsControl.ItemTemplate>
    <DataTemplate>
      <TextBlock Text="{Binding}"/>
    </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>

The array of strings bound in code-behind results in the following grid layout being rendered:

Great, all fixed. Time to grab a coffee.

Unfortunately it isn’t that simple. The above example has a single item per grid row, typically you would want to have multiple items in each row, with each item positioned in a different column. This is where it gets tricky. DataTemplate only supports a single child element, so we will have to wrap the elements which we want to add into our grid in a panel of some sort. However, if we do this, the column index that we apply to the element of each row will be ignored because the Grid only honours the Row and Column properties of its direct descendants.

Creating a Row Template

In order to circumnavigate this issue, I have created a ‘phantom’ panel, which hosts the elements that are added to each grid row. When a phantom panel is added to the grid, its children are removed and added to the grid and the phantom is destroyed.

public class PhantomPanel : Panel
{
 
}

The property changed handler shown above is extended to remove this phantom:

private static void OnItemsPerRowPropertyChanged(DependencyObject d,
                    DependencyPropertyChangedEventArgs e)
{
  Grid grid = d as Grid;
  int itemsPerRow = (int)e.NewValue;
 
  // construct the required row definitions
  grid.LayoutUpdated += (s, e2) =>
    {
      // iterate over any new content presenters (i.e. instances of our DataTemplate)
      // that have been added to the grid
      var presenters = grid.Children.OfType<ContentPresenter>().ToList();
      foreach (var presenter in presenters)
      {
        // the child of each DataTemplate should be our 'phantom' panel
        var phantom = VisualTreeHelper.GetChild(presenter, 0) as PhantomPanel;
        if (phantom != null)
        {
 
          // remove each of the children of the phantom and add to the grid
          foreach (FrameworkElement child in phantom.Children.ToList())
          {
            phantom.Children.Remove(child);
            grid.Children.Add(child);
            // ensure the child maintains its original datacontext
            child.DataContext = phantom.DataContext;
          }
 
          // remove the presenter (and phantom)
          grid.Children.Remove(presenter);
        }
      }
 
      var childCount = grid.Children.Count;
      int rowDifference = (childCount / itemsPerRow) - grid.RowDefinitions.Count;
 
      // if new items have been added, create the required grid rows
      // and assign the row index to each child
      if (rowDifference != 0)
      {
        grid.RowDefinitions.Clear();
        for (int row = 0; row < (childCount / itemsPerRow); row++)
        {
          grid.RowDefinitions.Add(new RowDefinition());
        }
 
        // set the row property for each chid
        for (int i = 0; i < childCount; i++)
        {
          var child = grid.Children[i] as FrameworkElement;
          Grid.SetRow(child, i / itemsPerRow);
        }
      }
    };
}

The above code is pretty straightforward, however there is one subtlety, we must ensure that the DataContext of each child element is set to the DataContext that the ItemsControl assigned to each PhantomPanel, i.e. the individual items that are bound to the Grid.

With the above code it is now possible to create a more complex grid layout:

<StackPanel Orientation="Vertical">
 
  <sdk:DataGrid ItemsSource="{Binding}" Margin="10"/>
 
  <Border x:Name="LayoutRoot" Background="White"
        BorderBrush="Black" BorderThickness="1" Margin="10">
 
    <ItemsControl ItemsSource="{Binding}">
      <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
          <Grid local:GridUtils.ItemsPerRow="2">
            <Grid.ColumnDefinitions>
              <ColumnDefinition/>
              <ColumnDefinition/>
            </Grid.ColumnDefinitions>
          </Grid>
        </ItemsPanelTemplate>
      </ItemsControl.ItemsPanel>
      <ItemsControl.ItemTemplate>
        <DataTemplate>
          <!-- the row ‘template’ within a phantom panel -->
          <local:PhantomPanel>
            <TextBlock Text="{Binding Path=Item}"/>
            <TextBlock Text="{Binding Path=Quantity}" Grid.Column="1"/>
          </local:PhantomPanel>
        </DataTemplate>
      </ItemsControl.ItemTemplate>
    </ItemsControl>
 
  </Border>
 
</StackPanel>

The above XAML renders a list of simple model objects that implement INotifyPropertyChanged in a DataGrid and an ItemsControl (using our Grid layout). Notice that if you edit the properties of the bound objects via the DataGrid, these changes are reflected in the Grid below, i.e. it is all working!

Handling CollectionChanged Events

The above example looks pretty complete, however there is one remaining issue, handling changes to the bound collection. The problem with the above solution is that it effectively subverts the ItemsControls usage of the ItemsPanel. The ItemsControl will expect that items that it adds to the Panel to appear at certain indices, by removing our phantom panels and adding their contente directly this is no longer the case.

A simple hack (and yes, it really is a hack!) is to confuse the ItemsControl into thinking that any change to our bound collection is a reset, i.e. the collection has changed completely, so the UI needs to be rebuilt from scratch. To do this, I have created an attached ItemsSource property that adapts the bound collection, ensuring that any collection changes result in the ItemsControl rebuilding its UI. The changed handler for this attached property is given below:

/// <summary>
/// Handles property changed event for the ItemsSource property.
/// </summary>
private static void OnItemsSourcePropertyChanged(DependencyObject d,
  DependencyPropertyChangedEventArgs e)
{
  ItemsControl control = d as ItemsControl;
 
  // set the ItemsSource of the ItemsControl that this property is attached to
  control.ItemsSource = e.NewValue as IEnumerable;
 
  INotifyCollectionChanged notifyCollection = e.NewValue as INotifyCollectionChanged;
  if (notifyCollection != null)
  {
    // if a collection changed event occurs, reset the ItemsControl's
    // ItemsSource, rebuilding the UI 
    notifyCollection.CollectionChanged += (s, e2) =>
      {
        control.ItemsSource = null;
        control.ItemsSource = e.NewValue as IEnumerable;
      };
  }
}

The above attached property is used in exactly the same way as the ItemsControl.ItemsSource property which it adapts. The XAML below shows how the above attached property can be used in place of the ItemsControl property:

<ItemsControl local:GridUtils.ItemsSource="{Binding}">
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <Grid local:GridUtils.ItemsPerRow="3">
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="2*"/>
          <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
      </Grid>
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>
  <ItemsControl.ItemTemplate>
    <DataTemplate>
      <local:PhantomPanel>
        <TextBlock Text="{Binding Path=Item}"/>
        <TextBlock Text="{Binding Path=Quantity}" Grid.Column="1"/>
        <Line Stroke="LightGray" StrokeThickness="1"
              VerticalAlignment="Bottom"
              X1="0" X2="1" Y1="0" Y2="0"
              Stretch="Fill"
              Grid.ColumnSpan="2"/>
      </local:PhantomPanel>
    </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>

And here it is with a DataGrid bound to the same data:

You can add, remove and update the bound objects and the Grid rendered via the ItemsControl remains synchronised with the bound data.

Conclusions

Finally, the ItemsControl and Grid can be used together, a cause for much rejoicing! I must admit, I am pretty pleased with my solution; however, it is far from perfect. There are a couple of areas of this implementation that you should be wary of.

Firstly, the use of the LayoutUpdated event which we use to determine when items are added to our Grid. This event gets fired when anything changes in our visual tree’s layout. It has the potential to be called a lot, which is why I am always careful to check conditions and exit this method as quickly as possible if the UI is not in the state I am interested in.

Secondly, adapting the ItemsSource to force a Reset whenever an items is added or removed is pretty costly!

If you do not use the technique for rendering large quantities of data it will probably be just fine. It may be fine for large volumes of data also, however, it is always better to be aware of potential issue.

If anyone has any ideas on how to improve on this technique, please leave a comment below:

You can download the full project sourcecode: ItemsControlGrid.zip

Regards, Colin E.

Silverlight MultiBinding updated, adding support for ElementName and TwoWay binding

August 12th, 2010

This blog post describes an update to the Silverlight 4 MultiBinding technique I blogged about a couple of months ago to add support for ElementName binding and TwoWay binding.

A few months ago I posted an update to my MultiBinding solution for Silverlight 4. This technique allows you to perform the same kind of multibindings which are possible in WPF, where a property value is bound to multiple sources via a special value converter that implements the IMultiValueConverter interface, which describes how these values are combined. This update proved popular once again, and I received a few requests to add support for ElementName and TwoWay binding. I like challenge! This blog post describes how these two features were implemented, but if you just want to grab the code, you will find the link at the end of this article.

A brief recap

Before I go into the implementation details for the new features I will provide a brief recap of how my multibinding solution works. The XAML for creating a multibinding looks like the following:

<TextBlock Foreground="White" 
  <local:BindingUtil.MultiBindings>
    <local:MultiBindings>
      <local:MultiBinding TargetProperty="Text"
                          Converter="{StaticResource TitleConverter}">
        <local:BindingCollection>
          <Binding Path="Surname"/>
          <Binding Path="Forename"/>
        </local:BindingCollection>
      </local:MultiBinding>
      <local:MultiBinding TargetProperty="ToolTipService.ToolTip"
                          Converter="{StaticResource TitleConverter}">
        <local:BindingCollection>
          <Binding Path="Surname"/>
          <Binding Path="Forename"/>
        </local:BindingCollection>
      </local:MultiBinding>
    </local:MultiBindings>
  </local:BindingUtil.MultiBindings>
</TextBlock>

Here the Text property and attached ToolTip property of the TextBlock are bound to both the Surname and Forename property of the business object which is set as the DataContext of our view.

The value converter is as follows:

public class TitleConverter : IMultiValueConverter
{
 
  public object Convert(object[] values, Type targetType,
    object parameter, CultureInfo culture)
  {
    string forename = values[0] as string;
    string surname = values[1] as string;
 
    return string.Format("{0}, {1}", surname, forename);
  }
 
  ...
}

In the example below you can see that as you change the forename or surname, the title text and tooltip are updated by the multibinding.

So how does this work?

When a multibinding is created and added to an element via the BindingUtil.MultiBindings attached property, the MultiBinding instance is added to a virtual-branch, this is a branch of the visual tree that obtains the DataContext of the element to which it is bound, but is not added to the visual tree itself. The MultiBinding then creates a BindingSlave instance for each of the given Bindings. These ‘slave’ elements inherit the MultiBinding DataContext and are used to evaluate each of the individual Bindings. The MultiBinding instance aggregate the results of each binding with the IMultiValueConverter used to compute the ConvertedValue property which is bound to the target property on the element to which this MultiBinding as attached.

ElementName binding

So why does the above solution not work if one of the multibindings uses ElementName to locate the source rather than using the inherited DataContext? The problem here is that the MultiBinding and its BindingSlave instances are located in a virtual branch and are therefore not in the same namescope as the target element. As a result, they cannot perform look up of named elements.

In order to solve this problem I created a BindingSlave subclass specifically for ElementName binding. This slave locates the source element named via the ElementName property and sets it as the Source of the binding it uses to compute its Value property.

The only minor complication I encountered is that the MultiBinding might be constructed before the element referenced via ElementName. For this reason the binding slave must handle LayoutUpdated events to ensure that the named element is located if it is constructed after the multibinding.

public class ElementNameBindingSlave : BindingSlave
{
  private FrameworkElement _multiBindingTarget;
 
  /// <summary>
  /// The source element named in the ElementName binding
  /// </summary>
  private FrameworkElement _elementNameSource;
 
  private Binding _binding;
 
  public ElementNameBindingSlave(FrameworkElement target, Binding binding)
  {
    _multiBindingTarget = target;
    _binding = binding;
 
    // try to locate the named element
    ResolveElementNameBinding();
 
    _multiBindingTarget.LayoutUpdated += MultiBindingTarget_LayoutUpdated;
  }
 
  /// <summary>
  /// Try to locate the named element. If the element can be located, create the required
  /// binding.
  /// </summary>
  private void ResolveElementNameBinding()
  {
    _elementNameSource = _multiBindingTarget.FindName(_binding.ElementName) as FrameworkElement;
    if (_elementNameSource != null)
    {
      SetBinding(BindingSlave.ValueProperty, new Binding()
      {
        Source = _elementNameSource,
        Path = _binding.Path,
        Converter = _binding.Converter,
        ConverterParameter = _binding.ConverterParameter
      });
    }
  }
 
  private void MultiBindingTarget_LayoutUpdated(object sender, EventArgs e)
  {
    // try to locate the named element 
    ResolveElementNameBinding();
  }
}

The example below demonstrates element name binding by binding the Value of two Slider elements to a TextBlock which displays the sum of the two values.

<TextBlock Grid.Column="1">
  <local:BindingUtil.MultiBindings>
    <local:MultiBindings>
      <local:MultiBinding TargetProperty="Text"
                          Converter="{StaticResource SliderValueConverter}">
        <local:BindingCollection>
          <Binding ElementName="sliderOne" Path="Value"/>
          <Binding ElementName="sliderTwo" Path="Value"/>
        </local:BindingCollection>
      </local:MultiBinding>
    </local:MultiBindings>
  </local:BindingUtil.MultiBindings>
</TextBlock>
 
<Slider x:Name="sliderOne" Value="10"/>
 
<Slider x:Name="sliderTwo"  Value="20"/>

Here is the value converter that is used to compute the sum:

public class SliderValueConverter : IMultiValueConverter
{
 
  public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  {
    if (values.Length != 2 ||
        values[0] == null ||
        values[1] == null)
      return null;
 
    return (double)values[0] + (double)values[1];
  }
 
  ...
}

You could of course use element name binding to combine the surname / forename in the first example in this blog post, but in this context it is more likely that you will want to back your UI with a business object. It is also possible to mix element name and regular (i.e. Source=DataContext) binding.

I find multibindings which use ElementName references most useful when creating complex UI layouts that do not lend themselves to being implemented using Panels. For a good example see my article on the implementation of a BulletGraph control which makes extensive use of this technique in order that the layout of the control is performed entirely within XAML.

TwoWay Binding

A two-way multibinding must be able to handle updates to the combined value and split it up into its constituent parts in order to update the multiple source bindings. In the first example where a forename and surname property are combined into a “surname, forename” string it is possible to convert back the other way, but in the example above where the values of two Sliders are combined a reverse conversion is not possible.

Modifying this multibinding solution to permit two way binding was relatively straightforward. A property changed event handler was added to the MultiBindig.ConverterValue (which is bound to the multi binding target) so that we can determine when changes have been made. This handler then uses the IMultiValueConverter to convert the value into the multiple source properties and updates the binding slaves accordingly:

/// <summary>
/// Handles property changes for the ConvertedValue property
/// </summary>
private void OnConvertedValuePropertyChanged()
{
    OnPropertyChanged("ConvertedValue");
 
    // if the value is being updated, but not due to one of the multibindings
    // then the target property has changed.
    if (!_updatingConvertedValue)
    {
      // convert back
      object[] convertedValues = Converter.ConvertBack(ConvertedValue, null,
          ConverterParameter, CultureInfo.InvariantCulture);
 
      // update all the binding slaves
      if (Children.Count == convertedValues.Length)
      {
        for (int index = 0; index < convertedValues.Length; index++)
        {
          ((BindingSlave)Children[index]).Value = convertedValues[index];
        }
      }
    }
}

In order to use two way multibinding both the multibinding mode and the bindings within the BindingCollection must be set to TwoWay. See the example given below:

<TextBlock Text="Surname:"/>
<TextBox  Grid.Column="1" Text="{Binding Path=Surname, Mode=TwoWay}"/>
 
<TextBlock Grid.Row="1" Text="Forename:"/>
<TextBox Grid.Row="1"  Grid.Column="1" Text="{Binding Path=Forename, Mode=TwoWay}"/>
 
<TextBlock Grid.Row="2" Text="Combined:"/>
<TextBox Grid.Row="2"  Grid.Column="1" >
  <local:BindingUtil.MultiBindings>
    <local:MultiBindings>
      <local:MultiBinding TargetProperty="Text"
                        Converter="{StaticResource TitleConverter}"
                        Mode="TwoWay">
        <local:BindingCollection>
          <Binding Path="Surname" Mode="TwoWay"/>
          <Binding Path="Forename" Mode="TwoWay"/>
        </local:BindingCollection>
      </local:MultiBinding>
    </local:MultiBindings>
  </local:BindingUtil.MultiBindings>
</TextBox>

You can have try of two way multibinding below where the values of the forename and surname text boxes are combined in the box below. However, you ca also make updates to the combined result which will then be converted back into its constituent parts:

And here is the value converter:

public class TitleConverter : IMultiValueConverter
{
  #region IMultiValueConverter Members
 
  public object Convert(object[] values, Type targetType,
    object parameter, System.Globalization.CultureInfo culture)
  {
    string forename = values[0] as string;
    string surname = values[1] as string;
 
    return string.Format("{0}, {1}", surname, forename);
  }
 
  public object[] ConvertBack(object value, Type[] targetTypes,
    object parameter, System.Globalization.CultureInfo culture)
  {
    string source = value as string;
    var pos = source.IndexOf(", ");
 
    string forename = source.Substring(pos + 2);
    string surname = source.Substring(0, pos);
 
    return new object[] { forename, surname };
  }
 
  #endregion
}

So, there you have it, two-way and element name multibinding :-)

You can download the full source, including a WPF build (thanks to Stefan Olson) here: SLMultiBinding.zip

Now … go forth and multibind.

Regards, Colin E.

Silverlight MultiBinding solution for Silverlight 4

May 10th, 2010

In this post I describe an update to the Silverlight MultiBinding solution I presented last year. This update includes support for Silverlight 4, attached properties and multiple bindings on a single object.

UPDATE: I have updated this code to include ElementName and TwoWay binding. Grab the latest copy here.

MultiBinding is a WPF feature that allows you to bind a single property to a number of sources, with the source values being combined by a value converter. This is a feature that is missing from Silverlight. About a year ago I developed a MultiBinding solution for Silverlight, which has proven very popular! I even had an email from an Microsoft Attorney asking if they could use it in the Silverlight Facebook client (How cool is that :-) ). I was also very happy when Stefan Olson made a few updates to this code to allow multiple MultiBindings on a single object and spotted the SL4 issue. This blog post is a quick demonstration of these new features …

The following application is an example of MultiBindings in action:

In the above application, the TextBox at the top of the page is bound to both the surname and forename properties of our data-object via converter that takes the first letter of the forename and the surname. If you edit the surname or forename (hitting enter or changing focus to commit), the title is updated automatically. The title tooltip is bound to the all three object properties via a different value converter that concatenates all three.

The XAML for this binding is show below:

<TextBlock x:Name="Block" Foreground="White" FontSize="13" Margin="5,0,0,0">
    <local:BindingUtil.MultiBindings>
        <local:MultiBindings>
            <local:MultiBinding TargetProperty="Text"
                                Converter="{StaticResource TitleSummaryConverter}">
                <local:MultiBinding.Bindings>
                    <local:BindingCollection>
                        <Binding Path="Surname"/>                            
                        <Binding Path="Forename"/>
                        </local:BindingCollection>
                </local:MultiBinding.Bindings>
            </local:MultiBinding>
            <local:MultiBinding TargetProperty="ToolTipService.ToolTip"
                                Converter="{StaticResource TitleConverter}">
                <local:MultiBinding.Bindings>
                    <local:BindingCollection>
                        <Binding Path="Surname"/>                            
                        <Binding Path="Forename"/>
                        <Binding Path="Age"/>
                    </local:BindingCollection>
                </local:MultiBinding.Bindings>
            </local:MultiBinding>
        </local:MultiBindings>
    </local:BindingUtil.MultiBindings>
</TextBlock>

In the above you can see that our TextBlock has two multibindings, one on Forename and Surname, and the other which includes all three properties. Note, the second multibinding is on the ToolTipService.ToolTip attached property.

You can download the source for Silverlight MultiBinding here: SLMultiBindingUpdate.zip – thanks again to Stefan Olson who added WPF support to this technique.

If you are interested in the technical details of how this works, I refer you to the original article which describes how the code builds a ‘virtual’ branch on the visual tree in order to evaluate your bindings:

I hope this update is of use to SL4 developers. If you have any feedback, please leave a comment below.

You can download the source for Silverlight MultiBinding here: SLMultiBindingUpdate.zip

Regards, Colin E.

Binding a Silverlight 3 DataGrid to dynamic data via IDictionary (Updated)

March 26th, 2010

In this post I demonstrate a method for binding a Silverlight 3 DataGrid to dynamic data, i.e. data which does not have properties that are known at design time. This technique results in a bound grid which is sortable and editable. This blog post is a bug fix (due to differences between SL2 and SL3) and expansion on my previous posts on this subject.

WinForms, WPF and ASP.NET are all perfectly capable of binding to dynamic data via a DataGrid, or custom TypeDescriptors. However Silverlight has neither of these features. Around one year ago I published a pair of articles that demonstrated a technique that could be used to bind a DataGrid to dynamic data presented as a list of dictionaries. The first article detailed how to use a ValueConverter to access the cell values within a dictionary and how a custom CollectionView could be created to permit sorting. The second article showed how to extend this solution to enable editing within the grid.

These two blog posts have proven very popular, with 84 comments between them :-) . However I have seen a recurring theme in the comments to these posts which I will now address:

  1. SL3 Sorting – I have had a number of reports that indicate sorting is broken in Silverlight 3
  2. SL3 Editing – It looks like editing is also broken in SL3 :-(
  3. Adding columns in code behind – My examples configured columns in XAML, but for truly dynamic data this would have to be done in code-behind. A number of readers have had difficulty with converting the XAML into the required C# code.

This blog post will address these specific issues, providing a solution that works for SL3. If you are interested in the technical solution you might want to read the first and second blog posts before you read this one. The solution for SL3 is essentially the same, it is just a few subtle differences in the DataGrid that cause these issues.

Starting with the first of the SL3 problems, sorting. The DataGrid uses the SortDescriptions property of our collection which implements ICollectionView in order to sort the data. This remains unchanged in SL3. However, the ICollectionView implementation that I presented in the previous blog post did not implement all the methods on this interface, leaving out the ones that the SL2 DataGrid does not use.

When the SL3 DataGrid performs a sort or group operation, it first calls the DeferRefresh method on ICollectionView. This is quite a neat little method; what it does is allow you to suppress the events that the collection would typically raise whilst you make a number of changes, for example, applying a sort then a grouping, then raises a single collection changed event. This results in much less work being performed by the UI as it now handles a single event rather than multiple events. You can find a good example of how this works on Matt Manela’s blog. DeferRefresh is implemented by returning in IDisposable object, the implementation is quite trivial:

public class SortableCollectionDeferRefresh : IDisposable
{
    private readonly SortableCollectionView _collectionView;
 
    internal SortableCollectionDeferRefresh(SortableCollectionView collectionView)
    {
        _collectionView = collectionView;
    }
 
    public void Dispose()
    {
        // refresh the collection when disposed.
        _collectionView.Refresh();
    }
}

It is used by our collection as follows:

public class SortableCollectionView : ObservableCollection<Row>, ICollectionView
{
  ...
  public IDisposable DeferRefresh()
  {
      return new SortableCollectionDeferRefresh(this);
  }
  ...
}

That solves the sorting issue :-)

The lack of editing issue was a but odd, someone on the Silverlight forums indicated that they think this is an undocumented breaking change. With a SL3 DataGrid if you bind to a property of type object, the column becomes read-only, even if the DataGrid itself is not read-only. The solution is simply to set the IsReadOnly of each column to false.

With these few changes we now how a fully functioning DataGrid bound to our dynamic data:

The final recurring question to my previous blog posts is how to create the bound DataGrid columns in code-behind. To illustrate how this is done I will create an example where the DataGrid is bound to some XML that sits in a TextBox underneath the grid.

The example looks like this:

The two buttons in the centre allow you to synchronise the DataGrid and the XML, one formats the current grid contents in XML, the other takes the XML and dynamically binds the contents to the grid. You can try editing the data, then updating the XML and vice-versa. You can even add new columns to the XML data (hopefully the XML structure is pretty self explanatory – there is no error checking so take care ;-) ). The DataGrid is of course editable and sortable.

The interesting part of the code is the method that takes the XML contents and binds it to the grid. It is as follows:

/// <summary>
/// Copies the XML contents of the textbox into the DataGrid
/// </summary>
private void XmlToGrid()
{
  // clear the grid
  _dataGrid.ItemsSource = null;
  _dataGrid.Columns.Clear();
 
  // grab the xml into a XDocument
  XDocument xmlDoc = XDocument.Parse(_xmlInput.Text);
 
  // find the columns
  List<string> columnNames = xmlDoc.Descendants("column")
                                   .Attributes("name")
                                   .Select(a => a.Value)
                                   .ToList();
 
  // add them to the grid
  foreach (string columnName in columnNames)
  {
    _dataGrid.Columns.Add(CreateColumn(columnName));
  }
 
  SortableCollectionView data = new SortableCollectionView();
 
  // add the rows
  var rows = xmlDoc.Descendants("row");
  foreach (var row in rows)
  {
    Row rowData = new Row();
    int index = 0;
    var cells = row.Descendants("cell");
    foreach(var cell in cells)
    {
      rowData[columnNames[index]] = cell.Value;
      index++;
    }
    data.Add(rowData);
  }
 
  _dataGrid.ItemsSource = data;
}

The above code clears the grid, then uses a bit of Linq to XML to query the XML within the TextBox, creating the SortableCollectionView and Row instances which are the data objects for our dynamic data as described in the previous blog posts. The columns are created in code behind as follows:

private RowIndexConverter _rowIndexConverter = new RowIndexConverter();
 
private DataGridColumn CreateColumn(string property)
{
  return new DataGridTextColumn()
  {
    CanUserSort = true,
    Header = property,
    SortMemberPath = property,
    IsReadOnly = false,
    Binding = new Binding("Data")
    {
      Converter = _rowIndexConverter,
      ConverterParameter = property
    }
  };
}

This is really no different to the technique that you use when creating the column definitions in XAML. There is nothing special about XAML, it is essentially just an XML markup for creating objects.

Hopefully this blog post will help answer the recurring questions, and reduce the number of “it doesn’t work in SL3″ mails I get. Perhaps I will just get 84 “Thank you” comments instead :-)

You can download the full sourcecode for this blog post: SilverlightTable.zip

Regards, Colin E.