Windows 8 – Control de Usuario

Mientras espero pacientemente a que pueda seguir programando mi teléfono móvil con Windows 8 y Visual Studio 2012

image

(Sí, ya sé que Windows 8 trae Hyper-V y podría instalarme una máquina virtual con Windows 7 y Visual Studio 2010).

Como decía, mientras tanto, he optado por empezar a explorar las posibilidades que tengo para programar aplicaciones XAML sobre Windows 8.

XAML (mi desconocimiento sobre…)

Lo primero es decir que no tengo mucha experiencia con WPF, así que con la idea de hacer una aplicación sencilla en la cabeza me he lanzado a ello.

Visual Studio 2012 permite comenzar con varias plantillas de aplicación para la tienda de Windows

  • Una aplicación vacía formada por una única pantalla sin contenido.

image

  • Una aplicación de cuadrícula organizada en tres pantallas y pensada en mostrar desde agrupaciones de elementos hasta los detalles de un elemento perteneciente a uno de los grupos

image

  • Una aplicación dividida que está organizada en dos pantallas, muy similar a la anterior en la que se muestran grupos de elementos en una pantalla y los elementos que pertenecen a un grupo junto con el detalle del elemento seleccionado en la otra pantalla.

image

Mi elección (con la idea de evitar complejidades en el primer contacto) ha sido la aplicación vacía.

Bibliotecas Portables

Mi objetivo es crear una aplicación donde el usuario puede ir creando de forma dinámica nodos que pueden tener relaciones entre sí (a modo de grafo).

Para ello he creado una biblioteca de clases donde he implementado la estructura de datos del grafo (así recuerdo un poco los estudios de la Universidad).

Una de las nuevas características que trae consigo el .NET Framework 4.5 es la Biblioteca Portable de Clases que te permite crear un ensamblado que puede ser reutilizado en diferentes tecnologías y dispositivos. Con este tipo de bibliotecas, puedo disponer de una colección de componentes que podré utilizar tanto en mis proyectos de Windows Phone (7.x y 8) y en mis proyectos basados en Windows 8. ¡Por fin!

image

Mi Primer Control de Usuario

El siguiente paso consiste en crear un control personalizado que me permita representar los nodos de un grafo.

Nada más sencillo ¿Verdad? (Pues, cosas de lo más triviales me han dado ciertos dolores de cabeza).

Lo primero, añadir a mi proyecto de Windows 8 un nuevo control. ¿Cuál escoger?

image

Tenemos dos tipos de controles.

  1. Controles con plantilla (Templated Controls)
  2. Controles de usuario (User Controls)

Bien, no voy a entrar ahora en diferencias entre unos y otros (todavía las estoy digiriendo). Como de lo que se trata es de aprender he optado por el camino del medio. ¡He implementado los dos tipos!

Mi objetivo es mostrar (en primera instancia) un rectángulo con un título, un área de texto descriptiva y una url que permita navegar a otro punto con más información (ese otro punto puede ser una web o una página de la propia aplicación. ¿Quién sabe?)

Algo de este estilo.

image

El proceso ha sido relativamente sencillo. Primero crear la estructura XAML que lo defina (esencialmente un grid con tres filas)

   1: <UserControl

   2:     x:Class="Jdmveira.Controls.NodeControl"

   3:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   4:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   5:     xmlns:local="using:Jdmveira.Controls"

   6:     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

   7:     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

   8:     mc:Ignorable="d"

   9:     d:DesignHeight="400"

  10:     d:DesignWidth="300">

  11:

  12:     <UserControl.Resources>

  13:         <Style x:Key="MainBorderDefaultNodeStyle" TargetType="Border">

  14:             <Setter Property="BorderThickness" Value="3" />

  15:             <Setter Property="CornerRadius" Value="1" />

  16:         </Style>

  17:

  18:         <Style x:Key="InnerBorderDefaultNodeStyle" TargetType="Border">

  19:             <Setter Property="BorderThickness" Value="2" />

  20:             <Setter Property="CornerRadius" Value="1" />

  21:         </Style>

  22:     </UserControl.Resources>

  23:

  24:     <Grid x:Name="CtrlMainLayout">

  25:         <Border x:Name="MainBorder" Style="{StaticResource MainBorderDefaultNodeStyle}">

  26:

  27:             <Border.BorderBrush>

  28:                 <SolidColorBrush x:Name="MainBorderColor" Color="#FF0067BD" />

  29:             </Border.BorderBrush>

  30:

  31:             <Border Style="{StaticResource InnerBorderDefaultNodeStyle}">

  32:

  33:                 <Border.BorderBrush>

  34:                     <SolidColorBrush x:Name="InnerBorderColor" Color="#FF84C7FF" />

  35:                 </Border.BorderBrush>

  36:

  37:                 <Grid MinHeight="150"

  38:                   MinWidth="150">

  39:                     <Grid.RowDefinitions>

  40:                         <RowDefinition Height="10*"/>

  41:                         <RowDefinition Height="50*"/>

  42:                         <RowDefinition Height="10*"/>

  43:                     </Grid.RowDefinitions>

  44:

  45:                     <TextBlock

  46:                         x:Name="TitleBlock"

  47:                         Margin="0"

  48:                         Text="Prueba"

  49:                         FontSize="24"

  50:                         Padding="6,2,0,0"

  51:                         Foreground="Black"

  52:                         Grid.Row="0">

  53:

  54:                     </TextBlock>

  55:

  56:                     <Rectangle

  57:                         Grid.Row="0"

  58:                         Fill="{StaticResource NodeCaptionBackground}"

  59:                         Canvas.ZIndex="-1"/>

  60:

  61:                     <TextBlock

  62:                         x:Name="DescriptionBlock"

  63:                         Text="Texto descriptivo que podría ocupar perfectamente más de una línea"

  64:                         TextWrapping="Wrap"

  65:                         Padding="4,4,4,0"

  66:                         Foreground="Black"

  67:                         Grid.Row="1"/>

  68:

  69:                     <Rectangle Grid.Row="1" Fill="White" Canvas.ZIndex="-1" />

  70:

  71:                     <HyperlinkButton

  72:                         x:Name="LinkMoreInfo"

  73:                         VerticalAlignment="Bottom"

  74:                         Grid.Row="2" HorizontalAlignment="Right">www.bing.es</HyperlinkButton>

  75:

  76:                         <Rectangle Grid.Row="2" Fill="White" Canvas.ZIndex="-1" />

  77:                 </Grid>

  78:             </Border>

  79:         </Border>

  80:         <Grid x:Name="GridShadows" Margin="-2,-2,-6,-6">

  81:             <Rectangle Stroke="Black" Margin="5" RadiusX="7" RadiusY="7" Opacity="0.3"/>

  82:             <Rectangle Stroke="Black" Margin="4" RadiusX="8" RadiusY="8" Opacity="0.25"/>

  83:             <Rectangle Stroke="Black" Margin="3" RadiusX="9" RadiusY="9" Opacity="0.2"/>

  84:             <Rectangle Stroke="Black" Margin="2" RadiusX="10" RadiusY="10" Opacity="0.15"/>

  85:             <Rectangle Stroke="Black" Margin="1" RadiusX="11" RadiusY="11" Opacity="0.1"/>

  86:             <Rectangle Stroke="Black" Margin="-37,0,37,0" RadiusX="12" RadiusY="12" Opacity="0.05"/>

  87:         </Grid>

  88:     </Grid>

  89: </UserControl>

Las diferencias para ambos han sido que el control de usuario tiene el XAML fuertemente asociado a la clase que implementa el control (el code behind, vamos), mientras que en el caso del control de plantilla define un fichero aparte con la correspondiente plantilla para cambiar el aspecto del control (Template)

   1: <Style x:Name="EstiloNodoGridView" TargetType="local:NodeControl2">

   2:     <Setter Property="Template">

   3:         <Setter.Value>

   4:             <ControlTemplate TargetType="local:NodeControl2">

   5:                 <Grid x:Name="CtrlMainLayout">

   6:                     <Border x:Name="MainBorder" Style="{StaticResource MainBorderDefaultNodeStyle}">

   7:

   8:                         <Border.BorderBrush>

   9:                             <SolidColorBrush x:Name="MainBorderColor" Color="#FF0067BD" />

  10:                         </Border.BorderBrush>

  11:

  12:                         <Border Style="{StaticResource InnerBorderDefaultNodeStyle}">

  13:

  14:                             <Border.BorderBrush>

  15:                                 <SolidColorBrush x:Name="InnerBorderColor" Color="#FF84C7FF" />

  16:                             </Border.BorderBrush>

  17:

  18:                             <Grid MinHeight="150" MinWidth="150">

  19:                                 <Grid.RowDefinitions>

  20:                                     <RowDefinition Height="10*"/>

  21:                                     <RowDefinition Height="50*"/>

  22:                                     <RowDefinition Height="10*"/>

  23:                                 </Grid.RowDefinitions>

  24:

  25:                                 <TextBlock

  26:                                     x:Name="TitleBlock"

  27:                                     Margin="0"

  28:                                     Text="Prueba"

  29:                                     FontSize="24"

  30:                                     Padding="6,2,0,0"

  31:                                     Foreground="Black"

  32:                                     Grid.Row="0">

  33:

  34:                                 </TextBlock>

  35:

  36:                                 <Rectangle

  37:                                     Grid.Row="0"

  38:                                     Fill="{StaticResource NodeCaptionBackground}"

  39:                                     Canvas.ZIndex="-1"/>

  40:

  41:                                 <TextBlock

  42:                                     x:Name="DescriptionBlock"

  43:                                     Text="Texto descriptivo que podría ocupar perfectamente más de una línea"

  44:                                     TextWrapping="Wrap"

  45:                                     Padding="4,4,4,0"

  46:                                     Foreground="Black"

  47:                                     Grid.Row="1"/>

  48:

  49:                                 <Rectangle Grid.Row="1" Fill="White" Canvas.ZIndex="-1" />

  50:

  51:                                 <HyperlinkButton

  52:                                     x:Name="LinkMoreInfo"

  53:                                     VerticalAlignment="Bottom"

  54:                                     Grid.Row="2" HorizontalAlignment="Right">www.bing.es</HyperlinkButton>

  55:

  56:                                 <Rectangle Grid.Row="2" Fill="White" Canvas.ZIndex="-1" />

  57:                             </Grid>

  58:                         </Border>

  59:                     </Border>

  60:                     <Grid x:Name="GridShadows" Margin="-2,-2,-6,-6">

  61:                         <Rectangle Stroke="Black" Margin="5" RadiusX="7" RadiusY="7" Opacity="0.3"/>

  62:                         <Rectangle Stroke="Black" Margin="4" RadiusX="8" RadiusY="8" Opacity="0.25"/>

  63:                         <Rectangle Stroke="Black" Margin="3" RadiusX="9" RadiusY="9" Opacity="0.2"/>

  64:                         <Rectangle Stroke="Black" Margin="2" RadiusX="10" RadiusY="10" Opacity="0.15"/>

  65:                         <Rectangle Stroke="Black" Margin="1" RadiusX="11" RadiusY="11" Opacity="0.1"/>

  66:                         <Rectangle Stroke="Black" Margin="-37,0,37,0" RadiusX="12" RadiusY="12" Opacity="0.05"/>

  67:                     </Grid>

  68:                 </Grid>

  69:             </ControlTemplate>

  70:         </Setter.Value>

  71:     </Setter>

  72: </Style>

(Os recuerdo que estoy aprendiendo, así que encontraréis probablemente cosas que haya que definir de mejor manera: estoy pensando en estilos fundamentalmente…)

Dándole un poco de interactividad

El objetivo es que el usuario pueda seleccionar nodos para editar sus contenidos, arrastrarlos por la ventana para ajustar su disposición, relacionarlos entre sí…)

Voy a empezar por algo sencillo (y que sólo tiene sentido si se está utilizando un ratón). Destacar el control que se encuentre bajo el cursor cambiando simplemente el color del borde (de momento a un rojo chillón para destacar, más adelante intentaremos mejorar el aspecto visual).

Mi primera tentación fue (tirando de experiencias previas desarrollando con aplicaciones Windows puras y duras) tirar de manejadores de eventos. Sin embargo, unas pocas visitas a la red parecieron indicar que esa era una de las peores ideas que se me podrían haber ocurrido.

   1: protected override void OnPointerEntered(Windows.UI.Xaml.Input.PointerRoutedEventArgs e)

   2: {

   3:     base.OnPointerEntered(e);

   4:

   5:     // ¿Qué tal si cambio aquí el aspecto del borde de mi control?

   6: }

   7:

   8: protected override void OnPointerExited(Windows.UI.Xaml.Input.PointerRoutedEventArgs e)

   9: {

  10:     base.OnPointerExited(e);

  11:

  12:     // Y aquí me limito a volverlo como estaba. ¿No?

  13: }

Así que a seguir investigando.

Tras algunas vueltas más salieron a flote algunos conceptos. Triggers y el VisualStateManager.

Resulta que los Triggers no funcionan en el XAML de Windows 8, así que me quedé con la otra opción.

El VisualStateManager

En esencia, se trata de un componente que me permite definir estados visuales de un control, grupos de estados visuales y (opcionalmente) transiciones para cambiar de un estado a otro. En resumen, ayuda a dejar en la capa de presentación la lógica de presentación asociada al cambio del estado visual de un control. Resumiendo: “evita” que tengas que escribir código en el Code Behind (ya veremos que he tenido que meter una línea, al menos).

Dentro del grid externo que defino en mi control (el que tiene como nombre “CtrlMainLayout”) me he limitado a introducir el siguiente código.

   1: <VisualStateManager.VisualStateGroups>

   2:     <VisualStateGroup x:Name="NodeStates">

   3:         <VisualStateGroup.Transitions>

   4:             <VisualTransition To="PointerOver" GeneratedDuration="0:0:0.5" />

   5:         </VisualStateGroup.Transitions>

   6:

   7:         <VisualState x:Name="Normal" />

   8:         <VisualState x:Name="PointerOver">

   9:             <Storyboard>

  10:                 <ColorAnimation

  11:                     Storyboard.TargetName="MainBorderColor"

  12:                     Storyboard.TargetProperty="Color"

  13:                     To="Red" />

  14:                 <ColorAnimation

  15:                     Storyboard.TargetName="InnerBorderColor"

  16:                     Storyboard.TargetProperty="Color"

  17:                     To="OrangeRed" />

  18:             </Storyboard>

  19:         </VisualState>

  20:     </VisualStateGroup>

  21:     <VisualStateGroup x:Name="FocusStates"/>

  22: </VisualStateManager.VisualStateGroups>

Lo que este código quiere decir es lo siguiente.

  1. He definido un grupo de estados visuales llamado “NodeStates”
  2. Dentro de dicho grupo he definido dos estados visuales
  1. Normal
  2. PointerOver
  • El estado visual “PointerOver” tiene definidas dos animaciones de color (una para cada uno de los bordes, interior y exterior, que defino dentro de mi control). En ambos casos pasamos del color que sea a Rojo (en el caso del borde exterior) y Naranja rojizo (en el caso del interior)
  • Una transición que indica que, cuando se transicione de cualquier estado (no se define el atributo “from”) al estado “PointerOver” la duración de la animación será de cinco segundos.

Bueno, pues ya está. ¿No? La web está llena de ejemplos que aplican a la plantilla de un botón y no hace falta nada más…

Craso error: compilo, paso mi cursor por encima del control y nada de nada.

¿Cómo cambio entre estados visuales?

Otra vez a internet y a darle vueltas. Después de cierta cantidad de esfuerzo (acompañado de bizquera) descubrí por qué funcionaba para un botón y no para mi control y es que, el botón ya tiene definidos una serie de estados visuales (Default, PointerOver, Pressed, Disabled) que gestiona internamente. Pero mi control no.

Así que, aquí viene la línea de código en mi code-behind (vale, en realidad son dos).

   1: protected override void OnPointerEntered(Windows.UI.Xaml.Input.PointerRoutedEventArgs e)

   2: {

   3:     base.OnPointerEntered(e);

   4:

   5:     VisualStateManager.GoToState(this, "PointerOver", true);

   6: }

   7:

   8: protected override void OnPointerExited(Windows.UI.Xaml.Input.PointerRoutedEventArgs e)

   9: {

  10:     base.OnPointerExited(e);

  11:

  12:     VisualStateManager.GoToState(this, "Normal", true);

  13: }

Sencillamente: capturo los eventos de entrada y salida del cursor sobre y desde el control y utilizo el VisualStateManager para indicar el cambio de estado (hacia “PointerOver” cuando el cursor entra y hacia “Normal” cuando el cursor sale).

¡Y ya está, funcionando!

image

(Los controles de los extremos son controles de plantilla y el central un control de usuario)

¿Alguien conoce una mejor forma de manejar situaciones como estas?

Empezando con Windows Phone 7.1 (y XXI)–La importancia de IoC en el diseño de pantallas

¿Y qué tendrá que ver una cosa con la otra? Podrías preguntarte.

Pues lo cierto es que, por la forma en que funcionan las principales herramientas de diseño (Visual Studio y Blend) para Windows Phone, la importancia es mucha.

¿Cómo funciona?

Tanto Visual Studio como Blend compilan el código de tu solución antes de tratar de mostrarte en pantalla el diseño de la página (pantalla).

image

image

Lo que pasa es que ambas son extremadamente cuidadosas en el control de errores, así que, cuando salta una excepción tratan de mostrarte todo lo posible. Y lo que no, lo dejan sin mostrar.

En una aplicación más o menos típica se consumirán servicios web o proveedores de datos antes de mostrar la información en pantalla. Puede ocurrir que algunas de estas fuentes de datos no se encuentren disponibles en tiempo de diseño. Aquí entra IoC.

¿Cuál es la relación entre IoC y la presentación en tiempo de Diseño?

Si estamos utilizando MVVM Light, disponemos de la clase ViewModelLocator.cs que es el proveedor de datos de toda nuestra aplicación (en otros frameworks las soluciones serán más o menos similares).

Si le echas un vistazo al esqueleto que se presenta un poco más abajo verás alguna cosa interesante.

   1: public class ViewModelLocator

   2: {

   3:     static ViewModelLocator()

   4:     {

   5:         ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

   6:  

   7:         if (ViewModelBase.IsInDesignModeStatic)

   8:         {

   9:             SimpleIoc.Default.Register<IConfiguracionDataService, Design.ConfiguracionDataService>();

  10:             SimpleIoc.Default.Register<IContextoGeneralDeDatos, Design.ContextoGeneralDeDatos>();

  11:             SimpleIoc.Default.Register<IFourSquareContexto, Design.FourSquareContexto>();

  12:  

  13:             SimpleIoc.Default.Register<ILocationService, Design.LocationService>();

  14:         }

  15:         else

  16:         {

  17:             // Proveedores de datos

  18:             SimpleIoc.Default.Register<IConfiguracionDataService, ConfiguracionDataService>();

  19:             SimpleIoc.Default.Register<IContextoGeneralDeDatos, ContextoGeneralDeDatos>();

  20:             SimpleIoc.Default.Register<IFourSquareContexto, FourSquare.FourSquareContexto>();

  21:  

  22:             // Para localización

  23:             SimpleIoc.Default.Register<ILocationService, LocationService>();

  24:         }

  25:  

  26:         // Para los recursos

  27:         SimpleIoc.Default.Register<ITraductorAppBar, TraductorAppBar>();

  28:  

  29:         // Registramos VistasModelo

  30:         SimpleIoc.Default.Register<ConfiguracionViewModel>();

  31:         SimpleIoc.Default.Register<FourSquareLoginViewModel>();

  32:         SimpleIoc.Default.Register<CheckInViewModel>();

  33:         SimpleIoc.Default.Register<UsuarioViewModel>();

  34:         SimpleIoc.Default.Register<MainViewModel>();

  35:     }

  36: }

Sin entrar en mucho detalle, se puede ver como hay tres secciones importantes.

  1. La primera aplica sólo cuando estamos en modo de diseño (es decir, mostrando las páginas en el diseñador de Visual Studio o de Blend)
  2. La segunda aplica sólo cuando no estamos en modo de diseño.
  3. La tercera aplica en todos los casos.

Ya ves, utilizando IoC podemos proporcionar alternativas en tiempo de diseño y en tiempo de ejecución a servicios que, de otra manera nos darían problemas cuando no estamos ejecutando la aplicación.

El mismo esquema se utiliza para proporcionar conjuntos de pruebas.

Desarrollo de Windows Phone 7 con Visual Studio 2012 RC

Estoy decepcionado, no lo puedo evitar. Visual Studio 2012 RC sigue sin dar soporte a Windows Phone 7 SDK.

image

image

La pregunta del millón es: ¿Lo hará? Lo cierto es que sería lógico que sí, pero quizá la empresa de Redmon vaya  a centrarse en el soporte del próximo Windows Phone 8.

Bueno. Toca esperar y, mientras tanto, seguir con Visual Studio 2010.

Utilizando NuGet

Muchos ya conoceréis NuGet, que lleva dando vueltas por ahí desde hace ya algún tiempo.

El propósito de NuGet es proporcionar una herramienta sencilla dentro del entorno Visual Studio (y fuera, mediante línea de comandos) para incorporar packetes y herramientas de terceros a tus desarrollos. Y lo mejor: gestiona las dependencias, de forma que, si estás interesado en un paquete concreto A para tu desarrollo y este, a su vez, depende de  otro paquete B, NuGet se encarga por ti de descargarte ambos paquetes A y B.

En el proyecto de Windows Phone 7 que estoy llevando a cabo en estos momentos hace uso de un par de utilidades de terceros; MVVM Light y Newtonsoft Json. Ambos pueden descargarse utilizando NuGet (aquí y aquí).

Es común ver cómo los paquetes y dependencias de terceros se incluyen en una . solución de Visual Studio (las DLLs, ejecutables, etc..). No voy a meterme en soluciones que se aplican en entornos de empresa cuando hay instalada alguna herramienta o solución avanzada de Gestión de la Configuración (SCM del inglés System Configuration Management)

Volviendo al caso de andar por casa:

El proceso es muy sencillo. Una vez instalado el addin de Visual Studio NuGet Manager Package, dispondrás de una serie de complementos como la consola del gestor de paquetes.

image

Es perfectamente posible forzar a NuGet a descargar las dependencias a una ruta específica así que es muy sencillo integrar con Team Fondation Server (echa un vistazo aquí). Simplemente pulsa el botón derecho sobre la solución y activa “Enable NuGet Package Restore” de esta forma todas las personas del equipo (en mi caso sólo yo desde dos máquinas diferentes) disponen no sólo de las dependencias, también de la posibilidad de que estas dependencias se descarguen automáticamente en la compilación en caso de que no estén en la máquina de desarrollo.