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!

16 comentários:

LTres disse...

Ótimo artigo, pra mim q tinha uma noção zero do MEF foi uma blz.
Apenas uma duvida que me apareceu. Claro sei que parece bobagem mas queria ver se é possivel. A extension criada poderia suportar outras extensions dentro dela? Algo do tipo a extensao que esta sendo adicionada a aplicação final ja contem outras extensões para ela mesma.

Kelps Leite de Sousa disse...

Nâo vejo motivo para não funcionar. Só fazendo um teste para ter certeza.

Gabriel disse...

Muito Legal o Exemplo de MEF.
Não sabia que ele ja estava disponível no SL3, hehe tenho que ver o video inteiro da próxima vez.
Uma experiencia na prática é sempre mais forte do que simplismente ler.
Ta de Parabens Kelps.
Gabriel

ninapicnic disse...

Olá,
Passei por aqui por um motivo diferente do tema do seu blog! Vi um comentário seu no Blog La Cucinetta, sobre requeijão e fiz a receita que você passou, muito boa mesmo, super rápida e fica um queijo delicioso para qualquer lanche!
Obrigada pela dica!
Nina

Kelps Leite de Sousa disse...

Nina,
Muito obrigado pelo seu comentário. Nunca imaginei ver meus "dotes" culinários aparecendo nesse blog.

Zote disse...

Kelps, ótimo post!
Só que fiquei com uma dúvida: como você faria pra atualizar a versão de uma extensão que foi salva no isolatedstorage?

Temos uma aplicação onde estamos começar a usar SL. Como nem todos os clientes vão usar todas as opções do sistema, você acha que seria uma boa ideia cada tela ou conjunto de telas, serem um xap e carregar eles usando este seu exemplo?

Abraço

Kelps Leite de Sousa disse...

Zote,
No start-up da aplicação, antes de carregar as extensões que foram salvas no IsolatedStorage, eu verificaria se há uma versão nova na url de origem e, se houver, baixaria a nova versão antes de carregar a extensão.

Para isso você terá que salvar, junto com a extensão, um arquivo dizendo qual é a url de origem da extensão e qual é a data original do arquivo. Daí você vê se há uma versão mais nova na url de origem. Se houver, você baixa a nova versão e carrega, se não houver você carrega a que já está salva.

Separar a sua aplicação em vários arquivos é uma boa opção, principalmente se a aplicação for grande e nem todos forem usar todas as funcionalidades. Se a aplicação não for muito grande não vale a pena incluir essa complexidade.

Gabriel disse...

Uma coisa que reparei é que existem alguns problemas para aplicações que usam staticresources eles acabam tendo que ser copiados pro App.xaml da aplicação que baixa essas packages, e o mesmo acontece com web.services ele procura o clientconfig.config no xap que baixa os packaged.
Recomendo os Videos de MEF do Mike Taulty que começam nesse aqui:

http://channel9.msdn.com/posts/mtaulty/MEF--Silverlight-4-Beta-Part-1-Introduction/

tem um acho que é o 5 dedicado só sobre isso. ( so que faz tudo em cima do SL4)

Abraços!

Anônimo disse...

Bom dia Kelps

Teria como fazer um plugin te uma TabItem do controle TabControl?

Mariel

Kelps Leite de Sousa disse...

Mariel,

Acredito que seja possível sim, mas nunca tentei. Qual é exatamente o seu cenário?

Kelps

Anônimo disse...

Opa,

Eu tenho uma TabControl com 2 abas(TabItem) uma com dados especifico que neste caso seria legal implementar MEF, pois iria variar conforme ramo de atividade do cliente.

Mariel

Anônimo disse...

Muito bacana o artigo, porém não estou conseguindo acesso as interfaces IView, IPageMetadata, onde elas estão

Anônimo disse...

Kelps, primeiramente gostaria de lhe parabenizar por seus posts, tem sido todos de grande utilidade para a comunidade de desenvolvimento.
Eu tenho uma dúvida, agradeço se puder ajudar a sana-la:
Estou construindo uma aplicação silverlight 5 utilizando o MEF, a arquitetura da aplicação possui o padrão de extensão já discutida em varias threads desse forum:


Aplicação Principal -> Aplicação Extendida 1

-> Aplicação Extendida 2



O meu problema é conseguir acessar recursos (metodos e propridades) da aplicação principal a partir de usercontrols das aplicações extendidas.

Vou tentar explicar melhor:

Em minha aplicação principal em sua MainPage utilizo um ContentFrame para navegação, nessa página tenho um método utilizado para forçar a navegação a partir de usercontrols carregados dinamicamente em uma outras páginas.

//Metodo em Minha MainPage

public void GoToPrincipalMenu()
{
ContentFrame.Navigate(new uri("/pgMenuPrincipal", UriKind.Relative));
}

Caso eu execute esse método de um usercontrol da propria aplicação principal ocorre tudo certo, faço dessa forma:

((pgMain)Application.Current.RootVisual).GoToPrincipalMenu();


Agora caso eu execute a partir de um usercontrol de uma das aplicações extendidas o método é executado sem erros mas a navegação não ocorre.

Faço da seguinte forma:

Type thisType = Application.Current.RootVisual.GetType();
object classInstance = Activator.CreateInstance(thisType, null);
MethodInfo theMethod = thisType.GetMethod("GoToPrincipalMenu");
theMethod.Invoke(classInstance, null);

Existe outras maneiras de fazer isso, chamar métodos da Mainpage da aplicação principal?

Adriano Silva

Kelps Sousa disse...

@Adriano Silva,

O seu não esta funcionando pois você esta criando uma nova instancia da janela principal quando na verdade você deveria estar tentando navegar na instancia existente.

Paramarrumar o seu código basta tirar a linha onde você usa o activator e na linha seguinte passar Application.Current.RootVisual no primeiro parâmetro do invoke.

Mas para fazer a coisa mais bonita e tipada eu faria o seguinte:
-Criaria uma interface IMainWindow na class Library onde você colocou as interfaces que são herdadas pelas suas extensões MEF. Nessa interface eu colocaria a definição do método GoToMainMenu()
-Faria a janela principal implementar essa interface
-Nas interfaces que você utilizou para criar suas extensões, definiria um método NavigateIn(IMainWindow main)
-Na aplicação principal, sempre que for carregar uma das extensões, chama a esse método da interface passando como parâmetro a janela principal.
-Nas extensões teria que implementar esse novo método, bastando para isso salvar o objeto IMainWindow que foi passado em uma propriedade local, para que possa ser chamado quando você quiser utilizar o método de navegação para a janela principal.


Atenciosamente

Kelps

Guilherme Seratti disse...

Ótimo Post, parabens!! Porém como faço depois de carregar todos esses xaps para exclui-los e caso clique de novo, ele refaça o download? e que quando eu exclua, ele entre no evento Unloaded do xaml.cs do XAP aberto ?

Kelps Sousa disse...

@Guilherme Serrati

Guilherme, o xap recem carregado só fica na app enquanto ela está em execução, a não ser que você faça algum código para salvar esse xap em algum lugar e o recarrege novamente de lá quando a app é carregada, portanto, para fazer com que você tenha que carregar novamente o xap, basta que você não faça nenhum código para salvá-lo e recarregá-lo.

Não há como descarregar um xap (ou qualquer assembly) da memória da sua app durante a execução. Uma vez carregado, só será descarregado da memória quando a app for fechada. Em aplicações .NET convencionais (não Silverlight) até é possível fazer isso, bastando carregar os novos assemblies em um novo application domain e para descarregar você "desliga" esse application domain, mas isso não é possível em Silverlight.

Mais uma coisa, o Unload só é executado no nível da app. Um xap carregado na sua app não é outra app, portato seu Unload nunca será executado. Você pode até fazer alguma coisa no sentido de executar código no unload de algum control que é criado pelo seu xap extra, ou pode fazer a sua interface do MEF definir um método de unload que você pode chamar no momento apropriado a partir do seu código principal, mas não há nada automático.

Espero ter ajudado.