Limits of data-binding expressions
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 Person
s 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.
Time to hack ASP.NET
How to hack ASP.NET (in four steps):
- Download and install Reflector
- Find out what code does the thing you want to change
- Find the smallest hack needed to make it work as you want
- Write hack and enjoy results
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);
}
}
And it just works
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.
History
- 17.03.2007: Posted to The Code Project