Общие сведения о платформе Managed Extensibility Framework

В этом разделе представлен обзор платформы Managed Extensibility Framework, предложенной в .NET Framework 4.

В этом разделе содержатся следующие подразделы.

  • Общие сведения о MEF
  • Проблема расширяемости
  • Общие сведения о возможностях MEF
  • Область применения MEF
  • MEF и MAF
  • Пример приложения SimpleCalculator
  • Контейнер композиции и каталоги
  • Импортируемые и экспортируемые компоненты с атрибутами
  • Дополнительные импортируемые компоненты и атрибут ImportMany
  • Логика калькулятора
  • Расширение программы SimpleCalculator с помощью нового класса
  • Расширение SimpleCalculator с помощью новой сборки
  • Заключение
  • Дополнительные сведения

Общие сведения о MEF

Платформа Managed Extensibility Framework, или MEF, – это библиотека для создания простых расширяемых приложений. Она позволяет разработчикам приложений находить и использовать расширения без каких-либо настроек. Кроме того, дает разработчикам расширений возможность легко инкапсулировать код и избежать использования ненадежных жестких зависимостей. MEF не только позволяет использовать зависимости повторно, но и дает возможности применять их в различных приложениях.

Проблема расширяемости

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

Простейший подход к решению этой проблемы заключается в добавлении этих компонентов в приложение в виде исходного кода и вызове их непосредственно из кода. У этого подхода есть множество очевидных недостатков. Самый основной из которых заключается в невозможности добавления новых компонентов без изменения исходного кода. Такое ограничение приемлемо, например, в веб-приложении, однако неприменимо для клиентского приложения. Таким же проблематичным может быть и отсутствие доступа к исходному коду компонентов, поскольку они могут разрабатываться сторонними производителями. По этой же причине разработчик не может предоставить им доступ к своему коду.

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

Из-за отсутствия у приложения каких-либо собственных средств обнаружения компонентов нужно явным образом объявлять о доступных компонентах и компонентах, которые необходимо загрузить. Обычно это сопровождается явной регистрацией доступных компонентов в файле конфигурации. А это означает, что обеспечение правильности компонентов превращается в проблему обслуживания, особенно, если обновлениями должен будет заниматься конечный пользователь, а не разработчик.

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

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

Общие сведения о возможностях MEF

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

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

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

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

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

Область применения MEF

Платформа MEF входит в состав .NET Framework 4 и ее можно использовать там же, где и платформу .NET. Платформу MEF можно использовать в клиентских приложениях не зависимо от применяемых в них технологий: Windows Forms, WPF или любая другая технология, а также серверных приложениях, использующих ASP.NET.

MEF и MAF

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

Пример приложения SimpleCalculator

Чтобы узнать возможности MEF, проще всего создать простое приложение MEF. В этом примере показано создание очень простого приложения-калькулятора с именем SimpleCalculator. Задача SimpleCalculator заключается в создании консольного приложения, принимающего основные арифметические команды в форме "5+3" или "6-2" и возвращающего правильные ответы. Использование MEF позволит добавлять новые операторы без изменения кода приложения.

Полный код для этого примера можно загрузить на веб-странице SimpleCalculator sample.

ПримечаниеПримечание

Пример SimpleCalculator призван продемонстрировать основные понятия и синтаксис MEF, а не предоставить абсолютно реалистичный сценарий использования этой платформы.Многие приложения, в которых можно в полной мере воспользоваться преимуществами MEF, гораздо сложнее приложения SimpleCalculator.Более полные примеры см. на странице Managed Extensibility Framework в сообществе Codeplex.

Для начала создайте в Visual Studio 2010 новый проект консольного приложения с именем SimpleCalculator. Добавьте ссылку на сборку System.ComponentModel.Composition, в которой размещается MEF. Откройте файл Module1.vb или Program.cs и добавьте операторы Imports или using для пространств имен System.ComponentModel.Composition и System.ComponentModel.Composition.Hosting. Оба этих пространства имен содержат типы MEF, необходимые для разработки расширяемого приложения. В Visual Basic добавьте ключевое слово Public в строку, объявляющую модуль Module1.

Контейнер композиции и каталоги

Основным элементом модели композиции MEF является контейнер композиции, в котором содержатся все доступные части и выполняется сама композиция. (Иными словами, сопоставление импортируемых компонентов с экспортируемыми.) Наиболее общим типом контейнера композиции является тип CompositionContainer, который будет использоваться для SimpleCalculator.

В Visual Basic добавьте в Module1.vb открытый класс с именем Program. Затем в файле Module1.vb или Program.cs добавьте в класс Program следующую строку.

Dim _container As CompositionContainer
private CompositionContainer _container;

Для обнаружения доступных частей в контейнерах композиции используется каталог. Каталог – это объект, который делает доступными части, обнаруженные в определенном источнике. В платформе MEF предусмотрены каталоги для обнаружения частей в предоставляемом типе, сборке или каталоге. Разработчики приложений могут легко создать новые каталоги для обнаружения частей в других источниках, например веб-службе.

Добавьте следующий конструктор в класс Program.

Public Sub New()
    'An aggregate catalog that combines multiple catalogs
     Dim catalog = New AggregateCatalog()

    'Adds all the parts found in the same assembly as the Program class
    catalog.Catalogs.Add(New AssemblyCatalog(GetType(Program).Assembly))

    'Create the CompositionContainer with the parts in the catalog
    _container = New CompositionContainer(catalog)

    'Fill the imports of this object
    Try
        _container.ComposeParts(Me)
    Catch ex As Exception
        Console.WriteLine(ex.ToString)
    End Try
End Sub
private Program()
{
    //An aggregate catalog that combines multiple catalogs
    var catalog = new AggregateCatalog();
    //Adds all the parts found in the same assembly as the Program class
    catalog.Catalogs.Add(new AssemblyCatalog(typeof(Program).Assembly));

    //Create the CompositionContainer with the parts in the catalog
    _container = new CompositionContainer(catalog);

    //Fill the imports of this object
    try
    {
        this._container.ComposeParts(this);
    }
    catch (CompositionException compositionException)
    {
        Console.WriteLine(compositionException.ToString());
   }
}

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

Импортируемые и экспортируемые компоненты с атрибутами

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

Добавьте в класс Program следующий код:

<Import(GetType(ICalculator))>
Public Property calculator As ICalculator
[Import(typeof(ICalculator))]
public ICalculator calculator;

Обратите внимание на необычность объявления объекта calculator: в нем используется атрибут ImportAttribute в качестве обозначения. Этот атрибут объявляет, что тот или иной компонент является импортируемым; то есть, он будет поставлен обработчиком композиции в процессе компоновки.

У каждого импортируемого компонента есть контракт, в котором определены соответствующие ему экспортируемые компоненты. Контрактом может быть явным образом заданная строка. Кроме того, он может создаваться платформой MEF автоматически на основе заданного типа, в данном случае интерфейса ICalculator. Любой экспортируемый компонент, объявленный с помощью контракта сопоставления, будет подставляться в качестве этого импортируемого компонента. Обратите внимание, что хотя типом объекта calculator по сути является ICalculator, определять этот тип необязательно. Контракт не зависит от типа импортируемого объекта. (В данном случае typeof(ICalculator) можно исключить. В платформе MEF автоматически предполагается, что если тип не задан явно, контракт создается на основе типа импортируемого компонента.)

Добавьте этот простейший интерфейс в модуль или пространство имен SimpleCalculator.

Public Interface ICalculator
    Function Calculate(ByVal input As String) As String
End Interface
public interface ICalculator
{
    String Calculate(String input);
}

Теперь, после определения интерфейса ICalculator, нужен класс, который будет его реализовывать. Добавьте следующий класс в модуль или пространство имен SimpleCalculator.

<Export(GetType(ICalculator))>
Public Class MySimpleCalculator
   Implements ICalculator

End Class
[Export(typeof(ICalculator))]
class MySimpleCalculator : ICalculator
{

}

Здесь используется экспортируемый компонент, соответствующий импортируемому компоненту в Program. Чтобы экспортируемый компонент соответствовал импортируемому компоненту, оба эти компонента должны содержать одинаковый контракт. Экспорт по контракту на основе typeof(MySimpleCalculator) приведет к несоответствию, и импорт завершится ошибкой; контракты должны в точности совпадать.

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

На уровне пользовательского интерфейса (Program) никакие другие сведения не нужны. Поэтому в метод Main можно подставить всю остальную логику пользовательского интерфейса.

Добавьте следующий код в метод Main.

Sub Main()
    Dim p As New Program()
    Dim s As String
    Console.WriteLine("Enter Command:")
    While (True)
        s = Console.ReadLine()
        Console.WriteLine(p.calculator.Calculate(s))
    End While
End Sub
static void Main(string[] args)
{
    Program p = new Program(); //Composition is performed in the constructor
    String s;
    Console.WriteLine("Enter Command:");
    while (true)
    {
        s = Console.ReadLine();
        Console.WriteLine(p.calculator.Calculate(s));
    }
}

Этот код просто считывает строку входных данных и вызывает функцию Calculate интерфейса ICalculator для полученного результата, который он записывает в консоль. Это весь код, который нужен для класса Program. Все остальные действия будут выполнять части.

Дополнительные импортируемые компоненты и атрибут ImportMany

Чтобы приложение SimpleCalculator было расширяемым, оно должно импортировать список операций. В качестве значения обычного атрибута ImportAttribute подставляет один и только один объект ExportAttribute. Если же их несколько, обработчик композиции выдает ошибку. Чтобы создать импортируемый компонент, в качестве которого можно подставить произвольное число экспортируемых компонентов, можно воспользоваться атрибутом ImportManyAttribute.

Добавьте в класс MySimpleCalculator следующее свойство операций.

<ImportMany()>
Public Property operations As IEnumerable(Of Lazy(Of IOperation, IOperationData))
[ImportMany]
IEnumerable<Lazy<IOperation, IOperationData>> operations;

Lazy<T, TMetadata> — это тип, предоставляемый MEF для хранения косвенных ссылок на экспортируемые компоненты. Здесь, помимо самих экспортируемых объектов, предоставляются также метаданные экспорта или сведения, описывающие экспортируемый объект. Каждый тип Lazy<T, TMetadata> содержит объект IOperation, представляющий фактическую операцию, и объект IOperationData, представляющий ее метаданные.

Добавьте следующие простые интерфейсы в модуль или пространство имен SimpleCalculator.

Public Interface IOperation
    Function Operate(ByVal left As Integer, ByVal right As Integer) As Integer
End Interface

Public Interface IOperationData
    ReadOnly Property Symbol As Char
End Interface
public interface IOperation
{
     int Operate(int left, int right);
}

public interface IOperationData
{
    Char Symbol { get; }
}

В этом случае метаданными каждой операции является символ, представляющий эту операцию, например +, -, * и т. д. Чтобы сделать доступной операцию сложения, добавьте следующий класс в модуль или пространство имен SimpleCalculator.

<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "+"c)>
Public Class Add
    Implements IOperation

    Public Function Operate(ByVal left As Integer, ByVal right As Integer) As Integer Implements IOperation.Operate
        Return left + right
    End Function
End Class
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '+')]
class Add: IOperation
{
    public int Operate(int left, int right)
    {
        return left + right;
    }
}

Принцип действия атрибута ExportAttribute остался неизменным. Атрибут ExportMetadataAttribute присоединяет к этому экспортируемому компоненту метаданные в форме пары "имя-значение". Хотя класс Add реализует IOperation, класс, реализующий интерфейс IOperationData, явно не определяется. Вместо этого он создается неявно платформой MEF со свойствами на основе имен предоставленных метаданных. (Это один из нескольких способов доступа к метаданным в MEF.)

Композиция в платформе MEF рекурсивная. Разработчик явным образом выполняет композицию объекта Program, импортирующего интерфейс ICalculator, который оказывается принадлежащим к типу MySimpleCalculator. MySimpleCalculator, в свою очередь, импортирует коллекцию объектов IOperation, и этот импортируемый компонент заполняется при создании MySimpleCalculator одновременно с импортируемыми компонентами Program. Если бы класс Add объявил дополнительный импортируемый компонент, нужно было бы заполнить и его и так далее. Любой не заполненный импортируемый компонент приводит к ошибке композиции. (Однако при этом можно объявить импортируемые компоненты как необязательные или присвоить им значения по умолчанию.)

Логика калькулятора

При наличии этих частей остается только сама логика калькулятора. Добавьте следующий код в класс MySimpleCalculator, чтобы реализовать метод Calculate.

Public Function Calculate(ByVal input As String) As String Implements ICalculator.Calculate
    Dim left, right As Integer
    Dim operation As Char
    Dim fn = FindFirstNonDigit(input) 'Finds the operator
    If fn < 0 Then
        Return "Could not parse command."
    End If
    operation = input(fn)
    Try
        left = Integer.Parse(input.Substring(0, fn))
        right = Integer.Parse(input.Substring(fn + 1))
    Catch ex As Exception
        Return "Could not parse command."
    End Try
    For Each i As Lazy(Of IOperation, IOperationData) In operations
        If i.Metadata.symbol = operation Then
            Return i.Value.Operate(left, right).ToString()
        End If
    Next
    Return "Operation not found!"
End Function
public String Calculate(String input)
{
    int left;
    int right;
    Char operation;
    int fn = FindFirstNonDigit(input); //finds the operator
    if (fn < 0) return "Could not parse command.";

    try
    {
        //separate out the operands
        left = int.Parse(input.Substring(0, fn));
        right = int.Parse(input.Substring(fn + 1));
    }
    catch 
    {
        return "Could not parse command.";
    }

    operation = input[fn];

    foreach (Lazy<IOperation, IOperationData> i in operations)
    {
        if (i.Metadata.Symbol.Equals(operation)) return i.Value.Operate(left, right).ToString();
    }
    return "Operation Not Found!";
}

На начальных этапах выполняется синтаксический анализ строки ввода для левого и правого операнда, а также символа оператора. В цикле foreach анализируется каждый элемент коллекции operations. Эти объекты имеют тип Lazy<T, TMetadata>, а доступ к значениям их метаданных и экспортированному объекту можно получить с помощью свойств Metadata и Value соответственно. В этом случае, если для свойства Symbol объекта IOperationData находится соответствие, калькулятор вызывает метод Operate объекта IOperation и возвращает результат.

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

Private Function FindFirstNonDigit(ByVal s As String) As Integer
    For i = 0 To s.Length
        If (Not (Char.IsDigit(s(i)))) Then Return i
    Next
    Return -1
End Function
private int FindFirstNonDigit(String s)
{
    for (int i = 0; i < s.Length; i++)
    {
        if (!(Char.IsDigit(s[i]))) return i;
    }
    return -1;
}

Теперь можно скомпилировать и запустить проект. В Visual Basic убедитесь, что ключевое слово Public добавлено в Module1. Введите в консоли операцию сложения, например "5+3", и калькулятор вернет результат. Любой другой оператор привет к отображению сообщения "Операция не найдена!"

Расширение программы SimpleCalculator с помощью нового класса

Теперь, когда калькулятор работает, в него можно легко добавить новую операцию. Добавьте следующий класс в модуль или пространство имен SimpleCalculator.

<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "-"c)>
Public Class Subtract
    Implements IOperation

    Public Function Operate(ByVal left As Integer, ByVal right As Integer) As Integer Implements IOperation.Operate
        Return left - right
    End Function
End Class
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '-')]
class Subtract : IOperation
{
    public int Operate(int left, int right)
    {
        return left - right;
    }
}

Скомпилируйте и запустите проект. Выполните операцию вычитания, например "5-3". Теперь калькулятор выполняет операции вычитания наряду со сложением.

Расширение SimpleCalculator с помощью новой сборки

Процедура добавления классов в исходный код довольно проста, однако платформа MEF позволяет использовать не только собственные источники частей приложения, но и внешние. Чтобы продемонстрировать это, нужно изменить приложение SimpleCalculator так, чтобы оно могло выполнять поиск частей не только в собственной сборке, но и в каталоге. Для этого нужно добавить DirectoryCatalog.

Добавьте новый каталог с именем Extensions в проект SimpleCalculator. Помните, что его нужно добавить на уровне проекта, а не на уровне решения. Затем добавьте в решение новый проект библиотеки классов с именем ExtendedOperations. Новый проект будет компилироваться в отдельную сборку.

Откройте конструктор свойств проекта для проекта ExtendedOperations и перейдите на вкладку Компиляция или Создание. Измените Выходной путь построения или Выходной путь, чтобы он вел к каталогу Extensions в каталоге проекта SimpleCalculator (.. \SimpleCalculator\Extensions\).

В файле Module1.vb или Program.cs добавьте в конструктор следующую строку Program.

catalog.Catalogs.Add(New DirectoryCatalog("C:\SimpleCalculator\SimpleCalculator\Extensions"))
catalog.Catalogs.Add(new DirectoryCatalog("C:\\SimpleCalculator\\SimpleCalculator\\Extensions"));

Замените пример пути путем к каталогу Extensions. (Этот абсолютный путь используется только для отладки. В рабочем приложении используется относительный путь.) Теперь DirectoryCatalog добавит все части, найденные в сборках в каталоге Extensions, в контейнер композиции.

В проекте ExtendedOperations добавьте ссылки на SimpleCalculator и System.ComponentModel.Composition. В файле класса ExtendedOperations добавьте оператор Imports или using для System.ComponentModel.Composition. В Visual Basic также добавьте оператор Imports для SimpleCalculator. Затем добавьте следующий класс в файл класса ExtendedOperations.

<Export(GetType(SimpleCalculator.IOperation))>
<ExportMetadata("Symbol", "%"c)>
Public Class Modulo
    Implements IOperation

    Public Function Operate(ByVal left As Integer, ByVal right As Integer) As Integer Implements IOperation.Operate
        Return left Mod right
    End Function
End Class
[Export(typeof(SimpleCalculator.IOperation))]
[ExportMetadata("Symbol", '%')]
public class Mod : SimpleCalculator.IOperation
{
    public int Operate(int left, int right)
    {
        return left % right;
    }
}

Обратите внимание, что для совпадения контрактов атрибут ExportAttribute должен принадлежать к тому же типу, что и атрибут ImportAttribute.

Скомпилируйте и запустите проект. Проверьте работу нового оператора Mod (%).

Заключение

В этом разделе были рассмотрены основные концепции платформы MEF.

  • Части каталоги и контейнер композиции

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

  • Импортируемые и экспортируемые компоненты

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

Дополнительные сведения

Полный код для этого примера можно загрузить на веб-странице SimpleCalculator sample.

Дополнительные сведения и примеры кода см. на странице Managed Extensibility Framework. Список типов MEF см. в описании пространства имен System.ComponentModel.Composition.

Журнал изменений

Дата

Журнал

Причина

Июль 2010

Обновлены некоторые действия. Добавлены отсутствующие действия для VB. Добавлена ссылка на загрузку примера.

Обратная связь от клиента.