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 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.
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:
- Databinding would commence
- The
InitializeRow
method would be executed for the header row - I'd update the
HeaderText
for the cells in the header row - 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 <
and >
, respectively. In other words, it was transforming a HeaderText
value of:
Price <img border="0" src="up.gif" /> |
To:
Price <img border="0" src="up.gif" /> |
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 second image shows the grid sorted by product name in ascending order. Here, a custom up arrow image is used.
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