Pages

Wednesday, February 12, 2014

Sorting a GridView Bound to a Custom Data Object

The ease of binding an ArrayList of custom data objects to a GridView makes it a very attractive practice.  Of course, creating a grid will often come with the requirement of making the contents sortable.  This article presents a quick and easy way to facilitate sorting of custom data objects without additional calls to the database or data source.  It utilizes ASP .NET 2.0. in C#.  Let’s start with a simple custom data object called HoursBE (Business Entity).  It contains fields for name, hours worked, and date, as outlined below:

[Serializable]
public class HoursBE
{
    private string _name;
    private double _hours;
    private DateTime _date;

    public HoursBE()
    {
    }

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }

    public double Hours
    {
        get { return _hours; }
        set { _hours = value; }
    }

    public DateTime Date
    {
        get { return _date; }
        set { _date = value; }
    }
}


It is important that the class be serializable for storage in the ViewState or Session.  I’ve also included several different data types to illustrate subtle differences in how each is sorted.  The data object is bound to the GridView as shown below in the Page_Load method.  In this sample source code, the data is retrieved from an XML file, but the workings would be the same for a database call.  The hourList variable contains a list of HoursBE objects.


protected void Page_Load(object sender, EventArgs e)
    {
        HoursDAL hoursDAL = new HoursDAL();
        ArrayList hourList = hoursDAL.GetHours();
        ViewState["HourList"] = hourList;
        gvHours.DataSource = hourList;
        gvHours.DataBind();
    }


This produces the following GridView on an aspx page.

 

Here is a representation of the GridView mark up in the aspx page, with some key attributes highlighted in bold.


<asp:GridView ID="gvHours" runat="server" AllowSorting="True"
     AutoGenerateColumns="False" CellPadding="4" ForeColor="#333333"
     GridLines="None" OnRowCreated="gvHours_RowCreated"
     OnSorting="gvHours_Sorting" Style="z-index: 102; left: 65px;
     position: absolute; top: 84px" Width="513px">
            <FooterStyle BackColor="#507CD1" Font-Bold="True"
                   ForeColor="White" />
            <RowStyle BackColor="#EFF3FB" />
            <Columns>
                <asp:BoundField DataField="Name" HeaderText="Name"
                     SortExpression="Name" />
                <asp:BoundField DataField="Hours" HeaderText="Hours"
                     SortExpression="Hours">
                    <ItemStyle HorizontalAlign="Center" />
                </asp:BoundField>
                <asp:BoundField DataField="Date"
                     DataFormatString="{0:d}" HeaderText="Date"
                     SortExpression="Date">
                    <ItemStyle HorizontalAlign="Center" />
                </asp:BoundField>
            </Columns>
            <PagerStyle BackColor="#2461BF" ForeColor="White"
                        HorizontalAlign="Center" />
            <SelectedRowStyle BackColor="#D1DDF1" Font-Bold="True"
                        ForeColor="#333333" />
            <HeaderStyle BackColor="#507CD1" Font-Bold="True"
                        ForeColor="White" />
            <EditRowStyle BackColor="#2461BF" />
            <AlternatingRowStyle BackColor="White" />
        </asp:GridView>


First, the AllowSorting attribute must be set to true.  Do not set the EnableSortingAndPagingCallbacks attribute to true, or the sorting will not work.  You will also need OnSorting and OnRowCreatedMethods event handlers in the code behind.  Finally, the SortExpression must be set to the name of the data field on which you want to sort for each bound field.  In this simple case, it will be the fields themselves.

All of the code that follows is contained in the page code behind.  Now let’s take a look at the OnSorting event handler method, to examine the nuts and bolts of the sorting mechanism. 


    private const string ASCENDING = " ASC";
    private const string DESCENDING = " DESC";

    protected void gvHours_Sorting(object sender,
                                   GridViewSortEventArgs e)
    {
        string sortExpression = e.SortExpression;
        ViewState["SortExpression"] = sortExpression;

        if (GridViewSortDirection == SortDirection.Ascending)
        {
            GridViewSortDirection = SortDirection.Descending;
            SortGridView(sortExpression, DESCENDING);
        }
        else
        {
            GridViewSortDirection = SortDirection.Ascending;
            SortGridView(sortExpression, ASCENDING);
        }
    }


This method obtains the sort expression (data field name) from the GridViewSortEventArgs, and stores it in the view state for future reference.  It then performs a simple flip to determine whether the sort should be ascending, or descending.  The sort direction is contained within the following, small field.


    private SortDirection GridViewSortDirection
    {
        get
        {
            if (ViewState["sortDirection"] == null)
                ViewState["sortDirection"] = SortDirection.Ascending;
            return (SortDirection)ViewState["sortDirection"];
        }
        set { ViewState["sortDirection"] = value; }
    }


The actual sorting work is performed by the SortGridView method shown below.  This method basically creates a DataTable object that contains the same columns as the GridView and populates this table from data in the ArrayList that we stored in the view state earlier.  Note that the default data type for DataTable columns is a string, so it need to be explicitly set to Double for the hours and DateTime for the date, or those columns will not sort correctly.  It then creates a new sortable DataView object from the DataTable and sets the sort property to the sort expression and sort order.  After that, it is only a matter of setting the GridView’s data source property to the DataView and then binding it.


    private void SortGridView(string sortExpression, string direction)
    {
        ArrayList hourList = (ArrayList)ViewState["HourList"];
        DataTable dt = new DataTable();
        dt.Columns.Add("Name");
        dt.Columns.Add("Hours");
        dt.Columns["Hours"].DataType =
                        System.Type.GetType("System.Double");
        dt.Columns.Add("Date");
        dt.Columns["Date"].DataType =
                        System.Type.GetType("System.DateTime");

        foreach (HoursBE hours in hourList)
        {
            DataRow dr = dt.NewRow();
            dr["Name"] = hours.Name;
            dr["Hours"] = hours.Hours;
            dr["Date"] = hours.Date;
            dt.Rows.Add(dr);
        }

        DataView dv = new DataView(dt);
        dv.Sort = sortExpression + direction;
        gvHours.DataSource = dv;
        gvHours.DataBind();
    }


As an additional touch, we can add an up or down arrow to the column header to indicate whether the sort is ascending or descending.  We can do this by using the RowCreated event handler method we added to the GridView.  It is shown below.  It uses two methods to retrieve the current sort index (column name) and then set the image within that column.  This method fires ever time there is a new DataBind call on the GridView.

  
    protected void gvHours_RowCreated(object sender,
                                      GridViewRowEventArgs e)
    {
        if (e.Row.RowType == DataControlRowType.Header)
        {
            int sortColumnIndex = GetSortColumnIndex();
            if (sortColumnIndex != -1)
            {
                AddSortImage(sortColumnIndex, e.Row);
            }
        }
    }


The first method called by the event handler, GetSortColumnIndex, will check each column in the GridView against the sort index stored in the view state, as shown below.  It returns the column index when it finds a match, or a -1 if no match is found.


    private int GetSortColumnIndex()
    {
        foreach (DataControlField field in gvHours.Columns)
        {
            if (field.SortExpression ==
                         (string)ViewState["SortExpression"])
            {
                return gvHours.Columns.IndexOf(field);
            }
        }
        return -1;
    }

                                                      
The second method called by the event handler, AddSortImage, is shown below.  It creates an arrow up or down image based upon the sorting direction and then adds it to the controls of the appropriate column header.


private void AddSortImage(int columnIndex, GridViewRow headerRow)
    {
        // Create the sorting image based on the sort direction.
        Image sortImage = new Image();
        if (GridViewSortDirection == SortDirection.Ascending)
        {
            sortImage.ImageUrl = "~/images/uparrow.gif";
            sortImage.AlternateText = "Ascending Order";
        }
        else
        {
            sortImage.ImageUrl = "~/images/downarrow.gif";
            sortImage.AlternateText = "Descending Order";
        }
        // Add the image to the appropriate header cell.
        headerRow.Cells[columnIndex].Controls.Add(sortImage);
    }


The sort direction arrow is depicted below with the hours column sorted in descending order.


 
Source code for the slolution is available for download here.

No comments:

Post a Comment