January 2009 - Artículos

Los que sigais mi blog ya habreis visto que últimamente comento algunas cosillas sobre PRISM, la librería para crear aplicaciones compuestas en WPF.

En este post, pero no quiero hablar de PRISM y WPF, sino sobre si es posible aprovechar PRISM para la creación de aplicaciones compuestas usando Winforms. Recordad que en Winforms ya tenemos una solución completa para la creación de aplicaciones compuestas: CAB  y SCSF. Ezequiel publicó hace tiempo un post lleno de enlaces sobre CAB y SCSF. Echadle una ojeada si os interesa el tema.

Aunque tengamos CAB y SCSF para crear nuestras aplicaciones compuestas en Winforms es lícito que nos planteemos si PRISM es una solución que se adapta a nuestras necesidades: Es más ligero que CAB y sus partes están menos acopladas. P.ej. si usamos CAB estamos atados forzosamente al ObjectBuilder, mientras que con PRISM el contenedor IoC que queramos usar lo podemos escojer. Además, los que hayan programado en CAB sabrán que tiene algunos comportamientos extraños, que aumentan mucho su curva de aprendizaje… si a todo esto le sumamos que PRISM es la futura guia de aplicaciones composite, quizá nos interese ver si podemos empezar a aplicarla ya en nuestras aplicaciones Winforms.

Voy a intentar dar una respuesta en este post, pero antes que nada un disclaimer: las aplicaciones winforms que creemos usando PRISM tendrán dependencias a assemblies de WPF, por lo que serán aplicaciones winforms sólo para el framework 3. Es posible usar PRISM sin ninguna dependencia a WPF, pero se pierde parte de su funcionalidad.

1. El punto de partida

El punto de partida, como muchas cosas en esta vida, la da Google. Buscando por Winforms y PRISM uno llega a un post Brian Noyes: Composite Extensions for Windows Forms. En este post Brian comenta el uso básico de PRISM para Winforms. Partiremos de su post y lo extendremos un poco.

Asumo que os habeis leído su post, y que os habeis descargado de su blog el proyecto. Si no, yo he puesto una copia tal cual (ver enlaces al final del post). Para compilar el proyecto de Brian, primero debeis compilar la solución CompositeApplicationLibrary.sln (dentro del directorio CAL), para generar PRISM. Para poder compilarlos necesitareis tener los assemblies de Unity (Microsoft.Practices.Unity.dll y Microsoft.Practices.ObjectBuilder2.dll). Unity os lo podeis descargar de la página de Unity en Codeplex.

Una vez tengais los assemblies de PRISM generados podeis abrir la solución CompositeExtensions.sln y usais los assemblies de PRISM para colocar las referencias que falten. Compilais la solución y ya tendreis las extensiones de Brian para PRISM.

2. Explorando el punto de partida

Las extensiones de Brian para PRISM, generan dos assembiles nuevos (más dos adicionales de tests unitarios):

  1. CompositeExtensions.Unity.dll: Contiene un bootstrapper nuevo (SimpleUnityBootstrapper) que nos permite crear aplicaciones winforms.
  2. CompositeExtensions.dll: Contiene un port a winforms del sistema de eventos de PRISM.

Ambas extensiones están totalmente libres de cualquier referencia a WPF. Para desarrollar una aplicación con las extensiones de Brian, simplemente es necesario crear un Bootstrapper derivado de SimpleUnityBootstrapper, registrar en Unity el control que queramos utilizar como “contenedor” de nuestras vistas, y en los módulos recuperar el  control “contenedor” y añadir en él los controles hijos. El post de Brian lo explica paso por paso y hay una aplicación de demo (muy simple) que os recomiendo que la mireis bien. Porqué ahora vamos a empezar a extender el trabajo de Brian.

3. Añadiendo soporte para regiones

Las extensiones de Brian están bien, pero no hay soporte para el concepto de regiones… Supongo que es deliberado, ya que las interfaces y clases que debemos usar tienen dependencias contra WPF, pero supongamos que eso no nos importa y vayamos a ver como añadir soporte para regiones windows forms en PRISM.

3.1. Nuestro Region Adapter

La clave es tener nuestro propio RegionAdapter que sea capaz de interaccionar con un contenedor Winforms (en este caso un Control). Crear un RegionAdapter es muuuuy fácil: basta con derivar de la clase RegionAdapter<T> (siendo T el tipo de contenedor) y redefinir CreateRegion() y Adapt(). En el primer método debemos devolver el tipo de region PRISM que queremos. En el segundo debemos “adaptar” los contenidos de la región al contenedor usado.

Veamos el código y estará todo mucho más claro:

public class ControlRegionAdapter : RegionAdapterBase<Control>
{
    protected override IRegion CreateRegion()
    {
        return new AllActiveRegion();
    }
    protected override void Adapt(IRegion region, Control regionTarget)
    {
        region.ActiveViews.CollectionChanged += delegate
        {
            regionTarget.Controls.Clear();
            foreach (object co in region.ActiveViews)
            {
                if (co is Control)
                {
                    regionTarget.Controls.Add((Control)co);
                }
            }
        };
    }
}

En el método CreateRegion devolvemos una AllActiveRegion, que es una región de PRISM que entiende que todas sus vistas son activas.

En el método Adapt, hacemos que cada vez que cambie la colección de ActiveViews de la región, nos coloque las vistas activas dentro de la colección Controls del contenedor. En este punto (al usar CollectionChanged) es cuando nos aparece la referencia contra el assembly de WPF WindowsBase.dll.

3.2 Indicar a PRISM que use nuestro nuevo Region Adapter

Para indicar a PRISM que use un Region Adapter deteminado, debemos usar la clase RegionAdapterMappings, y añadir el mapping correspondiente. Un mapping le indica a PRISM que RegionAdapter usar para cada tipo de contenedor de región.

En este punto deberemos modificar las extensiones de Brian: él no usa el concepto de regiones, así que no crea ningún RegionAdpaterMappings inicial. Para hacerlo deberemos modificar la clase SimpleUnityBootstrapper. En el método ConfigureContainer, dentro del if (_useDefaultConfiguration) añadimos la línea:

RegisterTypeIfMissing(typeof(RegionAdapterMappings),
typeof(RegionAdapterMappings), true);

Con esto hacemos que al crear el contenedor Unity, se cree un singleton de tipo RegionAdapterMappings. La clase RegionAdapterMappings está dentro del assembly Microsoft.Practices.Composite.Wpf.dll de PRISM, por lo que debereis añadir la referencia.

El siguiente paso es añadir un método protected virtual en la misma clase SimpleUnityBootstrapper:

protected virtual RegionAdapterMappings ConfigureMappings() 
{
    return Container.Resolve<RegionAdapterMappings>();
}

Y finalmente lo llamamos desde el método Run del propio SimpleUnityBootstrapper. Justo después de la llamada a ConfigureContainer(), llamamos a ConfigureMappings(). Con ello hemos modificado el bootstrapper inicial de Brian para que cree un RegionAdapterMappings (vacío) y nos de un punto de extensión (ConfigureMappings) donde nosotros podamos añadir nuestros propios mappings.

Ahora en nuestra clase bootstrapper podemos hacer un override del método ConfigureMappings y añadir nuestro mapping para que use la clase ControlRegionAdapter que hemos definido antes:

protected override RegionAdapterMappings ConfigureMappings()
{
    RegionAdapterMappings mappings = base.ConfigureMappings();
    mappings.RegisterMapping(typeof(Control), new ControlRegionAdapter());
    return mappings;
}

Con esto indicamos a PRISM que use nuestro Region Adapter cuando añadamos elementos a una región cuyo contenedor sea un Control.

3.3 Crear el RegionManager

Para poder crear Regiones, necesitamos crear un RegionManager. Para ello, lo más fácil és modificar, de nuevo, el SimpleUnityBootstrapper de Brian para que nos cree un RegionManager. Otra vez dentro del método ConfigureContainer, dentro del mismo if (_useDefaultConfiguration) añadimos la línea para que nos registre el RegionManager:

RegisterTypeIfMissing(typeof(IRegionManager), 
typeof(RegionManager), true);

Ahora ya tenemos un RegionManager de PRISM listo para usar… Ya sólo nos queda crear una región.

3.4 Crear una región

En WPF se pueden definir las regiones usando XAML, pero en winforms no tenemos nada parecido, así que vamos a hacerlo programáticamente. Por suerte el RegionManager tiene un método AttachNewRegion que nos va a servir para ello.

Primero, modificaremos de nuevo el SimpleUnityBootstrapper de Brian para añadir un método virtual:

protected virtual void AttachInitialRegions() { }

Y lo llamamos desde el método Run, justo después de la llamada a ConfigureMappings que añadimos antes.

Ahora, podemos volver a nuestro bootstrapper y hacer el override para crear la región inicial:

protected override void AttachInitialRegions()
{
    base.AttachInitialRegions();
    this.Container.Resolve<IRegionManager>().
        AttachNewRegion(this.Shell.MainRegionContainer,"Main");
}

Asumid que this.Shell es una referencia al formulario principal de la aplicación y que la propiedad MainRegionContainer me devuelve un Control que es el contenedor de la región.

4. Un módulo… para hacer algo

Las aplicaciones PRISM se componen de módulos (a la práctica objectos que implementan IModule) que colaboran entre ellos. Cada módulo se encarga de implementar parte de la aplicación, generalmente añadiendo vistas a las regiones existentes o bien añadiendo regiones nuevas.

Vamos a crear un módulo realmente simple, que añada una vista a nuestra región.

Para ello, añadimos una clase nueva que implemente IModule. El código puede ser como el siguiente:

public class Module1 : IModule
{
    private IUnityContainer container;
    private IRegionManager regionManager;

    public Module1(IUnityContainer ctl, IRegionManager rm)
    {
        this.container = ctl;
        this.regionManager = rm;
    }

    public void Initialize()
    {
        this.RegisterViewsAndServices();
        this.regionManager.Regions["Main"].
            Add(container.Resolve<IView1>());
    }

    private void RegisterViewsAndServices()
    {
        this.container.RegisterType<IView1, View1>();
    }
}

En el constructor recibimos el contenedor Unity y el RegionManager a usar. Los módulos los crea PRISM y ambos parámetros del constructor son suministrados via Dependency Injection por Unity.

El método Initialize() es el único método que declara IModule, y es donde tenemos que hacer todo el trabajo. En este caso llamamos a un método propio (RegisterViewsAndServices) que indica que cuando alguien pida un objeto IView1, devuelva un View1. Finalmente, accedemos al RegionManager y añadimos una instancia nueva de IView1.

Que son IView1  y View1? IView1 es una interfaz, los métodos de la cual no tienen importancia (de hecho, en mi ejemplo está vacía). View1 es la vista a añadir: un UserControl cuyo contenido puede ser el que se desee.

Finalmente sólo queda hacer que nuestro bootstrapper cargue el módulo. Para ello usamos el StaticModuleEnumerator para cargar e inicializar el módulo:

protected override IModuleEnumerator GetModuleEnumerator()
{
    return new StaticModuleEnumerator().
        AddModule(typeof(Module1));
}

Y listos! Con ello nuestra aplicación PRISM funcionando en Windows Forms está lista!

Aquí os dejo el siguiente código de ejemplo:

  1. Extensiones de Brian Noyes originales.
  2. Modificaciones a las extensiones de Brian Noyes (no incluye ni su aplicación de demo, ni tests ni el código de PRISM).
  3. Aplicación PRISM en Windows Forms usando regiones.

¡Espero que os sirva!

Saludos!

con 2 comment(s)
Archivado en:

Una aplicación PRISM se compone de varios módulos que colaboran entre ellos. Un módulo PRISM simplemente es un objeto que implementa la interfaz IModule. En un mismo assembly pueden haber tantos módulos PRISM como se desee.

PRISM ofrece dos métodos para la carga de los módulos: O bien se cargan todos al principio de la aplicación, o bien se cargan on-demand (es decir, cuando se necesitan). La primera opción es la más simple, pero en algunos casos nos interesa ir cargando los módulos cuando se necesiten (bien porque hay muchos módulos posibles o bien porque la inicialización de estos módulos es un poco pesada).

Igualmente los módulos pueden tener dependencias entre ellos: si el módulo A depende del módulo B, indica que el módulo B debe estar cargado cuando se cargue el módulo A. En caso contrario PRISM lanzará una excepción con el mensaje “A module declared a dependency on another module which is not declared to be loaded”.

Los módulos se cargan mediante dos clases: el IModuleEnumerator, que enumera los módulos existentes y el IModuleLoader que los carga e inicializa. Existen varias implementaciones de esta clases y nosotros nos podemos crear las nuestras. En función del IModuleEnumerator que usemos debemos usar un mecanismo u otro para indicar que el módulo no se carga por defecto, sino que se cargará on-demand. P.ej. si usamos el DirectoryLookupModuleEnumerator (que enumera todos los módulos de todos los assemblies de un directorio en particular), debemos decorar el módulo con el atributo Module con la propiedad StartupLoaded a false:

[Module(ModuleName = ModuleNames.MARKETPLACE_MODULE, 
StartupLoaded = false)] public class MarketModule : IModule { }

Para cargar un módulo on-demand debemos obtenerlo mediante el IModuleEnumerator y cargarlo mediante el IModuleLoader (usando el método Initialize):

ModuleInfo mi = Container.Resolve<IModuleEnumerator>().
GetModule(ModuleNames.MARKETPLACE_MODULE); Container.Resolve<IModuleLoader>().Initialize(new ModuleInfo[] { mi });

(Container es la propiedad que me da acceso al contenedor IoC usado, que me permite obtener el IModuleEnumerator y el IModuleLoader).

De forma similar a como indicamos que un módulo se cargará on-demand podemos especificar que un módulo depende de otro. La forma exacta de hacerlo depende de nuevo del IModuleEnumerator usado. Si usamos el DirectoryLookupModuleEnumerator debemos decorar la clase módulo con el atributo ModuleDependency indicando de que módulo depende dicho módulo:

[Module(ModuleName = ModuleNames.MARKETPLACE_MODULE, 
StartupLoaded = false)] [ModuleDependency(ModuleNames.CARDS_MODULE)] public class MarketModule : IModule { }

El módulo MARKETPLACE_MODULE depende del módulo CARDS_MODULE: el segundo debe estar cargado antes de cargar el primero. Así pues es de esperar que el IModuleLoader cuando cargue el módulo MARKETPLACE_MODULE cargue también el módulo CARDS_MODULE…

… pues no. El IModuleLoader no cargará automáticamente el módulo CARDS_MODULE, en su lugar si no está cargado lanzará la excepción previamente comentada.

Vosotros debeis saber que dependencias tiene cada módulo y aseguraros que cada módulo está cargado. Es decir, en mi caso yo debo cargar CARDS_MODULE antes que MARKETPLACE_MODULE, o como muy tarde a la vez:

ModuleInfo mc = Container.Resolve<IModuleEnumerator>().
GetModule(ModuleNames.CARDS_MODULE); ModuleInfo mm = Container.Resolve<IModuleEnumerator>().
GetModule(ModuleNames.MARKETPLACE_MODULE); Container.Resolve<IModuleLoader>().
Initialize(new ModuleInfo[] { mc, mm });

Por suerte es posible un workaround para no tener que ir buscando todas las dependencias: hacerse un método de extensión sobre IModuleEnumerator, y que devuelva un array de ModuleInfo: el módulo junto con todas sus dependencias:

namespace Microsoft.Practices.Composite.Modularity
{
    public static class ModuleEnumeratorExtensions
    {
        public static ModuleInfo[] GetModuleWithDependencies(
this IModuleEnumerator moduleEnumerator, string moduleName) { List<ModuleInfo> moduleInfoList = new List<ModuleInfo>(); ModuleInfo module = moduleEnumerator.GetModule(moduleName); moduleInfoList.Add(module); if (module.DependsOn != null) { foreach (string dependencyName in module.DependsOn) { if (!moduleInfoList.Exists(
existingModule => existingModule.ModuleName ==
dependencyName)) { moduleInfoList.AddRange(
GetModuleWithDependencies(
moduleEnumerator, dependencyName)); } } } return moduleInfoList.ToArray(); } } }

Y para cargar un módulo junto con todas sus dependencias:

ModuleInfo[] mis = Container.Resolve<IModuleEnumerator>().
GetModuleWithDependencies(ModuleNames.MARKETPLACE_MODULE);
Container.Resolve<IModuleLoader>().Initialize(mis);

Es una de esas cosas que uno se pregunta porque no lo habrán añadido de serie… :)

Nota: La información de este post está sacada de este post de Mariano Converti. Como es habitual, todo el mérito para él… Su blog sobre PRISM es de imprescindible consulta!

con no comments
Archivado en: ,

Hola a todos!

Conocéis PRISM? Viene a ser, salvando las distancias, la CAB de WPF: es decir un conjunto de buenas prácticas para la creación de aplicaciones compuestas en WPF y una librería que implementa dichas buenas prácticas. Si desarrollais aplicaciones en WPF es obligatorio echarle un vistazo. Pasaos por la página de PRISM en codeplex.

Por otro lado, AvalonDock es una muy buena librería que proporciona soporte para interfaces dockables usando WPF que simula al estilo de docking de Visual Studio.

Estoy desarrollando una aplicación usando ambas librerías y me he encontrado con un problemilla: al añadir una vista usando PRISM dentro de un contenedor de AvalonDock aparece un error. A ver, que me explico un poco mejor… :)

PRISM usa el concepto de “regiones” para dividir el espacio de la ventana de la aplicación. En cada “región” se pueden incrustar una o más vistas (objetos que se representan visualmente). Por ejemplo podemos mapear una región de PRISM a un ItemsControl y cada vista que añadamos aparecerá dentro de este ItemsControl. Para mapear una región PRISM a un control se usa XAML:

<ItemsControl Grid.Row="0" cal:RegionManager.RegionName="HelpZone" />

Por su lado AvalonDock se basa en proporcionar un contenedor especial (el DockingManager) dentro del cual se insertan otros contenedores especiales que contienen el contenido a mostrar… el cual tiene que ser un objeto de unas clases especiales, llamadas DockableContent o DocumentContent. Estas clases son las que realmente contienen el contenido real. P.ej. para mostrar una cadena dentro de una ventana dockable usando AvalonDock necesito el siguiente código XAML:

<ad:DockingManager Name="mainDockingManager" Grid.Row="1">
  <ad:DocumentPane>
     <ad:DockableContent>Hola AvalonDock</ad:DockableContent>
  </ad:DocumentPane>
</ad:DockingManager>

La clase DocumentPane contiene tantos DockableContent como ventanas dockables se quieran tener.

Al mezclar PRISM y AvalonDock es cuando surgen los primeros problemas. Si definimos una región dentro del DocumentPane:

<ad:DockingManager Name="mainDockingManager" Grid.Row="1">
  <ad:DocumentPane cal:RegionManager.RegionName="MainZone" />
</ad:DockingManager>

 

Cuando añadamos una vista, no dará una excepción: DocumentPane can contain only DockableContents or DocumentContents!

Esto ocurre porque PRISM intenta asociar como contenido del DocumentPane la vista que directamente le hemos indicado, que será un UserControl o algún otro objeto pero no un DockableContent o un DocumentContent.

Lo que hemos de conseguir es que PRISM, de forma transparente para nosotros, nos cree un DockableContent (o un DocumentContent) y nos lo añada al contenido del DocumentPane al cual está mapeado nuestra región de PRISM. Por suerte,  en PRISM tenemos el concepto de RegionAdapter, que como su nombre indica es una clase que “adapta” los contenidos de una región de PRISM al contenedor real (nuestro DocumentPane). Vamos a ver como podemos implementar un RegionAdapter para DocumentPane.

El código quedaría más o menos así:

public class DockableRegionAdapter : RegionAdapterBase<DocumentPane>
{
    protected override IRegion CreateRegion()
    {
        return new AllActiveRegion();
    }

    protected override void Adapt(IRegion region, 
DocumentPane regionTarget) { region.ActiveViews.CollectionChanged += delegate { var childs = new Dictionary<object, DockableContent> (); foreach (var child in regionTarget.Items) { if (child is DockableContent) { childs.Add(((DockableContent)child).Content, (DockableContent)child); } } regionTarget.Items.Clear(); foreach (object ci in region.Views) { DockableContent dc = childs.ContainsKey(ci)
? childs[ci] : new DockableContent() { Content = ci }; regionTarget.Items.Add(dc); } }; } }

Los dos métodos que hay que redefinir cuando se crea un RegioAdapter de PRISM son CreateRegion y Adapt. En el primero simplemente hemos de devolver el tipo de región que queremos. En este caso simplemente creo un objeto AllActiveRegion, que es un región de PRISM que todas las vistas que tenga las considera activas.

El segundo método es Adapt, y es donde se hace todo el trabajo. Cada vez que cambie la colección ActiveViews, básicamente borro el contenido del DocumentPane de AvalonDock y lo añado de nuevo, creando un DockableContent nuevo para cada vista que haya en la región. El código que rellena el diccionario childs es para reutilizar aquellos DockableContent que se hubiesen creado anteriormente.

Finalmente solo nos queda informar a PRISM que tenemos un RegionAdapter nuevo. Para ello redefinimos el método ConfigureRegionAdapterMappings del Bootstrapper para añadir nuestro RegionAdapter vinculado a contenedores de tipo DocumentPane:

protected override RegionAdapterMappings 
    ConfigureRegionAdapterMappings()
{
    RegionAdapterMappings mappings = 
        base.ConfigureRegionAdapterMappings();
    mappings.RegisterMapping(typeof(DocumentPane), 
        new DockableRegionAdapter());
    return mappings;
}

Y listos! Ahora al añadir una vista a la región de PRISM, se crea automáticamente un DockableContent y se añade al DocumentPane que contiene la región, lo que añade una ventana dockable con la nueva vista en la interfaz de usuario.

No puedo asegurar que sea la mejor implementación posible pero... a mi me funciona ;-)

¡Bien por PRISM!

con 2 comment(s)
Archivado en: ,

Hola! ¿Que tal os sienta el 2009? Espero que lo mejor posible :)

Hoy un post cortito para comentar un problemilla y su solución.

El problemilla es que al intentar realizar DataBinding desde un PasswordBox no funciona, porque la propiedad Password, no es una DependencyProperty.

Es decir, mientras que esto funciona y liga la propiedad Text a la propiedad Login del DataContext:

<TextBox Grid.Column="1" x:Name="txtLogin"  VerticalAlignment="Center"
Text="{Binding Login}" />

esto no funciona:

<PasswordBox Grid.Row="1" Grid.Column="1" x:Name="txtPassword" VerticalAlignment="Center" 
Password="{Binding Password}" />

Ya que la propiedad Password no es una DependencyProperty.

¿La solución? Añadir una propiedad enlazada que sí que sea una DependencyProperty y que tenga el mismo valor que la propiedad Password.

La solución completa la podeis encontrar en este post de Functional Fun: WPF PasswordBox and Data binding. Todo el mérito es suyo, yo sólo comparto el post, puesto que me ha parecido muy interesante.

¡Saludos!

con no comments
Archivado en: