Server-side ASP.NET Data Binding, Part 2: Customizing the DataGrid Control
Dino Esposito
Download the code for this article:Cutting0104.exe (46KB)
Browse the code for this article at Code Center: ASP.NET Data Binding 2

L

ast month (March 2001) I covered the basic techniques of binding ASP.NET server controls to fields of Microsoft® .NET managed data sources. In particular, I focused on the main features of the DataGrid control, which is probably the richest of all the ASP.NET list-bound controls. Unless you have very special needs, using a DataGrid is as easy as binding the .NET data source with the control and setting the size of the page. The control is capable of auto-generating the columns and exposes a wide variety of graphical properties. As a result, you can get a table report like the one in Figure 1 with surprisingly simple server-side code. Everything you need is only a few properties away.

Figure 1 Table Report
Figure 1 Table Report

      Data reporting is a common task for developers. However, when programming in Visual Basic®, developers tend to avoid using data-bound controls in real-world projects. One reason is that these controls become unmanageable when a project starts to scale upwards. Moreover, they force you to adopt their user interface. Developers want self-contained, ready-to-use tools that will help with data reporting, and they want them to be highly customizable. More importantly, these tools have to provide hooks for influencing the appearance and the behavior of any single piece that forms the overall user interface. The ASP.NET DataGrid meets most of these requirements.
      In the DataGrid programming interface, the abundance of customization features make the object model extremely rich, but not unwieldy. The ASP.NET team clearly spent considerable time designing the DataGrid control. In this column, I'm going to explore the customization features that make the DataGrid the ultimate off-the-shelf tool for ASP.NET development. In particular, I'll cover user-defined navigation between pages, user-bound columns, sorting, and templatized columns.

Hooking DataGrid Events

      Last month I only touched on DataGrid events and item creation. This month I'll expand on these topics because they're extremely important when building advanced functionality. Figure 2 lists the events exposed by the Web control. For now, let's skip the XxxCommand events, which I'll cover later when talking about bindable columns. Init, Load, and Unload are basic events in any control lifecycle. You normally don't need to take any particular action to respond to these events. PageIndexChanged is fundamentally important if you want to page your list of records. This event lets you refresh the data source to reflect the current page. You'll also have to use it if you plan to implement a custom pager, as I'll be doing in a moment. SelectedIndexChanged is equally important in the context of the item selection functionality.
      In last month's Cutting Edge, I hastily presented a technique to add some action to the hyperlink buttons used to move across pages. In particular, I showed how to highlight the clickable text currently under the mouse. The technique hooks the ItemCreated event and gets into the game whenever the pager is created.

  public void 
  
NewItem(Object Sender, DataGridItemEventArgs e)
{
ListItemType itemType = e.Item.ItemType;
if (itemType == ListItemType.Pager) {
TableCell pager = (TableCell)e.Item.Controls[0];
// more code...
}
}

 

      Next, all the child controls are enumerated through the pager's Controls collection:

  for (int n=0; n<pager.Controls.Count; n+=2) {
  
// n.th control is pager.Controls[n]
}

 

Notice that you must move the index by two. This is because the standard pager generates a code structure like the one in Figure 3. It's a table cell whose content is a blank-separated string formed by just one <SPAN> and as many <A> tags as needed. The <SPAN> represents the index of the current page. The <A> tags represent all the other pages. In nonnumeric mode, you only have two tags, either <SPAN> or <A>. Between these tags there's always a blank space (&nbsp;). Even this simple character in ASP.NET is rendered through a Web controlâ€"a System.Web.UI.LiteralControl in particular. That's why you must step by two.

Figure 3 Pager Code Structure
Figure 3 Pager Code Structure

      Once you hold a reference to the individual control for a pager button, there's nothing you can't do. You can apply any Cascading Style Sheets (CSS) style you want, including behaviors, depending on the tag type (<SPAN> or <A>). In addition, you can modify the text being displayed in numeric mode. (You can already decide the text of the buttons through handy properties, but you must do so in NextPrev mode.)
      There are two ways to recognize the type of control being created. One is by name, as shown here.

  String szLnk;
  
szLnk = "System.Web.UI.WebControls.DataGridLinkButton";
WebControl c = (WebControl) pager.Controls[n];
if (c.GetType().ToString() == szLnk)
c.CssClass = "PagerLink";

 

You instantiate a generic variable of type WebControl and then check its type against the two possible .NET types representing <SPAN> and <A> tags: System.Web.UI.WebControls.Label and System.Web.UI.WebControls.DataGridLinkButton.
      Another technique exploits the built-in exception-handling mechanism. Basically, you cast the nth control of the pager to Label or LinkButton in a try/catch block. Since those are the only two possible types you can have, either the try statement is successful or you fall into the right handler.

  try {
  
Label l = (Label) pager.Controls[n];
l.Text = "Page " + l.Text;
l.CssClass = "PagerSpan";
}
catch {
LinkButton h = (LinkButton) pager.Controls[n];
h.Text = "[ " + h.Text + " ]";
h.CssClass = "PagerLink";
}

 

      In Figure 4 you can see a slightly modified, yet perfectly acceptable, pager obtained with the previous code. You can use the Text property to set the text and the CssClass property to set all the user interface settings. Notice that behaviors can be extremely useful for adding more action to selected buttons. Don't be scared by the scarce support for behaviors you get outside Microsoft Internet Explorer 5.0 and higher. They are just ignored by all other browsers.

Figure 4 Pager with Colored Rows
Figure 4 Pager with Colored Rows

A Custom Pager Control

      Just by hooking the ItemCreated event, you can introduce significant modification to the pager. However, you could do more; you could completely replace the standard pager with a brand new component. To do so, simply hide the system-provided pager and insert your own paging controls. In this case, you're responsible for manually setting which page to display, but you can decide to move any number of pages at a time, jump to a specific page, or display extra text. Let's see how to create a pager with a descriptive text and a textbox to type in the page number.
      A DataGrid's pager should take care of the following tasks: setting the current page, rebinding to the data source and, more importantly, supplying the new user interface. If you just don't want the built-in pager, disable it through the DataGrid's PagerStyle-Visible attribute.

  <asp:datagrid id=DataGrid1 
  
runat="server"
PagerStyle-Visible = "False"
AllowPaging = "True" >

 

      At this point, there's just no way for you to page back and forth unless you define your own controls and teach them how to comply with the DataGrid's current page. Your custom pager is a piece of HTML code that's separate from the DataGrid. You could place it just below the table of recordsâ€"where the built-in pager would beâ€"or in any other corner of the page. Since it is a completely distinct piece of HTML, there's nothing to prevent you from displaying both the built-in and the custom pagers. In this case, you could consider the custom interface to be an extension of the standard one.
      To make the customized pager appear below the DataGrid and share the same width and user interface settings, just wrap both the DataGrid and the pager in a <TABLE>:

  <table class=PagerTable>
  
<tr><td>
<asp:datagrid ...>
</asp:datagrid>
</td></tr>
<tr><td>
your pager here
</td></tr>
</table>

 

      Suppose you're going to use the code in Figure 5. The pager is composed of a label followed by a TextBox and a button. In particular, the button component looks like this:

  <asp:button id="gotopage" 
  
runat="server"
text = "Go"
cssclass = "PagerPush"
CommandArgument = "goto"
OnClick = "GotoButtonClick" />

 

It has a CommandArgument property and an OnClick handler. CommandArgument represents the verb behind the button. It is a word, or a verb, that describes what's going to happen when the button is pressed. Of course this is supposed to have a unique name, in case you introduce more buttons. You don't strictly need it if you have only a single button. The OnClick handler points to the function responding to the user's click.

  void GotoButtonClick(Object sender, EventArgs e) 
  

 

      To access the button object and read the command argument, do the following:

  String cmd = ((Button)sender).CommandArgument;
  

 

      At this point, you only need to update the current page index and refresh the data source. CurrentPageIndex is a zero-based counter that ranges up to the value of the PageCount property of the DataGrid object minus one. You might want to make sure that any assigned value falls in this range. So the value read from the textbox

  int nPageNumber = PageNumber.Text.ToInt32()-1;
  

 

will be modularized against 0 and PageCount. To handle incorrect nonnumeric text being typed in the textbox, just use a try/catch block:

  try { 
  
nPageNumber = PageNumber.Text.ToInt32()-1; }
catch {
// if wrong content, don't move
nPageNumber = DataGrid1.CurrentPageIndex; }

 

      Once you've properly set the CurrentPageIndex property, you need to rebind the data source to make sure that the content shown reflects the current page. The code in Figure 5 always uses the same function (BindData) to load the ADO.NET dataset, regardless of the particular page to be displayed. As you may have guessed already, this is not the optimal solution. There's no need for the client to download the entire dataset created by the query. You should either apply filters to the SQL command or force the CreateDataSource function to return a smaller collection of rows based on the current page index. However, if you manage to use page-sized datasets, then remember to set the DataGrid's AllowCustomPaging property to True. If you don't, the first record displayed will be the one that occupies this position:

  CurrentPageIndex * PageSize + 1
  

 

      As you can see, this will result in erroneous results if you try to view page two with a total of PageSize records in the dataset. When custom paging is allowed instead, the DataGrid always displays the records from position 0, which would be perfect in this case. Figure 6 shows Figure 5's code in action. Similar examples can be found at https://www.gotdotnet.com/quickstart/aspplus.

Figure 6 Reflecting Current Page
Figure 6 Reflecting Current Page

Sorting Columns

      The DataGrid component also provides basic support for sorting. This ability depends on the sorting capabilities of the underlying data source. The data source is always responsible for returning a sorted set of records based on the specified sort fields. The DataGrid, in turn, provides a straightforward programming interface to turn this feature on. The built-in sorting mechanism is triggered by the AllowSorting property:

  <asp:datagrid id=DataGrid1 runat="server" 
  
AllowSorting="True"
OnSortCommand="SortByColumn">

 

      When AllowSorting is set to True, the DataGrid utilizes a LinkButton to render the caption of the column. The OnSortCommand event is fired whenever you click on any of the column captions.

  void SortByColumn(
  
Object sender,
DataGridSortCommandEventArgs e)

 

      The built-in sorting mechanism doesn't allow you to choose which column to sort. All displayed columns are therefore sortable. By handling the sort command, you are supposed to rebind to the data source and obtain sorted data to display again. The field on which to sort is passed through the SortField member of the DataGridSortCommandEventArgs class. Assuming you have a function called BindData to refresh the link between the DataGrid and data source, this is the typical body of OnSortCommand:

  BindData((String) e.SortField);
  

 

      The problem with sorting is keeping track of the current sorting field. Some of the samples available with the .NET Framework documentation use a global variable. While this solution is perfectly functional, I'd rather use the Attributes collection of the DataGrid object. You initialize the page like this:

  void Page_Load(Object Sender, EventArgs e) { 
  
// defaults to natural order
BindData(DataGrid1.Attributes["SortField"]);
}

 

      SortField becomes a custom attribute of the DataGrid object. If not defined, it returns an empty string. BindData will then use the argument as the sort field when creating the data source:

  // if no sort field, maintain current
  
if (sortField != "")
DataGrid1.Attributes["SortField"] = sortField;

// use current sort field
DataGrid1.DataSource = CreateDataSource(
DataGrid1.Attributes["SortField"]);

 

      To establish a sort order in an ADO.NET DataTable, just set the view's Sort field properly:

  DataView dv;
  
dv = oDS.Tables["EmployeesList"].DefaultView;
if (sortField != "")
dv.Sort = sortField;
return dv;

 

      Sorting the standard way is extremely easy, but it's not an all purpose solution. If you don't want to allow sorting for all columns, or if you want to put a bitmap on the header, you need custom sorting. You define custom sorting by including bound columns and template columns in the grid.
      Sorting is probably the feature that is best-suited to exploit the browser's capabilities. If the browser can refresh the page, then there's no reason to go back to the server to get the same dataset simply to sort it differently.

Binding Columns

      So far I've only considered DataGrids whose columns were generated automatically and ended up perfectly matching the total number of columns in the dataset. To disable this feature just set

  AutoGenerateColumns = False
  

 

Once you do this, you're responsible for providing columns yourself. The <asp:BoundColumn> tag allows you to do this. The following code is functionally equivalent to the code shown in Figure 6, but lets you handle the various columns as distinct objects.

  <asp:datagrid ...>
  
<property name="Columns">
<asp:BoundColumn DataField="employeeid"
HeaderText="Employee ID" />
<asp:BoundColumn DataField="EmployeeName"
HeaderText="Name" />
<asp:BoundColumn DataField="title"
HeaderText="Title" />
</property>
</asp:datagrid>

 

      This code has an immediate effect: you can now assign a custom caption to the columns through the HeaderText attribute. DataField is the name of the data source field to which the column is bound. You can adjust the interface of the columns by setting a number of graphical properties for the column as well as its header and footer. These attributes include font, colors, styles, borders, and alignment and apply on a per-column basis. For example, you can have a column with a different font style and a uniform background color regardless of each item's corresponding settings.
      Another pretty cool feature is the ability to specify an output mask for numeric values in the column. The key attribute of this feature is DataFormatString. Basically, this property contains a two-part, colon-separated string that indicates the rendering format for the data in the cell.
      DataFormatString is in the form {A:Bxx}, where A stands for the nth block of text (also called a parameter) in the cell. For data columns, this value can only be 0 since the content is always considered as a whole. The xx in the mask means the number of digits to meet. B specifies the format to display the value according to the table in Figure 7. The formatting character is not case-sensitive, with the sole exception of X/x, which will display the hexadecimal characters in the case specified. If you want a numeric representation with a given number of digits, then the final result will depend on the format. For example, the number 12345 with a mask of {0:D8} will be rendered with eight digits as 00012345. 12345.6789 with a mask of {0:E10} becomes 1.2345678900E+004. DataFormatString happens to be the simplest way to format the content of a column, but there's much more you can do, as I'll explain in a moment.

Sorting Bound Columns

      To make a column sortable, just add the SortField attribute to the <asp:BoundColumn> element. For example:

  <asp:BoundColumn HeaderText="ID" 
  
headerstyle-horizontalalign="Center"
DataField="employeeid"
SortField="employeeid" />

 

This attribute works in conjunction with AllowSorting and OnSortCommand, which I examined earlier. The SortField attribute simply allows you to decide which columns must be sortable, when, and by which field. You can also use an image to paint the column's caption. In this case, just use the HeaderImageUrl property, and set the URL to an image to display (instead of text) on the header of the column. If you want both text and image, you can embed an <img> in the HeaderText property.
      You might want to signal that a column is sorted by showing a little bitmap or using a new font style or color. The following code snippet shows how to draw the caption of the sorting column in yellow.

  // Update the columns headers
  
for (int i=0; i<DataGrid1.Columns.Count; i++)
{
TableItemStyle tis;
tis = (TableItemStyle) DataGrid1.Columns[i].HeaderStyle;

// reset to normal
tis.ForeColor = System.Drawing.Color.White;

// draw to reflect sorting
if (DataGrid1.Attributes["SortField"] ==
DataGrid1.Columns[i].SortField)
tis.ForeColor = System.Drawing.Color.Yellow;
}

 

The final effect is shown in Figure 8, where you can see how the page looks in Internet Explorer 5.5.

Figure 8 The Page in Internet Explorer 5.5
Figure 8 The Page in Internet Explorer 5.5

Types of Columns

      In order to manipulate the content and, more importantly, the layout of the columns, you can bind them dynamically by choosing from five different types: bound columns, button, hyperlink, edit, and template columns. Bound columns are just ordinary columns that mirror the content of a data field. Using bound columns is advantageous with respect to auto-generated columns only because you can decide on the styling, sorting, and position. But what if you need to associate an action with the items of a certain column? And what if you need it to show the result of a dynamically evaluated expression rather than the static text coming from the data source? What if you want to select, click, and edit the underlying row? In these cases you just need more powerful tools like the other types of DataGrid columns.
      Hyperlink and button columns have similar features. Both allow you to display clickable elements on each row in the grid and define custom functionality for them. Hyperlink buttons display information as hyperlinks, and when clicked they navigate to a separate page that provides details about the selected functionality. The following is the typical schema of such an element:

  <asp:HyperLinkColumn
  
HeaderText="..."
DataNavigateUrlField="..."
DataNavigateUrlFormatString="..."
DataTextField="..."
Target="_self"/>

 

      HeaderText is the title of the column, and Target represents the window or target frame that is used to display the contents resulting from the navigation. Use _self if you want to navigate to the same frame. The key attribute is DataNavigateUrlFormatString. It usually looks like this:

  page.aspx?param1={0}
  

 

The {0} substring is a placeholder for the corresponding value in the column specified by DataNavigateUrlField. There's no way to use more than one replaceable symbol. DataTextField is the name of the data source field you might want to associate with the column's Text property. If you just want a constant text to be displayed for all the columns, use the Text property instead. You're given some leeway to customize the data-bound text through the DataTextFormatString. The following code originates the page on the left-hand side of Figure 9.

Figure 9 The Pager with Hyperlinks and the Result of the Click
Figure 9 The Pager with Hyperlinks and the Result of the Click

  <asp:HyperLinkColumn HeaderText="More..."
  
headerstyle-horizontalalign="Center"
itemstyle-horizontalalign="Center"
DataTextFormatString = "Details about {0}"
DataTextField = "employeeid"
DataNavigateUrlField = "employeeid"
DataNavigateUrlFormatString="more.aspx?id={0}"
Target="_self" />

 

      On the right-hand side of Figure 9 you can see a button column that originated from the following text:

  <asp:ButtonColumn HeaderText="More Info" 
  
headerstyle-horizontalalign="Center"
itemstyle-horizontalalign="Center"
Text="..."
CommandName="MoreInfo" />

 

A button column draws a button with text. DataTextField and DataTextFormatString work as they do for HyperLink columns. A button can be rendered in two ways: like a hyperlink (the default) or like a pushbutton. In the latter case, just add

     ButtonType = "PushButton"
  

 

      To handle the click on a button, in the DataGrid define an attribute called OnItemCommand and make it point to a function with the following prototype:

     void Click(Object sender, DataGridCommandEventArgs e) 
  

 

In the code, you first check the value of the button's CommandName property, then call the appropriate handler code. For instance, the asp:ButtonColumn declaration shown earlier sets MoreInfo as the CommandName when the "More Info" column is clicked. The handler might look like this:

     LinkButton b = (LinkButton) e.CommandSource;
  
if (b.CommandName == "MoreInfo")
{...}

 

      The difference between button and hyperlink is minimal anyway. You should use a hyperlink if the consequent action requires the use of a new page. Otherwise, a button is more appropriate.

Template Columns

      Before I get into template columns, let me just mention edit columns. These columns are extremely powerful because they allow you to create in-place editing dialogs. However, since this is an important topic that deserves more space, I'll revisit it next month.
      Think of templatized columns as the ultimate way to format the content of a given column. DataFormatString is helpful as well as extremely easy to use. However, its formatting mask only applies to one data field. What if you want to combine in a single column the content of more fields by applying different styles? For example, you might want a column with the first and last name run together, but with the last name rendered in bold. While the following technique won't get you too far, it can sometimes be a simple workaround for similar problems. Since the text in DataGrid columns is plain HTML, you could insert some HTML formatting in a Microsoft SQL Serverâ„¢-computed column. For example:

  String strCmd = "";
  
strCmd += "SELECT ";
strCmd += "firstname + '<b> ' + lastname + '</b>';
strCmd += " AS EmployeeName FROM Employees";

 

      However, template columns are the perfect solution to create combinations of HTML text and controls, and provide a completely custom and data-bound layout for a column. For example, you can use a template column to render a data source Boolean value through a read-only checkbox control or to dress up the column's layout with images. Let's see how it works. You can insert a template anywhere in the DataGrid and have it host any combination of columns.

  <asp:TemplateColumn 
  
HeaderText="Employee"
headerstyle-horizontalalign="Center"
SortField="title">
•••
</asp:TemplateColumn>

 

      The column template is specified through the <template> tag. A template is characterized by a Name attribute, which defines how items have to be rendered. For DataGrids, the Name attribute must be ItemTemplate:

  <template name="ItemTemplate"> 
  
•••
</template>

 

      The body of <template> can contain any combination of static text, HTML elements, and ASP.NET server controls. Let's get rid of the EmployeeName column and ask the data source to return the following query:

  SELECT employeeid, firstname, lastname, 
  
titleofcourtesy, title
FROM Employees

 

      Now let's see how to get the same EmployeeName column through templates. In this case, you don't need more than a label control to render any column cell.

  <asp:label runat="server"
  
Text="..."
</asp:label>

 

      The Text attribute is data-bound. Here's a possible value for it:

  Text='<%# 
  
DataBinder.Eval(Container.DataItem, "titleofcourtesy") +
" " +
DataBinder.Eval(Container.DataItem, "firstname") +
" <b> " +
DataBinder.Eval(Container.DataItem, "lastname") +
" </b>"
%>' >

 

      You can use any kind of element in the data binding expression provided that there's a way to evaluate it at runtime. DataBinder.Eval is a static method that evaluates late-bound data binding expressions. The first two arguments must be the naming container for the data item and the data field name. In a DataGrid control, the naming container is always Container.DataItem. Another possible naming container is Page, but this applies to simple data binding and outside template-based data controls such as DataGrid, DataList, and Repeater. DataBinder.Eval can also take a third optional argument, which is the format string for the resulting text. Using DataBinder.Eval is not a must, but it saves you from creating less readable code. For example,

  DataBinder.Eval(Container.DataItem, "employeeid", "{0:g}")
  

 

evaluates to

  String.Format("{0:g}", 
  
((DataRowView)Container.DataItem)["employeeid"]);

 

      In Figure 10 you can see that the employee name is the combination of three different fields and the last name has been rendered in bold. You can also see a checkbox in one of the columns.

Figure 10 Name Composed from Different Fields
Figure 10 Name Composed from Different Fields

      The Northwind database Employees table has a field called ReportsTo, which is normally set to an ID identifying the employee's boss. The checkbox is set if the employee reports to someone.

  <template name="ItemTemplate">
  
<asp:checkbox runat=server enabled=false
checked='<% # HasBoss((int) DataBinder.Eval(
Container.DataItem, "reportsto"))
%>' />
</template>

 

      Now the template is given by an <asp:checkbox> control, whose state is determined by the result of the HasBoss function. The function takes an int argument which is the content of the ReportsTo field.

  bool HasBoss(int bossID)
  
{
if (bossID != 0)
return true;
return false;
}

 

      When I first ran this code, I came across a rather weird invalid cast exception. After a bit of investigation, I found that it happened because the ReportsTo column can contain NULL values and you cannot cast from System.DBNull to int. The solution was straightforward: just make SQL Server return a slightly modified dataset.

  SELECT employeeid, firstname, lastname, 
  
titleofcourtesy, title,
ISNULL(reportsto, 0) AS reportsto
FROM Employees

 

Now any NULL value is automatically replaced by 0, and everything works just fine.

What's Up Next?

      The previous caveat is useful because it reminds you that bindable and templated columns are extremely useful, but you'll benefit greatly from a solid understanding of the SQL language. The next step in this tour of ASP.NET and ADO.NET data binding is adding a bit of interactivity to an otherwise static grid, namely particular item selection and in-place editing.

Dino Esposito is a trainer and consultant based in Rome, Italy. Author of several books for Wrox Press , he now spends most of his time teaching classes on ASP.NET and ADO.NET for Wintellect (https://www.wintellect.com). Get in touch with Dino at dinoe@wintellect.com.

From the April 2001 issue of MSDN Magazine.