Friday, 21 March 2008

Extending the GridView to Include Sort Arrows

Introduction
Before ASP.NET version 2.0 was released, I wrote a book and dozens of articles on the DataGrid control, which was the most functional data Web control in the ASP.NET 1.x toolbox. While the DataGrid still exists in ASP.NET 2.0, the GridView control serves as a much more functional and feature-rich choice. The GridView can be bound to data source controls like the SqlDataSource and ObjectDataSource, and paging, sorting, editing, and deleting can be implemented without having to write a single line of code.

While the DataGrid (and GridView) offer built-in sorting support, there is no visual feedback as to what column the data is sorted by. In Part 18 of the An Extensive Examination of the DataGrid Web Control article series, I showed how to dynamically update a sortable DataGrid's header columns so that an up or down arrow image would appear next to the sorted column name depending on whether the column was sorted ascendingly or descendingly (view a live demo). This was accomplished by programmatically looping through the DataGrid's Columns collection and adding the up or down arrow image to column whose SortExpression value matched the DataGrid's SortExpression.

A down arrow image is shown in the Price column because the data is sorted in descending order, by price.
I recently needed to implement this functionality in a GridView control. Rather than adding this code to an ASP.NET page, like I did with the DataGrid demo, I decided to instead create a custom Web server control that extended the GridView control, adding the necessary functionality. In this article we will look at the steps for building such a custom control as well as how to use the control in an ASP.NET page. The custom control's complete source code and a demo application are available for download at the end of this article. Read on to learn more!

A Quick Primer on GridView Sorting
The GridView control makes sorting a breeze. Simply set the grid's AllowSorting property to True. Doing so renders each column's header as a LinkButton. When this LinkButton is clicked, a postback ensues and the GridView's Sorting event is fired. If the GridView is bound to a data source control that supports sorting, the GridView will internally re-bind to the data source using the new sort order. If you have programmatically bound data to the GridView, then you will need to create an event handler for the Sorting event, and manually sort and re-bind the data. After the Sorting event has fired and the data has been re-sorted, the GridView fires its Sorted event, thereby completing the sorting workflow.

Each column in the GridView has a SortExpression property that indicates the data field by which the data should be sorted if that column's sorting LinkButton is clicked. When the LinkButton in a column header is clicked, the GridView sets its SortExpression property value to the value of the clicked column's SortExpression. If the GridView's SortExpression matches the SortExpression of the sorted column, then the GridView's SortDirection property is toggled. This functionality enables the GridView control to provide built-in, bi-directional sorting.

The following diagram depicts the sorting workflow, including the GridView's assignment of its SortExpression and SortDirection properties. Keep in mind that the entire process is kicked off by the user clicking a LinkButton in one of the sortable column's headers, and completes by displaying the data in the requested sorted order. So, from the end user's perspective, she clicks the text in a particular column header and the data is sorted by that column.

The GridView's sorting workflow.

For more information on sorting with the GridView, along with a downloadable demo, check out the Paging and Sorting Report Data tutorial.

Creating a Custom Server Control that Extends the GridView
When I implemented the up and down arrow images for the DataGrid control in Part 18 of the An Extensive Examination of the DataGrid Web Control article series, I put the code that added (or removed) the arrow images in the ASP.NET page's code-behind class. Specifically, the code enumerated the DataGrid's Columns collection and, for every column, started by removing any <img> elements that were present in the HeaderText. This "cleaned out" the up or down arrow image from all columns. After removing any <img> elements, the code checked to see if the current column's SortExpression matched the DataGrid's SortExpression. If so, then an <img> element was tacked on to the end of the column's HeaderText, displaying the up or down arrow image depending on whether the data was being sorted in ascending or descending order.

Like the DataGrid, the GridView has a Columns collection; each column has a SortExpression; the GridView has SortExpression and SortDirection properties. In short, I could have implemented the up and down arrow images in the GridView using code in the ASP.NET page very similar to that used for the DataGrid. However, I decided that instead of having to write code for each page where I wanted to show up and down arrow images, I decided to instead build a custom server control that extended the GridView control. I could then bake in the necessary code by overriding the necessary GridView methods.

To create a custom server control that extends an existing control, create a public class that derives from the Web control that you want to extend. I created a class named GridView in my skmControls2 Class Library project. (The skmControls2 Class Library project was first created and discussed in Creating a TextBox Word / Character Counter Control.)

public class GridView : System.Web.UI.WebControls.GridView
{
    ... Override necessary GridView methods here ...
}

The GridView has a virtual InitializeRow method that is called each time a row is added to the GridView, including the header row. My first thought was to override this method and, for the header row, enumerate the fields and update each column's HeaderText appropriately (removing any image markup first, and then adding the up or down arrow image for the sorted row). However, if you attempt to update a GridView's field's properties during the databinding stage, the field reports to the GridView that its state has been changed and that the data needs to be re-bound. Consequently, I ended up stuck in an infinite loop:

  1. Databinding would commence
  2. The InitializeRow method would be executed for the header row
  3. I'd update the HeaderText for the cells in the header row
  4. Updating the HeaderText would signal the GridView to re-bind its data, taking us back to Step 1! Eep.
Instead, I needed to modify the HeaderText properties either before or after the databinding process. After some thought, I realized that I really only needed to update the HeaderText properties after the data has been sorted. The GridView's OnSorted method is what raises the Sorted event, which occurs after the data has been sorted. Therefore, I decided to override this method and update the HeaderText properties there.

public class GridView : System.Web.UI.WebControls.GridView
{
   protected override void OnSorted(EventArgs e)
   {
      string imgArrowUp = ...;
      string imgArrowDown = ...;
      
      foreach (DataControlField field in this.Columns)
      {
         // strip off the old ascending/descending icon
         int iconPosition = field.HeaderText.IndexOf(@" <img border=""0"" src=""");
         if (iconPosition > 0)
            field.HeaderText = field.HeaderText.Substring(0, iconPosition);

         // See where to add the sort ascending/descending icon
         if (field.SortExpression == this.SortExpression)
         {
            if (this.SortDirection == SortDirection.Ascending)
               field.HeaderText += imgArrowUp;
            else
               field.HeaderText += imgArrowDown;
         }
      }

      base.OnSorted(e);
   }
}

The overridden OnSorted method starts by defining the URLs for the up and down arrow images. We'll discuss how these image URLs are determined later on in this article. Next, the GridView's Columns collection is enumerated. For each column, if there is an <img> element it is stripped off. Next, a check is performed to see if the current column's SortExpression matches the GridView's. If so, the HTML to display the up or down arrow image is appended to the column's HeaderText depending on the value of the GridView's SortDirection property.

An HTML Encoding Gotcha

This Control Has Been Enhanced!
The "HTML Encoding Gotcha" outlined here is no longer an issue with the enhancements made to this control on February 6th, 2008. For more information on these enhancements, see: Improving the Sort Arrows GridView Control.

One subtlety that I quickly stumbled upon when testing, is that the BoundField HTML encodes its HeaderText's content, by default. That is, when the BoundField was being rendered it was taking the <img> tag added in the OnSorted method and HTML encoding it, replacing < and > with &lt; and &gt;, respectively. In other words, it was transforming a HeaderText value of:

Price <img border="0" src="up.gif" />

To:

Price &lt;img border="0" src="up.gif" /&gt;

Consequently, the browser would display the sorted column's header text as Price <img border="0" src="up.gif" /> rather than showing an image.

This BoundField has an HtmlEncode property that, if True (the default), HTML encodes the entire content of the BoundField. One way to fix the header text being HTML encoded, then, would be to set the BoundField's HtmlEncode property to False. But what if you wanted the data rows to be HTML encoded, but not the header text?

To allow for this level of flexibility, I created another custom class in the skmControls2 project. This one extended the BoundField class and overrode the InitializeCell method, which is where the HTML encoding takes place. The BoundField HTML encodes its content only if both its HtmlEncode and SupportsHtmlEncode properties are True. SupportsHtmlEncode is a read-only property that, for the BoundField, always returns True. I overrode this property and instead had it return a value based on a private member variable. Then, in the InitializeCell method I set this member variable to false when initializing a header cell.

public class BoundField : System.Web.UI.WebControls.BoundField
{
   bool allowHtmlEncode = true;

   protected override bool SupportsHtmlEncode
   {
      get
      {
         return allowHtmlEncode;
      }
   }

   public override void InitializeCell(DataControlFieldCell cell, DataControlCellType cellType, DataControlRowState rowState, int rowIndex)
   {
      if (this.HtmlEncode && cellType == DataControlCellType.Header)
      {
         allowHtmlEncode = false;
         base.InitializeCell(cell, cellType, rowState, rowIndex);
         allowHtmlEncode = true;
      }
      else
         base.InitializeCell(cell, cellType, rowState, rowIndex);
   }
}

Since the BoundField is the only GridView field that HTML encodes its data, this hack is only needed for the BoundField. All other fields - the TemplateField, CheckBoxField, ButtonField, and so on - will correctly display the up and down arrow images without any extra hacks. We'll see how to use this custom BoundField class in the "Using the Custom GridView Control in an ASP.NET Web Page" section.

Specifying the Image URLs for the Up and Down Arrows
At this point you may be wondering how the custom GridView control determines the URLs for the up and down image arrows. The simplest option would be to require the page developer to specify the URLs for these two images. I created two such properties: ArrowUpImageUrl and ArrowDownImageUrl. But I also wanted to allow the page developer to have a working GridView even if she didn't want to spend the time to find up and down arrow images. Therefore, I embedded an up and down arrow image into the skmControls2 assembly and use these in the case where the developer does not specify a value for the ArrowUpImageUrl and ArrowDownImageUrl properties.

For more information on embedding resources into an assembly, and retrieving these resources through an ASP.NET page, be sure to read Accessing Embedded Resources through a URL Using WebResource.axd.

Using the Custom GridView Control in an ASP.NET Web Page
The download available at the end of this article includes the complete source code for the custom GridView and BoundField controls, as well as a demo ASP.NET website. To use the skmControls2 controls in an ASP.NET website, copy the DLL to the website's /Bin directory and then add the following @Register directive to the tops of the .aspx pages where you want to use the controls:

<%@ Register Assembly="skmControls2" Namespace="skmControls2" TagPrefix="skm" %>

(Alternatively, you can add this @Register directive in the Web.config file so that you do not need to add it to every ASP.NET page that uses the controls. See Tip/Trick: How to Register User Controls and Custom Controls in Web.config.)

Then just use the same GridView declarative markup you normally would, but replace the "asp" in the <asp:GridView> tags with "skm", as in <skm:GridView>. That's it! Likewise, if you're using any BoundFields that have their HtmlEncode properties set to True (the default), you will want to replace the "asp" in <asp:BoundField with "skm".

The demo included in the download has an ASP.NET page that shows the ProductID, ProductName, CategoryName, UnitPrice, and Discontinued fields from the Northwind's Products database table in a sortable GridView using BoundFields (namely, skmControls2 BoundFields), a TemplateField, and a CheckBoxField.

<skm:GridView ID="ProductsGrid" runat="server" AutoGenerateColumns="False" DataSourceID="NorthwindDataSource" AllowSorting="True">
   <Columns>
      <skm:BoundField DataField="ProductID" HeaderText="ID" InsertVisible="False"
         SortExpression="ProductID" />
      <asp:TemplateField HeaderText="Name" SortExpression="ProductName">
         <ItemTemplate>
            <asp:Label runat="server" Text='<%# Bind("ProductName") %>' id="Label1" Font-Bold="True"></asp:Label>
         </ItemTemplate>
      </asp:TemplateField>
      <skm:BoundField DataField="CategoryName" HeaderText="Category Name" SortExpression="CategoryName" />
      <skm:BoundField HtmlEncode="False" DataFormatString="{0:c}" DataField="UnitPrice" HeaderText="Price" SortExpression="UnitPrice" />
      <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued" SortExpression="Discontinued" />
   </Columns>
</skm:GridView>

Note that the grid's declarative markup does not set the ArrowUpImageUrl or ArrowDownImageUrl properties. Consequently, the grid displays the standard up and down arrow images (see the first screen shot below). If you want to use custom arrow images, specify the URL either declaratively or programmatically. The properties can be set to absolute URLs (like http://www.example.com/images/UpArrow.gif) or relative paths using the ~ character, like ~/Images/UpArrow.png. The demo page includes a checkbox that allows you to toggle between using the default arrow images and custom ones.

The following two screen shots show the demo page in action. The first one shows the grid sorted by price in descending order. The built-in up and down arrow images are used here.

The grid sorted by price in descending order

The second image shows the grid sorted by product name in ascending order. Here, a custom up arrow image is used.

The grid sorted by product name in ascending order

Custom BoundField Markup is Lost When Using the Fields Dialog Box
If you use the custom BoundField controls from the skmControls2 library - e.g., <skm:BoundField ... /> - it is important to know that if you use the Fields dialog box to modify the GridView's columns, Visual Studio will replace the custom BoundField markup with the default markup (namely, <asp:BoundField ... />). The Fields dialog box is the dialog box displayed when you click the "Edit Columns" link from the GridView's Smart Tag.

Conclusion
In this article we looked at building a custom GridView server control (along with a custom BoundField control) in order to automatically display up and down arrow images for the sorted column. This arrow provides the user with visual feedback as to what column the data is sorted by, as well as whether the data is sorted in ascending or descending order. Best of all, this functionality is wrapped up in a custom server control, so you can utilize this behavior without having to write any code in your ASP.NET page's code-behind class. Be sure to download the source code and the demo application available at the end of this article.

Happy Programming!

  • By Scott Mitchell
  • Source: http://aspnet.4guysfromrolla.com/articles/012308-1.aspx

    No comments: