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.
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:
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):
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:
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:
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.