Thursday, January 5, 2012

GridView sorting with ViewState off - a solution to double querying and "why doesn't my Sorting event handler fire?"

I like to live a ViewState free lifestyle... call me prudish... My software engineering friends and I refer to ViewState as the "crack" or the "heroin" of ASP.Net development - darn hard to withdraw from once you've "sucked on the pipe" for a while, but once you're ViewState free, life is indeed more beautiful...

I pride myself in having resolved 99% of the withdrawal symptoms experienced when you make the leap to EnableViewState="false" on all your ASP.Net pages... I hope to share some of that knowledge with you all, gentle readers...

This blog post shall be devoted to one such solution I arrived at today: how to get a GridView to recognize that a user has clicked on a Sort header, with ViewState disabled, and without having to DataBind the GridView on every Page_Load.

THE PROBLEM

Your sorting event handler never fires! (could be other event handlers too btw)... You bang your head on your desk... your desk gets a dent... Your head gets a bruise... nothing helps... You go go over the steps you've taken:

You've EnableViewState="false" on your aspx page (because you like to live a "crack" free life!). You AllowSorting="true" on your GridView. You added a SortExpression to a column header. You added an asp:Button control and have a click event handler in your code behind. You believe in the use of "manual" databinding for your GridView, meaning GridView.DataSource = collection of custom objects queried from the database or middle tier of your choice, instead of using a GridView.DataSourceID (btw: those handy data source controls are VERY ViewState dependent and thus another form of "crack" or "heroin" IMO so I avoid them)...  From the click event handler you call GridView.DataBind() . You also call GridView.DataBind() upon intial load of the page (i.e., when Page.IsPostBack is false)... You add a GridView.Sorting event handler (OnSorting=handler) and from that handler you call GridView.DataBind() again...

BUT, when you load your page in your browser, and you click on one of the grid column headings to sort your page... it doesn't sort! The page POSTs back, but the grid does not even bind: it even shows up empty! When you set break points in your code, you find that the Sorting event handler is never even hit! WTF you say?!

You do a google search and you find this wonderful blogpost that you hope will help you out!

http://www.aarongoldenthal.com/post/2009/04/19/Manually-Databinding-a-GridView.aspx

You read this blog post with enthusiasm, until you reach the part that says you need to use ViewState (  ViewState["SortExpression"]  = something or other )... You sigh in agony that you just can't seem to beat this ViewState monster!

But then you go on to read the comments!... You read the following excellent comment from Aaron!
Aaron 8/17/2010 8:59 PM @Guy, That sounds like what you'd see if ViewState was disabled for the GridView.  If that's the case, try databinding on every page load, not just on postback.  This is yet another case where data source controls are helping you behind the scenes - if ViewState is disabled then the GridView will be databound as the post data is being processed (which needs to be done to re-populate the original data in the GridView, data that's otherwise stored in ViewState).
You jump for joy! Someone else had the same problem as you and lo and behold the solution is to bind your GridView on EVERY PageLoad - regardless of whether IsPostBack or not!

With glee you refactor your code and get rid of the if(!Page.IsPostBack) section and simply call the GridView.DataBind() code on every Page_Load or Page_Init! You fire up your browswer, click a sort heading, and lo and behold, your Sort event handler fires! Problem solved! ... so you think!...

Indeed, this was the solution I used for years, but it is somewhat of a compromise... The drawback is that for every Sort or button click on the page, the Grid will bind twice: once on page load and then again on the sort or click event handler. If you are doing a SQL query on every DataBind() and you don't have any fancy caching strategy, having two SQL queries on every Page_Load is of course wasteful. I lived for years with the double querying drawback since where I worked at the time had an amazing distributed caching architecture (both on the web server and on the middle tier), and double querying was not expensive... But today I am working with a plain old SQL server database, and I'd rather not query the DB twice, just so I can get Sorting working...

THE SOLUTION (BETA!!!)

I want a simple syntax on my aspx page, with minimal code, with a pattern I can use over and over. I want to use "manual" databinding to a collection of custom objects that comes from a middle tier or database.

Let's get right to code shall we? Stick a textBox, grid view and a button on to your aspx page:

<asp:TextBox ID="txtMemberName" runat="server" /> 
<asp:gridview id="grid" runat="server"/>
<asp:Button ID="btnSearch" runat="server" Text="Search" OnClick="btnSearch_Click" />

Next, add this to your Page_Load event:

protected void Page_Load(object sender, EventArgs e)
{
    if (!Page.IsPostBack)
    {
        //TODO: this is optional. If you expect your 
        //result set to be too large, 
        //you would obviously NOT bind on initial load of page
        loadData();
    }
    else
    {
        //On PostBack, check if grid is sorting 
        //(this is a custom extension method). 
        //The native GridView Sorting event handler is not 
        //called at this point in lifecycle so it's no good to us.
        if (grid.IsSorting())
        {
            loadData();

        }
    }
}

protected void btnSearch_Click(object sender, EventArgs e)
{
    loadData();
}


Add this to your code behind also... This is the method that loads the data from your SQL Server DB, middle-tier, or whatever:

private void loadData()
{

    //replace this with a custom call of your choice to your data layer or middle tier
    IList myList = BusinessLayer.SearchMethod(_someId, txtName.Text);

    //OR if you want a quick demo of how this works use this
    //instead and comment out line above
    var myList = (new int[] { 4, 3, 2, 1 }).Select(i => new { ID = i, Name = i + "Adam" }).ToList();

    //Normally, you would set grid.DataSource = myList but instead call custom
    //SetDataSourceSortable method to make grid sortable automagically
    grid.SetDataSourceSortable("ID", SortDirection.Descending, myList);

    //call grid databind
    grid.DataBind();

}



OK, the above code is the BARE MINIMUM of what you need on any given aspx page. Of course, it calls some custom code below. The above assumes that grid.AutoGenerateColumns="true" which is rarely the case in the real world... The above solution only supports searching by the value in the txtName TextBox if you actually implement that feature yourself. It also only supports Sorting in one direction (ASCENDING). See other blog posts for how to sort both ways...

I prefer to define my grids in the code behind as of late and I do it in complete strongly typed fashion:

//define GridView - note this code will NOT compile on your
//machine... I'm simply including it as an example of good coding 
//practices
grid.Columns.Add(new HyperLinkField() { HeaderText = "ID", DataTextField = MemberTable.MemberIDColumn, DataNavigateUrlFields = new string[] { MemberTable.MemberIDColumn }, DataNavigateUrlFormatString = PageName.MemberDetails.GetNavigateUrlFormatString() });
grid.Columns.Add(new BoundField() { HeaderText = "Name", DataField = MemberTable.NameColumn });
grid.Columns.Add(new EnumField<MemberStatusType>() { HeaderText = "Status", DataField = MemberTable.MemberStatusTypeIDColumn });


Next, for the magic part... Create a public static class. Give it a name of your choice (e.g., public static class DataTableUtils). Put the following code in it:

/// <summary>
/// Alternative to setting .DataSource property directly, takes in an IList of any type and 
/// converts that list to easily sortable DataView
/// </summary>
public static void SetDataSourceSortable<T>(this GridView grid, string defaultSortExpression, SortDirection defaultSortDirection, IList<T> dataSource)
{
    //turn on sorting so Headers have hyperlinks on them
    grid.AllowSorting = true;

    //call a dummy handler so we don't get a Sorting event not handled error!!!
    grid.Sorting += new GridViewSortEventHandler(grid_Sorting);

    //add sort expressions to all Header links
    AddSortExpressions(grid);

    //convert the IList<T> to a DataTable for easy sorting
    DataTable table = dataSource.ToDataTable();

    //TODO: get sort direction per column from post back - that's another blog post!
    //for now just default to "ASC" for post backs 
    string sortDirection = grid.Page.IsPostBack ? "ASC" : (defaultSortDirection == SortDirection.Ascending ? "ASC" : "DESC");

    //Get Sort Expression from PostBack (if any)
    table.DefaultView.Sort = GetSortExpressionFromPost(grid, defaultSortExpression) + " " + sortDirection;

    //set the datasource of the grid to the DataView of DataTable
    grid.DataSource = table.DefaultView;

}

static void grid_Sorting(object sender, GridViewSortEventArgs e)
{
    //DO NOTHING!!! IT'S TOO LATE! Instead, in calling page load need to call "if (Page.IsPostBack && grid.IsSorting()) loadCustomData()...";

}

/// <summary>
/// Checks the Request for Posts for example:
/// __EVENTARGUMENT=Sort$FirstName
/// __EVENTTARGET = ctl00$ctl00$Main$m$grid
/// </summary>
public static string GetSortExpressionFromPost(GridView grid, string defaultSortExpression)
{
    //__EVENTARGUMENT=Sort$FirstName
    //__EVENTTARGET = ctl00$ctl00$Main$m$grid

    string sortExpression = defaultSortExpression;
    string arg = HttpContext.Current.Request.Form["__EVENTARGUMENT"];
    string target = HttpContext.Current.Request.Form["__EVENTTARGET"];
    //check if target matches
    if (grid.UniqueID == target)
    {
        if (!string.IsNullOrEmpty(arg))
        {
            string[] args = arg.Split('$');
            if (args.Length == 2 && args[0] == "Sort")
            {
                sortExpression = args[1];
            }
        }
    }
    return sortExpression;
}



/// <summary>
/// Checks POST for __EVENTARGUMENT that is a sort expression which is the way
/// a grid SHOULD work in .Net IMHO
/// In calling page load need to call "if (Page.IsPostBack && grid.IsSorting()) loadCustomData()...";
/// </summary>
public static bool IsSorting(this GridView grid)
{
    return GetSortExpressionFromPost(grid, "DEFAULTSORT") != "DEFAULTSORT";
}

/// <summary>
/// Loops through all columns and adds a sort expression automatically. 
/// This is Experimental. 
/// </summary>
public static void AddSortExpressions(GridView grid)
{

    foreach (DataControlField column in grid.Columns)
    {
        if (column is BoundField)
        {
            column.SortExpression = ((BoundField)column).DataField;
        }
        else if (column is HyperLinkField)
        {
            column.SortExpression = ((HyperLinkField)column).DataTextField;
        }
        //TODO: add more column types or simply add SortExpressions manually
    }
}

/// <summary>
/// WARNING! This uses reflection and is slow!
/// http://stackoverflow.com/questions/564366/generic-list-to-datatable
/// The only reason to use this is to gain the built in sorting features
/// of DataViews on DataTables :(
/// http://www.codeproject.com/KB/cs/GenericListControl.aspx
/// </summary>
public static DataTable ToDataTable<T>(this IList<T> data)
{
    PropertyDescriptorCollection props =
        TypeDescriptor.GetProperties(typeof(T));
    DataTable table = new DataTable();
    for (int i = 0; i < props.Count; i++)
    {
        PropertyDescriptor prop = props[i];
        table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
    }
    object[] values = new object[props.Count];
    foreach (T item in data)
    {
        for (int i = 0; i < values.Length; i++)
        {
            values[i] = props[i].GetValue(item) ?? DBNull.Value;
        }
        table.Rows.Add(values);
    }
    return table;
}


OK, the above requires some explanation, doesn't it? Read the comments in the code... I hope they are self-explanatory... I hate hacking ASP.Net and getting the __EVENTARGUMENT field, but I feel it is justified in this case. I couldn't find any other way for the Page to know that someone had clicked on a Sortable column heading BEFORE I call DataBind(). That is what I want. I want to discover that a user has clicked on a column, BEFORE the grid is bound. Normally, with ViewState on, the grid re-binds to the (bloated) data posted in the ViewState, notices that a Sort event needs to be handled, and then rebinds the grid again. In my case, I don't rebind the grid again! In my generic Sort event handler I do nothing at all! Intentionally...

There are any number of improvements that can be made to the code above, but I'll leave that to you gentle reader! For starters, I could implement an .OrderBy() explicitly for each type of SortExpression, but I want as automagic as possible in this case.

I took the time to post this blog posting because I want to give back somehow to the "Internets" a little bit of what I've taken over the years! Let me know if this works for you!