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
Comments
- Anonymous
January 31, 2007
Thanks Linda this was what I was looking for. I would like to be able to have an class that implements runtime properties. Then attach to a objectDataSource control and hook the objectDataSource to a gridview. After making a few changes to the code I was able to get it almost working but there are a few problems.
- The dynamic fields do not appear by default in the gridview. They are in the bindable field list but they are not in the selected list.
- When I hit the refresh schema button in the designer an error occurs saying that one of the dynamic properties already exists. Any help would be welcome. Marc
- Anonymous
February 06, 2007
I ran into problems running this example, but was able to get it going with a few small changes. I'll post them here in case anyone else runs into this.
- In the GetProperties method change senderprop to addressPD. I'm not even sure what senderprop is?
- The wrong overloaded version of GetProperties is overridden. Use this one: public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
- Because of #2 you should call GetProperties of the base like so: PropertyDescriptorCollection cols = base.GetProperties(attributes);
Anonymous
February 06, 2007
The comment has been removedAnonymous
February 06, 2007
The comment has been removedAnonymous
February 06, 2007
Michael thanks for the post. I will try your changes. Appreciate the work you put in.Anonymous
April 17, 2007
Thanks Michael! I am terribly sorry that I made several mistakes in this artical. It should be 'PropertyDescriptorCollection homeAddr_child = addressPD.GetChildProperties();' in the override GetProperties method within the MyCustomTypeDescriptor class. In addition, what you've done to make the data bound second-level properties editable in the DataGridView is correct. But to get the DataGridView to bind to a second-level property in a data source, we only need to override the GetProperties method without parameter in the custom type descriptor. I will modify my sample code in the article.Anonymous
April 17, 2007
Hi Marc, thank you for your interest in this article! It seems that your problem is related to ASP.NET. Right? Sorry that I am not familiar with ASP.NET. If you still need help, please explain your problem more in detail. Thanks!Anonymous
May 09, 2007
Hi. I've tried but this is not working... I think I'm missing something... Maybe problems with names spaces of classes?Anonymous
May 14, 2007
One point of detail - is there a way to associate the newly created TypeDescriptionProvider with the target class without modifying the class file? that is, do all the stuff mentioned here without having access to the source for the 'Person' class?Anonymous
May 23, 2007
The comment has been removedAnonymous
June 08, 2007
Hi, this article is great. But, what can I do for n-level properties? -Product -Category -SubcategoryAnonymous
July 04, 2007
Thanks for a great example. One comment, is that you mentioned that a downsize to using ICustomTypeDescriptor was that it required modification of the class. However, your solution using TypeDescriptionProvider also requires that by way of the TypeDescriptionProvider attribute. You can apply your solution to any class, without having to apply the TypeDescriptionProvider attribute, by simply calling TypeDescriptor.AddProvider() to add the TypeDescriptionProvider for the type (or an instance of it). That means you can implement the solution for any class without the need for its source.Anonymous
July 19, 2007
I have a question that might be the same as was asked by Erick. I have a multi-level class structure. Something like this: ClassA contains an instance of ClassB. ClassA employs a description provider to expose properties of ClassB. ClassB contains an instance of ClassC. Now I have implemented ITypedList on ClassA and ClassB to get at ClassC's properties, but cannot seem to get at them. Do I need to add GetChildProperties calls in ClassA to get the properties in ClassC? Nothing seems to work. Any help would be appreciated.Anonymous
September 18, 2007
Here's the last step in creating our RoleProvider class, which surfaces custom properties for roles youAnonymous
December 19, 2007
Hi Linda. I adaptaded your solution to a more generic one using generics. This is what I was looking for. Thank you!!!Anonymous
January 31, 2008
The dotnet framework is an excellent tool, with many shining features. However, in my mind this represents a truly fundamental design flaw. The level of complexity that needs to be invoked here to accomplish this simple task is unacceptable.Anonymous
April 08, 2008
Hi. Men and Women. I have an easy solution for this problem. If Oneself overrides ToString() of the object Addres,his DataGridview could show the second level. public override ToString() { return cityname+" "postcode; }Anonymous
July 09, 2008
How about overriding the CellFormatting event of the DataGridView and using reflection with something like the following?: private void dataGridView1_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if ((dataGridView1.Rows[e.RowIndex].DataBoundItem != null) && (dataGridView1.Columns[e.ColumnIndex].DataPropertyName.Contains("."))) { string[] nameAndProp = dataGridView1.Columns[e.ColumnIndex].DataPropertyName.Split(new char[]{'.'}); object unboundValue = ((DataRowView)dataGridView1.Rows[e.RowIndex].DataBoundItem)[nameAndProp[0]]; PropertyInfo objectProperty = unboundValue.GetType().GetProperty(nameAndProp[1]); e.Value = objectProperty.GetValue(unboundValue, null).ToString(); } } This can be extended to make it recursive in case of nested subclasses...Anonymous
July 09, 2008
Sorry, I forgot to add that in the column we can use the "dot notation" we're used to: this.dataGridView1.Columns["name"].DataPropertyName = "Class.Property";Anonymous
February 20, 2009
I know I am very late. But still, if you see my post, I am lucky. I am doing a "web" application and using listview and objectcontainerdatasource for binding. My classes are as follows. public class StudentContactCC : PersonCC { public StudentContactCC() { } //Base Members public String ContactID { get; set; } public String StudentID { get; set; } public String ContactType { get; set; } } public class Person { .. PhoneNumberCC HomePhone{get;set;} } public class PhoneNumberCC { public String PersonID { get; set; } public String PhoneID { get; set; } public String PhoneType { get; set; } public String AreaCode { get; set; } public String ExchangeCode { get; set; } public String LocalNumber { get; set; } public String Extension { get; set; } } After reading your article I created the 3 classes: 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 true; } 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); } public class StudentContactTypeDescriptionProvider : TypeDescriptionProvider { private ICustomTypeDescriptor td; public StudentContactTypeDescriptionProvider() : this(TypeDescriptor.GetProvider(typeof(StudentContactCC ))) { } public StudentContactTypeDescriptionProvider(TypeDescriptionProvider parent) : base(parent) { } public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance) { if (td == null) { td = base.GetTypeDescriptor(objectType, instance); td = new StudentContactCCTypeDescriptor(td); } return td; } } public class StudentContactCCTypeDescriptor : CustomTypeDescriptor { public StudentContactCCTypeDescriptor(ICustomTypeDescriptor parent) : base(parent) { } public override PropertyDescriptorCollection GetProperties() { PropertyDescriptorCollection cols = base.GetProperties(); PropertyDescriptor homephonePD = cols["HomePhone"]; PropertyDescriptorCollection homePhone_child = homephonePD.GetChildProperties(); PropertyDescriptor[] array = new PropertyDescriptor[cols.Count + 3]; //3 for each home,cell,work cols.CopyTo(array, 0); array[cols.Count] = new SubPropertyDescriptor(homephonePD, homePhone_child["AreaCode"], "HomePhoneAreaCode"); array[cols.Count + 1] = new SubPropertyDescriptor(homephonePD, homePhone_child["ExchangeCode"], "HomePhoneExchangeCode"); array[cols.Count + 2] = new SubPropertyDescriptor(homephonePD, homePhone_child["LocalNumber"], "HomePhoneLocalNumber"); PropertyDescriptorCollection newcols = new PropertyDescriptorCollection(array); return newcols; } } Now, I created a list like this : [Serializable()] public class ContactList<T> : List<T>, ITypedList { [NonSerialized()] private PropertyDescriptorCollection properties; public ContactList() : base() { PropertyDescriptorCollection pdc = TypeDescriptor.GetProperties(typeof(T), new Attribute[] { new BrowsableAttribute(true) }); pdc.Sort(); } #region ITypedList Members public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors) { PropertyDescriptorCollection pdc = null; if (null == listAccessors) { pdc = properties; } else { StudentContactTypeDescriptionProvider ContactProvider = new StudentContactTypeDescriptionProvider(); StudentContactCC Contact = new StudentContactCC(); StudentContactCCTypeDescriptor Desccriptor = new StudentContactCCTypeDescriptor(ContactProvider.GetTypeDescriptor(Contact)); pdc = Desccriptor.GetProperties(); } return pdc; } public string GetListName(PropertyDescriptor[] listAccessors) { return typeof(T).Name; } #endregion } In my code behind, I assign a ContactList to my datasource: ContactList <StudentContactCC> Guardians = _controller.GetStudentContactList (_controller.GetStudentID()); And I bind it to my datasource: ocdsGuardians.DataSource = Guardians ; ocdsGuardians.DataBind(); In the UI, I do binding like this: <asp:TextBox ID="txtHomePhoneAreaCode" runat="server" Width="2em" MaxLength="3" Text='<%# Bind("HomePhoneAreaCode") %>'></asp:TextBox> When I go to the edit mode of my listview, areacode get populated.But when I try to update, I am not getting the user entered value. Please keep in mind that I am doing web development.Anonymous
March 15, 2010
Thank for the useful technique, but I figure out one problem while trying to put more than one. example: [TypeDescriptionProvider(typeof(MyTypeDescriptionProvider))] [TypeDescriptionProvider(typeof(MyOtherTypeDescriptionProvider))] class Person{} It shows the error message that duplicate TypeDescriptionProvider, Could you please pointing out how to solve this.Anonymous
January 18, 2011
The code below implements a DeepBindingList<T> class that exposes nested properties (to any level). It also supports sorting. I hope it's useful to some of you: using System; using System.ComponentModel; using System.Collections.Generic; using System.Text; namespace DeepBindingList { /// <summary> /// Extends the BindingList<T> to provide sorting and deep property /// binding (e.g. Address.Street). /// </summary> public class DeepBindingList<T> : BindingList<T>, ITypedList { //----------------------------------------------------------------------- #region ** IBindingList overrides (to provide sorting) PropertyDescriptor _sort; ListSortDirection _direction; protected override bool IsSortedCore { get { return _sort != null; } } protected override void RemoveSortCore() { _sort = null; } protected override ListSortDirection SortDirectionCore { get { return _direction; } } protected override PropertyDescriptor SortPropertyCore { get { return _sort; } } protected override bool SupportsSortingCore { get { return true; } } protected override void ApplySortCore(PropertyDescriptor pd, ListSortDirection direction) { // get list to sort var items = this.Items as List<T>; // apply the sort if (items != null) { var pc = new PropertyComparer<T>(pd, direction); items.Sort(pc); } // save new settings and notify listeners _sort = pd; _direction = direction; this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); } // PropertyComparer (used to sort the list) class PropertyComparer<TC> : IComparer<TC> { PropertyDescriptor _pd; ListSortDirection _direction; public PropertyComparer(PropertyDescriptor pd, ListSortDirection direction) { _pd = pd; _direction = direction; } public int Compare(TC x, TC y) { try { var v1 = _pd.GetValue(x) as IComparable; var v2 = _pd.GetValue(y) as IComparable; int cmp = v1 == null && v2 == null ? 0 : v1 == null ? +1 : v2 == null ? -1 : v1.CompareTo(v2); return _direction == ListSortDirection.Ascending ? +cmp : -cmp; } catch { return 0; // comparison failed... } } } #endregion //----------------------------------------------------------------------- #region ** ITypedList (to expose inner properties) public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors) { var list = new List<PropertyDescriptor>(); foreach (PropertyDescriptor pd in TypeDescriptor.GetProperties(typeof(T))) { AddProperties(pd, null, list); } return new PropertyDescriptorCollection(list.ToArray()); } void AddProperties(PropertyDescriptor pd, PropertyDescriptor parent, List<PropertyDescriptor> list) { // add this property pd = new DeepPropertyDescriptor(pd, parent); list.Add(pd); // and subproperties for non-value types if (!pd.PropertyType.IsValueType && pd.PropertyType != typeof(string)) { foreach (PropertyDescriptor sub in TypeDescriptor.GetProperties(pd.PropertyType)) { AddProperties(sub, pd, list); } } } public string GetListName(PropertyDescriptor[] listAccessors) { return null; } // property descriptor with support for sub properties (e.g. Address.Street) class DeepPropertyDescriptor : PropertyDescriptor { PropertyDescriptor _pd; PropertyDescriptor _parentPD; public DeepPropertyDescriptor(PropertyDescriptor pd, PropertyDescriptor parentPd) : base(pd.Name, null) { _pd = pd; _parentPD = parentPd; } public override string Name { get { return _parentPD != null ? string.Format("{0}.{1}", _parentPD.Name, _pd.Name) : _pd.Name; } } public override bool IsReadOnly { get { return _pd.IsReadOnly; } } public override void ResetValue(object component) { _pd.ResetValue(component); } public override bool CanResetValue(object component) { return _pd.CanResetValue(component); } public override bool ShouldSerializeValue(object component) { return _pd.ShouldSerializeValue(component); } public override Type ComponentType { get { return _pd.ComponentType; } } public override Type PropertyType { get { return _pd.PropertyType; } } public override object GetValue(object component) { if (_parentPD != null) { component = _parentPD.GetValue(component); } return _pd.GetValue(component); } public override void SetValue(object component, object value) { _pd.SetValue(_parentPD.GetValue(component), value); OnValueChanged(component, EventArgs.Empty); } } #endregion } }Anonymous
March 08, 2011
Overriding the CellFormatting event described by Guillermo works like a charm!Anonymous
April 08, 2012
hey Linda Liu where is some source code for download you stupid *** ***. As usual article is crapola and author is some foreign half burnt ***Anonymous
October 18, 2013
FYI, binding to nested public properties of basic types, i.e., "HomeAddr.CityName" works in .NET 4.0.Anonymous
February 14, 2014
@Dan That doesn't work for me. Using the same Person and Address class it just show blank cells.Anonymous
May 15, 2014
Hi, Thx Linda It's work for me. But I'm using Entities & WPF. It work on the server side but not on the client side. I tried to serialize the 3 classes but no success.