Интро
В последнее время решил немного освежить свои знания в ASP.NET, в связи с чем углубился в процессы генерации кода контролов по разметке (*.ascx, *.aspx) и обнаружил что можно делать очень интересные решения, о которых о хочу поведать. Итак сегодня мы узнаем, как подружить наш Dependency Injection контейнер с генерируемым контролами кодом.Поехали
В качестве DI-контейнера будет выступать Microsoft Unity, но это не принципиально, всё что будет касаться DI не зависит от используемого контейнера. Проблема состоит в следующем – есть некоторый ASP.NET Control, в который мы хотим внедрит зависимости, а так же воспользоваться услугами Service Locator’а для управления интересующими нас зависимостями. В Microsoft Unity есть некоторые средства для того, чтобы сделать это не прилагая особенных усилий: мы можем произвести инъекцию в свойство элемента управления, нас интересующее примерно следующим образом:- Отметить атрибутом Dependency необходимое свойство
public class MyControl : UserControl
{
[Dependency]
public MyPresenter Presenter
{
get { return _presenter; }
set
{
_presenter = value;
_presenter.View = this;
}
}
}
- Проинициализировать элемент управления можно следующим образом
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
_сontainer.BuildUp(GetType(), this);
}
- Позаботиться о местоположении контейнера в вашем приложении, я предлагаю использовать для этого HttpApplication, унаследовавшись от которого и произведя небольшие модификации файла global.asax мы получаем необходимое нам хранилище для контейнера, обращаться с ним необходимо примерно следующим образом ((Sapphire.Application)HttpContext.Current.ApplicationInstance).Container
public class MyControl : UserControl
{
public MyControl(MyPresenter presenter)
{
_presenter = presenter;
_presenter.View = this;
}
}
Предлагаю этим и заняться. Начнём с того, что у элементов управления, описанных в разметке страницы, при генерации страницы указываются их конструкторы без параметров, интересно, как можно управлять данным процессом, первоначально, покопавшись в web.config я предполагал сделать это через:
<buildProviders>
<add extension=".aspx" type=«System.Web.Compilation.PageBuildProvider»/>
<add extension=".ascx" type=«System.Web.Compilation.UserControlBuildProvider»/>
…
</buildProviders>
Однако реализация своего PageBuildProvider’а – довольно серьезное занятие, думаю отложить это для серьезной на то необходимости. Однако благодаря BuildProvider’ам можно генерить к примеру слой доступа к данным, для этого надо: Написать и зарегестрировать обработчик для какого-нибудь своего расширения, к примеру *.dal и сделать что-нибудь наподобее http://www.codeproject.com/KB/aspnet/DALComp.aspx кстати подобная логика реализована в SubSonic http://dotnetslackers.com/articles/aspnet/IntroductionToSubSonic.aspx так же интересная реализация наследования страницы от generic типов http://stackoverflow.com/questions/1480373/generic-inhertied-viewpage-and-new-property ещё можно, к примеру генерировать исключения, объекты передачи данных и многое другое, ограничением является лишь ваша фантазия. Вообщем, данный вариант нам не подходит, необходимо сделать что-нибудь проще, и есть отличное решение, с помощью атрибута ControlBuilder мы можем указать свою логику сборки элемента управления из разметки, это будет выглядеть примерно так
[ControlBuilder(typeof(MyControlBuilder))]
public class UserControl : System.Web.UI.UserControl
{
}
Теперь разберемся с реализацией MyControlBuilder, этот тип должен наследовать от ControlBuilder и с помощью перегрузки ProcessGeneratedCode мы с вами сможем указать сборщику на необходимость использования нашего кода вместо вызова конструктора без атрибутов элемента управления:
public override void ProcessGeneratedCode(CodeCompileUnit codeCompileUnit,
CodeTypeDeclaration baseType,
CodeTypeDeclaration derivedType,
CodeMemberMethod buildMethod,
CodeMemberMethod dataBindingMethod)
{
codeCompileUnit.Namespaces[0].Imports.Add(new CodeNamespaceImport(«Sapphire.Web.UI»));
ReplaceConstructorWithContainerResolveMethod(buildMethod);
base.ProcessGeneratedCode(codeCompileUnit, baseType, derivedType, buildMethod, dataBindingMethod);
}
самое интересно скрывает метод ReplaceConstructorWithContainerResolveMethod
private void ReplaceConstructorWithContainerResolveMethod(CodeMemberMethod buildMethod)
{
foreach (CodeStatement statement in buildMethod.Statements)
{
var assign = statement as CodeAssignStatement;
if (null != assign)
{
var constructor = assign.Right as CodeObjectCreateExpression;
if (null != constructor)
{
assign.Right =
new CodeSnippetExpression(
string.Format(«SapphireControlBuilder.Build<{0}>()»,
ControlType.FullName));
break;
}
}
}
}
следуя по коду можно обратить внимание, что он заменяет вызов конструктора на вызов генерик-метода Build, в котором мы и обратимся к нашему контейнеру с просьбой вызвать наш элемент управления и проинициализировать его конструктор необходимыми зависимостями. Однако это ещё не решении задания, т.к. есть метод динамической загрузки элемента управления Page.LoadControl(), для него придётся написать свой вариант public static class PageExtensions
{
public static UserControl LoadAndBuildUpControl(this Page page, string virtualPath)
{
var control = page.LoadControl(virtualPath);
return SapphireControlBuilder.Build<UserControl>(control.GetType());
}
}
Вот мы и справились с поставленной задачей, однако это ещё не всё. А почему теперь не воспользоваться всеми преимуществами Unity, и не внедрить в наш элемент управления AOP времени исполнения с помощью Unity Interception. К примеру мы можем сделать следующее public class MyControl : UserControl
{
[HandleException]
public override void DataBind()
{
base.DataBind();
}
}
Это будет означать, что обработка исключений должна добавляться на лету, к тому ж предоставляя нам возможность её изменения во время исполнения, для начала пусть её реализация будет примерно следующая [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class HandleExceptionAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(IUnityContainer container)
{
return new ExceptionHandler();
}
}
public class ExceptionHandler : ICallHandler
{
/// <exception cref=«SapphireUserFriendlyException»><c>SapphireUserFriendlyException</c>.</exception>
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
var result = getNext()(input, getNext);
if (result.Exception == null)
return result;
throw new SapphireUserFriendlyException();
}
public int Order { get; set; }
}
Ну и конечно же надо сконфигурировать контейнер для создания наших прокси-обработчиков public static T Build<T>()
{
return (T)((Application)HttpContext.Current.ApplicationInstance)
.Container
. AddNewExtension<Interception>()
.Configure<Interception>()
.SetInterceptorFor<T>(new VirtualMethodInterceptor())
.Container
.Resolve<T>();
}
Ресурсы
Sapphire.Application – для чего всё это реализовывалось http://github.com/butaji/Sapphire/tree/master/trunk/Sapphire.Application/Дэвид предлагает реализации связывания с данными следующего поколения “Databinding 3.0” на основе аналогичного подхода http://weblogs.asp.net/davidfowler/archive/2009/11/13/databinding-3-0.aspx