NOTE: This article is one of a three-part series based on the same code. As such, some of the code in the referenced download is not specifically targeted at this topic. The other two topics are Declarative Databinding of Nested Object Properties to GridView Columns and Dynamic Rendering of Images in ASP.NET.
Another NOTE: The code provided has been written to use the AdventureWorks database for SQL 2005, which can be found on Codeplex here.
In my previous post, I discussed being able to bind GridView columns directly to properties of nested objects returned via an ObjectDataSource. This time around, I'm going to take this a step further and dive into sorting, another very common bit of functionality that we often see used on GridViews.
The business objects in my sample code leverage the generic List<> class to make it a snap to create collection classes. So, for example, I have a Product class, and also a ProductCollection class, which inherits from List<Product>. The ProductCollection class contains a single method called GetAllProducts, which returns a ProductCollection that we can then bind to our GridView.
The only problem with using this generic List<> approach with an ObjectDataSource is that they are not inherently sortable. In fact, if you just enable sorting on the GridView, you would likely get an error similar to "The data source 'ods_allProducts' does not support sorting with IEnumerable data. Automatic sorting is only supported with DataView, DataTable, and DataSet." Un-awesome.
To make life simpler, what we really would like to have is an easy way to make it so that our List<> objects were just magically delicious (and by delicious, I mean sortable), from the perspective of the GridView. And this is where our GenericComparer comes in. By leveraging the power of Generics, with some of the magic of Reflection sprinkled in, our otherwise bland List<> becomes much tastier.
To make use of this magic, there are a trio of things that need to be in place. First, on the ObjectDataSource, we need to make use of the SortParameterName property. By providing a value here, the GridView can pass a sort expression to the ObjectDataSource, which can in turn pass it along to the underlying SelectMethod. This of course means that thing number 2 is that our method needs to take in a sortExpression parameter, and also be able to make use of it. Thing number 3 is our GenericComparer class, for which we will see some code shortly.
Here is code for each of the three pieces mentioned above. First, the ObjectDataSource and GridView controls:
<asp:ObjectDataSource
ID="ods_allProducts"
runat="server"
SelectMethod="GetAllProducts"
TypeName="Aptera.BlogSamples.AdventureWorks.Production.ProductCollection"
SortParameterName="sortExpression"
>
</asp:ObjectDataSource>
<asp:GridView
ID="gv_allProducts"
runat="server"
AutoGenerateColumns="False"
DataSourceID="ods_allProducts"
AllowPaging="True"
AllowSorting="True">
<Columns>
<asp:TemplateField HeaderText="Id" SortExpression="ProductID">
<ItemTemplate>
<asp:Label ID="Label1" runat="server"
Text='<%# Bind("ProductId") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Name" SortExpression="Name">
<ItemTemplate>
<asp:Label ID="Label2" runat="server"
Text='<%# Bind("Name") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Product Number" SortExpression="ProductNumber">
<ItemTemplate>
<asp:Label ID="Label3" runat="server"
Text='<%# Bind("ProductNumber") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="List Price" SortExpression="ListPrice">
<ItemTemplate>
<asp:Label ID="Label4" runat="server"
Text='<%# Bind("ListPrice", "{0:C}") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Size" SortExpression="Size">
<ItemTemplate>
<asp:Label ID="Label5" runat="server"
Text='<%# Bind("Size") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Size Unit" SortExpression="SizeUnitMeasureCode">
<ItemTemplate>
<asp:Label ID="Label6" runat="server"
Text='<%# Bind("SizeUnitMeasure.Name") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Weight" SortExpression="Weight">
<ItemTemplate>
<asp:Label ID="Label7" runat="server"
Text='<%# Bind("Weight") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Weight Unit" SortExpression="WeightUnitMeasureCode">
<ItemTemplate>
<asp:Label ID="Label8" runat="server"
Text='<%# Bind("WeightUnitMeasure.Name") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Category" SortExpression="ProductCategory">
<ItemTemplate>
<asp:Label ID="Label9" runat="server"
Text='<%# Bind("ProductSubcategory.ProductCategory.Name") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="SubCategory" SortExpression="ProductSubcategory">
<ItemTemplate>
<asp:Label ID="Label9" runat="server"
Text='<%# Bind("ProductSubcategory.Name") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Photo">
<ItemTemplate>
<img src='GetThumbnailPhoto.aspx?ProductPhotoID=<%# Eval("PrimaryPhoto.ProductPhotoID") %>' />
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
Next, here is the code for the ProductCollection class, showing how the GetAllProducts method takes in a sortExpression parameter, and how it is used to invoke the GenericComparer.
public class ProductCollection : List<Product>
{
public ProductCollection GetAllProducts(string sortExpression)
{
ProductCollection coll = new ProductCollection();
string query =
"select ProductID, Name, ProductNumber, " +
"ListPrice, Size, SizeUnitMeasureCode, " +
"WeightUnitMeasureCode, Weight, " +
"ProductSubcategoryID from Production.Product";
DataSet ds = SqlHelper.ExecuteDataset(
ConfigurationManager.ConnectionStrings["AdventureWorksConn"].ConnectionString,
CommandType.Text, query);
if (ds.Tables[0].Rows.Count == 0)
{
throw new Exception("No Production.Product records found.");
}
else
{
foreach (DataRow dr in ds.Tables[0].Rows)
{
coll.Add(new Product(dr));
}
}
ds.Dispose();
if (sortExpression != "")
{
Aptera.BlogSamples.GenericComparer.GenericComparer<Product>.SortCollection(coll, sortExpression);
}
return coll;
}
}
And last, but most certainly not least, is the code for the GenericComparer class itself.
public class GenericComparer<T> : IComparer<T>
{
private SortDirection _sortDirection;
private string _sortExpression;
public SortDirection SortDirection
{
get { return _sortDirection; }
set { _sortDirection = value; }
}
public GenericComparer(string sortExpression, SortDirection sortDirection)
{
_sortExpression = sortExpression;
_sortDirection = sortDirection;
}
public int Compare(T x, T y)
{
PropertyInfo propertyInfo = typeof(T).GetProperty(_sortExpression);
IComparable obj1 = (IComparable)propertyInfo.GetValue(x, null);
IComparable obj2 = (IComparable)propertyInfo.GetValue(y, null);
if (_sortDirection == SortDirection.Ascending)
{
return obj1.CompareTo(obj2);
}
else
{
return obj2.CompareTo(obj1);
}
}
public static List<T> SortCollection(List<T> collection, string sortExpression)
{
string[] exp = sortExpression.Split(" ".ToCharArray());
string prop = exp[0];
string dir;
GenericComparer<T> comp;
if (exp.Length == 2)
{
dir = exp[1];
}
else{
dir = "ASC";
}
if (dir == "ASC")
{
comp = new GenericComparer<T>(exp[0], SortDirection.Ascending);
}
else
{
comp = new GenericComparer<T>(exp[0], SortDirection.Descending);
}
collection.Sort(comp);
return collection;
}
}
Finally, in parting, I feel that it is necessary to mention that I did not come up with the GenericComparer all on my own. Rather, I started with some code that I found somewhere on the internet (sorry, I don't remember where I first found it, but I it can be found in several places), and added some additional functionality to it.
To download a Visual Studio 2008 solution containing all of the code to demonstrate this, click here. As mentioned above, this solution contains code that also demonstrates two other topics, which are covered in other posts.