Заметка
Доступ к этой странице требует авторизации. Вы можете попробовать войти в систему или изменить каталог.
Доступ к этой странице требует авторизации. Вы можете попробовать сменить директорию.
В этой статье представлен обзор управляемой платформы расширяемости, которая была представлена в .NET Framework 4.
Что такое MEF?
Платформа управляемого расширения (MEF) — это библиотека для создания упрощенных и расширяемых приложений. Это позволяет разработчикам приложений обнаруживать и использовать расширения без необходимости настройки. Он также позволяет разработчикам расширений легко инкапсулировать код и избегать хрупких жестких зависимостей. MEF не только позволяет повторно использовать расширения в приложениях, но и между приложениями.
Проблема расширяемости
Представьте, что вы являетесь архитектором большого приложения, которое должно обеспечить поддержку расширяемости. Приложение может включать потенциально большое количество небольших компонентов и отвечает за их создание и запуск.
Самый простой подход к проблеме заключается в том, чтобы включить компоненты в качестве исходного кода в приложение и вызвать их непосредственно из кода. Это имеет ряд очевидных недостатков. Самое главное, нельзя добавлять новые компоненты, не изменяя исходный код, ограничение, которое может быть приемлемым, например, веб-приложением, но не работает в клиентском приложении. В равной степени проблематично, у вас может не быть доступа к исходному коду для компонентов, так как они могут быть разработаны сторонними лицами, и по той же причине вы не можете разрешить им доступ к вашим компонентам.
Немного более сложный подход заключается в предоставлении точки расширения или интерфейса, чтобы разрешить разделение между приложением и его компонентами. В этой модели можно предоставить интерфейс, который может реализовать компонент, и API для его взаимодействия с приложением. Это решает проблему, требующую доступа к исходному коду, но она по-прежнему имеет свои собственные трудности.
Так как приложение не имеет возможности обнаруживать компоненты самостоятельно, должно однозначно указываться, какие компоненты доступны и должны быть загружены. Обычно это достигается путем явной регистрации доступных компонентов в файле конфигурации. Это означает, что обеспечение правильности компонентов становится проблемой обслуживания, особенно если это конечный пользователь, а не разработчик, который, как ожидается, будет выполнять обновление.
Кроме того, компоненты не могут взаимодействовать друг с другом, за исключением жестко определенных каналов самого приложения. Если архитектор приложения не ожидал необходимости определенного взаимодействия, обычно это невозможно.
Наконец, разработчики компонентов должны принимать жесткую зависимость от того, какая сборка содержит интерфейс, который они реализуют. Это затрудняет использование компонента в нескольких приложениях, а также может создавать проблемы при создании тестовой платформы для компонентов.
Что предоставляет MEF
Вместо явной регистрации доступных компонентов MEF предоставляет способ их обнаружения неявно с помощью композиции. Компонент MEF, называемый частью, декларативно указывает как его зависимости (известные как импорт), так и какие возможности (известные как экспорты) он предоставляет. При создании части подсистема композиции MEF удовлетворяет его импорту тем, что доступно из других частей.
Этот подход решает проблемы, описанные в предыдущем разделе. Так как компоненты MEF декларативно указывают их возможности, они обнаруживаются во время выполнения, что означает, что приложение может использовать части без жестко закодированных ссылок или хрупких файлов конфигурации. MEF позволяет приложениям обнаруживать и проверять части по их метаданным, не создавая их экземпляры или даже загружая их сборки. В результате нет необходимости тщательно указывать, когда и как следует загружать расширения.
Помимо предоставленных экспортов, часть может указать свои импорты, которые будут заполнены другими частями. Это делает обмен данными между частями не только возможными, но и простыми и позволяет хорошо учитывать факторы кода. Например, службы, общие для многих компонентов, могут быть вынесены в отдельную часть и легко изменены или заменены.
Так как модель MEF не требует жесткой зависимости от конкретной сборки приложения, она позволяет повторно использовать расширения из приложения в приложение. Это также упрощает разработку тестовой инфраструктуры, независимой от приложения, для тестирования расширяемых компонентов.
Расширяемое приложение, написанное с помощью MEF, объявляет импорт, который может быть заполнен компонентами расширения, а также может объявлять экспорт для предоставления служб приложений расширениям. Каждый компонент расширения объявляет экспорт, а также может объявлять импорт. Таким образом, сами компоненты расширения автоматически расширяемы.
Где доступен MEF
MEF является неотъемлемой частью .NET Framework 4 и доступна везде, где используется .NET Framework. MeF можно использовать в клиентских приложениях, независимо от того, используют ли они Windows Forms, WPF или любую другую технологию, или в серверных приложениях, использующих ASP.NET.
MEF и MAF
В предыдущих версиях .NET Framework появилась управляемая надстройка (MAF), предназначенная для изоляции приложений и управления расширениями. Фокус MAF немного выше, чем MEF, концентрируясь на изоляции расширений и загрузке сборок и выгрузке, в то время как основное внимание MEF уделяется обнаружению, расширяемости и переносимости. Две платформы взаимодействуют плавно, и одно приложение может воспользоваться преимуществами обоих.
SimpleCalculator: пример приложения
Самый простой способ увидеть, что может сделать MEF, заключается в создании простого приложения MEF. В этом примере вы создадите очень простой калькулятор с именем SimpleCalculator. Цель SimpleCalculator — создать консольное приложение, которое принимает основные арифметические команды в форме "5+3" или "6-2" и возвращает правильные ответы. С помощью MEF вы сможете добавлять новые операторы, не изменяя код приложения.
Чтобы скачать полный код для этого примера, см. пример SimpleCalculator (Visual Basic).
Замечание
Цель SimpleCalculator заключается в демонстрации концепций и синтаксиса MEF, а не для обязательного предоставления реалистичного сценария его использования. Многие из приложений, которые выиграют больше всего от возможностей MEF, являются более сложными, чем SimpleCalculator. Более подробные примеры см. в Managed Extensibility Framework на GitHub.
Чтобы начать, в Visual Studio создайте проект консольного приложения и назовите его
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, добавьте открытый класс с именем Program в Module1.vb.
Добавьте следующую строку в Program класс в Module1.vb или Program.cs:
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 CompositionException
Console.WriteLine(ex.ToString)
End Try
End Sub
private Program()
{
try
{
// 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);
_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(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, который будет создан специально для этой цели.
Уровень пользовательского интерфейса (Program) не должен знать ничего другого. Поэтому можно заполнить остальную часть логики пользовательского интерфейса в методе Main .
Добавьте следующий код в метод Main:
Sub Main()
' Composition is performed in the constructor.
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)
{
// Composition is performed in the constructor.
var p = new Program();
Console.WriteLine("Enter Command:");
while (true)
{
string s = Console.ReadLine();
Console.WriteLine(p.calculator.Calculate(s));
}
}
Этот код просто считывает строку входных данных и вызывает функцию Calculate на результате ICalculator, после чего выводит его обратно в консоль. Вот весь код, который вам нужен в Program. Все остальные действия будут выполняться в частях.
Атрибуты Imports и ImportMany
Чтобы simpleCalculator был расширяемым, необходимо импортировать список операций. Обычный ImportAttribute атрибут заполняется одним и только одним ExportAttribute. Если доступно более одного, подсистема композиции выдает ошибку. Чтобы создать импорт, который можно заполнить любым количеством экспортов, можно использовать ImportManyAttribute атрибут.
Добавьте следующее свойство operations в класс 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(left As Integer, 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(left As Integer, 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(input As String) As String Implements ICalculator.Calculate
Dim left, right As Integer
Dim operation As Char
' Finds the operator.
Dim fn = FindFirstNonDigit(input)
If fn < 0 Then
Return "Could not parse command."
End If
operation = input(fn)
Try
' Separate out the operands.
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;
// Finds the operator.
int fn = FindFirstNonDigit(input);
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(s As String) As Integer
For i = 0 To s.Length - 1
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(left As Integer, 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 части, найденные в любом каталоге расширений, будут добавлены в контейнер композиции.
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(left As Integer, 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 (Visual Basic).
Дополнительные сведения и примеры кода см. в разделе Managed Extensibility Framework. Список типов MEF см. в System.ComponentModel.Composition пространстве имен.