Friday, August 14, 2009

How to display items from a collection in one line of a DataGrid

Typically, data structures map closely to their representation in the user interface and so most display widgets work pretty well in this situation. In WPF, DataContexts and bindings expect the relationship between their controls and the bound data to be fairly similar. Recently, I had a weird requirement that caused me trouble getting information on the screen due to a mismatch between the data model and the view.

Requirements

For good reasons, the data needed to be modeled in a master-detail relationship. For equally good reasons, the user wanted this displayed on a single line in a grid control. I believe the model to be correct; I'm not too sure about the user interface. I understand the user's reasons for wanting a single line, but I expect that over time and as new features are added, they will want a change in how things are presented. Regardless of what happens in the future, for today, this is the current requirement that I had to implement.

So, the data model looked something like this: 

And, with the assumption that there will not be more than one instance for any particular descendant type of TestItem in Property3, the goal was to have a screen that looked like this:

The problem

Setting up the data structures was no problem, nor was getting the master's data on the screen. I used a DataGrid from WPFTools and simple binding sufficed for Property1 and Property2. But when it came to PropertyA1 and PropertyB1 I ran into problems. Reading the documentation for binding paths, I found I could use the slash character to indicate items in a collection. Going down this trail, I tried setting the paths for the last two columns to Property3/PropertyA1 and Property3/PropertyB1. This sort of worked:
As shown above, the correct data is displayed, but only for the first item in the list. If the order of things in the list is changed, the data changes appropriately. Through the binding path, I could find no way to select different items from the list for different columns in the grid. Perhaps it can be done and I didn't find how, but understandably, this is an odd thing to try to do and it's not a surprise if the framework doesn't support it. (See CollectionBinding1 in the source code.)

Solution: IValueConverter

I thought about trying to put the data in the cells directly without binding and, while searching how to do this, was reminded of IValueConverter. From the examples, the standard way of using this is to simply convert between types or to control formatting. However, I realized I could create one that takes the entire list and pulls out from it a specific field.

First, I changed the binding for the two problematic columns to be just Property3. Then I created a new converter class for each property, and finally instantiated and assigned them to the columns' bindings. This resulted in a one-to-one relationship between converter classes and each property. The converters iterated the list until an entry of its expected type was found and then returned the field value it knew about. A converter for each property: ugly, but it worked! (See CollectionBinding2 in the source code.)

After proving the concept, I looked to clean it up a bit. The only differences between the classes was the type it was looking for and the property it returned. I realized these two things could be parameterized. I could change the converter to a generic class and then use the generic type in the loop. Also, I could pass a lambda expression to the constructor that would later be used in the loop to get the property value if the given type was found. I made these relatively minor changes, eliminating the multiple classes and making the strategy effective for my real world scenario. (See CollectionBinding3 in the source code.)

Final thoughts

Typically, converters are instantiated and referenced in the XAML code with just the implementation residing on the C# side. This works because the number of types are relatively small. My original implementation would probably work with this usage pattern, however, in the real world situation, with its one-to-one relationship between properties and converter classes, I would have an inordinate number of classes to have to manage. By moving the instantiation into C#, I can use a single class to handle the different types of objects in the list and the multiple properties in each type.

Source code for the three stages of development is available in a zip file here.

Any comments are welcome.
Post a Comment