How to bind a DataGridView column to a second-level property of a data source

This is a frequently asked question. Suppose that we have a class Person which has a property of type Address(another class). The class Address has its own properties. Now we create a collection of Person objects, e.g. List<Person> and would like to bind a DataGridView to the collection.

The following is the code for class Person and Address.

class Person

{

         private string id;

         private string name;

         private Address homeAddr;

         public string ID

         {

                   get { return id;}

                   set { id = value;}

         }

         public string Name

         {

                   get { return name;}

                   set { name = value;}

         }

         public Address HomeAddr

         {

                   get { return homeAddr;}

                   set { homeAddr = value;}

         }

}

class Address

{

         private string cityname;

         private string postcode;

         public string CityName

         {

                   get { return cityname;}

                   set { cityname = value;}

         }

         public string PostCode

         {

                   get { return postcode;}

                   set { postcode = value;}

         }

}

As we all know, DataGridView columns could only be bound to the first-level properties of class Person, e.g. the ID, Name or HomeAddr properties. The following is the code to bind a DataGridView to a collection of Person objects.

List<Person> persons = new List<Person>();

// add some Person objects to the collection

...

dataGridView1.DataSource = persons;

dataGridView1.Columns[0].DataPropertyName = "ID";

dataGridView1.Columns[1].DataPropertyName = "Name";

dataGridView1.Columns[2].DataPropertyName = "HomeAddr";

When a DataGridView column is bound to the HomeAddr property, the name of the class Address will be displayed under the column, i.e. projectname.Address.

If we set the DataPropertyName property of a DataGridView column to "HomeAddr.CityName", an empty text will be displayed under the column, because DataGridView could not find a property called "HomeAddr.CityName" in the class Person.

Is there a way to bind a DataGridView column to the CityName or PostCode property of the HomeAddr property? The answer is YES!

.NET Framework 1.x can implement ICustomTypeDescriptor interface for the class Person. When a Person object in the collection is going to be displayed in a control, data binding calls ICustomTypeDescriptor.GetProperties method to get the object's properties. When implementing ICustomTypeDescriptor.GetProperties method, we could create PropertyDescriptor instances for the second-level properties and return them with the original PropertyDescriptor instances of class Person.

There're two problems here. One problem is that if there's no object in the collection, the ICustomTypeDescriptor.GetProperties method won't be called so that DataGridView couldn't 'see' secondary-level properties of class Person. The other problem is that it requires modifying the class Person because the class Person needs to implement the ICustomTypeDescriptor interface.

To work around the first problem, we could resort to ITypedList interface, i.e. implement the ITypedList interface for the collection. If a data source has implemented the ITypedList interface, data binding calls ITypedList.GetItemProperties to get the properties available for binding. In the ITypedList.GetItemProperties method, we could create an instance of class Person and then call TypeDescriptor.GetProperties(component) method passing the Person instance as the parameter, which in turn calls ICustomTypeDescriptor.GetProperties of the class Person implementation.

.NET Framework 2.0 way is to make use of TypeDescriptionProvider and CustomTypeDescriptor classes, which expand support for ICustomTypeDescriptor. It allows you to write a separate class that implements ICustomTypeDescriptor (for convenience, we could derive the class directly from CustomTypeDescriptor which provides a simple default implementation of the ICustomTypeDescriptor interface) and then to register this class as the provider of description for other types.

Even if a type is added a TypeDesciptionProvider, data binding won't call the custom type descriptor's GetProperties method to get the object's properties. So we still need to implement ITypedList interface for the collection and in the ITypedList.GetItemProperties method, call TypeDescriptor.GetProperties(type) which in turn calls the custom type descriptor's GetProperties method.

To take advantage of this new functionality, we first need to create a TypeDescriptionProvider, which simply derives from TypeDescriptionProvider and overrides its GetTypeDescriptor method. GetTypeDescriptor returns the ICustomTypeDescriptor implementation or the derived CustomTypeDescriptor that TypeDescriptor should use when querying for property descriptors.

When creating providers that are only intended to augment or modify the existing metadata for a type, rather than completely replace it, a recommended approach is to call 'TypeDescriptor.GetProvider' method in the new providers' constructor to get the current provider for the specified type. This base provider can then be used whenever you need to access the underlying type description.

As for PropertyDescriptor, it is an abstract class that derives from another abstract class, MemberDescriptor. MemberDescriptor provides the basic information for each property, and PropertyDescriptor adds functionality related to changing a property's value and determining when that value has changed. To create a PropertyDescriptor on the class Person for each second-level property, we first need to create a custom class that derives from PropertyDescriptor. Pass the original PropertyDescriptor of the second-level property in the new PropertyDescriptor class's constructor, and then use the original PropertyDescriptor whenever we need to query the information of the second-level property.

The following is the code of a custom class that derives from PropertyDescriptor.

public class SubPropertyDescriptor : PropertyDescriptor

{

        private PropertyDescriptor _subPD;

        private PropertyDescriptor _parentPD;

        public SubPropertyDescriptor(PropertyDescriptor parentPD,PropertyDescriptor subPD,string pdname)

            : base(pdname,null)

        {

            _subPD = subPD;

            _parentPD = parentPD;

        }

        public override bool IsReadOnly { get { return false; } }

        public override void ResetValue(object component) { }

        public override bool CanResetValue(object component) { return false; }

        public override bool ShouldSerializeValue(object component)

        {

            return true;

        }

        public override Type ComponentType

        {

            get { return _parentPD.ComponentType; }

   }

        public override Type PropertyType { get { return _subPD.PropertyType; } }

        public override object GetValue(object component)

        {

           return _subPD.GetValue(_parentPD.GetValue(component));

        }

        public override void SetValue(object component, object value)

        {

            _subPD.SetValue(_parentPD.GetValue(component), value);

            OnValueChanged(component, EventArgs.Empty);

        }

}

The following is the code of a derived CustomTypeDescriptor class.

public class MyCustomTypeDescriptor : CustomTypeDescriptor

{

        public MyCustomTypeDescriptor(ICustomTypeDescriptor parent)

            : base(parent)

        {

        }

        public override PropertyDescriptorCollection GetProperties()

  {

            PropertyDescriptorCollection cols = base.GetProperties();

            PropertyDescriptor addressPD = cols["HomeAddr"];

            PropertyDescriptorCollection homeAddr_child = addressPD.GetChildProperties();

          

            PropertyDescriptor[] array = new PropertyDescriptor[cols.Count + 2];

            cols.CopyTo(array, 0);

            array[cols.Count] = new SubPropertyDescriptor(addressPD,homeAddr_child["CityName"],"HomeAddr_CityName");

            array[cols.Count + 1] = new SubPropertyDescriptor(addressPD, homeAddr_child["PostCode"], "HomeAddr_PostCode");

            PropertyDescriptorCollection newcols = new PropertyDescriptorCollection(array);

            return newcols;

        }

  }

The following is the code of the custom TypeDescriptorProvider.

public class MyTypeDescriptionProvider : TypeDescriptionProvider

{

        private ICustomTypeDescriptor td;

        public MyTypeDescriptionProvider()

           : this(TypeDescriptor.GetProvider(typeof(Person)))

        {

        }

        public MyTypeDescriptionProvider(TypeDescriptionProvider parent)

            : base(parent)

        {

        }

        public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)

        {

            if (td == null)

            {

                td = base.GetTypeDescriptor(objectType, instance);

                td = new MyCustomTypeDescriptor(td);

            }

            return td;

        }

}

At the end, we adorn TypeDescriptionProviderAttribute to the class Person.

[TypeDescriptionProvider(typeof(MyTypeDescriptionProvider))]

class Person

{...}

Then we could bind a DataGridView column to the second-level properties in the class Person as follows:

dataGridView1.Columns[2].DataPropertyName = "HomeAddr_CityName";

dataGridView1.Columns[3].DataPropertyName = "HomeAddr_PostCode";

 

 

Linda Liu