Поиск на сайте: Расширенный поиск


Новые программы oszone.net Читать ленту новостей RSS
CheckBootSpeed - это диагностический пакет на основе скриптов PowerShell, создающий отчет о скорости загрузки Windows 7 ...
Вы когда-нибудь хотели создать установочный диск Windows, который бы автоматически установил систему, не задавая вопросо...
Если после установки Windows XP у вас перестала загружаться Windows Vista или Windows 7, вам необходимо восстановить заг...
Программа подготовки документов и ведения учетных и отчетных данных по командировкам. Используются формы, утвержденные п...
Red Button – это мощная утилита для оптимизации и очистки всех актуальных клиентских версий операционной системы Windows...
OSzone.net Microsoft Разработка приложений .NET Framework Создание составных приложений в .NET 4 на основе инфраструктуры управляемых расширений RSS

Создание составных приложений в .NET 4 на основе инфраструктуры управляемых расширений

Текущий рейтинг: 0 (проголосовало 0)
 Посетителей: 1265 | Просмотров: 3862 (сегодня 0)  Шрифт: - +

В выпуске Microsoft .NET Framework 4 вы найдете новую потрясающую технологию, которая значительно упростит разработку приложений. Если вас интересует проектирование приложений, которые были бы проще в сопровождении и расширении, читайте дальше.

Managed Extensibility Framework (MEF) — новая библиотека в составе .NET Framework 4 и Silverlight 4, облегчающая проектирование составных систем, которые после развертывания могут расширяться сторонними разработчиками. MEF позволяет постепенно добавлять в приложения новую функциональность разработчиками этих приложений, авторами инфраструктуры и сторонними компаниями.

Зачем мы создали MEF?

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

  • В Visual Studio 2010 создавали новый расширяемый редактор кода. Базовая функциональность этого редактора, равно как и функциональность от сторонних разработчиков, должна была развертываться в виде двоичных компонентов, которые распознавались бы в период выполнения. Одно из основных требований заключалось в необходимости поддержки отложенной загрузки расширений, чтобы ускорить время запуска и уменьшить потребление памяти.
  • В инфраструктуру «Oslo» был введен «Intellipad», новый расширяемый текстовый редактор для работы с MEF. В Intellipad подключаемые модули (плагины) должны были создаваться на IronPython.
  • Acropolis предоставляла инфраструктуру для построения составных приложений. Исполняющая среда Acropolis распознавала «фрагменты» («parts») компонента приложения в период выполнения и предоставляла эти фрагменты как свободно сопряженные сервисы. Для написания компонентов в Acropolis интенсивно использовался XAML.

Эта задача не уникальна для Microsoft. Клиенты издавна реализовали собственные решения для расширения существующих приложений. И здесь явно была бы уместна платформа, которая стала бы более универсальным решением как для Microsoft, так и для клиентов.

Нужно ли было нам что-то новое?

MEF ни в коем случае не является первым решением в этой области. До нее предлагалось много других решений — список ПО, способного пересекать границы платформ, достаточно длинный: это EJB, CORBA, Eclipse-реализация OSGI и Spring на стороне Java. На платформе Microsoft были компонентная модель и System.Addin в рамках самой .NET Framework. А еще несколько решений с открытым исходным кодом, в том числе архитектура SODA от SharpDevelop и контейнеры Inversion of Control вроде Castle Windsor, Structure Map и Unity (от Patterns & Practices).

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

Рассмотрим базовые концепции MEF, представленные на рис. 1 в общем виде.

*

Рис. 1. Базовые концепции Managed Extensibility Framework

Концепции

В основе MEF лежит несколько важных концепций, кратко описанных ниже.

Составляемый фрагмент (composable part) (или просто фрагмент) Фрагмент предоставляет сервисы другим фрагментам и использует их сервисы. Фрагменты MEF могут поступать откуда угодно — как от самого приложения, так и извне — с точки зрения MEF это не имеет ни малейшего значения.

Экспорт (export) Это сервис, предоставляемый фрагментом. То есть, когда фрагмент предоставляет экспорт, считается, что он экспортирует его. Например, фрагмент может экспортировать регистратор (logger) или в случае Visual Studio расширение редактора. Фрагменты могут предоставлять несколько экспортов, хотя большинство фрагментов предоставляет только один экспорт.

Импорт (import) Это сервис, используемый фрагментом. Когда фрагмент использует импорт, он импортирует его. Фрагменты могут импортировать один сервис (например, регистратор) или несколько (скажем, расширение редактора).

Контракты (contracts) Это идентификатор экспорта или импорта. Экспортер указывает предоставляемый им строковый контракт, а импортер сообщает, какой контракт нужен ему. В MEF имена контрактов наследуются от экспортируемых и импортируемых типов, так что в большинстве случаев вам не придется думать о них.

Композиция (composition) Фрагменты составляются MEF, которая создает их экземпляры, а затем соотносит экспортеров с импортерами.

Модели программирования — «лица» MEF

Разработчики используют MEF через модель программирования. Эта модель предоставляет средства для объявления компонентов как MEF-фрагментов. В готовом виде MEF предлагает модель программирования на основе атрибутов (attributed programming model), которая будет находиться в центре внимания этой статьи. Данная модель — лишь одна из множества возможных моделей программирования, поддерживаемых MEF. Базовый API в MEF полностью адаптирован к атрибутам.

Модель программирования на основе атрибутов

В этой модели фрагменты (также называемые фрагментами на основе атрибутов) определяются набором .NET-атрибутов, находящихся в пространстве имен System.ComponentModel.Composition. В следующих разделах мы изучим расширяемое WPF-приложение для управления заказами клиентов (sales order management), использующее такую модель. Это приложение позволяет клиентам добавлять новые, адаптированные под свои потребности представления простой установкой двоичного файла в папку bin. Я поясню, как реализовать такое с помощью MEF. По мере подачи материала я буду итеративно совершенствовать проект приложения, подробнее рассказывать о возможностях MEF и о том, что дает модель программирования на основе атрибутов.

Экспорт класса

Приложение для управления заказами клиентов дает возможность подключать новые представления. Для экспорта чего-либо в MEF нужно использовать атрибут Export:

[Export]
public partial class SalesOrderView : UserControl
{
public SalesOrderView()
  {
InitializeComponent();
  }
}

Приведенный выше фрагмент экспортирует контракт SalesOrderView. По умолчанию атрибут Export использует в качестве контракта конкретный тип члена (в данном случае — класса). Вы также можете явным образом указать контракт, передав параметр конструктору атрибута.

Импорт через свойства и поля

Во фрагментах на основе атрибутов можно помечать атрибутом Import свойство или поле. Приложение экспортирует фрагмент ViewFactory, с помощью которого другие фрагменты могут получать представления. Этот ViewFactory импортирует SalesOrderView, используя импорт свойства. Импорт свойства означает лишь то, что оно помечается атрибутом Import:

[Export]
public class ViewFactory
{
  [Import]
  public SalesOrderView OrderView { get; set; }
}

Импорт через конструкторы

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

[Export]
public class ViewFactory
{
  [ImportingConstructor]
  public ViewFactory(SalesOrderView salesOrderView)
{
}
}

В целом, выбор импорта через конструкторы или свойства — вопрос ваших предпочтений, хотя встречаются ситуации, где лучше использовать импорт через свойства, особенно при наличии фрагментов, экземпляры которых создаются вне инфраструктуры MEF, как в примере с WPF-приложением. Кроме того, параметры конструктора не поддерживают повторную композицию (recomposition).

Композиция

Подготовив SalesOrderView и ViewFactory, можно приступать к композиции. MEF-фрагменты автоматически не распознаются и не создаются. Вместо этого вам нужно написать стартовый код, который запускает композицию. Обычно это делают во входной точке приложения, каковой в данном случае является класс App.

Стартовый код должен выполнять несколько операций:

  • добавлять импорты контрактов, необходимые для создания контейнера;
  • создать каталог, с помощью которого MEF распознает фрагменты;
  • создать контейнер, который обеспечивает композицию экземпляров фрагментов;
  • выполнить композицию вызовом метода Composeparts контейнера и передать экземпляр, содержащий импорты.

Как показано ниже, я добавил импорт ViewFactory в класс App. Затем создал DirectoryCatalog, указывающий на папку bin, и создал контейнер, использующий каталог. Наконец, я вызвал Composeparts, что привело к композиции экземпляра App с импортом ViewFactory:

public partial class App : Application
{
  [Import]
public ViewFactory ViewFactory { get; set; }

public App()
  {
this.Startup += new StartupEventHandler(App_Startup);
  }

void App_Startup(object sender, StartupEventArgs e)
  {
var catalog = new DirectoryCatalog(@".\");
var container = new CompositionContainer(catalog);
container.Composeparts(this);
  }
}

В процессе композиции контейнер создаст ViewFactory и выполнит импорт SalesOrderView. Это приведет к созданию SalesOrderView. Наконец, класс приложения выполнит импорт ViewFactory. Благодаря этому MEF соберет целый граф объектов на основе декларативной информации.

Экспорт элементов, не относящихся к MEF, в инфраструктуру MEF через свойства

При интеграции MEF в существующее приложение или с другими инфраструктурами вы будете часто встречать экземпляры классов, не относящиеся к MEF (т. е. не являющиеся фрагментами), которые вы хотели бы сделать доступными импортерам. Это могут быть «запечатанные» инфраструктурные типы вроде System.String, Singleton-объекты уровня приложения, например Application.Current, или экземпляры, получаемые от фабрики, скажем, экземпляр регистратора из Log4Net.

Для таких ситуаций MEF поддерживает экспорт свойств (property exports). Чтобы использовать экспорт свойств, вы создаете промежуточный фрагмент со свойством, дополненным атрибутом Export. Это свойство, по сути, является фабрикой и выполняет любую вашу логику, необходимую для получения значения, не относящегося к MEF. В следующем примере кода Loggerpart экспортирует регистратор Log4Net, позволяя другим фрагментам вроде App импортировать его вместо того, чтобы полагаться на доступ через статический метод-акссесор:

public class Loggerpart
{
  [Export]
public ILog Logger
  {
get { return LogManager.GetLogger("Logger"); }
  }
}

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

Отделение реализации с помощью интерфейса

Вернемся к примеру с SalesOrderView. В нем было установлено тесное сопряжение между ViewFactory и SalesOrderView. Фабрика ожидает конкретный SalesOrderView, что ограничивает возможности расширения и тестирования самой фабрики. MEF позволяет отделять импорты от реализации экспортера, используя некий интерфейс как контракт:

public interface ISalesOrderView{}

[Export(typeof(ISalesOrderView))]
public partial class SalesOrderView : UserControl, ISalesOrderView
{
   ...
}

[Export]
public class ViewFactory
{
  [Import]
ISalesOrderView OrderView{ get; set; }
}

В предыдущем коде я изменил SalesOrderView так, чтобы он реализовал ISalesOrderView и явно экспортировал его. Я также модифицировал фабрику на стороне импортера для импорта ISalesOrderView. Заметьте, что импортер не должен явно указывать тип, так как MEF может вычислить его из типа свойства; в данном случае это ISalesOrderView.

Здесь возникает вопрос, а должен ли ViewFactory тоже реализовать интерфейс наподобие IViewFactory. Это не обязательно, но может пригодиться при необходимости подмены. В данном примере я не предполагаю, что кто-то станет заменять ViewFactory, он спроектирован в расчете на полные возможности тестирования, поэтому такой интерфейс ему не нужен. Фрагмент может выполнять импорт по нескольким контрактам. Так, SalesOrderView может экспортировать и UserControl, и ISalesOrderView, если вы включите дополнительный атрибут Export:

[Export (typeof(ISalesOrderView))]
[Export (typeof(UserControl))]
public partial class SalesOrderView : UserControl, ISalesOrderView
{
   ...
}

Сборки контрактов

Когда вы начнете создавать контракты, вам понадобится какой-то способ развертывания этих контрактов у ваших клиентов. Обычно делают так: создают сборку контрактов, содержащую интерфейсы для контрактов, которые будут реализовываться разработчиками расширений. Таким образом, сборка контрактов становится своего рода SDK, к которому могут обращаться фрагменты. Имя сборки контрактов принято формировать в виде имени приложения с добавлением постфикса «.Contracts», например SalesOrderManager.Contracts.

Импорт множества экспортов по одному контракту

Сейчас ViewFactory импортирует только одно представление. «Зашитый» в код член работает для очень ограниченного числа предопределенных типов представлений, которые меняются редко. Однако при таком подходе добавление нового представления потребует перекомпиляции фабрики.

Если ожидается много типов представлений, то MEF предлагает подход получше. Вместо интерфейса конкретного представления вы создаете обобщенный интерфейс IView, который экспортируют все представления. Затем фабрика импортирует набор всех доступных IView. Для импорта набора в модели на основе атрибутов используйте атрибут ImportMany:

[Export]
public class ViewFactory
{
  [ImportMany]
IEnumerable<IView> Views { get; set; }
}

[Export(typeof(IView))]
public partial class SalesOrderView : UserControl, IView
{
}
//in a contract assembly
public interface IView{}

Как видите, ViewFactory теперь импортирует набор экземпляров IView, а не конкретное представление. SalesOrder реализует IView и экспортирует его, а не ISalesOrderView. После такой переработки ViewFactory теперь поддерживает открытый набор представлений.

MEF также поддерживает импорт с использованием таких наборов, как ObservableCollection<T> или List<T>, и собственных наборов, предоставляющих конструктор по умолчанию.

Управление политикой создания фрагмента

По умолчанию все экземпляры фрагментов в контейнере являются Singleton-объектами (т. е. существующими только в одном экземпляре), а значит, они совместно используются любыми фрагментами, которые импортируют их в рамках контейнера. По этой причине все импортеры SalesOrderView и ViewFactory получат один и тот же экземпляр. Во многих случаях это самый удачный вариант, но иногда нужно, чтобы каждый импортер получал собственный экземпляр, например, чтобы на экране можно было просматривать сразу несколько экземпляров SalesOrderView.

Политика создания фрагмента в MEF может быть одной из следующих: CreationPolicy.Shared, CreationPolicy.NonShared или CreationPolicy.Any. Чтобы указать во фрагменте политику создания, вы должны дополнить его атрибутом partCreationPolicy:

[partCreationPolicy(CreationPolicy.NonShared)]
[Export(typeof(ISalesOrderView))]
public partial class SalesOrderView : UserControl, ISalesOrdderView
{
public SalesOrderView()
  {
  }
}

PartCreationPolicy также можно указывать на стороне импортера, задавая свойство RequiredCreationPolicy импорта.

Как различать экспорты с помощью метаданных

ViewFactory теперь работает с открытым набором представлений, но у меня нет возможности отличить одно представление от другого. Можно было бы добавить соответствующий член в IView и назвать его ViewType; тогда я фильтровал бы представления по этому свойству. В качестве альтернативы используйте метаданные MEF-экспорта и аннотируйте представление с помощью их ViewType. Применение метаданных дает дополнительное преимущество: создание экземпляра представления можно отложить до того момента, когда в нем реально возникнет необходимость, что позволяет экономить ресурсы и повышает быстродействие.

Определение метаданных экспорта

Чтобы определить метаданные экспорта, используйте атрибут ExportMetadata. В коде ниже SalesOrderView был изменен так, чтобы экспортировать интерфейс IView как контракт. Затем к нему были добавлены метаданные ViewType, чтобы данное представление можно было находить среди других, использующих тот же контракт:

[ExportMetadata("ViewType", "SalesOrder")]
[Export(typeof(IView)]
public partial class SalesOrderView : UserControl, IView
{
}

У ExportMetadata два параметра: строковый ключ и значение объекта типа. Применение магических строк, как в предыдущем примере, может оказаться проблематичным, поскольку они не надежны при компиляции (compile-safe). Вместо магической строки можно передать в ключе константу, а в качестве значения — перечислимое:

[ExportMetadata(ViewMetadata.ViewType, ViewTypes.SalesOrder)]
[Export(typeof(IView)]
public partial class SalesOrderView : UserControl, IView
{
  ...
}
//in a contract assembly
public enum ViewTypes {SalesOrderView}

public class ViewMetadata
{
public const string ViewType = "ViewType";
}

Использование атрибута ExportMetadata резко повышает гибкость, но на этом пути следует опасаться нескольких проблем:

  • ключи метаданных не распознаются в IDE. Автор фрагмента должен знать, какие ключи метаданных и типы допустимы для данного экспорта;
  • компилятор не проверяет корректность метаданных;
  • ExportMetadata загромождает код, делая его менее понятным.

MEF предоставляет решение всех этих проблем: собственные экспорты (custom exports).

Атрибуты собственного экспорта

MEF разрешает создавать собственные экспорты, которые включают свои метаданные. Создание собственного экспорта влечет за собой создание производного ExportAttribute, который тоже определяет метаданные. Мы можем использовать собственные экспорты для создания атрибута ExportView, включающего метаданные для ViewType:

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class ExportViewAttribute : ExportAttribute {
public ExportViewAttribute()
:base(typeof(IView))
  {}

public ViewTypes ViewType { get; set; }
}

ExportViewAttribute указывает, что он экспортирует IView, вызовом базового конструктора Export. Он дополнен атрибутом MetadataAttribute, предоставляющим метаданные. Этот атрибут сообщает MEF искать все открытые свойства и создавать соответствующие метаданные в экспорте, используя имя свойства как ключ. В нашем случае единственные метаданные — ViewType.

И последнее, что стоит отметить по поводу атрибута ExportView. Он дополняется атрибутом AttributeUsage. Тот указывает, что данный атрибут допустим только для классов и что разрешается наличие лишь одного атрибута ExportView.

В целом, AllowMultiple следует устанавливать в false; если он равен true, импортеру будет передан массив значений, а не единственное значение. AllowMultiple нужно устанавливать в true при наличии нескольких экспортов с разными метаданными одного контракта в одном и том же члене.

Применение нового ExportViewAttribute к SalesOrderView теперь приведет к следующему:

[ExportView(ViewType = ViewTypes.SalesOrder)]
public partial class SalesOrderView : UserControl, IView
{
}

Как видите, собственные экспорты гарантируют, что для конкретного экспорта будут предоставлены корректные метаданные. Они также уменьшают загромождение кода, делают его более понятным и обеспечивают распознавание через IntelliSense.

Теперь, когда в представлении определены метаданные, ViewFactory может импортировать их.

Импорт отложенного экспорта и обращение к метаданным

Чтобы разрешить доступ к метаданным, MEF использует новый API в .NET Framework 4 — System.Lazy<T>. Он позволяет откладывать создание экземпляра до реального обращения к свойству Value объекта Lazy. MEF расширяет Lazy<T> до Lazy<T,TMetadata>, который позволяет обращаться к метаданным экспорта без создания экземпляра нижележащего экспорта.

TMetadata — это тип представления метаданных. Представление метаданных (metadata view) — интерфейс, определяющий свойства только для чтения, которые соответствуют ключам в экспортируемых метаданных. При обращении к свойству метаданных MEF будет динамически реализовать TMetadata и присваивать значения на основе метаданных, предоставленных экспортом.

Вот как выглядит ViewFactory, когда свойство View изменяется на импорт с использованием Lazy<T,TMetadata>:

[Export]
public class ViewFactory
{
  [ImportMany]
IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; }
}

public interface IViewMetadata
{
ViewTypes ViewType {get;}
}

После импорта набора отложенных экспортов с метаданными вы можете использовать LINQ для фильтрации этого набора. В следующем фрагменте кода я реализовал метод GetViews представления ViewFactory для получения всех представлений заданного типа. Заметьте, что он обращается к свойству Value для создания реальных экземпляров представлений, только если эти представления соответствуют фильтру:

[Export]
public class ViewFactory
{
  [ImportMany]
IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; }

public IEnumerable<View> GetViews(ViewTypesviewType) {
return Views.Where(v=>v.Metadata.ViewType.Equals(viewType)).Select(v=>v.Value);
  }
}

Теперь ViewFactory распознает все представления, доступные на момент выполняемой MEF композиции фабрики. Если после первой композиции в контейнере или каталоге появятся новые реализации, они не будут видны ViewFactory, так как его композиция уже выполнена. Более того, MEF не допустит добавления представлений в каталог, генерируя исключение CompositionException; однако это относится к случаю, когда не разрешена повторная композиция.

Повторная композиция

Повторная композиция (recomposition) — функция MEF, позволяющая фрагмента автоматически получать обновленные импорты, когда в системе появляются новые подходящие экспорты. Одна из ситуаций, где очень полезна повторная композиция, — скачивание фрагментов с удаленного сервера. SalesOrderManager может быть изменен, поэтому при запуске он инициирует скачивание нескольких дополнительных (необязательных) представлений. По мере загрузки эти представления появляются в фабрике представлений. Чтобы разрешить повторную композицию ViewFactory, мы устанавливаем свойство AllowRecomposition атрибута ImportMany свойства Views в true:

[Export]
public class ViewFactory
{
[ImportMany(AllowRecomposition=true)]
IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; }

public IEnumerable<View>GetViews(ViewTypesviewType) {
return Views.Where(v=>v.Metadata.ViewType.Equals(viewType)).Select(v=>v.Value);
  }
}

При повторной композиции набор Views будет мгновенно заменен новым набором, содержащим обновленные представления.

Если повторная композиция разрешена, приложение может скачивать дополнительные сборки с сервера и добавлять их в контейнер. Вы можете делать это через каталоги MEF. В MEF есть несколько каталогов, два из которых поддерживают повторную композицию. DirectoryCatalog, который вы уже видели, — как раз один из таких: вызвав его метод Refresh, можно выполнить его повторную композицию. Другим таким каталогом является AggregateCatalog — каталог каталогов. Вы добавляете в него каталоги с помощью свойства-набора Catalogs, а это инициирует повторную композицию. Последний каталог, которым я воспользуюсь, — AssemblyCatalog; он принимает сборку, а затем создает на ее основе каталог. На рис. 2 показан пример, иллюстрирующий, как использовать все эти каталоги для динамической загрузки.

Рис. 2. Использование MEF-каталогов для динамической загрузки

void App_Startup(object sender, StartupEventArgs e)
{
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(newDirectoryCatalog((@"\.")));
var container = new CompositionContainer(catalog);
container.Composeparts(this);
base.MainWindow = MainWindow;
this.DownloadAssemblies(catalog);
}

private void DownloadAssemblies(AggregateCatalog catalog)
{
//asynchronously downloads assemblies and calls AddAssemblies
}

private void AddAssemblies(Assembly[] assemblies, AggregateCatalog catalog)
{
var assemblyCatalogs = new AggregateCatalog();
foreach(Assembly assembly in assemblies)
assemblyCatalogs.Catalogs.Add(new AssemblyCatalog(assembly));
catalog.Catalogs.Add(assemblyCatalogs);
}

Контейнер на рис. 2 создается с помощью AggregateCatalog. Затем в него добавляется DirectoryCatalog, чтобы получить локальные фрагменты в папке bin. Объединенный каталог (aggregate catalog) передается методу DownloadAssemblies, который асинхронно загружает сборки с сервера, а потом вызывает AddAssemblies. Этот метод создает новый AggregateCatalog, в который добавляет AssemblyCatalog для каждой загруженной сборки. Затем AddAssemblies добавляет AggregateCatalog, содержащий сборки для основного агрегата. Причина добавления в таком стиле — необходимость выполнения повторной композиции одним махом, без ее повторения для каждой сборки (что и произошло бы, если бы мы добавляли каталоги сборок напрямую).

Как только происходит повторная композиция, набор немедленно обновляется. В зависимости от типа свойства-набора результаты могут отличаться. Если свойство имеет тип IEnumerable<T>, набор заменяется новым экземпляром. Если же это конкретный набор, наследующий от List<T> или ICollection, то MEF для каждого элемента будет вызывать Clear и Add. В любом случае при выполнении повторной композиции вам стоит позаботиться о безопасности в многопоточной среде. Понятие повторной композиции относится не только к добавлению чего-либо, но и удалению. Если каталоги удаляются из контейнера, содержащиеся в них фрагменты тоже удаляются.

Стабильная композиция, отклонение и диагностика

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

Ниже SalesOrderView был изменен на импорт ILogger, хотя экземпляр регистратора отсутствует:

[ExportView(ViewType = ViewTypes.SalesOrder)]
public partial class SalesOrderView : UserControl, IView
{
[Import]
public ILogger Logger { get; set; }
}

Поскольку экспорта ILogger нет, экспорт SalesOrderView не появится в контейнере. Это не приведет к генерации исключения — просто SalesOrderView будет проигнорирован. Если вы проверите набор Views в ViewFactory, то увидите, что он пуст.

Отклонение (rejection) также происходит, когда для единственного импорта доступно множество экспортов. В таких случаях фрагмент, импортирующий единственный экспорт, отклоняется:

[ExportView(ViewType = ViewTypes.SalesOrder)]
public partial class SalesOrderView : UserControl, IView
{
[Import]
public ILogger Logger { get; set; }
}
 [Export(typeof(ILogger))]
public partial class Logger1 : ILogger
{
}
 [Export(typeof(ILogger))]
public partial class Logger2 : ILogger
{
}

В предыдущем примере SalesOrderView будет отклонен, так как имеется несколько реализаций ILogger, но импортируется только одна из них. Однако в MEF есть средства, разрешающие экспорт по умолчанию при наличии нескольких экспортов. Подробности см. по ссылке codebetter.com/blogs/glenn.block/archive/2009/05/14/customizing-container-behavior-part-2-of-n-defaults.aspx.

Возможно, вас интересует, почему MEF не создает SalesOrderView и не генерирует исключение. Если бы MEF генерировал исключение в открытой расширяемой системе, приложению было бы очень трудно его обработать или получить контекст, в котором было сгенерировано исключение, поскольку фрагмент может отсутствовать или же импорт вложен очень глубоко в композиции. Без должной обработки приложение перешло бы в недопустимое состояние и стало бы непригодным для использования. Поэтому MEF отклоняет фрагмент и тем самым сохраняет стабильность приложения. Подробнее о стабильной композиции см. по ссылке blogs.msdn.com/gblock/archive/2009/08/02/stable-composition-in-mef-preview-6.aspx.

Диагностика отклонения

Отклонение — очень мощный механизм, но иногда сложно диагностировать причину, особенно когда отклоняется целый граф зависимостей. В первом примере ViewFactory напрямую импортировал SalesOrderView. Допустим, MainWindow, импортируемый ViewFactory и SalesOrderView, отклоняется. А заодно отклоняется и ViewFactory. Обнаружив это, вы, возможно, будете долго чесать затылок, зная, что MainWindow и ViewFactory на самом деле присутствуют. А причина отклонения в отсутствующей зависимости.

Но MEF не оставляет вас в полной темноте. Чтобы помочь в диагностике этой проблемы, он поддерживает трассировку. В IDE все сообщения отклонения трассируются в окно вывода, но их можно перенаправлять любому допустимому слушателю трассировки. Например, когда приложение пытается импортировать MainWindow, выводятся трассировочные сообщения, показанные на рис. 3.

Рис. 3. Трассировочные сообщения MEF

System.ComponentModel.Composition Warning: 1 : The ComposablepartDefinition 'Mef_MSDN_Article.SalesOrderView' has been rejected. The composition remains unchanged. The changes were rejected because of the following error(s): The composition produced a single composition error. The root cause is provided below. Review the CompositionException.Errors property for more detailed information.

1) No valid exports were found that match the constraint '((exportDefinition.ContractName == "Mef_MSDN_Article.ILogger") AndAlso (exportDefini-tion.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "Mef_MSDN_Article.ILogger".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity"))))', invalid exports may have been rejected.

Resulting in: Cannot set import 'Mef_MSDN_Article.SalesOrderView.Logger (ContractName="Mef_MSDN_Article.ILogger")' on part 'Mef_MSDN_Article.SalesOrderView'.
Element: Mef_MSDN_Article.SalesOrderView.logger (ContractName="Mef_MSDN_Article.ILogger") -->Mef_MSDN_Article.SalesOrderView -->TypeCatalog (Types='Mef_MSDN_Article.MainWindow, Mef_MSDN_Article.SalesOrderView, ...').

Трассировочный вывод указывает на корень проблемы: SalesOrderView требует ILogger, а его найти не удалось. Далее видно, что его отклонение приводит к отклонению фабрики и в конечном счете MainWindow.

Анализ фрагментов в отладчике

Вы можете сделать еще один шаг и проверить доступные фрагменты в каталоге (см. также раздел по хостингу). На рис. 4 в окне Watch показаны доступные фрагменты (обведенные темными линиями), а также необходимый импорт ILogger (обведен более светлой линией).

*
Увеличить

Рис. 4. Доступные фрагменты и необходимый ILogger показываются в окне Watch

Диагностика отклонения средствами командной строки

Одна из целей MEF — поддержка возможности статического анализа, что позволяет анализировать композицию вне исполняющей среды. Соответствующих инструментов в Visual Studio пока нет, но Николас Блумхардт (Nicholas Blumhardt) создал программу MEFX.exe (mef.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=33536) — утилиту командной строки, которая как раз и делает то, что нужно. MEFX анализирует сборки и определяет, какие фрагменты отклонены и почему.

Запустив MEFX.exe из командной строки, вы увидите набор параметров; с их помощью вы можете перечислять конкретные импорты, экспорты или все доступные фрагменты. Например, ниже MEFX выводит список фрагментов:

C:\mefx>mefx.exe /dir:C:\SalesOrderManagement\bin\debug /parts
SalesOrderManagement.SalesOrderView
SalesOrderManagement.ViewFactory
SalesOrderManagement.MainWindow

Это полезно для изучения состава фрагментов, но MEFX также может отслеживать причины отклонений, которые нас и интересуют (рис. 5).

Рис. 5. Отслеживание отклонений с помощью MEFX.exe

C:\mefx>mefx.exe /dir:C:\SalesOrderManagement\bin\debug /rejected /verbose

[part] SalesOrderManagement.SalesOrderView from: DirectoryCatalog (Path="C:\SalesOrderManagement\bin\debug")
  [Primary Rejection]
  [Export] SalesOrderManagement.SalesOrderView (ContractName="SalesOrderManagement.IView")
  [Export] SalesOrderManagement.SalesOrderView (ContractName="SalesOrderManagement.IView")
  [Import] SalesOrderManagement.SalesOrderView.logger (ContractName="SalesOrderManagement.ILogger")
    [Exception] System.ComponentModel.Composition.ImportCardinalityMismatchException: No valid exports were found that match the constraint '((exportDefinition.ContractName == "SalesOrderManagement.ILogger") AndAlso (exportDefinition.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "SalesOrderManagement.ILogger".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity"))))', invalid exports may have been rejected.
at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition, AtomicCompositionatomicComposition)
at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition)
at Microsoft.ComponentModel.Composition.Diagnostics.CompositionInfo.AnalyzeImportDefinition(ExportProvider host, IEnumerable`1 availableparts, ImportDefinition id)

Проанализировав вывод на рис. 6, мы обнаружим корневую причину проблемы: ILogger не найден. Как видите, в крупных системах с большим количеством фрагментов MEFX — просто бесценный инструмент. Подробнее о MEFX см. blogs.msdn.com/nblumhardt/archive/2009/08/28/analyze-mef-assemblies-from-the-command-line.aspx.

*
Увеличить

Рис. 6. Пример фрагмента на IronRuby

Итак, модель программирования на основе атрибутов дает несколько преимуществ:

  • предоставляет фрагментам универсальный способ объявлять свои экспорты и импорты;
  • позволяет системам динамически обнаруживать доступные фрагменты;
  • обеспечивает статический анализ, позволяя таким утилитам, как MEFX, заранее определять возможные сбои.

Теперь я быстро пройдусь по архитектуре и покажу, что она поддерживает. В самом общем виде архитектура MEF разделена на три уровня: Programming Models (модели программирования), Hosting (хостинг) и Primitives (примитивы).

И снова о моделях программирования

Модель на основе атрибутов — просто одна из реализаций таких примитивов, которые используют атрибуты как средства распознавания. Примитивы могут представлять фрагменты без атрибутов или даже фрагменты без статической типизации, как, например, в Dynamic Language Runtime (DLR). На рис. 6 показан фрагмент на IronRuby, экспортирующий IOperation. Заметьте, что он использует «родной» синтаксис IronRuby для объявления фрагмента, а не модель на основе атрибутов, так как атрибуты в DLR не поддерживаются.

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

Подробнее об экспериментах с моделью программирования Ruby читайте в серии статей в блоге blogs.msdn.com/nblumhardt/archive/tags/Ruby/default.aspx.

Хостинг: там, где выполняется композиция

Модели программирования определяют фрагменты, импорты и экспорты. Для реального создания экземпляров и графов объектов MEF предоставляет API хостинга, который в основном заключен в пространстве имен System.ComponentModel.Composition.Hosting. Уровень хостинга обеспечивает высокую гибкость, широкие возможности в настройке и расширяемости. Это то самое место, где MEF выполняет большую часть работы и где начинается распознавание. Большинство разработчиков, просто создающих фрагменты, никогда не будет иметь дела с этим пространством имен. Но если вы являетесь хостером, то вы будете использовать его так, как это делал я для инициации композиции.

Каталоги предоставляют определения фрагментов (ComposablepartDefinition), описывающие доступные экспорты и импорты. Они являются основными единицами распознавания в MEF. MEF предоставляет несколько каталогов в пространстве имен System.ComponentModel.Composition, некоторые из которых вы уже видели, в том числе DirectoryCatalog (сканирует каталог), AssemblyCatalog (сканирует сборку) и TypeCatalog (сканирует конкретный набор типов). Каждый из этих каталогов специфичен для модели программирования на основе атрибутов. Однако AggregateCatalog нейтрален к моделяи программирования. Каталоги наследуют от ComposablepartCatalog и являются точками расширения в MEF. Собственные каталоги находят несколько применений — от предоставления совершенно новой модели программирования до обертывания и фильтрации существующих каталогов.

На рис. 7 приведен пример фильтрующего каталога, который принимает предикат для фильтрации внутреннего каталога, из которого возвращаются фрагменты, удовлетворяющие заданным критериям.

Рис. 7. Фильтрующий каталог

public class FilteredCatalog : ComposablepartCatalog,
{
private readonly composablepartcatalog _inner;
private readonly IQueryable<ComposablepartDefinition> _partsQuery;

public FilteredCatalog(ComposablepartCatalog inner,
Expression<Func<ComposablepartDefinition, bool>> expression)
  {
      _inner = inner;
    _partsQuery = inner.parts.Where(expression);
  }

public override IQueryable<ComposablepartDefinition> parts
  {
get
      {
return _partsQuery;
      }
  }
}

CompositionContainer выполняет композицию, т. е. создает фрагменты и удовлетворяет импорты этих фрагментов, используя пул доступных экспортов. Если у этих экспортов тоже есть импорты, контейнер сначала удовлетворяет их. Тем самым конейнер собирает по требованию целые графы объектов. Главный источник пула экспортов — каталог, но в контейнере тоже могут находиться экземпляры фрагментов, которые добавлены в него напрямую и над которыми уже выполнена композиция.

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

var catalog = new DirectoryCatalog(@".\");
var childCatalog = new DirectoryCatalog(@".\Child\";
var rootContainer = new CompositionContainer(rootCatalog));
var childContainer = new CompositionContainer(childCatalog,
rootContainer);

В этом коде childContainer является дочерним по отношению к rootContainer. И rootContainer, и childContainer предоставляют свои каталоги. Подробнее об использовании контейнера для хостинга MEF внутри ваших приложений см. по ссылке codebetter.com/blogs/glenn.block/archive/2010/01/15/hosting-mef-within-your-applications.aspx.

Примитивы: откуда берут свое начало фрагменты и модели программирования

Примитивы, находящиеся в пространстве имен System.ComponentModel.Composition.Primitives, занимают самый нижний уровень в MEF. Они являются «квантом» MEF, так сказать, и ее точками максимального расширения. В своей статье я рассказал о модели программирования на основе атрибутов, но контейнер MEF отнюдь не ограничен атрибутами; его основа — примитивы. Примитивы описывают абстрактное представление фрагментов, которые включают такие определения, как ComposablepartDefinition, ImportDefinition и ExportDefinition, а также Composablepart и Export, представляющие реальные экземпляры.

Изучение примитивов я оставлю на вашу долю и, возможно, напишу о них в одной из будущих статей. А пока читайте более подробную информацию о них по ссылке blogs.msdn.com/dsplaisted/archive/2009/06/08/a-crash-course-on-the-mef-primitives.aspx.

MEF в Silverlight 4

MEF также поставляется как часть Silverlight 4. Все, о чем я говорил в этой статье, полностью относится и к разработке расширяемых полнофункциональных веб-приложений (Rich Internet Applications, RIA). В Silverlight мы пошли еще дальше и ввели дополнительные API, упрощающие создание приложений с применением MEF. Эти расширения в конечном счете будут включены в .NET Framework.

Подробнее о MEF в Silverlight 4 см. в статье по ссылке codebetter.com/blogs/glenn.block/archive/2009/11/29/mef-has-landed-in-silverlight-4-we-come-in-the-name-of-extensibility.aspx.

Я лишь слегка затронул возможности, предоставляемые MEF. Это мощная, отказоустойчивая и гибкая инфраструктура, которая открывает перед вашими приложениями целый мир новых возможностей! А что с ней делаете вы?

Автор: Гленн Блок  •  Иcточник: Журнал MSDN  •  Опубликована: 02.12.2010
Нашли ошибку в тексте? Сообщите о ней автору: выделите мышкой и нажмите CTRL + ENTER
Теги:   .NET 4.


Оценить статью:
Вверх
Комментарии посетителей
Комментарии отключены. С вопросами по статьям обращайтесь в форум.