Introduction
The DataGrid provides web developers limitless options to display data in tabular format. Although it does provide a number of ways to display data, it does not inherently promote code and format reuse.
The article and the source code that accompanies it demonstrates the usage of a helper class called the GridHelper
which builds Datagrids dynamically using XML, template files, and the ITemplate
interface. The main goal is to provide a class that can be reused across many sites using your own custom template hierarchy. It also allows developers to append formatted columns to grids at run-time without recompilation.
Background
Nothing bothers me more than creating DataGrids with the same look and feel across multiple sites. I saw a need to create a mechanism that would embrace my lazy nature by removing the need to create Datagrids by hand. While CSS helps in the formatting of columns, it does not allow you to join two rows of data to create a single column or format a specific data type across multiple pages with multiple grids.
Using the code
There are three main pieces of this project:
TemplateLoader
– Object that inherits from the Page
object and hides the LoadTemplate
method for loading and caching templates from a file. GridFormats
– Stores the grid columns from the Web.Config file. This is serialized into this object from the custom ConfigurationSectionHandler
object. GridHelper
– Performs dynamic generation of grid columns using the TemplateLoader
and GridFormats
classes.
TemplateLoader
The purpose of this class is to load templates from .ascx files. LoadTemplate
is an inherited method available to Page
and UserControl
objects. TemplateLoader
is the centralized handler for loading the templates from file.
#region TemplateLoader
public class TemplateLoader: System.Web.UI.Page
{
private const string CACHE_HEADER =
"GridHelper.Templates.";
public new ITemplate LoadTemplate(string templateName)
{
ITemplate Template = null;
string CachedTemplate =
string.Concat(CACHE_HEADER,templateName);
Template =
this.Context.Cache.Get(CachedTemplate) as ITemplate;
if (Template == null)
{
Template = base.LoadTemplate(templateName);
this.Context.Cache.Insert(CachedTemplate, Template,
new System.Web.Caching.CacheDependency(Server.MapPath(templateName)),
System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromHours (1),
System.Web.Caching.CacheItemPriority.AboveNormal, null);
}
return Template;
}
}
#endregion
The method is shadowed to enable caching of template objects. If a file is changed, it will be invalidated from the cache and reloaded during the next request.
Gridformats
GridFormats
is nothing more than a container class for our grid columns. The main entry into the GridFormats
class is the gridname
property, therefore you must have unique gridname
s in your Web.Config file. The Column
and Grid
objects are serialized into the GridFormats
class using XmlSerialization
and the custom ConfigurationSectionHandler
object.
#region Column
[XmlRoot("Column")]
public class Column
{
private string _Type = string.Empty;
private string _Path = string.Empty;
private string _HeaderText = string.Empty;
private string _DataField = string.Empty;
private bool _Wrap = false;
[XmlAttributeAttribute("datafield")]
public string DataField
{
get
{
return _DataField;
}
set
{
_DataField = value;
}
}
[XmlAttributeAttribute("type")]
public string Type
{
get
{
return _Type;
}
set
{
_Type = value;
}
}
[XmlAttributeAttribute("path")]
public string Path
{
get
{
return this._Path;
}
set
{
this._Path = value;
}
}
[XmlAttributeAttribute("headertext")]
public string HeaderText
{
get
{
return _HeaderText;
}
set
{
this._HeaderText = value;
}
}
[XmlAttributeAttribute("wrap")]
public bool Wrap
{
get
{
return this._Wrap;
}
set
{
this._Wrap = value;
}
}
}
#endregion
#region Grid
[Serializable()]
public class Grid
{
private Column[] _Columns;
private string _Name = string.Empty;
[XmlAttributeAttribute("name")]
public string Name
{
get
{
return _Name;
}
set
{
_Name = value;
}
}
[XmlArray("Columns"), XmlArrayItem("Column")]
public Column[] Columns
{
get
{
return _Columns;
}
set
{
_Columns= value;
}
}
}
#endregion
#region GridFormats
[SerializableAttribute()]
public class GridFormats :
System.Collections.Specialized.NameObjectCollectionBase,
IXmlSerializable
{
public Grid this[int index]
{
get
{
return base.BaseGet(index) as Grid;
}
set
{
this.BaseSet(index, value);
}
}
public Grid this[string index]
{
get
{
return (base.BaseGet(index) as Grid);
}
set
{
this.BaseSet(index, value);
}
}
public void Add(Grid Item)
{
base.BaseAdd(Item.Name, Item);
}
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
public void ReadXml(System.Xml.XmlReader reader)
{
XmlSerializer l_Serializer = null;
l_Serializer = new XmlSerializer(typeof(Grid));
reader.ReadStartElement();
while ((reader.NodeType != System.Xml.XmlNodeType.EndElement &&
reader.NodeType != System.Xml.XmlNodeType.None))
{
Grid l_Grid;
l_Grid = ((Grid)(l_Serializer.Deserialize(reader)));
Add(l_Grid);
reader.MoveToContent();
}
reader.ReadEndElement();
}
public void WriteXml(System.Xml.XmlWriter writer)
{
XmlSerializer l_Serializer = null;
l_Serializer = new XmlSerializer(typeof(Grid));
foreach (string l_strKey in base.Keys)
{
Grid l_objServer = this[l_strKey];
l_Serializer.Serialize(writer, l_objServer);
}
}
}
#endregion
Here is the Web.Config entries that contain the raw XML for the GridFormats
object hierarchy.
<GridFormats
assembly="DynamicDataGrids,Version=2.2.0.0,
Culture=neutral,PublicKeyToken=null"
type="DynamicDataGrids.GridFormats">
<Grid name="Employees">
<Columns>
<Column type="template"
path="~/GridTemplates/LastNameFirstNameTemplate.ascx"
headertext="Name" wrap="false"></Column>
<Column type="bound" headertext="Title"
datafield="Title" wrap="false"></Column>
<Column type="template"
path="~/GridTemplates/DateTemplate.ascx"
headertext="B-Day" wrap="false"></Column>
</Columns>
</Grid>
<Grid name="Products">
<Columns>
</Columns>
</Grid>
<Grid name="Customers">
<Columns>
</Columns>
</Grid>
</GridFormats>
Each column is defined inside the Columns
tag and contains the following properties:
type
: Determines the type of the column to create. In this example the types do not define serialized objects, i.e. there is no template column object inside the code. You could add that to the code if you wish and create inherited Column
objects that define unique properties for each type of column. headertext
: The header text for the column. path
: Used for template columns only, this defines the path to the template file. You will notice a tilda '~' mark in the examples. Failure to add this to the template path will cause an invalid path exception in the project. datafield
: Used by bound columns, this field determines which field to bind to in the incoming data. wrap
: Used to turn on/off wrapping in the column.
You could add every possible tag and matching attribute in the XML/class where you need to create unique columns. (CSS, etc.)
Template example
Below is an example of a template that takes two columns from the incoming data source and joins them together to create a Lastname, Firstname link. Note that there is no code-behind for this class. The OnDataBinding
event calls the BindData
method which hides the link if the last name is empty:
<%@ Control Language="C#" %>
<asp:LinkButton id=lnkLastFirstName runat="server"
Text=<%# string.Format("{0}, {1}",
DataBinder.Eval(Container, "DataItem.LastName" ),
DataBinder.Eval(Container, "DataItem.FirstName" ))%>
OnDataBinding=BindData ></asp:LinkButton>
<script runat="server">
void BindData(Object sender, EventArgs e)
{
System.Web.UI.WebControls.DataGridItem container =
(System.Web.UI.WebControls.DataGridItem) this.NamingContainer;
string LastName =
DataBinder.GetPropertyValue(container.DataItem,
"LastName", string.Empty);
if (LastName == string.Empty)
{
LinkButton Link = (LinkButton) sender;
Link.Visible = false;
}
}
</script>
GridHelper
The Gridhelper
utilizes the TemplateLoader
and GridFormats
object to generate columns and append them into the grid. This class is very simple and can be modified/expanded to fit your needs.
Performance
This is always a major concern if you are in a high-volume environment. Caching the templates increases the performance quite a bit, but the main penalty involved in using this approach comes from DataBinder.Eval
. The good news is this can be avoided! For all the samples that I have provided, you can change the code to directly cast to your object type (DataView
, custom object, etc.) and avoid the DataBinder
reflection penalty. I did not perform a detailed performance analysis on the GridHelper
class, but I believe the performance from this class will be similar to adding columns to a grid at runtime using custom code. If anyone sees any major performance issues, please let me know and I will resolve them immediately.
History
In my full source code, I do have the ability to call ITemplate
columns defined in code and add them into the grid using reflection. Once I clean that code up, I will post it here along with a full implementation of the template columns. Of course, that will only be done if others would like to have it otherwise I am onto my new project. :)