Tips for binding grids to hierarchical data using the ITypedList interface

November 13th, 2006

Microsoft offers a rich set of interfaces and tools for displaying data in grids, but the documentation is terrible. For whatever reason, documentation is always by example and never actually explains the underlying concepts. Furthermore, the people who write examples (although I am indebted to many of them – so thank you) can’t seem to resist making them complete and featureful, increasing their usefulness from a “cut and paste” point of view, but making them poor as tutorials. This is an attempt to actually explain the essentials of the ITypedList interface (and the closely related PropertyDescriptor class).

Let’s start with an overview of the problem. If you want to create a grid-like user interface in an application, the easiest solution is to use the Microsoft DataGrid class or an off-the-shelf third party grid control such as the Infragistics UltraGrid, and assign the grid’s DataSource property to the data of interest.

This is simple if the data of interest is a DataSet object or other collection designed for easy integration with grids. But it’s not so easy if you want to connect the grid to a collection of application-specific objects. Most grids are designed such that if you assign the DataSource property to an arbitrary list of objects, something reasonable happens. The grid will typically use reflection to get the names and datatypes of the object’s members. But if you want to control which columns are displayed, how the values are formatted, and so on, then this solution is inadequate. And if the objects contain lists of other objects and you want the grid to be able to display the sublists, the problem is even more difficult.

Microsoft’s ITypedList interface provides a solution. It’s even rather elegant and minimal, although you wouldn’t think so from reading the documentation and examples. Underlying this solution is the PropertyDescriptor class, which provides all of the information required to deal with a particular table column, i.e. its datatype, display name, how to get its value given a row-level object, and so on. Specifically this class’s PropertyType property returns the column’s datatype as a Type object, it’s DisplayName property returns the column’s display name as a string, it’s GetValue method takes a row-level object and returns the column’s value, and so on. This is a great building block. All we need to do is provide the grid with a suitable list of property descriptors whenever the grid needs to know how to display a row. That’s where the ITypedList interface comes in. It provides a method called GetItemProperties which returns a list of PropertyDescriptor objects providing the grid with the information it needs. Thus the basic structure of our solution is this:

class MyCustomList : ArrayList, ITypedList
{
  PropertyDescriptorCollection ITypedList.GetItemProperties (PropertyDescriptor[] listAccessors)
  {
    ... return list of PropertyDescriptor objects ...
  }

  string ITypedList.GetListName(PropertyDescriptor[] listAccessors)
  {
    return "whatever";  // not important - ignore this for now
  }
}

The next question is: How do we make PropertyDescriptor objects that do what we need? There are two strategies for creating them. There’s the clever way (which is to use .NET’s reflection capabilities to give you a list of PropertyDescriptor objects for a particular class, remove the ones you don’t want, then customize the rest) and there’s the non-clever way (which is to create them from scratch). For this example, we will go with the non-clever way, which is better for tutorial purposes. Not only that, we will do the dumbest, simplest thing possible: Create a subclass called TutorialExamplePropertyDescriptor with no additional properties. The idea is to allocate one of these whenever we need a property descriptor, storing only the name in the base part of the object. The various methods (PropertyType, DisplayName, etc.) will be overridden with versions that switch on the name to dispatch to the right code. Here is the idea:

class TutorialExamplePropertyDescriptor : PropertyDescriptor
{
  public TutorialExamplePropertyDescriptor(string descriptorName)
    : base(descriptorName, new Attribute[0])
  {
    // constructor does nothing except store the name in the base class
  }

  public override Type PropertyType
  {
    get
    {
      // switch on the name to return the appropriate type
      switch (this.Name)
      {
        case "CustName":           return typeof(string);
        case "CustOrders":         return typeof(ArrayList);
        // etc...
      }
    }
  }

  // ... override other methods - DisplayName, GetValue, etc.
}

To return a list of property descriptors we just write:

return new PropertyDescriptorCollection (new PropertyDescriptor[]
{
  new TutorialExamplePropertyDescriptor("CustName"),
  new TutorialExamplePropertyDescriptor("CustOrders")
});

This is a poor design, inefficient and wasteful of memory, but is (hopefully) simple and readable and will help clarify the purpose of property descriptors. After you understand this example, you can improve the implementation by caching the lists of PropertyDescriptor objects, avoiding the runtime switch statements, using reflection, and so on. Note that if your custom collection is not actually a list of class instances, but something else – such as a giant 2D array, a list of Hashtables, or some other data structure – then you can’t use reflection anyway, since there is no class to reflect over. In that case you’ll be glad this example is the non-clever kind.

We now have all of the building blocks we need to handle simple bindable lists (i.e. without sublists). We simply implement the ITypedList.GetItemProperties method and have it return a suitable list of TutorialExamplePropertyDescriptor objects. Then we can bind a grid to our list and the rows should display exactly as we want.

But let’s not stop here. What if we want to have rows which contain sublists? For example what if we are binding a list of Customer objects which each have a sublist of Order objects? How does the grid know how to display the Order rows? And if the Order objects contain sublists of OrderItem objects, how does the grid know how to display those? The first point is that the grid needs to know which columns represent list values. That’s easily done – it checks the property descriptors and if any of them are list types (e.g. ArrayList) it displays a “plus” next to the row indicating that the row can be expanded to show the sublist. The second point is that the grid needs a separate list of property descriptors for each type of row it can display. It obtains these lists by calling the ITypedList.GetItemProperties method every time it needs a list of property descriptors for a particular type of row, passing an argument called listAccessors to specify which type of row it wants to know about. When the grid wants to know about the top-level (primary) rows, it passes a null argument. But then say the user navigates to the Orders sublist for a particular customer; then the grid calls the GetItemProperties method passing it the Orders property descriptor, meaning that it is looking for the PropertyDescriptor list for the Order row type. If the user then navigates to the OrderItems field within the order, then the grid calls the GetItemProperties method passing it an array with the Orders property descriptor as the first element, and the OrderItems property descriptor as the second element. The listAccessors array thus represents the array of PropertyDescriptors that the user has navigated to so far.

This listAccessors argument to the GetItemProperties method is a source of much confusion, but the basic idea is simple: It’s the grid’s way of asking “given where I’ve navigated so far, how do I display the next kind of row?” Generally the implementor of this method needs to look at only the last entry in that array to figure out the answer, e.g. if the user has navigated to the OrderItems property, then you don’t care how the user navigated there – you know which property descriptors to return as the result. But (to come up with an artificial example) you could imagine a Customer class containing the properties CurrentOrders and ShippedOrders, both containing lists of OrderItem objects – and it’s conceivable that you might want to display the OrderItem objects differently for each of the two order lists. In that case, whenever the last listAccessors entry is of type OrderItem, you would check the second-last entry as well, and return different property descriptors depending on whether it was for CurrentOrders or ShippedOrders.

All this complexity applies only to lists that contain sublists. If you want to bind a simple list without sublists, then the listAccessors property is simple to implement – you always return the same list of property descriptors (for the top-level rows) without even looking at the listAccessors argument. Or if you want to be tidy, you can throw an exception if listAccessors is non-null (since it shouldn’t be in this case).

Here’s the simplest possible code example I could think of to illustrate all of these concepts together. I’ve removed linefeeds, made properties public without writing getters and setters, and taken other measures for brevity. (Or you can download the working C# code example and build it in Visual Studio.)

namespace DataBindingTutorialExample
{
  class Customer
  {
    public string CustName;
    public ArrayList CustOrders;
  }

  class Order
  {
    public DateTime OrderDate;
    public ArrayList OrderItems;
  }

  class OrderItem
  {
    public int ItemQuantity;
    public string ItemDescription;
  }

  class CustomerList : ArrayList, ITypedList
  {
    PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor[] listAccessors)
    {
      if (listAccessors == null)
      {
        return new PropertyDescriptorCollection (new PropertyDescriptor[]
            {
              new TutorialExamplePropertyDescriptor("CustName"),
              new TutorialExamplePropertyDescriptor("CustOrders")
            });
      }
      else
      {
        string parentDescriptorName = listAccessors[listAccessors.Length - 1].Name;
        switch (parentDescriptorName)
        {
          case "CustOrders":
            return new PropertyDescriptorCollection(new PropertyDescriptor[]
                {
                  new TutorialExamplePropertyDescriptor("OrderDate"),
                  new TutorialExamplePropertyDescriptor("OrderItems")
                });

          case "OrderItems":
            return new PropertyDescriptorCollection(new PropertyDescriptor[]
                {
                  new TutorialExamplePropertyDescriptor("ItemQuantity"),
                  new TutorialExamplePropertyDescriptor("ItemDescription")
                });

          default:
            throw new Exception("Not implemented: " + parentDescriptorName);
        }
      }
    }

    string ITypedList.GetListName(PropertyDescriptor[] listAccessors)
    {
      return "whatever";
    }
  }

  class TutorialExamplePropertyDescriptor : PropertyDescriptor
  {
    public TutorialExamplePropertyDescriptor(string descriptorName)
      : base(descriptorName, new Attribute[0])
    {
    }

    public override Type ComponentType
    {
      get
      {
        switch (this.Name)
        {
          case "CustName":         return typeof(Customer);
          case "CustOrders":       return typeof(Customer);
          case "OrderDate":        return typeof(Order);
          case "OrderItems":       return typeof(Order);
          case "ItemQuantity":     return typeof(OrderItem);
          case "ItemDescription":  return typeof(OrderItem);
          default:                 return null;
        }
      }
    }

    public override Type PropertyType
    {
      get
      {
        switch (this.Name)
        {
          case "CustName":           return typeof(string);
          case "CustOrders":         return typeof(ArrayList);
          case "OrderDate":          return typeof(DateTime);
          case "OrderItems":         return typeof(ArrayList);
          case "ItemQuantity":       return typeof(int);
          case "ItemDescription":    return typeof(string);
          default:                   return null;
        }
      }
    }

    public override object GetValue(object component)
    {
      switch (this.Name)
      {
        case "CustName":         return ((Customer)component).CustName;
        case "CustOrders":       return ((Customer)component).CustOrders;
        case "OrderDate":        return ((Order)component).OrderDate;
        case "OrderItems":       return ((Order)component).OrderItems;
        case "ItemQuantity":     return ((OrderItem)component).ItemQuantity;
        case "ItemDescription":  return ((OrderItem)component).ItemDescription;
        default:                 return null;
      }
    }

    public override string DisplayName
    {
      get
      {
        switch (this.Name)
        {
          case "CustName":         return "Customer name";
          case "CustOrders":       return "Customer orders";
          case "OrderDate":        return "Order date";
          case "OrderItems":       return "Order items";
          case "ItemQuantity":     return "Quantity";
          case "ItemDescription":  return "Description";
          default:                 return null;
        }
      }
    }

    public override bool IsReadOnly { get { return true; } }
    public override void SetValue(object component, object value)  { throw new Exception("Not implemented."); }
    public override void ResetValue(object component)              { throw new Exception("Not implemented."); }
    public override bool CanResetValue(object component)           { throw new Exception("Not implemented."); }
    public override bool ShouldSerializeValue(object component)    { throw new Exception("Not implemented."); }
  }
}

Notes:

This code has been tested with the .NET 1.1 DataGrid and the Infragistics Ultragrid. It works as a data source for either, but looks much nicer with the Ultragrid.

If you change the property descriptor’s IsReadOnly property to return false and fill in the code for the SetValue method, the grid will allow simple edits. But for more complex editing capabilities (e.g. adding/deleting rows, sorting by field, etc.) your collection needs to implement the IBindingList interface.

In some cases when using the Microsoft DataGrid, setting the AllowNavigation property to true causes it to display useful navigation buttons on the right hand side of the title bar. This seems to work in .NET 2.0 but not .NET 1.1.

Note: This example does not work with the .NET 2.0 DataGridView class, which does not support hierarchical list display.

You can download a working C# code example here.

3 Responses to “Tips for binding grids to hierarchical data using the ITypedList interface”

  1. Kalani Thielen Says:

    Interesting. How does the DataGrid show headers for the different data types nested under the root type? (Or perhaps it doesn’t — sorry, I don’t have a C# compiler — can we add images to these web log posts?)

    Also, how does this support for hierarchical data sets work with the DataSet class? Does it support nested data sets? Can you write recursive SQL queries somehow (e.g.: “select * from employees where boss_id = $PARENT”)? If not, maybe that’s a useful library that could be built on this system you’ve figured out.

  2. Joe Morrison Says:

    Different grid implementations display the information in different ways. The Microsoft DataGrid shows one table or subtable at a time, so when viewing a sublist no other sublists are displayed and the navigation information is shown in the header. The Infragistics UltraGrid allows the display of any number of subtables simultaneously, repeating headers as needed.

    All of this works well on Microsoft DataSet objects, which can contain hierarchical information much like an in-memory database. Using a DataSet as your primary data structure is an alternative approach to creating the kind of custom collection I am proposing here.

  3. Adrian Soars Says:

    Good article Joe. Came across this article as I was asking someone about hierarchical data in a typelist – then came across your name.

    It fixed my problem.

    Hope you’re doing well!