Опубликован: 13.12.2011 | Уровень: для всех | Доступ: платный
Лекция 11:

Реализация паттерна MVVM с использованием IoC-контейнера, как метод избавления от зависимости между компонентами системы

< Лекция 10 || Лекция 11: 1234 || Лекция 12 >

MEF

Библиотека MEF появилась относительно недавно, но быстро завоевала популярность у .NET разработчиков за простоту использования и эффективность. Она позволяет строить модульные приложения с минимальным уровнем связности частей (parts) приложения. Эта библиотека включает в себя не только Dependency Injection контейнер, но большой объём инфраструктуры: множество механизмов поиска элементов композиции в сборках, удалённых XAP файлах, механизм пометки элементов композиции с помощью .Net атрибутов и т.д. Также существует версия MEF для Silverlight, которая имеет незначительные отличия от настольной версии.

IoC контейнер в MEF инкапсулируется классом CompositionContainer. Он содержит каталог типов, доступных для инъектирования. Наиболее удобным каталогом является DirectoryCatalog, включающий в себя типы из всех сборок, найденных в папке с приложением:

  1: CompositionContainer container =
  2:		new CompositionContainer(new DirectoryCatalog("."));

Для удовлетворения зависимостей определенного класса используется метод ComposeParts:

  1: container.ComposeParts(this);

Стоит иметь в виду, что как и другие IoC контейнеры, перед передачей компоненты в качестве импорта MEF удовлетворяет все её зависимости.

Компоненты в MEF не зависят друг от друга напрямую, вместо этого они зависят от контракта, который указывается строковым идентификатором. Каждый компонент объявляет контракты, которые он реализует, а каждая зависимость указывает необходимый ей контракт.

Реализация какого-либо контракта в терминологии MEF называется экспортом. Экспорты задаются при помощи атрибута [System.ComponentModel.Composition.ExportAttribute]. Данным атрибутом помечается класс сервиса, реализующего контракт. В качестве аргумента в конструктор атрибута передается имя реализуемого контракта. Следующий пример определяет контракт ILogger и объект Logger, его реализующий:

  1: public interface ILogger
  2: {
  3:     void Log(string message);
  4: }
  5:
  6: [Export(typeof(ILogger))]
  7: public class Logger : ILogger
  8: {
  9:     public void Log(string message)
 10:     {
 11:         Console.WriteLine(message);
 12:     }
 13: }

Чтобы различать несколько реализаций одного контракта, в экспорте возможно указать его имя:

  1: [Export("GUI", typeof(ILogger))]
  2: public class Logger : ILogger
  3: {
  4:     public void Log(string message)
  5:     {
  6:         MessageBox.Show(message);
  7:     }
  8: }

В таком случае возможно как получить все реализации контракта ILogger, так и найти конкретную спецификацию, работающую с графическим интерфейсом, по ключевому слову GUI.

По умолчанию MEF работает по принципу пассивной инверсии зависимостей. Зависимости от компонент в терминологии MEF называются импортами. Импорты с внедрением через устанавливаемое свойство или поле задаются при помощи атрибута [System.ComponentModel.Composition.ImportAttribute].

  1: public class Processor
  2: {
  3:     [Import]
  4:     public ILogger Logger { private get; set; }
  5:
  6:     public void Process()
  7:     {
  8:         Logger.Log("Hello world");
  9:     }
 10: }

При внедрении через конструктор, этот конструктор помечается атрибутом [System.ComponentModel.Composition.ImportingConstructorAttribute].

  1: public class Processor
  2: {
  3:     [ImportingConstructor]
  4:     public Processor(ILogger logger)
  5:     {
  6:         _logger = logger;
  7:     }
  8:
  9:     // Private fields
 10:     private ILogger _logger;
 11:
 12:     public void Process()
 13:     {
 14:         _logger.Log("Hello world");
 15:     }
 16: }

Обычно MEF определяет имя импортируемого контракта исходя из CLR типа свойства, поля или параметра конструктора (в зависимости от выбранного способа внедрения). Помимо этого существует возможность указать имя контракта вручную, через аргумент конструктора атрибута:

  1: public class Processor
  2: {
  3:     [Import("GUI")]
  4:     public ILogger Logger { private get; set; }
  5:
  6:     public void Process()
  7:     {
  8:         Logger.Log("Hello world");
  9:     }
 10: }

Помимо пассивной инверсии зависимостей при помощи MEF возможно реализовать активную инверсию зависимостей с использованием библиотеки CommonServiceLocator. Данная библиотека содержит обобщенный интерфейс для разрешения зависимостей, абстрагированный от конкретного IoC контейнера:

  1: var locator = new MefServiceLocator(CompositionContainer);
  2: ServiceLocator.SetLocatorProvider(() => locator);

MVVM и IoC

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

Здесь необходимо принять архитектурное решение, так как при разрешении зависимостей необходимо иметь в виду порядок создания объектов:

  • Конструировать модель представления первой. В таком случае представление будет иметь зависимость (прямую, или на уровне конвенции наименований) от модели представления.
  • Конструировать представление первым. Тогда модель представления будет требовать в качестве зависимости представление.

В большинстве случаев первый вариант является оптимальным выбором, т.к., с одной стороны, для покрытия кода тестами модель представления должна быть полностью работоспособной, даже если с ней не ассоциировано ни одного представления, и с другой стороны, работа представления без модели представления бессмысленна – без логики и привязки данных это будет всего лишь пустой неинтерактивный макет.

Определение моделей представления

Для возможности разрешения моделей представления через IoC контейнер, безусловно, в первую очередь необходимо зарегистрировать их в качестве экспорта в MEF:

  1: [Export]
  2: [PartCreationPolicy(CreationPolicy.NonShared)]
  3: public class MainViewModel
  4: {
  5:     //
  6: }

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

Для разрешения модели в контексте текущего IoC контейнера представления достаточно воспользоваться определенным выше интерфейсом ServiceLocator: так, для получения модели представления MainViewModel необходимо выполнить код:

  1: var mainViewModel =
  2: 			ServiceLocator.Current.GetInstance<MainViewModel>();

В данном случае можно возразить, что с точки зрения языка корректно создать экземпляр модели представления посредством оператора new, не привлекая к работе IoC контейнер. Однако, хоть такая операция и не произведет ошибок компиляции, код будет не совсем верен: у созданной через конструктор по умолчанию модели представления не будут удовлетворены зависимости (импорты), так как IoC контейнер о ней будет попросту не знать. Также использование IoC контейнера для создания моделей представления открывает возможность использования элементов аспектно-ориентированного программирования, о чем будет сказано несколько позже.

Определение представлений

Следующим этапом является регистрация представлений и декларативная привязка их к соответствующим моделям представления.

Для абстрагирования от конкретной реализации представления в соответствии с принципами инверсии управления необходимо определить общий интерфейс:

  1: public interface IView
  2: {
  3:     object DataContext { set; }
  4: }

Представления будут реализовывать данный интерфейс, в качестве ключа экспорта указывая свое имя:

  1: [Export("MainView", typeof(IView))]
  2: [PartCreationPolicy(CreationPolicy.NonShared)]
  3: public partial class MainView : IView
  4: {
  5:     public MainView()
  6:     {
  7:         InitializeComponent();
  8:     }
  9: }
< Лекция 10 || Лекция 11: 1234 || Лекция 12 >
Анисимов Михаил
Анисимов Михаил
Украина
Наталия Шаститко
Наталия Шаститко
Украина, Днепропетровск, Днепропетровский Гуманитарный Университет, 2014