![]() |
Web Development »
ASP.NET »
General
Advanced
A Typed Repeater in ASP.NETBy Andrey ShchekinHacking ASP.NET to build a Repeater with generics support |
C# 2.0, Windows, .NET 2.0, ASP.NET, WebForms, VS2005, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
I think that the ASP.NET data-binding expressions (<%# %>) are just great. They're simple, powerful, intellisensed and checked at compile time. They reduce everyday coding efforts but do not pollute presentation with any heavy logic.
The most popular use of <%# %> expressions is the definition of template contents. Repeater and GridView are the controls where I see <%# %> most often.
But there are some problems with using these expressions within templates. The most interesting thing to databind within a template is the data item of the row template is instantiated in.
This data item is accessible through the IDataItemContainer interface that is implemented by each RepeaterItem and GridViewRow. Within the template expressions, a container is available as a Container variable or through the Eval/Bind pseudo-functions.
This interface inherited the .NET 1.0/1.1 fundamental problem - IDataItemContainer.DataItem is untyped. Let's see an example for the Repeater control: imagine that you have a Person business entity class:
public class Person
{
private string name;
private string email;
public string Name
{
get { return name; }
set { name = value; }
}
public string EMail
{
get { return email; }
set { email = value; }
}
}
And you want to show a list of Persons in a Repeater. There are two ways to do it: use Eval or Cast DataItem. Let's examine both.
Eval:
<asp:Repeater ID="repeaterWithEval" runat="server">
<ItemTemplate>
<div>
<%# Eval("Name") %>: <%# Eval("EMail")%>
</div>
</ItemTemplate>
</asp:Repeater>
This is the fastest and gravely wrong way. Minor problem: no intellisense on property names. Major problem: imagine someone decides EMail should be now called Mail. You change it, compile everything, but if you have a lot of pages and lazy test team or no test team at all. In such a case, ages may pass till somebody notices this page does not work anymore.
Cast:
<asp:Repeater ID="repeaterWithCast" runat="server">
<ItemTemplate>
<div>
<%# (Container.DataItem as Person).Name %>:
<%# (Container.DataItem as Person).EMail %>
</div>
</ItemTemplate>
</asp:Repeater>
This is safe (and even highlighted), but now it is way too verbose � just think about writing all these casts for ten properties. And what if your business entity is called AReallyLongCalledClass<OtherClass>?
Obviously, the way it should work is:
<asp:Repeater ID="repeaterWithHack" runat="server"
DataItemTypeName="Person">
<ItemTemplate>
<div>
<%# Container.DataItem.Name %>: <%# Container.DataItem.EMail %>
</div>
</ItemTemplate>
</asp:Repeater>
Fortunately, there is a way to make it work.
How to hack ASP.NET (in four steps):
The code of interest to us is TemplateContainerAttribute and ControlBuilder class. ControlBuilder examines the attribute to find out the type of the generated Container variable. The attribute is applied to the template property we are using:
[TemplateContainer(typeof(RepeaterItem))] public virtual ITemplate ItemTemplate
The smallest change was somewhat complex in this case, but still quite small. What I needed was to change the type of container in TemplateContainerAttribute depending on the DataItemTypeName specified in Repeater markup. This means that there is no actual way to specify this with an attribute � TemplateContainerAttribute is sealed so I cannot put any dynamic logic into it. Instead, I intercept the GetCustomAttributes() call on the ItemTemplate property and return a new attribute with the correct type.
But before going to the interception, let's build the generic classes that will serve as Container and new Repeater. There are three classes:
Generic RepeaterItem
public class RepeaterItem<TDataItem> :
System.Web.UI.WebControls.RepeaterItem
{
public RepeaterItem(int itemIndex, ListItemType itemType) :
base(itemIndex, itemType)
{
}
public new TDataItem DataItem
{
get { return (TDataItem)base.DataItem; }
set { base.DataItem = (TDataItem)value; }
}
}
Generic Repeater
public class Repeater<TDataItem> : Repeater
{
protected override RepeaterItem CreateItem(int itemIndex,
ListItemType itemType)
{
return new RepeaterItem<TDataItem>(itemIndex, itemType);
}
}
Subclassed Repeater
[ControlBuilder(typeof(RepeaterControlBuilder))]
public class Repeater : System.Web.UI.WebControls.Repeater
{
private string dataItemTypeName;
public string DataItemTypeName
{
get { return dataItemTypeName; }
set { dataItemTypeName = value; }
}
}
Subclassed Repeater is required since ASP.NET markup does not understand generics. But it is quite easy to trick ASP.NET to use the ControlBuilder from Repeater to actually build Repeater<T>. I will explain it while talking about interception.
And interception is not as hard as it sounds. There are three steps to do it:
Create a custom Type that will wrap typeof(Repeater<TDataItem>) and intercept the request of ItemTemplate property.
This is actually very easy � Microsoft provides the TypeDelegator class to wrap any Type.
So I just inherited TypeDelegator and overwrote the GetPropertyImpl method to wrap ItemTemplate PropertyInfo into FakePropertyInfo.
internal class RepeaterFakeType : TypeDelegator
{
private class FakePropertyInfo : PropertyInfoDelegator
{
�
}
private Type repeaterItemType;
public RepeaterFakeType(Type dataItemType)
: base(typeof(Repeater<>).MakeGenericType(dataItemType))
{
this.repeaterItemType = typeof(RepeaterItem<>).MakeGenericType(dataItemType);
}
protected override PropertyInfo GetPropertyImpl(string name, �)
{
PropertyInfo info = base.GetPropertyImpl(name, �);
if (name == "ItemTemplate")
info = new FakePropertyInfo(info, this.repeaterItemType);
return info;
}
}
The code is quite easy and self-documenting. One interesting thing is that it receives the DataItemType and then presents itself as a correct Repeater<> type with the MakeGenericType method.
Create custom PropertyInfo to override the GetCustomAttributes method. This was a bit harder since there is no predefined PropertyInfoDelegator. So I just built one and then inherited it:
private class FakePropertyInfo : PropertyInfoDelegator
{
private Type templateContainerType;
public FakePropertyInfo(PropertyInfo real,
Type templateContainerType) : base(real)
{
this.templateContainerType = templateContainerType;
}
public override object[] GetCustomAttributes
(Type attributeType, bool inherit)
{
if (attributeType == typeof(TemplateContainerAttribute))
return new Attribute[]
{ new TemplateContainerAttribute(templateContainerType) };
return base.GetCustomAttributes(attributeType, inherit);
}
}
This code is also quite straightforward.
Create the RepeaterControlBuilder that substitutes the RepeaterFakeType in place of typeof(Repeater). That was the easiest part, just overriding Init:
public class RepeaterControlBuilder : ControlBuilder
{
public override void Init(TemplateParser parser,
ControlBuilder parentBuilder,
Type type,
string tagName,
string id,
IDictionary attribs)
{
string dataItemTypeName = attribs["DataItemTypeName"] as string;
Type dataItemType = BuildManager.GetType(dataItemTypeName, true);
Type repeaterFakeType = new RepeaterFakeType(dataItemType);
base.Init(parser, parentBuilder, repeaterFakeType,
tagName, id, attribs);
}
}
With all of this, I can now write
<my:Repeater ID="repeater" runat="server" DataItemTypeName="AshMind.Web.UI.Research.Samples.Person"> <ItemTemplate> <div><%# Container.DataItem.Name %> : <%# Container.DataItem.EMail %></div> </ItemTemplate> </my:Repeater>
I can also get intellisense and compile-time checks. The only thing left is intellisense in DataItemTypeName attribute, but this is definitely a minor issue and I'll think about this later. Meanwhile, you can download the code and play with it.
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 17 Mar 2007 Editor: Chris Maunder |
Copyright 2007 by Andrey Shchekin Everything else Copyright © CodeProject, 1999-2009 Web10 | Advertise on the Code Project |