Templated Columns in Action

Templated Columns in Action

As powerful as it is, a DataGrid control handles data only in terms of columns, and it can display only information that can be expressed by using a data field name. More often than not, these restrictions are too limiting to build a really user-friendly interface. For example, suppose you have three fields to display: first name, last name, and a title of courtesy, such as "Mr.", "Mrs.", or "Dr." The query selects three distinct fields, and your simplest option is to display that data in a DataGrid control that uses three distinct columns. Figure 3-1 shows the results.

Figure 3-1
The DataGrid control must display even simple information in a column format.

Figure 3-1 is clear, but the full name looks unusual spread over three columns, and the layout does not transmit that feeling of informality that makes users—especially nonexpert users—feel comfortable with the application. (You’ll see a marked improvement in Figure 3-2, which I discuss later.) A better approach would be tying together the three fields in a single string, such as "Ms. Davolio, Nancy".

You can ask the database to return a string that is already in the desired format, but such a request typically results in increased network traffic and more work for the database engine. It can also leave you with less useful information at hand. For example, if you return only the concatenated string, you lose any ready-to-use information such as the first and last names and, among other options, your ability to sort by these fields.

In Chapter 1, I discussed the pros and the cons of the database returning data fields in a format that is ready to be displayed. That discussion focused on the CheckBoxList control, but the pros and cons are really the same for the Data­Grid control. You obtain preformatted data more effectively by building precalculated columns that are created in memory in a DataTable object that you then associate with a grid.

When you have to create expressions based on data fields, using templates gives you more flexibility than any other solution. Unfortunately, templated columns do not come free. The TemplateColumn class does have parsing costs and occupies more memory than, say, the BoundColumn class. My advice is to avoid using templated columns for relatively simple tasks that you can accomplish in other ways. For example, if you have a cached Data­Table object that already has all fields in a displayable format, the page will regenerate even faster if you use BoundColumn only. Let’s review the code for creating and using templated columns.

Concatenating Data Fields

The following code is template code that mimics the standard behavior of bound columns:

<asp:TemplateColumn runat="server" HeaderText="Last Name">
        <%# DataBinder.Eval(Container.DataItem, "lastname") %>

You can also use a Label control and fill its Text property with any string, data bound or not. The following code creates a column like the Employee Name column in Figure 3-2. The full source code for the TemplateColumns.aspx application is available on the companion CD.

<asp:TemplateColumn runat="server" HeaderText="Employee Name">
        <asp:label runat="server" Text='<%# 
            DataBinder.Eval(Container.DataItem, "TitleOfCourtesy") + 
            "<b> " + 
            DataBinder.Eval(Container.DataItem, "LastName") + 
            "</b>" + ", " + 
            DataBinder.Eval(Container.DataItem, "FirstName") %>' />
Figure 3-2
The Employee Name column is created by combining ­multiple fields.

The three fields—TitleOfCourtesy, LastName, and FirstName—have been merged into a single string containing some additional HTML attributes. Compared with Figure 3-1, you can see that the resulting single column displays logically related information in a more readable, user-friendly way.

Sorting Templated Columns

Sorting templated columns is similar to sorting any other type of column. You just set the SortExpression property to a comma-separated list of fields and make sure the DataGrid control is properly configured for sorting. (See Chapter 2 for more information on this.)

Of course, you can sort the template column by one or more data source fields, so in the case of Figure 3-2, the Employee Name column can be sorted by any combination of columns present in the grid’s data source but not according to the plain text displayed. If you need to sort the text as it appears in the column, you need to have an in-memory column with that content. Figure 3-3 shows the Employee Name column sorted by lastname. The full source code for the SortableTemplateColumns.aspx application is available on the companion CD.

Figure 3-3
The Employee Name column sorted by the lastname field.
Grouping Columns Under a Single Header

Figure 3-3 demonstrates a little bug in the output of the grid: the last names are not aligned, resulting in a display problem. Nothing is wrong with the code—this is the natural consequence of the string format. Is there a way to ensure that, within the same column, a given data field begins at an absolute position? Using a single-row table to wrap up all the data fields just doesn’t work.

            <asp:label runat="server" Text='<%# 
                DataBinder.Eval(Container.DataItem, "TitleOfCourtesy") %>' 
            /> </td>
            <asp:label runat="server" Text='<%# 
                "<b>" + 
                DataBinder.Eval(Container.DataItem, "lastname") %>' + 
                "</b>, " + 
                DataBinder.Eval(Container.DataItem, "firstname") %>'
            /> </td>

The alignment problem arises between rows, and only a table that encompasses all the cells in the column can solve it effectively. Using child tables poses the subsequent problem of bubbling down font and color settings. A much better approach is using distinct columns placed under the umbrella of a common header. Figure 3-4 demonstrates what I mean.

Figure 3-4
A DataGrid control that groups two columns under the same heading section.

To group even more columns under the same header, you need to manipulate the grid structure—something that can be accomplished only by handling the ItemCreated event. Here’s what the grid’s collection of columns looks like in code:

<asp:DataGrid id="grid" runat="server"
    <asp:BoundColumn runat="server" DataField="employeeid" HeaderText="ID">
        <itemstyle backcolor="lightblue" font-bold="true" />
    <asp:BoundColumn runat="server" DataField="titleofcourtesy" />
    <asp:TemplateColumn runat="server" HeaderText="Employee Name">
            <asp:label runat="server" Text='<%# "<b> " + 
                DataBinder.Eval(Container.DataItem, "LastName") + 
                "</b>" + ", " + 
                DataBinder.Eval(Container.DataItem, "FirstName") %>' 

The columns to group are the BoundColumn column that is currently linked to the titleofcourtesy field, and the templated column whose caption is Employee Name. The BoundColumn column has an empty caption and occupies the second position in the collection.

The ItemCreated event is fired when the control is done preparing the given item. You check the item type by using the ItemType property of the event data structure, as the following code shows. (The full source code for the GroupingColumns.aspx application is available on the companion CD.)

public void ItemCreated(Object sender, DataGridItemEventArgs e)
    ListItemType lit = e.Item.ItemType;
    if (lit == ListItemType.Header)
        // Each cell corresponds to a column header
        TableCell cell;

        // One cell must be dropped because we have one too many.
        // It can be the second or the third, one of the two we
        // want to incorporate.
        cell = (TableCell) e.Item.Cells[1];

        // The second heading (the first of the two) spans to 
        // cover two columns
        cell = (TableCell) e.Item.Cells[1]; 
        cell.ColumnSpan = 2;

After you make sure the event fired because the header of the DataGrid control is being processed, you access the cells that form the header. The cells are contained in the e.Item.Cells collection. Your next goal is to fuse the headers of columns 2 and 3. To do this, you must remove the extra cell from the table row and then create a header cell that spans the other two columns. You can delete the header of either column 2 or column 3. (Bear in mind that the Cells collection is 0-based, just like any other collection in the .NET Framework.) So you might want to drop the column without a caption to save an extra line of code that you would have needed to restore the caption for the column on the left—in this case, column 2. Once the extra cell has been dropped from the table row, you take what is now column 2 and span it over two table columns by using the ColumnSpan property.

Adjusting Column Margins

To make the page more readable, you might want to add a few empty pixels to both horizontal sides of the cell text. The DataGrid control provides the CellPadding property to handle this adjustment. The CellPadding property, which maps to the cellpadding attribute of the HTML <table> tag, has two significant effects, however: it indiscriminately applies to all cells in the grid, and it provides both horizontal and vertical padding.

Other cell formatting options include CellSpacing, which indicates the space between columns, and GridLines, which lets you specify whether you want horizontal lines or vertical lines, or both. By combining the values of properties such as CellPadding, CellSpacing, and GridLines, you end up with a nice report table in which the cell text is easily readable. Figure 3-5 shows the result of setting CellPadding to 5, CellSpacing to 0, and leaving GridLines to its default value of Both.

Figure 3-5
A good mix of values for cell padding, cell spacing, and grid lines improves readability.

Having empty pixels between columns is helpful, especially when you have right-aligned columns. If you set CellSpacing to a nonzero value, the result is worse, as Figure 3-6 shows.

Figure 3-6
Setting CellSpacing to a nonzero value gives you a more cluttered effect.

The root of the problem is that both the CellSpacing and GridLines properties apply to all cells horizontally and vertically. The ideal solution is to set both properties to 0 and pad as needed at the cell level by using the various margin CSS styles, as shown in this example:


The preceding code sets the horizontal distance between two side-by-side cells to 5 pixels. As you know, the DataGrid control is a table made of <tr> and <td> elements. These tags are not affected by the value of the margin CSS style. The margin style produces an effect only if applied to the content of the cell. This behavior is hard coded in the CSS definition; the margin is calculated from the parent control. As a result, if you want the cell text drawn 5 pixels from the left border, you have to wrap the cell text in an HTML tag and set the margin style attributes for the tag.

There is a trick to accomplish this in a declarative way for all columns without resorting to templates. When you use templates, padding text is trivial because the TemplateColumn class requires you to define the exact layout of the cells, as the following code shows:

<asp:TemplateColumn runat="server" HeaderText="...">
<itemstyle ... />
    <span style="margin-left:5;margin-right:5;">
    <%# DataBinder.Eval(...) %>

Notice that using a <span> tag is more efficient than using a Label control because the tag evaluates to a LiteralControl control and therefore does not require any server-side processing. For other types of columns, the key to padding the text effortlessly is to use the DataFormatString property:

<asp:BoundColumn runat="server" 
    DataField="employeeid" HeaderText="ID" 
    DataFormatString="<span style=\"margin-left:5;\">{0}</span>">

The default cell text is represented by the {0} placeholder, which is wrapped by a <span> tag that has appropriate margin values set. The DataFormatString property is not available (and not needed) for templated columns.

Customizing Column Headers

A templated column allows you to define a custom layout for the header and the footer sections. Changing the layout of the header can be problematic if you need to sort that column by an expression. The sorting mechanism is triggered by a HyperLink control that the DataGrid control automatically embeds in the column heading. The href attribute rendered by this HyperLink control generates a postback event when the user clicks the element. The target of the link is a piece of client-side JavaScript code whose internals have not been fully documented yet.

<a href="javascript:__doPostBack(...)" style="color:White;">
    Caption of the column goes here

What this code does, at the highest level of abstraction, is clear: it posts back to the Web server the form contents of the ASP.NET page. In doing so, it passes to the HTTP runtime some extra information—the parameters of __doPostBack—which is responsible for processing the page. At the time this book is being written, you cannot safely make assumptions about the structure and the role of these parameters.

In summary, if you want to change the template of a column heading, by all means do so as long as you don’t need sorting capabilities. If you can’t just eliminate the sorting, use the ItemCreated event to add extra controls to the header. The following code dynamically adds a drop-down list to the header of a template column, allowing you to choose the expression to sort by. Figure 3-7 shows the final result.

public void ItemCreated(Object sender, DataGridItemEventArgs e)
    ListItemType lit = e.Item.ItemType;
    if (lit == ListItemType.Header)
        // Create and fill a drop-down list control
        DropDownList dd = new DropDownList();
        dd.ID = "ddSort";
        ListItem li1, li2, li3;

        // ListItem constructor takes Text and Value for the item
        li1 = new ListItem("Title of courtesy", "titleofcourtesy");

        li2 = new ListItem("Last Name", "lastname");

        li3 = new ListItem("First Name", "firstname");

        // Select the item, if any, that was selected last time
        dd.SelectedIndex = Convert.ToInt32(grid.Attributes["FieldIndex"]);

        // Add the drop-down list to the header of the 2nd column
        TableCell cell = (TableCell) e.Item.Controls[1];
Figure 3-7
A custom layout for a column header. You pick up a sort expression from the drop-down list and click Sort By to sort.

Customizing the header is useful when you have templated columns that group together several fields. The preceding code creates a dynamic drop-down list with the available sort expressions. Next it retrieves the currently selected expression when users click the header’s link. The header text of the template column must be set to a non-empty string so that the standard infrastructure for sorting can be built to work properly.

<asp:TemplateColumn runat="server" 
    HeaderText="Sort by" SortExpression="*">

Notice the unusual value assigned to the SortExpression property. It plays a critical role in that it allows the sort command handler to recognize that the user clicked a column requiring special treatment for sorting. The SortExpression attribute, in fact, is the only element you have for recognizing the clicked column.

public void SortCommand(Object sender, DataGridSortCommandEventArgs e)
    // Code that retrieves the grid's data source GOES HERE
    if (e.SortExpression != "*")
        dv.Sort = e.SortExpression;
        // Retrieves the drop-down list control through its ID 
        DataGridItem dgi = (DataGridItem) e.CommandSource;
        DropDownList dd = (DropDownList) dgi.FindControl("ddSort");

        // Retrieves the sorting expression from the list
        dv.Sort = dd.SelectedItem.Value; 

        // Persists the currently selected drop-down item
        ViewState["FieldIndex"] = dd.SelectedIndex.ToString();

    // Refreshes the grid

The expression you assign to SortExpression does not matter as long as it allows you to identify the column.