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

Switching to english

When I first started to write this blog I decided to do it in swedish mainly for two reasons;

  1. There aren't many blogs on the topic written in swedish.
  2. It will be a whole lot easier for me to get posts written.

The first one is still true, but the second one might not be. One problem with writing in swedish is that not that many will find the blog, since even the swedes will google for solutions to their programming problems in english. So for the time being I will switch to english to see if that in any way increases traffic to the blog, which might encourage me to write more often. Please feel free to comment and rate my articles (as well as my english ;-). Any feedback, good or bad, is most welcome.

Of course my posts in swedish will remain and even if you don't understand this northern language you might get something out of looking at the screen caps and code snippets.

Länk med querystring i XML

Idag har jag lärt mig att om du vill inkludera en URL i XML, där URL:en innehåller en querystring och därmed &-tecken så måste dessa skrivas om till &amp; för att det ska bli giltig XML. Till exempel så här;

<myXmlElement>
    Go to <a href="http://www.somepage.com?somekey=1&amp;anotherKey=2">the page</a>
</myXmlElement>

Om du sen har kod som läser XML-filen och till exempel skriver ut innehållet på en webbsida eller skickar med det i ett mejl så är det inga problem att &amp; hänger med även där, när man väl klickar på länken så att den hamnar i adressfältet i en webläsare så blir det ett helt vanligt &-tecken.

Kolla in fler Html Entities

I väntan på deap.nu

Inför att min nya personliga webbplats snart kommer ut på deap.nu har bloggen fått sig en ansiktslyftning och den nya adressen ondotnet.deap.nu. Så uppdatera dina bokmärken! :-)

Allt har jag inte gjort själv, så här kommer länkar till en del bra resurser jag använt till den här bloggen;

  • För att visa upp mina senaste programmeringsrelaterade bokmärken (t.v.) från delicious har jag använt deras linkroll-funktion, använd formuläret för att generera en liten javascript-referens, lägg in på bloggen och vips visas dina senaste länkar på din blogg/hemsida.
  • En liknande funktion har bloglines, den gör det möjligt att visa länkar till de bloggar jag prenumererar på. Fördelen är förstås att i samma stund som jag väljer att prenumerera på bloggen så visas länken även här utan att något extra steg behöver tas.
  • Till höger har jag använt detta script för att tweaka om bloggers label-lista till ett taggmoln, och för att rita molnet har jag kikat på denna photoshop-tutorial.

deap.nu kommer att bli en liten orgie i javascript eller rättare sagt jQuery som är ett fantastiskt javascript-ramverk som nyligen fått intellisense-stöd i visual studio(!). Kolla in jQuery.com.