воскресенье, 30 января 2011 г.

Расширяемость приложения при помощи MEF


   Одним из требований, предъявляемых к проектируемому приложению, может быть его расширяемость. В данной статье я предлагаю рассмотреть механизм расширения функционала .NET приложения при помощи MEF.


   MEF (Managed Extensibility Framework) является проектом с открытым исходным кодом (Ms-PL), кроме того его сборка включена в Framework .NET 4.
   В качестве примера создадим приложение, которое будет по-разному выводить текст на консоль.

   Для начала создадим в Visual Studio 2010 проект типа Class Library Components (договоримся, что все проекты в решении будут на C#). Этот проект будет служить описанием бизнес-логики нашего приложения. Добавим в него интерфейс отправки сообщения.

  1. using System;
  2.  
  3. namespace Components
  4. {
  5.   public interface IStringSender
  6.   {
  7.     void Send(String message);
  8.   }
  9. }
* This source code was highlighted with Source Code Highlighter.
   Опишем класс, который будет выводить текст при помощи плагинов:

  1. using System;
  2. using System.Collections.Generic;
  3.  
  4. namespace Components
  5. {
  6.   public class MessageSender
  7.   {
  8.     private IEnumerable<IStringSender> _stringSenders;
  9.  
  10.     public void SendMessage(string message)
  11.     {
  12.       foreach (var sender in _stringSenders)
  13.         sender.Send(message);
  14.     }
  15.   }
  16. }
* This source code was highlighted with Source Code Highlighter.
  Как видно из кода, класс MessageSender содержит закрытое  перечисляемое множество отправителей текста _stringSenders. Метод SendMessage отправляет сообщение при помощи всех отправителей из множества. Отправители и будут выводить текст на консоль по-разному.
 Чтобы зайдействовать механизмы MEF, требуется включить сборку System.ComponentModel.Composition в проект. Предложим MEF заполнять список отправителей во время выполнения приложения, пометив его атрибутом ImportMany:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel.Composition;
  4.  
  5. namespace Components
  6. {
  7.   public class MessageSender
  8.   {
  9.     [ImportMany(typeof(IStringSender))]
  10.     private IEnumerable<IStringSender> _stringSenders;
  11.  
  12.     public void SendMessage(string message)
  13.     {
  14.       foreach (var sender in _stringSenders)
  15.         sender.Send(message);
  16.     }
  17.   }
  18. }
* This source code was highlighted with Source Code Highlighter.
    Атрибут ImpotMany будет указывать MEF, что данный список нужно заполнить доступными плагинами.
  Теперь можно приступить непосредственно к написанию самих плагинов.
  Первый плагин будет выводить сообщение на консоль как обычный текст. Добавим в решение новый проект Class Library TextOutputPlugin. Для того чтобы иметь возможность использовать  интерфейс IStringSender в проекте, добавим в проект ссылку на проект Components. Ссылка на сборку System.ComponentModel.Composition нам тоже пригодится. Добавим в проект TextOutputPlugin класс TextSender:

  1. using System;
  2. using System.ComponentModel.Composition;
  3. using Components;
  4.  
  5. namespace TextOutputPlugin
  6. {
  7.   [Export(typeof(IStringSender))]
  8.   public class TextSender : IStringSender
  9.   {
  10.     #region IStringSender Members
  11.  
  12.     public void Send(string message)
  13.     {
  14.       Console.WriteLine(message);
  15.     }
  16.  
  17.     #endregion
  18.   }
  19. }
* This source code was highlighted with Source Code Highlighter.
   TextSender реализует интерфейс IStringSender из сборки Components. Его единственный метод выводит сообщение на консоль отдельной строкой. Атрибут Export  используется, чтобы пометить класс как расширение (плагин).
   Пусть плагина будет два, поэтому давайте добавим еще один проект. Как и TextOutputPlugin, проект XmlOutputPlugin будет типа Class Library и содержать ссылки на Components и System.ComponentModel.Composition. Опишем в этом проекте новый класс XmlSender:

  1. using System;
  2. using System.ComponentModel.Composition;
  3. using System.Xml;
  4. using Components;
  5.  
  6. namespace XmlOutputPlugin
  7. {
  8.   [Export(typeof(IStringSender))]
  9.   public class XmlSender: IStringSender
  10.   {
  11.     #region IStringSender Members
  12.  
  13.     public void Send(string message)
  14.     {
  15.       var doc = new XmlDocument();
  16.       doc.LoadXml("<message>" + message + "</message>");
  17.       doc.Save(Console.Out);
  18.       Console.WriteLine();
  19.     }
  20.  
  21.     #endregion
  22.   }
  23. }
* This source code was highlighted with Source Code Highlighter.
   Класс XmlSender выводит сообщение на консоль в виде документа XML.
   Теперь нам нужно указать MEF, что MessageSender из сборки Components должен содержать в списке плагинов объекты классов из сборок TextOutputPlugin и XmlOutputPlugin. Для этого добавим в класс MessageSender конструктор вида:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel.Composition;
  4. using System.ComponentModel.Composition.Hosting;
  5.  
  6. namespace Components
  7. {
  8.   public class MessageSender
  9.   {
  10.     [ImportMany(typeof(IStringSender))]
  11.     private IEnumerable<IStringSender> _stringSenders;
  12.  
  13.     public MessageSender(string extentionsDirectory)
  14.     {
  15.       var catalog = new DirectoryCatalog(String.IsNullOrEmpty(extentionsDirectory) ? "." : extentionsDirectory);
  16.       var container = new CompositionContainer(catalog);
  17.       container.ComposeParts(this);
  18.  
  19.     }
  20.  
  21.     public void SendMessage(string message)
  22.     {
  23.       foreach (var sender in _stringSenders)
  24.         sender.Send(message);
  25.     }
  26.   }
  27. }

* This source code was highlighted with Source Code Highlighter.
   Предположим, что расширение приложения будет происходить просто добавлением плагинов в определенное местно на диске (папку). Тогда в качестве каталога расширений будем использовать DirectoryCatalog. Параметр его конструктора - абсолютный или относительный путь к плагинам. Параметр "." означает, что плагины нужно искать в той же папке, что и исполняемое приложение. CompositionContatiner загружает плагины из этого каталога, а затем при помощи метода ComposeParts соотносит точки импорта и экспорта в сборках к друг другу.
   Отлично! Проекты бизнес-логики и плагинов готовы. Приступим к исполняемому приложению. Добавим в решение проект консольоного приложения Client. Наше приложение должно ссылаться на Components, ведь не зря же мы все это писали ;-). Главный метод будет выглядеть так:

  1. using Components;
  2.  
  3. namespace Client
  4. {
  5.   class Program
  6.   {
  7.     static void Main()
  8.     {
  9.       var ms = new MessageSender(string.Empty);
  10.       ms.SendMessage("Hello World!");
  11.       System.Console.WriteLine("Press any key...");
  12.       System.Console.ReadKey();
  13.     }
  14.   }
  15. }
* This source code was highlighted with Source Code Highlighter.
  В конструктор MessageSender мы передаем пустую строку и тем самым даем понять, что нас интересуют плагины, которые находятся в одной папке с исполняемым приложением. Если скомпилировать и запустить наше решение сейчас, то будет ошибка. Это же очевидно! Ведь сборки с плагинами будут в своих Bin, а не рядом с исполняемым файлом. При помощи свойств проекта укажем, что плагины будут строится в одну папку с Client проектом - закладка Build, Output path = '..\Client\bin\Debug\' для дабага и '..\Client\bin\Release\' для релиза.
   Запускаем решение и ... вуаля:

  Как мы видим, оба плагина вывели сообщения на консоль, но каждый по-своему.

2 комментария:

  1. Я не работал с MEF, но мне кажется, что если и использовать его, то лучше применять тактику constructor injection а не property injection. А как вам?

    ОтветитьУдалить
  2. Да это определенно лучше. Но в целях простоты понимания все же больше подходит property injection. Это основная цель записи.

    ОтветитьУдалить