A Remedy for DataGrid Vertigo
The AlternatingItemStyle can be a wonderful thing. But in many cases I found that I would start to get
a feeling like vertigo if I scrolled the page up or down. So I thought back to my days in Manhattan in the financial
industry and remembered how I used to prepare reports to be viewed on screen. I alternated the
colors of the rows, but I did it every three or five rows (depending on the width of the report,
the content of the report, or my own aesthetic taste).
The ASP.NET DataGrid is an extremely popular control. People know how to use it (with varying
levels of depth) and it is pretty simple to display a lot of content with only a handful of lines
of code. With its functionality and popularity in mind, I decided to use the DataGrid itself
as a base class and enhance what people were already used to.
In this article I will show you how to create a control that we'll call DataGridPlus. Our control
will allow developers to specify the number of alternating rows before we change from our ItemStyle
to our AlternatingItemStyle or back again.
We'll start by creating a new ASP.NET web application called dgPlusTester. By default we should
have a single web form called webform1.aspx that we'll use to display our new control. Once our
web project is created we'll right-click the solution in the Solution Explorer and choose New
Project... from the Add menu. After you've selected the language folder of your choice, click
Web Control Library in the "Templates" area and call this new project DataGridPlus.
By default Visual Studio will create a new control called WebCustomControl1.cs or WebCustomControl1.vb
and add it to the web control library project. My habit is to just delete that control and create
my own, so right-click the file name and choose "Delete". Once you have removed that control, right-click
the web control project in Solution Explorer and choose Add New Item from the Add menu. Choose
Web Custom Control as your template and call your control DataGridPlus.cs or DataGridPlus.vb
(depending on what language you are using).
Our new web custom control will most likely have a Text property. We won't be needing it, so there are
three sections of code to modify before we get to the fun stuff. Look in your code for:
C#
[DefaultProperty("Text"),
ToolboxData("<{0}:DataGridPlus
runat=server>{0}:DATAGRIDPLUS>")]
VB
<DefaultProperty("Text"),
ToolboxData("<{0}:DataGridPlus runat=server>{0}:DATAGRIDPLUS>")>
We'll remove the code DefaultProperty("Text"), (since there will be
no property called "Text" to default to). The next bit of code to remove is the Text
property itself. Look in your control for the following code and delete it.
C#
private string text;
[Bindable(true),
Category("Appearance"),
DefaultValue("")]
public string Text
{
get
{
return text;
}
set
{
text = value;
}
}
VB
Dim _text As String
<Bindable(True), Category("Appearance"), DefaultValue("")> Property [Text]() As String
Get
Return _text
End Get
Set(ByVal Value As String)
_text =
Value
End Set
End Property
The final place that uses this Text property is the Render method. For now we'll simply replace the
reference to our Text property with an empty string. So our Render method should now look like this:
C#
protected override void Render(HtmlTextWriter output)
{
output.Write("");
}
VB
Protected Overrides Sub Render(ByVal output As System.Web.UI.HtmlTextWriter)
output.Write("")
End Sub
Congratulations. We're now ready to begin writing our new control. To begin with we'll need to reference
some of the .NET namespaces - add code at the top of your control to use
System.Drawing and System.Web.UI.WebControls if you don't already
have them referenced.
In order for our new DataGridPlus control to have all the functionality of the ASP.NET DataGrid we'll have
to inherit the DataGrid in our new control. So we'll change the inheritance to
System.Web.UI.WebControls.DataGrid from the default System.Web.UI.WebControls.WebControl.
Since we'll let the developer choose how many rows before alternating styles we'll need to add a property that will allow it.
We could specify the Category under which we'd like our new property to appear
in the properties window. If we had the code [Category("Appearance")] immediately
preceding the code for our property we would find it grouped with the other properties in the Appearance
category.
C#
private Int16 _rows;
public Int16 AlternatingRows
{
get { return _rows; }
set { _rows = value; }
}
VB
Private _rows As Int16
Public Property AlternatingRows As Int16
Get
Return _rows
End Get
Set
_rows =
Value
End Set
End Property
In addition to binding data, a DataGrid applies TableItemStyles to the DataGridItems it
contains. What we need to do is apply the ItemStyle and AlternatingItemStyle when WE want
to instead of letting the DataGrid decide for us. We will have to keep track of how many rows we've
rendered since the last time we changed styles. We'll also need to
know what style we're supposed to be using at any given moment as we loop through the DataGridItems.
To make all this work we will override the Render (which is already done
for us by default) and RenderContents methods of the DataGrid control.
This will allow us a huge amount of control over what is rendered to the browser, but it is also easy to
make one little tiny mistake that will prevent the control from rendering anything useful at all. Now that
we're all confident that we never make any mistakes, let's override our second base class method.
C#
protected override void Render(HtmlTextWriter output)
{
output.Write("");
}
protected override void RenderContents(HtmlTextWriter output)
{
}
VB
Protected Overrides Sub Render(ByVal
output As System.Web.UI.HtmlTextWriter)
output.Write("")
End Sub
Protected Overrides Sub RenderContents(ByVal
output As System.Web.UI.HtmlTextWriter)
End Sub
First let's take care of the Render method. This is the method that will
write out the HTML necessary to start our TABLE (including the I.D., the style tag, and anything else).
The HtmlTextWriter is a great tool in that it allows us to pile up some HTML attributes and then call
RenderBeginTag with our tag name as a parameter. When we do that the HtmlTextWriter
outputs a properly-formatted opening HTML tag with all the attributes that we piled up before we called
RengerBeginTag. This means we don't have to concantenate a bunch of strings
together and try to get all the quotations and syntax straight.
We know the first thing that we'll need is our opening TABLE tag. So we'll use the
AddAttribute method of the HtmlTextWriter object called output to render
our control's "id" property. In addition, we'll need to get our hands on all the style information the developer
might have set for our DataGridPlus. Luckily this information is at our fingertips. We can use the
ControlStyleCreated property of our control to find out whether or not a ControlStyle has
been created for our control. The ControlStyle is simply one object that will encompass every property that
affects the style of our control including any font, border, or color information. As long as our control has a
ControlStyle object we can simply call ControlStyle.AddAttributesToRender method
and we're done. Our ControlStyle will pass all of the HTML style information we need
to whatever HtmlTextWriter object we pass to it. We can also handle those developers that make good use of
style sheets by throwing in one more line of code and using the AddAttribute
method of the HtmlTextWriter class to output the name of the CssClass the developer specified.
After our opening TABLE tag has been generated it is time to output all of the rows and then close up our
TABLE. Since we decided to override RenderContents all we have to do is
call that method (passing it our Render method's HtmlTextWriter object) and
then we'll close up our TABLE tag by calling RenderEndTag.
C#
protected override void Render(HtmlTextWriter output) {
output.AddAttribute("id", this.ID);
if (this.ControlStyleCreated && this.ControlStyle != null)
{
ControlStyle.AddAttributesToRender(output);
}
if (this.CssClass != string.Empty) output.AddAttribute("class", CssClass);
output.RenderBeginTag("table");
this.RenderContents(output);
output.RenderEndTag();
}
VB
Protected Overrides Sub Render(ByVal
output As System.Web.UI.HtmlTextWriter)
output.AddAttribute("id", Me.ID)
If Me.ControlStyleCreated AndAlso Not IsNothing(Me.ControlStyle) Then
Me.ControlStyle.AddAttributesToRender(output)
End If
If Me.CssClass <> "" Then output.AddAttribute("class", CssClass)
output.RenderBeginTag("table")
Me.RenderContents(output)
output.RenderEndTag()
End Sub
Now that our Render method is complete we'll need to take care of
RenderContents. This method is in charge of building the HTML necessary
to represent all of the rows (DataGridItems) in our table whether the row is a Header, Footer, Separator,
Item, AlternatingItem, or the like. Since our purpose for this control is to alternate the rows' styles
every x rows we'll need to introduce a couple of variables to help us through.
First we'll need to know how many rows we have rendered with the current style. We'll do this by
declaring an Int16. We'll also obviously have to know what style we're supposed to use on each row
as we render it. The ItemStyle and AlternatingItemStyle are just properties of the
DataGrid that return TableItemStyle objects, so we'll create our own private TableItemStyle
object that will hold the style we should be using as we loop through our DataGridItems.
The ASP.NET DataGrid contains a control called DataGridTable which serves as the
container for all of our rows that we'll render. Therefore, we don't expect to loop through the
Controls collection of our DataGridPlus expecting to find
DataGridItems because we won't find any. With that in mind, we'll loop through the controls that
belong to the first child control of our DataGridPlus - the DataGridTable. As we loop through the
DataGridItems we'll be looking for those items that are of type Item or
AlternatingItem. When we find a DataGridItem of these types we'll start modifying styles.
If we find an item of another type, we'll need to be good control creators and output THOSE
item styles as well. We just won't be modifying them.
C#
protected override void RenderContents(HtmlTextWriter output)
{
Int16 rw = 0;
TableItemStyle _style = this.ItemStyle;
if (HasControls())
{
foreach (DataGridItem r in Controls[0].Controls)
{
if (r.ItemType == ListItemType.Item || r.ItemType == ListItemType.AlternatingItem)
{
rw += 1;
if (rw > _rows)
{
if (_style == this.AlternatingItemStyle)
{ _style = this.ItemStyle; }
else
{ _style = this.AlternatingItemStyle; }
rw = 1;
}
_style.AddAttributesToRender(output);
}
else
{
switch (r.ItemType)
{
case ListItemType.Header:
this.HeaderStyle.AddAttributesToRender(output);
break;
case ListItemType.Footer:
this.FooterStyle.AddAttributesToRender(output);
break;
case ListItemType.Pager:
this.PagerStyle.AddAttributesToRender(output);
break;
case ListItemType.EditItem:
this.EditItemStyle.AddAttributesToRender(output);
break;
case ListItemType.SelectedItem:
this.SelectedItemStyle.AddAttributesToRender(output);
break;
}
}
output.RenderBeginTag("tr");
for (int w = 0; w < r.Controls.Count; w++)
{
r.Controls[w].RenderControl(output);
}
output.RenderEndTag();
}
}
}
VB
Protected Overrides Sub RenderContents(output As HtmlTextWriter)
Dim rw As Int16 = 0
Dim _style As TableItemStyle = Me.ItemStyle
If HasControls() Then
Dim r As DataGridItem
For Each r In Controls(0).Controls
If r.ItemType = ListItemType.Item Or r.ItemType = ListItemType.AlternatingItem Then
rw += 1
If rw > _rows Then
If _style Is Me.AlternatingItemStyle Then
_style = Me.ItemStyle
Else
_style = Me.AlternatingItemStyle
End If
rw = 1
End If
_style.AddAttributesToRender(output)
Else
Select Case r.ItemType
Case ListItemType.Header
Me.HeaderStyle.AddAttributesToRender(output)
Case ListItemType.Footer
Me.FooterStyle.AddAttributesToRender(output)
Case ListItemType.Pager
Me.PagerStyle.AddAttributesToRender(output)
Case ListItemType.EditItem
Me.EditItemStyle.AddAttributesToRender(output)
Case ListItemType.SelectedItem
Me.SelectedItemStyle.AddAttributesToRender(output)
End Select
End If
output.RenderBeginTag("tr")
Dim w As Integer
For w = 0 To r.Controls.Count - 1
r.Controls(w).RenderControl(output)
Next
output.RenderEndTag()
Next
End If
End Sub
Now for the big moment. Compile the DataGridPlus project because we'll be using it right away.
To test our control we'll add it to the toolbox in Visual Studio. Open webform1.aspx and then right-click
the toolbox and choose Customize Toolbox. Select the .NET Framework Components tab and
then click the Browse button. By default Visual Studio saves new projects in its own folder inside
My Documents (if it's not a web application). We'll need to navigate to the folder containing
our DataGridPlus control's bin directory. Inside the bin folder should be our new DataGridPlus.dll
file. Double-click that and it should appear in the list of controls. If we click Ok we should
see our new control in the toolbox. Drag one of them onto webform1.aspx.
We'll need a reference to System.Data.SqlClient in
the code-behind of webform1.aspx. Then we'll simply bind our DataGridPlus to the Northwind database's
Employee Sales By Country stored procedure. This sproc requires two parameters (indicating the
beginning date and end date to serve as record selection criteria). Don't forget to pick an
AlternatingItemStyle and/or an ItemStyle after you drop our new control onto the form.
C#
private void Page_Load(object sender, System.EventArgs e)
{
SqlConnection _cn = new SqlConnection("Data Source=nemserver; Initial Catalog=Northwind; User ID=sa;Password=mulgrew");
SqlCommand _cmd = new SqlCommand("Employee Sales By Country", _cn);
_cmd.CommandType = CommandType.StoredProcedure;
SqlParameter _prm1 = _cmd.Parameters.Add(new SqlParameter("@Beginning_date", SqlDbType.DateTime));
_prm1.Direction = ParameterDirection.Input;
_prm1.Value = Convert.ToDateTime("1/1/1970");
SqlParameter _prm2 = _cmd.Parameters.Add(new SqlParameter("@Ending_date", SqlDbType.DateTime));
_prm2.Direction = ParameterDirection.Input;
_prm2.Value = Convert.ToDateTime("12/31/2010");
_cn.Open();
SqlDataReader _sdr = _cmd.ExecuteReader(CommandBehavior.CloseConnection);
DataGridPlus1.DataSource = _sdr;
DataGridPlus1.DataBind();
_sdr.Close();
}
VB
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim _cn As SqlConnection = New SqlConnection(ConfigurationSettings.AppSettings("ConnectionString"))
Dim _cmd As SqlCommand = New SqlCommand("Employee Sales By Country", _cn)
_cmd.CommandType = CommandType.StoredProcedure
Dim _prm1 As SqlParameter = _cmd.Parameters.Add(New SqlParameter("@Beginning_date", SqlDbType.DateTime))
_prm1.Direction = ParameterDirection.Input
_prm1.Value = CDate("1/1/1970")
Dim _prm2 As SqlParameter = _cmd.Parameters.Add(New SqlParameter("@Ending_date", SqlDbType.DateTime))
_prm2.Direction = ParameterDirection.Input
_prm2.Value = CDate("12/31/2010")
_cn.Open()
Dim _sdr As SqlDataReader = _cmd.ExecuteReader(CommandBehavior.CloseConnection)
DataGridPlus1.DataSource = _sdr
DataGridPlus1.DataBind()
_sdr.Close()
End Sub
Provided everything went as planned we should now be able to build and browse to webform1 in our web
application and see the results of our efforts. I chose a light green as my alternating BackColor and
you can see my results below.
| Country | LastName | FirstName | ShippedDate | OrderID | SaleAmount |
| UK | Buchanan | Steven | 7/16/1996 12:00:00 AM | 10248 | 440 |
| UK | Suyama | Michael | 7/10/1996 12:00:00 AM | 10249 | 1863.4 |
| UK | Suyama | Michael | 8/16/1996 12:00:00 AM | 10274 | 538.6 |
| USA | Davolio | Nancy | 8/9/1996 12:00:00 AM | 10275 | 291.84 |
| USA | Callahan | Laura | 8/14/1996 12:00:00 AM | 10276 | 420 |
| USA | Fuller | Andrew | 8/13/1996 12:00:00 AM | 10277 | 1200.8 |
| USA | Callahan | Laura | 8/16/1996 12:00:00 AM | 10278 | 1488.8 |
| USA | Callahan | Laura | 8/16/1996 12:00:00 AM | 10279 | 351 |
| USA | Fuller | Andrew | 9/12/1996 12:00:00 AM | 10280 | 613.2 |
| USA | Peacock | Margaret | 8/21/1996 12:00:00 AM | 10281 | 86.5 |
| USA | Peacock | Margaret | 8/21/1996 12:00:00 AM | 10282 | 155.4 |
| USA | Leverling | Janet | 8/23/1996 12:00:00 AM | 10283 | 1414.8 |
| USA | Peacock | Margaret | 8/27/1996 12:00:00 AM | 10284 | 1170.38 |
| USA | Davolio | Nancy | 8/26/1996 12:00:00 AM | 10285 | 1743.36 |
| USA | Callahan | Laura | 8/30/1996 12:00:00 AM | 10286 | 3016 |
| USA | Callahan | Laura | 8/28/1996 12:00:00 AM | 10287 | 819 |
| USA | Peacock | Margaret | 9/3/1996 12:00:00 AM | 10288 | 80.1 |
| UK | King | Robert | 8/28/1996 12:00:00 AM | 10289 | 479.4 |
| USA | Callahan | Laura | 9/3/1996 12:00:00 AM | 10290 | 2169 |
| UK | Suyama | Michael | 9/4/1996 12:00:00 AM | 10291 | 497.52 |
| USA | Davolio | Nancy | 9/2/1996 12:00:00 AM | 10292 | 1296 |
| USA | Davolio | Nancy | 9/11/1996 12:00:00 AM | 10293 | 848.7 |
| USA | Peacock | Margaret | 9/5/1996 12:00:00 AM | 10294 | 1887.6 |
| USA | Fuller | Andrew | 9/10/1996 12:00:00 AM | 10295 | 121.6 |