Skip to main content Skip to footer

Binding C1TrueDbGrid to Hierarchical Object DataSource

While developing an application using C1TrueDBGrid which is bound to a business object, one may come across a situation when data needs to be displayed in a Hierarchical View. Now, if we need to create a hierarchical-grid-like general user interface in an application, the easiest solution is to assign the grid's DataSource property to the data of interest and let the control take care of the display of data in whichever view we need. This is simple if the data is contained as a DataSet or a collection object for easy integration with grids. But its not so easy if you want to connect the grid to a collection of application-specific objects. The grid is designed such that if you assign the DataSource property to an arbitrary list of objects, it'll typically use reflection to get the names and datatypes of the objects members. However, if you want to control which columns are displayed, how the values are formatted, and so on, then this solution is in-adequate. And it gets more complicated, if the Object Class definition contain Datamembers, again of Collection type and you wish to display the Sub Lists in the Grid. Let's begin adding a new class to our project and name it "Customerlist". Then add three more classes to it.


class Customer  
{  
  public string CustName = null;  
  public IBindingList CustOrders = null;  
}  

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

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

Now, we implement ITypedList interface. It does provide a solution and is rather elegant and minimal. 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, its 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. Let's create a class within "Customerlist.cs" using the same basic structure:


class CustomerList : ArrayList, ITypedList  
{  
 PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor[] listAccessors)  
 {  
 if (listAccessors == null)  
 {  
  // Return the property descriptors for top-level rows  
  return new PropertyDescriptorCollection (new PropertyDescriptor[]  
  {  
   new MyPropertyDescriptor("CustName"),  
   new MyPropertyDescriptor("CustOrders")  
  });  
 }  
 else  
 {  
  // Return the property descriptors for second-level and third-level rows  
  string parentDescriptorName = listAccessors[listAccessors.Length - 1].Name;  
  switch (parentDescriptorName)  
   {  
    case "CustOrders":  
    return new PropertyDescriptorCollection(new PropertyDescriptor[]  
    {  
     new MyPropertyDescriptor("OrderDate"),  
     new MyPropertyDescriptor("OrderItems")  
    });  

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

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

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

Now, create a subclass of PropertyDescriptor called "myPropertyDescriptor", 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.


class MyPropertyDescriptor : PropertyDescriptor  
{  
 public MyPropertyDescriptor(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."); }  
}  

However, what if we want to have rows which contain sub-lists? For example, what if we are binding a list of Customer objects which each have a sub-list 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-sign 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 sub-list 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 simply the grid's way of asking given where I've navigated so far, how do I display the next kind of row? Generally the implementer 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 know which property descriptors to return as the result. But imagine a Customer class containing the properties CurrentOrders and ShippedOrders, both containing lists of OrderItem objects? And its 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 sub lists. If you want to bind a simple list without sub lists, then the listAccessors property is simple to implement ? You can 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). Finally, we need instantiate a "CustomerList" object, populate it and then assign it as C1TrueDbgrid's DataSource. Please make sure that you set the grid's DataView property to DataViewEnum.Hierarchical.


private void Form1_Load(object sender, EventArgs e)  
{  
 c1TrueDBGrid1.DataView = C1.Win.C1TrueDBGrid.DataViewEnum.Hierarchical;  
 c1TrueDBGrid1.DataSource = GetCustomerList();  
}  

private CustomerList GetCustomerList()  
{  
 CustomerList customers = new CustomerList();  

 Order order1 = new Order();  
 order1.OrderDate = DateTime.Now;  
 order1.OrderItems = new BindingList<OrderItem>();  
 addOrderItem(order1, 2, "Ice Station");  
 addOrderItem(order1, 3, "Area7");  

 Order order2 = new Order();  
 order2.OrderDate = DateTime.Now;  
 order2.OrderItems = new BindingList<OrderItem>();  
 addOrderItem(order2, 7, "Scarecrow");  
 addOrderItem(order2, 1, "Hell Island");  

 Customer c1 = new Customer();  
 c1.CustName = "Matthew R.";  
 c1.CustOrders = new BindingList<Order>();  
 c1.CustOrders.Add(order1);  
 c1.CustOrders.Add(order2);  

 customers.Add(c1);  

 Order order3 = new Order();  
 order3.OrderDate = DateTime.Now;  
 order3.OrderItems = new BindingList<OrderItem>();  
 addOrderItem(order3, 12, "The Godfather");  

 Order order4 = new Order();  
 order4.OrderDate = DateTime.Now;  
 order4.OrderItems = new BindingList<OrderItem>();  
 addOrderItem(order4, 6, "The Sicilian");  
 addOrderItem(order4, 1, "Omerta");  

 Customer c2 = new Customer();  
 c2.CustName = "Mario Puzo";  
 c2.CustOrders = new BindingList<Order>();  
 c2.CustOrders.Add(order3);  
 c2.CustOrders.Add(order4);  

 customers.Add(c2);  

 return customers;  
}  

private void addOrderItem(Order order, int quantity, string description)  
{  
 OrderItem item = new OrderItem();  
 item.ItemQuantity = quantity;  
 item.ItemDescription = description;  
 order.OrderItems.Add(item);  
}  

Finally, this is how the grid looks like: Refer to the attached samples for complete implementation. Download VB Sample Download C# Sample

MESCIUS inc.

comments powered by Disqus