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

    Интро

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

    Поехали

    DependencyInjection В качестве DI-контейнера будет выступать Microsoft Unity, но это не принципиально, всё что будет касаться DI не зависит от используемого контейнера. Проблема состоит в следующем – есть некоторый ASP.NET Control, в который мы хотим внедрит зависимости, а так же воспользоваться услугами Service Locator’а для управления интересующими нас зависимостями. В Microsoft Unity есть некоторые средства для того, чтобы сделать это не прилагая особенных усилий: мы можем произвести инъекцию в свойство элемента управления, нас интересующее примерно следующим образом:
    1. Отметить атрибутом Dependency необходимое свойство
      public class MyControl : UserControl
      {
              [Dependency]
              public MyPresenter Presenter
              {
                  get { return _presenter; }
                  set
                  {
                      _presenter = value;
                      _presenter.View = this;
                  }
              }
      }

    2. Проинициализировать элемент управления можно следующим образом
      protected override void OnInit(EventArgs e)
      {
          base.OnInit(e);
          _сontainer.BuildUp(GetType(), this);
      } 

    3. Позаботиться о местоположении контейнера в вашем приложении, я предлагаю использовать для этого 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>();
        }



    Ресурсы

    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
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 12
    • –1
      ничё не понял, но ыло интересно :D
      • 0
        по количеству комментариев мне показалось, что не вы одни ) что именно затруднило понимание? может стиль изложения не ясен? или не достаточно знаний по технологии?
        • 0
          я по ссылкам не переходил, поэтому из текста мне не стало очевидно, что такое DI-контейнер. Ну и сишарп тут по-новее того, на котором мне сейчас приходится программировать, поэтому вникать было немного лень. Извините :(
      • 0
        Как-то все действительно сложно. Почему решили не писать свой модуль, который выполнял бы инъекцию в страницу и во все её контролы?
        unity.codeplex.com/Thread/View.aspx?ThreadId=57051 (внизу страницы)

        А вот с UserControlBuildProvider думаю можно будет покопаться. Спасибо за идею. Возможно это будет намного быстрее, нежели пробегаться по всем контролам страницы.
        • 0
          то, что написано у них, делается без каких-либо вмешательств в сборщик страницы, и является фактически стандартной практикой
          • 0
            Page.LoadControl() заменяется => все стандартные вещи, которые используют этот метод перестанут работать? Например WebParts. Или подобное учтено?
            • 0
              никакая стандартная логика поведения поменяться не должна, скорее всего всё будет работать без проблем
      • НЛО прилетело и опубликовало эту надпись здесь
        • –1
          вернусь с работы — перечитаю еще раз. есть где покопаться.
          • 0
            Подсветка странная у вас) Все внимание идет на кейворды, а код не видно)
            • 0
              Только вот наткнулся на этот пост. Подсветка жжет, серьезно. Что касается BuildUp(), мне кажется следует отдельно написать о том как это работает и какие ньюансы есть при использовании.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.