Как подружить ASP.NET Controls и DI-контейнер

Как подружить ASP.NET Controls и DI-контейнер

.NET


Интро


В последнее время решил немного освежить свои знания в ASP.NET, в связи с чем углубился в процессы генерации кода контролов по разметке (*.ascx, *.aspx) и обнаружил что можно делать очень интересные решения, о которых о хочу поведать. Итак сегодня мы узнаем, как подружить наш Dependency Injection контейнер с генерируемым контролами кодом.

Поехали

DependencyInjection
  1. В качестве DI-контейнера будет выступать Microsoft Unity, но это не принципиально, всё что будет касаться DI не зависит от используемого контейнера. Проблема состоит в следующем – есть некоторый ASP.NET Control, в который мы хотим внедрит зависимости, а так же воспользоваться услугами Service Locator’а для управления интересующими нас зависимостями. В Microsoft Unity есть некоторые средства для того, чтобы сделать это не прилагая особенных усилий: мы можем произвести инъекцию в свойство элемента управления, нас интересующее примерно следующим образом:Отметить атрибутом Dependency необходимое свойство
  2. public class MyControl : UserControl
  3. {
  4.         [Dependency]
  5.         public MyPresenter Presenter
  6.         {
  7.             get { return _presenter; }
  8.             set
  9.             {
  10.                 _presenter = value;
  11.                 _presenter.View = this;
  12.             }
  13.         }
  14. }
  15. Проинициализировать элемент управления можно следующим образом
  16. protected override void OnInit(EventArgs e)
  17. {
  18.     base.OnInit(e);
  19.     _сontainer.BuildUp(GetType(), this);
  20. } 
  21. Позаботиться о местоположении контейнера в вашем приложении, я предлагаю использовать для этого HttpApplication, унаследовавшись от которого и произведя небольшие модификации файла global.asax мы получаем необходимое нам хранилище для контейнера, обращаться с ним необходимо примерно следующим образом ((Sapphire.Application)HttpContext.Current.ApplicationInstance).Container

Решение вполне пригодное, однако пуристические воззрения не дают оставить решение на данной стадии, и думаю, что просто необходимо заменить инъекцию свойства на инъекцию в конструктор, тем более подобный подход – это далеко не то, что мы можем выжать из Unity. Т.е. наш интерес состоит в том, чтобы класс MyUserControl выглядел примерно так (думаю сборщику страницы это не совсем понравится)

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 { getset}

  }

Ну и конечно же надо сконфигурировать контейнер для создания наших прокси-обработчиков    public static T Build<T>()

    {

      return (T)((Application)HttpContext.Current.ApplicationInstance)

        .Container

          . AddNewExtension<Interception>()

          .Configure<Interception>()

            .SetInterceptorFor<T>(new VirtualMethodInterceptor())

        .Container

          .Resolve<T>();

    }


Report Page