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:
And in code-behind:
So what is wrong with the above? There are a couple of problems:
- 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.
- 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:
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:
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.
The property changed handler shown above is extended to remove this phantom:
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:
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:
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:
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.
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.