Register  
Saturday, July 04, 2009
Knowledge Base  


ActiReq Preview
  
Latest Blogs
  
Articles
19

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's 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).

Using the above code, this is the result:

The code is available for download here (registration is required)

Actions: E-mail | Permalink | Comments (25) RSS comment feed | Kick it! | DZone it! | del.icio.us | Bookmark and Share




Post Rating

Comments

VinK
Tuesday, August 14, 2007 10:26 AM
what about multiple columns ?
Antonio Bello
# Antonio Bello
Tuesday, August 14, 2007 10:41 AM
Hello Vink,

what do you exactly mean by "multiple columns"?
Please leave another message here or use the forum (I have created a forum for Winforms development)

Thanks
Ant.
Shmee007
# Shmee007
Tuesday, September 11, 2007 11:31 AM
When you edit the value, then move of the cell, it reverts back to the objects value instead of updating the objects value?
What would you do to bind so updates are done back to the object?
seesharper
Tuesday, September 18, 2007 4:58 PM
Take a look at my ObjectBindingSource at codeproject.
Name
# Name
Sunday, December 16, 2007 8:43 AM
BindProperty should return object, so that a format can be applied (e.g. string can not be formatted as currency).
JDS
# JDS
Wednesday, March 26, 2008 1:23 PM
protected override void OnCellFormatting(DataGridViewCellFormattingEventArgs e)
{
object dataItem;
string bindingPath;

if ((dataItem = this.Rows[e.RowIndex].DataBoundItem) != null
&&
(bindingPath = this.Columns[e.ColumnIndex].DataPropertyName).Contains("."))
{
e.Value = Bind(dataItem, bindingPath, 0);
}
else
{
base.OnCellFormatting(e);
}
}

private object Bind(object dataItem, string bindingPath, int index)
{
string[] tokens = bindingPath.Split(new char[] { '.' });
string token = tokens[index];
Type t = dataItem.GetType();
PropertyInfo pi = t.GetProperty(token);
object nextDataItem;

if (pi != null)
{
nextDataItem = pi.GetValue(dataItem, null);

if (index == tokens.Length - 1)
{
return nextDataItem;
}

return Bind(nextDataItem, bindingPath, index + 1);
}

return null;
}
Ali
# Ali
Wednesday, May 07, 2008 5:56 AM
This is very good demonstration for beginners.
stuart
# stuart
Monday, August 25, 2008 5:13 AM
This was a really clear method of achieving datagridview binding with nested objects and was just what I needed. I can't believe the grid doesn't support this functionality by default though!!!!

Great effort, cheers.
Paul
# Paul
Tuesday, September 02, 2008 10:12 AM
how would you now be able to sort the grid by sa Address.Street, As the Address object is not comparable like a base type?

I have tried this example (http://www.timvw.be/presenting-the-sortablebindinglistt/), but it only allows sorting by the Base Objects base types (Strings, Ints etc). Could you supply a sort routine which would sort the grid by any of the fields?
Antonio Bello
# Antonio Bello
Saturday, September 27, 2008 1:47 AM
Sorting was behind the scope of this article. I hope you have solved the problem in the meanwhile.
Anyway you gave me an idea for a new article - thanks
chillibug
# chillibug
Thursday, October 09, 2008 5:48 PM
Hello. I have created a gridview using list. I can fill the grid view in the same way as you hve with the array of objects. But I do not know how to use the gridview withe the list (insert new items, delete items, Update). I know how to manipulate the list, so i just need so help with interfacing it with the gridview. Can you help or at least point me to some where that can help. I have looked, but only found the above
Article, which has been the most informative - Thank you
chillibug
# chillibug
Thursday, October 09, 2008 5:50 PM
ps in the above where it says list, im refering to a generic List
Antonio Bello
# Antonio Bello
Wednesday, October 15, 2008 8:55 AM
Hi chillibug,

I hope I haven't misunderstood your question, it looks like you're asking about changing an underlying generic list and have these changes updated in the grid view - am I right?

The easiest solution is to bind the generic list to the data grid via a BindingSource, and each time the list is changed call its ResetBinding() method to force the grid reload the data.

If you are using a List<>, then you should do something like the following:

_bindingSource = new BindingSource();
dataGridView1.DataSource = _bindingSource;
_bindingSource.DataSource = _customers;

Then, when you add, remove or update the Customers list, you simply have to call the following method:

_bindingSource.ResetBindings(false);

I've uploaded a new version of the sample code with a new "GridViewGenerics" project implementing what discussed above.

Hope this helps - but let me know if I misunderstood your question.
chillibug
# chillibug
Wednesday, October 15, 2008 12:26 PM
Yep spot on!

I couldn’t work it out. My hack was to set the datasource to null then set the datasoursce back to the object. I’m trying to get edit and delete, of a selected row, to work.

Thank you.
rajeev kumar
# rajeev kumar
Thursday, October 30, 2008 12:24 AM
this example for windows application some of event and properties does not exist in web application like (e.g. gridCustomers_CellFormatting,DataPropertyName)



from rajeev
Antonio Bello
# Antonio Bello
Thursday, October 30, 2008 3:22 AM
Hi Rajeev,

in fact this article is for the WinForms Data Grid View. I haven't tried with the corresponding web control - I presume it doesn't work as is described in this article.
dami
# dami
Wednesday, November 12, 2008 9:17 AM
this article really helped me.
thanks Antonio :D
Anonymous User
# Anonymous User
Sunday, January 25, 2009 11:40 AM
DataGridView: how to bind nested objects
dotnet coder
# dotnet coder
Sunday, February 01, 2009 7:03 AM
Gridview binding to both base n derived classes

I have a employee class and manager class derived from it . I want to show the objects/records of both the base

class (employee class) and derived class (manager class) in a single gridview . How can i bind both the base

class n derived class in a gridview ? pls send me a link or code . In addition to this , i want to show the

records of base class and dervied class with different colours on grid ? how can i do that ? pls help , its

urgent !
Antonio Bello
# Antonio Bello
Sunday, February 01, 2009 8:54 AM
I presume that you want to bind a collection of objects to the grid, where each object can be an instance of either a base or a derived class.

If both classes expose the same properties/fields (or better they implement an interface), than I think that it should work with no additional work. Derived classes would be treated as instances of the corresponding base class.

Otherwise I think it's not easy to bind classes having different properties/fields to the same grid. How would fields mapped to columns? There would be a different mapping for each class type?

I'm unable to provide an answer without having more details.

As for the second question, I guess the best way is to work with one of the following events: CellPainting, RowsPostPaint, RowPrepaint.

By the way what you're asking for is not simple to implement, as well as out of the scope of this article. I can do some custom development for you in order to implement what you need, but this would be subject to a cost for you.

Oscar
# Oscar
Wednesday, February 04, 2009 9:06 PM
The article is so great! It helps me a lot!
How to edit the Address properties via the datagridview ?
AlfaHoroz
# AlfaHoroz
Sunday, February 22, 2009 5:59 PM
it's greater than greate ! it solves all of my orm problems thank you very much
SchattenTod
# SchattenTod
Wednesday, February 25, 2009 2:48 PM
Ejecute el codigo pero en Vb.Net, y funciono correctamente como lo esperaba.

en el datapropertyname le puse "grupo.nombre" y muestra el dato del contenedor...

excelente

scott
Friday, March 27, 2009 9:11 AM
Hi - this is great for small sets of data (maybe < 100 rows) but becomes unusable for a larger set, because the DataGridView has to build its own collection of strings (via e.Value for every row) as it starts up.

The app I'm working on builds a DataGridView for 10,000 rows in around 3 seconds. When I use nested properties, setting e.Value, then the control takes around 4 minutes to populate. I guess the difference is that the control is only interested in the view on the DataSource that is actually being displayed.

So, I've had to formulate a hack, where I present read-only members on the data source that de-normalise the nested entity. For example;

public class Person
{
public string Name;
public string Address; // contains Address.Street
public string NestedStreet { get { return Address.Street; } }
}

Then I bind the DataGridView on the column "NestedStreet" instead of "Address.Street"
dong
Tuesday, June 02, 2009 3:39 AM
thanksssssssssssssssssssssssss




Post Comment

Name (required)

Email (required)

Website

Enter the code shown above:

Syndicate    

 

stock icons

 

 

© 2006-2008 Developer's Corner - Powered by Elapsus   |  Privacy Statement  |  Terms Of Use