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