Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Creación de aplicaciones que se pueden componer en .NET 4 con Managed Extensibility Framework
Glenn Block
Con el próximo Microsoft .NET Framework 4, encontrará una emocionante tecnología nueva en su puerta que simplificará en gran medida el desarrollo de sus aplicaciones. Si ha tenido problemas con el diseño de aplicaciones que sean más fáciles de mantener y ampliar, siga leyendo.
Managed Extensibility Framework (MEF) es una biblioteca nueva en .NET Framework 4 y en Silverlight 4 que simplifica el diseño de sistemas que se pueden componer y que terceros pueden ampliar después de su implementación. MEF abre sus aplicaciones, al permitir que desarrolladores de aplicaciones, autores de marcos y extensores de terceros introduzcan poco a poco nuevas funciones.
Por qué lo construimos
Hace varios años, al interior de Microsoft, diversos grupos trabajaban para encontrar soluciones a un problema: cómo construir aplicaciones a partir de componentes reutilizables que se pueden detectar, volver a usar y componer de manera dinámica:
- Visual Studio 2010 estaba construyendo un nuevo editor de código extensible. Todas las capacidades básicas del editor, así como las capacidades de terceros, se iban a implementar como códigos binarios que se detectarían en tiempo de ejecución. Uno de los requisitos principales era admitir extensiones que se cargarían de manera diferida a fin de mejorar el tiempo de inicio y el consumo de memoria.
- “Oslo” introdujo “Intellipad”, un editor de texto extensible nuevo para trabajar con MEF. En Intellipad, los complementos se iban a crear en IronPython.
- Acropolis estaba entregando un marco para construir aplicaciones compuestas. El tiempo de ejecución de Acropolis detectó “partes” de componente de aplicación en tiempo de ejecución y proporcionó servicios a esas partes con un acoplamiento flexible. Acropolis utilizó XAML en gran medida para la creación de componentes.
Este problema no era específico de Microsoft. Los clientes han estado implementando sus propias soluciones de extensibilidad personalizadas por años. Ésta era una oportunidad clara para que la plataforma hiciera su entrada y proporcionara una solución más generalizada para ayudar a Microsoft y a los clientes.
¿Necesitábamos algo nuevo?
MEF no es ningún caso la primera solución en el espacio de este problema. Se han propuesto muchas soluciones: una larga lista de aventuras que cruzan los límites de las plataformas e incluyen esfuerzos como la implementación OSGI de EJB, CORBA, Eclipse y Spring por el lado de Java. En la plataforma de Microsoft, están el modelo Component y System.Addin dentro del mismo .NET Framework. Y existen varias soluciones de código abierto, incluida la arquitectura SODA de SharpDevelop y contenedores de Inversión de control como Castle Windsor, Structure Map y Unity de patrones y prácticas.
Con todos los enfoques existentes, ¿por qué pensar en uno nuevo? Nos dimos cuenta de que ninguna de nuestras soluciones actuales eran ideales para una extensibilidad general de terceros. Eran demasiado pesadas para uso general o requería demasiado esfuerzo de parte del host o del desarrollador de extensiones. MEF representa la culminación del aprendizaje a partir de cada una de estas soluciones y un intento por abordar los puntos conflictivos que se acaban de mencionar.
Echemos un vistazo a los conceptos básicos de MEF, ilustrados en la Figura 1.
Figura 1 Conceptos básicos en Managed Extensibility Framework
Conceptos
En el corazón de MEF se encuentran unos cuantos conceptos esenciales:
Parte que se puede componer (o, simplemente, parte): parte que proporciona servicios a otras partes y que utiliza servicios proporcionados a su vez por otras partes. Las partes de MEF pueden provenir de cualquier lugar, desde dentro de la aplicación o desde fuera; desde una perspectiva de MEF, no hay diferencia.
Exportación: servicio que una parte proporciona. Cuando una parte proporciona una exportación, se dice que la parte la exporta. Por ejemplo, una parte puede exportar un registrador o, en el caso de Visual Studio, una extensión del editor. Las partes pueden proporcionar varias exportaciones, aunque la mayor parte de las partes proporcionan una única exportación.
Importación: servicio que una parte utiliza. Cuando una parte utiliza una importación, la parte la importa. La partes pueden importar servicios únicos, como el registrador, o importar varios servicios, como una extensión de editor.
Contratos: un contrato es un identificador para una exportación o importación. Un exportador especifica un contrato de cadena que proporciona y un importador especifica el contrato que necesita. MEF deriva nombres contractuales de los tipos que se exportan e importan, de manera que en la mayoría de los casos no tiene pensar en eso.
Composición: las partes las compone MEF, el cual les crea una instancia y luego calza exportadores con importadores.
Modelos de programación: las facetas de MEF
Los desarrolladores utilizan MEF a través de un modelo de programación. Un modelo de programación proporciona un medio para declarar componentes como partes de MEF. De fábrica, MEF proporciona un modelo de programación atributivo, el cual será el foco principal de este artículo. Ese modelo es sólo uno de muchos modelos de programación posibles que MEF permite. La API central de MEF es totalmente independiente de los atributos.
Detalles en profundidad del modelo de programación atributivo
En el modelo de programación atributivo, las partes (conocidas como partes atribuidas) se definen con un conjunto de atributos de .NET, que viven en el espacio de nombres System.ComponentModel.Composition. En la siguiente sección, exploraré la creación de una aplicación extensible de administración de pedidos de ventas de Windows Presentation Foundation (WPF) mediante este modelo. Esta aplicación permite que los clientes agreguen vistas nuevas, personalizadas dentro de sus entornos simplemente al implementar un código binario en la carpeta Bin. Echaré un vistazo a cómo esto se puede implementar a través de MEF. Iré mejorando poco a poco el diseño conforme avanzo y explicaré más acerca de las capacidades de MEF y lo que el modelo de programación atributivo proporciona en el proceso.
Exportación de una clase
La aplicación de administración de pedidos permite integrar nuevas vistas. Para exportar algo a MEF, lo hace mediante el atributo Export tal como se muestra acá.
[Export]
public partial class SalesOrderView : UserControl
{
public SalesOrderView()
{
InitializeComponent();
}
}
La parte de arriba exporta el contrato SalesOrderView. De manera predeterminada, el atributo Export usa el tipo concreto del miembro (en este caso, la clase) como el contrato. También puede especificar de manera explícita el contrato al pasar un parámetro al constructor del atributo.
Importación a través de propiedades y campos
Las partes atributivas pueden expresar las cosas que necesitan mediante el uso del atributo de importación en una propiedad o un campo. La aplicación exporta una parte ViewFactory, que otras partes pueden usar para obtener vistas. ViewFactory importa SalesOrderView mediante la importación de una propiedad. Importar una propiedad simplemente significa decorar una propiedad con un atributo Import.
[Export]
public class ViewFactory
{
[Import]
public SalesOrderView OrderView { get; set; }
}
Importación a través de constructores
Las partes también se pueden importar a través de constructores (conocidos comúnmente como inyección de constructor) al usar el atributo ImportingConstructor tal como se muestra a continuación. Cuando se usa un constructor de importación, MEF supone que todos los parámetros son importaciones, con lo cual es atributo de importación se vuelve innecesario.
[Export]
public class ViewFactory
{
[ImportingConstructor]
public ViewFactory(SalesOrderView salesOrderView)
{
}
}
En general, importar a través de constructores en vez de propiedades es una cuestión de preferencia, aunque hay veces en que es apropiado usar importaciones de propiedades, en particular cuando hay partes para las que MEF no puede crear instancias, como en el ejemplo de App de WPF. La recomposición tampoco se admite en los parámetros del constructor.
Composición
Con SalesOrderView y ViewFactory en su lugar, ahora puede empezar la composición. Las partes de MEF no se detectan ni crean de manera automática. En su lugar, debe escribir algún código de secuencia de arranque que establecerá la composición. Un lugar común para hacerlo es en el punto de entrada de su aplicación, que en este caso es la clase App.
Arrancar MEF implica unos cuantos pasos:
- Agregue importaciones de los contratos que necesita que cree el contenedor.
- Cree un catálogo que MEF use para detectar partes.
- Cree un contenedor que componga instancias de partes.
- Componga al llamar el método Composeparts en el contenedor y pasar la instancia que tiene las importaciones.
Como puede ver aquí, agregué la importación ViewFactory en la clase App. Luego creé un DirectoryCatalog que apuntaba a la carpeta Bin y creé un contenedor que usa el catálogo. Por último, llamé a Composeparts, lo que provocó que se compusiera una instancia APP y se satisficiera la importación ViewFactory.
public partial class App : Application
{
[Import]
public ViewFactory ViewFactory { get; set; }
public App()
{
this.Startup += new StartupEventHandler(App_Startup);
}
void App_Startup(object sender, StartupEventArgs e)
{
var catalog = new DirectoryCatalog(@".\");
var container = new CompositionContainer(catalog);
container.Composeparts(this);
}
}
Durante la composición, el contenedor creará ViewFactory y satisfará su importación SalesOrderView. Esto dará como resultado que se cree SalesOrderView. Por último, la clase Application tendrá su importación ViewFactory satisfecha. De esta manera, MEF ha ensamblado todo el gráfico de objetos basado en información declarativa, en vez de requerir manualmente que el código imperativo realice el ensamblado.
Exportación de elementos que no son de MEF a MEF a través de propiedades
Al integrar MEF en una aplicación existente, o con otros marcos, a menudo encontrará instancias de clase relacionadas que no son de MEF (lo que significa que no son partes) que le conviene dejar disponibles para los importadores. Puede tratarse de tipos de marco sellado como System.String; singletons para toda la aplicación como Application.Current; o instancias recuperadas desde una fábrica, como una instancia de registrador desde Log4Net.
Para respaldar esto, MEF permite exportaciones de propiedades. Para usar exportaciones de propiedades, puede crear una parte intermediaria con una propiedad decorada con una exportación. Esa propiedad es en esencia una fábrica y ejecuta cualquier lógica personalizada necesaria para recuperar el valor que no es de MEF. En el siguiente ejemplo de código, puede ver que Loggerpart exporta un registrador Log4Net, lo que permite que otras partes como App lo importen en vez de depender de la obtención de acceso al método del descriptor de acceso estático.
public class Loggerpart
{
[Export]
public ILog Logger
{
get { return LogManager.GetLogger("Logger"); }
}
}
Las exportaciones de propiedades son como las navajas Swiss Army en su funcionalidad, ya que permiten que MEF se lleve bien con los demás. Las encontrará extremadamente útiles para integrar MEF en sus aplicaciones existentes y para hablar con los sistemas heredados.
Desacoplamiento de la implementación con una interfaz
Volviendo al ejemplo de SalesOrderView, se ha formado una relación de acoplamiento ajustado entre ViewFactory y SalesOrderView. La fábrica espera una función SalesOrderView concreta que limite las opciones de extensibilidad así como la capacidad de prueba de la fábrica misma. MEF permite que las importaciones se desacoplen de la implementación del exportador mediante una interfaz como contrato:
public interface ISalesOrderView{}
[Export(typeof(ISalesOrderView))]
public partial class SalesOrderView : UserControl, ISalesOrderView
{
...
}
[Export]
public class ViewFactory
{
[Import]
ISalesOrderView OrderView{ get; set; }
}
En el código anterior, cambié SalesOrderView para implementar ISalesOrderView y exportarlo de manera explícita. También cambié la fábrica del lado del importador para importar ISalesOrderView. Tenga en cuenta que el importador no tiene que especificar el tipo de manera explícita, ya que MEF puede derivarlo desde el tipo de propiedad, que es ISalesOrderView.
Esto plantea la pregunta de si ViewFactory debe implementar también una interfaz como IViewFactory. Esto no es un requisito, aunque puede tener sentido en un escenario ficticio. En este caso, no espero que nadie reemplace ViewFactory y como está diseñado en modo de prueba, está bien. Puede tener varias exportaciones en una parte para importar la parte según varios contratos. SalesOrderView, por ejemplo, puede exportar UserControl y ISalesOrderView al tener un atributo de exportación adicional:
[Export (typeof(ISalesOrderView))]
[Export (typeof(UserControl))]
public partial class SalesOrderView : UserControl, ISalesOrderView
{
...
}
Ensamblados de contrato
Cuando empiece a crear contratos, necesitará una manera de implementar esos contratos con terceros. Una manera común de hacerlo es tener un ensamblado de contrato que contenga interfaces para los contratos que implementarán los extensores. El ensamblado de contrato se convierte en una forma de SDK al que las partes harán referencia. Un patrón común es nombrar el ensamblado de contrato como el nombre de la aplicación + .Contracts, como SalesOrderManager.Contracts.
Importación de muchas exportaciones del mismo contrato
Actualmente ViewFactory importa sólo una vista única. Codificar a un miembro (parámetro de propiedad) para cada vista funciona para un número muy pequeño de tipos definidos previamente de vistas que no cambian con frecuencia. Sin embargo, con semejante enfoque, agregar nuevas vistas requiere que la fábrica se vuelva a compilar.
Si se esperan muchos tipos de vistas, MEF ofrece un mejor enfoque. En vez de usar una interfaz de vista específica, puede crear una interfaz IView genérica que todas las vistas exportan. La fábrica luego importa una colección de todas las IViews disponibles. Para importar una colección en el modelo atributivo, use el atributo ImportMany.
[Export]
public class ViewFactory
{
[ImportMany]
IEnumerable<IView> Views { get; set; }
}
[Export(typeof(IView))]
public partial class SalesOrderView : UserControl, IView
{
}
//in a contract assembly
public interface IView{}
Aquí puede ver que ViewFactory ahora importa una colección de instancias IView en vez de una vista específica. SalesOrder implementa IView y la exporta en vez de ISalesOrderView. Con esta refactorización, ViewFactory ahora puede respaldar un conjunto abierto de vistas.
MEF también admite la importación mediante colecciones concretas como ObservableCollection<T> o List<T>, así como colecciones que proporcionan un constructor predeterminado.
Control de la directiva de creación de partes
De manera predeterminada, las instancias de todas las partes del contenedor son singletons, por tanto las comparte cualquier parte que las importe dentro del contenedor. Por este motivo, todos los importadores de SalesOrderView y ViewFactory obtendrán la misma instancia. En muchos casos esto es conveniente, ya que reemplaza tener miembros estáticos de los que dependen otros componentes. Sin embargo, algunas veces es necesario que cada importador obtenga su propia instancia, por ejemplo, para permitir que se vean varias instancias de SalesOrderView en la pantalla al mismo tiempo.
La directiva de creación de partes en MEF puede ser una de tres valores: CreationPolicy.Shared, CreationPolicy.NonShared o CreationPolicy.Any. Para especificar la directiva de creación en una parte, decórela con el atributo partCreationPolicy, tal como se muestra aquí:
[partCreationPolicy(CreationPolicy.NonShared)]
[Export(typeof(ISalesOrderView))]
public partial class SalesOrderView : UserControl, ISalesOrdderView
{
public SalesOrderView()
{
}
}
PartCreationPolicy también se puede especificar en el lado del importador al establecer la propiedad RequiredCreationPolicy en la importación.
Distinción entre exportaciones con metadatos
ViewFactory ahora funciona con un conjunto abierto de vistas, pero no tengo manera de distinguir una vista de otra. Podría agregar un miembro a IView denominado ViewType, el cual proporcionaría la vista y luego filtrar contra esa propiedad. Una alternativa es usar las instalaciones de metadatos de exportación de MEF para anotar las vista con su ViewType. Usar los metadatos proporciona una ventaja adicional de permitir el retraso de la creación de instancias de vista hasta que sea necesario, lo cual puede conservar recursos y mejorar el rendimiento.
Definición de metadatos de exportación
Para definir los metadatos en una exportación, use el atributo ExportMetadata. A continuación, se ha cambiado SalesOrderView para exportar una interfaz de marcador IView como su contrato. Luego agrega metadatos adicionales de “ViewType” para que se pueda localizar entre otras vistas que comparten el mismo contrato:
[ExportMetadata("ViewType", "SalesOrder")]
[Export(typeof(IView)]
public partial class SalesOrderView : UserControl, IView
{
}
ExportMetadata tiene dos parámetros, una clave que es una cadena y un valor de objeto tipo. Usar cadenas mágicas como en el ejemplo anterior puede ser problemático porque no es seguro desde el punto de vista de la compilación. En lugar de una cadena mágica, podemos suministrar una constante para la clave y una enumeración para el valor:
[ExportMetadata(ViewMetadata.ViewType, ViewTypes.SalesOrder)]
[Export(typeof(IView)]
public partial class SalesOrderView : UserControl, IView
{
...
}
//in a contract assembly
public enum ViewTypes {SalesOrderView}
public class ViewMetadata
{
public const string ViewType = "ViewType";
}
Usar el atributo ExportMetadata proporciona mucha flexibilidad, pero hay varias advertencias al usarlo:
- Las claves de metadatos no se pueden detectar en el IDE. El autor de la parte debe saber cuáles claves y tipos de metadatos son válidos para la exportación.
- El compilador no validará metadatos para asegurar que está correcto.
- ExportMetadata agrega más ruido al código y oculta el propósito.
MEF proporciona una solución para abordar los problemas anteriores: exportaciones personalizadas.
Atributos de exportaciones personalizadas
MEF permite la creación de exportaciones personalizadas que incluyen sus propios metadatos. Crear una exportación personalizada implica la creación de un ExportAttribute derivado que también especifica metadatos. Podemos usar exportaciones personalizadas para crear un atributo ExportView que incluya metadatos para ViewType:
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class ExportViewAttribute : ExportAttribute {
public ExportViewAttribute()
:base(typeof(IView))
{}
public ViewTypes ViewType { get; set; }
}
ExportViewAttribute especifica que exporta IView al llamar al constructor base de la exportación. Está decorado con un MetadataAttribute, el cual especifica que el atributo proporciona metadatos. Este atributo le dice a MEF que mire todas las propiedades públicas y cree metadatos asociados en la exportación con el nombre de la propiedad como la clave. En este caso, los únicos metadatos son ViewType.
La última cosa importante que hay que tener en cuenta acerca del atributo ExportView es que está decorado con un atributo AttributeUsage. Esto especifica que el atributo es válido sólo en clases y que sólo un atributo ExportView único puede estar presente.
En general, AllowMultiple debe configurarse en falso; si es verdadero, al importador se le pasará una matriz de valores en vez de un valor único. AllowMultiple debe dejarse como verdadero cuando haya varias exportaciones con diferentes metadatos del mismo contrato en el mismo miembro.
Aplicar el nuevo ExportViewAttribute a SalesOrderView ahora arroja lo siguiente:
[ExportView(ViewType = ViewTypes.SalesOrder)]
public partial class SalesOrderView : UserControl, IView
{
}
Como puede ver, las exportaciones personalizadas aseguran que se proporcionen los metadatos correctos para una exportación particular. También reducen el ruido en el código, son más detectables a través de IntelliSense y expresan mejor el propósito al ser específicas al dominio.
Ahora que los metadatos se definieron en la vista, ViewFactory los puede importar.
Importación de exportaciones diferidas y acceso a metadatos
Para permitir el acceso de metadatos, MEF aprovecha una nueva API de .NET Framework 4, System.Lazy<T>. Permite implementar la creación de instancias de una instancia hasta que se obtenga acceso a la propiedad de valor de Lazy. MEF después extiende Lazy<T> con Lazy<T,TMetadata> para permitir el acceso a metadatos de exportación sin crear instancias para la exportación subyacente.
TMetadata es un tipo de vista de metadatos. Una vista de metadatos es una interfaz que define propiedades de sólo lectura que corresponden a claves en los metadatos exportados. Cuando se obtenga acceso a la propiedad de metadatos, MEF implementará de manera dinámica TMetadata y establecerá los valores según los metadatos proporcionados desde la exportación.
Así es como ViewFactory se ve cuando la propiedad View se cambia a importar al usar Lazy<T,TMetadata>:
[Export]
public class ViewFactory
{
[ImportMany]
IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; }
}
public interface IViewMetadata
{
ViewTypes ViewType {get;}
}
Una vez que se ha importado una colección de exportaciones diferidas con metadatos, puede usar LINQ para filtrar contra el conjunto. En el siguiente fragmento de código, implementé un método GetViews en ViewFactory para recuperar todas las vistas del tipo especificado. Tenga en cuenta que obtiene acceso a la propiedad Value a fin de fabricar las instancias de vista reales sólo para las vistas que coinciden con el filtro:
[Export]
public class ViewFactory
{
[ImportMany]
IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; }
public IEnumerable<View> GetViews(ViewTypesviewType) {
return Views.Where(v=>v.Metadata.ViewType.Equals(viewType)).Select(v=>v.Value);
}
}
Con estos cambios, ViewFactory ahora detecta todas las vistas disponibles al momento que MEF compone la fábrica. Si las nuevas implementaciones aparecen en el contenedor o los catálogos después de la composición inicial, ViewFactory no las verá, puesto que ya se compuso. No sólo eso, sino que MEF en realidad evitará que las vistas se agreguen al catálogo al lanzar CompositionException, es decir, a menos que se habilite la recomposición.
Recomposición
La recomposición es una característica de MEF que permite que las partes actualicen sus importaciones de manera automática según aparezcan nuevas exportaciones coincidentes en el sistema. Un escenario en que la recomposición en útil es para descargar partes desde un servidor remoto. SalesOrderManager se puede cambiar para que cuando parta, inicie una descarga para varias vistas opcionales. Cuando aparezcan las vistas, aparecerán en la fábrica de vistas. Para que ViewFactory se pueda volver a componer, configuramos la propiedad AllowRecomposition en el atributo ImportMany de la propiedad Views en verdadero, tal como se muestra aquí:
[Export]
public class ViewFactory
{
[ImportMany(AllowRecomposition=true)]
IEnumerable<Lazy<IView, IViewMetadata>> Views { get; set; }
public IEnumerable<View>GetViews(ViewTypesviewType) {
return Views.Where(v=>v.Metadata.ViewType.Equals(viewType)).Select(v=>v.Value);
}
}
Cuando se produce la recomposición, la colección Views se reemplazará al instante por una colección nueva que contiene el conjunto actualizado de vistas.
Con la recomposición habilitada, la aplicación puede descargar ensamblados adicionales desde el servidor y agregarlos al contenedor. Esto se puede hacer a través de catálogos de MEF. MEF ofrece varios catálogos, dos de los cuales se pueden volver a componer. DirectoryCatalog, que ya vio, es uno que se puede volver a componer al llamar el método Refresh. Otro catálogo que se puede volver a componer es AggregateCatalog, el cual es un catálogo de catálogos. Agréguele catálogos mediante la propiedad de colección Catalogs, la cual inicia la recomposición. El último catálogo que usaré es AssemblyCatalog, el cual acepta un ensamblado sobre el cual crear luego un catálogo. En la Figura 2 se muestra un ejemplo que ilustra cómo puede usar estos catálogos juntos para una descarga dinámica.
Figura 2 Uso de catálogos de MEF para carga dinámica
void App_Startup(object sender, StartupEventArgs e)
{
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(newDirectoryCatalog((@"\.")));
var container = new CompositionContainer(catalog);
container.Composeparts(this);
base.MainWindow = MainWindow;
this.DownloadAssemblies(catalog);
}
private void DownloadAssemblies(AggregateCatalog catalog)
{
//asynchronously downloads assemblies and calls AddAssemblies
}
private void AddAssemblies(Assembly[] assemblies, AggregateCatalog catalog)
{
var assemblyCatalogs = new AggregateCatalog();
foreach(Assembly assembly in assemblies)
assemblyCatalogs.Catalogs.Add(new AssemblyCatalog(assembly));
catalog.Catalogs.Add(assemblyCatalogs);
}
El contenedor de la Figura 2 se crea con AggregateCatalog. Luego se le agrega DirectoryCatalog para usar las partes locales de la carpeta Bin. El catálogo agregado se pasa al método DownloadAssemblies, el cual descarga de manera asincrónica ensamblados y luego llama AddAssemblies. Ese método crea un nuevo AggregateCatalog, al cual agrega AssemblyCatalogs para cada ensamblado de descarga. AddAssemblies luego agrega AggregateCatalog que contiene los ensamblados para el agregado principal. El motivo de que agregue de esta manera es que la recomposición ocurra de una sola vez, en vez de una y otra vez, que es lo que pasaría si agregáramos catálogos de ensamblado directamente.
Cuando la recomposición ocurre, la colección se actualiza inmediatamente. Dependiendo del tipo de propiedad de colección, el resultado es diferente. Si la propiedad es de tipo IEnumerable<T>, se reemplaza por una instancia nueva. Si es una colección concreta que se hereda de List<T> o ICollection, entonces MEF llamará Clear y luego Add para cada elemento. En cualquier caso, significa que tendrá que considerar la seguridad de subprocesos al usar Recomposition. La recomposición no sólo se relaciona con lo que se agrega, sino también con lo que se elimina. Si los catálogos se eliminan del contenedor, esas partes también se eliminarán.
Composición estable, rechazo y diagnóstico
Algunas veces una parte puede especificar que falta una importación, ya que no está presente en el catálogo. Cuando esto pasa, MEF evita que se detecte la parte que pierde la dependencia (o cualquier cosa que depende de ella). MEF hace esto para estabilizar el sistema y evitar errores de tiempo de ejecución que de seguro ocurrirían si se creara la parte.
Aquí, SalesOrderView se ha cambiado para importar un ILogger aunque no hay ninguna instancia de registrador presente:
[ExportView(ViewType = ViewTypes.SalesOrder)]
public partial class SalesOrderView : UserControl, IView
{
[Import]
public ILogger Logger { get; set; }
}
Como no hay ninguna exportación ILogger disponible, la exportación de SalesOrderView no aparecerá en el contenedor. Esto no arrojará una excepción; en su lugar, SalesOrderView simplemente se ignorará. Si revisa la colección Views de ViewFactory, estará vacía.
También se producirá rechazo en casos en que haya varias exportaciones disponibles para una importación única. En tales situaciones, se rechaza la parte que importa la exportación única.
[ExportView(ViewType = ViewTypes.SalesOrder)]
public partial class SalesOrderView : UserControl, IView
{
[Import]
public ILogger Logger { get; set; }
}
[Export(typeof(ILogger))]
public partial class Logger1 : ILogger
{
}
[Export(typeof(ILogger))]
public partial class Logger2 : ILogger
{
}
En el ejemplo anterior, SalesOrderView se rechazará porque hay varias implementaciones de Ilogger, pero se importa una implementación única. MEF sí proporciona instalaciones para permitir una exportación predeterminada en la presencia de varias. Para obtener más información acerca de esto, consulte codebetter.com/blogs/glenn.block/archive/2009/05/14/customizing-container-behavior-part-2-of-n-defaults.aspx.
Podría preguntar por qué MEF no crea SalesOrderView y arroja una excepción. En un sistema abierto extensible, si MEF arroja una excepción, sería muy difícil que la aplicación la controlara o que tuviera el contexto para saber qué hacer, porque la parte podría faltar o la importación podría anidarse muy profundamente en la composición. Sin el control adecuado, la aplicación estaría en un estado no válido y no se podría usar. MEF rechaza la parte, por tanto se mantiene la garantía de estabilidad de la aplicación. Para obtener más información acerca de la composición estable, consulte: blogs.msdn.com/gblock/archive/2009/08/02/stable-composition-in-mef-preview-6.aspx.
Diagnóstico del rechazo
El rechazo es una característica muy eficaz, pero a veces puede ser difícil de diagnosticar, sobre todo cuando se rechaza todo el gráfico de dependencia. En el primer ejemplo, ViewFactory importa directamente un SalesOrderView. Digamos que MainWindow importó ViewFactory y SalesOrderView se rechazó. Entonces ViewFactory y MainWindow también se rechazarán. Se puede rascar la cabeza si ve que ocurre esto, ya que sabe que MainWindow y ViewFactory en realidad están presentes; el motivo del rechazo es una dependencia que falta.
MEF no lo deja a oscuras. Para ayudar a diagnosticar este problema, proporciona seguimiento. En el IDE, se hace un seguimiento a todos los mensajes de rechazo hasta la ventana de resultados, aunque también se pueden seguir hasta cualquier escucha de seguimiento. Por ejemplo, cuando la aplicación intenta importar MainWindow, los mensajes de seguimiento en la Figura 3 se arrojan como resultado.
Figura 3 Mensajes de seguimiento de MEF
System.ComponentModel.Composition Warning: 1 : The ComposablepartDefinition 'Mef_MSDN_Article.SalesOrderView' has been rejected. The composition remains unchanged. The changes were rejected because of the following error(s): The composition produced a single composition error. The root cause is provided below. Review the CompositionException.Errors property for more detailed information.
1) No valid exports were found that match the constraint '((exportDefinition.ContractName == "Mef_MSDN_Article.ILogger") AndAlso (exportDefini-tion.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "Mef_MSDN_Article.ILogger".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity"))))', invalid exports may have been rejected.
Resulting in: Cannot set import 'Mef_MSDN_Article.SalesOrderView.Logger (ContractName="Mef_MSDN_Article.ILogger")' on part 'Mef_MSDN_Article.SalesOrderView'.
Element: Mef_MSDN_Article.SalesOrderView.logger (ContractName="Mef_MSDN_Article.ILogger") -->Mef_MSDN_Article.SalesOrderView -->TypeCatalog (Types='Mef_MSDN_Article.MainWindow, Mef_MSDN_Article.SalesOrderView, ...').
El resultado de seguimiento muestra la causa raíz del problema: SalesOrderView requiere un ILogger y uno no se puede localizar. Luego podemos ver que rechazarlo provocó que la fábrica se rechazara y finalmente a MainWindow.
Inspección de partes en el depurador
Puede ir un paso más adelante y realmente inspeccionar las partes disponibles en el catálogo, lo cual discutiré en la sección acerca de hospedaje. En la Figura 4 puede ver en la ventana de inspección las partes disponibles (en los círculos verdes) así como la importación ILogger requerida (en el círculo azul).
Figura 4 Partes disponibles e ILogger requerido mostrados en una ventana de inspección
Diagnóstico del rechazo en la línea de comandos
Uno de los objetivos de MEF era apoyar la capacidad estática de análisis, para así permitir que la composición se analizara fuera del entorno de tiempo de ejecución. Aún no contamos con tal soporte técnico de herramientas en Visual Studio, sin embargo, Nicholas Blumhardt creó MEFX.exe (mef.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=33536), una herramienta de línea de comandos que sirve bastante. MEFX analiza ensamblados y determina qué partes se rechazan y por qué.
Si ejecuta MEFX.exe en la línea de comandos, verá un host de opciones; puede hacer una lista de importaciones específicas, exportaciones o todas las partes disponibles. Por ejemplo, aquí puede ver el uso de MEFX para mostrar la lista de partes:
C:\mefx>mefx.exe /dir:C:\SalesOrderManagement\bin\debug /parts
SalesOrderManagement.SalesOrderView
SalesOrderManagement.ViewFactory
SalesOrderManagement.MainWindow
Esto es útil para obtener un inventario de partes, pero MEFX también puede localizar rechazos, que es lo que nos interesa acá, tal como se muestra en la Figura 5.
Figura 5 Localización de rechazos con MEFX.exe
C:\mefx>mefx.exe /dir:C:\SalesOrderManagement\bin\debug /rejected /verbose
[part] SalesOrderManagement.SalesOrderView from: DirectoryCatalog (Path="C:\SalesOrderManagement\bin\debug")
[Primary Rejection]
[Export] SalesOrderManagement.SalesOrderView (ContractName="SalesOrderManagement.IView")
[Export] SalesOrderManagement.SalesOrderView (ContractName="SalesOrderManagement.IView")
[Import] SalesOrderManagement.SalesOrderView.logger (ContractName="SalesOrderManagement.ILogger")
[Exception] System.ComponentModel.Composition.ImportCardinalityMismatchException: No valid exports were found that match the constraint '((exportDefinition.ContractName == "SalesOrderManagement.ILogger") AndAlso (exportDefinition.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "SalesOrderManagement.ILogger".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity"))))', invalid exports may have been rejected.
at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition, AtomicCompositionatomicComposition)
at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition)
at Microsoft.ComponentModel.Composition.Diagnostics.CompositionInfo.AnalyzeImportDefinition(ExportProvider host, IEnumerable`1 availableparts, ImportDefinition id)
Analizar los resultados de la Figura 6 revela la raíz del problema. ILogger no se puede localizar. Como puede ver, en sistemas grandes con muchas partes, MEFX es una herramienta invaluable. Para obtener más información acerca de MEFX, consulte blogs.msdn.com/nblumhardt/archive/2009/08/28/analyze-mef-assemblies-from-the-command-line.aspx.
Figura 6 Ejemplo de parte en IronRuby
Resumiendo, el modelo atributivo tiene varias ventajas:
- Proporciona una manera universal para que las partes declaren sus exportaciones e importaciones.
- Permite que los sistemas detecten de manera dinámica partes disponibles en vez de requerir un registro previo.
- Es estadísticamente analizable, al permitir que herramientas como MEFX determinen errores por adelantado.
Ahora haré un breve repaso de la arquitectura y veré lo que permite. A alto nivel, la arquitectura de MEF se subdivide en capas: Modelos de programación, hospedaje y elementos primitivos.
Repaso de los modelos de programación
El modelo atributivo es simplemente una implementación de esos elementos primitivos que usa atributos como medios de detección. Los elementos primitivos pueden representar partes no atributivas o partes iguales que no se escriben estadísticamente, como en Dynamic Language Runtime (DLR). En la Figura 6 puede ver una parte IronRuby que exporta una IOperation. Tenga en cuenta que usa sintaxis nativa de IronRuby para declarar una parte en vez del modelo atributivo, ya que los atributos no son compatibles con DLR.
MEF no se envía con un modelo de programación IronRuby, aunque es probable que agreguemos soporte técnico de lenguaje dinámico a futuro.
Puede leer más acerca de experimentos para crear un modelo de programación Ruby en esta serie de blog: blogs.msdn.com/nblumhardt/archive/tags/Ruby/default.aspx.
Hospedaje: donde ocurre la composición
Los modelos de programación definen partes, importaciones y exportaciones. A fin de crear realmente instancias y gráficos de objetos, MEF proporciona API de hospedaje ubicadas principalmente en el espacio de nombres System.ComponentModel.Composition.Hosting. La capa de hospedaje ofrece mucha flexibilidad, capacidad de configuración y extensibilidad. Es el lugar donde se realiza gran parte del “trabajo” en MEF y donde comienza la detección en MEF. La mayoría de las personas son simplemente partes creadoras que nunca tocarán este espacio de nombres. Si es un proveedor de servicios de hosting, sin embargo, los usará como lo hacía yo antes para arrancar la composición.
Los catálogos proporcionan definiciones de partes (ComposablepartDefinition), que describen las exportaciones e importaciones disponibles. Son la unidad principal para detección en MEF. MEF proporciona varios catálogos en el espacio de nombres System.ComponentModel.Composition, algunos de los cuales ya vio, incluido DirectoryCatalog, el cual explora un directorio; AssemblyCatalog, que explora un ensamblado; y TypeCatalog, que explora a través de un conjunto específico de tipos. Cada uno de estos catálogos es específico al modelo de programación atributivo. AggregateCatalog, sin embargo, es independiente de los modelos de programación. Los catálogos heredan de ComposablepartCatalog y son un punto de extensibilidad en MEF. Los catálogos personalizados tienen varios usos, desde proporcionar un modelo de programación completamente nuevo a contener y filtrar catálogos existentes.
En la Figura 7 se muestra un ejemplo de un catálogo filtrado, el cual acepta que un predicado se filtre contra el catálogo interno desde el que se devolverán las partes.
Figura 7 Catálogo filtrado
public class FilteredCatalog : ComposablepartCatalog,
{
private readonly composablepartcatalog _inner;
private readonly IQueryable<ComposablepartDefinition> _partsQuery;
public FilteredCatalog(ComposablepartCatalog inner,
Expression<Func<ComposablepartDefinition, bool>> expression)
{
_inner = inner;
_partsQuery = inner.parts.Where(expression);
}
public override IQueryable<ComposablepartDefinition> parts
{
get
{
return _partsQuery;
}
}
}
CompositionContainer compone, es decir, crea partes y satisface importaciones de esas partes. Al satisfacer las importaciones, echará mano al grupo de exportaciones disponibles. Si esos expertos también tienen importaciones, el contenedor los satisfará primero. De esta manera, el contenedor ensambla todos los gráficos de objetos a pedido. La fuente principal para el grupo de exportaciones es un catálogo, pero el contenedor también puede agregarle y componer instancias de partes existentes directamente. Es habitual agregar manualmente la clase de punto de entrada al contenedor, combinada con partes extraídas del catálogo, aunque en la mayoría de los casos las partes provendrán del catálogo.
Los contenedores también se pueden anidar en una jerarquía para respaldar escenarios de control. Los contenedores secundarios consultarán al contenedor principal de maneta predeterminada, pero también pueden proporcionar sus propios catálogos de partes secundarias, las cuales se crearán dentro del contenedor secundario:
var catalog = new DirectoryCatalog(@".\");
var childCatalog = new DirectoryCatalog(@".\Child\";
var rootContainer = new CompositionContainer(rootCatalog));
var childContainer = new CompositionContainer(childCatalog,
rootContainer);
En el código anterior, childContainer se dispone como elemento secundario de rootContainer. rootContainer y childContainer proporcionan sus propios catálogos. Para obtener más información acerca del uso del contenedor para hospedar MEF dentro de sus aplicaciones, consultecodebetter.com/blogs/glenn.block/archive/2010/01/15/hosting-mef-within-your-applications.aspx.
Elementos primitivos: donde naces las partes y los modelos de programación
Los elementos primitivos ubicados en System.ComponentModel.Composition.Primitives son el nivel más bajo de MEF. Son el universo cuántico de MEF, por así decir, y su punto de uber-extensibilidad. Hasta ahora, he abarcado el modelo de programación atributivo. El contenedor de MEF, sin embargo, no está en absoluto enlazado a atributos; en su lugar, está enlazado con los elementos primitivos. Estos elementos definen una presentación abstracta de las partes, la cual incluye definiciones como ComposablepartDefinition, ImportDefinition y ExportDefinition, así como Composablepart y Export, las cuales representan instancias reales.
Explorar los elementos primitivos es un viaje en todo el sentido de la palabra y uno que probablemente haga en un próximo artículo. Por ahora, puede encontrar más detalles acerca de esto en blogs.msdn.com/dsplaisted/archive/2009/06/08/a-crash-course-on-the-mef-primitives.aspx.
MEF en Silverlight 4… y más allá
MEF también se envía como parte de Silverlight 4. Todo lo que analicé aquí es pertinente para el desarrollo Aplicaciones de Internet enriquecidas extensibles. En Silverlight, hemos ido más allá e introducido API adicionales para facilitar la experiencia de crear aplicaciones en MEF. Estas mejoras en definitiva se implementarán en .NET Framework.
Puede obtener más información acerca de MEF en Silverlight 4 en esta publicación: codebetter.com/blogs/glenn.block/archive/2009/11/29/mef-has-landed-in-silverlight-4-we-come-in-the-name-of-extensibility.aspx.
Sólo he repasado por encima lo que puede hacer con MEF. Es una herramienta eficaz, sólida y flexible que puede agregar a su arsenal para ayudar a abrir sus aplicaciones a un mundo de posibilidades completamente nuevo. Estoy ansioso por ver qué hará con él.
Glenn Block es PM para el nuevo Managed Extensibility Framework (MEF) en .NET Framework 4. Antes de MEF, era planeador de productos en patrones y prácticas, responsable de la orientación de Prism así como de otros clientes. Block es un adicto a la informática de corazón y pasa gran parte de su tiempo difundiendo su universo a través de conferencias y grupos como ALT.NET. Lea su blog en codebetter.com/blogs/glenn.block.
Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Ward Bell, Nicholas Blumhardt, Krzysztof Cwalina, Andreas Håkansson, Krzysztof Kozmic, Phil Langeberg, Amanda Launcher, Jesse Liberty, Roger Pence, Clemens Szypierski, Mike Taulty, Micrea Trofin y Hamilton Verissimo