Empezando con Windows 7.1 (y XVII) – Comunicación entre diferentes VistaModelo

El problema

A medida de que voy avanzando en la aplicación, veo que poco a poco se van cubriendo escenarios comunes. En este post me voy a centrar en la comunicación entre diferentes VistaModelo.

En vez de dibujar aburridos diagramas de clases, vamos a tratar de ilustrarlo con un par de pantallas.

El programa

Disponemos de una primera pantalla de configuración. Esta pantalla permite activar notificaciones y, además, muestra información sobre la configuración del usuario en FourSquare (extraída de una entidad que consume información de la base de datos)

configuracion_1

Así mismo, el “tile” sobre los campos informativos “ID de Usuario” y “Nombre Completo” se puede pulsar para acceder a la página que nos permite registrar la información del usuario.

configuracion_2

Esta página (bueno, la página no, la vistamodelo que hay detrás), una vez el usuario autoriza el acceso de la aplicación, guarda la información en base de datos y vuelve atrás (a la página anterior).

configuracion_1

Que, no se ha enterado de que ha habido cambios en la entidad de configuración que consume para mostrar la información de FourSquare (lógico, al fin y al cabo, estos cambios han tenido lugar fuera de su ámbito).

La solución

La solución al problema es sencilla (aproximación prácticamente idéntica a las que hay en otro patrones de diseño como MVC o MVP).

En pocas palabras:

  1. La página de configuración se registra a posibles cambios de la entidad (o entidades) en que se encuentra interesada.
  2. La página de registro, que actúa sobre dicha entidad, lanza un mensaje “al mundo” cuando realiza algún cambio sobre la misma.
  3. La página de configuración se puede dar por enterada del cambio e inicia sus tareas de avisar, a su vez, a la página de que ha habido cambios y que tiene que refrescar la información que está mostrando en pantalla.

El ejemplo

MVVM Light nos proporciona un conjunto de clases que nos ayuda en estas tareas. Se encuentran bajo el espacio de nombres Messaging.

image

Aunque veremos algunos de los tipos definidos en este espacio de nombres en algún futuro post, me voy a centrar ahora en un par de clases.

La clase Messenger se encarga de gestionar el envío y recepción de mensajes. Los mensajes se envían “a quien quiera oírlos”. Los receptores de los mensajes tienen que haberse suscrito para poder recibir mensajes de un tipo.

Como explicaba antes, la VistaModelo de Registro se encarga de actualizar las preferencias de usuario tras el proceso de registro en FourSquare

   1: /// <summary>

   2: /// Registra las preferencias de usuario a partir de la información obtenida del mismo de FourSquare

   3: /// </summary>

   4: private void RegistrarPreferenciasDeUsuario()

   5: {

   6:     try

   7:     {

   8:         Entities.Preferencias pref = _dataService.ObtenerPreferenciasConfiguracion();

   9:         Entities.Preferencias prevPref = pref.Clone();

  10:  

  11:         pref.FourSquareFullName = String.Format("{0} {1}", _user.firstName, _user.lastName);

  12:         pref.FourSquareUserId = _user.id.ToString();

  13:         pref.FourSquareToken = _fsContexto.Token;

  14:  

  15:         _dataService.ActualizarPreferencias(pref);

  16:  

  17:         RaisePropertyChanged<Entities.Preferencias>("Preferencias", prevPref, pref, false);

  18:     }

  19:     catch (Exception ex)

  20:     {

  21:         // Hacer algo con la excepción

  22:         throw;

  23:     }

  24: }

Como podéis ver en el código, nada fuera del otro mundo. Se invoca el método RaisePropertyChanged. En este caso, no estamos interesados en ver qué propiedad de la entidad ha cambiado (ya que cambian varias y, a todos los efectos, no estoy interesado en obtener este grado de detalle) y he decidido pasar como primer parámetro el nombre de la clase. Se pasa también el valor original, el valor actualizado (para poder realizar esto, he tenido que montar un método para clonar la entidad) y se indica si el mensaje será lanzado “a quien quiera oírlo” (broadcast). Si este último parámetro no se pone a true, otra VistaModelo no recibirá esta mensaje.

En cuanto a la VistaModelo correspondiente a la configuración está interesada en recibir mensajes que le informen de cambios en las preferencias, para ello en el constructor realizaremos la operación de sucripción al tipo de mensaje.

   1: /// <summary>

   2: /// Este método subscribe a la VistaModelo a cualquier cambio realizado las propiedades de las <see cref="Preferencias"/>

   3: /// más concretamente sobre las preferencias que afectan a FourSquare

   4: /// </summary>

   5: private void SubscribirseCambiosEnPreferenciasConfiguracion()

   6: {

   7:     MessengerInstance.Register<PropertyChangedMessage<Preferencias>>(this, (p) =>

   8:     {

   9:         _preferencias = p.NewValue;

  10:  

  11:         DispatcherHelper.CheckBeginInvokeOnUI(() =>

  12:         {

  13:             RaisePropertyChanged(FourSquareUserIdNombreProp);

  14:             RaisePropertyChanged(FourSquareFullNameNombreProp);

  15:         });

  16:     });

  17: }

Este código tiene un poco más de enjundia. Vamos a analizarlo.

Primero, hacemos uso de la propiedad MessengerInstance que estamos heredando de la clase base (ViewModelBase) definida en el framework. A través de esta clase nos registramos a los mensajes del tipo PropertyChangedMessage<Preferencias> (que, si os fijáis, es el mismo tipo que estábamos lanzando en la VistaModelo anterior).

A continuación utilizamos un delegado anónimo que actualiza la referencia que teníamos con el nuevo valor y hace uso de una clase llamada DispatcherHelper (también definida en el framework MVVM Light) que nos permite acceder fácilmente al Dispatcher.

Mediante este Dispatcher, estamos pidiendo que se ejecute en el hilo de UI la actualización de las propiedades para que la página pueda refrescarlas.

¿Por qué esto es importante? En Silverlight (en realidad no exclusivamente en Silverlight) se define un hilo de UI donde se ejecutan todas las acciones que afectan a la interfaz de usuario (UI). Cualquier intento de actualizar algún elemento de la UI fuera de dicho hilo se va estrellar irremediablemente con la excepción: Invalid cross-thread access.

image

Por último, es necesario inicializar el DispatcherHelper antes de que seamos capaces de hacer funcionar todo esto. ¿Cómo lo hacemos? En la documentación encontrada por la red se insiste en inicializar dicha clase en App_Startup porque este método siempre se invoca en el contexto del hilo de UI.

Desgraciadamente, en WP7, tal método no existe. ¿Dónde inicializamos el dispatcher entonces? La respuesta es: en el constructor de App, que siempre se ejecuta en el hilo de UI.

image

A partir de aquí, todo funciona como debería.

configuracion_1

Ya que al actualizar Preferencias desde la vista de Registro

configuracion_2

El mensaje de cambio llega a la vista de configuración que se encarga de avisar a su vista de que ha de refrescarse.

configuracion_3

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.

Empezando con Windows Phone 7.1 (y XVI)–Manejando Eventos con Comandos

En otros posts he probado a manejar un evento de un control Silverlight y redirigirlo a una propiedad del tipo ICommand en mi VistaModelo.

Actualmente estoy trabajando en una página de configuración en la que, una de las acciones a llevar a cabo es registrar la aplicación en Foursquare. El mecanismo de autenticación utilizado es OAuth v2 (un estándar en la web, si echáis por ahí un vistazo). En el caso de Foursquare, el mecanismo recomendado para autenticación desde dispositivos móviles viene indicado aquí.

Esencialmente el proceso es muy simple y consiste en lo siguiente.

  1. Navegamos a la página de autenticación proporcionando el código de nuestra aplicación y una url de vuelta “callback”. Esta url no tiene por que ser una url real para una aplicación en desarrollo (algo del estilo http://localhost te va a funcionar perfectamente).
  2. Foursquare recibe la petición, comprueba el ID de la aplicación y la url de vuelta. Si el usuario está ya autenticado pasa al paso 4, si no.
  3. Si el usuario no está autenticado, pide sus credenciales de Foursquare.
  4. Una vez autenticado el usuario, Foursquare le pregunta si quiere registrar la aplicación.
  5. Si el usuario acepta, se redirige a la url de vuelta (callback) con un token específico para el usuario, que es el que utilizaremos para hacer llamadas al API de Foursquare desde la aplicación, en nombre del usuario.

En una primera aproximación me voy a limitar a crear un control de tipo WebBrowserControl con el que realizar este proceso.

image

Nada muy complicado.

   1: <!--ContentPanel - place additional content here-->

   2: <Grid x:Name="ContentPanel"

   3:       Grid.Row="1"

   4:       Margin="12,0,12,0">

   5:     <phone:WebBrowser 

   6:         HorizontalAlignment="Stretch" 

   7:         Name="webBrowser1" 

   8:         VerticalAlignment="Stretch" 

   9:         IsScriptEnabled="True" 

  10:         Source="{Binding Path=FourSquareAuthUrl}">

  11:         

  12:     </phone:WebBrowser>

  13:  

  14: </Grid>

Atención a la propiedad IsScriptEnabled, sin ella no he conseguido hacer funcionar el ejemplo ya que, por defecto, el valor está a falso y no permite ejecutar scripts.

Como con el resto de la aplicación, estoy tratando de mantener un patrón MVVM con lo que:

  • La url (propiedad Source) la he de obtener de la VistaModelo
  • Cuando navegemos a la URL de callback y deba de obtener el token, debo de hacerlo desde la VistaModelo también.

El primer punto no supone dificultad, el problema viene cuando queremos manejar los eventos de navegación. Veamos los intentos que he realizado.

Navegando con Code-behind

El primer intento evidente es crear un manejador en el “code behind” de la página.

   1: <phone:WebBrowser 

   2:     HorizontalAlignment="Stretch" 

   3:     Name="webBrowser1" 

   4:     VerticalAlignment="Stretch" 

   5:     IsScriptEnabled="True" 

   6:     Source="{Binding Path=FourSquareAuthUrl}" 

   7:     Navigating="webBrowser1_Navigating" />

Y el manejador…

   1: namespace Lcdad.SquareMatrix.ViewModel

   2: {

   3:     using GalaSoft.MvvmLight.Ioc;

   4:     using Microsoft.Phone.Controls;

   5:  

   6:     /// <summary>

   7:     /// Description for FourSquareLogin.

   8:     /// </summary>

   9:     public partial class FourSquareLogin : PhoneApplicationPage

  10:     {

  11:         /// <summary>

  12:         /// Initializes a new instance of the FourSquareLogin class.

  13:         /// </summary>

  14:         public FourSquareLogin()

  15:         {

  16:             InitializeComponent();

  17:         }

  18:  

  19:         private void webBrowser1_Navigating(object sender, NavigatingEventArgs e)

  20:         {

  21:             // Manejar aquí el evento de navegación para ver si estamos en la 

  22:             // uri de callback.

  23:         }

  24:     }

  25: }

Sin embargo habíamos quedado en que esta no es la forma adecuada.

Navegando con MVVM (no funciona como me gustaría)

El segundo intento me llevó un poco más de tiempo y lectura. El framework MVVM Light implementa un comportamiento (behavior) que puede utilizarse como mecanismo para asociar el evento con un comando en la VistaModelo. El EventToCommand Behavior

Primero de todo hemos de incluir la referencia correspondiente del espacio de nombres dentro de la página.

   1: xmlns:gse="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WP71"

A continuación declaramos unos cuantos disparadores para capturar los eventos de navegación.

   1: <phone:WebBrowser

   2:     HorizontalAlignment="Stretch"

   3:     Name="webBrowser1"

   4:     VerticalAlignment="Stretch"

   5:     IsScriptEnabled="True"

   6:     Source="{Binding Path=FourSquareAuthUrl}">

   7:     <i:Interaction.Triggers >

   8:         <i:EventTrigger EventName="Navigating" >

   9:             <gse:EventToCommand

  10:                 Command="{Binding Path=EstaNavegando, Mode=OneWay}"

  11:                 MustToggleIsEnabled="True" 

  12:                 CommandParameter="{Binding ElementName=webBrowser1, Path=Source.AbsoluteUri}" />

  13:         </i:EventTrigger>

  14:  

  15:         <i:EventTrigger EventName="Navigated" >

  16:             <gse:EventToCommand

  17:                 Command="{Binding Path=HaNavegado, Mode=OneWay}"

  18:                 MustToggleIsEnabled="True" 

  19:                 CommandParameter="{Binding ElementName=webBrowser1, Path=Source.AbsoluteUri}" />

  20:         </i:EventTrigger>

  21:  

  22:         <i:EventTrigger EventName="NavigationFailed" >

  23:             <gse:EventToCommand

  24:                 Command="{Binding Path=ErrorNavegando, Mode=OneWay}"

  25:                 MustToggleIsEnabled="True" 

  26:                 CommandParameter="{Binding ElementName=webBrowser1, Path=Source.AbsoluteUri}" />

  27:         </i:EventTrigger>

  28:     </i:Interaction.Triggers>

  29: </phone:WebBrowser>

Como puede verse, se declaran disparadores de eventos y se incluye el comportamiento EventToCommand para manejar dicho evento. En este segundo intento, pasé la Uri del control. Sin embargo, no conseguí el objetivo deseado.

Mi intención era, aprovechar la url durante las redirecciones, analizarla y cuando estuviéramos en la url de callback, extraer el token. Para mi mala suerte, pude comprobar que la url pasada como parámetro no cambiaba en absoluto durante las redirecciones (quizá tenga que analizar esto un poco más y sea una cuestión de configuración).

En resumen, lo que yo quería era disponer del estado de la navegación. Había podido observar que, en los manejadores de evento los argumentos NavigatingEventArgs, NavigationEventArgs y NavigationFailedEventArgs sí contenían la información que buscaba, así que. ¿Cómo pasar los argumentos del evento directamente a la VistaModelo?

Navegando con MVVM (ahora sí que sí)

Si le echas un vistazo a la documentación de la MSDN se indica claramente que el evento Navigating contiene información de navegación incluso durante redirecciones, así que poder pasar los parámetros de dicho evento a mi VistaModelo es fundamental para poder obtener mi token (al menos durante las pruebas).

La respuesta es muy sencilla, eliminar el primer intento de pasara la Url del control y, simplemente, poner a true el atributo PassEventArgsToCommand.

   1: <phone:WebBrowser

   2:     HorizontalAlignment="Stretch"

   3:     Name="webBrowser1"

   4:     VerticalAlignment="Stretch"

   5:     IsScriptEnabled="True"

   6:     Source="{Binding Path=FourSquareAuthUrl}" >

   7:         <i:Interaction.Triggers >

   8:             <i:EventTrigger EventName="Navigating" >

   9:                <gse:EventToCommand

  10:                    Command="{Binding Path=EstaNavegando, Mode=OneWay}"

  11:                    PassEventArgsToCommand="True"

  12:                    MustToggleIsEnabled="True" />

  13:             </i:EventTrigger>

  14:         </i:Interaction.Triggers>

  15: </phone:WebBrowser>

Y en la VistaModelo…

   1: namespace Lcdad.SquareMatrix.ViewModel

   2: {

   3:     public class FourSquareLoginViewModel : ViewModelBase

   4:     {

   5:         private IFourSquareHelper _fsHelper;

   6:         private ICommand _haNavegado;

   7:         private ICommand _estaNavegando;

   8:         private ICommand _errorNavegando;

   9:         private ICommand _paginaCargada;

  10:  

  11:         public FourSquareLoginViewModel()

  12:         {

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

  14:  

  15:             _fsHelper = SimpleIoc.Default.GetInstance<IFourSquareHelper>();

  16:  

  17:             _haNavegado = new RelayCommand<NavigationEventArgs>(AccionNavegado);

  18:             _estaNavegando = new RelayCommand<NavigatingEventArgs>(AccionNavegando);

  19:             _errorNavegando = new RelayCommand<NavigationFailedEventArgs>(AccionErrorNavegando);

  20:             _paginaCargada = new RelayCommand<NavigationEventArgs>(AccionPaginaCargada);

  21:         }

  22:  

  23:         private void AccionNavegado(NavigationEventArgs nea)

  24:         {

  25:         }

  26:  

  27:         private void AccionPaginaCargada(NavigationEventArgs nea)

  28:         {

  29:         }

  30:  

  31:         private void AccionNavegando(NavigatingEventArgs nea)

  32:         {

  33:             // Cuando llega la redirección del token, esta no se procesa enteramente, debemos de capturarla aquí mismo.

  34:             if (_fsHelper.EsUriCallback(nea.Uri))

  35:             {

  36:                 var token = _fsHelper.ObtenerTokenDesdeUri(nea.Uri);

  37:  

  38:                 if (!String.IsNullOrEmpty(token))

  39:                 {

  40:                     // TODO: Almacenar el token en BBDD

  41:                 }

  42:             }

  43:         }

  44:  

  45:         private void AccionErrorNavegando(NavigationFailedEventArgs nfea)

  46:         {

  47:         }

  48:     }

  49: }

Con esto ya lo hemos conseguido. ¡Nos registramos en Foursquare!

Lo siguiente: consumir los servicios expuestos.

Una historia “real”

Una mañana entró en el concesionario de coches una persona importante. Muy importante.

No hacía mucho tiempo habría sufrido una experiencia traumática en un avión, con lo que había decidido comprar un coche de ciertas características para poder realizar en él la mayor parte de sus viajes.

Por un lado debía de ofrecer lo último en seguridad, pero por otro lado, sabedor de que algunas cosas no pueden automatizarse (por mucho que uno quiera) cada uno de los elementos de seguridad debían de poder ser monitorizados y mostrar claros indicadores de cómo iba funcionamiento y conducción.

Tras comprobar dos o tres modelos diferentes se decantó finalmente por uno que se ajustaba perfectamente a sus necesidades. Inmediatamente, llamó a su chófer y le indicó que se acercara por el concesionario, donde el vendedor (ahora mucho más feliz que hacía unas horas) le mostraría el funcionamiento y características de su nuevo vehículo. A continuación, con un firme apretón de manos se fue del concesionario.

Poco después llegó el chófer, francamente interesado por la nueva adquisición. Conocedor de las necesidades de su patrón y dispuesto a aprenderse al dedillo el funcionamiento del coche desde el primer momento, prestó gran atención a las indicaciones del vendedor:

El indicador de velocidad, en tres colores que mostraban la variación de la velocidad con respecto a la programada.

El indicador de consumo de combustible, en tres colores y con una bonita gráfica de progresión. Mostrando en varias fases el porcentaje de combustible consumido y pendiente de consumir,así como una preciosa gráfica que le permitía revisar cuál era el punto óptimo de equilibrio entre velocidad y consumo.

El indicador de asistencia en el agarre. Ya que no toda la conducción está condicionada por factores del conductor y del vehículo y es necesario estar muy atento a factores externos, como todo el mundo sabe.

Incluso dos pequeños sensores de posición, radar anti-colisión y un asistente de mantenimiento de la dirección que avisaba de posibles desviaciones respecto a la línea continua que marcaba la frontera entre carril y arcén.

Terminada la explicación, el vendedor descansó un segundo, tomó aire y le invitó al chófer a realizar tantas preguntas como considerara necesarias para aclarar la operativa y el uso de todas las herramientas de seguridad.

– Estoy maravillado. – Dijo el chófer con un brillo especial en los ojos. – Sólo una pregunta. ¿Cómo desactivo todo esto?

El vendedor no atinaba a encajar la mandíbula en su sitio.

– Verá – dijo el chófer, plenamente consciente de la cara de su interlocutor. – Mi jefe es un hombre muy preocupado por la seguridad, así que, mientras conduzco estará mirando todos y cada uno de los indicadores. En un viaje cualquiera, es imposible que alguno de los indicadores no se ponga en amarillo o incluso en rojo en algún momento. Cuando eso ocurra, mi trabajo se va a convertir en un infierno teniéndole que explicar a mi jefe el por qué de cada cambio en los indicadores. No le digo ya, si se pone a ver las gráficas de consumo en relación a la velocidad. Así que, si no le importa, preferiría disponer de un botoncito que deje los indicadores en niveles óptimos. Le aseguro que mi conducción será mucho más llevadera y el viaje de mi jefe, más confortable.

– ¿Y si sufre algún percance en el camino? ¿Y si se termina el combustible, o la velocidad media no se ajusta a la de la carretera? – Preguntó el vendedor con lágrimas en los ojos.

– Bueno, en ese caso, ya veremos. ¿No?

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

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

Creando una cuenta en Windows Azure

Estoy trabajando en un nuevo proyecto de aprendizaje con Windows Phone 7.5 y veo que, más pronto que tarde, habrá que proporcionar a los usuarios la posibilidad de compartir información. Aprovechando la cuenta de MSDN que me brinda la empresa donde trabajo (Avanade) he decidido crear una cuenta en Azure.

Nada más sencillo, dispongo de una oferta de prueba de 90 días y la posibilidad de limitar el gasto asociado a la cuenta (importante para no llevarte ninguna sorpresa desagradable a fin de mes). Por defecto he establecido 0€ de gasto que es más que razonable en este momento Sonrisa

El siguiente paso ha consistido en instalar las herramientas de desarrollo. Azure dispone de una gran cantidad de posibilidades independientemente del lenguaje de programación que escojas.

image

Como os imaginaréis, he optado por .NET que, al fin y al cabo, es lo mío.

image

Y ahí va… Instalándose poco a poco.

Un vistazo al Portal de Administración recién estrenadito.

image

Próximamente crearé un par de servicios web sencillos que pueda consumir desde mi Windows Phone para disponer de una alternativa de almacenamiento y servicios en la Nube.

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