Introduction
There are occasions when one may work with a data source that provides dynamic columns. A stored procedure that aggregates year-to-date totals, with columns representing the months-to-date, is an example. Towards the beginning of the year, there are only a few columns of data; towards the end of the year, there are many. When displaying data like this in a DataGrid
or a GridView
control, it is convenient to set AutoGenerateColumns
=true
, letting ASP.NET create the necessary display columns at runtime. Unfortunately, columns generated this way are not available to the developer through the grid’s Columns
property, and thus the developer lacks the ability to establish column-based formatting.
In spite of this limitation, a developer may trap an appropriate grid event during data-binding to apply custom formatting. This technique also allows the application of conditional formatting based on individual cell values. Furthermore, the code to apply such formatting may be wrapped in a custom control that implements the IExtenderProvider
interface, permitting a page designer to apply complex conditional cell formatting to grids, without any programming.
For a GridView: Trap the RowDataBound event
A GridView
control exposes the RowDataBound
event during the databinding process. A handler for this event is defined with the following signature:
RowDataBound_Handler(Object grid, GridViewRowEventArgs e)
{
}
The GridViewRowEventArgs
object provides access to the table row that has just been bound through its Row
property. Row
is of the type GridViewRow
, which is a derivative of TableRow
. As such, there is a Cells
property used to access individual TableCell
objects.
In this example from the file formattingGridView.aspx in the article download (see the link at the top of the article), the assumption is made that the first column of data returned by a stored procedure (or other data source) is a text category of some kind. The remaining columns (potentially one or more) are values that need to be right-aligned, with alternating background colors on a column by column basis. To demonstrate conditional formatting for individual cells, a check is also made so special highlighting may be applied for high values. Just for fun, header cells are also styled in this code.
private void GV_RowDataBound(object o, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
TableCell cell = e.Row.Cells[0];
cell.Width = new Unit("120px");
cell.Style["border-right"] = "2px solid #666666";
cell.BackColor = System.Drawing.Color.LightGray;
for(int i=1; i<e.Row.Cells.Count; i++)
{
cell = e.Row.Cells[i];
cell.HorizontalAlign = HorizontalAlign.Right;
cell.Width = new Unit("90px");
if (i % 2 == 1)
cell.BackColor
= System.Drawing.ColorTranslator.FromHtml("#EFEFEF");
if (GetCellValue(cell) >= 8000)
{
cell.Font.Bold = true;
cell.BorderWidth = new Unit("1px");
cell.BorderColor = System.Drawing.Color.Gray;
cell.BorderStyle = BorderStyle.Dotted;
cell.BackColor = System.Drawing.Color.Honeydew;
}
}
}
if (e.Row.RowType == DataControlRowType.Header)
{
foreach (TableCell cell in e.Row.Cells)
{
cell.Style["border-bottom"] = "2px solid #666666";
cell.BackColor=System.Drawing.Color.LightGray;
}
}
}
The event is then attached declaratively to the GridView
by setting the OnRowDataBound
attribute:
<asp:GridView id="myList" runat="server"
AutoGenerateColumns="true"
OnRowDataBound="GV_RowDataBound"
. . .
>
</asp:GridView>
The code results in a grid that when rendered looks like the following:
For a DataGrid: Trap the ItemDataBound event
The technique for a DataGrid
is similar. A DataGrid
exposes the ItemDataBound
event, which gives access to a DataGridItemEventArgs
object. This object contains an Item
property, with a Controls
collection that is populated by TableCell
objects during databinding. The following example from formattingDataGrid.aspx shows the same application of formatting as above but for a DataGrid
:
private void DG_ItemDataBound(object o, DataGridItemEventArgs e)
{
if (e.Item.ItemType == ListItemType.Item
|| e.Item.ItemType == ListItemType.AlternatingItem)
{
TableCell cell = (e.Item.Controls[0] as TableCell);
cell.Width = new Unit("120px");
cell.Style["border-right"] = "2px solid #666666";
cell.BackColor = System.Drawing.Color.LightGray;
for(int i=1; i<e.Item.Controls.Count; i++)
{
cell = (e.Item.Controls[i] as TableCell);
cell.HorizontalAlign = HorizontalAlign.Right;
cell.Width = new Unit("90px");
if (i % 2 == 1)
cell.BackColor
= System.Drawing.ColorTranslator.FromHtml("#EFEFEF");
if (GetCellValue(cell) >= 8000)
{
cell.Font.Bold = true;
cell.BorderWidth = new Unit("1px");
cell.BorderColor = System.Drawing.Color.Gray;
cell.BorderStyle = BorderStyle.Dotted;
cell.BackColor = System.Drawing.Color.Honeydew;
}
}
}
if (e.Item.ItemType == ListItemType.Header)
{
foreach (TableCell cell in e.Item.Controls)
{
cell.Style["border-bottom"] = "2px solid #666666";
cell.BackColor=System.Drawing.Color.LightGray;
}
}
}
Similar to the GridView
, the assignment of this event handler to the DataGrid
is made declaratively by setting the OnItemDataBound
attribute:
<asp:DataGrid id="myList" runat="server"
AutoGenerateColumns="true"
OnItemDataBound="DG_ItemDataBound"
. . .
>
</asp:DataGrid>
Wrapping Common Formatting in a Custom Extender
Perhaps you or your organization has designed some standardized report formats that require column-based or conditional cell formatting. If you find that you apply that same report formatting to a number of grids across a project, you may find it useful to wrap custom formatting code in an extender control. An extender is a custom component that implements the IExtenderProvider
interface and provides additional (extended) functionality to existing controls.
There are a number of good articles that introduce the IExtenderProvider
interface and describe implementation issues in detail, including the following:
For this implementation, there isn’t that much more to do. We’ll start with a new custom class called CustomReportFormatExtender
that inherits from Control
and implements IExtenderProvider
.
[ProvideProperty("UseCustomReportFormat", typeof(GridView))]
[ . . . other attributes . . .] public class CustomReportFormatExtender : Control, IExtenderProvider
{
. . .
}
The ProvidePropertyAttribute
decorating this class specifies that controls of type GridView
should be extended with a new property called UseCustomReportFormat. When this property on a GridView
is set to true
, the formatting will be applied. The extender indicates that it only applies to GridView
controls by implementing the CanExtend
(
)
method, required by the IExtenderProvider
interface:
bool IExtenderProvider.CanExtend(object o)
{
return (o is GridView);
}
It is the responsibility of the extender control to provide storage space and methods for setting and retrieving extended properties. To store extended property values, we have a custom ExtendedProperties
class. This class records the ID
of a GridView
as well as the value of its extended UseCustomReportFormat
boolean property.
public class ExtenderProperties
{
private bool _useCustomReportFormat;
private string _gridID;
public bool UseCustomReportFormat
{
get { return _useCustomReportFormat; }
set { _useCustomReportFormat = value; }
}
public string GridID
{
get { return _gridID; }
set { _gridID = value; }
}
}
The extender control keeps a collection of ExtenderProperties
in its Props
property. Props
is exposed and decorated with designer attributes purely to ensure that Visual Studio will persist the extended property values. Without this attention, the extended property values are not remembered by Visual Studio.
private ExtenderPropertiesCollection _props
= new ExtenderPropertiesCollection();
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[PersistenceMode(PersistenceMode.InnerProperty)]
[Browsable(false)]
public ExtenderPropertiesCollection Props
{
get
{
return _props;
}
}
The extender control then provides a Get
and Set
method for each extended property, following the naming convention GetPropertyName and SetPropertyName. In this case, there is only the boolean UseCustomReportFormat
property to expose. The collection class ExtenderPropertiesCollection
is defined with utility methods used in these Get
and Set
methods to simplify the management of the extended property.
public bool GetUseCustomReportFormat(GridView grid)
{
return Props.GetUseCustomReportFormatForGridID(grid.ID);
}
public void SetUseCustomReportFormat(GridView grid, bool value)
{
Props.SetUseCustomReportFormatForGridID(grid.ID, value);
NotifyDesignerOfChange();
}
Finally, the extender needs to provide the actual formatting functionality. It does so by hooking the RowDataBound
event for desired GridView
controls. The formatting code is wrapped in the function Handle_RowDataBound
, and assigned during the OnInit
event of the extender.
protected void Handle_RowDataBound(object o, GridViewRowEventArgs e)
{
. . . formatting code . . .
}
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
foreach (ExtenderProperties p in Props)
{
GridView g = FindControl(p.GridID) as GridView;
if (g != null && p.UseCustomReportFormat)
g.RowDataBound += new
GridViewRowEventHandler(Handle_RowDataBound);
}
}
That’s it. This custom extender object may be compiled, added to the toolbox, and dragged onto an .aspx page. Then it is simply a matter of specifying which GridView
controls to format by setting their new UseCustomReportFormat
property to true
.
For a page designer, this provides a simple, standardized means for assigning complex cell-by-cell formatting in a declarative way. The sample gridWithExtender.aspx from the article download demonstrates the use of this extender control.
Other Approaches to Dynamic Column Formatting
There are alternatives to this approach of injecting code during databinding for applying formatting to dynamically generated columns. One alternative is to inspect the data source before binding it to a display control and programmatically create DataGrid
or GridView
columns. Properties for these DataGridColumn
objects (in the case of a DataGrid
) or DataControlField
objects (in the case of a GridView
) may be set to influence formatting behavior during databinding. The sample downloads in this CodeProject article[^] demonstrate an example of this approach. While column formatting may be applied in this way without an additional pass through individual table cells, conditional formatting on a cell-by-cell basis must still be applied using an approach like that explained in this article.
Summary
This article presents one approach for applying formatting to a GridView
or DataGrid
when the columns in the grid are dynamically generated. When AutoGenerateColumns
set to true
, cell appearance can be manipulated during databinding by trapping an appropriate event – RowDataBound
in the case of a GridView
, and ItemDataBound
in the case of a DataGrid
. Formatting applied to individual TableCell
objects may be conditional based on values in the cells. For additional ease-of use, common report formatting code may be wrapped in a custom extender control, allowing for a completely declarative solution for page designers.