lunes, 26 de septiembre de 2011

Tutorial: Crear plugins con MEF (Managed Extensibility Framework)

Introducción

MEF es un conjunto de componentes  incluidos en el framework 4.0 que nos permite hacer aplicaciones extensibles. MEF intenta resolver el problema que supone extender una aplicación en tiempo de ejecución, es decir, da una solución estandarizada para aquellas aplicaciones que pretendan usar un modelo de plugins.
Para trabajar con MEF debemos conocer los siguientes conceptos:
·    Composition part. Parte extensible, le llamaremos simplemente parte la cual proporcionará servicio a otras partes y consumirá servicios proporcionados por otras partes. Estas partes pueden estar dentro del propio ensamblado de la aplicación o en un ensamblado externo. Una parte puede ser una clase, propiedad o método. Estas partes se decoran con un atributo de exportación o de importación.
·    Export. Es un servicio exportado por una parte.
·    Import. Es un servicio consumido por una parte.
·    Contrat. Contrato, es un identificador para un Export o un Import. Un Export especifica el contrato que proporciona y un Import especifica el contrato que necesita.
·    Composition. Es la acción por la cual de forma dinámica y en tiempo de ejecución  las distintas partes son instanciadas y se hace coincidir los Import con los Export.
A groso modo el funcionamiento de MEF consistiría en definir primero el contrato que van a utilizar las partes, el contrato puede ser una cadena de texto o un tipo, yo personalmente me gusta crear un interface que deberán implementar todas las clases de exportación y que luego usaré su tipo como el contrato. Al implementar en las clases Export el interfaz me permite que la aplicación esté débilmente acoplada. Una vez declarado el contrato, se definirán las clases que se van a utilizar como Export, estas las decoraremos con el atributo Export el cual recibirá como parámetro el contrato a utilizar, lo cual indica que esta clase está disponible para cualquier Import que coincida con su contrato. Después definiremos el/los Import para ello declararemos una propiedad o campo y la decoraremos con el atributo Import pasándole como parámetro el contrato que se va a utilizar. En la propiedad o campo no se realizará ninguna implementación dado que MEF buscará el/los Export que cumplan el contrato y asociará la implementación del Export a la propiedad o campo.
Una vez tenemos definido el contrato y los Import y Export los pasos necesarios para realizar el Composition son :
·    Crear un catálogo que MEF utilizará para descubrir las partes.
·    Crear un contenedor que contenga las instancias de las partes.
·    Componer llamando al método ComposeParts del contenedor.
Para hacer plugins utilizando como contrato el interfaz, esté deberá ser público en el ensamblado que se defina o mejor aún crear un ensamblado en el que se defina dicho interfaz y que este sea distribuido a terceros a modo de SDK.

Otras aplicaciones

Además de para realizar aplicaciones extensibles,  MEF también nos permite realizar aplicaciones cuyos componente sean reutilizables, gracias a ser componentes débilmente acoplados. Este desacoplamiento se realiza por medio de contratos. Un contrato es 1 Import y 1 ó más Export. MEF también nos puede servir para crear entornos de pruebas sin necesidad de modificar el código, podemos tener en un ensamblado el código de acceso a datos el cual referenciaríamos en la aplicación. Por otro lado tenemos la aplicación de test en ésta podríamos o bien apuntar al ensamblado de acceso a datos o bien tener definido otro ensamblado en el que tengamos los datos en el propio código y utilicemos sólo para los casos de pruebas, así únicamente referenciando en el programa de test este ensamblado sin tocar nada en la aplicación lo tendríamos funcionando.

Para muestra un botón

Se va a implementar con MEF un ejemplo muy sencillo, crearé un proyecto de línea de comandos para evitar que los árboles no nos dejen ver el bosque. Además a la solución añadiremos otro proyecto de tipo dll para incluir en él el contrato.  La aplicación nos permitirá dada una cadena realizar dos acciones sobre ella: ponerla en mayúsculas y ponerla en minúsculas. Pero para que la aplicación sea más flexible permitiremos el uso de plugins para poder añadir funcionalidades extra.
Veamos el código de la interfaz que nos servirá de contrato y que ubicaremos en la dll externa:
namespace DemoMEF.Contract
{
    public interface ITransformString
    {
        string Descripcion { get; }
        string Transformar(string str);
    }
}

A continuación crearemos el ensamblado principal en el cual añadiremos la referencia a la dll System.ComponentModel.Composition.dll y a la dll en la cual hemos declarado el interfaz que nos servirá como contrato. Y añadiremos los using a estas dos dll:
using System.ComponentModel.Composition;
using DemoMEF.Contract;

Ahora se mostrará la implementación de las dos operaciones antes citadas que serán los Export de la aplicación y que se definirán en el mismo ensamblado  que la aplicación:
[Export(typeof(ITransformString))]
    public class TransformToUpper : ITransformString
    {
        public string Descripcion { get { return ("Pasar a mayúsculas"); } }
        public string Transformar(string str)
        {
            return (str.ToUpper());
        }
    }
    [Export(typeof(ITransformString))]
    public class TransformToLower : ITransformString
    {
        public string Descripcion { get { return("Pasar a minúsculas");}}
        public string  Transformar(string str)
        {
            return(str.ToLower());
        }
    }

Como vemos implementa el interfaz y tiene el atributo Export indicando el contrato. Veamos la declaración del Import:
[ImportMany(typeof(ITransformString))]
IEnumerable<ITransformString> _functions;

En este caso como se trata de varias funciones, utilizamos el atributo ImportMany en lugar de Import, y declaramos la variable como un IEnumerable<ITransformString> para poder acceder a todos los elementos.
Ahora vamos a ver como se pone en marcha todo esto:
    private void Init()
    {
            // Descubrimos las partes del propio ensamblado
            var almacenCatalogos = new AggregateCatalog();
            var catalogo = new AssemblyCatalog(
System.Reflection.Assembly.GetExecutingAssembly());
            almacenCatalogos.Catalogs.Add(catalogo);
            // Descubrimos las partes que puedan
            // haber en el directorio "plugins"
            try {
                var dirCatalogo = new DirectoryCatalog(".\\plugins");
                almacenCatalogos.Catalogs.Add(dirCatalogo);
            } catch {
                // Puede que no exista el directorio de
                // plugins, lo ignoramos
            }

            var contenedor = new CompositionContainer(almacenCatalogos);
            // Instanciamos las partes
            contenedor.ComposeParts(this);
           
        }
    }

Como se puede ver en el código, primero se cargan dos catálogos: uno para el propio ensamblado y otro en el que se recogerá todos los ensamblados que hayan en el directorio “plugins”, a continuación se asignan los catalogos al contenedor y se hace ComposeParts para instanciar las partes y enlazar los Import con los Export.
Por último el código de la aplicación es el siguiente:
string str = "Hola mundo";
System.Console.WriteLine(string.Format("Cadena a transformar: {0}",str));
foreach (ITransformString ts in _functions) {
System.Console.WriteLine("Acción: {0}. Resultado: {1}",
ts.Descripcion, ts.Transformar(str));
}
System.Console.ReadLine();
´
En la aplicación simplemente se recorren las partes y se ejecutan las distintas funciones obteniendo el siguiente resultado:

Cadena a transformar: Hola mundo
Acción: Pasar a mayúsculas. Resultado: HOLA MUNDO
Acción: Pasar a minúsculas. Resultado: hola mundo


Ahora crearemos la dll que vamos a utilizar como plugin. Primero le añadiremos la referencia a System.ComponentModel.Composition.dll y otra referencia a la dll en la que hemos implementado la interfaz que usaremos como contrato. Una vez hecho esto escribiremos la implementación de las dos nuevas operaciones que vamos a añadir sobre las cadenas de la aplicación anterior:
using System.ComponentModel.Composition;
using DemoMEF.Contract;

[Export(typeof(ITransformString))]
public class TransformReverse : ITransformString
{
    public string Descripcion { get { return ("Invertir Cadena"); } }
    public string Transformar(string str)
    {
        char[] array = str.ToCharArray();
        Array.Reverse(array);
        return new string(array);
    }
}
[Export(typeof(ITransformString))]
public class TransformCapitalize : ITransformString
{
    public string Descripcion {
get { return("Poner primera letra en mayúsculas"); } }
    public string Transformar(string str)
    {
        return (System.Globalization.CultureInfo.CurrentCulture.
TextInfo.ToTitleCase(str));
    }
}

 
Ahora si compilamos esta dll y la copiamos en el directorio “plugins” de la aplicación anterior, al ejecutarla obtendremos lo siguiente:
Cadena a transformar: Hola mundo
Acción: Pasar a mayúsculas. Resultado: HOLA MUNDO
Acción: Pasar a minúsculas. Resultado: hola mundo
Acción: Invertir Cadena. Resultado: odnum aloH
Acción: Poner primera letra en mayúsculas.
Resultado: Hola Mundo



Como se puede ver en la nueva salida del programa, se realizan dos nuevas acciones sobre la cadena y no se ha realizado ninguna modificación sobre el programa original aplicando MEF su “magia” carga la nueva dll y asigna sus exports para poder ejecutar las nuevas acciones.

1 comentario: