Microsoft & .NET ASP Dynamic Template Columns in the ASP.NET 2.0 GridView Control

Dynamic Template Columns in the ASP.NET 2.0 GridView Control

One of the nice things about ASP.NET is its depth: the sheer number
of tools and techniques built into this Web application framework can
be quite staggering. Recently I was involved in a project where we
needed to present the results of a database query as part of an
ASP.NET application, but we needed extensive control over the on-screen
formatting, down to the level of controls used to present individual
columns of data, CSS classes used, and more. To make matters even
trickier, we didn’t know until runtime what the query would be. After
some discussion and experimentation among the design team, though, we
decided that there was no need to buy a third-party control to handle
these demands. The built-in GridView could handle all of our
requirements. The key lay in understanding and using the little-known
ability to add columns to the GridView dynamically using templates at
runtime.

A GridView template is a class that implements the ITemplate interface.
It defines the controls that will be displayed on the GridView in a
column, how they will bind to data, and can have special-case code to
handle headers and footers. In this article I’ll show you a simplified
example of building a GridView up from scratch using a template to respond
to a dynamic query; the technique can be extended to cover much more
complex situations.

The Template Class

Let’s start with the Template class itself. This is the class that
holds the code that will do the actual heavy lifting of putting controls
in the DataGrid, as well as formatting them and binding them to data. It
starts off with some private member variables and a constructor to set
them:


// dynamically added label column
public class GridViewLabelTemplate : ITemplate
{

    private DataControlRowType templateType;
    private string columnName;
    private string dataType;

    public GridViewLabelTemplate(DataControlRowType type, 
        string colname, string DataType)
    {
        templateType = type;
        columnName = colname;
        dataType = DataType;
    }

The next block of code gets called whenever an instance of this
template is instantiated. If you think of a template as corresponding to a
column in the GridView, this happens every time a header, cell, or footer
of the GridView is created for that column. You can inspect the
templateType member to figure out which of these is the case.
Here, you want to create whatever control or controls you need to display
the data. You’re not limited to a single control, though for this article
I’m only using one label for display. You can also do whatever you need to
format the control to your liking. I’m going to grab the container for the
control (which ends up being the wrapping table cell) and set its CSS
style so that I can right-justify numeric columns. This method also sets
up for data-binding by registering an event handler.


public void InstantiateIn(System.Web.UI.Control container)
{
    DataControlFieldCell hc = null;

    switch (templateType)
    {
        case DataControlRowType.Header:
            // build the header for this column
            Literal lc = new Literal();
            lc.Text = "<b>" + BreakCamelCase(columnName) + "</b>"; 
            container.Controls.Add(lc);
            break;
        case DataControlRowType.DataRow:
            // build one row in this column
            Label l = new Label();
            switch (dataType)
            {
                case "DateTime":
                    l.CssClass = "ReportNoWrap";
                    break;
                case "Double":
                    hc = (DataControlFieldCell)container;
                    hc.CssClass = l.CssClass = "ReportNoWrapRightJustify";
                    break;
                case "Int16":
                case "Int32":
                    hc = (DataControlFieldCell)container;
                    hc.CssClass = l.CssClass = "ReportNoWrapRightJustify";
                    break;
                case "String":
                    l.CssClass = "ReportNoWrap";
                    break;
            }
            // register an event handler to perform the data binding
            l.DataBinding += new EventHandler(this.l_DataBinding);
            container.Controls.Add(l);
            break;
        default:
            break;
    }
}

As you’d expect, the event handler you set up for databinding gets
called when data is bound to the GridView. In this case, I’m going to use
this event handler to do some formatting of the bound data:


private void l_DataBinding(Object sender, EventArgs e)
{
    // get the control that raised this event
    Label l = (Label)sender;
    // get the containing row
    GridViewRow row = (GridViewRow)l.NamingContainer;
    // get the raw data value and make it pretty
    string RawValue = 
        DataBinder.Eval(row.DataItem, columnName).ToString();
    switch (dataType)
    {
        case "DateTime":
            l.Text = String.Format("{0:d}", DateTime.Parse(RawValue));
            break;
        case "Double":
            l.Text = String.Format("{0:###,###,##0.00}", 
                Double.Parse(RawValue));
            break;
        case "Int16":
        case "Int32":
            l.Text = RawValue;
            break;
        case "String":
            l.Text = RawValue;
            break;
    }
}

The last thing in my template class is a little helper method that’s
used in displaying column headers. Here I’m making an assumption about
naming conventions in my database – that column names are all CamelCase,
and that I’d prefer to display these on the GridView interface as
individual words broken at the obvious points.


// helper method to convert CamelCaseString to Camel Case String
// by inserting spaces
private string BreakCamelCase(string CamelString)
{
    string output = string.Empty;
    bool SpaceAdded = true;

    for (int i = 0; i < CamelString.Length; i++)
    {
        if (CamelString.Substring(i, 1) == 
            CamelString.Substring(i, 1).ToLower())
        {
            output += CamelString.Substring(i, 1);
            SpaceAdded = false;
        }
        else
        {
            if (!SpaceAdded)
            {
                output += " ";
                output += CamelString.Substring(i, 1);
                SpaceAdded = true;
            }
            else
                output += CamelString.Substring(i, 1);
        }
    }

    return output;
}

The Test Page

To test this, I banged together a simple ASP.NET page consisting of
three controls:

  • A TextBox control named txtQuery
  • A Button control named btnDisplay
  • A GridView control named grdMain. The GridView control has its
    AutoGenerateColumns property set to False.

The idea is simple: when the user clicks the button, I’ll build a new
DataTable based on whatever text is entered in the TextBox (so I’m
depending on the user to enter a valid SQL query; naturally, in a
production application, you’d want to do some error checking!). Then the
code will walk through all of the columns of the DataTable and add one
dynamic column to the GridView for each DataTable column. Here’s how it
looks in code:


protected void btnDisplay_Click(object sender, EventArgs e)
{
    // create new DataTable from user input
    string connectionString = 
        "Data Source=localhost;Initial Catalog=AdventureWorks;"
        + "Integrated Security=True";
    SqlConnection conn;
    conn = new SqlConnection(connectionString);
    DataTable dtReport = new DataTable();
    SqlCommand cmd = new SqlCommand(txtQuery.Text);
    cmd.CommandType = CommandType.Text;
    cmd.Connection = conn;
    SqlDataAdapter da = new SqlDataAdapter();
    da.SelectCommand = cmd;
    da.Fill(dtReport);

    // clear any existing columns
    grdMain.Columns.Clear();

    // walk the DataTable and add columns to the GridView
    for (int i = 0; i < dtReport.Columns.Count; i++)
    {
        TemplateField tf = new TemplateField();
        // create the data rows
        tf.ItemTemplate = 
            new GridViewLabelTemplate(DataControlRowType.DataRow, 
            dtReport.Columns[i].ColumnName, 
            dtReport.Columns[i].DataType.Name);
        // create the header
        tf.HeaderTemplate = 
            new GridViewLabelTemplate(DataControlRowType.Header, 
            dtReport.Columns[i].ColumnName, 
            dtReport.Columns[i].DataType.Name);
        // add to the GridView
        grdMain.Columns.Add(tf);
    }

    // bind and display the data
    grdMain.DataSource = dtReport;
    grdMain.DataBind();
    grdMain.Visible = true;
}

The only tricky part is the little bit of plumbing that actually hooks
the template class up to the GridView. This is accomplished by creating a
new TemplateField object, and telling the TemplateField what template to
use for its ItemTemplate and HeaderTemplate (you can set other templates
as well, such as the AlternatingItemTemplate and FooterTemplate, and they
need not all point to the same template class).

Figure 1 shows the whole thing in action. This version isn’t
excessively pretty because I stripped it down to just the essentials, but
it demonstrates enough that you should be able to add your own formatting
back in when you need it.

Building a dynamic GridView

Where Do You Go From Here?

This technique can be extremely powerful when you want to use some of
the built-in services of the GridView framework (such as the overall
databinding and ability to set cell foreground and background colors) and
yet maintain close control over your data. While I can’t share much code
from
our production application with you, I can indicate a couple of the areas
where we pushed this technique even further. First, depending on the
nature of your data, it may make sense to build special cases within your
code to handle particular columns. For example, we’ve also implemented a
hyperlink template column that accepts both text to display and a URL to
link to:


public class GridViewHyperlinkTemplate : ITemplate
{
    private DataControlRowType templateType;
    private string columnName;
    private string url;
    private string text;

    public GridViewHyperlinkTemplate(DataControlRowType type, 
        string colname, string URL, string Text)
    {
        templateType = type;
        columnName = colname;
        url = URL;
        text = Text;
    }

    public void InstantiateIn(System.Web.UI.Control container)
    {
        switch (templateType)
        {
            case DataControlRowType.Header:
                Literal lc = new Literal();
                lc.Text = "<b>" + columnName+ "</b>"; 
                container.Controls.Add(lc);
                break;
            case DataControlRowType.DataRow:
                HyperLink hl = new HyperLink();
                hl.Target = "_blank";
                hl.CssClass = "ReportNoWrap";
                hl.DataBinding += new EventHandler(this.hl_DataBinding);

                container.Controls.Add(hl);
                break;
            default:
                break;
        }
    }

    private void hl_DataBinding(Object sender, EventArgs e)
    {
        HyperLink hl = (HyperLink)sender;
        GridViewRow row = (GridViewRow)hl.NamingContainer;
        hl.NavigateUrl = DataBinder.Eval(row.DataItem, url).ToString();
        hl.Text = DataBinder.Eval(row.DataItem, text).ToString();
    }

}

Note that the data binding code for this template sets both the
Text and the NavigateUrl of the Hyperlink
control. We use this template in some cases where we can recognize
patterns in the underlying SQL Server data thanks to naming conventions in
our data columns:


for (int i = 0; i < dtReport.Columns.Count;i++ )
{
    // special cases: If SiteName & HomePageURL present, 
    // create hyperlink column
    if (dtReport.Columns[i].ColumnName == "HomePageURL")
    {
        UrlFound = true;
        TemplateField tf = new TemplateField();
        tf.ItemTemplate = 
            new GridViewHyperlinkTemplate(DataControlRowType.DataRow, 
            "Site Name", "HomePageURL", "SiteName");
        tf.HeaderTemplate = 
            new GridViewHyperlinkTemplate(DataControlRowType.Header, 
            "Site Name", "HomePageURL", "SiteName");
        grdReport.Columns.Add(tf);
        continue;
    }
    if ((dtReport.Columns[i].ColumnName == "SiteName") && UrlFound)
        continue;
    // other special cases and general case here ...
}

The other thing to note is that you may also want to get specific
formatting on a row-by-row as well as a column-by-column basis. In this
case, don’t spend a lot of time barking up the template tree! Instead,
you’ll need to dig into the RowDataBound event of the GridView.

The built-in GridView with automatic column creation can probably
handle 95% of your data display needs. But for the other 5%, it’s nice to
know that these powerful techniques exist. Microsoft’s designers didn’t
think of everything, but in ASP.NET 2.0 they did a lot of work to expose
the functionality we need to extend the basic framework, and it’s
certainly made life a lot easier for those of us working with Web
applications.

Click here to download the code.

Mike Gunderloy is the author of over 20 books and numerous articles on
development topics, and the Senior Technology Partner for Adaptive Strategy, a
Washington State consulting firm. When
he’s not writing code, Mike putters in the garden on his farm in eastern
Washington state.

Latest Posts

Related Stories