Managed Extensibility Framework

74

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

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

Архитектура MEF

Это полная картина того, как загружаются части. Они находятся в каталоге. Каталог использует экспорты для нахождения частей. Поставщик экспорта обращается к каталогу, чтобы предложить экспорты из каталога. Множественные поставщики экспорта могут быть соединены в цепочки для настройки экспорта, например, со специальным поставщиком экспорта, который допускает только части для определенных пользователей или ролей. Контейнер использует поставщиков экспорта для подключения импортов к экспортам, и сам по себе является поставщиком экспорта.

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

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

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

Начнем демонстрацию работы с архитектурой MEF с простого примера. Приложениехост может динамически загружать дополнения. В MEF дополнение называется частью (part). Части определены, как экспорты и загружаются в контейнер, который импортирует части. Контейнер находит части с использованием каталога (catalog). В каталоге перечислены части.

В данном примере простое консольное приложение служит хостом, позволяющим подключать дополнения-калькуляторы из библиотеки. Чтобы обеспечить независимость хоста и дополнения-калькулятора, потребуется создать три сборки. Одна сборка SimpleContract хранит контракты, используемые и сборкой дополнения, и исполняемым хостом. Сборка дополнения SimpleCalculator реализует контракт, определенный сборкой контракта SimpleContract. Хост использует эту сборку для вызова дополнений. Контракт в сборке SimpleContract определяется двумя интерфейсами — ICalculator и IOperation.

Интерфейс ICalculator определяет методы GetOperations() и Operate(). Метод GetOperations() возвращает список всех операций, поддерживаемых дополнением-калькулятором, а с помощью метода Operate() операция вызывается. Этот интерфейс гибок в том смысле, что калькулятор может поддерживать различные операции. Если бы интерфейс определял методы Add() и Substract() вместо гибкого метода Operate(), для поддержки операций Divide() и Multiply() потребовалась бы новая версия интерфейса.

С помощью интерфейса ICalculator, как определено в этом примере, калькулятор может предоставлять любое количество операций с любым числом операндов:

using System.Collections.Generic;

namespace MEF
{
    public interface ICalculator
    {
        IList<IOperation> GetOperations();
        double Operate(IOperation operation, double[] operands);
    }
}

Интерфейс ICalculator использует интерфейс IOperation для возврата списка операций и их вызова. Интерфейс IOperation определяет доступные только для чтения свойства Name и NumberOperands:

namespace MEF
{
    public interface IOperation
    {
        string Name { get; }
        int NumberOperands { get; }
    }
}

Сборка SimpleContract не требует никаких ссылок на сборки MEF. В ней содержатся только простые интерфейсы .NET.

Сборка дополнений SimpleCalculator содержит классы, реализующие интерфейсы, которые определены контрактами. Класс Operation реализует интерфейс IOperation. Этот класс содержит только два свойства, как определено в интерфейсе. Интерфейс определяет средства доступа get для свойств; внутреннее средство доступа set используется для установки свойств изнутри сборки:

namespace MEF
{
    public class Operation : IOperation
    {
        public string Name { get; internal set; }
        public int NumberOperands { get; internal set; }
    }
}

Класс Calculator обеспечивает функциональность данного дополнения, реализуя интерфейс ICalculator. Класс Calculator экспортируется как часть, что определено атрибутом Export. Этот атрибут определен в пространстве имен System.ComponentModel.Composition в одноименной сборке:

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;

namespace MEF
{
    [Export(typeof(ICalculator))]
    public class Calculator : ICalculator
    {
        public IList<IOperation> GetOperations()
        {
            return new List<IOperation>()
            {
                new Operation { Name="+", NumberOperands=2},
                new Operation { Name="-", NumberOperands=2},
                new Operation { Name="/", NumberOperands=2},
                new Operation { Name="*", NumberOperands=2}
            }; 
        }

        public double Operate(IOperation operation, double[] operands)
        {
            double result = 0;
            switch (operation.Name)
            {
                case "+":
                    result = operands[0] + operands[1];
                    break;
                case "-":
                    result = operands[0] - operands[1];
                    break;
                case "/":
                    result = operands[0] / operands[1];
                    break;
                case "*":
                    result = operands[0] * operands[1];
                    break;
                default:
                    throw new InvalidOperationException(
                        String.Format("Некорректная операция {0}", operation.Name));
            }
            return result;
        }
    }
}

Приложение-хост является простым консольным приложением. Дополнение использует атрибут Export для определения того, что должно экспортироваться; в приложении хосте атрибут Import определяет, что им должно использоваться. Здесь атрибут Import аннотирует свойство Calculator, который устанавливает и получает объект, реализующий ICalculator. Таким образом, здесь может использоваться любое дополнение-калькулятор, которое реализует этот интерфейс:

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using MEF.Properties;

namespace MEF
{
    class Program
    {
        [Import]
        public ICalculator Calculator { get; set; }
        ...

В методе Main() консольного приложения создается новый экземпляр класса Program и вызывается метод Run(). В методе Run() создается каталог DirectoryCatalog, инициализируемый дополнением AddInDirectory, который настроен в конфигурационном файле приложения. Settings.Default.AddInDirectory использует свойство проекта Settings для применения строго типизированного класса с целью доступа к специальной конфигурации.

Класс CompositionContainer — это репозиторий частей. Данный контейнер инициализируется DirectoryCatalog для получения частей из папки, обслуживаемой этим каталогом. ComposeParts() — расширяющий метод, который расширяет класс CompositionContainer и определяется классом AttributedModelServices. Этот метод требует частей, передаваемых в аргументах атрибута Import. Поскольку класс Program имеет атрибут Import со свойством Calculator, экземпляр класса Program может быть передан в этот метод.

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

static void Main()
{
   var p = new Program();
   p.Run ();
}

public void Run()
{
   var catalog = new DirectoryCatalog(Settings.Default.AddInDirectory);
   var container = new CompositionContainer(catalog);
   
   try
   {
     container.ComposeParts(this);
   }
   catch (ChangeRejectedException ex)
   {
      Console.WriteLine(ex.Message);
      return;
   }

С помощью свойства Calculator могут использоваться методы из интерфейса ICalculator. Метод GetOperations() вызывает методы ранее созданного дополнения и возвращает четыре операции. После опроса пользователя, какая операция должна быть вызвана, и запроса значений операндов вызывается метод дополнения Operate().

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

Пройди тесты
Лучший чат для C# программистов