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方法,将ItemTemplatePropertyInfo包装到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
