DataTablePager Now Has Multi-Column Sort Capability For DataTables.Net February 9, 2011
Posted by ActiveEngine Sensei in .Net, ActiveEngine, Ajax, ASP.Net, C#, DataTables.Net, jQuery.Tags: ActiveEngine, C#, DataTables.Net, jQuery, JSON.Net, server-side paging
trackback
Some gifts just keep on giving, and many times things can just take on a momentum that grow beyond your expectation. Bob Sherwood wrote to Sensei and pointed out that DataTables.net supports multiple column sorting. All you do is hold down the shift key and click on any second or third column and DataTables will add that column to sort criteria. ”Well, how come it doesn’t work with the server side solution?” Talk about the sound of one hand clapping. How about that for a flub! Sensei didn’t think of that! Then panic set in – would this introduce new complexity to the DataTablePager solution, making it too difficult to maintain a clean implementation? After some long thought it seemed that a solution could be neatly added. Before reading, you should download the latest code to follow along.
How DataTables.Net Communicates Which Columns Are Involved in a Sort
If you recall, DataTables.Net uses a structure called aoData to communicate to the server what columns are needed, the page size, and whether a column is a data element or a client side custom column. We covered that in the last DataTablePager post. aoData also has a convention for sorting:
bSortColumn_X=ColumnPosition
In our example we are working with the following columns:
,Name,Agent,Center,,CenterId,DealAmount
where column 0 is a custom client side column, column 1 is Name (a mere data column), column 2 is Center (another data column), column 3 is a custom client side column, and the remaining columns are just data columns.
If we are sorting just by Name, then aoData will contain the following:
bSortColumn_0=1
When we wish to sort by Center, then by Name we get the following in aoData”
bSortColumn_0=2
bSortColumn_1=1
In other words, the first column we want to sort by is in position 2 (Center) and the second column(Name) is in position 1. We’ll want to record this some where so that we can pass this to our order routine. aoData passes all column information to us on the server, but we’ll have to parse through the columns and check to see if one or many of the columns is actually involved in a sort request and as we do we’ll need to preserve the order of that column of data in the sort.
SearchAndSortable Class to the Rescue
You’ll recall that we have a class called SearchAndSortable that defines how the column is used by the client. Since we iterate over all the columns in aoData it makes sense that we should take this opportunity to see if any column is involved in a sort and store that information in SearchAndSortable as well. The new code for the class looks like this:
public class SearchAndSortable
{
public string Name { get; set; }
public int ColumnIndex { get; set; }
public bool IsSearchable { get; set; }
public bool IsSortable { get; set; }
public PropertyInfo Property{ get; set; }
public int SortOrder { get; set; }
public bool IsCurrentlySorted { get; set; }
public string SortDirection { get; set; }
public SearchAndSortable(string name, int columnIndex, bool isSearchable,
bool isSortable)
{
this.Name = name;
this.ColumnIndex = columnIndex;
this.IsSearchable = isSearchable;
this.IsSortable = IsSortable;
}
public SearchAndSortable() : this(string.Empty, 0, true, true) { }
}
There are 3 new additions:
IsCurrentlySorted - is this column included in the sort request.
SortDirection - “asc” or “desc” for ascending and descending.
SortOrder - the order of the column in the sort request. Is it the first or second column in a multicolumn sort.
As we walk through the column definitions, we’ll look to see if each column is involved in a sort and record what direction – ascending or descending – is required. From our previous post you’ll remember that the method PrepAOData is where we parse our column definitions. Here is the new code:
// Sort columns
this.sortKeyPrefix = aoDataList.Where(x => x.Name.StartsWith(INDIVIDUAL_SORT_KEY_PREFIX))
.Select(x => x.Value)
.ToList();
// Column list
var cols = aoDataList.Where(x => x.Name == "sColumns"
& string.IsNullOrEmpty(x.Value) == false)
.SingleOrDefault();
if(cols == null)
{
this.columns = new List();
}
else
{
this.columns = cols.Value
.Split(',')
.ToList();
}
// What column is searchable and / or sortable
// What properties from T is identified by the columns
var properties = typeof(T).GetProperties();
int i = 0;
// Search and store all properties from T
this.columns.ForEach(col =>
{
if (string.IsNullOrEmpty(col) == false)
{
var searchable = new SearchAndSortable(col, i, false, false);
var searchItem = aoDataList.Where(x => x.Name == BSEARCHABLE + i.ToString())
.ToList();
searchable.IsSearchable = (searchItem[0].Value == "False") ? false : true;
searchable.Property = properties.Where(x => x.Name == col)
.SingleOrDefault();
searchAndSortables.Add(searchable);
}
i++;
});
// Sort
searchAndSortables.ForEach(sortable => {
var sort = aoDataList.Where(x => x.Name == BSORTABLE + sortable.ColumnIndex.ToString())
.ToList();
sortable.IsSortable = (sort[0].Value == "False") ? false : true;
sortable.SortOrder = -1;
// Is this item amongst currently sorted columns?
int order = 0;
this.sortKeyPrefix.ForEach(keyPrefix => {
if (sortable.ColumnIndex == Convert.ToInt32(keyPrefix))
{
sortable.IsCurrentlySorted = true;
// Is this the primary sort column or secondary?
sortable.SortOrder = order;
// Ascending or Descending?
var ascDesc = aoDataList.Where(x => x.Name == "sSortDir_" + order)
.SingleOrDefault();
if(ascDesc != null)
{
sortable.SortDirection = ascDesc.Value;
}
}
order++;
});
});
To sum up, we’ll traverse all of the columns listed in sColumns. For each column we’ll grab the PorpertyInfo from our underlying object of type T. This gives only those properties that will be displayed in the grid on the client. If the column is marked as searchable, we indicate that by setting the IsSearchable property on the SearchAndSortable class. This happens starting at line 28 through 43.
Next we need to determine what we can sort, and will traverse the new list of SearchAndSortables we created. DataTables will tell us what if the column can be sorted by with following convention:
bSortable_ColNumber = True
So if the column Center were to be “sortable” aoData would contain:
bSortable_1 = True
We record the sortable state as shown on line 49 in the code listing.
Now that we know whether we can sort on this column, we have to look through the sort request and see if the column is actually involved in a sort. We do that by looking at what DataTables.Net sent to us from the client. Again the convention is to send bSortColumn_0=1 to indicate that the first column for the sort in the second item listed in sColumns property. aoData will contain many bSortColum’s so we’ll walk through each one and record the order that column should take in the sort. That occurs at line 55 where we match the column index with the bSortColumn_x value.
We’ll also determine what the sort direction – ascending or descending – should be. At line 63 we get the direction of the sort and record this value in the SearchAndSortable.
When the method PrepAOData is completed, we have a complete map of all columns and what columns are being sorted, as well as their respective sort direction. All of this was sent to us from the client and we are storing this configuration for later use.
Performing the Sort
(Home stretch so play the song!!)If you can picture what we have so far we just basically created a collection of column names, their respective PropertyInfo’s and have recorded which of these properties are involved in a sort. At this stage we should be able to query this collection and get back those properties and the order that the sort applies.
You may already be aware that you can have a compound sort statement in LINQ with the following statement:
var sortedCustomers = customer.OrderBy(x => x.LastName)
.ThenBy(x => x.FirstName);
The trick is to run through all the properties and create that compound statement. Remember when we recorded the position of the sort as an integer? This makes it easy for us to sort out the messy scenarios where the second column is the first column of a sort. SearchAndSortable.SortOrder takes care of this for us. Just get the data order by SortOrder in descending order and you’re good to go. So that code would look like the following:
var sorted = this.searchAndSortables.Where(x => x.IsCurrentlySorted == true)
.OrderBy(x => x.SortOrder)
.ToList();
sorted.ForEach(sort => {
records = records.OrderBy(sort.Name, sort.SortDirection,
(sort.SortOrder == 0) ? true : false);
});
On line 6 in the code above we are calling our extension method OrderBy in Extensions.cs. We pass the property name, the sort direction, and whether this is the first column of the sort. This last piece is important as it will create either “OrderBy” or the “ThenBy” for us. When it’s the first column, you guessed it we get “OrderBy”. Sensei found this magic on a StackOverflow post by Marc Gravell and others.
Here is the entire method ApplySort from DataTablePager.cs, and note how we still check for the initial display of the data grid and default to the first column that is sortable.
private IQueryable ApplySort(IQueryable records)
{
var sorted = this.searchAndSortables.Where(x => x.IsCurrentlySorted == true)
.OrderBy(x => x.SortOrder)
.ToList();
// Are we at initialization of grid with no column selected?
if (sorted.Count == 0)
{
string firstSortColumn = this.sortKeyPrefix.First();
int firstColumn = int.Parse(firstSortColumn);
string sortDirection = "asc";
sortDirection = this.aoDataList.Where(x => x.Name == INDIVIDUAL_SORT_DIRECTION_KEY_PREFIX + "0")
.Single()
.Value
.ToLower();
if (string.IsNullOrEmpty(sortDirection))
{
sortDirection = "asc";
}
// Initial display will set order to first column - column 0
// When column 0 is not sortable, find first column that is
var sortable = this.searchAndSortables.Where(x => x.ColumnIndex == firstColumn)
.SingleOrDefault();
if (sortable == null)
{
sortable = this.searchAndSortables.First(x => x.IsSortable);
}
return records.OrderBy(sortable.Name, sortDirection, true);
}
else
{
// Traverse all columns selected for sort
sorted.ForEach(sort => {
records = records.OrderBy(sort.Name, sort.SortDirection,
(sort.SortOrder == 0) ? true : false);
});
return records;
}
}
It’s All in the Setup
Test it out. Hold down the shift key and select a second column and WHAMO – multiple column sorts! Hold down the shift key and click the same column twice and KAH-BLAMO multiple column sort with descending order on the second column!!!
The really cool thing is that our process on the server is being directed by DataTables.net on the client. And even awseomer is that you have zero configuration on the server. Most awesome-est is that this will work with all of your domain objects, because we have used generics we can apply this to any class in our domain. So what are you doing to do with all that time you just got back?


Subscribe to this Blog!
Great stuff, do you have the dll you are using? I cannot compile it because you are using a namespace i dont have or something in that matter
never mind figured it, great stuff
I have been struggling to implement datatables with asp.net web forms. finally i found your post the solution worked .
But i still have performance issue to fetch only 80 records it taked a lot of time. and when you sort it again take a lot of time.
I switched serverside loading using datatables due to performance reason only. But it has become worst. Earlier it consume time once at the time of loading after that search/ sorting work very smoothly.
now it takes a lot time for each operation.
The dataset provded with sample took no time at all.I have found when i create my IQueryable object from database there it take major time.
Please suggest something to improve performance.
First of all Amrit, thanks for reading my blog. Hopefully I can be of help to you.
Let’s start with the database. How many records are you pulling back and converting to IQueryable? Do you need to go to the database each time. or can you cache the recordset as IQueryable and perform a query against the cached record set? My design is essentially going against a repository of some type. However, in production I have used this technique against a 6000 record database and the performance has been acceptable. For anything larger my approach is probably not optimal.
If you can’t cache the results and have a large collection of objects we may have to take a different approach. At the DataTables site there is a solution for building a pass through SQL statement such as:
strRequeteA += “SELECT * FROM (”
strRequeteA += “SELECT TOP ”
strRequeteA += Request(“iDisplayLength”).ToString
strRequeteA += ” * FROM (”
strRequeteB_1 = “SELECT ”
strRequeteB_2 = “TOP ” + (Convert.ToInt32(Request(“iDisplayStart”)) + Convert.ToInt32(Request(“iDisplayLength”))).ToString
strRequeteB_3 += ” id, engine, browser, platform, version, grade ”
strRequeteB_3 += “FROM ajax ”
Here is the link. You’ll want to read through that document to get the idea. But before you give up on the IQueryable approach I’d do some bench marking against your database. If you have to use a grid against 100,000 records, then it’s best to use the SQL technique and let SQL do what it does best. If you’re at 10,000 records you should examine your retrieval speed from the database and find your bottleneck.
Another tactic that you may take is to use the pipeline feature in DataTables.Net. The documentation is here. This allows you to fetch 5 pages of data, but will only display the first page. When you perform paging and other operations, the cached pages are used first, then the request is sent to the server. So in your case, if fetching 400 records takes as long as fetching 80, fetch 400, display the first 80 and you are making less trips to the database.
I hope this helps. I’d focus on the database first and if you do indeed have a large record set to work with, try the SQL statement approach. Let me know how it turns. I have been wondering if I needed to write a SQL Statement generator for the DataTablesPager solution. Any feedback could help towards that goal. Good luck.
Hi and thanks
I forget to mention in earlier comment.
I have figured out performance issue.
It is decent now. but when i try use search it gives null parameter name error.
and i tried use fnRender function to render a hyperlink to delete record but it does not show row id in fnRender.
Great news that you found your performance issue. As I said, there is a limit to my solution. Hopefully you haven’t reached that upper limit yet with your system.
As to your error with searching I have found that the expression tree will error when you have a null value in the record. I haven’t had the time to work as conversion of null to string.empty in the lambda expression, but as a work around I replace the null in the record set with string.empty before I apply the pager solution. It drove me crazy for a while trying to figure it out. To ensure that this is indeed the case I wold eliminate columns from your grid and see if it renders.
With respect to fnRender I’ll have to ponder a bit. Can you see the column with the appropriate row id if you do NOT use fnRender? That could be a place to start.
On a different note, I noticed that you replied at 3 am Eastern Standard Time. I hope you are in a different time – I’d hate to think that you are crazy like I am and work all sorts of hours!
In the GenericSearchFilter() expression change the LINQ in the for loop to this:
var propertyQuery = (from property in filterProperties
let toStringMethod = Expression.Call(
Expression.Call(Expression.Coalesce(Expression.Property(paramExpression, property), Expression.Constant(new object())), convertToString, null),
typeof(string).GetMethod(“ToLower”, new Type[0]))
select Expression.Call(toStringMethod, typeof(string).GetMethod(“Contains”), searchExpression)).ToArray();
The coalesce will take a null value and put in an empty object.
Awesome – thanks. I had struggled over this and set the concern aside in hopes that someone would point out a solution
.