Implementing a multisort column in GridView

This is my first article on this blog. I would really like to get your feedback about the way article should have been best written or about its content, etc. Hope you’ll enjoy it.

Ok, let’s get started…

Recently I’ve been working on a page, that displayed detailed user information, nothing special, but a problem that I faced was that user information contained a whole bunch of data and the resulting GridView was badly stretched horizontally so that I had to scroll a lot to see all underlying data. I’ve decided to group some data together, in single column, like user address and move the rest of non-primary information to the user control that resided just after each grid row, but was initially hidden and could be toggled by button click (I will describe a way to do this in upcoming articles).
I’ve done so, now my GridView had enough information displayed to describe main user details and had some grouped columns like Address, this column contained user’s street address, city, state and zip code. Everything was cool, except one, a lack of valuable sorting for grouped columns. I could set only one sorting expression for one of four grouped fields, yeah, that’s not a solution, end user might want to be able to sort on street address, city or state. Let’s do it.
So our goal is to create a page with a GridView, add sorting and multisorting for grouped columns.

Important notice! We’ll bind data using gvUsers.DataSource property rather than using DataSourceId. I did some research because when I did binding using DataSource I faced a well-known event validation failure issue which seems is not a problem when I did binding to data source through DataSourceId. I’ll post my thoughts on that topic soon.

First we need to create a new ASP.NET Web Application, please do so if you haven’t yet. Open Default.aspx page in designer and add GridView control.
For a better understanding I changed its id from GridView1 to gvUsers.
Good, now we need some data to be bind, for a sake of simplicity I will create a class that describes user, create a couple of its instances, set their properties and add to array, at last I’ll bind that array to GridView.

So, here is our class:

 public class User

    {

        private string _firstName, _lastName, _streetAddress, _city, _state;

 

        public User(string firstName, string lastName, string streetAddress, string city, string state)

        {

            FirstName = firstName;

            LastName = lastName;

            StreetAddress = streetAddress;

            City = city;

            State = state;

        }

 

        public string FirstName

        {

            get { return _firstName; } set { _firstName = value; }

        }

 

        public string LastName

        {

            get { return _lastName; }  set { _lastName = value; }

        }

 

        public string StreetAddress

        {

            get { return _streetAddress; } set { _streetAddress = value; }

        }

 

        public string City

        {

            get { return _city; } set { _city = value; }

        }

 

        public string State

        {

            get { return _state; } set { _state = value; }

        }

    }

Now in codebehind of Default.aspx page we need to create a generic array and add initialized User class instances to it:

public partial class _Default : System.Web.UI.Page

    {

        List<User> _users;

And add property:

 private List<User> Users

        {

            get

            {

                if (_users == null)

                {

                    _users = new List<User>();

 

                    _users.Add(new User("Ronald", "Akers", "Lion Circle", "Bothell", "NV"));

                    _users.Add(new User("Michelle", "Michelle", "Riverside Drive", "Dallas", "CT"));

                    _users.Add(new User("Jay", "Adams", "Acorn Avenue", "Ottawa", "NY"));

                    _users.Add(new User("Kenny", "Mc", "someStreet", "South Park", "CO"));

                }

                return _users;

            }

        }

Now let’s setup GridView properties, define columns and group some if them:

   <asp:GridView ID="gvUsers" runat="server" AutoGenerateColumns="false" AllowSorting="true"
OnSorting="gvUsers_Sorting" OnRowCreated="gvUsers_RowCreated">
<Columns>
<asp:BoundField DataField="FirstName" SortExpression="FirstName" HeaderText="FirstName" />
<asp:BoundField DataField="LastName" SortExpression="LastName" HeaderText="LastName" />
<asp:TemplateField HeaderText="Address">
<ItemTemplate>
<%# string.Format("{0},<br/>{1}, {2}",
Eval("StreetAddress"),
Eval("City"),
Eval("State"))
%>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>

Very good, now add data binding method and call it from Page Load event:

 private void BindData()

        {

            gvUsers.DataSource = Users;

            gvUsers.DataBind();

        }

 

        protected void Page_Load(object sender, EventArgs e)

        {

            if (!Page.IsPostBack)

            {

                BindData();

            }

        }

Let’s run the application, you will see something like this:

If you try to click on underlined soring links you will get an exception, because sorting event handler in not defined, we need to correct this:

Important notice! Further I’ll provide a code that will sort our generic array, I used reflection and static variables, as you can understand use of static variables for a sorting purposes is not a proper solution, but it’s ok in this case, because how sorting will be performed is not out main aim, in real projects you usually sort data using DataViews, ask SQL to do so or whatever else.

We need two variables:

private static string SortingColumn;

private static int SortDir;

a ViewState property to store current GridView sorting direction, cause of common problem where GridView’s sorting direction is always ascending:

private bool IsSortAsc

        {

            get { return (ViewState["IsSortAsc"] == null) ? true : (bool)ViewState["IsSortAsc"]; }

            set { ViewState["IsSortAsc"] = value; }

        }

a comparing method that we will pass to Comparison delegate:

 private static int CompareUsers(User first, User second)

        {

            Type u1 = first.GetType();

            Type u2 = second.GetType();

 

            PropertyInfo piUser1 = u1.GetProperty(SortingColumn);

            PropertyInfo piUser2 = u2.GetProperty(SortingColumn);

 

            string u1Value = piUser1.GetValue(first, null).ToString();

            string u2Value = piUser2.GetValue(second, null).ToString();

 

            return u1Value.CompareTo(u2Value) * SortDir;

        }

And at last sorting event handler:

 protected void gvUsers_Sorting(object sender, GridViewSortEventArgs e)

        {

            SortingColumn = e.SortExpression;

            SortDir = IsSortAsc ? 1 : -1;

 

            IsSortAsc = !IsSortAsc;

 

            Users.Sort(CompareUsers);

            BindData();

        }

It’s pretty simple, in event handler I set up our static variables to point to a valid sorting field and direction, then I call the Sort method of our generic array that accepts a comparing method as a parameter. Inside the method I access static variables to get sorting information. As soon as our User class has 5 public properties of string type we should have defined 5 similar comparison methods, I used reflection mechanism to create a generic one that will deal with all sorting options defined.

Just run application again and click on the header links, yeah, sorting works. At last, now we can deal with our Address column, remember that? :

					 <asp:TemplateField HeaderText="Address">
<ItemTemplate>
<%# string.Format("{0},<br/>{1}, {2}",
Eval("StreetAddress"),
Eval("City"),
Eval("State"))
%>
</ItemTemplate>
</asp:TemplateField>

We don’t need it to have HeaderText property anymore, I’ll get rid of it.
Ok, let’s think for a moment how can we add more sorting options to third column, if you take a look in the source code of the Default.aspx page in the browser you will notice what sorting links are:

<a href="javascript:__doPostBack('gvUsers','Sort$FirstName')" mce_href="javascript:__doPostBack('gvUsers','Sort$FirstName')">FirstName</a>
<a href="javascript:__doPostBack('gvUsers','Sort$LastName')" mce_href="javascript:__doPostBack('gvUsers','Sort$LastName')">LastName</a>

what we need is to create similar links, but with different sorting parameter like Sort$City or Sort$State.
So we need an asp.net server control that get’s rendered into simple html anchor - <a>, HyperLink control is exactly what we need, it has NavigateUrl property that renders as href at the end. Cool, let’s do it, first we need to override GridView’s RowCreated event, there we’ll check row type and if it’s a header type we’ll add 3 HyperLinks in the header cell of the third column:

        protected void gvUsers_RowCreated(object sender, GridViewRowEventArgs e)

        {

            if (e.Row.RowType == DataControlRowType.Header)

            {

                HyperLink hStreet = new HyperLink();

                hStreet.Text = "Street";

 

                HyperLink hCity = new HyperLink();

                hCity.Text = "City";

 

                HyperLink hState = new HyperLink();

                hState.Text = "State";

 

                e.Row.Cells[2].Controls.Add(hStreet);

                e.Row.Cells[2].Controls.Add(new LiteralControl(" / "));

 

                e.Row.Cells[2].Controls.Add(hCity);

                e.Row.Cells[2].Controls.Add(new LiteralControl(" / "));

 

                e.Row.Cells[2].Controls.Add(hState);

            }

        }

Run application - we have new labels in the third column:

now let’s make them behave as real sorting links that will cause postback, this is where a new game player comes in – PostBackOptions class. I’ll setup it’s properties and at the end we’ll get almost final version of our grid:

 

  protected void gvUsers_RowCreated(object sender, GridViewRowEventArgs e)

        {

            if (e.Row.RowType == DataControlRowType.Header)

            {

                PostBackOptions poStreet = new PostBackOptions(gvUsers, "Sort$StreetAddress", string.Empty, false, true, false, true, false, string.Empty);

                PostBackOptions poCity = new PostBackOptions(gvUsers, "Sort$City", string.Empty, false, true, false, true, false, string.Empty);

                PostBackOptions poState = new PostBackOptions(gvUsers, "Sort$State", string.Empty, false, true, false, true, false, string.Empty);

 

                HyperLink hStreet = new HyperLink();

                hStreet.Text = "Street";

                hStreet.NavigateUrl = Page.ClientScript.GetPostBackEventReference(poStreet);

 

                HyperLink hCity = new HyperLink();

                hCity.Text = "City";

                hCity.NavigateUrl = Page.ClientScript.GetPostBackEventReference(poCity);

 

                HyperLink hState = new HyperLink();

                hState.Text = "State";

                hState.NavigateUrl = Page.ClientScript.GetPostBackEventReference(poState);

 

                e.Row.Cells[2].Controls.Add(hStreet);

                e.Row.Cells[2].Controls.Add(new LiteralControl(" / "));

 

                e.Row.Cells[2].Controls.Add(hCity);

                e.Row.Cells[2].Controls.Add(new LiteralControl(" / "));

 

                e.Row.Cells[2].Controls.Add(hState);

            }

        }

I’ve created three PostBackOptions class instances, used overloaded contructor to define behavior and setup NavigateUrl properties of HyperLink class instances to a string used in a client event to cause post back to the server by calling GetPostBackEventReference method. Run the application now:

Wow, we’ve almost done it! A single column with multiple sort options, try to sort on FirstName, LastName – works!
Now sort on Street, oops, exception:

Invalid postback or callback argument.  Event validation is enabled using <pages enableEventValidation="true"/> in configuration or <%@ Page EnableEventValidation="true" %> in a page.  For security purposes, this feature verifies that arguments to postback or callback events originate from the server control that originally rendered them.  If the data is valid and expected, use the ClientScriptManager.RegisterForEventValidation method in order to register the postback or callback data for validation.

Seems to be true, let’s to do as stated in exception. ClientScriptManager.RegisterForEventValidation method can be called only from Render method of the page, so in our case we need to override it and call RegisterForEventValidation there, but wait, we need to pass a parameters to it, one of the overloads accepts PostBackOptions class instance, sounds good, we have three instances of PostBackOptions class in RowCreated event handler and need them to get registered. I’ll use small trick and create another generic array:

List<PostBackOptions> poSortLinks = new List<PostBackOptions>();

Populate it at the end if the RowCreated event handler:

protected void gvUsers_RowCreated(object sender, GridViewRowEventArgs e)

{

    if (e.Row.RowType == DataControlRowType.Header)

       {

      ...

      .....

    poSortLinks.Add(poStreet);

    poSortLinks.Add(poCity);

    poSortLinks.Add(poState);

}

 

Finally, override Render method and register those post backs:

protected override void Render(HtmlTextWriter writer)

        {

            foreach (PostBackOptions option in poSortLinks)

            {

                Page.ClientScript.RegisterForEventValidation(option);

            }

            base.Render(writer);

        }

Run, try it and enjoy! It works!

That’s it, as you can see it’s pretty easy to implement multisorting column, I’ve tried to write everything in details so you don’t miss a point and follow the whole process implementation step by step.
In upcoming articles I will primarily concentrate on essentials, will not drill into minor parts, assume that you are already familiar with common aspects.

Complete source code is attached! 

Thank you and have a good day!

3 Comments

Comments have been disabled for this content.