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!

3 comments:

MytyMyky said...

Working on an interesting way go get all that refactoring made easy. (Lessons learned...). The solution i've been currently working on is a DSL and T4 templates. I generate the tables in the DSL (data tables, type and enum). Create the SQL, BO, DAL, BLL and WS classes and methods automaticly (and using interfaces templates and a doubled derived inheritance scheme, generate DAL classes for diferent database systems - currently Postgre and SQL server. Hope to get SQLite in soon). Customization is done where needed without being afected by the autogenerated code. ALOT (but not all) of the project gets built very quickly. And repitive tasks gets automated.

After that just customize when needed. Results in a fastast CRUD prototyping and a better idea of what needs to be customized and done next. Even gridbased user controls and what not can be easily generated.

On a parralel project, I've got javascript objects being prepared through the transforms for a more jquery & ajax based UI. (since it's a personal project, wanna try moving an alternative to the server control model, as a test).

Quando apareceres por terras lusas, vê lá se passas cá no escritório (que já n é o anexo da casa, para já), e vês esta coisa a funcionar :D

Joaquim Rendeiro said...

Nice to hear about the generation of all that stuff, JS output and new office! I suppose that means business is going well :D

Tiago Teixeira said...

Olá Joaquim,

Gracias pelo post. Foi-me muito util.
Este objectdatasource ate se perceber bem ( para quem é newbie em ASP.NET) tem que se lhe diga... ou talvez não estivesse muito inspirado no dia em que precisei de o utilizar pela primeira vez :)

Bom blog gostei!

Uma abraco de um vizinho tuga, na Noruega.

Tiago Teixeira