Data binding is awesome. Sometimes we take it for granted that a list or grid control can, with only a few lines of code, both visualise and manipulate a collection. It’s a principle that supports the separation of model and presenter, not to mention making the task of developing a GUI a real breeze. More than that, though; when you understand the underlying principles behind data binding, it becomes so much cooler. In a previous article, I gave some background on how data binding accesses properties (which, as I explained, might not be actual property members; they could be logical, like columns in a DataTable). PropertyDescriptor is the magic class in all this, defining the ‘property’, naming it and providing a mechanism to get/set its value. The properties exposed by a data source (either by reflection or by ITypedList) can be used for the DisplayMember/ValueMember properties (of ListBox and ComboBox) and as columns in a DataGridView.
That’s all fine and dandy, but what if the property we want to bind to doesn’t belong directly to the list item? What if that property belongs to an object that the list item contains? For example, consider the following situation:
A Person has a Name and a HairStyle. In UML, we’d say that Person “has a” HairStyle (or, HairStyle is aggregated by Person). A HairStyle, in turn, has a Colour and a Length. Say we’re binding a List<Person> to a DataGridView control. How can we bind one column to the Person‘s Name property and another to the Person‘s HairStyle‘s Colour property?
One column’s DataPropertyName will be set to “Name”. What about the other? Can we have “HairStyle.Colour”? The answer is that, no, with conventional data binding, we can’t. Only properties which belong directly to Person are available for binding. The good news is that, thanks to the aforementioned ITypedList interface (already used in, for example, DataTable) we can solve this problem.
Okay, before we go any further, you might be asking, “Why not transform the data into a flat form first?”. True, we could use LINQ to return a sequence of anonymous types which would contain bindable properties from both classes. There are other approaches too, however all of these destroy the potential two-way relationship that data binding offers. Such a data source would be immutable, hence it would be read-only in the DataGridView control. Without editing capabilities, the power of the grid control is significantly diminished. What if we want the user to be able to view and edit the person’s name and hair colour in the same place? It would be really cool if we could bind to properties on aggregated objects…
Yes, we can (with ITypedList)
The ITypedList interface exposes a method called GetItemProperties(), which returns a collection of PropertyDescriptor objects. By implementing this interface, we can create a collection class which exposes not only the properties on the list items, but also those on objects owned by the list items. In fact, we can use recursion to get the properties on the properties, and the properties on those properties, and so on! We’ll extend the existing List<T> generic class and implement ITypedList:
public class AggregationBindingList<T> : List<T>, ITypedList { public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors) { /* ... */ } IEnumerable<PropertyDescriptor> GetPropertiesRecursive(Type t, PropertyDescriptor parent, Attribute[] attributes, int depth) { /* ... */ } }
We can get the properties on the list items (and the aggregated objects) using the TypeDescriptor class, as follows:
IEnumerable<PropertyDescriptor> GetPropertiesRecursive(Type t, PropertyDescriptor parent, Attribute[] attributes, int depth) { if (depth >= MAX_RECURSION) yield break; foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(t, attributes)) { yield return property; foreach (PropertyDescriptor aggregated in GetPropertiesRecursive(property.PropertyType, parent, attributes, depth+1)) { yield return property; } } }
(I included a cap on the depth of recursion; if an class has a property of its own type, walking through all the aggregated properties would result in an infinite loop.)
See anything wrong with the code above? On the face of it, it looks sound, however one must recall how PropertyDescriptor is accessed by data binding; namely, it will pass an instance of the list item to the descriptor’s GetValue/SetValue method. A PropertyDescriptor for a property on an aggregated object will expect an instance of the aggregated object, not the list item! The other problem presented is that we have no way of uniquely identifying each property, making it impossible to relate the properties to the properties that own them.
A PropertyDescriptor for properties on aggregated objects
We can solve both of the aforementioned problems by creating our own class which derives from PropertyDescriptor. Essentially, we need to wrap the property descriptor supplied to us by TypeDescriptor.GetProperties() and hold a reference to the property which owns it; this property may, in turn, be owned by another property, and so on (due to the use of recursion). When data binding calls GetValue or SetValue, supplying an instance of the list item, we’ll call the corresponding method on the owning property first, in order to return an instance of the aggregated object. We can then call the wrapped property’s method to get or set the value on this object:
public override object GetValue(object component) { return AggregatedProperty.GetValue(OwningProperty.GetValue(component)); }
In the constructor for our AggregatedPropertyDescriptor class, we collect the inner and outer properties and set the name of the aggregated property appropriately:
Note: We cannot use the dot (.) symbol to delimit aggregation (e.g. “HairStyle.Colour”) because the ComboBox control will truncate the string when it encounters that character. Instead, i’ve opted to use the C++ pointer-to-member symbol (->).
public AggregatedPropertyDescriptor(PropertyDescriptor owner, PropertyDescriptor aggregated, Attribute[] attributes) : base(owner.Name + "->" + aggregated.Name, attributes) { OwningProperty = owner; AggregatedProperty = aggregated; }
Completing the GetItemProperties method
Now that we have a mechanism to correctly handle properties on aggregated objects, we can finish the GetItemProperties() method on our AggregationBindingList<T> class:
IEnumerable<PropertyDescriptor> GetPropertiesRecursive(Type t, PropertyDescriptor parent, Attribute[] attributes, int depth) { if (depth >= MAX_RECURSION) yield break; foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(t, attributes)) { if (parent == null) { // property belongs to root type, return as-is yield return property; } else { // property is on an aggregated object, wrap and return yield return new AggregatedPropertyDescriptor(parent, property, attributes); } foreach (PropertyDescriptor aggregated in GetPropertiesRecursive(property.PropertyType, parent, attributes, depth+1)) { yield return new AggregatedPropertyDescriptor(property, aggregated, attributes); } } }
And there you have it; when data binding enumerates the properties on the list items, it will see:
- Name
- HairStyle
- HairStyle->Colour
- HairStyle->Length
Furthermore, we will be able to get and set values on all of these properties.
Example usage
A more complex example using a self-referencing Person class:
public class Person { public string Name { get; set; } public int Age { get; set; } public Person Father { get; set; } public Person Mother { get; set; } }
Assuming a Form with a DataGridView control on it:
dataGridView.AutoGenerateColumns = false; dataGridView.Columns.Add("Name", "Person"); dataGridView.Columns["Name"].DataPropertyName = "Name"; dataGridView.Columns.Add("Age", "Age"); dataGridView.Columns["Age"].DataPropertyName = "Age"; dataGridView.Columns.Add("FatherName", "Father's name"); dataGridView.Columns["FatherName"].DataPropertyName = "Father->Name"; dataGridView.Columns.Add("MotherName", "Mother's name"); dataGridView.Columns["MotherName"].DataPropertyName = "Mother->Name"; dataGridView.Columns.Add("GrandfatherName", "Grandfather's name"); dataGridView.Columns["GrandfatherName"].DataPropertyName = "Father->Father->Name"; AggregationBindingList<Person> people = new AggregationBindingList<Person>(); Person harry = new Person { Name = "Harry", Age = 75, Father = null, Mother = null }; Person frank = new Person { Name = "Frank", Age = 65, Father = null, Mother = null }; Person angela = new Person { Name = "Angela", Age = 68, Father = null, Mother = null }; Person bob = new Person { Name = "Bob", Age = 35, Father = frank, Mother = angela }; Person fred = new Person { Name = "Fred", Age = 32, Father = harry, Mother = angela }; Person mary = new Person { Name = "Mary", Age = 36, Father = null, Mother = null }; Person jim = new Person { Name = "Jim", Age = 5, Father = bob, Mother = mary }; people.AddRange(new Person[] { bob, fred, mary, jim }); dataGridView.DataSource = people;
Thanks for the Binding to Aggregated Objects article. It was precisely what I was looking for and two days of online searches basically said it wasn’t doable.
Great article! Especially for me – DataGridView newb.
I myself just changed it to inherit from BindingList instead of List in order for DataGridView to get notifications when items are added/removed.
Great article. I’d be interested in seeing Julius’s BindingList example..
I did find a minor bug when having properties that are of “Type”. In which case it goes into an endless loop and you run out of memory! 🙂
Easy fix:
IEnumerable GetPropertiesRecursive(Type t, PropertyDescriptor parent, Attribute[] attributes, int depth) {
// self-referencing properties can cause infinite recursion, so place a cap on the depth of recursion
if (depth >= MAX_RECURSION) yield break;
// get the properties of the current Type
foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(t, attributes)) {
//Don’t iterate over properties that Type
if (property.PropertyType != typeof(Type))
{
if (parent == null)
{
// property belongs to root type, return as-is
yield return property;
}
else
{
// property is on an aggregated object, wrap and return
yield return new AggregatedPropertyDescriptor(parent, property, attributes);
}
// repeat for all properties belonging to this property
foreach (PropertyDescriptor aggregated in GetPropertiesRecursive(property.PropertyType, parent, attributes, depth + 1))
{
yield return new AggregatedPropertyDescriptor(property, aggregated, attributes);
}
}
}
}
Ah, very well spotted. Thank you 🙂
Do you have a version of the code w/out yield statement? I have VS2010 and it’s not supported in vb.net.
In general terms, you can rewrite any method in the form:
With this equivalent form: