Типизированный повторитель в ASP.NET

Взлом ASP.NET для создания повторителя с поддержкой обобщений

•    Скачать исходники - 8.5 Кб

Ограничения выражений привязки данных

Выражения привязки данных 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; }
    }
}

И вы хотите показать список Person в Repeater. Есть два способа сделать это: использовать Eval или Cast 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>

Этот безопасен (и даже выделен), но слишком многословен – только подумайте о написании всех этих преобразований для 10 свойств, и что, если ваша бизнес-сущность называется 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 (за 4 шага):
1.    Скачать и установить Reflector
2.    Выяснить, какой код делает то, что вы хотите изменить
3.    Найти наименьший прием программирования, необходимый, чтобы заставить его работать нужным вам образом
4.    Написать прием программирования и наслаждаться результатами

Искомый код - TemplateContainerAttribute и класс ControlBuilder. ControlBuilder проверяет атрибут, чтобы узнать тип сгенерированной переменной Container. Атрибут применяется к используемому  свойству  шаблона:

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

Наименьшее изменение было несколько сложным в данном случае, но все же маленьким. Надо было изменять тип контейнера в TemplateContainerAttribute в зависимости от DataItemTypeName, заданного в разметке Repeater. Это означает, что нельзя задать это  с помощью атрибута – TemplateContainerAttribute изолирован, поэтому нельзя вставить в него никакую динамическую логику. Вместо этого перехватываем вызов GetCustomAttributes() для свойства ItemTemplate и возвращаем новый атрибут с правильным типом.

Но прежде чем перейти к перехвату, создадим обобщенные классы, которые будут служить в качестве Container и нового Repeater. Есть три класса:

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; }
    }
}

Подкласс  Повторитель нужен, так как разметка ASP.NET не понимает обобщения. Но довольно легко хитростью заставить ASP.NET использовать ControlBuilder из Repeater для сборки 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 и затем представляет себя как правильный тип  Repeater<> с методом  MakeGenericType.

Шаг 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, но это мелкая проблема, которую рассмотрим позже. Пока скачайте код и поиграйте с ним.