Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Typed Repeater in ASP.NET

0.00/5 (No votes)
17 Mar 2007 1  
Hacking ASP.NET to build a Repeater with generics support

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 Persons in a Repeater. There are two ways to do it: use Eval or Cast DataItem. Let's examine both.

  1. 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.

  2. 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):

  1. Download and install Reflector
  2. Find out what code does the thing you want to change
  3. Find the smallest hack needed to make it work as you want
  4. 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:

  1. 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; }
        }
    }
    
  2. Generic Repeater
    public class Repeater<TDataItem> : Repeater
    {
        protected override RepeaterItem CreateItem(int itemIndex, 
                            ListItemType itemType)
        {
            return new RepeaterItem<TDataItem>(itemIndex, itemType);
        }
    }
    
  3. 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:

    1. 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.

    2. 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.

    3. 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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here