In newsgroup post in the Developer Express support newsgroup for their XPO product, I was recently asked if it was possible to show information that’s really stored in a one-to-many relationship as additional columns on the main object. Actually, .NET makes this possible using an implementation of the ITypedList
interface together with a custom property descriptor — in this way, arbitrary additional properties on an object can be “simulated”, regardless of the real source of the data. First, let me explain the problem in the sample case (a download link is at the bottom of the post) a little further. Let’s assume you have a class (MasterRecord
) that stores a collection of detail values (DetailValue
) objects in a typed collection. The following diagram shows the structure, just as an overview of the classes involved:
Now, for presentation purposes, the data is to be represented in a grid. But the user doesn’t want to have a long hierarchical structure where all the detail values are listed underneath the corresponding master record. Instead, the detail values are supposed to be represented by additional columns in the grid row of the master record. Or maybe you know exactly which detail values (with which names, in this example) will be there, so you actually know which and how many columns you will need. Consider you have the following data:
- MasterRecord: “TestRecord1”
- DetailValue: Name: “TheAnswer”, Value: 42
- DetailValue: Name: “TestRecord1Value”, Value: 101
- MasterRecord: “Another test record”
- DetailValue: Name: “TheAnswer”, Value: 52
- DetailValue: Name: “another value”, Value: 824
This data should result in a grid view like this:
MasterRecord | TheAnswer | TestRecord1Value | another value |
---|---|---|---|
TestRecord1 | 42 | 101 | |
Another test record | 52 | 824 |
The custom property descriptor
The custom property descriptor is the first step of the implementation. The concept behind property descriptors is simple: Every time a property of an object is accessed by the .NET data binding mechanisms, a property descriptor for that property is used. The property descriptor is the class that has the real knowledge of how exactly the data for the property is to be accessed. This is true for every bound property, even the “normal” ones, for these a ReflectionPropertyDescriptor
is used internally. Now say we have created a property descriptor instance for the property called TheAnswer
from the example. What a property descriptor would have to do to get the value for this “virtual” property from a MasterRecord
instance is this:
- Iterate over the MasterRecord’s detail values collection and see if there’s an entry called “TheAnswer”.
- If such an entry is found, return its value.
- If such an entry is not found, possibly return some kind of placeholder for an “empty value”.
Here’s the code from the sample property descriptor that does just that:
public override object GetValue(object component) {
MasterRecord mr = (MasterRecord) component;
foreach(DetailValue dv in mr.DetailValues)
if (dv.Name == this.Name)
return dv.Value;
return 0;
}
Obviously, the descriptor could get the needed value from any arbitrary source instead of from the detail value collection. For example, I have used a property descriptor in a project that would get values from specific interfaces if the object implemented it, falling back to defaults if it didn’t. If you’ve ever had the problem that a collection typed for a base class would always show only the properties of the base class in data binding, this is the solution: Just create a property descriptor that will check for the real type of the given object and return properties from derived types if necessary. Note, though: as you’ll see in the next paragraph, all objects in a collection are still expected to have the same set of properties (what would the grid look like if that wasn’t a requirement?), so your descriptor must be able to deal with objects that don’t have the expected data.
Implementing ITypedList
The second part of the implementation is the ITypedList
implementation. This is the point where we tell the binding mechanisms which properties it should access for our given collection. It’s an important fact that this interface is implemented on the collection: all objects in the collection are expected to have the same set of properties. The interesting method of that interface is the GetItemProperties
method, which returns a PropertyDescriptorCollection
. In many cases, I’ve found that it makes sense to calculate the set of property descriptors once (or at least at some specific point) and store it in a static variable for the collection type, unless there are reasons to want to recalculate it every time it’s being queried. This is the method from the sample that does the calculation:
public void CalculatePropertyDescriptors() {
PropertyDescriptorCollection origProperties =
TypeDescriptor.GetProperties(typeof(MasterRecord));
ArrayList properties = new ArrayList();
foreach(PropertyDescriptor desc in origProperties)
if (!typeof(ICollection).IsAssignableFrom(desc.PropertyType))
properties.Add(desc);
ArrayList handledNames = new ArrayList();
foreach(MasterRecord mr in this)
foreach(DetailValue dv in mr.DetailValues)
if (!handledNames.Contains(dv.Name)) {
properties.Add(new DetailValuePropertyDescriptor(dv.Name));
handledNames.Add(dv.Name);
}
propertyDescriptors = new PropertyDescriptorCollection((PropertyDescriptor[])
properties.ToArray(typeof(PropertyDescriptor)));
}
There are mainly two things happening here, the whole process blurred a bit by the handling code needed for the PropertyDescriptorCollection
:
- After getting the default property descriptors for a
MasterRecord
type object, they are filtered while being transferred into our own temporary array. Descriptors for collection types are left out because we don’t want to show the field for the collection in the grid. This is of course an optional step. - For every unique name of a detail value in any of our master records, one instance of our own custom property descriptor is created and also added to the temporary array. This step establishes the “virtual” properties, together with their names, for which our code will be called automatically.
The result
Data is initialized in the sample with the following code, the descriptors calculated and the datasource bound:
MasterRecordCollection coll = new MasterRecordCollection();
MasterRecord mr = new MasterRecord("TestRecord 1");
mr.DetailValues.Add(new DetailValue("TheAnswer", 42));
mr.DetailValues.Add(new DetailValue("TestRecord1Value", 101));
coll.Add(mr);
mr = new MasterRecord("Another test record");
mr.DetailValues.Add(new DetailValue("TheAnswer", 52));
mr.DetailValues.Add(new DetailValue("another Value", 824));
coll.Add(mr);
coll.CalculatePropertyDescriptors();
dataGrid1.DataSource = coll;
This results in the following output:
Download
The sample that has been mentioned in this post can be downloaded here.