Wednesday, 14 April 2010

ASP.NET GridView: Adding Sorting and Paging with Business Object Binding


So recently, I was given the challenge of binding a complex business object to a GridView... I didn't think it would be tricky, but I did find a few niggles along the way. I thought I would share them so anybody else attempting to do this can pick up some lessons learned. The following is my implementation of this task...

This example displays a list of courses. Each course has an ID, a name and a production status. The production status' will be rendered as images within the gridview. Below is the designer structure of the grid..

        <asp:GridView ID="grdBantyGridView"
                    CssClass="gridView"
                    rowstyle-borderstyle="none"
                    GridLines="None" BorderStyle="None"
                    RowStyle-CssClass="row"
                    runat="server"
                    ShowFooter="True"
                    AutoGenerateColumns="False"
                    ShowHeader="True"
                    AlternatingRowStyle-CssClass="alternate"
                    AllowSorting="True"
                    UseAccessibleHeader="True"
                    onrowdatabound="grdBantyGridView_RowDataBound" 
                    onsorting="grdBantyGridView_Sorting"
                    OnPageIndexChanging="grdBantyGridView_PageIndexChanging" 
                    AllowPaging="True"
                    PageSize="10"
                    PagerSettings-Mode="NumericFirstLast"
                    PagerSettings-PageButtonCount="10"
                    DataKeyNames="ID" EnableViewState="False"
                    >
            <HeaderStyle CssClass="gridViewHeader" />
            <RowStyle BorderStyle="None" CssClass="row" />
            <Columns>
                <asp:BoundField DataField="ID" Visible="False" />
                <asp:TemplateField AccessibleHeaderText="Name" HeaderText="Name" 
                    SortExpression="Name">
                    <ItemTemplate>
                        <asp:Label ID="lblName" runat="server" 
                            Text='<%# Eval("Name") %>'></asp:Label>
                    </ItemTemplate>
                </asp:TemplateField>
                <asp:TemplateField AccessibleHeaderText="Description" HeaderText="Description" 
                    SortExpression="Description">
                    <ItemTemplate>
                        <asp:Label ID="lblDescription" runat="server" 
                            Text='<%# Eval("Description") %>'></asp:Label>
                    </ItemTemplate>
                </asp:TemplateField>
                <asp:TemplateField AccessibleHeaderText="Status" HeaderText="Status" 
                    SortExpression="Status">
                    <ItemTemplate>
                        <asp:Image ID="imgStatus" runat="server" 
                            ImageUrl='<%# GetCourseStatusUrl((Banty.Library.DTO.CourseStatus)Eval("Status")) %>' />
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
            <FooterStyle CssClass="gridViewFooter" />
            <EmptyDataTemplate>
            No data available.
            </EmptyDataTemplate>
        <PagerStyle height="5px" verticalalign="Bottom" horizontalalign="Center" />
            <AlternatingRowStyle CssClass="alternate" />
    </asp:GridView>



Things to note:
AutoGenerateColumns - This gives us more flexibility over what columns appear when we bind out business objects.
AllowSorting - Allows the sorting, but because we have disabled AutoGenerateColumns, we need to override the 'onsorting' event.
AllowPaging - Allow the paging. We must override the 'OnPageIndexChanging' event to make this work.
DataKeyNames - Set this so that we can retrieve each row during the 'onrowdatabound' event. This will store a datakey against each row. I am using this to enable javascript highlights of rows in this example.

For each column in the Gridview definition, be sure to add a SortExpression value. This helps us in the code-behind to identify while column is requesting the sort.

The above points are key to creating this pagable and sortable grid, and must be taken into consideration... Lets now have a look at the code behind and how we fit this all together...


        private List<Banty.Library.DTO.Course> courses = null;
 
        #region >>> Properties
 
        public List<Banty.Library.DTO.Course> Courses { get { return this.courses; } set { this.courses = value; } }
 
        #endregion
 
        protected void Page_Load(object sender, EventArgs e)
        {
        }
 
        /// <summary>
        /// Initialises the user control.
        /// </summary>
        public void InitControl()
        {
            if (this.Courses != null)
            {
                this.SetDataSource(true);
 
                pnlNoData.Visible = false;
                pnlGridView.Visible = true;
            }
            else
            {
                pnlNoData.Visible = true;
                pnlGridView.Visible = false;
            }
        }
 
        /// <summary>
        /// Sets the datasource to the gridview.
        /// </summary>
        private List<Banty.Library.DTO.Course> SetDataSource(bool dataBind)
        {
            // Bind apps to the grid view.
            this.grdBantyGridView.DataSource = this.Courses;
 
            // Bind if requested
            if (dataBind)
                this.grdBantyGridView.DataBind();
 
            return this.Courses;
        }
 
        /// <summary>
        /// Performs actions as the row data is bound.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void grdBantyGridView_RowDataBound(object sender, GridViewRowEventArgs e)
        {
            if (e.Row.RowType == DataControlRowType.DataRow)
            {
                int uniqueID = 0;
                if (Int32.TryParse(this.grdBantyGridView.DataKeys[e.Row.RowIndex].Value.ToString(), out uniqueID))
                {
                    // Add applicationID to row
                    e.Row.Attributes["uniqueid"] = uniqueID.ToString();
                }
 
                // So that we can distinguish rows
                e.Row.Attributes.Add("OnMouseUp", "grdBantyGridView_OnMouseUp(" + e.Row.ClientID + ");");
            }
        }
 
        /// <summary>
        /// Provides an image URL for value.
        /// </summary>
        /// <param name="input">data value</param>
        /// <returns>A URL to the value icon</returns>
        protected string GetCourseStatusUrl(CourseStatus input)
        {
            string result = string.Empty;
 
            switch (input.ID)
            {
                case 1:
                    result = "../images/icon-no.gif";
                    break;
                case 2:
                    result = "../images/icon-yes.gif";
                    break;
                case 3:
                    result = "../images/icon-verified.gif";
                    break;
            }
 
            return result;
        }
 
        /// <summary>
        /// Handles the sorting of the gridview columns
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void grdBantyGridView_Sorting(object sender, GridViewSortEventArgs e)
        {
            // Determine weather column is valid
            string[] validFields = new string[] { "Name", "Description", "Status"};
 
            // if we have a valid field to sort....
            if (validFields.Contains(e.SortExpression))
            {
                // Get Sort direction
                string sortDir = this.GetSortDirection(e.SortExpression);
                List<Banty.Library.DTO.Course> courses = this.SetDataSource(false);
 
                // Filter on sort expression...
                switch (e.SortExpression)
                {
                    case "Name":
                        if (sortDir.Equals("ASC"))
                            courses.Sort(delegate(Banty.Library.DTO.Course c1, Banty.Library.DTO.Course c2) { return c1.Name.CompareTo(c2.Name); });
                        else
                            courses.Sort(delegate(Banty.Library.DTO.Course c1, Banty.Library.DTO.Course c2) { return -c1.Name.CompareTo(c2.Name); });
                        break;
 
                    case "Description":
                        if (sortDir.Equals("ASC"))
                            courses.Sort(delegate(Banty.Library.DTO.Course c1, Banty.Library.DTO.Course c2) { return c1.Description.CompareTo(c2.Description); });
                        else
                            courses.Sort(delegate(Banty.Library.DTO.Course c1, Banty.Library.DTO.Course c2) { return -c1.Description.CompareTo(c2.Description); });
                        break;
 
                    case "Status":
                        if (sortDir.Equals("ASC"))
                            courses.Sort(delegate(Banty.Library.DTO.Course c1, Banty.Library.DTO.Course c2) { return c1.Status.Status.CompareTo(c2.Status.Status); });
                        else
                            courses.Sort(delegate(Banty.Library.DTO.Course c1, Banty.Library.DTO.Course c2) { return -c1.Status.Status.CompareTo(c2.Status.Status); });
                        break;
                }
 
                // Rebind the gridview
                this.grdBantyGridView.DataBind();
            }
        }
 
        /// <summary>
        /// Handle page changing on the grid.
        /// </summary>
        /// <param name="sender">This parameter is not used.</param>
        /// <param name="e">This parameter is not used.</param>
        protected void grdBantyGridView_PageIndexChanging(object sender, GridViewPageEventArgs e)
        {
            // Update data source
            this.SetDataSource(false);
 
            this.grdBantyGridView.PageIndex = e.NewPageIndex;
            this.grdBantyGridView.DataBind();
        }
 
        // This is sort the column, by default, asc.
        // Unless, the column has been sorted already...
        // then it will sort the column in the opposite direction.
        private string GetSortDirection(string column)
        {
            // By default, set the sort direction to ascending.
            string sortDirection = "ASC";
 
            // Retrieve the last column that was sorted.
            string sortExpression = ViewState["SortExpression"] as string;
 
            if (sortExpression != null)
            {
                // Check if the same column is being sorted.
                // Otherwise, the default value can be returned.
                if (sortExpression == column)
                {
                    string lastDirection = ViewState["SortDirection"] as string;
                    if ((lastDirection != null) && (lastDirection == "ASC"))
                    {
                        sortDirection = "DESC";
                    }
                }
            }
 
            // Save new values in ViewState.
            ViewState["SortDirection"] = sortDirection;
            ViewState["SortExpression"] = column;
 
            return sortDirection;
        }
 
    }



We can also add javascript to apply styles to the selected row
//
// Handles highlighting table rows on mouseup event.
//
function grdBantyGridView_OnMouseUp(row) {
 
    // Reset grid styles
    ResetGrid();
 
    // Add selected class
    row.setAttribute('class', 'selected');
}
 
function ResetGrid()
{
    // Remove selected from all rows that have a unique attribute...
    $("table.gridView tr[uniqueid]").removeClass("selected");
 
    // Now add the alternate style to every 2nd row...
    for (var i = 1; i < $("table.gridView tr[uniqueid]").length + 1; i++)
    {
        if (i % 2 == 0)
            $("table.gridView tr[uniqueid]")[i-1].setAttribute('class', 'alternate');
    }
}

No comments: