15 janeiro, 2010

Criando aplicações extensíveis em Silverlight 3 com MEF

Atualmente estou trabalhando em um projeto, em Silverlight 3, cuja principal característica é que será uma aplicação extensível. Isso significa que depois que a aplicação for publicada deverá ser possível escrever e instalar extensões facilmente, sem que seja necessário mexer no código da aplicação principal. Existem várias formas de atingir esse objetivo e vários padrões de desenvolvimento que podem ser utilizados para ajudar a realizar essa tarefa com mais facilidade. Neste post vou demonstrar como fazer isso utilizando o MEF – Managed Extensibility Framework, que ao meu ver é a forma mais eficiente e segura de tornar sua aplicação extensível.

Mas o que é exatamente o MEF?

MEF é um framework open source que está sendo desenvolvido já há algum tempo pela Microsoft, cujo objetivo é tornar a tarefa de criar aplicações extensíveis o mais simples possível. O código fonte pode assim com a última versão do MEF podem ser encontrados no Codeplex em http://mef.codeplex.com. Os códigos fonte e binários do MEF presentes no Codeplex são para o .NET Framework 3.5 e Silverlight 3. O MEF já faz parte do SDK do Silverlight 4, então não é necessário fazer nenhuma instalação extra (além do Silverlight 4 Tools para Visual Studio ou pelo menos o próprio Silverlight 4 SDK).

Por que utilizar o MEF?

Essa aplicação na qual estou trabalhando começou a ser concebida aproximadamente em Julho de 2009. Na época eu já havia ouvido falar do MEF mas ainda não fazia idéia do que realmente era e como ele poderia me ajudar. Além disso eu não tinha visto nada ainda sobre MEF e Silveright.

A aplicação tem como base o uso de serviços. Ela tem alguns serviços padrão implementados nativamente mas também oferece interfaces que podem ser implementadas por terceiros que estejam interessados em disponibilizar seus próprios serviços na aplicação. Minha idéia inicial era fazer a implementação toda manualmente, ou seja, eu teria que:

  • escrever todo o código responsável por baixar/ler o zip contendo a extensão,
  • ler um arquivo de manifesto que estaria listando todas as extensões disponíveis naquele pacote,
  • instanciar cada extensão e adicioná-la nas coleções internas da aplicação

Comecei a fazer uma prova de conceito e vi que tudo isso era possível de ser feito mas não era uma tarefa trivial. Havia bastante código a ser escrito e muitas situações a ser levadas em consideração.

Decidimos começar fazendo uma aplicação como prova de conceito sem a funcionalidade de extensibilidade e depois eu me preocuparia com isso. O tempo passou, a POC foi feita e foi um sucesso.

A questão de extensibilidade ainda me incomodava. Eu não queria ter todo aquele trabalho pois parecia que eu estava reinventado a roda. Era final de Outubro e o PDC havia terminado. Enquanto vasculhava as sessions que eu queria baixar para assistir vi uma que me interessou chamada Building Extensible Rich Internet Applications with the Managed Extensibility Framework. Fiz o download, assisti e no mesmo instante fiquei convencido de que seria assim que eu implementaria a extensibilidade da minha aplicação. Com aproximadamente 5 linhas de código e 2 referencias dava pra fazer 80% da funcionalidade que eu precisava!

Chega de blá, blá, blá, me mostra logo como funciona!

Chegamos no presente. Voltei a trabalhar naquela aplicação, só que que agora não é mais uma prova de conceito. Como o Silverlight 4 ainda não tem licença Go-Live a aplicação está sendo feita em Silverlight 3 mas já pensando em ser migrada para Silverlight 4 assim que for possível. Tudo que será mostrado aqui serve para Silverlight 3 e Silverlight 4.

Para utilizar o MEF é necessário fazer referência a 2 dlls (presentes no Codeplex) :

  • System.ComponentModel.Composition
  • System.ComponentModel.Composition.Initialization

O MEF funciona da seguinte forma: Você cria uma interface ou classe base e a marca com um atributo do MEF que indica que ela é uma extensão (chamada pelo MEF de Export). Essa interface será o seu contrato. Depois você cria classes que implementam o seu contrato (esses serão os seus serviços que serão exportados). Depois você cria uma propriedade (array ou collection) que conterá a sua coleção de serviços e marca essa propriedade com um atributo do MEF que indica que ele deve colocar alí as extensões apropriadas que ele achar (o MEF chama isso de Import). Depois basta chamar uma linha de código que diz ao MEF “preencha todos os Imports do meu objeto”. Parece meio confuso explicando assim então é melhor eu mostrar.

Criei uma solution de exemplo (o link para download está no final do post) contendo 5 projetos.

Solution no Visual Studio 2010

  • MEF.Extension : Contém uma extensão para a aplicação. É um exemplo do que será feito por terceiros e parceiros.
  • MEF.Lib : É a biblioteca core da aplicação. Contém a interface de contrato do serviço e 1 implementação nativa.
  • MEF.Tests : É a aplicação principal. Contém outras 2 implementações do serviço e todo o resto da lógica da aplicação.
  • MEF.Tests.Web : Web application utilizado para testar e depurar a aplicação.
  • System.ComponentModel.Composition.Packaging.Toolkit : Recompilação em Silverlight 3 de uma dll que só existe no SDK do Silverlight 4. Explicarei melhor mais adiante.

O projeto MEF.Lib faz referência a System.ComponentModel.Composition. 

Referência do MEF.Lib

No projeto MEF.Lib temos a interface IServico que foi marcada com o atributo InheritedExport. Esse atributo serve para dizer que qualquer classe que implementar essa interface será automáticamente considerada pelo MEF como um Export.

interface IServico

Temos também uma classe chamada ServicoRoot que é uma implementação de IServico.

classe ServicoRoot

O projeto MEF.Tests faz referência ao projeto MEF.Lib, às duas dlls do MEF e à dll de Packaging de MEF do Toolkit que foi recompilada e adicionada nessa solution. Temos também outras 2 implementações de IServico, chamadas ServicoA e ServicoB, implementadas exatamente da mesma forma que ServicoRoot.

Referencias do projeto MEF.Tests

Temos uma página principal chamada MainPage que contém apenas um botão e um listbox. A aplicação está implementada utilizando o padrão MVVM, então temos uma classe que é o ViewModel de MainPage. Nessa classe há uma propriedade chamada “Servicos” que será onde o MEF irá colocar todas as implementações que ele achar de IServico.

Classe MainPageViewModel

Na linha 9 do ViewModel podemos ver uma chamada ao “PartInitializer.SatisfyImports(this);” O que isso faz é avaliar a classe atual e procurar por todas as propriedades marcadas com o atributo Import ou ImportMany. Depois ela procura no seu catalogo por todas as classes marcadas como Export que satisfaçam os Imports solicitados, instancia cada uma dessas classes e preenche as propriedades dos Imports.

Na tela há um ListBox fazendo binding com a propriedade Servicos do ViewModel. Se executarmos a aplicação como está aparecerão 3 serviços no ListBox, como pode ser visto abaixo:

Aplicação rodando com extensões internas

Ótimo, isso já é metade do caminho, mas ainda falta carregar a extensão externa que está no projeto MEF.Extension. Como isso será feito?

Coloquei um botão na tela que fará o download do xap gerado pelo projeto MEF.Extension e o adicionará ao catalogo do MEF para que ele saiba onde procurar mas para que isso fosse possível foram necessários 2 passos:

  1. Alterarar o catalogo padrão do MEF para um tipo de catalogo que permita modificações dinâmicas em runtime.
  2. Recompilar e utilizar uma dll do Toolkit do Silverlight 4 que contém esse novo tipo catalogo além de uma classe que já faz o trabalho de executar o download do xap, ler seu manifesto, carregar suas dlls na memória e adicioná-las ao catalogo.

Na verdade nós começamos pelo passo 2, baixando o código fonte do Toolkit para o Silverlight 4. Depois criamos na nossa solution um projeto class library em Silverlight 3 chamado “System.ComponentModel.Composition.Packaging.Toolkit” com as mesmas propriedades do original que está no código fonte que baixamos e copiamos os arquivos das classes de um projeto para o outro.

Depois vamos para o passo 1, onde temos que fazer uma alteração, preferencialmente na inicialização da aplicação, criando o novo catalogo e preparando o MEF para aceitar módulos externos em runtime. Abaixo temos as aterações que foram feitas no arquivo App.xaml.cs.

Alterações na classe App.xaml.ca

Foi criada uma propriedade estática Catalog (que será utilizada mais adiante) e um método InitializeContainer() que registra o novo catalogo para ser utilizado pelo MEF.

Na linha 37 o xap atual é adicionado ao catalogo. Isso garante que tudo que estiver no xap da aplicação já funcione por padrão. Na linha 42 esse novo catalogo é registrado para ser utilizado pelo MEF. A linha 40, que está comentada, é de um exemplo em Silverlight 4 e não funciona (nem pareceu ser necessária) em Silverlight 3.

Na linha 46 adicionamos a chamada ao novo método no Startup da aplicação, antes de instanciar a tela principal.

A única coisa que falta agora é fazer o download do xap que contém a extensão e adicioná-lo ao catalogo. Para isso estou utilizando o evento de click do botão que está na tela:

 Código para download da extensão

No click do botão é feito o seguinte:

  • É criada uma nova Uri com o endereço de onde será feito o download do xap com a extensão. Estou usando UriKind.Relative pois o xap está no mesmo diretório onde está o xap da aplicação principal.
  • É chamado o método estático Package.DownloadPackageAsync passando a Uri do xap e uma função lambda que será responsável por adicionar o novo pacote no catalogo.

O que o DownloadPackageAsync faz é executar o download do xap, ler seu manifesto e carregar todas suas dlls na memória. No final ele está executando a função lambda que passamos, que adiciona esse pacote xap no catalogo.

Esse novo tipo de catalogo então notifica o MEF de que há um novo pacote disponível e o MEF recompõe a aplicação automaticamente (não é necessário pedir par ao MEF “satifazer os imports” novamente). É por isso que no atributo ImportMany da propriedade Servicos está sendo passado o parâmetro AllowRecomposition=true. Sem ele a recomposição automática não aconteceria.

Se executarmos a aplicação novamente agora, continuarão aparecendo apenas 3 serviços no ListBox, mas ao clicar no botão um quarto serviço aparece.

Aplicação rodando com extensões internas e externas

A aplicação agora é extensível e se recompõe automaticamente quando uma nova extensão é baixada. E o mais importante: Não tivemos que escrever nenhum código para atualizar a tela novamente!

Mas espere um momento, você não mostrou como a extensão foi feita!

Sim, eu sei disso, mas não tem segredo. É que eu queria mostrar algumas dicas para criar as extensões.

Vocês devem ter notado que a extensão é um xap. Para fazer isso , adicione um novo projeto Silverlight Application na solution e remova todo o seu código (App.xaml, App.xaml.cs, MainPage.xaml, MainPage.xaml.cs). Depois trate ele normalmente como uma class library. Não se esqueça de vinculá-lo à sua aplicação web de testes para que ele seja atualizado na pasta ClientBin sempre que compilar.

Vinculando projetos Silverlight na aplicação web

Esse projeto tem que fazer referencia à dll que contém os contratos (MEF.Lib) que por consequencia faz referência a dll principal do MEF. Se você simplesmente gerar o xap ele ficará quase do tamanho da aplicação principal pois conterá as 2 dlls, mas elas não precisam estar nesse xap pois já estarão no xap da aplicação então você deve marcar a opção “Copy Local” da MEF.Lib para falso.

 Diminuindo o tamanho da extensão

O que falta então?

Em uma palavra? Cache. Do jeito que está se eu fechar e abrir novamente a aplicação, a extensão que havia baixado antes não estará mais lá, mas isso é fácil de resolver. Basta salvar a extensão no IsolatedStorage e procurar por todas as extensões que foram salvas durante a criação do container. Eu criaria um diretorio addins salvaria os xaps lá. Depois basta procurar todos os xaps que estiverem nesse diretório, carregá-los na memória e adicioná-los no catálogo.

Conclusão

Esse post todo foi apenas para mostrar como é simples usar MEF e como isso pode ajudar. Você não precisa mais criar todas as funcionalidades da sua aplicação de uma vez ou sozinho. Crie pontos de extensão e vá incluindo as funcionalidades em pacotes ou deixe que a comunidade crie extensões para seu aplicativo.

Vale lembrar que sempre que você adiciona referencias que não estão no runtime aos seus projetos, o tamanho do xap final aumenta. As dlls do MEF aumentam o tamanho do xap em 96KB.

O MEF é muito mais do que o que coloquei aqui hoje, por isso tem esse tamanho. Eu pesei os prós e contras e para essa aplicação vale muito a pena.

Abaixo tem um link para o código fonte do projeto mostrado aqui. Happy MEFing!