The number of times I have run into the following problem is astounding. I need to bind a list of complex/nested objects (those that contain other objects) to a gridview and show the values of nested objects. For example, let's say I have the following Customer class which contains a State object that holds information about in which state the customer lives.
The class definitions are as follows:
public class Customer
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Title { get; set; }
public string Company { get; set; }
public string Address { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public State State { get; set; }
public string PostalCode { get; set; }
}
public class State
{
public int StateID { get; set; }
public string Name { get; set; }
public string PostalCode { get; set; }
}
As you can see, the Customer object contains a State object. Here is the gridview declaration (note the DataField="State.Name"):
<asp:GridView ID="uxCustomerView" runat="server" CssClass="grid" Width="90%" GridLines="None"
AutoGenerateColumns="false" DataKeyNames="CustomerID" RowStyle-CssClass="viewRow"
OnRowEditing="uxCustomerView_RowEditing" OnRowDeleting="uxCustomerView_RowDeleting">
<Columns>
<asp:BoundField DataField="FirstName" HeaderText="First Name" />
<asp:BoundField DataField="LastName" HeaderText="Last Name" />
<asp:BoundField DataField="Title" HeaderText="Title" />
<asp:BoundField DataField="Company" HeaderText="Company" />
<asp:BoundField DataField="Address1" HeaderText="Address" />
<asp:BoundField DataField="City" HeaderText="City" />
<asp:BoundField DataField="State.Name" HeaderText="State"/>
<asp:BoundField DataField="PostalCode" HeaderText="Postal Code" />
</Columns>
</asp:GridView>
After binding a list of Customers to a gridview, the gridview will complain that the BoundField cannot decipher the DataField = "State.Name". Why this doesn't work is beyond me; I could never quite find an elegant solution on Google to assist me. Here is the solution I came up with.
First, I created a new class called ComplexBoundField that inherits from DataControlField (the base class for all gridview fields). After implementing one abstract method, I then overrode the InitializeCell method and created a new Literal control. The Literal control has a clutch DataBinding event which we can hook into in order to properly bind our complex object. Here is the ComplexBoundField class:
public class ComplexBoundField : DataControlField
{
public string DataField { get; set; }
public override void InitializeCell(DataControlFieldCell cell, DataControlCellType cellType, DataControlRowState rowState, int rowIndex)
{
switch (cellType)
{
case DataControlCellType.DataCell:
{
var complexDisplay = new Literal();
complexDisplay.DataBinding += complexDisplay_DataBinding;
cell.Controls.Add(complexDisplay);
break;
}
}
base.InitializeCell(cell, cellType, rowState, rowIndex);
}
private void complexDisplay_DataBinding(object sender, EventArgs e)
{
var complexDisplay = (Literal)sender;
var container = (GridViewRow)complexDisplay.NamingContainer;
var value = DataBinder.Eval(container.DataItem, DataField);
if (value == null)
{
return;
}
complexDisplay.Text = value.ToString();
}
protected override DataControlField CreateField()
{
return new ComplexBoundField();
}
}
As you can see, in the DataBinding method, we simply cast the sender to the Literal we defined in the InitializeCell method and find its naming container. This allows us to grab the row's DataItem which holds information specific to the Customer for each row. Finally, we set the literal's Text property to the result of calling DataBinder.Eval on the container's DataItem using the DataField we have set in the gridview declaration. Finally, we need to update the gridview declaration to show our new complex field:
<asp:GridView ID="uxCustomerView" runat="server" CssClass="grid" Width="90%" GridLines="None"
AutoGenerateColumns="false" DataKeyNames="CustomerID" RowStyle-CssClass="viewRow"
OnRowEditing="uxCustomerView_RowEditing" OnRowDeleting="uxCustomerView_RowDeleting">
<Columns>
<asp:BoundField DataField="FirstName" HeaderText="First Name" />
<asp:BoundField DataField="LastName" HeaderText="Last Name" />
<asp:BoundField DataField="Title" HeaderText="Title" />
<asp:BoundField DataField="Company" HeaderText="Company" />
<asp:BoundField DataField="Address1" HeaderText="Address" />
<asp:BoundField DataField="City" HeaderText="City" />
<lowrymedia:ComplexBoundField DataField="State.Name" HeaderText="State"/>
<asp:BoundField DataField="PostalCode" HeaderText="Postal Code" />
</Columns>
</asp:GridView>
Everything works as expected! Hopefully, this will help someone else with the problem I had.
* Disclaimer: if you need to use other functionality such as sorting, you may have to implement the code yourself when using this class. Just crack open Reflector and figure out how they handled each use case.