Wednesday, July 18, 2007

DataGridView: how to bind nested objects

The WinForms DatagridView control is not capable of handling nested properties, as it works with base data types only. But implementing such feature is not complicated. Let’s see how.

The Problem

A DataGridView can be bound to a collection of objects to display any of its base data type (i.e. string, int, and so on) properties. For example, a collection of instances of the following class:
public class Customer
{
  public string FirstName { get; set;}
  public string LastName { get; set;}
  public string Street { get; set;}
  public string PostalCode { get; set;}
  public string City { get; set;}
} 
can be used to display any of the Customer class properties in the DataGridView.
But what happens when the class exposes other objects which are not base data type? If an Address class is created and used as a property of the Customer class:
public class Address
{
  public string Street { get; set;}
  public string PostalCode { get; set;}
  public string City { get; set;}
}

public class Customer
{
  public string FirstName { get; set;}
  public string LastName { get; set;}
  public Address Address { get; set;}
}
a DataGridView bound to a collection of Customer instances is not able to display any of the Address properties. In such cases the displayed cells are empty.

The Solution

In order to enable the DataGridView to display properties exposed by class members, some custom development is required. The first step is to implement the DataGridView's CellFormatting event handler
private void gridCustomers_CellFormatting(
  object sender, 
  DataGridViewCellFormattingEventArgs e)
{
  ...
}
The CellFormatting event handler needs to differentiate whether the field to be displayed is one of the base data type or an object exposing its own properties. This is achieved by looking for the dot character '.' in the Column's DataPropertyName field:
private void gridCustomers_CellFormatting(
  object sender, 
  DataGridViewCellFormattingEventArgs e)
{
  if ((gridCustomers.Rows[e.RowIndex].DataBoundItem != null) && 
      (gridCustomers.Columns[e.ColumnIndex].DataPropertyName.Contains(".")))
  {
    ...
  }
}
If the DataPropertyName field doesn't contain a dot character, then the actual value of the base data type property must be displayed - in this case no action is required, since it is already filled in e.Value.
On the other hand, if the DataPropertyName field contains one or more dot characters, then it points to a property exposed by one of the bound class properties. For example, Address.Street contains the dot character, and it points to the Street property of the Address property inside a Customer's instance.
To handle this cases,  a recursive function BindProperty is used:
private void gridCustomers_CellFormatting(
  object sender, 
  DataGridViewCellFormattingEventArgs e)
{
  if ((gridCustomers.Rows[e.RowIndex].DataBoundItem != null) && 
      (gridCustomers.Columns[e.ColumnIndex].DataPropertyName.Contains(".")))
  {
	e.Value = BindProperty(
                gridCustomers.Rows[e.RowIndex].DataBoundItem,
                gridCustomers.Columns[e.ColumnIndex].DataPropertyName
              );
  }
}
The BindProperty function resolves the data property name and provides the actual value to be displayed in the grid cell by using reflection and (if required) recursion. Two arguments are passed to the BindProperty function: the class property value (which is an instance of a class, in the above example an instance of the Address class) and the DataPropertyName:
private string BindProperty(object property, string propertyName)
{
  ...
}
The first thing to check is whether the property name contains the dot character - although this never happen when called directly from the CellFormatting event handler, since the if statement prevent this, it may happen when BindProperty calls itself recursively.
private string BindProperty(object property, string propertyName)
{
  if (propertyName.Contains("."))
  {
    ...
  }
  else
  {
    ...
  }
}
If the property name doesn't contain any dot character, then the propertyName variable contains the name of the property object to be displayed in the grid. Reflection is used to read the property value, obtained by retrieving the PropertyInfo from the property variable, and then getting the property value by calling the GetValue() method of the PropertyInfo instance:
private string BindProperty(object property, string propertyName)
{
  string retValue = "";
  
  if (propertyName.Contains("."))
  {
    ...
  }
  else
  {
    Type propertyType;
    PropertyInfo propertyInfo;

    propertyType = property.GetType();
    propertyInfo = propertyType.GetProperty(propertyName);
    retValue = propertyInfo.GetValue(property, null).ToString();
  }
}
This completes the else branch. As for the if branch, it is executed when the property name contains at least one dot character. In this case, still using reflection, the PropertyInfo of the desired property is retrieved, and using recursion, passed to the same BindProperty function.
private string BindProperty(object property, string propertyName)
{
  string retValue = "";

  if (propertyName.Contains("."))
  {
    PropertyInfo[] arrayProperties;
    string leftPropertyName;

    leftPropertyName = propertyName.Substring(0, propertyName.IndexOf("."));
    arrayProperties = property.GetType().GetProperties();

    foreach (PropertyInfo propertyInfo in arrayProperties)
    {
      if (propertyInfo.Name == leftPropertyName)
      {
        retValue = BindProperty(
          propertyInfo.GetValue(property, null), 
          propertyName.Substring(propertyName.IndexOf(".") + 1));
        break;
      }
    }
  }
  else
  {
    Type propertyType;
    PropertyInfo propertyInfo;

    propertyType = property.GetType();
    propertyInfo = propertyType.GetProperty(propertyName);
    retValue = propertyInfo.GetValue(property, null).ToString();
  }

  return retValue;
}
The leftPropertyName variable holds the name of the leftmost property, for instance if propertyName is Address.Street, it is filled in with Address. By looping through the array of PropertyInfo of the property object, the PropertyInfo instance of the leftPropertyName property is retrieved. Then, BindProperty is called again, passing the instance of the leftPropertyName property and the right part of propertyName (for instance, for Address.Street the provided property name is Street).

Souce code available at github: DataGridViewSample

8 comments:

  1. Awesome stuff. Works like a charm even after 4 years of this article being written. Thank you very much.

    ReplyDelete
  2. Thanks - and glad to know you found it useful.
    If you need the sample sources, I've uploaded at github here:

    https://github.com/jeden/DataGridViewSample

    ReplyDelete
  3. Nice! This came very handy. Thanks

    ReplyDelete
  4. does this not work when trying to commit edits to the datagrid column values?

    ReplyDelete
  5. No it does't support editing - this solution works in read only mode.

    ReplyDelete
  6. any way to pass the defaultcellstyle.format down through this? or another trick to format (by column) cell values?

    ReplyDelete
  7. Almost 6 years ago and still works like a charm!

    Just awesome!

    Thank you so much!

    ReplyDelete
  8. Almost 7 years ago and still works like a charm!!!

    ReplyDelete

Copyright © 2013. All Rights Reserved.