# Thursday, June 26, 2008

Binding to a GridView

ASP.Net data binding is a great technology for developers who want to create interactive web applications without writing a lot of code. For example, a developer can drop a GridView and a DataObject onto a web form, set a few properties and have a fully editable grid, allowing users to easily view and update data. 

Even sorting the grid data can be performed without writing any code.  Just set the GridView's AllowSorting property to "true" and set the SortExpression property of any "sortable" column. 

<asp:GridView
   ID="GridView1"
   runat="server"
   AllowSorting="true"

...

   <asp:BoundField
      DataField="Name"
      HeaderText="Product Name"
      SortExpression="Name" />

...

By default, the grid renders a link at the top of each sortable column.  Clicking this link toggles the sort order between 3 states: Ascending, Descending and No Sort. 


Sorting Limitations

But there is a "Gotcha".  The GridView can be bound to any set of data - that is, to any object that implements the System.Collections.IEnumerable interface.  This includes a DataTable, ArrayList, List and HashTable, among others.  However the automatic sorting only works when binding to a DataTable.

Personally, I prefer to work with and bind to generic Lists.  A List is more flexible than a DataTable and does a better job of abstracting the user interface from the back-end data source, making it easier to swap out one or the other.

But by binding to a List, I sacrifice the automatic sorting that happens when I bind to a DataTable.  Fortunately, it doesn't take a lot of code to implement sorting on a GridView bound to a generic List.

How to sort a GridView bound to a List

First, we set the GridView AllowSorting property and each column's SortExpression property as described above.  This provides "sort" links at the top of each column.  Unfortunately these links will not work properly - in fact, clicking them will generate an error.

To get this to work, you must do the following

  1. Add an extra "sortby" parameter of type string to your Select method.
  2. Add code to your sort method to sort the list before returning it (more on this later)
  3. Add a SortParameterName attribute to your ObjectDataSource.  The value of this parameter should be the same as the parameter you added to your Select method.

By setting the SortParameterName attribute, we are telling ASP.Net to pass sorting information to the Select method and which parameter to pass it to.  The Select method gets called when the grid loads or refreshes and whenever the user clicks the "sort" links at the top of each column.  Most of the time, the value passed to the sortby parameter is an empty string (indicating no sort order), but if the user clicks a "sort" link, this parameter will contain one of the following three values

  • "<SortField> ASC"
  • "<SortField> DESC"
  • ""

where <SortField> is the string specified in the SortExpression attribute of the column clicked.  With each click, the grid column cycles its sort between ascending order, descending order, and no sort.  The parameter value passed depends on which is currently the active sort. 


<asp:ObjectDataSource ID="ProductObjectDataSource" runat="server" 
      SelectMethod="GetProductsList"
      TypeName="DemoGridBusLayer.NWindBL"
      SortParameterName="sortBy">
</asp:ObjectDataSource>

Now, how do we sort a List?  More specifically, how do we sort a list when we don't know in advance on which column we are sorting or in which direction (ascending or descending)? 

A List has a Sort method, so we can call that.  But what does it mean to sort a list of objects?  An object has properties and we can sort on any one (or more) of those properties, as long as the property is of a type that can be sorted.  We need to specify on which object property we will be sorting.  To do this, we use an overload of the List.Sort method that accepts an IComparer object.  IComparer has a Compare method that tells the Sort method how to order each pair of objects in the list.  We can create a class that implements IComparer, override the Compare method and use reflection to determine at runtime on which property to sort.  The name of the property and the sort order (Ascending or Descending) can be passed into the class constructor. 

The code for this class (named GenericComparer) below:

using System;
using System.Collections.Generic;
using System.Collections;
using System.Text;
using System.Reflection;

namespace DemoGridBusLayer
{
    /// <summary>
    /// This class is used to compare any
    /// type(property) of a class for sorting.
    /// This class automatically fetches the
    /// type of the property and compares.
    /// </summary>
    public sealed class GenericComparer<T> : IComparer<T>
    {
        public enum SortOrder { Ascending, Descending };

        #region Constructors
        public GenericComparer(string sortColumn, SortOrder sortingOrder)
        {
            this._sortColumn = sortColumn;
            this._sortingOrder = sortingOrder;
        }

        /// <summary>
        /// Constructor when passing in a sort expression containing both the Sort Column and the Sort Order
        /// e.g., "BPCode ASC".
        /// </summary>
        /// <param name="sortExpression"></param>
        /// <remarks>
        /// This constructor is useful when using this with the ASP.NET ObjectDataSource,
        /// which passes the SortParameterName in this format
        /// </remarks>
        public GenericComparer(string sortExpression)
        {
            string[] sortExprArray = sortExpression.Split(" ".ToCharArray());
            string sortColumn = sortExprArray[0];
            SortOrder sortingOrder;
            sortingOrder = SortOrder.Ascending;
            if (sortExprArray.Length > 1)
            {
                if (sortExprArray[1].ToUpper() == "DESC")
                {
                    sortingOrder = SortOrder.Descending;
                }
            }

            this._sortColumn = sortColumn;
            this._sortingOrder = sortingOrder;
        }

        #endregion



        #region public properties
        /// <summary>
        /// Column Name(public property of the class) to be sorted.
        /// </summary>
        private string _sortColumn;
        public string SortColumn
        {
            get { return _sortColumn; }
        }

        /// <summary>
        /// Sorting order (ASC OR DESC)
        /// </summary>
        private SortOrder _sortingOrder;
        public SortOrder SortingOrder
        {
            get { return _sortingOrder; }
        }

        #endregion

        /// <summary>
        /// Compare two objects of the same class,
        /// based on the value of a given property
        /// </summary>
        /// <param name="x">First Object</param>
        /// <param name="y">Second Object</param>
        /// <returns>int</returns>
        public int Compare(T x, T y)
        {

            // User reflection to get the property
            PropertyInfo propertyInfo = typeof(T).GetProperty(_sortColumn);

            // Cast the property to IComparable, so we can use the built-in compare.
            IComparable obj1 = (IComparable)propertyInfo.GetValue(x, null);
            IComparable obj2 = (IComparable)propertyInfo.GetValue(y, null);

            // Order depends on Asc vs Desc.
            if (_sortingOrder == SortOrder.Ascending)
            {
                return (obj1.CompareTo(obj2));
            }
            else
            {
                return (obj2.CompareTo(obj1));
            }
        }
    }
}


Within our select method, we can call the List's Sort method and pass in an instance of the GenericComparer class, specifying on which column and in which direction to sort the list.  The SelectMethod is shown below.  (The details of querying a database and storing results into a List of objects is omitted.)

        /// <summary>
        /// Get a sorted list of all products
        /// </summary>
        /// <param name="sortBy"></param>
        /// <returns></returns>
        public static List<Product> GetProductsList(string sortBy)
        {
            // Get a list of Business Processes
            List<Product> prodList = GetProductsList();

            // Sort list
            if (sortBy != "")
            {
                GenericComparer<Product> cmp = new GenericComparer<Product>(sortBy);
                prodList.Sort(cmp);
            }
            return prodList;

        }

    }
}

With a small amount of code, we can enable sorting of a List of objects bound to a GridView in the same way that sorting is enabled for a DataTable bound to a GridView.

Demo

You can download the entire sample described in this article here: DemoGridSort.zip (39.63 KB).  You will need the AdventureWorks sample SQL Server database for this demo.

Wednesday, November 23, 2011 12:21:05 AM (GMT Standard Time, UTC+00:00)
Thanks for this article. I've been bashing my head against this problem for a few hours now. Your solution worked perfectly.
Derek
Thursday, December 15, 2011 2:01:56 PM (GMT Standard Time, UTC+00:00)
Great article! Thank you very very much!
Ben
Friday, March 9, 2012 3:03:11 PM (GMT Standard Time, UTC+00:00)
Great article for generic list sorting...

But what if there is "ProductCategory" table's FK in the "Product" table in your demo..and if i want to display "CategoryName" in grid and want sorting on it..

Thank you...
plz reply
Mehul Pandya
Monday, November 5, 2012 5:02:36 PM (GMT Standard Time, UTC+00:00)
Nice! I like binding to generics but had always considered the cost of implementing sorting a pain point until seeing a generic class like this to handle it.

One modification to your GenericComparer class's Compare method so it can handle objects with null properties...

// Order depends on Asc vs Desc.
if (_sortingOrder == SortOrder.Ascending)
{
if (obj1 == null && obj2 == null)
return 0;
else if (obj1 == null)
return -1;
else if (obj2 == null)
return 1;
else
return (obj1.CompareTo(obj2));
}
else
{
if (obj1 == null && obj2 == null)
return 0;
else if (obj1 == null)
return 1;
else if (obj2 == null)
return -1;
else
return (obj2.CompareTo(obj1));
}
Eric
Comments are closed.