[Universal App] Roaming Settings, compartiendo datos entre plataformas

SynchronizeIntroducción

Algo muy habitual en cualquier tipo de aplicación móvil es trabajar con distntos tipos de datos, datos locales de la aplicación, archivos de configuración, etc. Segun el tipo de dato debemos proporcionar un tratamiento diferente pero siempre buscando facilitar el uso de la aplicación por parte del usuario.

Ante un mundo lleno de múltiples dispositivos para distintas situaciones (PCs, Tablets, móviles, Bands, etc.) es cada vez más habitual que un mismo usuario se instale la misma aplicación en diferentes dispositivos. Ante este tipo de situación el usuario espera que los datos básicos y de configuración se mantengan sincronizados y no tener que establecer su configuración básica una y otra vez.

¿Cómo lo conseguimos?

Almacenamiento de datos de aplicación

Tanto en aplicaciones Silverlight 8.1 como en aplicaciones Windows XAML tenemos disponible las siguientes carpetas:

  • LocalFolder: Un viejo conocido. Ya lo teníamos disponible en Windows Phone 8 e incluso era la carpeta usada como Isolated Storage desde Windows Phone 7. Guardaremos información que persiste entre actualizaciones de la aplicación y entra dentro de los datos guardados al realizar un backup del sistema.
  • RoamingFolder: Almacenamiento muy útil. Al guardar datos en RoamingData, la información estará disponible en todos los dispositivos donde la aplicación este instalada (con el mismo id). Ideal para guardar la configuración de la aplicación y mantenerla sincronizada entre la aplicación Windows Phone y la Windows Store por ejemplo.
  • TemporaryFolder: Aqui guardaremos información sin tener la necesidad de borrarla más tarde. La información se guardará entre las distintas sesiones pero cuando el sistema requiera espacio (Ejemplo: poca memoria disponible), eliminará la información.  Es un lugar idóneo donde guardar datos obtenidos de peticiones web, servicios o imágenes por ejemplo.

Roaming Data

Si un usuario obtiene la aplicación en múltiples dispositivos, Windows y Windows Phone, es fantástico para el que la configuración de la misma se comparta. De modo que, cualquier cambio realizado en la configuración de la Aplicación uno de los dispositivos se vea reflejado en el otro. Esto lo podemos conseguir utilizando Roaming Data entre aplicaciones Windows Phone 8.1 y Windows 8.1 que compartan el mismo PFN (Package Family Name).

El Roaming de datos nos proporciona una forma de compartir y sincronizar datos entre distintos dispositivos físicos de manera sencilla.

NOTA: Si publicamos dos versiones de la misma aplicación (una para la Windows Store y otra para la Windows Phone Store) podemos compartir configuración e información utilizando el mismo PFN en cada aplicación.

Roaming Data

Roaming Data

Utilizaremos diccionarios (clave / valor) que automáticamente se almacenarán en el OneDrive del usuario. El tamaño máximo de los datos Roaming viene establecido en la propiedad ApplicationData.RoamingStorageQuota (normalmente 100KB).

NOTA: Los datos de Roaming no afecta al tamaño de almacenamiento del usuario.

Manos a la obra

Comenzamos creando un nuevo proyecto:

Nueva Aplicación Universal

Nueva Aplicación Universal

Añadimos las carpetas Views, ViewModels y Services además de las clases base necesarias para implementar el patrón MVVM de la misma forma que vimos en este artículo.

Nuestro objetivo en este ejemplo sera simular una opción de configuración sencilla, como por ejemplo, el color básico utilizado en la aplicación y utilizar datos Roaming para mantener el color seleccionado por el usuario sincronizado entre las aplicaciones Windows y Windows Phone.

Vamos a tener una configuración de colores por lo tanto empezaremos definiendo… el color. En nuestra carpeta Models crearemos la siguiente clase:

public class Accent
{
     public string Name { get; set; }

     public string Hex { get; set; }
}

Nos permite definir el nombre y valor hexadecimal de cada color. A continuación, en nuestra viewmodel tendremos una colección de colores:

private Accent _current;
private ObservableCollection<Accent> _accents;

public ObservableCollection<Accent> Accents
{
     get { return _accents; }
     set { _accents = value; }
}

public Accent Current
{
     get { return _current; }
     set
     {
          _current = value;
          RaisePropertyChanged("Current");
     }
}

Al entrar en la vista, utilizaremos un método para obtener el listado de colores disponibles:

_accents = new ObservableCollection<Accent>
{
     new Accent { Name = "Blue", Hex= "#1BA1E2" },
     new Accent { Name = "Brown", Hex= "#A05000" },
     new Accent { Name = "Green", Hex= "#339933" },
     new Accent { Name = "Pink", Hex= "#E671B8" },
     new Accent { Name = "Purple", Hex= "#A200FF" },
     new Accent { Name = "Red", Hex= "#E51400" },
     new Accent { Name = "Teal", Hex= "#00ABA9" },
     new Accent { Name = "Lime", Hex= "#A2C139 " },
     new Accent { Name = "Magenta", Hex= "#D80073 " }
};

Todo preparado para poder definir la interfaz:

<ListView
     Grid.Row="1"
     ItemsSource="{Binding Accents}"
     SelectedItem="{Binding Current, Mode=TwoWay}">
     <ListView.ItemsPanel>
         <ItemsPanelTemplate>
             <WrapGrid MaximumRowsOrColumns="3" />
         </ItemsPanelTemplate>
     </ListView.ItemsPanel>
     <ListView.ItemTemplate>
         <DataTemplate>
             <Grid Background="{Binding Hex}"
                   Height="100"
                   Width="100"
                   Margin="5">
                 <TextBlock Text="{Binding Name}"
                            VerticalAlignment="Bottom"/>
             </Grid>
         </DataTemplate>
     </ListView.ItemTemplate>
</ListView>

Hasta aqui nuestro ejemplo básico. Un listado de colores que el usuario seleccionará àra utilizar como color principal en la aplicación. Utilizamos datos Roaming para guardar el color seleccionado. Para ello, comenzamos…

Asociando la aplicación con la Store

Para que la infraestructura de datos Roaming funcione (datos en OneDrive), debemos asociar la aplicación con la tienda.

NOTA: No es necesario publicar la aplicación para realizar pruebas.

Para asociar la aplicación con la tienda haremos clic derecho sobre la misma y elegiremos la opción “Associate App with the Store…”:

Reservar un nombre de App

Reservar un nombre de App

Nos aparecerá una ventana modal como la siguiente:

Asociar la App con la Store

Asociar la App con la Store

Registraremos un nombre de Aplicación:

Nombre de la App

Nombre de la App

Al finalizar el proceso, nuestra aplicación quedará asociada a la tienda y tendremos un resumen de todos los valores correctos a utilizar en el archivo de manifiesto:

Resumen

Resumen

Una vez realizado el proceso en la aplicación Windows Store o Windows Phone, debemos repetir lo mismo en la otra seleccionando el nombre reservado previamente. Antes de continuar sería oportuno revisar el archivo de manifiesto de cada aplicación para asegurar que el PFN (Package Family Name) es el mismo.

Misma cuenta Microsoft en Windows/Phone

Para que la sincronización de datos funcione correctamente, ambos dispositivos deben utilizar la misma cuenta Microsoft. Esto que es sencillo, en el punto final es fácil de cumplir pero quizas no tanto en el desarrollo. En la máquina de desarrollo tendremos una cuenta Microsoft establecida pero probablemente no en el emulador de Windows Phone. Recuerda utilizar la misma cuenta en el emulador para garantizar que todo funcione o utiliza un dispositivo físico.

Usando Roaming Settings

Llegamos a la parte central del artículo, el uso de Roaming. Para utilizar Roaming en nuestro ejemplo vamos a crear un servicio llamado RoamingSettingsService con la siguiente definición:

/// <summary>
/// Almacenar y recuperar configuraciones y archivos desde el almacén de datos móviles de aplicaciones.
/// </summary>
public interface IRoamingSettingsService
{
     void SaveData(string key, string value);

     void SaveData(string container, string key, string value);

     void SaveData(string key, ApplicationDataCompositeValue value);

     bool ContainsKey(string key);

     object GetData(string key);

     object GetData(string container, string key);

     ApplicationDataCompositeValue GetDataComposite(string key);

     void RemoveData(string key);

     ApplicationDataContainer CreateContainer(string container);

     void RemoveContainer(string container);
}

Ahora nos centramos en la definición del servicio. Comenzamos creando una propiedad RoamingSettings que accederá a la propiedad ApplicationData.RoamingSettings para obtener la configuración.

/// <summary>
///  Almacenar y recuperar configuraciones y archivos desde el almacén de datos móviles de aplicaciones.
///
/// Más info: http://msdn.microsoft.com/es-es/library/windows/apps/xaml/hh700362.aspx
/// </summary>
public class RoamingSettingsService : IRoamingSettingsService
{
     internal ApplicationDataContainer RoamingSettings
     {
         get { return ApplicationData.Current.RoamingSettings; }
     }
}

Definimos el método que nos permitirá guardar datos Roaming. Ya comentamos previamente que trabajaremos con diccionarios con clave y valor. El método para guardar datos recibirá la clave y el valor a almacenar:

/// <summary>
/// Escribir datos en una configuración.
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void SaveData(string key, string value)
{
     RoamingSettings.Values[key] = value;
}

NOTA: Podríamos pasar un tercer parámetro para indicar la prioridad utilizando la propiedad HighPriority pero solo tendría efecto en la aplicación Windows Store.

Antes de almacenar un valor nos puede interesar verficar si ya existe un valor almacenado con la clave otorgada. Definiremos un sencillo método que nos haga esta verificación:

/// <summary>
/// Verifica si existe algun dato relacionado con la clave facilitada.
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public bool ContainsKey(string key)
{
     return RoamingSettings.Values.ContainsKey(key);
}

Una vez guardado datos desearemos recuperarlos posteriormente… creamos el método que nos permite recuperar el valor de una clave otorgada:

/// <summary>
/// Leer datos desde una configuración.
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public object GetData(string key)
{
     return RoamingSettings.Values[key];
}

Utilizamos la propiedad ApplicationDataContainer.Values para acceder al valor almacenado con la clave dada.

Para eliminar una configuración, utilizaremos el método ApplicationDataContainerSettings.Remove:

/// <summary>
/// Eliminar configuraciones.
/// </summary>
/// <param name="key"></param>
public void RemoveData(string key)
{
     RoamingSettings.Values.Remove(key);
}

Utilizando el servicio en nuestra viewmodel, al cargar el color seleccionado verificaremos si esta ya guardado como una configuración almacenada en Roaming y en caso afirmativo, recuperaremos el valor. En caso de no existir, seleccionamos el primer color del listado:

if (_roamingSettingsService.ContainsKey(Key))
{
     var hex = _roamingSettingsService.GetData(Key).ToString();
     Current = Accents.First(c => c.Hex.Equals(hex));
}
else
     Current = Accents.First();

Cada vez que el usuario cambie de color, guardaremos el mismo:

public Accent Current
{
     get { return _current; }
     set
     {
          _current = value;
          _roamingSettingsService.SaveData(Key, _current.Hex);

          RaisePropertyChanged("Current");
     }
}
Configuración de color de la App

Configuración de color de la App

Recibiendo la notificación de cambio en los datos

El evento DataChanged se lanzará cada vez que los datos Roaming cambien siempre y cuando la aplicación este activa en el momento del cambio.

Debemos suscribirnos al evento:

ApplicationData.Current.DataChanged -=
                new TypedEventHandler<ApplicationData, object>(DataChangeHandler);

Hemos establecido DataChangeHandler como el controlador para cambios de datos móviles:

void DataChangeHandler(ApplicationData appData, object args)
{

}

Podéis descargar el ejemplo realizado a continuación:

Más opciones utilizando Roaming Data

Hasta este punto hemos visto todo lo necesario para utilizar datos en Roaming de manera simple, tal y como hemos usado en nuestro ejemplo. Sin embargo, tenemos muchas otras opciones disponibles. Si recordáis la definición de nuestro servicio veréis muchos otros métodos que no hemos usado hasta ahora. Vamos a revisarlos uno a uno.

Utilizamos el método ApplicationDataContainer.CreateContainer para crear un contenedor de configuraciones:

/// <summary>
/// Crea o abre el contenedor de configuración especificado.
/// </summary>
/// <param name="container"></param>
/// <returns></returns>
public ApplicationDataContainer CreateContainer(string container)
{
     return RoamingSettings.CreateContainer(container, ApplicationDataCreateDisposition.Always);
}

Utilizamos el método ApplicationDataContainer.DeleteContainer para eliminar el contenedor de configuraciones:

/// <summary>
/// Elimina el contenedor de configuración especificado.
/// </summary>
/// <param name="container"></param>
public void RemoveContainer(string container)
{
     RoamingSettings.DeleteContainer(container);
} 

Guardamos el valor value para acceder a la configuración dada por el parámetro key del contenedor container:

/// <summary>
/// Escribir datos en una configuración.
/// </summary>
/// <param name="container"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public void SaveData(string container, string key, string value)
{
     RoamingSettings.Containers[container].Values[key] = value;
}

Usamos la propiedad ApplicationDataContainer.Values para acceder a la configuración dada por el parámetro key del contenedor container:

/// <summary>
/// Leer datos desde una configuración de un contenedor de configuración especificado.
/// </summary>
/// <param name="container"></param>
/// <param name="key"></param>
/// <returns></returns>
public object GetData(string container, string key)
{
     return RoamingSettings.Containers[container].Values[key];
}

Podemos guardar settings utilizando un objeto ApplicationDataCompositeValue que contiene un conjunto de  configuraciones:

/// <summary>
/// Escribir datos en una configuración.
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void SaveData(string key, ApplicationDataCompositeValue value)
{
     RoamingSettings.Values[key] = value;
}

Consejos al usar Roaming Data

A la hora de utilizar Roaming Data es aconsejable que tengas en cuenta:

  • La sincronización de los datos se realiza en background.
  • Utilizar datos en Roaming es ideal para poca cantidad de información como por ejemplo la configuración de la aplicación, o pequeños volúmenes de datos relacionados con actividad reciente. Sin embargo, NO se debe utilizar Roaming para grandes cantidades de información.
  • La propiedad HighPriority para forzar una sincronización rápida solo esta disponible en Windows, no tiene efecto en Windows Phone.
  • A la hora de depurar aplicaciones que hagan uso de Roaming, si hay problemas en la sincronización, asegúrate que ambos dispositivos usan la misma cuenta Microsoft, que en el archivo de manifiesto tenemos el mismo PFN y la misma versión.

Más información

Anuncios

[Universal App] Usando las APIs Credential Locker

Login-01Introducción

En determinadas ocasiones, nuestras aplicaciones requieren seguridad o personalización por usuario. En estos casos son típicos problemas como, como almacenar usuario y contraseña, encriptar la información, gestionar múltiples dispositivos, etc.

Era un proceso “habitual” pero que requiere tener en cuentas bastantes aspectos. Ahora con la llegada de las APIs Credential Locker disponibles en Windows.Security.Credentials, todo el proceso es mucho más simple.

En este artículo vamos a conocer las APIs de Credential Locker utilizñandolas en un ejemplo real donde veremos como almacenar, recuperar y eliminar claves.

¿Te apuntas?

¿Credential Locker?

El almacén de credenciales nos permite almacenar y administrar de forma segura contraseñas de usuarios de una aplicación o servicio específico de modo que por un lado, los datos almacenados de una aplicación se transfieren automáticamente a los otros dispositivos de confianza del usuario, simplificando el proceso de autenticación tanto a los usuarios como a nosotros, y por otro lado, no es posible desde una aplicación o servicio acceder a los credenciales asociados con otra aplicación o servicio.

Primeros pasos

Comenzamos creando un nuevo proyecto:

Nueva Aplicación Universal

Nueva Aplicación Universal

Añadimos las carpetas Views, ViewModels y Services además de las clases base necesarias para implementar el patrón MVVM de la misma forma que vimos en este artículo.

Nuestro objetivo sera muy sencillo. Nuestra aplicación de ejemplo pedirá fuente, usuario y contraseña permitiéndo con tres sencillos botones, añadir, recuperar y eliminar credenciales utilizando la API Credential Locker.

Comenzamos definiendo la interfaz de usuario:

<StackPanel Margin="12">
     <StackPanel Orientation="Vertical">
          <TextBox Header="Source" PlaceholderText="https://facebook.com" />
          <TextBox Header="User" PlaceholderText="user@mail.com" />
          <PasswordBox Header="Password" PlaceholderText="1234abcd" />
     </StackPanel>
     <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
          <Button Content="Guardar" />
          <Button Content="Recuperar" Margin="12, 0" />
          <Button Content="Borrar" />
     </StackPanel>
</StackPanel>

Muy simple. Permitimos obtener la información de seguridad requerida y contamos con tres botones de acción. En la viewmodel correspondiente contaremos con propiedades para obtener la información escrita en cada una de las cajas de texto:

// Variables
private string _source;
private string _user;
private string _password;
private string _info;

public string Source
{
     get { return _source; }
     set { _source = value; }
}

public string User
{
     get { return _user; }
     set { _user = value; }
}

public string Password
{
     get { return _password; }
     set { _password = value; }
}

public string Info
{
     get { return _info; }
     set
     {
          _info = value;
          RaisePropertyChanged("Info");
     }
}

Y cada botón, ejecutará un comando en la viewmodel:

// Commands
private ICommand _saveCommand;
private ICommand _readCommand;
private ICommand _deleteCommand;

public ICommand SaveCommand
{
     get { return _saveCommand = _saveCommand ?? new DelegateCommand(SaveCommandDelegate); }
}

public ICommand ReadCommand
{
     get { return _readCommand = _readCommand ?? new DelegateCommand(ReadCommandDelegate); }
}

public ICommand DeleteCommand
{
     get { return _deleteCommand = _deleteCommand ?? new DelegateCommand(DeleteCommandDelegate); }
}

public void SaveCommandDelegate()
{

}

public void ReadCommandDelegate()
{

}

public void DeleteCommandDelegate()
{

}

De modo que nuestra interfaz bindeada a las propiedades y comandos correspondientes quedara como podemos ver a continuación:

<StackPanel Margin="12">
     <StackPanel Orientation="Vertical">
          <TextBox Text="{Binding Source, Mode=TwoWay}" Header="Source" PlaceholderText="https://facebook.com" />
          <TextBox Text="{Binding User, Mode=TwoWay}" Header="User" PlaceholderText="user@mail.com" />
          <PasswordBox Password="{Binding Password, Mode=TwoWay}" Header="Password" PlaceholderText="1234abcd" />
     </StackPanel>
     <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
          <Button Content="Guardar" Command="{Binding SaveCommand}" />
          <Button Content="Recuperar" Margin="12, 0" Command="{Binding ReadCommand}" />
          <Button Content="Borrar" Command="{Binding DeleteCommand}" />
     </StackPanel>
     <ScrollViewer>
          <TextBlock Text="{Binding Info}"/>
     </ScrollViewer>
</StackPanel>

Hasta aqui, la interfaz y estructura básica de nuestro ejemplo.

Interfaz del ejemplo

Interfaz del ejemplo

Continuamos centrándonos en la parte importante, la gestión de credenciales.

Gestión de credenciales

Para realizar la gestión de credenciales utilizaremos las APIs Credential Locker disponibles en Windows.Security.Credentials. Vamos a crear un servicio con la siguiente definición:

public interface IPasswordVaultService
{
     void Save(string resource, string userName, string password);

     PasswordCredential Read(string resource, string userName);

     IReadOnlyList<PasswordCredential> GetAll();

     void Delete(string resource, string userName);
}

NOTA: El servicio será inyectado por Ioc en nuestra viewmodel.

El servicio contará con cuatro métodos:

  • Save: Nos permitirá guardar credenciales.
  • Read: Nos permitirá recuperar un credencial concreto.
  • GetAll: Recupera todos los credenciales que tengamos almacenados en el Credential Locker.
  • Delete: Eliminará un credencial concreto almacenado previamente.

Nos centramos a continuación en la implementación de cada método. Comenzamos por el método Save:

public void Save(string resource, string userName, string password)
{
     PasswordVault vault = new PasswordVault();
     PasswordCredential cred = new PasswordCredential(resource, userName, password);
     vault.Add(cred);
}

Analicemos el código superior. Primero, obtenemos una referencia al Credential Locker usando on objeto de tipo PasswordVault definido en el namespace Windows.Security.Credentials. Continuamos creando un objeto de tipo PasswordCredential que representará el credencial a almacenar con la referencia a nuestra Aplicación o el tipo de Login utilizado además de los credenciales en si, usuario y contraseña. Añadiremos el credencial creado al almacén de credenciales utilizando el método PasswordVault.Add.

Continuamos con el método Read:

public PasswordCredential Read(string resource, string userName)
{
     PasswordVault vault = new PasswordVault();

     return vault.Retrieve(resource, userName);
}

Contamos con una gran variedad de opciones para recuperar credenciales del almacén. En el código de la parte superior utilizamos la forma más simple posible. Contando con el nombre de la App o tipo de Login además del nombre de usuario, podemos recuperar la información utilizando el método PasswordVault.Retrieve.

En el método GetAll utilizamos el método PasswordVault.RetrieveAll  para recuperar todos los credenciales almacenados en la Aplicación:

public IReadOnlyList<PasswordCredential> GetAll()
{
     PasswordVault vault = new PasswordVault();

     return vault.RetrieveAll();
}

Además de las dos formas utilizadas contamos con otras opciones para recuperar credenciales:

Por último, nos centramos en el método Delete, que como podemos imaginar se encargará de eliminar un credencial en concreto:

public void Delete(string resource, string userName)
{
     PasswordVault vault = new PasswordVault();
     PasswordCredential cred = vault.Retrieve(resource, userName);
     vault.Remove(cred);
}

De nuevo, es un proceso muy sencillo que podemos hacer con pocas líneas. Accedemos de nuevo al almacén de credenciales mediante un objeto de tipo PasswordVault y utilizamos el método PasswordVault.Remove  para eliminar el credencial almacenado pasado como parámetro.

Con nuestro servicio para gestionar el almacén de credenciales preparado, solo nos falta definir la acción de cada comando. Al guardar el credencial:

public void SaveCommandDelegate()
{
     if (string.IsNullOrEmpty(Source) || string.IsNullOrEmpty(User) || string.IsNullOrEmpty(Password))
     {
          return;
     }

     try
     {
          _passwordVaultService.Save(Source, User, Password);
          Info += string.Format("Datos guardatos. Resource: {0}, User: {1}, Password: {2}",
          Source, User, Password)
     }
     catch(Exception ex)
     {
          Info += ex.Message;
     }
}
Guardando credenciales

Guardando credenciales

Sencillo, verificamos que los datos que definen el credencial son válidos y en caso afirmativo utilizamos el método Save de nuestro servicio PasswordVaultService. Para recuperar un credencial:

public void ReadCommandDelegate()
{
     if (string.IsNullOrEmpty(Source) || string.IsNullOrEmpty(User))
     {
          Info += "La fuente y el usuario son obligatorios.";
          return;
     }

     try
     {
          var cred = _passwordVaultService.Read(Source, User);
          Info += string.Format("Datos obtenidos con éxito. Resource: {0}, User: {1}, Password: {2}",
          cred.Resource, cred.UserName, cred.Password);
     }
     catch (Exception ex)
     {
          Info += ex.Message;
     }
}
Recuperar un credencial

Recuperar un credencial

Utilizaremos tras validar la información, el método Read de nuestro servicio. Y por último, en el comando para eliminar credenciales:

public void DeleteCommandDelegate()
{
     if (string.IsNullOrEmpty(Source) || string.IsNullOrEmpty(User))
     {
          Info += "La fuente y el usuario son obligatorios.";
           return;
     }

     try
     {
          _passwordVaultService.Delete(Source, User);
          Info += string.Format("Datos eliminados con éxito. Resource: {0}, User: {1}, Password: {2}",
          Source, User, Password);
     }
     catch (Exception ex)
     {
          Info += ex.Message;
     }
}

Utilizamos el método Delete del servicio.

Eliminar un credencial

Eliminar un credencial

Podéis descargar el ejemplo realizado a continuación:

Buenas prácticas

  • El almacén de credenciales esta pensado para facilitar la tarea de la gestión de la seguridad en nuestras aplicaciones. No se recomienda su uso para almacenar grandes cantidades de información. Contamos con otras APIs válidas y más adecuadas para esta necesidad.
  • Debemos controlar el ciclo de almacenamiento de credenciales en el Password Vault correctamente. No almacenar información hasta haber sido autenticado correctamente y haber marcado el usuario que desea recordar la información.

Conclusiones

En aplicaciones universales, es decir, tanto en aplicaciones Windows Phone como en aplicaciones Windows Store, contamos con una API, disponible bajo el namespace Windows.Security.Credentials llamada Credential Locker que nos permite gestionar credenciales de usuario con suma facilidad. La gran ventaja de utilizar la API es que nos almacena la información en un almacén seguro, la información es encriptada al ser almacenada. Además, otra de las grandes ventajas de utilizar la API es el roaming de los credenciales entre dispositivos de confianza bajo la misma cuenta Microsoft.

Más información

[Universal App] Uso del Zoom Semantico, creando Jumplists

JumpListIntroducción

Estas envuelto en el desarrollo de una aplicación universal y quieres utilizar un control jumplist, un listado con organizado en grupos que además nos permiten acceder rapidamente a elementos de otros grupos. Si ya has desarrollado previamente para Windows Phone habrás intentando utilizar un contorl LongListSelector pero… ¿que ocurre?, ¿dónde se encuentra?. Las aplicaciones universales tanto Windows como Windows Phone funcionan bajo WinRT donde no tenemos disponible el control. Pero esperad, tranquilos, con la unificación de las plataformas el control LongListSelector de Windows Phone 8 pasa a ser reemplazado por el zoom semántico que ya teníamos en las aplicaciones Windows Store. En este artículo vamos a crear un proyecto universal desde cero y a utilizar en una aplicación el control SemanticZoom para ver como reemplaza al LongListSelecto ren Windows Phone y su uso en Windows Store.

¿Te apuntas?

¿Qué es el Zoom Semántico?

En zoom semántico es la forma que podemos utilizar en nuestras aplicaciones universales a la hora de presentar conjuntos grandes de datos o datos relacionados entre si en una única vista. El control SemanticZoom nos permite mostrar la información al usuario en dos vistas distintas del mismo contenido.

Para hacer zoom semántico, podemos hacerlo mediante un simple gesto (acercando ambos dedos alejamos el zoom, alejando los dedos lo acercamos). Además en Windows podemos pulsar la tecla CTRL del teclado mientras hacemos scroll con el ratón (o pulsamos las teclas + o -).

Veamos un simple ejemplo para dejarlo todo más claro. Imaginaos que estamos desarrollando una aplicación de mensajería. Estamos viendo el historial de mensajes que se ha mantenido con un usuario en concreto. Veremos la vista reducida o alejada:

Vista reducida

Vista reducida

Si queremos tener de un simple vistazo todos los periodos de fechas en los que hemos mantenido mensajes con el usuario bastará con hacer hacer el gesto de acercar los dedos para mostrar la vista ampliada (tras una pequeña animación):

Vista ampliada

Vista ampliada

Como podéis ver en el ejemplo, sin necesidad de hacer scroll podemos mostrar de una simple vistazo la información deseada. Es más, tenemos la posibilidad de añadir información extra. En nuestro ejemplo además de mostrar los distintos periodos en los que se han mantenido conversaciones, se muestra el número de mensajes que se han realizados en cada uno de ellos.

Manos a la obra!

Vamos a crear una aplicación simple que muestre una colección de películas agrupadas por año. El objetivo final será añadir el uso del SemanticZoom para mostrar por defecto la colección de películas y al hacer el zoom out mostrar los distintos años en los que están agrupadas las películas.

Como siempre solemos hacer vamos a realizar un ejemplo lo más simple posible pero que nos sea válida para lograr nuestros objetivos:

Nueva aplicación universal

Nueva aplicación universal

Añadimos las carpetas Views, ViewModels y Services además de las clases base necesarias para implementar el patrón MVVM de la misma forma que vimos en este artículo. Para probar el control necesitamos una fuente de información. En nuestro ejemplo simplificaremos el proceso al máximo construyendo una colección de elementos en memoria.

Comenzamos creando el modelo de nuestra aplicación:

public class Movie
{
     public string Title { get; set; }

     public string Description { get; set; }

     public string Image { get; set; }

     public int Year { get; set; }
}

Para simplificar el ejemplo, no vamos a conectar nuestra aplicación con internet (rss, servicio web, azure, etc) sino que trabajaremos con datos estáticos. Para ello, en el constructor de nuestra viewmodel creamos un listado de películas:

var movies = new List<Movie>
{    
     new Movie {Title = "John Carter", Year = 2012, Image = "ms-appx:///Assets/Carter.jpg"},
     new Movie {Title = "El caballero oscuro: La leyenda renace", Year = 2012, Image = "ms-appx:///Assets/Batman.jpg"},
     new Movie {Title = "Cisne Negro", Year = 2011, Image = "ms-appx:///Assets/Cisne.jpg"},
     new Movie {Title = "Drive", Year = 2011, Image = "ms-appx:///Assets/Drive.jpg"},
     new Movie {Title = "Toy Story 3", Year = 2010, Image = "ms-appx:///Assets/Toy.jpg"},
     new Movie {Title = "El discurso del rey", Year = 2010, Image = "ms-appx:///Assets/Rey.jpg"},
     new Movie {Title = "Origen", Year = 2010, Image = "ms-appx:///Assets/Origen.jpg"},    
     new Movie {Title = "Avatar", Year = 2009, Image = "ms-appx:///Assets/Avatar.jpg"}
};

NOTA: Las imágenes utilizadas son recursos locales compartidos en una carpeta llamada Assets del proyecto Shared.

Llegamos a este punto podríamos crear una propiedad pública para poder hacer binding (enlazar) desde nuestra interfaz con la colección creada. Sin embargo, dentro de nuestros objetivos está el mostrar la colección de películas agrupadas por año. Además, con el control SemanticZoom queremos mostrar el listado de grupos (años) distintos que contienen películas. Debemos agrupar nuestra colección antes de continuar. Para ello, dentro de nuestra carpeta “Models” anteriormente creada vamos a añadir una nueva clase:

public class MoviesByYear
{
     public int Year { get; set; }
    public List<Movie> Movies { get; set; }
}

Es simple. Almacenará un año junto a la colección de películas de dicho año correspondientes. Tenemos ya donde almacenar la información, vamos a hacerlo.

var moviesByYear = movies.GroupBy(f => f.Year).Select(f => new MoviesByYear { Year = f.Key, Movies = f.ToList() });

Ahora si, creamos una propiedad pública que contendrá la colección de películas agrupadas por año:

public List<MoviesByYear> Movies
{
     get { return _movies; }
     set { _movies = value; }
}

Hemos dejado el view model totalmente preparado. Llega el momento de centrarnos en la vista. Vamos a añadir nuestro control SemanticZoom a nuestra vista MainView:

<SemanticZoom>
     <SemanticZoom.ZoomedInView>

     </SemanticZoom.ZoomedInView>
     <SemanticZoom.ZoomedOutView>

     </SemanticZoom.ZoomedOutView>
</SemanticZoom>

La estructura que puedes ver en la parte superior es la estructura básica a utilizar en un control SemanticZoom. Como puedes observar el control contendrá otros dos controles (normalmente listas). El primero de los controles se añadirá en la vista ZoomedOutview, vista mostrada por defecto. El segundo, se añadirá a la vista ZoomedInView. Vista que mostramos al hacer Zoom Semántico. Ambas vistas están relacionadas semánticamente.

Antes de continuar, veamos algunos miembros importantes del control:

Propiedades

  • ZoomedInView. Vista Zoomed-In del control SemanticZoom.
  • ZoomedOutView. Vista Zoomed-Out del control SemanticZoom.
  • CanChangesView. Determina si el control permite cambiar de vista (Propiedad de sólo lectura).
  • IsZoomedInViewActive. Propiedad de tipo bool nos permite definir con que vista se muestra el control SemanticZoom (ZoomIn o ZoomOut).

Eventos

  • ViewChangeStarted. Se lanza al comenzar el cambio de una vista.
  • ViewChangedCompleted. Se lanza al completarse el cambio de una vista (la nueva vista se muestra).
<SemanticZoom>
     <SemanticZoom.ZoomedInView>
          <ListView>
          </ListView>
     </SemanticZoom.ZoomedInView>
     <SemanticZoom.ZoomedOutView>
          <GridView />
     </SemanticZoom.ZoomedOutView>
</SemanticZoom>

Añadiremos en los recursos de la página un CollectionViewSource que hará binding con nuestra propiedad pública Movies creada en la viewmodel:

<CollectionViewSource
            x:Name="groupedMoviesViewSource"
            Source="{Binding Movies}"
            IsSourceGrouped="true"
            ItemsPath="Movies"/>

Pasamos a definir los DataTemplates:

<DataTemplate x:Key="MovieJumpTemplate">
     <Border Padding="5">
          <Border Background="{Binding Converter={StaticResource BackgroundConverter}}"
                  Width="82" Height="82" HorizontalAlignment="Left">
                <TextBlock Text="{Binding Group.Year}"
                           Foreground="{Binding Converter={StaticResource ForegroundConverter}}"
                           FontSize="24" Padding="6"
                           HorizontalAlignment="Center" VerticalAlignment="Center"/>
          </Border>
     </Border>
</DataTemplate>

<DataTemplate x:Key="MovieItemTemplate">
     <Grid>
          <Grid.ColumnDefinitions>
               <ColumnDefinition Width="Auto"/>
               <ColumnDefinition Width="*"/>
          </Grid.ColumnDefinitions>
          <Image Source="{Binding Image}" Stretch="Uniform" MaxWidth="100"
                 Margin="0, 5"/>
          <TextBlock Grid.Column="1" FontSize="32"
                     Text="{Binding Title}" TextWrapping="WrapWholeWords"
                     Margin="5" />
     </Grid>
</DataTemplate>

<DataTemplate x:Key="MovieGroupHeaderTemplate">
     <Border Background="Transparent" Padding="5">
          <TextBlock Text="{Binding Year}" Foreground="{StaticResource PhoneForegroundBrush}" FontSize="32" Padding="6"
                       FontFamily="{StaticResource PhoneFontFamilySemiLight}" HorizontalAlignment="Left" VerticalAlignment="Center"/>
     </Border>
</DataTemplate>

Necesitamos varias plantillas, en nuestro ejemplo:

  • MovieJumpTemplate: Cada uno de los elementos mostrados en la vista ZoomedOutView del zoom semántico.
  • MovieItemTemplate: Elementos básicos del listado mostrados en la vista por defecto.
  • MovieGroupHeaderTemplate: Cabecera de cada grupo mostrado en la lista por defecto.

NOTA: Creamos una carpeta Themes específica para clada plataforma, en los proyectos Windows y Windows Phone. Aqui añadiremos recursos de diccionarios donde agruparemos los estilos y plantillas específicar a usar en cada plataforma.

Finalmente nuestro zoom semántico quedaría:

<SemanticZoom>
     <SemanticZoom.ZoomedInView>
          <ListView
               IsHoldingEnabled="True"
               ItemsSource="{Binding Source={StaticResource groupedMoviesViewSource}}"
               ItemTemplate="{StaticResource MovieItemTemplate}">
               <ListView.GroupStyle>
                    <GroupStyle HidesIfEmpty="True"
                                HeaderTemplate="{StaticResource MovieGroupHeaderTemplate}">
                    </GroupStyle>
               </ListView.GroupStyle>
          </ListView>
     </SemanticZoom.ZoomedInView>
     <SemanticZoom.ZoomedOutView>
          <GridView ItemsSource="{Binding Source={StaticResource groupedMoviesViewSource}, Path=CollectionGroups}"
                    ItemTemplate="{StaticResource MovieJumpTemplate}"/>
     </SemanticZoom.ZoomedOutView>
</SemanticZoom>

Analicemos el código anterior:

  • Ya hemos visto el funcionamiento del zoom semántico además de conocer sus dos vistas, ZoomedInView y ZoomedOuView.
  • En la vista por defecto mostramos un control ListView. Este control nos permite organizar los elementos en grupos utilizando la propiedad GroupStyle. La apariencia de cada cabecera vendrá definido por la plantilla indicada en la propiedad HeaderTemplate y podemos ocultar cabeceras sin contenido gracias a la propiedad HidesIfEmpty.
  • El JumpList lo conseguimos utilizando el ListView con sus grupos junto al zoom semántico.
  • En la vista ZoomOutView mostramos un GridView bindeado a la misma fuente de información que el ListView, el CollectionViewSource pero accediendo a la propiedad CollectionGroups de la vista que contiene la colección de grupos.

El resultado del ejemplo es el siguiente:

Vista reducida en Windows Phone

Vista reducida en Windows Phone

Podemos ver el listado de películas agrupadas por año (vista ZoomInView), mostrando el año como cabecera. Al pulsar sobre cualquiera de los años:

Vista ampliada en Windows Phone

Vista ampliada en Windows Phone

Pasamos a la vista ZoomOutView viendo todos los años disponibles. La aplicación Windows Store mostrará un resultado similar:

Vista reducida en Windows

Vista reducida en Windows

Mediante un simple gesto (acercando ambos dedos alejamos el zoom, alejando los dedos lo acercamos) o pulsando la tecla CTRL del teclado mientras hacemos scroll con el ratón (o pulsamos las teclas + o -) pasamos a la vista ZoomOutView:

Vista ampliada en Windows

Vista ampliada en Windows

En la vista ZoomOutView vemos el listado de años mostrando además información extra, en este caso el número de películas disponibles en cada año.

Puedes descargar el ejemplo realizado:

Espero que lo visto en la entrada os resulte interesante. Recordar, cualquier duda o sugerencia será bienvenida en los comentarios de la entrada.

Conclusiones

  • El control SemanticZoom forma parte del conjunto de controles WinRT compartido entre Windows y Windows Phone.
  • A pesar de tener el control compartido entre ambas plataformas, en cada una de ellas se adapta para otorgar la experiencia adecuada.
  • El control nos permite trabajar con dos vistas distintas a la hora de mostrar colecciones con un número elevado de elementos. Evita scrolls muy largos, facilita la navegación e interacción entre los elementos.
  • ZoomedInView es la vista por defecto que muestra el Zoom Semántico.
  • ZoomedOutView es la vista mostrada al hacer Zoom Semántico.
  • Para poder utilizar un control en alguna de las vistas del SemanticZoom debe implementar la interfaz ISemanticZoomInfo.
  • Por defecto los controles que heredan de ListViewBase implementan la interfaz ISemanticZoomInfo.
  • CUIDADO: Con el uso del control Listbox que no hereda de ListViewBase y por lo tanto NO implementa ISemanticZoomInfo. No se puede utilizar un control Listbox en una de las vistas del control SemanticZoom.
  • Normalmente se muestra la colección de elementos en la vista ZoomedInView y los grupos de dicha colección en la vista ZoomedOutView. Por ello, también es normal que la fuente de datos del control sea un CollectionViewSource. Aunque no tiene porque ser siempre asi.
  • No tiene una propiedad ItemsSource. No hacemos Binding directamente a una colección de datos. El Binding se realiza desde los controles lista añadidos a cada una de las vistas del SemanticZoom.
  • Por supuesto, el contenido de las distintas vistas pueden hacer scroll.
  • No esta pensado para mostrar una colección y al pulsar sobre un elemento de la misma navegar a otra colección de detalles (Drill-Down). La información mostrada en ambas vistas es la misma.

Artículos relacionados

Más información

[Tips and Tricks] Detectar si una App Universal pasa a segundo plano

Send-To-BackIntroducción

Las aplicaciones universales corren bajo un ciclo de vida de modo que si el usuario cambia (switch) nuestra aplicación por otra, en un plazo de 10 segundos, el sistema pasará nuestra aplicación al estado de suspensión notificando a la misma vía el evento Suspending. Si el usuario cambia de nuevo a nuestra aplicación, el sistema “despierta” a la misma siendo notificada por el evento Resuming. Mientras la aplicación esta suspendida el sistema puede poner fin a la misma por necesidad de recursos. En este caso, la aplicación lanzará el evento Activated la próxima vez que el usuario la ejecute.

Ciclo de vida

Ciclo de vida

Estos eventos entre otros nos permiten controlar en la mayoría de situaciones a la perfección el ciclo de vida de las aplicaciones. Sin embargo, en ocasiones esto no es suficiente. A veces, necesitamos ser notificados de manera inmediata cuando nuestra aplicación pasa a segundo plano y no podemos confiar en el evento de suspensión ya que se lanzará 10 segundos despues de que otra aplicación pase a primer plano ocupando la pantalla. Por ejemplo, en el caso de un juego, se debe hacer una pausa inmediatamente tras pasar a segundo plano. En caso contrario sería fatal que pasasen aunque sean unos segundos sin control sobre el mismo!

¿Que podemos hacer?

Detectar el paso a segundo plano de manera inmediata

Para detectar el paso inmediato a segundo plano podemos utilizar el evento VisibilityChanged de la ventana de la aplicación. Para ello, en el evento OnLaunched del archivo App.xaml.cs tras asignar el Frame a utilizar como contenido de la ventana:

Window.Current.Content = rootFrame;

Nos suscribimos al evento VisibilityChanged de la ventana de la aplicación:

Window.Current.VisibilityChanged += Current_VisibilityChanged;

El evento anterior se lanzará tanto cuando la ventana de la aplicación pasa a segundo plano como cuando pasa a primer plano. Lo podremos detectar con facilidad gracias al argumento Windows.UI.Core.VisibilityChangedEventArgs:

void Current_VisibilityChanged(object sender, Windows.UI.Core.VisibilityChangedEventArgs e)<br />
{
     if (e.Visible)
  
          //  App en primer plano
     }
     else
     {
          //  App en segundo plano
     }
}

El argumento VisibilityChangedEventArgs cuenta con una propiedad booleana que nos indica si esta visible o no, de modo que podemos diferenciar facilmente si estamos en primer plano o en segundo plano.

Fácil, ¿verdad?. Puedes descargar un pequeño ejemplo con el código anterior a continuación:

Más información