2007-12-30

Easily sorting an ASP.NET GridView binded to an ObjectDataSource

Last summer, I was involved in building a complex, multi-tiered application in ASP.NET. It has the usual data, business logic and presentation layers, together with some additional modules. Every layer and module exposes a webservice and that's how it's all wired up. The project had all the data needs and operations very well specified and translated into stored procedures. The data layer is abstracted by business objects, that are passed from one service to another and finally to the presentation layer. Very enterprise-ish, wouldn't you say?

When the system was designed it was assumed that the presentation layer, or in other words, the ASP.NET pages, could easily paginate, order, and do other presentation transforms to the lists of business objects returned by the lower layer. The thing is, it was not so simple. More about that in a moment.

ASP.NET 2.0 has evolved in terms of data visualization and interaction, providing easy-to-use but also powerful controls like the GridView and FormView, just to name two of the most popular. There has also been some improvement in terms of integration with different sources of data other than SQL, namely XML and business objects / data objects. For the latter, the ObjectDataSource control, we can provide a type and a method to retrieve data and the wire it with a GridView to display some data. Very nice and easy, about 12 mouse clicks total. Now rinse, repeat in as many pages as you need.

Oh wait, we need sorting and pagination. Well, pagination is implemented by default, no problems there.
«Sorting, let me see... Oh, here is the AllowSorting property, let me just set this to true. It's don... Oh fsck!»

This was aproximately my mental monologue some months ago when asked to add sorting to a GridView and encountering a nasty error. A quick Google-ing leads me to a Microsoft MSDN page that explains what must be done in order to add sorting capabilities to an ObjectDataSource. Long story short, it was necessary to create an overload to every method that returns data, with two additional parameters (SortExpression and SortDirection, if I recall correctly).

We were on a pretty tight schedule to present a prototype to the client, so there was not much time to go around modifying every method to include sorting support. And there definitely wasn't enough time to traverse all the layers so that sorting was introduced at the stored procedures level, which is where it belonged in the first place!

When push comes to shove, it's time for "Reflection and XORs".
(I love this expression! It's used by some friends of mine to express unnecessary complexity of design and/or implementation.)

My quick (and a little dirty) solution to this problem – which I've already shared with some people and might be useful for others, so that's why I'm making this post – was to create a user control that "catches" the sorting events on the GridView and transforms the data coming from the ObjectDataSource so that it reaches the grid sorted as expected. The downside is that in the headings of the table there are no visual cues to what is being sorted or in what direction.

I didn't have to use XORs, but I sure used a lot of reflection, because I didn't know beforehand what type of objects and which properties I would be sorting. It was also interesting to explore the reflection world of generics, particularly creating a generic delegate in run-time!

Here is the most interesting method in the code:

protected void DataSource_Selected(object sender, ObjectDataSourceStatusEventArgs e) {
if(e.ReturnValue != null) {
if(sortExpression != null && sortExpression != string.Empty) {
MethodInfo sort = new List(e.ReturnValue.GetType().GetMethods()).Find(
delegate(MethodInfo m) { return m.ToString().ToLower().StartsWith("void sort(system.comparison"); });
if(sort != null) {
object helper = Activator.CreateInstance(
typeof(ComparisonDelegateHelper<>).MakeGenericType(sort.GetParameters()[0].ParameterType.GetGenericArguments()[0]));
MethodInfo generateComparisonDelegate = new List(helper.GetType().GetMethods()).Find(
delegate(MethodInfo m) { return m.ToString().Contains("GenerateComparisonDelegate"); });
sort.Invoke(e.ReturnValue,
new object[] {generateComparisonDelegate.Invoke(helper, new object[] {sortExpression, sortAscending})});
}
}
}
}
If it comes in handy or you're just curious, download the whole thing.

The usage of the GridViewSorter control is as simple as this:
<gv:GridViewSorter runat="server" ID="gvs" EnableViewState="true"
GridViewID="SomeGridView" DataSourceID="SomeDataSource">
</gv:GridViewSorter>

Temporary variables regarding the last sorted field and direction are kept on the viewstate, but could also be as hidden fields on the form or even in the session (the latter would have to take care of collisions with other pages or even the same page open in two browser windows).


Hope your Christmas was good. Happy New Year!