Juanma Santoyo

En ocasiones me llaman friki

Arquitectura de capas en aplicaciones para Microsoft Surface

| No hay comentarios

En la primera aplicación que hice para Surface, cometí un gran error de arquitectura que bajo mi punto de vista estropeó un poco lo que yo considero que fue un buen trabajo. Ahora que estoy trabajando en otra aplicación para Surface, me propuse desde el primer día no repetirlo. A la solución que he encontrado la he llamado arquitectura de capas. No estoy inventando nada, pues en la programación gráfica dividir la interfaz en capas es muy común. Este artículo realmente sólo propone una forma de hacerlo en una aplicación WPF orientada a Surface.

Mi gran error.

Me dí cuenta de que era realmente complicado gestionar las cosas que había en el ScatterView. Si veis el vídeo, comprobaréis que básicamente estoy hablando de tres tipos de elementos: los controles de usuario, el coche, y las imágenes del interior del coche. La solución por la que opté en aquel momento se basaba en usar métodos para introducir y eliminar cada elemento en el ScatterView, y registrar la existencia de cada uno en tres listas globales (una lista para cada tipo de elemento). De esa forma ponía y quitaba los elementos de cada tipo.

Era una mala solución, por que no permitía una gran modularización del código y sobrecargaba la clase principal del programa de funcionalidades que realmente no eran de su competencia. Todo se complicaba mucho más si tenemos en cuenta que no pude encapsular el ScatterView en un control de usuario como me hubiese gustado y lo tenía todo en la ventana inicial de la aplicación. En pocas palabras, aunque creo que algunos aspectos de la aplicación quedaron bastante bien, la ventana inicial era sucia y desordenada. El tiempo y la falta de experiencia me impidieron mejorar ese aspecto. Se podría decir que esa aplicación tiene un serio problema de arquitectura.

La solución: Arquitectura de capas.

Sin embargo, cuando inicié el desarrollo de la aplicación que tengo ahora entre manos, decidí desde el primer momento plantear las cosas de otro modo. No quería repetir el mismo error, sobre todo por que esta aplicación es aún más compleja en lo que respecta a los elementos libres por la pantalla y un error de esas características hubiese tenido un impacto negativo en la aplicación mucho más devastador.

El principal error que cometí inicialmente fue pensar que un ScatterView no puede posicionarse sobre otro. En realidad, sí es posible, y ambos ScatterViews son totalmente funcionales. Esto nos permite organizar los elementos en varios ScatterViews, uno sobre otro; como si fuesen capas.

Básicamente, para implementar esta arquitectura sólo necesitaremos:

  1. Diferentes controles de usuario que serán las capas.
  2. Una clase estática o Singleton que se encargará de gestionar las interacciones entre capas.

Cada capa se preocupa únicamente de sus competencias, mientras que para cualquier tipo de interacción con los elementos de las otras capas se usarán funcionalidades implementadas en la clase estática.

Un ejemplo sencillo.

No voy a complicarlo, ya que realmente sólo nos interesa ver cómo funcionaría esta arquitectura.

El ejemplo en cuestión será el siguiente: vamos a hacer dos capas en forma de controles de usuario. En cada capa vamos a poner un ScatterView y un ScatterViewItem.

La idea es que cuando un ScatterViewItem se mueva, el otro se posicionará en una posición reflejada. Es decir, reflejará las coordenadas X e Y del ScatterViewItem que hemos movido.

Obviamente, la comunicación entre controles de usuario que necesitamos se implementará con una clase estática.

Las capas.

Vamos a programar las dos capas. Serán dos controles prácticamente idénticos, sólo cambiaremos un par de características del ScatterViewItem (para que no se confundan). Los ScatterViewItems tendrán colores diferentes y ubicaciones diferentes (obviamente). Uno estará situado en el punto (100, 100) y el otro en su punto simétrico (924, 668). Para simplificar el asunto, haremos que los ScatterViewItems no se puedan rotar ni escalar.

Los controles de usuario se llamarán LayerA y LayerB, este es su XAML:

LayerA.
<s:SurfaceUserControl x:Class="ArquitecturaDeCapas.LayerA"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:s="http://schemas.microsoft.com/surface/2008">
    <Grid>
        <s:ScatterView>
            <s:ScatterViewItem x:Name="Svi"
                Orientation="0" Center="100,100"
                Width="50" Height="50"
                CanScale="False" CanRotate="False"
                ScatterManipulationDelta="ScatterViewItem_ScatterManipulationDelta"
            >
                <Rectangle Fill="Yellow" />
            </s:ScatterViewItem>
        </s:ScatterView>
    </Grid>
</s:SurfaceUserControl>
LayerB.
<s:SurfaceUserControl x:Class="ArquitecturaDeCapas.LayerB"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:s="http://schemas.microsoft.com/surface/2008">
    <Grid>
        <s:ScatterView>
            <s:ScatterViewItem x:Name="Svi"
                Orientation="0" Center="924,668"
                Width="50" Height="50"
                CanScale="False" CanRotate="False"
                ScatterManipulationDelta="ScatterViewItem_ScatterManipulationDelta"
            >
                <Rectangle Fill="Red" />
            </s:ScatterViewItem>
        </s:ScatterView>
    </Grid>
</s:SurfaceUserControl>

Como veis en ambos controles, los ScatterViewItems tienen asignado un manejador para el evento “ScatterManipilationDelta“. Este evento se lanza cada vez que una manipulación provoca un cambio en el ScatterViewItem, así que lo usaremos para notificar a la clase estática que el ScatterViewItem tiene una nueva posición (y la clase estática lo notificará a su vez a la otra capa).

Vamos pues a ver ese manejador. En ambas capas será igual, ya que será el método de la clase estática quien diferencie entre una capa o otra (esto no tendría por qué ser así).

El manejador de ScatterManipilationDelta.
private void ScatterViewItem_ScatterManipulationDelta(object sender, ScatterManipulationDeltaEventArgs e)
{
    ScatterViewItem svi = (ScatterViewItem) sender;
    Layers.NotifyNewPosition(this, svi.Center);
}

Fijaos en el método Layers.NotifyNewPosition. Este método será el método que comunicará ambas capas. La idea es que haga dos cosas:

  1. Identifica la capa que llama al método.
  2. Notifica a la otra capa la nueva posición respecto a la que debe reflejar su ScatterViewItem.

Para identificar la capa que llama al método, vemos que se envía el parámetro this. Para saber la nueva posición del ScatterViewItem, envía el parámetro svi.Center.

Por otra parte, la clase estática informará a la otra capa de la nueva posición mediante un método público en la capa que deba ser notificada. En este caso, como la posición se refleja; también se harán los cálculos necesarios. El método público de cada capa, sería una cosa así:

public void ReflexSviPosition(Point Position)       
{
    this.Svi.Center = new Point(1024 - Position.X, 768 - Position.Y);
}

Ahora que ya sabemos como son las capas, vamos a ver que forma tiene esa clase estática que nos comunicará ambas capas.

Una clase estática para comunicar las distintas capas.

A la capa en cuestión la llamaremos Layers. Como he dicho, puede ser estática o Singletone. Aunque seamos francos, realmente el único requisito que debe cumplir esta clase es que tiene que ser accesible por todas las capas que queramos relacionar.

public static class Layers
{
	private static LayerA layerA = null;
	public static LayerA LayerA
	{
		get { return layerA; }
		set { layerA = value; }
	}

	private static LayerB layerB = null;
	public static LayerB LayerB
	{
		get { return Layers.layerB; }
		set { Layers.layerB = value; }
	}

	public static void NotifyNewPosition(SurfaceUserControl Layer, Point Position)
	{
		if(Layer.GetType() == typeof(LayerA))
		{
			layerB.ReflexSviPosition(Position);
		}
		else if(Layer.GetType() == typeof(LayerB))
		{
			layerA.ReflexSviPosition(Position);
		}
	}
}

Podéis ver el método “NotifyNewPosition” y cómo hace la distinción de capas.

Otro aspecto muy importante de esta clase, son las propiedades LayerA y LayerB. No son más que referencias a las capas, para poder acceder posteriormente a ellas. ¿Dónde se inician estas propiedades? Pues allí donde estén las propias capas. En este caso es la ventana inicial de mi programa, que se llama Main.xaml.

El contenedor de las capas.

Este sería el xaml:

<s:SurfaceWindow x:Class="ArquitecturaDeCapas.Main"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:s="http://schemas.microsoft.com/surface/2008"
    xmlns:self="clr-namespace:ArquitecturaDeCapas"

    Title="ArquitecturaDeCapas"
>
    <s:SurfaceWindow.Resources>
        <ImageBrush x:Key="WindowBackground" Stretch="None" Opacity="0.6" ImageSource="pack://application:,,,/Resources/MainBackground.jpg"/>
    </s:SurfaceWindow.Resources>

    <Grid Background="{StaticResource WindowBackground}">
        <self:LayerA x:Name="LayerA" />
        <self:LayerB x:Name="LayerB" />
    </Grid>
</s:SurfaceWindow>

Y también se necesita un poco de C# para guardar en la clase Layers las referencias a cada capa. En mi caso, he añadido estas dos líneas de código al final del constructor de Main.

Layers.LayerA = this.LayerA;
Layers.LayerB = this.LayerB;

De esta clase Main destacar tres cosas:

  1. Añadimos el namespace self, para poder incluír controles de usuario de nuestro proyecto.
  2. Añadimos las dos capas.
  3. Guardamos en Layers las referencias a nuestras capas. Yo lo he hecho al final del constructor, pero se podría hacer en el evento Loaded, por ejemplo.

Hemos acabado.

Pues no hay mucho más que decir, ya tenemos dos ScatterViews sincronizados. Rápido, sencillo, y para toda la familia; espero que os ayude.

Share on FacebookTweet about this on TwitterShare on Google+Share on LinkedInEmail this to someone

Deja un comentario

Los campos obligatorios están marcados con *.