Pull to refresh

Использование MEF (Managed Extensibility Framework) для разработки Asp.Net WebForms приложений

Reading time11 min
Views3.9K
MEF является хорошим фреймворком для написания расширяемых приложений. Он позволяет легко отделять реализацию от абстракции, добавлять/изменять/удалять реализации во время работы приложения (рекомпозиция), работать с множественными реализациями абстракций, разделять монолитные приложения на независимые части и пр.

Большинство примеров работы MEF являются консольными или WPF приложениями. Почему? Потому что в этом случае легче всего контролировать время жизни составных элементов (composable parts) т.к. сам MEF заботится об этом, а разработчик концентрируется на задачах примера.

Ситуация в веб приложениях кардинально отличается. Разработчики не ответственны за создание страниц, контролов и пр., т.к. Asp.net Runtime берёт всё на себя.

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

Известные решения


Существует решение, как MEF может быть использован для WebForms приложений. Но у этого примера есть несколько важных ограничений.
  • Разработчики должны переунаследовать все контролы, страницы и пользовательские контролы, что не всегда возможно
  • Этот пример не предлагает решения для поддержки HttpModule и HttpHandler

Высокоуровневая архитектура


Целью решения является осуществить поддержки MEF контейнера без необходимости использовать механизм переунаследования и обеспечить поддержку HttpModule и HttpHandler.

Не смотря на то, что я отверг представленное выше решение, я собираюсь использовать его в качестве основы. Это значит, что я буду использовать два контейнера — локальный (per request) и глобальный.

Каждый раз, когда приходит запрос, asp.net runtime создаёт запрошенную страницу или хэндлер и создаёт все зависимые контролы. Я предлагаю инициализировать импортируемые элементы сразу после того, как все части страницы созданы, и сохранять все инициализированные импорты в локальный контейнер.

C другой стороны HttpModule создаются один раз сразу после старта всего приложения. Таким образом, импорт для них должен осуществляться как можно раньше и все HttpModule должны сохраняться в глобальном контейнере.

Реализация


Страницы и контролы

Для того, что бы выполнить операцию импортирования для страницы и всех её зависимостей, следует воспользоваться дополнительным HttpModule. В этом модуле следует добавить обработчики Pre_Init и Init для текущей запрашиваемой страницы. В первом обработчике появляется возможность выполнить композицию для страницы, мастер-страницы и пользовательских контролов. Событие Init позволит выполнить композицию для серверных контролов, т.к. на Pre_Init они ещё не существуют.

Пример:
public class ComposeContainerHttpModule : IHttpModule<br>{<br>  public void Init(HttpApplication context)<br>  {<br>    context.PreRequestHandlerExecute += ContextPreRequestHandlerExecute;<br>  }<br><br>  private void ContextPreRequestHandlerExecute(object sender, EventArgs e)<br>  {<br>    Page page = HttpContext.Current.CurrentHandler as Page;<br>    if (page != null)<br>    {<br>      page.PreInit += Page_PreInit;<br>      page.Init += Page_Init;<br>    }<br>  }<br><br>  private void Page_Init(object sender, EventArgs e)<br>  {<br>    Page handler = sender as Page;<br><br>    if (handler != null)<br>    {<br>      CompositionBatch batch = new CompositionBatch();<br>      batch = ComposeWebPartsUtils.BuildUpControls(batch, handler.Controls);<br>      ContextualCompositionHost.Container.Compose(batch);<br>    }<br>  }<br><br>  private void Page_PreInit(object sender, EventArgs e)<br>  {<br>    Page handler = sender as Page;<br><br>    if (handler != null)<br>    {<br>      CompositionBatch batch = new CompositionBatch();<br>      batch = ComposeWebPartsUtils.BuildUp(batch, handler);<br>      batch = ComposeWebPartsUtils.BuildUpUserControls(batch, handler.Controls);<br>      batch = ComposeWebPartsUtils.BuildUpMaster(batch, handler.Master);<br>      ContextualCompositionHost.Container.Compose(batch);<br>    }<br>  }<br><br>  public void Dispose()<br>  {<br>  }<br>}<br><br>* This source code was highlighted with Source Code Highlighter.

Изначально импорт осуществляется для страницы, затем для пользовательских контролов и мастер-страниц, а в самом конце рекурсивная функция реализует импорт для серверных контролов и их контролов.

Метод BuildUp(CompositionBatch batch, Object o) проверяет, имеет ли объект Object o какие-либо импортируемые элементы и добавляет его в список объектов для осуществления композиции. Как только все контролы обработаны, объект CompositionBatch может быть использован для инициализации контейнера. После этого все импорты будут проинициализированы и доступны в течении жизни запроса.

Пример:
public static class ComposeWebPartsUtils<br>{<br>  public static CompositionBatch BuildUp(CompositionBatch batch, Object o)<br>  {<br>    ComposablePart part = AttributedModelServices.CreatePart(o);<br><br>    if (part.ImportDefinitions.Any())<br>    {<br>      if (part.ExportDefinitions.Any())<br>        throw new Exception(string.Format("'{0}': Handlers cannot be exportable", o.GetType().FullName));<br><br>      batch.AddPart(part);<br>    }<br><br>    return batch;<br>  }<br><br>  public static CompositionBatch BuildUpUserControls(CompositionBatch batch, ControlCollection controls)<br>  {<br>    foreach (Control c in controls)<br>    {<br>      if (c is UserControl)<br>        batch = ComposeWebPartsUtils.BuildUp(batch, c);<br>      batch = BuildUpUserControls(batch, c.Controls);<br>    }<br><br>    return batch;<br>  }<br><br>  public static CompositionBatch BuildUpControls(CompositionBatch batch, ControlCollection controls)<br>  {<br>    foreach (Control c in controls)<br>    {<br>      batch = ComposeWebPartsUtils.BuildUp(batch, c);<br>      batch = BuildUpControls(batch, c.Controls);<br>    }<br><br>    return batch;<br>  }<br><br>  public static CompositionBatch BuildUpMaster(CompositionBatch batch, MasterPage master)<br>  {<br>    if (master != null)<br>      batch = BuildUpMaster(ComposeWebPartsUtils.BuildUp(batch, master), master.Master);<br><br>    return batch;<br>  }<br>}<br><br>* This source code was highlighted with Source Code Highlighter.

Примечание:
Я не могу использовать туже технику, что используется в примере с сайта codeplex (наследование PageHandlerFactory и переопределение метода GetHandler()), т.к. к этому времени ни один контрол для страницы ещё не создан.

HttpHandler

Хэндлеры не имеют таких событий, где все импорты могли бы быть удовлетворены. Было бы идеально использовать соответствующий HandlerFactory и переопределить метод GetHandler() как это было сделано для страниц в примере с codeplex. И такой класс существует (SimpleWebHandlerFactory), но он является внутренним (internal). Я не знаю, по какой причине программисты из Microsoft сделали это, но это выглядит странно, т.к. фабрика для страниц является public.

Я не вижу никакого другого варианта кроме реализации моей собственной фабрики SimpleWebHandlerFactory вместо присутствующей в .net framework. Главная цель любого HandlerFactory заключается в том, что бы определить тип, который должен быть инстанцирован для текущего запроса. HandlerFactory может получить тип только через парсинг ресурса, который был запрошен. Таким образом, мне требуется парсер, который мог бы разбирать код HttpHandler. К счастью, такой парсер существует (SimpleWebHandlerParser), является публичным и мне требуется сделать всего лишь обёртку для него (WebHandlerParser).

Ниже приведена диаграмма последовательности, которая описывает работу алгоритма для создания композиции для HttpHandler

Ниже код классов, которые реализуют описанный выше функционал.
public class SimpleWebHandlerFactory : IHttpHandlerFactory<br>{<br>  public virtual IHttpHandler GetHandler(HttpContext context, string requestType, string virtualPath, string path)<br>  {<br>    Type type = WebHandlerParser.GetCompiledType(context, virtualPath, path);<br>    if (!(typeof(IHttpHandler).IsAssignableFrom(type)))<br>      throw new HttpException("Type does not implement IHttpHandler: " + type.FullName);<br><br>    return Activator.CreateInstance(type) as IHttpHandler;<br>  }<br><br>  public virtual void ReleaseHandler(IHttpHandler handler)<br>  {<br>  }<br>}<br><br>internal class WebHandlerParser : SimpleWebHandlerParser<br>{<br>  internal WebHandlerParser(HttpContext context, string virtualPath, string physicalPath)<br>    : base(context, virtualPath, physicalPath)<br>  {<br>  }<br><br>  public static Type GetCompiledType(HttpContext context, string virtualPath, string physicalPath)<br>  {<br>    WebHandlerParser parser = new WebHandlerParser(context, virtualPath, physicalPath);<br>    Type type = parser.GetCompiledTypeFromCache();<br>    if (type != null)<br>      return type;<br>    else<br>      throw new HttpException(string.Format("File '{0}' is not a web handler.", virtualPath));<br>  }<br>  <br>  protected override string DefaultDirectiveName<br>  {<br>    get<br>    {<br>      return "webhandler";<br>    }<br>  }<br>}<br><br>public class ComposableWebHandlerFactory : SimpleWebHandlerFactory<br>{<br>  public override IHttpHandler GetHandler(HttpContext context, string requestType, string virtualPath, string path)<br>  {<br>    IHttpHandler handler = base.GetHandler(context, requestType, virtualPath, path);<br><br>    if (handler != null)<br>    {<br>      CompositionBatch batch = new CompositionBatch();<br>      batch = ComposeWebPartsUtils.BuildUp(batch, handler);<br>      ContextualCompositionHost.Container.Compose(batch);<br>    }<br><br>    return handler;<br>  }<br>}<br><br>* This source code was highlighted with Source Code Highlighter.

HttpModule

Как я упоминал ранее, все HttpModules создаются на старте приложения. Таким образим, я должен произвести композицию сразу после того, как приложение стартует.
Пример:
public class ScopedContainerHttpModule : IHttpModule<br>{<br>  private CompositionContainer _container;<br><br>  public void Init(HttpApplication app)<br>  {<br>    ComposeModules(app);<br>  }<br><br>  private void ComposeModules(HttpApplication app)<br>  {<br>    CompositionBatch batch = ComposeWebPartsUtils.BuildUpModules(app);<br>    _container.Compose(batch);<br>  }<br>}<br><br>public static class ComposeWebPartsUtils<br>{<br>  public static CompositionBatch BuildUpModules(HttpApplication app)<br>  {<br>    CompositionBatch batch = new CompositionBatch();<br><br>    for (int i = 0; i < app.Modules.Count - 1; i++)<br>      batch = BuildUp(batch, app.Modules.Get(i));<br><br>    return batch;<br>  }<br>}<br><br>* This source code was highlighted with Source Code Highlighter.

Я получаю HttpApplication объект, извлекаю из него все модули и на основе этой информации заполняю глобальный контейнер.

Как результат, мне следует добавить несколько строк в файл web.config любого WebForms проекта для того, что бы моё решение заработало.
<httpHandlers><br>  ......<br>  <remove path="*.ashx" verb="*" /><br>  <add path="*.ashx" verb="*" type="common.composition.WebExtensions.ComposableWebHandlerFactory, common.composition" /><br>  ......<br></httpHandlers><br><httpModules><br>  <add name="ContainerCreator" type="common.composition.WebExtensions.ScopedContainerHttpModule, common.composition"/><br>  ......<br>  <add name="ComposeContainerModule" type="common.composition.WebExtensions.ComposeContainerHttpModule, common.composition" /><br></httpModules><br><br>* This source code was highlighted with Source Code Highlighter.

Я удаляю обработчик *.ashx файлов, который присутствует по умолчанию, и добавляю мой (ComposableWebHandlerFactory).

Я добавляю модуль ContainerCreator для создания и инициализации инфраструктуры для локального и глобального контейнеров.

ComposeContainerModule будет использован для того, что бы инициализировать локальный контейнер.

Вот и всё!

Мне не потребовалось использовать наследование контролов, писать дополнительный код в основной программе. Это решение может быть добавлено в любой веб проект, базирующийся на web forms, и только web.config должен быть обновлён, что бы все представленные возможности стали доступны.

Пример


Я использую демо, которое взял из примера WebFomsAndMef с небольшими модификациями.

Каждый элемент приложения импортирует класс SampleCompositionPart с атрибутом [PartCreationPolicy(CreationPolicy.NonShared)], что гарантирует, что будет создаваться новый экземпляр SampleCompositionPart при акте импорта. Этот класс содержит единственное поле Id, которое возвращает Guid.

Приложение отображает значение Guid для текущего отображаемого элемента — страницы, контрола, пользовательского контрола, мастер страницы HttpHandler и HttpModule.

Пример:
///PageSample.aspx<br><asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent"><br>  <% =SamplePart.Id %><br></asp:Content><br> <br>///PageSample.cs<br>public partial class PageSample : System.Web.UI.Page<br>{<br>  [Import]<br>  public SampleCompositionPart SamplePart<br>  {<br>    get;<br>    set;<br>  }<br>}<br><br>* This source code was highlighted with Source Code Highlighter.


Исходный код может быть загружен здесь.

Надеюсь, эта статья поможет начать использование MEF в asp.net WebForms приложениях.
Tags:
Hubs:
+4
Comments1

Articles

Change theme settings