Some words about the Gridview.RowDataBound-event

A common situation when using a GridView in an Asp.NET-application is that you might want to manipulate the appearance on some cell based on the value of that cell, for example displaying negative numbers in a different color. Now, I couldn't find any negative numbers in the Northwind sample database so here is my example;

CropperCapture[1] 

This is a product list from the Northwind Database and the goal here is to change the text color of the product name to red if the product is discontinued. To do this we will implement the RowDataBound-event of the GridView. The eventhandler has the following signature;

protected void gvProducts_RowDataBound(object sender, GridViewRowEventArgs e)

Through the EventArgs object e you can now access a property named Row of type GridViewRow that lets you manipulate and access the row that was just databound. Now, here is what you don't do; to read the values in the row you could access the Cells-collection of the row. This example would read the UnitPrice-value and assign it to the foo variable;

string foo = e.Row.Cells[2].Text; //Don't do this

However, this isn't very good since rearranging the columns would break the code, and in this example it would be even more awkward since we would have to access the checkbox control in the last cell and read it's Checked property. What we want is a way to read each value through the corresponding data source column name, i.e. "ProductName", "QuantityPerUnit" etc. Of course it would be nice if we in the code snippet above could write e.Row.Cells["UnitPrice"].Text but this is not possible, instead we will make use of the e.Row.DataItem object;

DataRowView dataItem = (DataRowView)e.Row.DataItem;
if ((bool)dataItem["Discontinued"] == true)
{
    //Do something
}

The DataItem property is of type Object, so we need to cast it to the right type before we can access the values. In this case (where the gridview is bound to an SqlDataSource) the correct type is DataRowView. (More on this here.)

After the cast we can access the columns by indexing the dataItem object with the proper string value. However, running this code will serve us the dreaded "Object reference not set to an instance of an object." error message;

CropperCapture[4]

This is due to the fact that the RowDataBound is raised whenever a row is databound, which also applies to the header row of the gridview. When the header row is bound, e.Row.DataItem will be null because the header row does not reference one of the posts in the data source. So we need to wrap it with the following if statement (bold);

if (e.Row.RowType == DataControlRowType.DataRow)
{
    DataRowView dataItem = (DataRowView)e.Row.DataItem;
    if ((bool)dataItem["Discontinued"] == true)
    {
        //Do something
    }
}

Now we could replace the comment above with the following to make the product name red;

e.Row.Cells[0].ForeColor = Color.Red; //Don't do this

But now we are back to the problem with referencing cells through a hard coded index number, so if the columns are rearranged it might not be the product name that turns red. I googled this a bit and found this and this, but I can't figure out a really good solution to this. The problem with the first link is that looping through the Columns collection that is objects of type DataControlField does not give you a secure access to the data source column name nor the column index. Through the e.Row.DataItem.Row.Table you can access DataColumn objects through which you can access the propertys ColumnName and Ordinal (column position), but it seems that this table object stays true to the column order of the datasource and not the column order in the gridview. Another try at google led me to this hack, which of course is better than hardcoding numbers but still not what I really want. After reading this forum thread i decided to give up, use the hack idea, but instead of using the AccessibleHeaderText property use the SortExpression property since that is almost always the same as the data source column name (if sorting isn't done on multiple columns) and it is automatically set in the aspx markup when working in the design view so no extra work is needed.

So, the previous code snippet will be replaced with this;

int cellIndex = 0;
for (int i = 0; i < gvProducts.Columns.Count; i++)
{
    if (gvProducts.Columns[i].SortExpression == "ProductName")
    {
        cellIndex = i;
        break;
    }
}
e.Row.Cells[cellIndex].ForeColor = Color.Red;

And after some refactoring making use of the C# 3.0 Extension Method feature you get this reusable method;

public static class Extensions
{
    public static int GetColumnIndex(this GridView gv, string columnName)
    {
        int result = -1;
        for (int i = 0; i < gv.Columns.Count; i++)
        {
            if (gv.Columns[i].SortExpression.Split(',')[0].Trim().ToLower() == columnName.ToLower())
            {
                result = i;
                break;
            }
        }

        if (result < 0)
        {            
            throw new Exception("No column in '" + gv.ID + "' has a SortExpression that starts with '" + columnName + "'");
        }

        return result;
    }
}

In this last version I've made some changes to the comparing of the SortExpression and the columnName parameter. By splitting the SortExpression on ',' (comma), trimming it and sending it to lower case I get a less error prone comparison that also will evaluate to true if the SortExpression starts with the columnName given. Also I decided to throw an exception if the columnName asked for is not found, so that such an error is easily found during development.

And finally, the final version of the RowDataBound event;

protected void gvProducts_RowDataBound(object sender, GridViewRowEventArgs e)
{
    if (e.Row.RowType == DataControlRowType.DataRow)
    {
        DataRowView dataItem = (DataRowView)e.Row.DataItem;
        if ((bool)dataItem["Discontinued"] == true)
        {
            int cellIndex = gvProducts.GetColumnIndex("ProductName");
            e.Row.Cells[cellIndex].ForeColor = Color.Red;
        }
    }
}

Instead of manipulating the ForeColor property of the TableCell you should consider assigning the CssClass property instead, and have a style sheet in place for the actual textformatting you want done. This way, when the color needs to be changed all you have to do is change the stylesheet instead of the C# code.

Googled links during the making of this blogpost

4 comments :

  1. These are very useful words about RowDataBound Event in GridView.

    Thank You for sharing it.

    ReplyDelete
  2. Gentlemen,

    If you wanna refer to the column in grid row by ColumnName you just need to do the following in advance:

    In DataGridViewCell > Columns for the column in question you set (Name) property to "XYZ" (real name). By default (Name) property gets the value "DataGridViewTextBox1" as an example.

    Afterwards you just use the syntax, for instance, DataGridView.Rows[e.RowIndex].Cells["XYZ"].Value

    For more info please refer to:
    DataGridViewCell DataGridViewCellCollection [string columnName]

    Enjoy!
    /Alex (ani.kiev@gmail.com)

    ReplyDelete
  3. Alex, you are referring to a windows.forms-control, the blog post is about the gridview control for Asp.net, where the column objects has no Name property as you suggest.

    ReplyDelete