ASP.NET 中的强类型 Repeater






4.89/5 (38投票s)
2007 年 3 月 17 日
4分钟阅读

137684

1204
破解 ASP.NET 以构建支持泛型的 Repeater
数据绑定表达式的局限性
我认为 ASP.NET 数据绑定表达式(<%# %>)非常出色。它们简单、强大、支持智能感知并且在编译时进行检查。它们减少了日常编码工作量,同时又不会让表示层充斥任何复杂的逻辑。
最常用的 <%# %> 表达式是定义模板内容。Repeater
和 GridView
是我最常看到 <%# %> 的控件。
但是在模板中使用这些表达式存在一些问题。在模板中数据绑定的最有趣之处是模板行所实例化的数据项。
该数据项可以通过 IDataItemContainer
接口访问,该接口由每个 RepeaterItem
和 GridViewRow
实现。在模板表达式中,容器可以通过 Container
变量或通过 Eval
/Bind
伪函数访问。
该接口继承了 .NET 1.0/1.1 的根本问题——IDataItemContainer.DataItem
是非强类型的。以 Repeater
控件为例:假设你有一个 Person
业务实体类。
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; }
}
}
你想在 Repeater
中显示 Person
列表。有两种方法可以做到:使用 Eval
或 Cast
DataItem
。让我们分别考察一下。
Eval
:<asp:Repeater ID="repeaterWithEval" runat="server"> <ItemTemplate> <div> <%# Eval("Name") %>: <%# Eval("EMail")%> </div> </ItemTemplate> </asp:Repeater>
这是最快但严重错误的方法。小问题:属性名没有智能感知。大问题:想象一下有人决定将
EMail
重命名为Mail
。你更改了它,编译了所有内容,但如果你有很多页面,并且测试团队很懒惰,或者根本没有测试团队。在这种情况下,可能需要很长时间才能有人注意到这个页面不再工作了。Cast
:<asp:Repeater ID="repeaterWithCast" runat="server"> <ItemTemplate> <div> <%# (Container.DataItem as Person).Name %>: <%# (Container.DataItem as Person).EMail %> </div> </ItemTemplate> </asp:Repeater>
这是安全的(甚至突出显示了),但现在它非常冗长——只需考虑为十个属性编写所有这些类型转换。如果你的业务实体名为
AReallyLongCalledClass<OtherClass>
怎么办?显然,它应该这样工作:
<asp:Repeater ID="repeaterWithHack" runat="server" DataItemTypeName="Person"> <ItemTemplate> <div> <%# Container.DataItem.Name %>: <%# Container.DataItem.EMail %> </div> </ItemTemplate> </asp:Repeater>
幸运的是,有一种方法可以实现这一点。
是时候破解 ASP.NET 了
如何破解 ASP.NET(分四步)
- 下载并安装 Reflector
- 找出要更改的代码
- 找到实现你想要的效果所需的最小破解
- 编写破解并享受结果
我们感兴趣的代码是 TemplateContainerAttribute
和 ControlBuilder
类。ControlBuilder
检查该属性以找出生成的 Container
变量的类型。该属性应用于我们正在使用的模板属性。
[TemplateContainer(typeof(RepeaterItem))] public virtual ITemplate ItemTemplate
在这种情况下,最小的更改有点复杂,但仍然相当小。我需要根据 Repeater
标记中指定的 DataItemTypeName
来更改 TemplateContainerAttribute
中容器的类型。这意味着实际上无法通过属性指定这一点——TemplateContainerAttribute
是密封的,因此我无法在其中放入任何动态逻辑。相反,我拦截对 ItemTemplate
属性的 GetCustomAttributes()
调用,并返回一个具有正确类型的类。
但在进行拦截之前,让我们构建将用作 Container
和新 Repeater
的泛型类。有三个类:
泛型 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; } } }
泛型 Repeater
public class Repeater<TDataItem> : Repeater { protected override RepeaterItem CreateItem(int itemIndex, ListItemType itemType) { return new RepeaterItem<TDataItem>(itemIndex, itemType); } }
子类化 Repeater
[ControlBuilder(typeof(RepeaterControlBuilder))] public class Repeater : System.Web.UI.WebControls.Repeater { private string dataItemTypeName; public string DataItemTypeName { get { return dataItemTypeName; } set { dataItemTypeName = value; } } }
需要子类化 Repeater,因为 ASP.NET 标记不支持泛型。但要欺骗 ASP.NET 使用
Repeater
的ControlBuilder
来实际构建Repeater<T>
相对容易。我将在讨论拦截时进行解释。拦截并不像听起来那么难。有三个步骤:
-
创建一个自定义类型,它将包装
typeof(Repeater<TDataItem>)
并拦截对ItemTemplate
属性的请求。这实际上很容易——Microsoft 提供了
TypeDelegator
类来包装任何Type
。因此,我只需继承
TypeDelegator
并重写GetPropertyImpl
方法,将ItemTemplate
PropertyInfo
包装到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; } }
代码相当简单且自文档化。有趣的一点是,它接收
DataItemType
,然后通过MakeGenericType
方法假装是正确的Repeater<>
类型。 -
创建自定义
PropertyInfo
以重写GetCustomAttributes
方法。这有点难,因为没有预定义的PropertyInfoDelegator
。所以我创建了一个然后继承了它。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); } }
这段代码也相当直接。
-
创建
RepeaterControlBuilder
,它将RepeaterFakeType
替换为typeof(Repeater)
。这是最简单的部分,只需重写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); } }
-
它就能正常工作。
有了这一切,我现在可以这样写:
<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>
我还可以获得智能感知和编译时检查。唯一剩下的是 DataItemTypeName
属性中的智能感知,但这绝对是一个小问题,我以后会考虑它。在此期间,你可以下载代码并进行尝试。
历史
- 2007 年 3 月 17 日:发布到 The Code Project