Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Este artigo fornece uma visão geral do Managed Extensibility Framework que foi introduzido no .NET Framework 4.
O que é MEF?
O Managed Extensibility Framework (MEF) é uma biblioteca para criar aplicativos leves e extensíveis. Ele permite que os desenvolvedores de aplicativos descubram e usem extensões sem necessidade de configuração. Ele também permite que os desenvolvedores de extensões encapsulam facilmente o código e evitem dependências frágeis. O MEF não só permite que as extensões sejam reutilizadas dentro de aplicativos, mas também entre aplicativos.
O problema da extensibilidade
Imagine que você é o arquiteto de um aplicativo grande que deve fornecer suporte para extensibilidade. Seu aplicativo deve incluir um número potencialmente grande de componentes menores e é responsável por criá-los e executá-los.
A abordagem mais simples para o problema é incluir os componentes como código-fonte em seu aplicativo e chamá-los diretamente do seu código. Isto tem uma série de inconvenientes óbvios. Mais importante ainda, você não pode adicionar novos componentes sem modificar o código-fonte, uma restrição que pode ser aceitável, por exemplo, em um aplicativo Web, mas não é viável em um aplicativo cliente. Igualmente problemático, você pode não ter acesso ao código-fonte dos componentes, porque eles podem ser desenvolvidos por terceiros e, pela mesma razão, você não pode permitir que eles acessem o seu.
Uma abordagem um pouco mais sofisticada seria fornecer um ponto de extensão ou interface, para permitir a dissociação entre o aplicativo e seus componentes. Nesse modelo, você pode fornecer uma interface que um componente pode implementar e uma API para permitir que ele interaja com seu aplicativo. Isso resolve o problema de exigir acesso ao código-fonte, mas ainda tem suas próprias dificuldades.
Como o aplicativo não tem capacidade para descobrir componentes por conta própria, ele ainda deve ser explicitamente informado sobre quais componentes estão disponíveis e devem ser carregados. Isso geralmente é feito registrando explicitamente os componentes disponíveis em um arquivo de configuração. Isso significa que garantir que os componentes estão corretos se torna um problema de manutenção, especialmente se for o usuário final e não o desenvolvedor que deverá fazer a atualização.
Além disso, os componentes são incapazes de se comunicar uns com os outros, exceto através dos canais rigidamente definidos do próprio aplicativo. Se o arquiteto de aplicativos não previu a necessidade de uma comunicação específica, geralmente é impossível.
Finalmente, os desenvolvedores de componentes devem aceitar uma dependência rígida de qual assembly contém a interface que implementam. Isso dificulta o uso de um componente em mais de um aplicativo e também pode criar problemas quando você cria uma estrutura de teste para componentes.
O que o MEF fornece
Em vez deste registo explícito dos componentes disponíveis, o MEF fornece uma forma de os descobrir implicitamente, através da composição. Um componente MEF, chamado parte, especifica de forma declarativa as suas dependências (conhecidas como importações) e quais recursos (conhecidos como exportações) esse disponibiliza. Quando uma peça é criada, o motor de composição MEF satisfaz as suas importações com o que está disponível de outras peças.
Esta abordagem resolve os problemas discutidos na seção anterior. Como as partes MEF especificam declarativamente seus recursos, elas podem ser detetadas em tempo de execução, o que significa que um aplicativo pode usar partes sem referências codificadas ou arquivos de configuração frágeis. O MEF permite que os aplicativos descubram e examinem peças por seus metadados, sem instanciá-las ou mesmo carregar suas montagens. Como resultado, não há necessidade de especificar cuidadosamente quando e como as extensões devem ser carregadas.
Além das exportações fornecidas, uma parte pode especificar suas importações, que serão preenchidas por outras partes. Isso torna a comunicação entre as partes não apenas possível, mas fácil, e permite uma boa fatoração de código. Por exemplo, serviços comuns a muitos componentes podem ser incluídos em uma peça separada e facilmente modificados ou substituídos.
Como o modelo MEF não requer nenhuma dependência rígida de um assembly de aplicativo específico, ele permite que as extensões sejam reutilizadas de aplicativo para aplicativo. Isso também facilita o desenvolvimento de um chicote de teste, independente da aplicação, para testar componentes de extensão.
Um aplicativo extensível escrito usando MEF declara uma importação que pode ser preenchida por componentes de extensão e também pode declarar exportações para expor serviços de aplicativo a extensões. Cada componente de extensão declara uma exportação e também pode declarar importações. Desta forma, os próprios componentes de extensão são automaticamente extensíveis.
Onde o MEF está disponível
O MEF é uma parte integrante do .NET Framework 4 e está disponível onde quer que o .NET Framework seja utilizado. Você pode usar o MEF em seus aplicativos cliente, quer eles usem Windows Forms, WPF ou qualquer outra tecnologia, ou em aplicativos de servidor que usam ASP.NET.
MEF e MAF
As versões anteriores do .NET Framework introduziram o MAF (Managed Add-in Framework), projetado para permitir que os aplicativos isolem e gerenciem extensões. O foco do MAF é num nível ligeiramente mais elevado do que o MEF, concentrando-se no isolamento de extensões e no carregamento e descarregamento de assemblagens, enquanto o foco do MEF está na descoberta, extensibilidade e portabilidade. As duas estruturas interoperam sem problemas, e um único aplicativo pode tirar proveito de ambas.
SimpleCalculator: Um exemplo de aplicação
A maneira mais simples de ver o que o MEF pode fazer é construir um aplicativo MEF simples. Neste exemplo, você cria uma calculadora muito simples chamada SimpleCalculator. O objetivo do SimpleCalculator é criar um aplicativo de console que aceite comandos aritméticos básicos, na forma "5+3" ou "6-2", e retorne as respostas corretas. Usando o MEF, você poderá adicionar novos operadores sem alterar o código do aplicativo.
Para baixar o código completo para este exemplo, consulte o exemplo SimpleCalculator (Visual Basic).
Observação
O objetivo do SimpleCalculator é demonstrar os conceitos e a sintaxe do MEF, em vez de necessariamente fornecer um cenário realista para o seu uso. Muitos dos aplicativos que mais se beneficiariam do poder do MEF são mais complexos do que o SimpleCalculator. Para obter exemplos mais extensos, consulte o Managed Extensibility Framework no GitHub.
Para iniciar, no Visual Studio, crie um novo projeto de Aplicativo de Console e nomeie-o
SimpleCalculator
.Adicione uma referência à
System.ComponentModel.Composition
assemblagem, onde reside o MEF.Abra Module1.vb ou Program.cs e adicione
Imports
ouusing
diretivas paraSystem.ComponentModel.Composition
eSystem.ComponentModel.Composition.Hosting
. Esses dois namespaces contêm tipos MEF que você precisará para desenvolver um aplicativo extensível.Se você estiver usando o Visual Basic, adicione a
Public
palavra-chave à linha que declara oModule1
módulo.
Contêiner de composição e catálogos
O núcleo do modelo de composição MEF é o recipiente de composição, que contém todas as peças disponíveis e executa a composição. A composição é a correspondência entre as importações e as exportações. O tipo mais comum de recipiente de composição é CompositionContainer, e você usará este para o SimpleCalculator.
Se você estiver usando o Visual Basic, adicione uma classe pública nomeada Program
no Module1.vb.
Adicione a seguinte linha à Program
classe em Module1.vb ou Program.cs:
Dim _container As CompositionContainer
private CompositionContainer _container;
Para descobrir as peças disponíveis, os recipientes de composição fazem uso de um catálogo. Um catálogo é um objeto que disponibiliza peças descobertas de alguma fonte. O MEF fornece catálogos para encontrar peças de um tipo especificado, uma montagem específica ou um diretório. Os desenvolvedores de aplicativos podem criar facilmente novos catálogos para descobrir partes de outras fontes, como um serviço Web.
Adicione o seguinte construtor à Program
classe:
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());
}
}
A chamada para diz ao ComposeParts contêiner de composição para compor um conjunto específico de partes, neste caso a instância atual de Program
. Neste momento, no entanto, nada vai acontecer, uma vez que Program
não tem importações para preencher.
Importações e Exportações com atributos
Primeiro, tens de Program
importar uma calculadora. Isso permite separar as preocupações da interface do utilizador, como a entrada e saída do console que serão direcionadas para Program
, da lógica da calculadora.
Adicione o seguinte código à Program
classe:
<Import(GetType(ICalculator))>
Public Property calculator As ICalculator
[Import(typeof(ICalculator))]
public ICalculator calculator;
Observe que a calculator
declaração do objeto não é incomum, mas que ele é decorado com o ImportAttribute atributo. Este atributo declara que algo é uma importação; ou seja, será preenchido pelo motor de composição quando o objeto for composto.
Cada importação tem um contrato, que determina com que exportações será correspondida. O contrato pode ser uma cadeia de caracteres explicitamente especificada, ou pode ser gerado automaticamente pelo MEF a partir de um determinado tipo, neste caso a interface ICalculator
. Qualquer exportação declarada que tenha um contrato correspondente cumprirá esta importação. Observe que, embora o tipo do calculator
objeto seja de fato ICalculator
, isso não é necessário. O contrato é independente do tipo do objeto de importação. (Neste caso, você pode deixar de fora o typeof(ICalculator)
. O MEF assumirá automaticamente que o contrato será baseado no tipo de importação, a menos que você o especifique explicitamente.)
Adicione esta interface muito simples ao módulo ou SimpleCalculator
namespace:
Public Interface ICalculator
Function Calculate(input As String) As String
End Interface
public interface ICalculator
{
string Calculate(string input);
}
Agora que você definiu ICalculator
, você precisa de uma classe que o implemente. Adicione a seguinte classe ao módulo ou ao espaço de nomes SimpleCalculator
.
<Export(GetType(ICalculator))>
Public Class MySimpleCalculator
Implements ICalculator
End Class
[Export(typeof(ICalculator))]
class MySimpleCalculator : ICalculator
{
}
Aqui está a exportação que corresponderá à importação em Program
. Para que a exportação corresponda à importação, a exportação deve ter o mesmo contrato. Exportar sob um contrato baseado em typeof(MySimpleCalculator)
produziria uma incompatibilidade, e a importação não seria realizada; o contrato precisa corresponder exatamente.
Uma vez que o recipiente de composição será preenchido com todas as peças disponíveis nesta montagem, a MySimpleCalculator
peça estará disponível. Quando o construtor de Program
realiza a composição no objeto Program
, a sua importação será completada por um objeto MySimpleCalculator
, que será criado para essa finalidade.
A camada de interface do usuário (Program
) não precisa saber mais nada. Portanto, você pode preencher o restante da lógica da interface do usuário no Main
método.
Adicione o seguinte código ao método 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));
}
}
Esse código simplesmente lê uma linha de entrada e chama a função Calculate
de ICalculator
no resultado, que ele grava de volta no console. Esse é todo o código que você precisa no Program
. Todo o resto do trabalho será realizado nas partes.
Atributos Imports e ImportMany
Para que o SimpleCalculator seja extensível, ele precisa importar uma lista de operações. Um atributo comum ImportAttribute é preenchido por um único ExportAttribute. Se houver mais de um disponível, o mecanismo de composição produz um erro. Para criar uma importação que possa ser preenchida por qualquer número de exportações, você pode usar o ImportManyAttribute atributo.
Adicione a seguinte propriedade operations à MySimpleCalculator
classe:
<ImportMany()>
Public Property operations As IEnumerable(Of Lazy(Of IOperation, IOperationData))
[ImportMany]
IEnumerable<Lazy<IOperation, IOperationData>> operations;
Lazy<T,TMetadata> é um tipo fornecido pelo MEF para conter referências indiretas às exportações. Aqui, além do objeto exportado em si, você também obtém metadados de exportação ou informações que descrevem o objeto exportado. Cada Lazy<T,TMetadata> um contém um IOperation
objeto, representando uma operação real, e um IOperationData
objeto, representando seus metadados.
Adicione as seguintes interfaces simples ao módulo ou ao namespace 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; }
}
Nesse caso, os metadados de cada operação são o símbolo que representa essa operação, como +, -, * e assim por diante. Para disponibilizar a operação de adição, adicione a seguinte classe ao módulo ou SimpleCalculator
namespace:
<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;
}
}
O ExportAttribute atributo funciona como antes. O ExportMetadataAttribute atributo anexa metadados, na forma de um par nome-valor, a essa exportação. Enquanto a classe Add
implementa IOperation
, uma classe que implementa IOperationData
não é explicitamente definida. Em vez disso, uma classe é implicitamente criada pelo MEF com propriedades baseadas nos nomes dos metadados fornecidos. (Esta é uma das várias maneiras de acessar metadados no MEF.)
A composição no MEF é recursiva. Você compôs explicitamente o Program
objeto, que importou um ICalculator
que acabou sendo do tipo MySimpleCalculator
.
MySimpleCalculator
, por sua vez, importa uma coleção de IOperation
objetos, e essa importação será preenchida quando MySimpleCalculator
for criada, ao mesmo tempo que as importações de Program
. Se a Add
classe declarasse uma nova importação, isso também teria de ser preenchido, e assim por diante. Qualquer importação não preenchida resulta em um erro de composição. (É possível, no entanto, declarar as importações como opcionais ou atribuir-lhes valores por defeito.)
Lógica da calculadora
Com essas partes no lugar, tudo o que resta é a própria lógica da calculadora. Adicione o seguinte código na MySimpleCalculator
classe para implementar o Calculate
método:
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!";
}
As etapas iniciais dividem a cadeia de caracteres de entrada nos operandos esquerdo e direito e num carácter de operador. No loop foreach
, cada membro da coleção operations
é examinado. Esses objetos são do tipo Lazy<T,TMetadata>, e seus valores de metadados e objeto exportado podem ser acessados com a Metadata propriedade e a Value propriedade, respectivamente. Nesse caso, se a Symbol
propriedade do IOperationData
objeto for descoberta como uma correspondência, a calculadora chamará o Operate
método do IOperation
objeto e retornará o resultado.
Para completar a calculadora, você também precisa de um método auxiliar que retorna a posição do primeiro caractere sem dígitos em uma cadeia de caracteres. Adicione o seguinte método auxiliar à MySimpleCalculator
classe:
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;
}
Agora você deve ser capaz de compilar e executar o projeto. No Visual Basic, certifique-se de que adicionou a Public
palavra-chave ao Module1
. Na janela do console, digite uma operação de adição, como "5+3", e a calculadora retorna os resultados. Qualquer outro operador resulta na mensagem "Operação não encontrada!".
Estenda o SimpleCalculator usando uma nova classe
Agora que a calculadora funciona, adicionar uma nova operação é fácil. Adicione a seguinte classe ao módulo ou ao espaço de nomes 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;
}
}
Compile e execute o projeto. Digite uma operação de subtração, como "5-3". A calculadora agora suporta subtração, bem como adição.
Estenda o SimpleCalculator usando um novo assembly
Adicionar classes ao código-fonte é simples o suficiente, mas o MEF fornece a capacidade de procurar partes fora da própria fonte de um aplicativo. Para demonstrar isso, será necessário modificar o SimpleCalculator para pesquisar partes num diretório, assim como no seu próprio conjunto, adicionando um DirectoryCatalog.
Adicione um novo diretório nomeado Extensions
para o projeto SimpleCalculator. Certifique-se de adicioná-lo no nível do projeto, e não no nível da solução. Em seguida, adicione um novo projeto de Biblioteca de Classes à solução, chamado ExtendedOperations
. O novo projeto será reunido em um conjunto separado.
Abra o Project Properties Designer para o projeto ExtendedOperations e clique na guia Compilar ou Construir . Altere o caminho de saída Build ou o caminho de saída para apontar para o diretório Extensions no diretório do projeto SimpleCalculator (.. \SimpleCalculator\Extensões\).
Em Module1.vb ou Program.cs, adicione a seguinte linha ao Program
construtor:
catalog.Catalogs.Add(
New DirectoryCatalog(
"C:\SimpleCalculator\SimpleCalculator\Extensions"))
catalog.Catalogs.Add(
new DirectoryCatalog(
"C:\\SimpleCalculator\\SimpleCalculator\\Extensions"));
Substitua o caminho de exemplo pelo caminho para o diretório das suas Extensões. (Este caminho absoluto é apenas para fins de depuração. Em um aplicativo de produção, você usaria um caminho relativo.) O DirectoryCatalog agora adicionará quaisquer partes encontradas em quaisquer montagens no diretório Extensões ao contêiner de composição.
ExtendedOperations
No projeto, adicione referências a SimpleCalculator
e System.ComponentModel.Composition
. No arquivo de classe ExtendedOperations
, adicione uma diretiva Imports
ou using
para System.ComponentModel.Composition
. No Visual Basic, adicione também uma Imports
instrução para SimpleCalculator
. Em seguida, adicione a seguinte classe ao ExtendedOperations
arquivo de classe:
<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;
}
}
Observe que, para que o contrato corresponda, o ExportAttribute atributo deve ter o mesmo tipo que o ImportAttribute.
Compile e execute o projeto. Teste o novo operador Mod (%).
Conclusão
Este tópico abordou os conceitos básicos do MEF.
Peças, catálogos e o recipiente de composição
As peças e o recipiente de composição são os blocos de construção básicos de um aplicativo MEF. Uma parte é qualquer objeto que importa ou exporta um valor, até si mesmo. Um catálogo fornece uma coleção de peças de uma fonte específica. O contêiner de composição usa as peças fornecidas por um catálogo para realizar a composição, a vinculação das importações às exportações.
Importações e exportações
As importações e as exportações são a forma pela qual os componentes comunicam. Com uma importação, o componente especifica a necessidade de um determinado valor ou objeto e, com uma exportação, especifica a disponibilidade de um valor. Cada importação é associada a uma lista de exportações através do seu contrato.
Próximos passos
Para baixar o código completo para este exemplo, consulte o exemplo SimpleCalculator (Visual Basic).
Para obter mais informações e exemplos de código, consulte Managed Extensibility Framework. Para obter uma lista dos tipos de MEF, consulte o System.ComponentModel.Composition namespace.