Extensibilidade com MEF ou System.AddIn?
Semana passada fiz um webcast sobre extensibilidade de aplicativos (veja no link) e como o MEF (Microsoft Extensibility Framework) e as classes do namespace System.AddIn do .Net 4.0 podem ajudar.
A mensagem principal foi: hoje existem técnicas simples que permitem estender seu aplicativo baseadas na Injeção de Dependência.
A primeira tecnica que mostrei se baseia no cenário em que você tem um aplicativo e gostaria de ter terceiros criando novas extensões – os chamados AddIns. Veja os browsers, o Office e IDE´s como o Visual Studio ou o Eclipse, eles aceitam e até mesmo pedem AddIns. Este código de terceiro empacotado como AddIn deve ser gerenciável (temos que poder ativá-los ou desativá-los), trabalhar num sandbox (para não causar problemas em caso de erro) e ser carregado ou descarregado a qualquer hora. É para isto que existe o System.AddIn.
O System.AddIn utiliza uma arquitetura de desacoplamento que é baseada em um contrato, permitindo o versionamento, uma execução em Application Domains diferentes e um mecanismo de chamadas entre AppDomains. Veja a figura abaixo a arquitetura básica de um AddIn:
A segunda técnica se baseia no cenário em que você tem pontos de extensão no seu aplicativo e quer ter variabilidade de componentes de acordo com o contexto. Aqui, pense ou em aspectos horizontais (como Log, tratamento de erros, etc.) que você queira mudar de acordo com o contexto (log em banco de dados, ou em arquivo, ou etc.) ou em habilitar novas funcionalidades disponibilizando novos executáveis que serão percebidos e carregados pelo seu aplicativo principal (o hospedeiro dos componentes). O MEF foi feito para este cenário. Porém, ao contrário do AddIn, ele não cria um sandbox e não permite a descarga em tempo de execução, tornando, eventualmente, o seu aplicativo suscetível aos erros do componente injetado. Por isto ele ser mais considerado para uso pela mesma empresa que desenvolveu o hospedeiro/aplicativo principal.
Se estiver interessado, convido você a assistir o webcast (veja no link).
Uma promessa que fiz no webcast e que tenho de cumprir é a de mostrar o código do exemplo que fiz para mostrar o MEF. Como ele é complexo, sugiro ver o webcast antes.
Neste exemplo crio um form MDI, um form-filho normal (chamo de form embedded), e mais três forms que serão carregados pelo MEF (isto é, não dou new para criá-los, é o MEF quem vai tratar de achá-los e executá-los): um no mesmo assembly (FormAssembly), um numa DLL diferente (FormUm) mas ainda no mesmo projeto, e um terceiro que está num projeto completamente distinto (Form Auxiliar).
Na programação, o form MDI declara uma coleção de IForms (interface que todos os forms a serem carregados devem obedecer) e usa o atributo ImportMany indicando ao MEF que ele depende e gostaria de incluir IForms encontrados nos catálogos (diretórios, assemblies ou outro lugar onde possa encontrar código pronto para ser executado). Veja o código abaixo.
// forms to be imported
[ImportMany(AllowRecomposition=true)]
private IEnumerable<IForms> forms;
Em seguida, ele faz a carga destes forms indicando de quais catálogos eles devem ser carregados. É neste ponto que o MEF faz sua mágica e cria objetos da classe que herda de IForm de acordo com o lugar de procura – assembly ou diretório, neste caso.
// catalog where all external forms live
private DirectoryCatalog dirCatalog;
public FormMain()
{
InitializeComponent();
Compose();
}
// open the catalog and ask to compose all parts
private void Compose()
{
dirCatalog = new DirectoryCatalog(@"..\..\..\FormsUm\bin\debug");
var catalog = new AggregateCatalog(
new AssemblyCatalog(Assembly.GetExecutingAssembly()), dirCatalog );
var container = new CompositionContainer(catalog);
container.ComposeParts(this);
}
Para entender o código deste exemplo, é necessário falar do tempo de vida de um form. Quando um form é criado, uma classe C# é criada para conter o handle da janela do Sistema Operacional que representa o form. Quando alguém fecha a janela, o handle desta janela é zerado, mas o objeto C# que contém este handle continua existindo até que fique livre pelo Garbage Collector.
Bem, o MEF cria os objetos IForm e os inclui na coleção forms, porém, ao fecharmos o form, cabe a nós matarmos este objeto retirando-o ou não da coleção e recriando num futuro um outro form da mesma classe (quando o menu de criação do form é clicado). O MEF não trata da morte destes objetos.
Para possibilitar a recriação dos forms e habilitar itens de menu criei neste exemplo classes como o IForm, que define o contrato que um form deve ter, o FormControler que gerencia a criação/deleção de um IForm e um dicionário dictControlers que correlaciona os FormControlers com o string do item de menu que irá criar ou ativar um IForm.
O IForm foi definido assim:
// Interface to needed to control a Form from outside
public interface IForms
{
// returns the form type
Type Type();
// sets the method to callback when a Form is closed by the user
void SetCloseAction(Func<Boolean> closeAction );
// sets the MDI parent for this form
void SetMdiParent( Form mdiForm );
// asks the menu string to create this form
String GetMenuString();
// regular show/hide and close
void Show();
void Hide();
void Close();
}
O FormControler foi definido assim:
// A controller is a 1:1 map with a child form
// It exists mainly to connect the create method and close event that it's not considered by MEF
public class FormControler
{
private IForms myForm ;
private Type formType;
private Form myMdiForm;
#region IFormControler Members
public void Clear()
{
myForm = null;
formType = null;
myMdiForm = null;
}
public string SetForm(Form mdiForm, IForms form)
{
myForm = form;
myMdiForm = mdiForm;
formType = myForm.Type();
myForm.SetCloseAction(CloseAction);
myForm.SetMdiParent(mdiForm);
String menuString = myForm.GetMenuString();
return menuString;
}
public void RunCommand(string text)
{
if (myForm == null)
{
Object obj = Activator.CreateInstance(formType);
myForm = (IForms)obj;
myForm.SetMdiParent(myMdiForm);
myForm.SetCloseAction(CloseAction);
}
myForm.Show();
}
public Boolean CloseAction()
{
Form f = myForm as Form;
f.Dispose();
myForm = null;
return true;
}
#endregion
}
Note como ele armazena o tipo do form no método SetForm. Este tipo será usado quando houver necessidade de criar o form mais uma vez.
Note também que o método SetForm chama o método SetCloseAction passando o delegate para seu método CloseAction. O form (que herda de IForm) é obrigado a chamar este delegate no evento de Close para que o Dispose seja chamado. Caso o FormControler seja chamado para criar de novo o Form ele o fará via reflection chamando o comando Activator.CreateInstance(formType).
O código de um form, neste exemplo, é simples e padrão:
// IForm exported to be controlled from outside
[Export(typeof(IForms))]
public partial class FormUm : Form, IForms
{
public FormUm()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("Hello FormUm");
}
// All IForms methods
#region IForms Members
protected Func<bool> myCloseAction;
void IForms.Show()
{
this.Show();
this.Focus();
}
void IForms.SetMdiParent(Form f)
{
this.MdiParent = f;
}
Type IForms.Type()
{
return this.GetType();
}
void IForms.SetCloseAction(Func<bool> closeAction)
{
myCloseAction = closeAction;
}
string IForms.GetMenuString()
{
return "Run Um";
}
private void FormUm_FormClosed(object sender, FormClosedEventArgs e)
{
myCloseAction();
}
void IForms.Hide()
{
this.Hide();
}
void IForms.Close()
{
this.Close();
}
#endregion
}
Há mais para ver, mas o principal está aqui. Se quiser, baixe o código completo aqui.
Lembre-se apenas que é um código de exemplo e que existem projetos completos disponíveis, como o Composite Application Library.
Abraços