65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET 中的强类型 Repeater

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (38投票s)

2007 年 3 月 17 日

4分钟阅读

viewsIcon

137684

downloadIcon

1204

破解 ASP.NET 以构建支持泛型的 Repeater

数据绑定表达式的局限性

我认为 ASP.NET 数据绑定表达式(<%# %>)非常出色。它们简单、强大、支持智能感知并且在编译时进行检查。它们减少了日常编码工作量,同时又不会让表示层充斥任何复杂的逻辑。

最常用的 <%# %> 表达式是定义模板内容。RepeaterGridView 是我最常看到 <%# %> 的控件。

但是在模板中使用这些表达式存在一些问题。在模板中数据绑定的最有趣之处是模板行所实例化的数据项。

该数据项可以通过 IDataItemContainer 接口访问,该接口由每个 RepeaterItemGridViewRow 实现。在模板表达式中,容器可以通过 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 列表。有两种方法可以做到:使用 EvalCast DataItem。让我们分别考察一下。

  1. Eval:
    <asp:Repeater ID="repeaterWithEval" runat="server">
      <ItemTemplate>
        <div>
          <%# Eval("Name") %>: <%# Eval("EMail")%>
        </div>
      </ItemTemplate>
    </asp:Repeater>
    

    这是最快但严重错误的方法。小问题:属性名没有智能感知。大问题:想象一下有人决定将 EMail 重命名为 Mail。你更改了它,编译了所有内容,但如果你有很多页面,并且测试团队很懒惰,或者根本没有测试团队。在这种情况下,可能需要很长时间才能有人注意到这个页面不再工作了。

  2. 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(分四步)

  1. 下载并安装 Reflector
  2. 找出要更改的代码
  3. 找到实现你想要的效果所需的最小破解
  4. 编写破解并享受结果

我们感兴趣的代码是 TemplateContainerAttributeControlBuilder 类。ControlBuilder 检查该属性以找出生成的 Container 变量的类型。该属性应用于我们正在使用的模板属性。

[TemplateContainer(typeof(RepeaterItem))]
public virtual ITemplate ItemTemplate

在这种情况下,最小的更改有点复杂,但仍然相当小。我需要根据 Repeater 标记中指定的 DataItemTypeName 来更改 TemplateContainerAttribute 中容器的类型。这意味着实际上无法通过属性指定这一点——TemplateContainerAttribute 是密封的,因此我无法在其中放入任何动态逻辑。相反,我拦截对 ItemTemplate 属性的 GetCustomAttributes() 调用,并返回一个具有正确类型的类。

但在进行拦截之前,让我们构建将用作 Container 和新 Repeater 的泛型类。有三个类:

  1. 泛型 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; }
        }
    }
    
  2. 泛型 Repeater
    public class Repeater<TDataItem> : Repeater
    {
        protected override RepeaterItem CreateItem(int itemIndex, 
                            ListItemType itemType)
        {
            return new RepeaterItem<TDataItem>(itemIndex, itemType);
        }
    }
    
  3. 子类化 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 使用 RepeaterControlBuilder 来实际构建 Repeater<T> 相对容易。我将在讨论拦截时进行解释。

    拦截并不像听起来那么难。有三个步骤:

    1. 创建一个自定义类型,它将包装 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<> 类型。

    2. 创建自定义 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);
          }
      }
      

      这段代码也相当直接。

    3. 创建 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
© . All rights reserved.