Custom Object Binding to a GridView...
Larger-scale applications that create custom collections can still make use of the .NET framework controls in
ASP.NET applications. With a little extra work, the gridview control can still take control of all the functionality
it uses while binding to a complex collection object.
By: Brian Mains
Date: May 14, 2006
Download the code.
Printer Friendly Version
In this article, I'm going to look at the aspects of binding a custom collection to the GridView control. It isn't
as easy as binding a DataSet, DataTable, or DataView object, but is still possible. It is good to understand how the
GridView works with custom objects because most enterprise applications use complex objects to represent their data,
rather than only objects in the .NET framework. We are going to look at such a system, which has two complex
objects, Customers and Orders. I will also illustrate how sorting works with a custom collection. Each of these
objects exposes the following properties:
- Customers
- ID – A numerical ID value representing the customer
- Name – The name of the customer
- Order
- ID – A numerical ID value representing the order
- Cost – The cost of the order
- PurchaseDate – The date the order was purchased
- Customer – A reference to the customer object who made the order
The OrderCollection class contains all of the orders made, with some helper functions to search the collection for a
specific order. Most of the functions are straightforward, and I won’t touch upon here. But I will talk about the
sorting methods, which I will show later. First, I have to setup what the example code does.
The page uses the Atlas framework to handle the server-side postbacks, which it uses built-in scripting capabilities
available with the ScriptManager to do this, thus making the client richer. But the real reason for using this is
the timer control. The timer control posts back at a fixed interval to add a randomly-created order to the list.
This illustrates the sorting capabilities of the collection, plus one other feature. The GridView control has
EnableViewState set to false, which means it isn’t storing the data in the viewstate upon posting back. This
makes posting back quicker for larger data sets. Only when the cache object expires is it reloaded. Caching, in
this example, uses the Microsoft Enterprise Library Caching Block; this won’t be discussed in this article. But in
the example, it is pretty simple to see how the code works. Under normal circumstances, I would be getting the data
from a database, and upon cache expiration, refreshing the data, but static data is used to illustrate the example
and for ease of use in creating it.
The GridView control appears below (the UpdatePanel is omitted):
<asp:GridView ID="gvwOrders" runat="server" AutoGenerateColumns="False" CellPadding="4" ForeColor="#333333" GridLines="None" AllowSorting="true" OnSorting="gvwOrders_Sorting" EnableViewState="false">
<FooterStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" />
<Columns>
<asp:TemplateField HeaderText="Customer ID" SortExpression="Customer.ID">
<ItemTemplate>
<asp:Label ID="lblCustomerID" runat="server" Text='<%# ((Customer)Eval("Customer")).ID %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Customer Name" SortExpression="Customer.Name">
<ItemTemplate>
<asp:Label ID="lblCustomerName" runat="server" Text='<%# ((Customer)Eval("Customer")).Name %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Order ID" SortExpression="ID">
<ItemTemplate>
<asp:Label ID="lblOrderID" runat="server" Text='<%# Eval("ID") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Cost" SortExpression="Cost">
<ItemTemplate>
<asp:Label ID="lblCost" runat="server" Text='<%# Eval("Cost") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Purchase Date" SortExpression="PurchaseDate">
<ItemTemplate>
<asp:Label ID="lblPurchaseDate" runat="server" Text='<%# ((DateTime)Eval("PurchaseDate")).ToShortDateString() %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
</Columns>
<RowStyle BackColor="#EFF3FB" />
<EditRowStyle BackColor="#2461BF" />
<SelectedRowStyle BackColor="#D1DDF1" Font-Bold="True" ForeColor="#333333" />
<PagerStyle BackColor="#2461BF" ForeColor="White" HorizontalAlign="Center" />
<HeaderStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" />
<AlternatingRowStyle BackColor="White" />
</asp:GridView>
The key is to notice how the data bindings take place. TemplateFields have to be used to get the values from the
data source (the Order object) in the OrderCollection class (BoundField’s do not work correctly in this scenario.)
Each item pulls the data from the Order object. When you want to access complex properties, however, you have to
convert the object reference returned from Eval() to the appropriate type. In the grid, I have customer information,
accessed from the customer property of the order class. Look at the binding statement below:
<%# ((Customer)Eval("Customer")).ID %>
<%# ((Customer)Eval("Customer")).Name %>
These are the statements to access child properties for a complex object; note that you don’t need to convert it to
the appropriate type if option explicit is off. Also note that a null customer property value will cause problems
with this approach. Using a local page or static method to handle the rendering can make this approach work. What
I mean by that is a method similar to Eval(); Eval is a method in the Page class; a custom method in your code-behind
page could handle getting the customer object, making sure it isn't null, and if it is, creating a dummy customer
object. Or it could output the property directly, so if it is null, return a default value. There are several ways
to approach it.
When the timer interval elapses, the Tick event fires, and the following code is run. This code gets the
OrderCollection object from the cache, randomly get an existing customer from an existing order, create a new order
with static information, and add the updated collection back to the cache. The code is showing with documentation
at each step.
protected void tmeUpdate_Tick ( object sender, EventArgs e )
{
OrderCollection orders = _cache.GetData ( "DATA" ) as OrderCollection;
//Get a random customer from the current orders listing
int value = ( ( new Random ( ).Next ( ) ) % orders.Count - 2 ) + 1;
Customer customer = orders[ value ].Customer;
//Add the order to the collection
orders.Add ( new Order ( 324543, 44.34, DateTime.Today, customer ) );
//Update the cache with the new item
_cache.Add ( "DATA", orders, CacheItemPriority.High, null, null );
}
When the page runs, you will see a new order added to the GridView every five seconds. You will also be able to
sort it. To do this, I have a SortByProperty method in the OrderCollection class that receives a string value
denoting the property, and a Boolean stating whether it is sorting ascending. The code for this is below; note
that whenever overriding the Sorting method for the GridView class (shown later), the sort direction enumeration
always had a value of ascending, which I’m not sure as to why. To counter this, if the property name and ascending
value are exactly the same as previously, the sorting direction is reversed. If not (remember that the enumeration,
after reversing, it won’t match a value of ascending), then set the new properties to the local variables, and call
the Compare method to sort. Note that the Compare method handles the sorting operation, which is why private
variables are needed to store the property name to sort by and the direction. Also, this method uses the Sort
method defined for the List base class, which, using the Comparison object, the delegate is fired for each item to
compare in the list. It is an easier way to do the sort.
public void SortByProperty ( string propName, bool ascending )
{
//Flip the properties if the parameters are the same
if (_propName == propName && _ascending == ascending)
_ascending = !ascending;
//Else, new properties are set with the new values
else
{
_propName = propName;
_ascending = ascending;
}
//Use the compare method with IComparer
this.Sort ( new Comparison<Order> ( Compare ) );
}
One thing you may have not seen is that the property name comes from the Sorting event's GridViewSortingEventArgs,
which is the value in the SortExpression for the template fields above. These values could be any string value, and
I duplicated them here:
<asp:TemplateField HeaderText="Customer ID" SortExpression="Customer.ID">
<asp:TemplateField HeaderText="Customer Name" SortExpression="Customer.Name">
<asp:TemplateField HeaderText="Order ID" SortExpression="ID">
<asp:TemplateField HeaderText="Cost" SortExpression="Cost">
<asp:TemplateField HeaderText="Purchase Date" SortExpression="PurchaseDate">
Note that the values, even though they look like they match specific property names, can be whatever you want to
assign them to. I used similar constructs to their actual values for ease of use. These values are used in the
IComparer<Order>.Compare() method to sort the collection based on the string values above.
public int Compare ( Order x, Order y )
{
int i;
switch (_propName.ToLower ( ))
{
case "customer.id":
i = x.Customer.ID.CompareTo ( y.Customer.ID );
if (i == 0) i = x.ID.CompareTo ( y.ID );
break;
case "customer.name":
i = x.Customer.Name.CompareTo ( y.Customer.Name );
if (i == 0) i = x.ID.CompareTo ( y.ID );
break;
case "id":
i = x.ID.CompareTo ( y.ID );
break;
case "cost":
i = x.Cost.CompareTo ( y.Cost );
if (i == 0) i = x.PurchaseDate.CompareTo ( y.PurchaseDate );
break;
case "purchasedate":
i = x.PurchaseDate.CompareTo ( y.PurchaseDate );
if (i == 0) i = x.Cost.CompareTo ( y.Cost );
break;
default:
//To let me know of the lack of coverage
throw new NotImplementedException ( "The sort doesn't implement this property yet: " + _propName );
}
//If in descending mode, reverse the -1 to 1 and vice versa
if (!_ascending)
i = 0 - i;
return i;
}
Several things to note here. First, to do the comparison, you have to compare objects that are IComparable or
compare a simple property that does have this method. For instance, string, int, DateTime, bool, etc. all have
a CompareTo method that returns the right integer value based on which value is greater or lesser than the other.
These simple properties are used to determine the sort. For instance, if the ordering was done by customer ID, you
compare the x.Customer.ID property against y.Customer.ID value, which does an integer comparison. Also note that I
have several "if (i == 0)" statements above, then have a second comparison statement. Zero means the values are
equal. If equal, I want to do a secondary sort. The reason I do is if I don’t, whenever you sort by the customer.ID
field, it will sort the other values in any random way, shape, and form, and will vary every time your sort. This
makes the sorting more understandable by first sorting by customer.ID, then by order.ID.
For this sort to work, the SortByProperty must be called, instead of calling Sort directly. Next, the sorting has
to be handled in the web page. The Sorting event calls the SortByProperty, passing in GridViewSortEventArgs
e.SortExpression value, and converting e.SortDirection into a Boolean expression. Whenever a modification is made
to the collection, it must be added back to the cache, then bound to the gridview.
protected void gvwOrders_Sorting ( object sender, GridViewSortEventArgs e )
{
OrderCollection orders = _cache.GetData ( "DATA" ) as OrderCollection;
//Must call sortbyproperty to work right
orders.SortByProperty ( e.SortExpression, ( e.SortDirection == SortDirection.Ascending ) );
//Add the sorted collection back to the cache
_cache.Add ( "DATA", orders, CacheItemPriority.High, null, null );
//Rebind the gridview
this.gvwOrders.DataSource = orders;
this.gvwOrders.DataBind ( );
}
If you try out the example, it will keep creating records until you close it down. You can sort ascending or
descending based on any of the fields. Note that, if you use the example, the Cache.GetData() method uses a
static key, which each user has to have a custom unique key. If you are using windows authentication, the
User.Identity.Name can be appended to the key to ensure uniqueness; if using forms authentication, the
Session.SessionID value can be used to ensure uniqueness. Note with caching, when you go back to the page,
the cache may still exist with all of the entries randomly generated, so you may still have the last subset of data.