WP7, WPF y Silverlight: Un ControlTemplate, Tres plataformas.

Hola a todos

Siguiendo con mi idea de que debemos reutilizar la mayor parte posible de nuestro código, hoy vamos a ver como poder escribir Styles y ControlTemplates que se reaprovechen entre diferentes plataformas como son Windows Phone 7, WPF y Silverlight.

Anteriormente ya vimos como gracias a MVVM podíamos reutilizar la lógica de presentación (ViewModel) de nuestras vistas entre estas tres plataformas (parte 1 y parte 2), para interfaces de usuario simples no existe problema en diseñar por separado para cada plataforma las views, pero cuando nuestras interfaces de usuario necesitan ser más elaboradas, incluir un esquema de color específico, animaciones o efectos, mantener estos cambios por separado puede causarnos muchos dolores de cabeza y, por supuesto estamos nadando contra corriente, desarrollando lo mismo por triplicado.

Para evitar esto vamos a hacer uso de la capacidad de Visual Studio 2010 llamada Multi targeting, por medio de la cual podemos enlazar un archivo de un proyecto A a otros proyectos B y C, no copiándolo, simplemente manteniendo un enlace que permita a todos los proyectos trabajar sobre el mismo archivo.

También vamos a hacer uso del VisualStateManager de Silverlight/WPF. En WPF es muy común usar Triggers dentro de un ControlTemplate para cambiar el aspecto de un control en respuesta a cambios en eventos o propiedades (IsEnabled, MouseOver, Click…) Sin embargo en Silverlight no existe esta riqueza de Triggers, teniendo a nuestra disposición solamente un trigger que se ejecuta junto con el evento Loaded de la página en la que reside el control.

Para solventar este problema, tanto WPF como Silverlight (y por supuesto Windows Phone 7) soportan el VisualStateManager, dentro del cual podemos especificar grupos (VisualStateGroups) que a su vez contienen estados (Disabled, MouseOver, Pressed) y nos permiten ejecutar animaciones cuando el control entra a un estado en concreto, lo veremos más adelante con detenimiento.

1: Solución de trabajo.

Vamos a empezar por el primer paso, definir la solución con la que vamos a trabajar. Se compone de 3 proyectos, un proyecto WPF, un proyecto Silverlight y un proyecto Silverlight for Windows Phone, dentro de cada uno vamos a crear una carpeta Templates, quedando algo parecido a esto:

image

Una vez hecho esto, debemos crear en alguno de los proyectos un Diccionario de recursos, normalmente suelo elegir el más restrictivo de los proyectos, en este caso WPF soporta muchas opciones de Templates y Styles que no están disponibles en Silverlight o Windows Phone 7 (Property Triggers, Event Triggers…), por lo que he decidido crear el archivo en el directorio Templates del proyecto Silverlight. Una vez creado el diccionario de recursos original, vamos a enlazarlo al resto de proyectos.

image

Simplemente debemos ir a la opción “Add Existing Item” en el proyecto de Windows Phone 7 y WPF y seleccionar el diccionario de recursos que hemos creado en el proyecto Silverlight, pero en vez de presionar el botón “Add”, presionamos la flecha que se encuentra justo a su lado y del desplegable que aparece seleccionamos “Add as a link”, con esto se añadirá una referencia al archivo, pero físicamente seguirá existiendo solo uno, con lo que todos los cambios que realicemos se refrescarán automáticamente para todos los enlaces.

2: VisualStateManager vs Triggers

El VisualStateManager se introdujo en Silverlight y con la versión 4 de .NET lo tenemos disponible en WPF también. Su función es la de controlar la apariencia de un control dependiendo de su estado visual, esto es, podemos definir dentro de nuestro Control Template que estados visuales soporta nuestro control, ya sean standard (normal, mouseover, pressed, disabled) o definidos por nosotros mismos. En el caso de los estados standard, automáticamente se entrará en cada estado dependiendo del control mientras que para los estados definidos propios deberemos ser nosotros quienes indiquemos al control que se encuentra en un estado concreto con el método GoToState. Así mismo también nos permite definir opciones para las transiciones entre estados como duración de la transición, a que combinación de origen/destino de estados queremos aplicar la configuración de transición o incluso una animación a ejecutar para llevar a cabo la transición.

<ControlTemplate TargetType="Button">
    <Grid>
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="CommonStates">
                <VisualStateGroup.Transitions>
                    <!--Take one half second to transition to the MouseOver state.-->
                    <VisualTransition From="Normal" To="MouseOver" GeneratedDuration="0:0:0.5" />
                </VisualStateGroup.Transitions>
                <!-- NORMAL VISUAL STATE -->
                <VisualState x:Name="Normal">
                </VisualState>
                <!-- MOUSE OVER VISUAL STATE -->
                <VisualState x:Name="MouseOver"> 
                    <Storyboard>
                        <ColorAnimation Duration="0" Storyboard.TargetName="borderColor" 
                                        Storyboard.TargetProperty="Color" To="Cyan"/>
                    </Storyboard>
                </VisualState>
                <!-- PRESSED VISUAL STATE -->
                <VisualState x:Name="Pressed"> 
                    <Storyboard>
                        <ColorAnimation Duration="0" Storyboard.TargetName="borderColor" 
                                        Storyboard.TargetProperty="Color" To="Red"/>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <Ellipse>
            <Ellipse.Fill>
                <SolidColorBrush x:Name="borderColor" Color="Black"/>
            </Ellipse.Fill>
        </Ellipse>
        <Ellipse x:Name="defaultOutline" Stroke="{TemplateBinding Background}" StrokeThickness="2" Margin="2"/>
        <Ellipse x:Name="ButtonShape" Margin="5" Fill="{TemplateBinding Background}"/>
        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
    </Grid>
</ControlTemplate>

¿Significa esto que ya no debemos usar los Triggers nunca más? No exactamente, cuando necesitemos disparar acciones o cambios que no impliquen explícitamente un cambio visual del control podemos seguir usando los Triggers perfectamente, pero VisualStateManager nos facilita la creación de animaciones y transiciones entre estados visuales de un control. Además tenemos la ventaja de que el VisualStateManager es totalmente compatible con Silverlight y Windows Phone 7, por lo que es un paso indispensable si pensamos compartir nuestras plantillas entre WPF, Silverlight y WP7. Por descontado, en un mismo Control Template podemos usar Triggers y VisualStateManager al mismo tiempo sin ningún problema.

3: Creando nuestro ControlTemplate común

Bueno, como la teoría siempre es sencilla, vamos a ver un ejemplo practico para ilustrar el uso real del Visual State Manager y las distintas peculiaridades para aplicarla a las tres plataformas objetivo (Silverlight, Windows Phone 7 y WPF).

Tenemos que recordar, que el VisualStateManager se debe declarar en el elemento raíz que componga nuestro Control Template:

    <ControlTemplate x:Key="template" TargetType="Button">
        <Grid x:Name="Grd">
            <VisualStateManager.VisualStateGroups>
                <VisualStateGroup x:Name="CommonStates">
                    <VisualState x:Name="Normal">
                        <Storyboard FillBehavior="HoldEnd">
                            <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.Background).Color" Storyboard.TargetName="borde">
                                <EasingColorKeyFrame KeyTime="0:0:0.5" Value="DarkGray"/>
                            </ColorAnimationUsingKeyFrames>
                        </Storyboard>
                    </VisualState>
                    <VisualState x:Name="MouseOver">
                        <Storyboard FillBehavior="HoldEnd">
                            <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.Background).Color" Storyboard.TargetName="borde">
                                <EasingColorKeyFrame KeyTime="0:0:0.5" Value="Red"/>
                            </ColorAnimationUsingKeyFrames>
                        </Storyboard>
                    </VisualState>                    
                    <VisualState x:Name="Pressed">
                        <Storyboard FillBehavior="HoldEnd">
                            <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.Background).Color" Storyboard.TargetName="borde">
                                <EasingColorKeyFrame KeyTime="0:0:0.5" Value="LightBlue"/>
                            </ColorAnimationUsingKeyFrames>
                        </Storyboard>
                    </VisualState>
                    <VisualState x:Name="Disabled">
                        <Storyboard FillBehavior="HoldEnd">
                            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Grd">
                                <EasingDoubleKeyFrame KeyTime="0" Value=".3"/>
                            </DoubleAnimationUsingKeyFrames>
                        </Storyboard>
                    </VisualState>                    
                </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>
            
            <Border x:Name="borde" BorderBrush="Red" BorderThickness="1" Background="DarkGray" CornerRadius="10,0,10,0">            
            </Border>
            <ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Center"></ContentPresenter>
        </Grid>
    </ControlTemplate>

En este caso, usaremos un Style para aplicar el Control Template y modificar algunas propiedades extras del botón relacionadas con la fuente de letra usada:

    <Style TargetType="Button" x:Key="ButtonTemplate">
        <Setter Property="Template" Value="{StaticResource template}"></Setter>
        <Setter Property="Foreground" Value="DarkBlue"></Setter>
        <Setter Property="FontWeight" Value="Bold"></Setter>
        <Setter Property="FontSize" Value="16"></Setter>
    </Style>

Fijaros que en el Style he especificado una Key, lo que hará que no se aplique de forma automática. Esto es así porque en Windows Phone 7 los estilos no se aplican por defecto (siempre se aplica el estilo por defecto de Windows) y hay que indicarlo de forma manual, el ControlTemplate si que se aplicaría instantáneamente, pero tendríamos las propiedades de fuente de letra standard y en esta plantilla deseaba que fuesen otras.

Una vez hecho esto en nuestro resource dictionary, si lo abrimos desde cualquiera de los 3 proyectos veremos que tenemos el mismo código, por lo que solo nos quedan unos pocos pasos para poder ejecutar nuestros tres front-ends:

Lo primero es que establezcamos en las propiedades de nuestro ResourceDictionary (en cada proyecto) la propiedad BuildAction a Resource y Copy to Output directory a Do Not Copy.

Ahora deberemos referenciar en cada app.xaml a nuestro nuevo ResourceDictionary:

    <Application.Resources>
        <ResourceDictionary Source="Templates/Templates.xaml"></ResourceDictionary>
    </Application.Resources>

 

Daros cuenta de que usamos la barra / para indicar la ruta, en vez de usar la barra de directorio normal , esto es así debido a que tenemos el archivo enlazado y no existe físicamente.

En cada proyecto tenemos una página/ventana principal con un botón, le establecemos nuestro nuevo estilo a ese botón como lo haríamos normalmente:

<Button Content="Button" Height="75" Margin="9,6,9,0" Name="button1" 
        VerticalAlignment="Top"  Style="{StaticResource ButtonTemplate}" />

 

Con esto hemos terminado de configurar nuestra plantilla y aplicaciones, podemos ejecutar o incluso abrir la vista de diseño de XAML de cada proyecto y veremos inmediatamente el resultado:

Capture

 

Aquí os dejo el código fuente del artículo para que podáis trastear con el y para cualquier duda aquí espero vuestros comentarios, igual que en twitter @JosueYeray o en MSDN.

Un abrazo y Happy Coding!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *