PRISM y Winforms: Mostrar vistas en nuevos formularios

En un post anterior (PRISM y Winforms), comentaba como usar PRISM para realizar aplicaciones Winforms.

Un comentario de Jose en esta entrada, me ha motivado a escribir el siguiente post, para mostrar como podríamos mostrar vistas en regiones que estén incrustadas no en un UserControl (típicamente un Panel) de la ventana principal, sino incrustadas en un nuevo formulario.

Para poder usar regiones en Winforms era necesario definirnos un RegionAdapter para la clase “Control” que era básicamente el objetivo del post anterior. En el método Adapt() teníamos el código que “incrustaba” la vista dentro del control. Dicho método recibía (de PRISM) la región y el target, o control donde colocar dicha región.

Antes que nada recordad tres conceptos que a veces se confunden:

  1. Region: En PRISM una región es un conjunto de vistas (algunas activas, otras no activas) que se muestran en algún sitio determinado.
  2. Target: El target de una región es el lugar donde se muestran. P.ej. un TableLayout podría ser el target de una región y cada vista de la región podría mostrarse en distintas celdas.
  3. Vista: Una vista muestra una determinada información. P.ej. si tenemos una aplicación que nos muestra la cotización de varias acciones, podríamos tener varias instancias de una vista, donde cada instancia nos mostraría la cotización de una acción. En winforms generalmente es un UserControl.

En PRISM las vistas se colocan en regiones y las regiones se incrustan en los targets… recordad que una región puede tener varias vistas.

En el RegionAdapter que vimos en el post anterior, siempre recibíamos la región y el target. El target ya estaba creado porque era un control ya existente en la ventana principal.

Esto no nos sirve si queremos mostrar una vista en un formulario nuevo ya que ahora debemos crear el formulario cada vez que queramos mostrar la región… ¿como podemos hacerlo?

Además del método Adapt() que usamos en el post anterior, los RegionAdapter pueden redefinir otro método llamado AttachBehaviors. En este método podemos añadir la lógica que queramos para personalizar el comportamiento de la región… en este caso podremos aprovechar para crear el target.

Un vistazo al código…

El código que sigue a continuación es una adaptación de la clase WindowRegionAdapter de Composite WPF Contrib, que he adaptado para que funcione con Windows Forms.

El método AttachBehaviors lo redefinimos de la siguiente manera:

protected override void AttachBehaviors(IRegion region, 
    Form regionTarget)
{
    base.AttachBehaviors(region, regionTarget);
    FormRegionBehavior behavior = 
        new FormRegionBehavior
            (regionTarget, region, FormBorderStyle.FixedSingle);
    behavior.Attach();
}

Los parámetros que recibe son los mismos que Adapt: la región y el target (en este caso un Form) ya que derivamos de RegionAdapterBase<Form>. En este método creamos un objeto FormRegionBehavior que será el que tendrá todo el código para gestionar regiones que estén dentro de un formulario. En concreto dicha clase será la responsable de:

  1. Crear un formulario por cada nueva vista añadida a la región y incrustar dicha vista en el formulario
  2. Cerrar el formulario que contiene una vista si esta se elimina de la región.
  3. Eliminar una vista de la región si se cierra el formulario que la contiene.
  4. Activar o desactivar la vista cuando su formulario es activado o desactivado.

A continuación pongo el código más relevante de dicha clase. Al final del post adjunto una aplicación de demo.

1 y 2. Crear un formulario por cada nueva vista y eliminar el formulario de una vista eliminada

El método Attach() de la clase FormRegionBehavior de suscribe a los dos eventos CollectionChanged de las colecciones Views y ActiveViews de la región:

internal void Attach()
{
    IRegion region = _regionWeakReference.Target as IRegion;
    if (region != null)
    {
        region.Views.CollectionChanged +=
            new NotifyCollectionChangedEventHandler
                (Views_CollectionChanged);
        region.ActiveViews.CollectionChanged += 
            new NotifyCollectionChangedEventHandler
                (ActiveViews_CollectionChanged);
    }
}

En el método Views_CollectionChanged es donde sabemos si se ha añadido una vista a la región o se ha eliminado, y así podemos crear o destruir el formulario asociado:

private void Views_CollectionChanged(object sender,
NotifyCollectionChangedEventArgs e) { Form owner = _ownerWeakReference.Target as Form; if (owner == null) { Detach(); return; } if (e.Action == NotifyCollectionChangedAction.Add) { foreach (object view in e.NewItems) { // Creamos un formulario por cada vista y lo mostramos... } } else if (e.Action == NotifyCollectionChangedAction.Remove) { foreach (object view in e.OldItems) { // Buscamos el formulario que contiene cada vista // para cerrarlo y "disposarlo" 🙂 } } }

3. Eliminar la vista de la región si se cierra el formulario

Para ello, cuando creamos un formulario, nos suscribimos al evento Closed para poder eliminar la vista asociada a la región:

private void Form_Closed(object sender, EventArgs e)
{
    Form frm = sender as Form;
    IRegion region = _regionWeakReference.Target 
        as IRegion;
    if (frm != null && frm.Controls.Count > 0 
        && region != null)
        if (region.Views.Contains(frm.Controls[0]))
            region.Remove(frm.Controls[0]);
}

4. Activar o desactivar la vista cuando su formulario es activado o desactivado

Para ello, cuando creamos un formulario nos suscribimos a los eventos Activated y Deactivate, para desactivar o activar la vista correspondiente:

private void Form_Activated(object sender, EventArgs e)
{
    IRegion region = _regionWeakReference.Target 
        as IRegion;
    Form frm = sender as Form;
    if (frm != null && frm.Controls.Count > 0 &&
        !region.ActiveViews.Contains(frm.Controls[0]))
        region.Activate(frm.Controls[0]);
}

5. Algunas consideraciones finales

Para PRISM toda región tiene un solo contenedor (o target) que es aquel que se usa cuando se llama a AttachNewRegion. Aunque este RegionAdapter va creando distintos formularios, para PRISM la región debe estar vinculada a un único target… que debe existir cuando llamemos a  AttachNewRegion y cuyo tipo debe ser Form puesto que nuestro RegionAdapter trabaja con targets de tipo Form.

Esto nos deja en una situación curiosa, puesto que necesitamos tener un Form creado para poder crear la región. Aunque luego este formulario no contendrà ninguna de las vistas de la región. Consideraremos a este formulario el formulario padre, y cuando el formulario padre muera el RegionAdapter dejarà de tratar a la región (digamos que la región habrá muerto).

Otra cosa que también debemos solucionar es dado una vista, encontrar que formulario la contiene. El RegionAdapter no se guarda una lista de los formularios creados, en su lugar solo tiene una WeakReference al formulario padre (el usado para llamar a AttachNewRegion y que nunca contendrá vistas de esta región) y usa el formulario padre para encontrar a los formularios que contiene la vista. Esto es posible porque al crear un formulario que contiene una vista se indica que su padre es el formulario padre:

Form owner = _ownerWeakReference.Target as Form;
// Creamos un formulario por cada vista y lo mostramos...

frm.Owner = owner;

De este modo se puede usar la propiedad OwnedForms del formulario padre para iterar sobre todos los formularios creados y ver cual contiene la vista en cuestión.

Finalmente, el hecho de tener que pasar un formulario ya existente a AttachNewRegion, no es excesivamente problemático: lo más fácil es usar el formulario principal del programa!

Adjunto el programa de demostración. El RegionAdapter DialogRegionAdapter es el que muestra vistas en nuevos formularios, mientras que el RegionAdapter ControlRegionAdapter las muestra en paneles (de hecho en cualquier control).

Descargar el código de demostración.

Espero que os sea útil! 🙂

Evento ALM: Como Team System cambió mi (ciclo de) vida

Hola a todos!

Aprovecho la ocasión para comunicaros un evento que organizamos en raona, en colaboración con Microsoft, sobre ALM en general y Team System en particular.

Lo hemos llamado “Como Team System cambió mi (ciclo de) vida”. Durante unas 4 horas y pico, Magda, Enric y yo mismo (sí, nada es perfecto y yo voy a estar… pero no sufrais, ya dejaré hablar a los que saben :p) vamos a comentar distintos aspectos de Team System. Empezaremos con una introducción y luego iremos viendo aspectos concretos (personalización, distintos clientes de foundation server, team build,…).

El evento será en Madrid el 18 de marzo y en Barcelona al dia siguiente (o sea el 19).

Enlace de la página del evento en raona

Enlace del registro para Madrid

Enlace del registro para Barcelona

Esperamos veros por allí!!! 😉

Saludos!

[DllImport] y clases genéricas

Un post rápido para decir sólo dos cosas:

  • DllImport y clases genéricas no se llevan bien. Meter un DllImport en una clase genérica (o derivada de alguna genérica) lanza un TypeLoadException.
  • Más importante que la anterior: No nos habríamos topado con el error de haber seguido las recomendaciones de uso de DllImport. Y ni siquiera podemos alegar desconocimiento de ellas, ya que si hubiesemos usado el análisis estático de código se nos habría avisado.

En resumen, ya se ha dicho varias veces por aquí, pero el análisis estático de código es tu amigo… 🙂

Saludos!

XmlSerializer y propiedades ocultadas

Hola! Ayer un compañero de trabajo me comentó un problema con el que se encontró trabajando con propiedades ocultadas y el serializador xml.

En concreto, quería serializar dos clases tales como las que siguen:

public class FOO
{
    private List<FOO> _items;
    public List<FOO> Items
    {
        get { return _items; }
        set { _items = value; }
    }
}
public class DerivedFOO : FOO
{
    private List<DerivedFOO> _items;
    public new List<DerivedFOO> Items
    {
        get { return _items; }
        set { _items = value; }
    }
    public DerivedFOO() { Items = new List<DerivedFOO>(); }
}

Son dos clases, una que deriva de la otra, que cada una de ellas tiene una lista de elementos de la propia clase.

Al intentar serializar un objeto DerivedFOO el serializador da una excepción: “El miembro DerivedFOO.Items de tipo System.Collections.Generic.List`1[ConsoleApplication10.DerivedFOO] oculta al miembro de clase base FOO.Items de tipo System.Collections.Generic.List`1[ConsoleApplication10.FOO]. Utilice XmlElementAttribute o XmlAttributeAttribute para especificar un nombre nuevo.

La solución que propone la propia excepción (el uso de [XmlElement] o [XmlAttribute]) no funciona, y el uso de [XmlArray] tampoco.

Analicemos un poco la situación: Tenemos dos clases, una derivada de la otra, donde la derivada oculta una propiedad de la clase base. Declarar la propiedad Items como virtual y redefinirla en la clase derivada, no funciona por dos razones. La primera es que C# no acepta propiedades covariantes, lo que implica que desde una clase derivada no podemos redefinir una propiedad de la clase base para que devuelva un tipo más específico (derivado) del que declara la propiedad base. Es decir, esto no compila en C#:

class A
{
    public virtual A Self { get; set; }
}
class B : A
{
    // C# no tiene propiedades covariantes!
    public override B Self { get;set;}
}

La segunda razón por la cual, declarar la propiedad como virtual en la clase base tampoco funcionaría, es que incluso suponiendo que C# tuviese propiedades covariantes, List<FOO> y List<DerivedFOO> son dos tipos completamente distintos. Recordad que aunque B derive de A, List<B> no deriva de List<A>.

Está claro que debemos buscar otro enfoque… Una posible solución pasa por el uso de genéricos, es decir definir una sola clase base que tenga la propiedad Items, y que esta sea genérica:

public class FOOBase<T>
{
    private List<FOOBase<T>> _items;
    public List<FOOBase<T>> Items
    {
        get { return _items; }
        set { _items = value; }
    }
    public FOOBase()
    {
        Items = new List<FOOBase<T>>();
    }
}
public class FOO : FOOBase<FOO> { }
public class DerivedFOO : FOOBase<DerivedFOO> {}

En este caso definimos tres clases: La genérica FOOBase<T> que es la que contiene la definición de la propiedad, y luego dos especializaciones, que hemos llamado FOO y DerivedFOO…

Esto funciona, pero no podemos negar que hemos modificado la relación entre FOO y DerivedFOO. Inicialmente la segunda era derivada de la primera, pero ahora entre FOO y DerivedFOO no hay ninguna relación. Si queremos poder trabajar indistintamente con ambas clases (FOO y DerivedFOO) debemos trabajar a nivel de FOOBase<T>, lo que según el caso puede ser problemático:

// error CS0246: The type or namespace name 'T' could not be found
static void f(FOOBase<T> t) {}

Para que esto compile el método f debe ser a su vez genérico:

static void f<T>(FOOBase<T> t) { }

Por suerte, al menos, Visual Studio puede inferir el tipo genérico del método a partir del argumento de llamada, por lo que al menos esto funciona:

DerivedFOO df = new DerivedFOO();
f(df);

Para evitar tener que arrastrar tantos métodos genéricos es interesante tener una clase base, que no sea genérica y que defina todas las propiedades (y métodos) base que no dependen en absoluto del tipo genérico:

public class FOOBase     
{ 
    // Nueva clase base de la jerarquía
}
public class FOOBase<T> : FOOBase where T : FOOBase { private List<FOOBase<T>> _items; public new List<FOOBase<T>> Items { get { return _items; } set { _items = value; } } public FOOBase() { Items = new List<FOOBase<T>>(); } } public class FOO : FOOBase<FOO> { } public class DerivedFOO : FOOBase<DerivedFOO> {}

Ahora la clase FOOBase pasa a ser la clase principal, de la que deriva FOOBase<T>. Esto nos permite definir métodos que acepten FOOBase y que no deben ser genéricos. Evidentemente esto tiene un precio: no podemos acceder a la propiedad Items desde una referencia a FOOBase, por lo que si necesitamos acceder a esta propiedad, si que no tenemos más remedio que trabajar con métodos genéricos…

Ahora podemos serializar objetos FOO y DerivedFOO y que ambos contienen una propiedad List<T> siendo T el propio tipo, además de otras propiedades que vendrían heredades de FOOBase.

¿Satisface esto los requerimientos de mi compañero? Pues no lo sé, pero a mi no se me ha ocurrido ninguna idea más…

Saludos!

CommandPattern extendiendo Unity

Hola a todos! Hoy voy a hablar del poder que nos da el mecanismo de extensiones de Unity. Doy por supuesto que todos conoceis lo que es un contenedor IoC en general y Unity en particular. Si no, echad un vistazo a los posts “IoC o el poder de ceder el control” (para una explicación general de IoC) y “Microsoft Unity: Inyección de dependencias .NET” (para una explicación general sobre Unity en concreto).

Para ilustrar el poder que nos da extender Unity voy a poner una posible implementación del patron Command Pattern. Este patrón es un clásico para la construcción de interfaces desacopladas, donde el elemento de la UI que genera una acción y el código que implementa esta acción no tienen porque estar relacionados. Esto aumenta la mantenibilidad y la reutilización del código.

Lo que voy a exponer aquí, es una implementación de dicho patrón, usando Unity y que funciona con cualquier “tecnología” (Winforms, WPF, cónsola). Dado que esto va a ser un poco largo, coged una buena cervecita que empezamos! 😉

También comentaros que el código que mostraré, aunque funcional, no está 100% completo, pero tiene las bases para que sea fácilmente completable.

1. Extensiones de Unity… que son?

Las extensiones de Unity son el mecanismo que nos permite personalizar el comportamiento del contenedor cuando deba crear o destruir un objeto, o bien cuando se registre algún mapping entre tipos. En este post vamos a hablar de dos mecanismos para extender Unity:

  • Extensiones: Una extensión es básicamente una clase que deriva de UnityContainerExtension. Cuando se añade una extensión a Unity (cosa que puede hacerse programáticamente o por configuración), se llama al método Initialize() de la extensión. Las extensiones se usan para registrar en el contenedor nuevas Estrategias o Políticas y para suscribirnos a eventos de cuando se registra un mapping entre tipos.
  • Estrategias: Una estrategia es algo que debe hacer Unity antes o después de crear o destruir un objeto. P.ej. si colocamos un atributo [Dependency] en cualquier propiedad pública de un objeto, Unity nos rellenerá automáticamente dicha propiedad. Esto se hace a través de una estrategia (built-in dentro de Unity).

2. Unity y ObjectBuilder2

Unity en si mismo, en el fondo es simplemente, un wrapper sore ObjectBuilder2 (Una evolución del ObjectBuilder que venia con CAB y EntLib). Aunque por norma general podemos usar Unity sin preocuparnos del ObjectBuilder2 subyacente, cuando nos ponemos a extender el contenedor, entonces si que debemos interactuar con ObjectBuilder2. En concreto las estrategias se definen siempre a nivel de ObjectBuilder2, mientras que las extensiones son un mecanismo propio de Unity.

3. Nuestro modelo de Command Pattern

Para este ejemplo he pensado en un modelo de command pattern, extremadamente sencillo y declarativo (es decir, basado en atributos). Para ello vamos a usar dos atributos propios:

  • CommandSource: Para indicar que un determinado evento debe vincularse a un command.
  • CommandTarget: Para indicar que un método es la implementación de un command.

El primer atributo se define a nivel de clase (tantas veces como sea necesario), mientras que el segundo se define a nivel de método (una sola vez).

Un ejemplo de su uso:

[CommandSource("Command1", "button1", "Click")]
public partial class View1 : UserControl
{
    public View1()
    {
        InitializeComponent();
    }
}

Cuando se lance el evento “Click” del objeto “button1” se debe invocar el command “Command1”. En algún sitio habrá una clase (que no debe porque tener ninguna relación con View1) con el siguiente código para gestionar el command:

[CommandTarget("Command1")]
private void Foo()
{
    MessageBox.Show("Command1 Invocado");
}

4. Qué vamos a hacer…

Lo que queremos hacer es lo siguiente:

  • Cuando se registre un mapping en Unity, vamos a mirar la clase que se registra para inspeccionar si tiene atributos CommandSource y/o CommandTarget y vamos a guardar esta información en una clase (el CommandBroker).
  • Cuando se resuelva un tipo (es decir se cree una instancia) vamos a inspeccionar su tipo para ver si tiene atributos CommandSource y/o CommandTarget (sólo lo haremos si es necesario, que será si no se había definido un mapping para este objeto previamente), y luego nos suscribiremos a los eventos necesarios.

Así el CommandBroker es una clase que:

  • Tiene toda la información de que atributos CommandSource y/o CommandTarget tiene cada tipo creado por Unity.
  • Se suscribe a todos los eventos que vengan de un CommandSource y ejecuta el CommandTarget asociado.

El primer punto lo llevaremos a cabo mediante una extensión, y el segundo mediante una estrategia.

5. La extensión: CommandExtension

Vamos a definir nuestra extensión de Unity, para que haga tres cosas principales:

  • Cree una instancia del CommandBroker y la coloque como singleton dentro de Unity.
  • Cree la estrategia que necesitaremos y la “instale” en Unity.
  • Se registre al evento de creación de mapping para poder inspeccionar los tipos (evento Registering).

El código es realmente simple:

public class CommandExtension : UnityContainerExtension
{
    protected override void Initialize()
    {
        this.Container.RegisterInstance<ICommandBroker>(new CommandBroker());
        this.Context.Strategies.Add(new CommandStrategy(this.Container), 
            UnityBuildStage.Creation);
        this.Context.Registering += (o, e) =>
            this.Container.Resolve<ICommandBroker>().
            ProcessTypeInfo(e.TypeTo ?? e.TypeFrom);

        }
}

El método “ProcessTypeInfo” de la clase CommandBroker es la que inspecciona un tipo para ver si tiene atributos CommandSource y CommandTarget.

6. La estrategia: CommandStrategy

En la extensión creada previamente instalamos la estrategia CommandStrategy. Una estrategia se llama cada vez que Unity debe crear (BuildUp) o destruir (TearDown) un objeto.

En mi caso el código de la estrategia también es realmente simple:

class CommandStrategy : IBuilderStrategy
{
    private IUnityContainer container;
    public CommandStrategy(IUnityContainer container)
    {
        this.container = container;
    }
    #region IBuilderStrategy Members
    public void PostBuildUp(IBuilderContext context)
    {
        ICommandBroker cb = this.container.Resolve<ICommandBroker>();
        cb.ProcessObjectInfo(context.Existing);
    }
    public void PostTearDown(IBuilderContext context) { }
    public void PreBuildUp(IBuilderContext context) { }
    public void PreTearDown(IBuilderContext context) { }
    #endregion
}

En el método PostBuildUp (después de que ObjectBuilder2 haya construido el objeto) se llama a ProcessObjectInfo del CommandBroker. Este método hace dos cosas básicas:

  • Se suscribe a todos los eventos declarados en los atributos CommandSource del tipo del objeto
  • Guarda delegados a todos los métodos decorados con un CommandTarget (guardando el nombre del command asociado a cada delegado).

Así el CommandBroker se suscribirá a cualquier evento que se lance desde cualquier objeto creado por Unity, que esté referenciado por un CommandSource. Y en la función gestora de dicho evento mirará en su tabla interna de delegados  si hay alguno que pueda responder a dicho evento (basándose en el nombre del comando).

Si observais el constructor de la CommandStrategy, vereis que recibe una instancia del propio Unity. Esto es para poder obtener el CommandBroker, puesto que en la extensión lo registrábamos como singleton en Unity. Esto puede parecer sorprendente (que desde una estrategia de Unity no se tenga acceso al propio Unity)… es lo que decía antes que las estrategias se definen a nivel de ObjectBuilder2.

7. El código…

No comento más el resto del código, puesto que sólo conseguiria liar el post. Para los interesados lo dejo todo en un zip: UnityExtensions.zip. El código está comentado para que sea fácilmente entendible. Destacar que las clases que hacen el trabajo pesado son CommandTypeDescriptor (que es quien realmente mira todos los [CommandSource] y [CommandTarget], CommandInfo (que mantiene toda la información de UN comando) y el propio CommandBroker.

Como he dicho el código NO está del todo completo, p.ej. si se registra un singleton con RegisterInstance, dicho singleton no participa del sistema de comandos, y luego en ningún caso el CommandBroker se “desuscribe” a ningún evento. También para que fuese una implementación completa del patrón CommandPattern, se debería poder (de alguna manera) activar o desactivar comandos. Al desactivar un comando todos los elementos de la UI vinculados a él (o sea todos sus [CommandSource]) deberían deshabilitarse

… pero bueno, todo es meterse! 😉

Espero que este post os haya dado una idea del potencial de extender Unity (y también del patron Command Pattern”).

Saludos!

Strings en .NET y el BOM

¿Conoceis el BOM? Los que no, teneis suerte… los que sí, seguro que lo habeis sufrido… 🙂 Para los que no, contaros que el BOM, o Byte Order Mask que es lo que significan sus siglas, no es nada más que una marca (de entre 2 y 3 bytes) al principio de un archivo Unicode que indica el formato de los datos… si están en little endian o big endian p.ej.

Quereis verlo en acción? Abrid el bloc de notas y teclead cualquier palabra, como p.ej. Agüero (algún fan del atleti por aqui???). Ahora haced un “guardar como” y marcad la opción “Unicode big endian” en codificación.

Ahora si miramos el tamaño del archivo, vereis que ocupa 14 bytes… Las cuentas no salen: Agüero tiene 6 letras, a 2 bytes la letra Unicode… sobran 2 bytes. El BOM. ¿Queréis más pruebas? Haced un type del archivo desde una consola. Vereis algo como:

■  A g ³ e r o

Este “cuadradito negro” que aparece al principio es el BOM. Que pasa si os lo cargais??? Si abris el fichero con un editor hexadecimal (como el mismo Visual Studio) vereis algo como:

FE FF 00 41 00 67 00 FC 00 65 00 72 00 6F   …A.g…e.r.o

Los dos primeros bytes (FE FF) son el BOM… borradlos para que vuestro archivo quede tal como:

00 41 00 67 00 FC 00 65 00 72 00 6F   .A.g…e.r.o

Lo guardais de nuevo y lo abrís con el bloc de notas… y esto es lo que vereis:

A g ü e r o

Sin BOM el bloc de notas identifica este archivo de texto como ANSI en lugar de Unicode, e interpreta el byte 00 de cada carácter Unicode como un carácter ANSI adicional.

¿Divertido, eh? Pues no os digo nada cuando uno se encuentra que según el protocolo o producto que use el BOM puede ser opcional, obligatorio o hasta prohibido…

¿Y porque os cuento todo esto? Pues porque me he encontrado con un comportamiento curioso (no digo ni que esté mal ni que esté bien) con las strings de .NET y el BOM. Tengo el siguiente código:

class Program
{
    static void Main(string[] args)
    {
        string foo = (char)0xfeff + "Foo";
    }
}

Fácil, eh? Creo una string y le añado el BOM al principio… Y ahora viene lo curioso:

  1. foo.Length devuelve 4 porque cuenta el BOM como un carácter más
  2. foo[0] es un carácter con valor 0xfeff
  3. foo[1] es un carácter con valor 0x0046 (‘F’)
  4. foo.StartsWith(foo[0] + “”) devuelve true, indicando que la cadena empieza con el BOM
  5. foo.StartsWith(foo[1] + “”) también devuelve true, indicando que la cadena empieza por “F”
  6. foo.Equals(foo.Substring(1)) devuelve false, indicando que ambas cadenas son distintas
  7. foo.CompareTo(foo.Substring(1)) devuelve 0, indicando que ambas cadenas son iguales
  8. foo.Trim se carga el BOM (o sea foo.Trim().Length vale 3)

En fin… parece ser que algunos métodos conocen el BOM y lo ignoran y otros no y lo tratan como un caracter más…

¿Curioso, no?

Unity? Sí gracias, pero no me abraces demasiado…

No hace mucho, Jorge Dieguez escribió un interesante post sobre Unity y el patrón de Dependency Injection. Resumiendo mucho este patrón permite eliminar las dependencias de nuestro código, trasladandolas todas a un sólo elemento, que se conoce generalmente como “contenedor de DI”. Este contenedor es el responsable de devolvernos todas las referencias a clases que nostros precisemos.

Las ventajas es que tenemos un código mucho menos acoplado, que por lo tanto es más fácil de probar y de mantener.

Contenedores de DI hay muchos, p.ej. en .NET tenemos a Unity (que viene de la mano de la gente de P&P) a Windsor Container o a Spring.NET sólo por citar tres ejemplos. Cada uno de ellos (y de los muchos otros que hay) tienen sus características y peculiaridades y dado que estamos pensando en hacer código débilmente acoplado… quizá deberíamos evitar ligarnos a nuestro contenedor DI, porque nunca sabemos cuando nos puede interesar cambiar.

Cojamos el caso de Unity. Imaginemos el siguiente proyecto super sencillo:

namespace UnityTest
{
    interface IFoo {}
    class FooClass : IFoo { }
    class Program
    {
        static void Main(string[] args)
        {
            UnityContainer uc = new UnityContainer();
            uc.RegisterType<IFoo, FooClass>();
            IFoo foo = uc.Resolve<IFoo>();
            Console.WriteLine(foo.GetType().FullName);
            Console.ReadLine();
        }
    }
}

Es una aplicación de consola, donde se declara una interfaz (IFoo), una clase que la implementa (FooClass) y luego en el método Main:

  1. Se crea una instancia de Unity
  2. Se registra el mapping entre IFoo y FooClass
  3. Se obtiene una instancia de IFoo.
  4. Miramos el tipo de la referencia obtenida

Como os podeis suponer lo que este programa muestra por pantalla es: UnityTest.FooClass

Unity nos devuelve una instancia de FooClass cada vez que le pedimos una instancia de IFoo, porque así lo especifica el mapping creado con RegisterType.

Hasta ahora todo perfecto: nuestra interfaz IFoo y nuestra clase FooClass no estan ligadas a Unity en ningún modo… vamos a ver como se nos pueden torcer las cosas…

Modificamos la clase FooClass para que tenga dos constructores:

class FooClass : IFoo 
{
    public FooClass() 
    { 
        Console.WriteLine("FooClass::Default ctor"); 
    }
    public FooClass(BarClass bar) 
    { 
        Console.WriteLine("FooClass::Bar ctor"); 
    }
}

El resto del código es igual que antes, con la excepción de que nos aparece una clase nueva (BarClass) que da igual (puede ser vacía, no nos afecta). La pregunta es obvia: ¿que constructor usará Unity para construir un objeto FooClass?

La respuesta es: el que tenga el mayor número de parámetros (¿la razón? Quien sabe…). Unity utilizará el constructor con mayor número de parámetros e inyectará todos los parámetros (llamando internamente al método Resolve) a dicho constructor. ¡Eh, un momento! Esto está muy bien pero… nosotros no hemos definido ningún mapping para BarClass en Unity… ¿No debería quejarse? Pues no: El método Resolve<T> de Unity es capaz de crear cualquier clase para que lo no haya un mapping definido, siempre y cuando T sea una clase, no una interfaz.

Si alguien se pregunta que pasaría si FooClass tuviese dos constructores con un parámetro, entonces Unity se quejará con una excepción parecida a: The type FooClass has multiple constructors of length 1. Unable to disambiguate.

Supongamos que queremos que Unity use un constructor en concreto (bien sea porque tenemos varios constructores con mayor número de parámetros o bien porque queremos usar alguno en concreto). En este caso, una solución es aplicar el atributo InjectionConstructor al constructor que queramos que use Unity:

class FooClass : IFoo 
{
    [InjectionConstructor]
    public FooClass() 
    { 
        Console.WriteLine("FooClass::Default ctor"); 
    }
    public FooClass(BarClass bar) 
    { 
        Console.WriteLine("FooClass::Bar ctor"); 
    }
}

Bueno… esto funciona correctamente, pero en este momento hemos creado una dependencia entre FooClass y Unity. Si algluna vez nos cambiamos a cualquier otro conenedor de DI, no podemos esperar que entienda el atributo InjectionConstructor, ya que éste, obviamente, es propio de Unity. Así que ahora nuestro código está acoplado a Unity… lo cual no es la solución ideal (en algunos casos).

Por suerte existe una solución que nos permite que nuestra clase FooClass no tenga dependencias contra Unity: cuando especificamos el mapping podemos indicarle que constructor utilizar. Para ello podemos usar el método Configure de Unity:

uc.RegisterType<IFoo, FooClass>().Configure<InjectedMembers>().
    ConfigureInjectionFor<FooClass>(new InjectionConstructor());

Aquí estamos registrando el mapping entre IFoo y FooClass, y configuramos los miembros inyectados para el tipo FooClass para que use el constructor sin parámetros. Si quisieramos usar el constructor con un parámetro BarClass:

uc.RegisterType<IFoo, FooClass>().Configure<InjectedMembers>().
    ConfigureInjectionFor<FooClass>(
    new InjectionConstructor(new BarClass()));

Con esto Unity cada vez que deba crear un FooClass, usará el constructor que acepta un parámetro BarClass y lo invocará con una instancia de BarClass.  Unity usará siempre la misma instancia de BarClass para todos los FooClass que cree. Es decir, si tenemos:

IFoo foo = uc.Resolve<IFoo>();
IFoo foo2 = uc.Resolve<IFoo>();

Tanto foo como foo2 serán creados con el constructor que acepta un BarClass pero el BarClass que ambos reciban será el mismo. Incluso aunque no creemos ningún objeto FooClass, el objeto BarClass sí que se crea.

Si queremos que Unity cree cada vez un BarClass para cada FooClass, entonces podemos utilizar el siguiente código:

uc.RegisterType<IFoo, FooClass>().Configure<InjectedMembers>().
    ConfigureInjectionFor<FooClass>(
    new InjectionConstructor(typeof(BarClass)));

En este caso, Unity creará un BarClass nuevo cada vez que deba crear un FooClass… Bueno, esto no es estrictamente cierto: Realmente Unity cada vez llamará a su método Resolve<BarClass>, cada vez que deba obtener un BarClass para llamar al constructor de FooClass. Si no tenemos mapping definido, Resolve<BarClass> crea un BarClass nuevo cada vez. Pero, si definiesemos el siguiente mapping:

uc.RegisterInstance<BarClass>(new BarClass());

Aquí estamos registrando una instancia de BarClass como singleton dentro de Unity. Por lo tanto todas las llamadas a Resolve<BarClass> devolverán el mismo objeto… volvemos a la situación anterior: todos los constructores de FooClass recibirán el mismo BarClass. E igual que antes el BarClass se crea cuando se llama a RegisterInstance, por lo que aunque no creemos ningún FooClass, el BarClass sí que es creado.

Tenemos una variación de este caso. Si registramos BarClass como singleton, usando RegisterType:

uc.RegisterType<BarClass>(new ContainerControlledLifetimeManager());

Ahora BarClass está registrado como singleton,  pero no se creará la primera instancia de BarClass hasta que sea necesario. Así, cuando creemos el primer FooClass, Unity creará el BarClass y lo usará como parámetro del constructor. Para crear el segundo FooClass, Unity usará el BarClass previamente creado.

Pero lo importante es que nuestra clase FooClass no depende para nada de Unity, de forma que podemos reutilizarla en cualquier otro proyecto o bien cambiar de contenedor de DI… Que sí, que Unity está muy bien pero tampoco es plan de que nos atemos a él, no????

Saludos!

PRISM y Winforms

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!

Gestionando las dependencias entre módulos cargados on-demand en PRISM

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!

PRISM y AvalonDock

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!