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:

image

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).

image

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