Really complex databinding: ITypedList with weakly typed collections
Preface
When you, as a developer, have written a class library which has to be bound to complex user controls like a datagrid, and you want control over the databinding process, you are confronted with one of the most complex interfaces to implement: ITypedList. I'm not sure if the reason why complex databinding is called complex is because of the complexity of the interface which makes this all possible, ITypedList, but I wouldn't be surprised.
Complex databinding is the kind of databinding where a set (for example an IList or collection) of objects is bound to a multi-element control, like a datagrid control. The datagrid control for example has to display columns, name them, display the correct data in each of these columns, so in other words it has to do a variety of things and it's essential that the datagrid knows what kind of objects are stored in the collection that's bound. Everybody who has bound an ArrayList of objects to a datagrid control will know that this won't be a problem, you'll see a nice set of columns and each column reflects a property of the object at a given index in the ArrayList. However examining the ArrayList documentation shows that it doesn't implement ITypedList. So why is this interface so important when complex databinding databinding seems to work without ITypedList?
The reason for that is that ITypedList lets you control which columns are visible, with which description and how they should be treated, for example, should they be read only, even if there is a set clause in the property definition? An ArrayList with objects will show up in a datagrid with all columns editable (except if the related property has just a get clause) and every public property of the objects inside the ArrayList will have a column in the datagrid (unless the attribute BrowsableAttribute(false) is applied (winforms only)). In situations where data in inner structures have to be exposed as properties (e.g. the column objects in a DataView) or some properties have to be read only based on the state of the object, the default behaviour of the complex binding functionality of a collection is not sufficient: you have to implement ITypedList to tell the bound control exactly how the objects inside the bound collection have to show up in the control and which properties should show up and how they should be treated. Another issue with simple collections bound to a datagrid is the absence of any columns in the datagrid if the collection bound is empty (e.g. when datagrid columns are autogenerated from the bound datasource). This is understandable, as the datagrid doesn't see any objects in the bound collection and thus can't determine which properties are exposed by the objects in the collection, which results in an empty datagrid and no columns.
You can solve this by implementing ITypedList and simply supply the bound control with the data it otherwise would have retrieved from the actual objects in the bound collection. This becomes fairly complex when we throw in navigation through the object graph bound to the datagrid via the collection. Imagine you bind an ArrayList of Customer objects to the datagrid and each Customer contains an 'Orders' collection and each Order in that collection contains an OrderRows collection. How are these properties going to show up in the datagrid, if the datagrid is bound to an ArrayList with Customer objects? ITypedList will be able to supply the datagrid with this information.
ITypedList inner workings in general
ITypedList defines two methods: GetListName and GetItemProperties. GetListName is not that interesting and not required for what we want to do so we concentrate on GetItemProperties. Complex databinding works with property descriptors. A property descriptor is an object, often derived from the abstract class System.ComponentModel.PropertyDescriptor, which exposes information about a property like its name, type, if it is read only and also methods to get and set the property's value. A datagrid control tries to grab a list of property descriptors for the object type in the bound collection. Most datagrid controls can handle one type of object per hierarchy level or 'band', and the vanilla .NET grids (webforms/winforms) are no exception on this, so it will try this once per bound collection on a hierarchy level. With that list of property descriptors, one per publicly exposed property, it can then build the columns and read the data of each property and also set the data if the user changes the value of a cell.
ITypedList inner workings: single level hierarchy
If ITypedList is implemented on the collection bound to the datagrid, the datagrid will simply call ITypedList.GetItemProperties and will use the PropertyDescriptorCollection object returned. If ITypedList is not implemented (and with an ArrayList object, that's the case), the datagrid will call an overload of the static method System.ComponentModel.TypeDescriptor.GetProperties() to retrieve the property descriptors for the type of the object in the bound collection. With an empty ArrayList, there is no type information of the objects in the collection, because they're not there, nor does ArrayList implement ITypedList, which results in the situation where the datagrid doesn't have any property information whatsoever so it will simply display no columns. Some datagrid controls will allow you to pre-define columns and these will also be visible even if the data bound to the datagrid is completely empty, like an empty ArrayList, however in this situation, we don't have predefined columns or our datagrid control doesn't support showing pre-defined columns even if you bind an empty ArrayList.
ITypedList inner workings: multi level hierarchy
In the example mentioned earlier with an ArrayList of Customer objects and with each Custom object containing an ArrayList of Order objects, you have a hierarchy of objects which you can navigate through in a datagrid control (most datagrid controls allow you to do that, either by displaying 'bands' or 'levels' or another sort of 'level' description). When you, as a user see the Customer objects in the grid and you navigate to the Orders collection of a particular Customer object, the grid has to retrieve the property descriptors of the objects in the Orders collection. However it doesn't know of this collection, its DataSource is bound to an ArrayList of Customer objects. If the bound collection, in this case thus an ArrayList of Customer objects, doesn't implement ITypedList, the datagrid will try to access an instance of the Orders collection it has to view and will try to retrieve the property descriptors from that instance. If the bound collection does implement ITypedList, it will ask that collection to provide the property descriptors for the collection to view, even if this is another collection.
It does this by passing the property descriptors of the properties the user has navigated to the GetItemProperties call in the listAccessors array. Say, we implement ITypedList on a subclass of ArrayList and bind that subclass to the datagrid control. The user navigates via a given Customer object to its Orders collection. At that moment, our implementation of ITypedList which contains the Customer objects gets the call to supply property descriptors for the Orders collection of a given Customer object. There is however no instance information passed in, just property descriptors. A property descriptor object does have a method GetValue(), however you can't use that method without a reference to the object holding the property described by the particular property descriptor. We run into a problem: how do we know the properties of the object stored in the collection returned by the property described by the property descriptor passed in the listAccessors list? When we examine the type of that property, it's an ArrayList, that won't help much. We need some extra information. We'll see later on how that's supplied.
We have to solve the mistery of the listAccessors array before we can move on. What does it contain? As said, it contains all properties the user has used to navigate to the property which exposes the collection of objects we have to provide the property descriptors for. So this means that we can ignore the complete array except the last entry! The last entry in the array will be the property descriptor we're looking for. This is essential information as it will make our code much simpler.
The example
As an example I'll use a class called Customer which holds the data for a single customer such as contact name and company name. It will furthermore expose a collection object which holds Order objects and which is exposed as the property Orders. It will also contain a Hashtable with runtime information for the business logic. As mentioned briefly above, when you apply the .NET BrowsableAttribute attribute (located in the System.ComponentModel namespace) with a value of 'false' to a property, most controls which support complex binding will skip that property as a property to bind to. I say 'most', because ASP.NET datagrid controls and seem to ignore the BrowsableAttribute completely: a webforms datagrid will not skip a property which has a BrowsableAttribute(false) applied to it. This makes the BrowsableAttribute not usable if we also want to bind a collection of our classes to a webforms datagrid, we need something stronger, as otherwise the Hashtable will show up in the datagrid, and that's not what we want. Because we don't own the webforms datagrid source code, we have to use a generic mechanism to tell the control which properties to bind to. ITypedList is the interface to make that magic happen, together with a custom attribute class. As ITypedList is an interface we have to implement ourselves, we need a collection class to implement ITypedList on, the collection class which will be used to store our Customer objects in and also to store our Order objects in, which is present in each Customer object. The easiest way to do that is to create a subclass of System.Collections.ArrayList, as we're going to use a generic collection class, which are weakly typed. Weakly typed means: there is a single class definition for all types, not a specific, strongly typed collection which solely allows a single type to be added to the collection. To make it more real, the Order object contains an OrderRows collection of OrderRow objects.
The classes
Our grocery store list looks like this:
- Customer, Order and OrderRow classes
- Our collection class derived from ArrayList
- Own attribute class for hiding a property for databinding
- Own attribute class for specifying the type contained in the collection
That last one is not described yet. As mentioned above, in a hierarchy bound to a grid, the bound collection has to supply the property descriptors for all collections in the hierarchy without instance information or references. We'll use a custom attribute to specify the type inside a generic collection exposed by a property, like the Orders property of the Customer class. With that attribute, we can supply the property descriptors for the type specified as value in the attribute, and solve the problem of not having enough information to supply the correct property descriptors.
Our example will illustrate the following elements:
- Custom attributes
- ITypedList implementation on a weakly typed collection
- How to hide properties for databinding if they're marked with a given attribute
I've kept the example simple however it does illustrate all information required to successfully implement ITypedList. More advanced topics could deal with own PropertyDescriptor derived classes which override GetValue() and SetValue() (e.g. to set a given object somewhere in a collection instead of a property), or decentralized property descriptor manufacturing, for example by defining an interface every object has to implement and which is called by the GetItemProperties() method to provide the property descriptors for the class implementing that interface.
Enough ramblings for now, let's get to the real deal and discuss some code! I'll show the C# code here, VB.NET users have to convert this to VB.NET using one of the online converts like this one. The full example archive uses an example form, which you have to convert by hand to get the events properly wired, but it contains just two controls, so that's not a big problem.
TypeContainedAttribute class
The first class I'll discuss is the TypeContainedAttribute class. This attribute class will contain the type information for the type inside a collection exposed by a property. It is this attribute which will tell our GetItemProperties implementation which type is inside a collection deep in the hierarchy.
/// <summary>
/// Attribute to use on properties which return a weakly typed collection.
/// This attribute will tell the property descriptor construction code to construct a list of
/// properties of the type set as the value of the attribute.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class TypeContainedAttribute : Attribute
{
#region Class Member Declarations
private Type _typeContainedInCollection;
#endregion
/// <summary>
/// CTor
/// </summary>
/// <param name="typeContainedInCollection">The type of the objects contained in the collection
/// returned by the property this attribute is applied to.</param>
public TypeContainedAttribute(Type typeContainedInCollection)
{
_typeContainedInCollection = typeContainedInCollection;
}
#region Class Property Declarations
/// <summary>
/// Gets typeContainedInCollection, the type set in the constructor
/// </summary>
public Type TypeContainedInCollection
{
get
{
return _typeContainedInCollection;
}
}
#endregion
}
The attribute class is pretty simple, it takes a Type object and stores that internally and exposes that Type object via a property.
HiddenForDataBindingAttribute class
Similar is our other attribute, the one we'll use to mark properties as hidden for databinding:
/// <summary>
/// Attribute to use on properties to make them hidden for complex databinding.
/// This attribute will tell the property descriptor construction code to skip this property
/// for databinding purposes, the property will then not show up in the bound control.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class HiddenForDataBindingAttribute : Attribute
{
#region Class Member Declarations
private bool _isHidden;
#endregion
/// <summary>
/// CTor
/// </summary>
public HiddenForDataBindingAttribute()
{
_isHidden = false;
}
/// <summary>
/// CTor
/// </summary>
/// <param name="isHidden">Set to true if the property this attribute is applied
/// to should not be used in a complex databinding scenario. Default: false</param>
public HiddenForDataBindingAttribute(bool isHidden)
{
_isHidden = isHidden;
}
#region Class Property Declarations
/// <summary>
/// Gets isHidden
/// </summary>
public bool IsHidden
{
get
{
return _isHidden;
}
}
#endregion
}
This attribute takes a boolean as value in its constructor and exposes that value via a property. The value, when omitted, is false, so the attribute will not have any effect. As with all custom attribute classes, these attributes will not have any effect unless there is code implemented which uses the attributes to perform certain logic, in our case, constructing property descriptors in GetItemProperties(). As a side note: attributes are applied to/in types, not to instances. This means that we can't control the attribute's value for a particular instance, we can only apply them to types, or parts of types like a property or public field.
Customer class
One of our three classes (Customer, Order, OrderRow) is listed below. It illustrates the usage of our two custom attributes and also shows the usage of our own collection class which is described later on in more detail. The code's formatting is compressed a bit to save space (hence the 'K & R formatting')
/// <summary>
/// Simple Customer class. For illustration purposes only.
/// </summary>
public class Customer
{
#region Class Member Declarations
private int _customerID;
private string _contactName, _companyName;
private Hashtable _runtimeAttributes;
private ComplexDatabindingArrayList _orders;
#endregion
/// <summary>
/// CTor
/// </summary>
public Customer() {
_contactName = string.Empty;
_companyName = string.Empty;
_runtimeAttributes = new Hashtable();
_orders = new ComplexDatabindingArrayList(typeof(Order));
}
#region Class Property Declarations
/// <summary>
/// Gets / sets CustomerID, this class' primary key field and uniquely
/// identifying this customer.
/// </summary>
public int CustomerID {
get { return _customerID; }
set { _customerID = value;}
}
/// <summary>
/// Gets / sets ContactName
/// </summary>
public string ContactName {
get { return _contactName; }
set { _contactName = value; }
}
/// <summary>
/// Gets / sets CompanyName
/// </summary>
public string CompanyName {
get { return _companyName; }
set { _companyName = value; }
}
/// <summary>
/// Gets the RuntimeAttributes collection, a set of name-value pairs used for business logic.
/// </summary>
[HiddenForDataBinding(true)]
public Hashtable RuntimeAttributes {
get { return _runtimeAttributes; }
}
/// <summary>
/// Gets the orders collection of this customer object
/// </summary>
[TypeContained(typeof(Order))]
public ComplexDatabindingArrayList Orders {
get { return _orders; }
}
#endregion
}
Two properties are of importance here: RuntimeAttributes and Orders. RuntimeAttributes, because we don't want that property to show up in a datagrid and Orders because we have applied our custom attribute TypeContainedAttribute() to it to illustrate that it exposes a collection with Order objects. Remember, we can't get a reference to the physical collection object exposed by this property at runtime in our GetItemProperties() method.
ComplexDatabindingArrayList class
It's now time to introduce our core class for this example, the ComplexDatabindingArrayList class. This class is a subclass of ArrayList and implements ITypedList. It delegates any other logic to the ArrayList class so it's pretty compact as well. Again, the formatting is compressed a bit to save space.
/// <summary>
/// ComplexDatabindingArrayList implementation. This class derives from ArrayList and
/// implements ITypedList and some other logic to make it possible to bind an instance
/// of this class to a grid or other complex databinding aware control, while keeping
/// the weakly typed nature of the ArrayList, and be able to have advanced ITypedList
/// provided databinding functionality, e.g.: this class makes it possible to autogenerate
/// columns in a grid even though the collection is empty.
/// </summary>
/// <remarks>It stores the type of the objects it contains. This is because attributes are
/// stored with the type, not the instance. So the initial collection of objects has to
/// have a type definition internally.</remarks>
public class ComplexDatabindingArrayList : ArrayList, ITypedList {
#region Class Member Declarations
private Type _containedType;
#endregion
/// <summary>
/// CTor
/// </summary>
/// <param name="containedType">The type of the objects to be stored in this instance</param>
public ComplexDatabindingArrayList(Type containedType) {
_containedType = containedType;
}
/// <summary>
/// CTor
/// </summary>
/// <param name="c">The ICollection whose elements are copied to the new list.</param>
/// <param name="containedType">The type of the objects to be stored in this instance</param>
public ComplexDatabindingArrayList(ICollection c, Type containedType):base(c) {
_containedType = containedType;
}
/// <summary>
/// CTor
/// </summary>
/// <param name="capacity">The number of elements that the new list is initially capable
/// of storing.</param>
/// <param name="containedType">The type of the objects to be stored in this instance</param>
public ComplexDatabindingArrayList(int capacity, Type containedType):base(capacity) {
_containedType = containedType;
}
/// <summary>
/// ITypedList.GetItemProperties implementation. This implementation will simply return
/// all property descriptors retrieved for the object stored (or specified to be stored)
/// in this instance, except properties which have the HiddenForDataBindingAttribute attribute
/// applied to them with a value of true.
/// </summary>
/// <param name="listAccessors">list of accessors which contains the complete navigation
/// path in a hierarchical bound object graph</param>
/// <returns>PropertyDescriptorCollection to be used by the calling control</returns>
/// <remarks>It will provide an array of property descriptors
/// for the type contained in this instance of the ArrayList. If this type is not determinable,
/// it will return an empty collection.</remarks>
public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors) {
Type typeOfObject = null;
// determine the type of the object to return the properties of. We first have to check
// whether the properties requested are for an object in this instance (listAccessors==null or empty), or
// that we're called to supply the property descriptors for an instance deeper in the object graph we're the
// root of.
if((listAccessors==null)||listAccessors.Length==0)
{
// provide the property descriptors for objects of this instance.
typeOfObject = _containedType;
}
else
{
// use the last entry in the listAccessors, grab its TypeContainedAttribute and instantiate an instance of
// the type in that attribute, use that entity instance to produce properties.
TypeContainedAttribute typeAttribute =
(TypeContainedAttribute)listAccessors[listAccessors.Length-1].Attributes[typeof(TypeContainedAttribute)];
if(typeAttribute==null)
{
// not found, not specified, can't determine properties.
return new PropertyDescriptorCollection(null);
}
typeOfObject = typeAttribute.TypeContainedInCollection;
}
// create property descriptors.
return GetPropertyDescriptors(typeOfObject);
}
/// <summary>
/// ITypedList.GetListName implementation. Not used in this example, so it returns always the
/// same string.
/// </summary>
/// <param name="listAccessors"></param>
/// <returns>Always the same string.</returns>
public string GetListName(PropertyDescriptor[] listAccessors) {
return "A Complex Databinding aware list";
}
/// <summary>
/// Creates the actual property descriptor collection for the type passed in.
/// </summary>
/// <param name="typeOfObject">the type of the object to get the property descriptors for</param>
/// <returns>filled property descriptor collection</returns>
/// <remarks>It will skip every property which has the HiddenForDataBindingAttribute attribute
/// applied to it with a value of true.</remarks>
private PropertyDescriptorCollection GetPropertyDescriptors(Type typeOfObject) {
PropertyDescriptorCollection typePropertiesCollection = TypeDescriptor.GetProperties(typeOfObject);
ArrayList propertyDescriptorsToUse = new ArrayList();
// now walk all properties in the property descriptor collection. If the property has the
// HiddenForDataBindingAttribute applied to it, it should be skipped.
foreach(PropertyDescriptor property in typePropertiesCollection)
{
HiddenForDataBindingAttribute hiddenAttribute =
(HiddenForDataBindingAttribute)property.Attributes[typeof(HiddenForDataBindingAttribute)];
if(hiddenAttribute!=null)
{
// check if the value is false.
if(hiddenAttribute.IsHidden)
{
// skip
continue;
}
}
// add it, as it doesn't have the attribute applied to it OR the attribute's value is false
propertyDescriptorsToUse.Add(property);
}
return new PropertyDescriptorCollection((PropertyDescriptor[])propertyDescriptorsToUse.ToArray(typeof(PropertyDescriptor)));
}
#region Class Property Declarations
/// <summary>
/// Gets / sets containedType
/// </summary>
public Type ContainedType {
get { return _containedType; }
set { _containedType = value; }
}
#endregion
}
The first thing to notice is the _containedType member. As attributes can't be applied to instances, we can't tell a particular instance of this class which type is in that collection. So our initial collection, with customers, which type does that collection contain? This question can be answered with different solutions, one is to store the type contained in the collection as well. The advantage of this is that it also works if the collection is empty, which is what we want.
The second thing to notice is of course the GetItemProperties routine. The first thing it does is to determine if it has to provide the properties of the objects in a collection deep in the hierarchy or for objects it contains itself. Be sure to check for null and for an empty array, as some controls supply null and others supply an empty array when the properties of the objects inside itself have to be provided. When there is no hierarchy navigated, we can simply use the type specified as containedType for this instance of the ComplexDatabindingArrayList. When this is not the case and listAccessors is not empty or null, we have to use our TypeContainedAttribute attribute to help us determine which type is contained by the collection exposed by the property. If the attribute is not applied, the routine simply gives up, as it doesn't know what types are in the exposed collection. This is a weakness in the mechanism, because we can't fall back on 'do it the hard way, datagrid!', which is performed by the datagrid when you bind a vanilla ArrayList or other collection which doesn't implement ITypedList.
When the type is determined, the actual work can start, which is performed by a separate private routine, GetPropertyDescriptors. This routine first grabs the property descriptors for all the properties of the type, by using TypeDescriptor.GetProperties(type). Because we want to hide properties which have our own HiddenForDataBindingAttribute attribute applied to them, we have to filter these out. To do this, we walk the retriever property descriptors and check if the property has the attribute present and with a true value. If so, the property is skipped. The properties which are left are the ones interesting for databinding and we return these.
To use our classes, we create simple form with a vanilla .NET datagrid control on it and bind to it a ComplexDatabindingArrayList instance with a single customer. This illustrates that properties are hidden and also that navigating to the Orders collection will show the columns but no data. With a normal ArrayList no column would have appeared.
Please download the complete VS.NET 2003 example project (C#) by clicking here (.zip, 9KB) or a VB.NET port here. Thanks to Gary L. Winey for the port.