Empezando con Windows Phone 7.1 (y XV) – Navegación con MVVM Light

En Windows Phone 7, la navegación se maneja desde la vista. Concretamente, todas las páginas de una aplicación comparten la misma instancia del servicio de navegación a través de una propiedad: Page.NavigationService

Los patrones de diseño para la interfaz de usuario como MVP, MVC o MVVM procuran sacar de la Vista toda la lógica de navegación posible. Nos encontramos, sin embargo, que en Windows Phone, el servicio de navegación se accede desde la propia PhoneApplicationPage. Concretamente, en una aplicación WP7 el responsable de la navegación es el marco principal (main frame) o RootFrame.

Existen varias alternativas para implementar navegación utilizando el patrón MVVM y, concretamente, haciendo uso de MVVM Light.

Navegación con Mensajes

Una primera forma es haciendo uso de la infraestructura de mensajes que proporciona MVVM Light. Esta infraestructura es utilizada con frecuencia en el patrón MVP para coordinar diferentes vistas. Me dejo una entrada en el futuro para los detalles de este tipo de infraestructuras. Si queréis saber cómo implementar navegación haciendo uso de esta técnica, echadle un vistazo a este artículo.

Utilizando el Servicio de Navegación desde la Vista Modelo

Para esta entrada he preferido sin embargo, otra aproximación que es hacer uso del servicio de navegación desde la Vista-Modelo.

Si le echáis un vistazo al servicio de navegación que tenemos en Windows Phone, os encontraréis con esto.

image

De toda la firma me voy a quedar con lo justo declarando una interfaz muy sencilla.

 1: /// <summary>

 2: /// Interfaz del servicio de navegación.

 3: /// </summary>

 4: public interface INavigationService

 5: {

 6:     /// <summary>

 7:     /// Evento de navegación

 8:     /// </summary>

 9:     event NavigatingCancelEventHandler Navigating;

 10:

 11:     /// <summary>

 12:     /// Permite navegar a la página indicada como parámetro

 13:     /// </summary>

 14:     /// <param name="pageUri">Instancia de <see cref="Uri"/> que hace referencia a la página a la que se navega</param>

 15:     void NavigateTo(Uri pageUri);

 16:

 17:     /// <summary>

 18:     /// Permite navegar hacia atrás en la pila de navegación

 19:     /// </summary>

 20:     void GoBack();

 21: }

Ahora, crearemos una clase para el servicio de navegación que implemente esta interfaz (acordaos de lo que os decía más arriba sobre el hecho de que es el RootFrame de una página el que se encarga de acceder al servicio).

En mi caso, estas clases se están yendo a una biblioteca de utilidades con lo que tendré que añadir una referencia al Assembly Microsoft.Phone.

 1: namespace Jdmveira.WindowsPhone.Mvvm.Navigation

 2: {

 3:     using System;

 4:     using System.Windows;

 5:     using Microsoft.Phone.Controls;

 6:

 7:     /// <summary>

 8:     /// Clase encargada de implementar el servicio de navegación

 9:     /// </summary>

 10:     public class NavigationService : INavigationService

 11:     {

 12:         #region Constructores

 13:

 14:         /// <summary>

 15:         /// Constructor por defecto de la clase

 16:         /// </summary>

 17:         public NavigationService()

 18:         {

 19:             // No podemos estar seguros de que en cualquier momento existe el RootFrame.

 20:             EnsureMainFrame();

 21:         }

 22:

 23:         #endregion Constructores

 24:

 25:         #region Miembros Públicos

 26:

 27:         /// <summary>

 28:         /// Evento de navegación

 29:         /// </summary>

 30:         public event System.Windows.Navigation.NavigatingCancelEventHandler Navigating;

 31:

 32:         /// <summary>

 33:         /// Método para navegar a la página indicada por la <see cref="Uri"/> pasada como parámetro

 34:         /// </summary>

 35:         /// <param name="pageUri">Instancia de <see cref="Uri"/> que indica la página a la que se navega</param>

 36:         public void NavigateTo(Uri pageUri)

 37:         {

 38:             if (EnsureMainFrame())

 39:             {

 40:                 _mainFrame.Navigate(pageUri);

 41:             }

 42:         }

 43:

 44:         /// <summary>

 45:         /// Navega hacia atrás en la pila de navegación

 46:         /// </summary>

 47:         public void GoBack()

 48:         {

 49:             if (EnsureMainFrame())

 50:             {

 51:                 _mainFrame.GoBack();

 52:             }

 53:         }

 54:

 55:         #endregion Miembros Públicos

 56:

 57:         #region Miembros Privados

 58:

 59:         /// <summary>

 60:         /// Se asegura de que existe una referncia al frame principal de una página

 61:         /// </summary>

 62:         private bool EnsureMainFrame()

 63:         {

 64:             if (_mainFrame != null)

 65:                 return true;

 66:

 67:             _mainFrame = Application.Current.RootVisual as PhoneApplicationFrame;

 68:             if (_mainFrame != null)

 69:             {

 70:                 // Adjuntamos nuestro propio manejador de eventos

 71:                 _mainFrame.Navigating += (s, e) =>

 72:                     {

 73:                         if (Navigating != null)

 74:                             Navigating(s, e);

 75:                     };

 76:

 77:                 return true;

 78:             }

 79:

 80:             return false;

 81:         }

 82:

 83:         private PhoneApplicationFrame _mainFrame;

 84:

 85:         #endregion Miembros Privados

 86:     }

 87: }

A partir de aquí, registrar el servicio en nuestro IoC y utilizarlo desde cualquier Vista-Modelo es un asunto trivial.

Echadle un vistazo a este post, de donde he sacado esta solución.

Pegas a esta implementación

Fundamentalmente, que en el fondo, la implementación depende de la clase PhoneApplicationFrame, pero no podemos evitar esto. ¿Verdad? Guiño

Anuncios

Empezando con Windows Phone 7.1 (y XIV) – Asignando Comandos a Eventos.

En anteriores posts hemos visto que algunos controles de presentación, como la Barra de Aplicación, no son sencillos de montar con un patrón MVVM (fundamentalmente porque son objetos del shell y no objetos Silverlight).

En este caso vamos a ver como otros controles Silverlight que no se ajustan exactamente al modelo y, aun así, podremos manejar eventos con facilidad utilizando el patrón.

Para ello vamos a basarnos en una página de configuración que estoy montando en mi próximo proyecto.

 

image

Como podéis ver, los dos controles inferiores permiten activar y desactivar notificaciones (Live Tiles y Toast Notifications). Ambos controles son de la clase ToggleSwitch.

La clase ToggleSwitch describe eventos para manjar (entre otros) el Click, Checked y Unchecked. Sin embargo, no dispone de la posibilidad de invocar un comando (como sí pasa con Button).

Aquí es donde vamos a trabajar con una funcionalidad incorporada en el SDK 7.1 que nos permitirá asociar fácilmente (y de forma declarativa) eventos a comandos.

Lo primero es hacer referencia en la página al siguiente espacio de nombres.

   1:  

   2: xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

   3:  

A continuación, en cada uno de los controles declararemos “disparadores” (Triggers) que invocarán un comando.

   1: <tool:ToggleSwitch Grid.Column="1" x:Name="PermitirTiles" Header="{Binding Source={StaticResource LocationHelper}, Path=Recursos.PermitirActiveTiles}" Content="{Binding Path=TextoActivarTiles}" IsChecked="{Binding Path=ActivarTiles}">

   2:    <i:Interaction.Triggers>

   3:        <i:EventTrigger EventName="Checked">

   4:            <i:InvokeCommandAction Command="{Binding Path=ComandoActivarTiles}" CommandParameter="{Binding ElementName=PermitirTiles, Path=IsChecked}" />

   5:        </i:EventTrigger>

   6:  

   7:        <i:EventTrigger EventName="Unchecked">

   8:            <i:InvokeCommandAction Command="{Binding Path=ComandoActivarTiles}" CommandParameter="{Binding ElementName=PermitirTiles, Path=IsChecked}" />

   9:        </i:EventTrigger>

  10:    </i:Interaction.Triggers>

  11: </tool:ToggleSwitch>

Como podéis ver, en este caso se disparan “Triggers” en los eventos Checked y Unchecked del control, en ambos casos se invoca al mismo comando y, en ambos casos pasando como parámetro la propiedad "IsChecked” del propio control. La diferencia es que, dependiendo del evento lanzado, dicha propiedad valdrá true o false.

A continuación en el ModeloVista declaremos las siguientes propiedades (he eliminado algo de código para facilitar el seguimiento).

   1: public class ConfiguracionViewModel : ViewModelBase

   2: {

   3:     private ICommand _comandoActivarTiles;

   4:     private ICommand _comandoActivarToast;

   5:  

   6:     /// <summary>

   7:     /// Initializes a new instance of the ConfiguracionViewModel class.

   8:     /// </summary>

   9:     /// <param name="dataService">Instancia de <see cref="IConfiguracionDataService"/> que proporciona acceso al servicio de datos</param>

  10:     public ConfiguracionViewModel(IConfiguracionDataService dataService)

  11:     {

  12:         if (dataService != null)

  13:             _dataService = dataService;

  14:     

  15:         // Creamos el comando para activar /desactivar tiles y el de activar /desactivar toast

  16:         _comandoActivarTiles = new RelayCommand<bool>((bool p) => ActivarTiles = p);

  17:         _comandoActivarToast = new RelayCommand<bool>((bool p) => ActivarToast = p);

  18:     

  19:         LoadData();

  20:     }

  21:  

  22:     private static readonly string TextoActivarTilesNombreProp = "TextoActivarTiles";

  23:  

  24:     /// <summary>

  25:     /// Texto que se mostrará dependiendo del estado actual de la activación de tiles

  26:     /// </summary>

  27:     public string TextoActivarTiles

  28:     {

  29:         get { return _textoActivarTiles; }

  30:         private set

  31:         {

  32:             _textoActivarTiles = value;

  33:             RaisePropertyChanged(TextoActivarTilesNombreProp);

  34:         }

  35:     }

  36:  

  37:     private static readonly string TextoActivarToastNombreProp = "TextoActivarToast";

  38:  

  39:     /// <summary>

  40:     /// Texto que se mostrará dependiendo del estado actual de la activación de tiles

  41:     /// </summary>

  42:     public string TextoActivarToast

  43:     {

  44:         get { return _textoActivarToast; }

  45:         private set

  46:         {

  47:             _textoActivarToast = value;

  48:             RaisePropertyChanged(TextoActivarToastNombreProp);

  49:         }

  50:     }

  51:  

  52:     /// <summary>

  53:     /// Propiedad que permite acceder al comando de activación /desactivación de tiles

  54:     /// </summary>

  55:     public ICommand ComandoActivarTiles

  56:     {

  57:         get { return _comandoActivarTiles; }

  58:     }

  59:  

  60:     /// <summary>

  61:     /// Propiedad que permite acceder al comando de activación /desactivación de notificaciones Toast

  62:     /// </summary>

  63:     public ICommand ComandoActivarToast

  64:     {

  65:         get { return _comandoActivarToast; }

  66:     }

  67:  

  68:     private static readonly string ActivarTilesNombreProp = "ActivarTiles";

  69:  

  70:     /// <summary>

  71:     /// Permite acceder al valor que indica si los tiles activos están o no habilitados

  72:     /// </summary>

  73:     public bool? ActivarTiles

  74:     {

  75:         get { return _preferencias.EnableTileNotif; }

  76:         set

  77:         {

  78:             if (_preferencias.EnableTileNotif == value)

  79:                 return;

  80:  

  81:             _preferencias.EnableTileNotif = value;

  82:             TextoActivarTiles = ObtenerCaptionToggleSwitch(value);

  83:  

  84:             RaisePropertyChanged(ActivarTilesNombreProp);

  85:         }

  86:     }

  87:  

  88:     private static readonly string ActivarToastNombreProp = "ActivarToast";

  89:  

  90:     /// <summary>

  91:     /// Permite acceder al valor que indica si las notificaciones toast están o no habilitadas

  92:     /// </summary>

  93:     public bool? ActivarToast

  94:     {

  95:         get { return _preferencias.EnableToastNotif; }

  96:         set

  97:         {

  98:             if (_preferencias.EnableToastNotif == value)

  99:                 return;

 100:  

 101:             _preferencias.EnableToastNotif = value;

 102:             TextoActivarToast = ObtenerCaptionToggleSwitch(value);

 103:  

 104:             RaisePropertyChanged(ActivarToastNombreProp);

 105:         }

 106:     }

 107:  

 108:     /// <summary>

 109:     /// Devuelve una cadena con el título de un ToggleSwitch en función del valor pasado como parámetro.

 110:     /// </summary>

 111:     /// <param name="param">Valor booleano asociado al estado del ToggleSwitch</param>

 112:     /// <returns>Cadena con el título que debe de mostrar el ToggleSwitch</returns>

 113:     private static string ObtenerCaptionToggleSwitch(bool? param)

 114:     {

 115:         return (param.HasValue && param.Value) ? Resources.CaptionActivo : Resources.CaptionInactivo;

 116:     }

 117: }

Esencialmente se crean comandos que cambian el valor del modelo y los literales asociados al estado del control (buscando dichos literales en los recursos).

La clase RelayCommand está declarada en el framework MVVM Light.

El código no está limpio pero probado y funciona.

image

Empezando con Windows Phone 7.1 (y XIII) – Creando una nueva vista. Navegación entre páginas.

¡El horror, oh el horror!

En el anterior post nos quedamos con una forma de crear un nuevo lugar un tanto deleznable.

image

Así pues, creo que lo mejor es hacer que la nueva interfaz aproveche la barra de aplicación de Windows Phone.

Aplicando algunos cambios.

Para ello, he realizado algunos cambios en la aplicación. He creado una carpeta en la solución donde podré organizar más fácilmente las páginas y he movido la página principal a dicha carpeta.

image

Esto me ha obligado a realizar algunos cambios menores en el código.

En el manifiesto de la aplilcación

image

En el cual se indica la página de inicio para la aplicación, he cambiado la ruta.

 1: <Tasks>

 2:     <DefaultTask

 3:         Name="_default"

 4:         NavigationPage="/Pages/MainPage.xaml" />

 5: </Tasks>

Y, en la propia página, he cambiado la url relativa a las imágenes.

 1: <controls:Panorama.Background>

 2:     <ImageBrush

 3:         ImageSource="../Resources/images/Panorama%20Green.png"/>

 4: </controls:Panorama.Background>

Ahora vamos a realizar algunos cambios en la página principal. De momento eliminaremos el PanoramaItem que estaba destinado a contener los controles para crear un nuevo lugar y añadiremos la posibilidad de crear un nuevo lugar desde cualquier punto de la página principal. Para ello, vamos a crear una ApplicationBar en la página principal.

 1: <phone:PhoneApplicationPage.ApplicationBar>

 2:     <shell:ApplicationBar

 3:             Mode="Minimized"

 4:             IsMenuEnabled="False"

 5:             Opacity="0.7">

 6:

 7:         <shell:ApplicationBarIconButton

 8:             x:Name="NuevoLugarButton"

 9:             IconUri="/Resources/icons/Add%20New%20Place.png"

 10:             Text="Nuevo Lugar" />

 11:     </shell:ApplicationBar>

 12: </phone:PhoneApplicationPage.ApplicationBar>

Esto nos permite disponer de una barra de aplicaciones que no siempre estará visible, de esta forma aprovecharemos al máximo el espacio vertical.

Asignando Acciones a los botones de la ApplicationBar

Maldición. El primer problema que me he encontrado es. ¿Cómo puedo utilizar Comandos con los botones de la Application Bar para aprovecharme del patrón visto antes en MVVM? El control dispone de una propiedad Click que te permite asignar código en el “Code Behind” de la Vista, pero no de la VistaModelo como vimos en el anterior post (mediante Binding).

El otro problema es. ¿Cómo puedo asignar al texto del botón una entrada de mi fichero de recursos? Ni con Visual Studio ni con Blend me permite hacer uso de mi clase para manejar los recursos de la aplicación.

Proporcionando soporte multi-idioma y comandos en un ApplicationBarIconButton

¿Queréis la respuesta rápida?

No es posible sin cierta cantidad de esfuerzo ya que la barra de aplicación no es un componente Silverlight. Se trata de un objeto controlado por el Shell de Windows Phone, con lo que no aplican las mismas reglas que a cualquier otro control que incluyas en tu aplicación.

Confused smile

Resumiento la situación actual

En fin, algo que parecía bastante trivial se está convirtiendo en un pequeño problema:

  1. La barra de aplicación no es un control Silverlight, con lo que trabajar con la misma haciendo uso del patrón MVVM resulta complicado (recordad que la idea es evitar, en la medida de lo posible) tocar el Code Behind de la página.
  2. La navegación se realiza mediante la clase NavigationService que está accesible desde la propia págia como una propiedad.
  3. Desde la VistaModelo deberíamos de evitar instanciar nuevas clases que nos introduzcan dependencias (si tienes la palabra clave “new” en tu código lo mejor que puedes hacer es revisar el código). Un patrón como IoC viene aquí que ni al pelo.
  4. Crear una nueva vista y asociar correctamente su VistaModelo de forma flexible.

Siguientes Pasos

Si bien es perfectamente posible realizar todas las tareas descritas en el punto anterior (con mayor o menor esfuerzo). Este es un buen momento para hacer un alto en el camino y revisar algunas opciones que nos ayudarán con el Modelo Vista Vista Modelo y proseguir con el desarrollo apoyándonos en alguno de los siguientes Frameworks.

Empezando con Windows Phone (y XII) – Iconos, Botones y Metro Studio

¿Para qué?

Llegado a este punto, me queda asignar cierta lógica a la interfaz de alta de nuevos lugares. Actualmente (por poco tiempo, espero) se encuentra dentro del ModeloVista de la página principal, en breve se convertirá en una página aparte (momento en que aprovecharé para trastear un poco con la navegación).

Asignando Comandos a un Botón (al estilo MVVM)

La interfaz de alta de nuevos lugares es muy sencilla:

  1. Un bloque de texto que indica que este es el sitio de tu aplicación preparado para el alta.
  2. Un cuadro de texto encargado de recoger el nombre del nuevo lugar.
  3. Un botón para crear el nuevo lugar.

Para el botón, simplemente he enlazado el Comando al creado en la VistaModelo y, como parámetro, el texto de la caja de texto que se utiliza para crear el nombre del nuevo lugar. Así:

 1: <Button

 2:     Command="{Binding Path=CrearNuevoLugar}"

 3:     CommandParameter="{Binding ElementName=NuevoLugarTextBox, Path=Text}"

 4:

 5:     Margin="0,0,0,0"

 6:     x:Name="NuevoLugarBtn"

 7:     Grid.Row="2"

 8:     Content="{Binding LocalizedResources.TituloBotonNuevoLugar, Source={StaticResource LocalizationHelper}}"

 9:     ClickMode="Press"

 10:     Background="{StaticResource PhoneBackgroundBrush}"

 11:     BorderBrush="{StaticResource PhoneBorderBrush}"

 12:     Foreground="{StaticResource PhoneForegroundBrush}"/>

El “Binding” está realizado contra la propiedad “CrearNuevoLugar” que está expuesta en la vista.

 1: public class MainViewModel : IMainViewModel

 2: {

 3:     public MainViewModel()

 4:     {

 5:         _comandoCrearLugar = new DelegateCommand(AccionCrearNuevoLugar, PuedeCrearNuevoLugar);

 6:         _nombreNuevoLugar = Resources.MisSitiosFavoritosResources.TextoNuevoLugar;

 7:

 8:         ActualizarVistas();

 9:     }

 10:

 11:     public ICommand CrearNuevoLugar

 12:     {

 13:         get { return _comandoCrearLugar; }

 14:     }

 28: }

Por último, el comando delega la funcionalidad en dos métodos de la propia vista (AccionCrearNuevoLugar y PuedeCrearNuevoLugar). El primero se encarga de crear un nuevo lugar, el segundo se utiliza para determinar si dicha acción puede llevarse o no a cabo.

 1: private void AccionCrearNuevoLugar(object nuevo)

 2: {

 3:     if (nuevo == null)

 4:         throw new ArgumentException("nuevo");

 5:

 6:     var sNuevo = nuevo.ToString();

 7:     MiLugar nuevoLugar = Logica.NuevoLugar(sNuevo, string.Empty);

 8: }

 9:

 10: private bool PuedeCrearNuevoLugar(object nuevo)

 11: {

 12:     var result = false;

 13:     if (nuevo != null)

 14:     {

 15:         string sNuevo = nuevo.ToString();

 16:

 17:         result = !String.IsNullOrEmpty(sNuevo) && (String.Compare(sNuevo, Resources.MisSitiosFavoritosResources.TextoNuevoLugar) != 0);

 18:     }

 19:

 20:     return result;

 21: }

Ambos métodos recogen como parámetro el nombre del nuevo lugar. Uno de ellos se limita a crear una instancia del nuevo lugar, el otro comprueba el nombre (asegurándose de que no está vacío y que, además, no es la cadena de texto por defecto que se muestra en el control).

Lo bonito de esta aproximación es lo siguiente:

  1. Nada de código en un manejador de eventos que se dispara cuando se presiona el botón.
  2. De nuevo el Modelo-Vista no sabe absolutabmente nada de la vista y, con la ayuda de la clase DelgateCommand la lógica correspondiente al comando sigue dentro del Modelo-Vista.

Bueno, no mucho más que decir. Ahora a crear el botón.

Creando un botón (feo) y otro (menos feo) con Metro Studio

Ha llegado el momento de tener lista la interfaz para dar de alta un nuevo lugar (en este momento no está separada de la vista principal, pero en breve lo estará).

El primer intento consistió en añadir un simple botón con un texto en su interior.

image

Como podéis ver, se trata de un horrible botón que me permite dar de alta un nuevo lugar de forma bastante sencilla utilizando DataBinding.

En esta primera versión voy a reemplazar dicho botón por otro con un estilo visual más cercano a Metro. Para ello voy a utilizar una herramienta gratuita de Syncfusion llamada Metro Studio.

Metro Studio es una colección de iconos completamente gratuitos que siguen las normas de estilo y diseño de la interfaz Metro (la propia interfaz de la herramienta sigue dichas normas). Es muy sencilla de utilizar.

image

Simplemente busca iconos en el buscador o utilizando las categorías.

image

Selecciona tu icono (en mi caso, un original “Save”) y edítalo haciendo uso de una herramienta muy sencilla.

image

Finalmente puedes optar por almacenar el XAML generado por la herramienta.

image

O bien, obtener una imagen de la misma en tu formato preferido.

image

El resultado: algo igual de feo y que habrá que cambiar: Crear una nueva vista con su página correspondiente y, probablemente, una barra de herramientas con la botonera correspondiente.

image

 1: <controls:PanoramaItem Header="{Binding LocalizedResources.TituloNuevoSitio, Source={StaticResource LocalizationHelper}}" Orientation="Vertical" Foreground="{StaticResource PhoneBackgroundBrush}">

 2:     <Grid x:Name="LayoutForm" Background="Transparent">

 3:         <Grid.RowDefinitions>

 4:             <RowDefinition Height="Auto"/>

 5:             <RowDefinition Height="Auto" />

 6:             <RowDefinition Height="Auto"/>

 7:         </Grid.RowDefinitions>

 8:

 9:         <TextBlock Height="45" Margin="0,0,0,0" TextWrapping="Wrap" Text="{Binding LocalizedResources.TituloTextoNuevoLugar, Source={StaticResource LocalizationHelper}}" VerticalAlignment="Top" Width="400" Grid.Row="0" />

 10:         <TextBox Margin="0,0,0,0" x:Name="NuevoLugarTextBox" Text="{Binding Path=NombreNuevoLugar}" Style="{StaticResource LugarTextBox}" Padding="0,0,0,0" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" Grid.Row="1" />

 11:         <Button Margin="0,0,0,0" x:Name="NuevoLugarBtn" Command="{Binding Path=CrearNuevoLugar}" CommandParameter="{Binding ElementName=NuevoLugarTextBox, Path=Text}" Grid.Row="2" ClickMode="Press" Height="96" Width="96" HorizontalAlignment="Right">

 12:             <Button.Background>

 13:                 <ImageBrush ImageSource="Resources/images/SaveButton.png"/>

 14:             </Button.Background>

 15:         </Button>

 16:     </Grid>

 17: </controls:PanoramaItem>

Eso sí, tendrá que ser en el próximo post.

Winking smile

Empezando con Windows Phone 7.1 (y XI) – Un repaso a MVVM y el formulario de Nuevo Lugar

Para terminar de cerrar la funcionalidad básica de la aplicación de “Mis Lugares Favoritos” es necesario proporcionar al usuario la posibilidad de dar de alta un nuevo lugar.

Para ello bastará con hacer un formulario sencillo donde el usuario pueda rellenar los campos mínimos del nuevo lugar (me apunto la posibilidad de sugerir lugares próximos a la posición actual). En gran medida este formulario podrá utilizarse para modificar lugares existentes también.

Antes de lanzarme a construir dicho formulario me gustaría repasar un poco el patrón Modelo Vista Vista-Modelo.

El Patrón Modelo Vista VistaModelo

image

Como se puede ver en la figura superior el patrón es muy parecido al Modelo Vista Presentación.

  1. Vista: Representa lo que el usuario ve en pantalla. Idealmente, el código que contiene es un constructor y una llamada a InitializeComponents. La vista conoce al componente VistaModelo (normalmente a través de su DataContext). Esto se hace, idealmente, a través de una interfaz que evite el acoplamiento. Normalmente la forma en que  la Vista conoce a la VistaModelo es mediante el DataBinding y mediante Comandos. Hemos visto ya algo del DataBinding y, espero, en este blog veremos algo sobre los comandos.
  2. VistaModelo: Envía notificaciones a la Vista. Para evitar acoplamiento y que la VistaModelo sepa nada de la Vista, las notificaciones se envían implementando la interfaz INotifyPropertyChanged o INotifyCollectionChanged. A su vez, conoce el Modelo ya que lo actualiza. En general se encarga de sincronizar el trabajo entre Modelo y Vista, transformando o validando el Modelo antes de proporcionárselo a la Vista.
  3. Modelo: Envía notificaciones a la VistaModelo, de nuevo mediante la interfaz INotifyPropertyChanged o INotifyCollectionChanged. El Modelo no “conoce” a ninguno de los demás componentes. Suele encapsular la lógica de negocio y los datos.

¿Cómo se comunican los componentes?

Por un lado tenemos la forma en que comunicamos que una propiedad o una colección ha cambiado. Para ello disponemos de las interfaces INotifyPropertyChanged e INotifyCollectionChanged.

Por otro lado, es muy común (y es la razón por la que escribo este post) que la vista deba comunicar a la VistaModelo que ha de realizar algún tipo de acción sobre el modelo (lo común es que sea a petición del usuario) como añadir un elemento al modelo, modificar o borrar uno existente, etc… Para ello se hace uso de los Commands.

Commands

Como decía, es normal que el usuario quiera realizar algún tipo de operación sobre el modelo. Para ello se hace uso de Comandos. Los comandos son una abstracción que permite asociar una operación a un control en la Vista. Encapsulan el código relacionado con la operación de forma que se facilita el desacoplamiento de su representación en la vista (un botón, un control para ordenar. Vaya, multitud de posibilidades…).

La VistaModelo puede implementar dichos comandos de varias formas, pero resumiento, expondrá objetos que implementen la interfaz ICommand. De esta forma, la manera en que la Vista interactúa con el comando definido en la VistaModelo puede definirse de manera declarativa, eliminando así la necesidad de escribir código.

Nada de todo esto es un invento de Microsoft. Se trata de, sencillamente, la aplicación de patrones de diseño existentes a una tecnología (Silverlight) de manera que pueda sacársele el mayor partido.

¿Por qué?

Bueno, como ya hemos dicho. La implementación de patrones de diseño no sólo permite aplicar soluciones ya probadas a problemas concretos. En este caso, mediante el desacoplamiento y evitando el solapamiento de responsabilidades dispondremos de un código más fácil de mantener y probar.

¿Qué componentes puedo cambiar y de qué forma afectan a los demás componentes? Bueno, como se deduce del diagrama de componentes, el más delicado es el Mdelo, ya que cambios en el mismo van a obligarme a cambiar la VistaModelo. A su vez, cambios en la VistaModelo, pueden obligarme a cambiar la Vista. Si el “conocimiento” que cada componente tiene sobre otro es a través de una interfaz, entonces evitaré que cambios en la implementación de un componente puedan llegar a afectar a otro.

Al lío.

Bien, todo esta parrafada previa era necesaria para poder realizar la pantalla de creación de nuevo Lugar. La razón es evidente: necesito montar un comando que permita crear un nuevo lugar.

Para ello vamos a modificar la interfaz que implementa mi VistaModelo y vamos a crear una nueva propiedad que devuelva una instancia de ICommand y que nos permita crear un nuevo lugar. Cualquier clase que implemente esta interfaz tiene a su cargo dos responsabilidades:

  1. Definir una función específica (el método ICommand.Execute)
  2. Definir el estado de un componente visual que, normalmente, será el encargado de lanzar dicha función (la propiedad ICommand.CanExecute)

Por ejemplo, un botón que nos permita crear un nuevo lugar estará o no habilitado dependiendo de la propiedad CanExecute y, cuando sea pulsado por el usuario, invocará el método Execute. Para cumplir religiosamente con el patrón MVVM, se implementa también un evento, CanExecuteChanged, que indica si la propiedad CanExecute ha cambiado de estado o no.

Para ello, voy a crear primero un método dentro de mi VistaModelo que me permita crear un nuevo lugar. Este método no será invocado directamente desde la vista, así que no estará presente en la interfaz de mi VistaModelo.

image

Por otro lado, imaginad que creo otro dos o tres comandos (por ejemplo, ActualizarLugar, BorrarLugar y CompartirLugar). Esto significaría crear los correspondientes métodos en la VistaModelo y otras tres clases que implementen ICommand.

La solución está en la red (en docenas de sitios la solución aportada es más o menos la misma) yo no voy a ser menos y voy a montar una clase que implemente ICommand y que, a la vez, me permita delegar fácilmente la lógica en la VistaModelo sin hacer que el número de clases en mi proyecto crezca de forma descontrolada.

   1: public class DelegateCommand : ICommand

   2: {

   3:     private Func<object, bool> _puedeEjecutarAccion;

   4:     private Action<object> _accion;

   5:  

   6:     public DelegateCommand(Action<object> accion)

   7:         : this(accion, null)

   8:     {

   9:     }

  10:  

  11:     public DelegateCommand(Action<object> accion, Func<object, bool> puedeEjecutarAccion)

  12:     {

  13:         if (accion == null)

  14:         {

  15:             throw new ArgumentNullException(Resources.Jdmveira_WindowsPhone_Util.AccionParameterNull);

  16:         }

  17:  

  18:         _accion = accion;

  19:         _puedeEjecutarAccion = puedeEjecutarAccion;

  20:     }

  21:  

  22:     public event EventHandler CanExecuteChanged;

  23:  

  24:     public bool CanExecute(object parameter)

  25:     {

  26:         bool result = false;

  27:  

  28:         if (_puedeEjecutarAccion != null)

  29:             result = _puedeEjecutarAccion(parameter);

  30:  

  31:         return result;

  32:     }

  33:  

  34:     public void Execute(object parameter)

  35:     {

  36:         _accion(parameter);

  37:     }

  38:  

  39:     public void RaiseCanExecuteChanged(EventArgs args)

  40:     {

  41:         if (CanExecuteChanged != null)

  42:             CanExecuteChanged(this, args);

  43:     }

  44: }

Si tengo que ponerle una pega a la clase que podéis ver arriba es que no es muy limpia la forma en que se comunica que la propiedad CanExecute ha cambiado. Como la clase DelegateCommand delega la funcionalidad en métodos externos, no tiene control sobre cuándo ha cambiado el valor de CanExecute así que ha de “confiar” en que la clase externa que lo utiliza se acordará de invocar el evento correspondiente. Mientras intento dar con una forma más limpia y completa tendrá que valer. ¿No?
 
Ahora queda preparar el comando desde la VistaModelo para que pueda ser invocado fácilmente desde la vista (utilizando DataBinding).
   1: public class MainViewModel : IMainViewModel

   2: {

   3:     #region Constantes

   4:  

   5:     private const string PropNameItems = "Items";

   6:     private DataHelper _dataHelper;

   7:     private CollectionViewSource _favoritos;

   8:     private ObservableCollection<MiLugar> _items;

   9:     private ICommand _comandoCrearLugar;

  10:  

  11:     #endregion Constantes

  12:  

  13:     #region Constructores

  14:  

  15:     public MainViewModel()

  16:     {

  17:         // Si estamos en modo diseño no tiene sentido cargar la base de datos

  18:         if (!DesignerProperties.IsInDesignTool)

  19:             _dataHelper = new DataHelper();

  20:  

  21:         _items = new ObservableCollection<MiLugar>();

  22:         _favoritos = new CollectionViewSource();

  23:         _comandoCrearLugar = new DelegateCommand(AccionCrearNuevoLugar, PuedeCrearNuevoLugar);

  24:  

  25:         ActualizarVistas();

  26:     }

  27:  

  28:     public ICommand CrearNuevoLugar    

  29:     {

  30:         get { return _comandoCrearLugar; }

  31:     }

  32:  

  33:     private void AccionCrearNuevoLugar(object nuevo)

  34:     {

  35:         throw new NotImplementedException();

  36:     }

  37:  

  38:     private bool PuedeCrearNuevoLugar(object nuevo)

  39:     {

  40:         throw new NotImplementedException();

  41:     }

  42:  

  43:     ...

Por último, habrá que ir creando una nueva interfaz para que el usuario introduzca los datos del nuevo lugar. Aprovecharé una nueva entrada en el blog para este tema, ya que parece un buen momento para sacar información de localización del usuario (al fin y al cabo podemos asumir que está dando de alta un nuevo lugar cuando se encuentra en el mismo… Probablemente).

Empezando con Windows Phone 7.1 (y X) – Haciendo DataBinding sobre Colecciones

Tres modos de ver Mis Lugares

Como habéis visto, mi intención es que el control Panorama disponga de varias vistas sobre la misma colección de datos (Items).

El siguiente esquema muestra las clases que estoy utilizando en la capa de presentación.

Mis Lugares Favoritos: Clases MVVM
Mis Lugares Favoritos: Clases MVVM

Si le echáis un vistazo a la interfaz que declaro hay varias propiedades públicas que hacen referencia a diferentes filtros que voy a utilizar a la hora de mostrar mi conjunto de datos (Mis Lugares de Interés):

  • Items: Colección observable (ObservableCollection<T>) que contiene los lugares de interés del usuario.
  • Favoritos: Colección observable de aquellos lugares de interés que están marcados como favoritos.
  • Recientes: Colección observable de aquellos lugares de interés que se han creado recientemente.
  • Cercanos: Colección observable de aquellos lugares de interés que se encuentran próximos a la ubicación actual del usuario.

Como podéis ver, se trata de una coleccíon padre y diferentes subconjuntos de dicha colección. Uno podría suponer que algo así no nos debería de provocar demasiados problemas. Pongamos un ejemplo:

 1: public class MainViewModel: IMainViewModel {

 2:

 3:     public MainViewModel() {

 4:         Items = new ObservableCollection<MiLugar> ();

 5:     }

 6:

 7:     /// <summary>

 8:     /// La colección principal de lugares

 9:     /// </summary>

 10:     public ObservableCollection<MiLugar> Items { get; private set; }

 11:

 12:     /// <summary>

 13:     /// Filtra la colección principal por la propiedad Favorito

 14:     /// </summary>

 15:     public ObservableCollection<MiLugar> Favoritos {

 16:         get {

 17:

 18:             var result = from i en Items

 19:                          where i.Favorito == true

 20:                          select i;

 21:

 22:             return new ObservableCollection<MiLugar> (result);

 23:         }

 24:     }

 25:

 26:     /// <summary>

 27:     /// Ordena la colección principal mostrando primero los lugares más recientes

 28:     /// </summary>

 29:     public ObservableCollection<MiLugar> Recientes {

 30:         get {

 31:

 32:             var result = from i en Items

 33:                          orderby i.Creado descending

 34:                          select i;

 35:

 36:             return new ObservableCollection<MiLugar> (result);

 37:         }

 38:     }

 39:

 40:     /// <summary>

 41:     /// Ordena la colección principal mostrando primero los lugares más cercanos

 42:     /// </summary>

 43:     public ObservableCollection<MiLugar> Cercanos {

 44:         get {

 45:

 46:             // TODO: Mmmm... Aquí hay trabajo por hacer para ver cómo averiguamos los más "cercanos". De momento devolveremos todo

 47:             var result = from i en Items

 48:                          select i;

 49:

 50:             return new ObservableCollection<MiLugar> (result);

 51:         }

 52:     }

 53: }

Parece que no va a ser tan fácil

Una colección principal, dos o tres consultas Linq y devolvemos el resultado. ¿Sencillo, no?

Pues me temo que, además, completamente inútil: el código que veis arriba no nos sirve. En cualquier caso, dejad que os cuente alguna cosilla sobre el DataBinding y el ObservableCollection<T>. Cuando la página se carga, la infraestructura de DataBinding se subscribe a cambios en las colecciones. La idea es buena: mi vista expone unas pocas colecciones de datos que implementan el patrón Observable (no confundir con el patrón Observer), cuando cargo mis vistas con datos filtrados, la infraestructura de DataBinding se da cuenta y voilá, muestra automáticamente los datos actualizados. Lástima que, cada vez que realizo un filtrado de datos creo una nueva colección observable. La infraestructura de DataBinding estaba suscrita a los cambios de la anterior, con lo que no será capaz de darse cuenta de que ha habido cambios.

¿Qué puedo hacer que no sea extremadamente ineficiente? (Como vaciar la colección inicial e ir llenándola con los datos filtrados, que provocaría una notificación de cambio para el borrado y una nueva notificación por cada elemento nuevo que insertara)

CollectionViewSource al rescate

Pues existe un tipo que nos puede resultar de mucha ayuda en este aspecto: CollectionViewSource. Esta clase actúa como proxy que muestra vistas de una colección permitiendo Filtros, Ordenaciones y Agrupaciones.

Una forma de hacer uso de este control, muy cómoda todo sea dicho, pero no me termina de convencer desde el punto de vista de patrones de diseño (aunque todo es discutible).

Como habéis podido ver más arriba, mi ModelView expone una propiedad que representa la colección de todos “Mis Lugares”. Exponiendo tan solo esta colección, puedo montar todos los CollectionViewSource que quiera en la vista de manera declarativa. Veamos un ejemplo (he eliminado mucho código para centrarnos en lo importante)

 1: <phone:PhoneApplicationPage

 2:

 3:     xmlns:compMod="clr-namespace:System.ComponentModel;assembly=System.Windows"

 4:

 5:     <phone:PhoneApplicationPage.Resources>

 6:         <CollectionViewSource x:Key="filtroRecientes" Source="{Binding Path=Items}">

 7:             <CollectionViewSource.SortDescriptions>

 8:                 <compMod:SortDescription PropertyName="Modificado" Direction="Descending"></compMod:SortDescription>

 9:             </CollectionViewSource.SortDescriptions>

 10:         </CollectionViewSource>

 11:     </phone:PhoneApplicationPage.Resources>

 12:

 13:         <!--LayoutRoot is the root grid where all page content is placed-->

 14:      <Grid x:Name="LayoutRoot" Background="Transparent">

 15:         <!--Panorama control-->

 16:         <controls:Panorama Title="{Binding Source={StaticResource LocalizationHelper}, Path=LocalizedResources.TituloPanorama}" Foreground="{StaticResource PhoneTextBoxForegroundBrush}" >

 17:             <controls:Panorama.Background>

 18:                 <ImageBrush ImageSource="Resources/images/Panorama Green.png"/>

 19:             </controls:Panorama.Background>

 20:

 21:             <controls:PanoramaItem Header="{Binding Source={StaticResource LocalizationHelper}, Path=LocalizedResources.TituloTodos}" Orientation="Horizontal">

 22:                 <ListBox ItemsSource="{Binding Path=Items}" Foreground="{StaticResource PhoneTextBoxForegroundBrush}" />

 23:             </controls:PanoramaItem>

 24:

 25:             <controls:PanoramaItem Header="{Binding Source={StaticResource LocalizationHelper}, Path=LocalizedResources.TituloRecientes}" Orientation="Horizontal">

 26:                 <ListBox ItemsSource="{Binding Source={StaticResource filtroRecientes}}" Foreground="{StaticResource PhoneTextBoxForegroundBrush}" />

 27:             </controls:PanoramaItem>

 28:         </controls:Panorama>

 29:     </Grid>

 30: </phone:PhoneApplicationPage>

Lo primero de todo, he incluido una referencia al espacio de nombres System.ComponentModel. A continuación, he creado un nuevo recurso que será mi CollectionViewSource. Como podéis ver, dicha vista apunta a la propiedad Items en mi ModelView e incluye la descripción de una ordenación basada en una propiedad de mi Modelo, concretamente la propiedad Modificado que indica la fecha de la última modificación de los datos de un lugar.

 

image
Modelo MiLugar: Representa un Lugar de Interés

A continuación, el ListBox incluido dentro del PanoramaItem que muestra los lugares recientes. Simplemente tiene como fuente de datos el CollectionViewSource que acabamos de crear.

Sin embargo, no es ésta la forma en la que más me gusta hacer uso de este componente. Parece más adecuado exponer el CollectionViewSource como una propiedad más del ModelView.

 1: public class MainViewModel : IMainViewModel

 2: {

 3:     #region Miembros privados

 4:

 5:     private ObservableCollection<MiLugar> _items;

 6:     private CollectionViewSource _favoritos;

 7:     #endregion

 8:

 9:     #region Propiedades Públicas

 10:

 11:     /// <summary>

 12:     /// Una colección de lugares

 13:     /// </summary>

 14:     public ObservableCollection<MiLugar> Items

 15:     {

 16:         get { return _items; }

 17:         private set

 18:         {

 19:             if (_items != value)

 20:             {

 21:                 _items = value;

 22:                 NotifyPropertyChanged(PropNameItems);

 23:

 24:                 ActualizarVistas();

 25:             }

 26:         }

 27:     }

 28:

 29:

 30:     /// <summary>

 31:     /// Un subconjunto de lugares favoritos

 32:     /// </summary>

 33:     public CollectionViewSource Favoritos

 34:     {

 35:         get { return _favoritos; }

 36:     }

 37:     #endregion

 38:

 39:     /// <summary>

 40:     /// Actualiza las diferentes vistas expuestas sobre el conjunto de lugares favoritos.

 41:     /// </summary>

 42:     private void ActualizarVistas()

 43:     {

 44:         Favoritos.Source = Items;

 45:

 46:         Favoritos.View.Filter = new Predicate<object>(FiltroFavoritos);

 47:     }

 48:

 49:     /// <summary>

 50:     /// Establece como filtro para favoritos aquellos lugares marcados con el parámetro Favorito a true

 51:     /// </summary>

 52:     /// <param name="obj">Instancia sobre la que se establece el filtro</param>

 53:     /// <returns>true si el lugar es favorito, false en c.c.</returns>

 54:     private bool FiltroFavoritos(object obj)

 55:     {

 56:         var lugar = obj as MiLugar;

 57:         if (lugar == null)

 58:         {

 59:             return false;

 60:         }

 61:

 62:         return (lugar.Favorito == true);

 63:     }

 64: }

Ahora no hay más que hacer el DataBinding en la Vista XAML para que apunte a la nueva propiedad. ¿Correcto? Bueno, un matiz. la clase CollectionViewSource tiene una propiedad llamada CollectionViewSource.View que es la propiedad que realmente muestra la vista con el filtro.

 1: <controls:PanoramaItem Header="{Binding Source={StaticResource LocalizationHelper}, Path=LocalizedResources.TituloFavoritos}" Orientation="Horizontal" Foreground="{StaticResource PhoneBackgroundBrush}">

 2:     <ListBox ItemsSource="{Binding Path=Favoritos.View}" Foreground="{StaticResource PhoneTextBoxForegroundBrush}" />

 3: </controls:PanoramaItem>

El siguiente paso: añadir un formulario para el alta de lugares y tendremos nuestra primera versión operativa de la aplicación. Algo que poder enseñar y revisar. En el próximo post.

Empezando con Windows Phone 7.1 (y VI) – El Contexto de Datos

En esta primera aplicación, estoy creando un modelo de datos muy sencillo: esencialmente se trata de una colección de lugares de interés para el usuario. Poco más que añadir en esta primera iteración ya que, de momento, no daré soporte anidamientos y no habrá relaciones con otras entidades (al menos no las he identificado todavía). Queda para más adelante anidar lugares dentro de lugares y relacionar lugares con personas.

Para la creación del modelo de datos he comenzado con la entidad principal MiLugar: Se trata de una clase muy sencilla que proporciona dos propiedades. un identificador único y un nombre.

Entidad MiLugar en Iteración 1

Entidad MiLugar en Iteración 1

Caben destacar muy poquitas cosas de la entidad:

  1. Implementa la interfaz INotifyPropertyChanged que es de gran utilidad para comunicar cambios en propiedades de la entidad (una buena forma de que, por ejemplo, una vista sepa que la propiedad que tiene asociada a, digamos por ejemplo, un campo de texto ha cambiado y que, en consecuencia, ha de redibujar dicho campo de texto para mostrar el cambio). Veréis más adelante que esta interfaz está en el corazón de la forma en la que se asocian datos y vistas (llamadlas pantallas, si queréis).
  2. Que la entidad ha sido decorada con el atributo TableAttribute y que ambos campos han sido decorados, a su vez con el atributo ColumnAttribute. De esta manera estamos estableciendo las reglas con las que el contexto de datos podrá crear su implementación física a partir de la definición de mi entidad.
DetalleCodigoMiLugar_Iteracion1

Detalle de la entidad MiLugar en Iteración 1: Atributos Table y Column.

Como veis no tiene gran cosa (el código que veis aquí aun no ha sido probado, así que cruzo los dedos para que no me de ningún susto).

Actualización: Cabe decir que el código reflejado ahí arriba compila, pero no funciona en Windows Phone. ¿La razón? El tipo GUID no existe en SQL Server CE, poned en cambio uniqueidentifier y todo irá como la seda.

El siguiente paso es crear la clase encargada de representar el contexto de datos.

DataContext_Iteracion1

Diagrama de clases para el Contexto de Datos en Iteración 1

En este caso se trata de una clase que implementa el patrón Singleton y hereda de la clase DataContext. La única instancia del contexto de datos se puede obtener a través de la propiedad Instancia y contiene una colección de MiLugar que representamos como una propiedad de la clase Table (LugaresDeInteres).

En general, el contexto de datos tiene como propósito abstraernos de los detalles de acceso de la base de datos, limitándose a proporcionar colecciones (tablas) de entidades y mecanismos para buscar, insertar, editar y borrar elementos de dichas colecciones. En ningún momento nuestro código conocerá una sola línea orientada a abrir, crear, consultar o actualizar una base de datos.

Como podéis ver, el proceso de creación de la instancia de DataContext es bastante sencillo. Se pretende garantizar la concurrencia de hilos (cosa que, en esta aplicación será muy sencillo). A continuación, una implementación típica de Singleton y un pequeño detalle. Si la base de datos no ha sido creada todavía: la creamos mediante la línea _dataContext.CreateDatabase (); Fijaos que la cadena de conexión nos indica que la base de datos se referenciará en nuestro Almacén Aislado (IsolatedStorage)

DetalleInstanciaDataContext_Iteracion1

Detalle de la instanciación del DataContext en Iteración 1

A continuación, vamos a llevarnos el contexto de datos hacia arriba en nuestras capas de aplicación. La idea es utilizarlo en nuestras pantallas y, para eso, vamos a aprovechar la infraestructura que crea Visual Studio 2010 para implementar un patrón de capa de presentación llamado Modelo Vista – Vistamodelo (MVVM: Model view – ViewModel). Probablemente le dedique alguna entrada a este patrón, pero no ahora que todavía no lo tengo demasiado claro. Un par de trazos: los patrones de diseño para la capa de presentación llevan dando vueltas por ahí décadas. En un patrón típico Modelo Vista Presentación tienes lo siguiente:

  1. La Vista: Puedes asemejarla sin problemas a tu pantalla
  2. El Modelo: Los datos que se muestran en la vista.
  3. La Presentación: Lo que junta ambas cosas (definición donde las haya. ¿Eh?)

El MVVM es un patrón específico para Windows Presentation Foundation (WPF) que, os recuerdo, es un superconjunto de Silverlight que es con lo que montáis vuestras pantallas en Windows Phone.

Si no os parece mal, dejaremos la capa de presentación y su asociación con los datos para la siguiente entrada.