[Xamarin.Forms] Uso de SQLite, múltiples tablas, relaciones y operaciones en cascada

Introducción

El trabajo con datos en dispositivos móviles se ha convertido ya en algo común y habitual en el desarrollo de aplicaciones. Existe una gran variedad de tipos de datos y formas de almacenamiento:

  • Archivos de texto. Texto plano o html cacheado en el espacio de almacenamiento aislado de la aplicación.
  • Imágenes. En el espacio de almacenamiento aislado de la aplicación o almacenadas en directorios conocidos del sistema.
  • Archivos serializados. Archivos XML o Json con objetos serializados.
  • Bases de datos. Cuando se requieren datos estructurados, obtener información más compleja con consultas avanzadas entre otro tipo de necesidades, la posibilidad de las bases de datos es la elección idónea.

Las ventajas de utilizar una base de datos son múltiples:

  • Almacenamiento estructurado con eficacia alta.
  • Posibilidad de utilizar consultas y aplicar filtros.
  • Posibilidad de reutilizar conocimientos de base de datos en la gestión de datos en nuestras aplicaciones móviles.

Introducción a SQLite

SQLite es un motor de base de datos Open Source utilizado en todas las plataformas móviles y adoptado tanto por Apple como Google como Microsoft. El uso de SQLite en aplicaciones móviles es una gran opción ya que:

  • La base de datos es pequeña y fácil de portar.
  • La base de datos se concentra en un pequeño archivo.
  • Implementa la mayor parte del estándar SQL92.

Arrancamos el proyecto

Comenzamos creando una aplicación Xamarin.Forms utilizando una librería NET Standard:

Nueva aplicación Xamarin.Forms usando Net Standard

Tras crear la aplicación, añadimos las carpetas básicas para aplicar el patrón MVVM además del paquete NuGet de Autofac para la gestión del contenedor de dependencias.

Estructura del proyecto

Con el proyecto y estructura base creada, vamos a añadir SQLite al proyecto. Para ello, vamos a usar NuGet. Vamos a añadir en cada proyecto de la solución la última versión disponible del paquete utilizando NuGet. El paquete a utilizar es sql-net-pcl, implementación Open Source con soporte a .NET.

sqlite-net-pcl

Tras añadir la referencia vamos a crear una interfaz que defina como obtener la conexión con la base de datos y abstraer la funcionalidad específica de cada plataforma. Trabajando con SQLite, el único trabajo específico a implementar en cada plataforma es determinar la ruta a la base de datos.

public interface IPathService
{
     string GetDatabasePath();
}

En Android, la implementación de IPathService nos permite obtener la ruta a la base de datos.

[assembly: Dependency(typeof(PathService))]
namespace TodoSqlite.Droid.Services.Sqlite
{
     public class PathService : IPathService
     {
          public string GetDatabasePath()
          {
               string path = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
               return Path.Combine(path, AppSettings.DatabaseName);
          }
     }
}

NOTA: Utilizamos el atributo assembly:Dependency para poder realizar la resolución de la implementación con DependencyService.

En iOS:

[assembly: Dependency(typeof(PathService))]
namespace TodoSqlite.iOS.Services.Sqlite
{
     public class PathService : IPathService
     {
          public string GetDatabasePath()
          {
                string docFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
                string libFolder = Path.Combine(docFolder, "..", "Library", "Databases");

                if (!Directory.Exists(libFolder))
                {
                     Directory.CreateDirectory(libFolder);
                }

                return Path.Combine(libFolder, AppSettings.DatabaseName);
          }
     }
}

El archivo de la base de datos lo situamos dentro de la carpeta Library dentro del espacio de almacenamiento de la aplicación.

Y en UWP:

[assembly: Dependency(typeof(PathService))]
namespace TodoSqlite.UWP.Services.Sqlite
{
     public class PathService : IPathService
     {
          public string GetDatabasePath()
          {
               return Path.Combine(ApplicationData.Current.LocalFolder.Path, AppSettings.DatabaseName);
          }
     }
}

Todo listo para comenzar!

La definición de modelos

En nuestra aplicación, trabajaremos con elementos del listado ToDo, una única entidad sencilla.

public class TodoItem
{
     [PrimaryKey, AutoIncrement]
     public int Id { get; set; }
     public string Name { get; set; }
     public string Notes { get; set; }
     public bool Done { get; set; }
}

La gestión de campos especiales o relacionados las gestionamos mediante el uso de etiquetas. En nuestro ejemplo establecemos el campo Id como clave primaria gracias a la etiqueta PrimaryKey y además que autoincremente con el uso de AutoIncrement.

Trabajando con SQLite

Para trabajar con la base de datos utilizaremos DependencyService para obtener la implementación de IPathService y obtener la ruta a la base de datos en cada plataforma.

var databasePath = DependencyService.Get<IPathService>().GetDatabasePath();

Con la ruta de la base de datos, creamos una conexión:

var sqlCon = new SQLiteAsyncConnection(databasePath);

Comenzamos creando la tabla necesaria en la base de datos.

await _sqlCon.CreateTableAsync<TodoItem>().ConfigureAwait(false);

Continuamos con las operaciones básicas de CRUD. Para obtener la información almacenada en una tabla podemos acceder a la tabla y obtener el listado utilizando el método ToListAsync.

public async Task<IList<TodoItem>> GetAll()
{
     var items = new List<TodoItem>();
     using (await Mutex.LockAsync().ConfigureAwait(false))
     {
          items = await _sqlCon.Table<TodoItem>().ToListAsync().ConfigureAwait(false);
     }

     return items;
}

NOTA: Podemos realizar consultar SQL utilizando el método QueryAync.

A la hora de insertar, verificamos si estamos ante un registro existente o no, para realizar el registro de un nuevo elemento o actualizar uno existente con los métodos InsertAsync o UpdateAsync respectivamente.

public async Task Insert(TodoItem item)
{
      using (await Mutex.LockAsync().ConfigureAwait(false))
      {
           var existingTodoItem = await _sqlCon.Table<TodoItem>()
           .Where(x => x.Id == item.Id)
           .FirstOrDefaultAsync();

          if (existingTodoItem == null)
          {
               await _sqlCon.InsertAsync(item).ConfigureAwait(false);
          }
          else
          {
               item.Id = existingTodoItem.Id;
               await _sqlCon.UpdateAsync(item).ConfigureAwait(false);
          }
     }
}

Eliminar es una acción sencilla realizada con el método DeleteAsync.

await _sqlCon.DeleteAsync(item);

El resultado del ejemplo:

Tenéis el código fuente disponible e GitHub:

Ver GitHub

Múltiples tablas

Con lo visto hasta aquí, tenemos suficiente para gestionar una base de datos local en Android, iOS y Windows. Sin embargo, ¿cómo hacemos relaciones entre diferentes tablas?, ¿y operaciones en cascada que afecten a múltiples tablas a la vez?.

Con sqlite-net-pcl, podemos trabajar con diferentes tablas y realizar relaciones. Veamos un ejemplo:

public class Stock 
{ 
     [PrimaryKey, AutoIncrement] 
     public int Id { get; set; } 
     public string Symbol { get; set; } 
} 
 
public class Valuation 
{ 
     [PrimaryKey, AutoIncrement] 
     public int Id { get; set; } 
     [Indexed] 
     public int StockId { get; set; } 
     public DateTime Time { get; set; } 
     public decimal Price { get; set; } 
}

Es importante resaltar el uso de atributo Indexed. Estamos asociando las tablas Valuation y Stock.

Creamos ambas tablas utilizando el método CreateTable por cada tabla:

db.CreateTable<Stock>();       
db.CreateTable<Valuation>();

Y obtenemos la información relacionada con ambas tablas gracias al uso de queries:

return db.Query<Valuation> ("select * from Valuation where StockId = ?", stock.Id);

De esta forma obtendríamos los registros de la tabla Valuation dado un SotckId específico.

Esta es una forma simple de trabajar. Sin embargo, existe un wrapper de SQLite.NET llamado SQLite-Net Extensions que amplía funcionalidades como permitir gestionar relaciones (uno a uno, uno a varios, varios a uno y varios a varios), operaciones en cascada además de otras opciones interesantes de forma muy sencilla.

Vamos a regresar a nuestra aplicación de tareas. Para aprender como gestionar más de una tabla, vamos a añadir una segunda tabla de pasos en las tareas. De modo que, una tarea tendrá N pasos.

Veamos la creación de las tablas. Creamos una segunda tabla (recuerda, una clase) llamada Step:

[Table("Steps")]
public class Step
{
     [PrimaryKey, AutoIncrement]
     public int Id { get; set; }

     public string Name { get; set; }

     [ForeignKey(typeof(TodoItem))]
     public int WorkerId { get; set; }
}

Al crear directamente la definición, vamos a especificar relaciones entre tablas. En este caso, destacamos una propiedad que será la ForeignKey de la tabla TodoItem.

[Table("TodoItems")]
public class TodoItem
{
     [PrimaryKey, AutoIncrement]
     public int Id { get; set; }
     public string Name { get; set; }
     public string Notes { get; set; }
     public bool Done { get; set; }
     [OneToMany(CascadeOperations = CascadeOperation.CascadeInsert)]
     public List<Step> Steps { get; set; }
}

Gracias a las extensiones aportadas, podemos definir directamente la relación entre las tablas TodoItems y Steps. En este caso será un OneToMany. Por defecto, las operaciones no son recursivas. Podemos modificar este compartamiento estableciendo operaciones en cascada. En nuestro caso, usaremos CascadeOperations para establecer el insertar registros en cascada (al registrar un nuevo TodoItem, registraremos también sus Steps).

Para crear ambas tablas, al igual que con sqlite-net-pcl, utilizamos el método CreateTableAsync:

await _sqlCon.CreateTableAsync<Step>(CreateFlags.None).ConfigureAwait(false);
await _sqlCon.CreateTableAsync<TodoItem>(CreateFlags.None).ConfigureAwait(false);

Operaciones en cascada

A la hora de insertar registros, contamos con diferentes métodos en las extensiones de SQLite:

  • InsertWithChildren
  • InserOrReplaceWithChildren
  • InserAllWithChildren
  • InsertOrReplaceAllWithChildren

Su uso dependerá de si vamos a insertar o reemplazar y del número de registros.

NOTA: De cada método existe una version asíncrona.

await _sqlCon.InsertWithChildrenAsync(item, recursive: true).ConfigureAwait(false);

También existen otras opciones interesantes como:

  • Actualizar en cascada.
  • Eliminar en cascada.
  • Propiedades sólo de lectura.
  • Diferentes tipos de relaciones y relaciones inversas.
  • Etc.

Tenéis el código fuente disponible e GitHub:

Ver GitHub

Recordad que podéis dejar cualquier comentario, sugerencia o duda en los comentarios.

Más información

[Xamarin.Forms] Propiedades específicas desde XAML para Windows Desktop y Mobile

Introducción

Xamarin.Forms añade una capa de abstracción sobre la capa de la interfaz de usuario permitiendo definir la misma una única vez siendo válida para todas las plataformas.

Xamarin.Forms
Xamarin.Forms

A pesar de definir la interfaz de usuario una única vez para todas las plataformas, tenemos la posibilidad de realizar personalizaciones y adaptaciones para ofrecer la mejor experiencia posible en cada una de las plataformas soportadas.

La clase Device

La clase Device sin duda alguna es una de las claves a la hora de personalizar la interfaz de usuario por plataforma. Esta clase contiene diversas propiedades y métodos para personalizar la UI y la funcionalidad en base a la plataforma.

La propiedad OS permite identificar la plataforma en la que se ejecuta la aplicación. Puede ser un valor de la enumeración TargetPlatform con uno de los siguientes valores:

  • iOS
  • Android
  • WinPhone (Windows 8 Silverlight)
  • Windows (Windows 8.1, Windows Phone 8.1 y UWP)

Permite de forma sencilla y efectiva personalizar la interfaz o la lógica de la aplicación en base a la plataforma:

if (Device.OS == TargetPlatform.iOS) 
{
    stackLayout.Padding = new Thickness (0, 20, 0, 0);
}

La interfaz de usuario de las aplicaciones Xamarin.Forms podemos definirla en C# o desde código XAML. Desde XAML también podemos realizar personalizaciones en base a la plataforma utilizando el método OnPlatform. Device.OnPlatform es un método que cuenta con uno de los siguientes parámetros opcionales:

  • iOS
  • Android
  • WinPhone
<BoxView 
     WidthRequest="100"
     HorizontalOptions="Center">
  <BoxView.Color>
    <OnPlatform x:TypeArguments="Color"
      iOS="Green"
      Android="#738182"
      WinPhone="Accent" />
  </BoxView.Color>
</BoxView>

Lo visto esta genial pero hay un pequeño detalle que nos puede causar problemas…

¿Qué ocurre si deseamos personalizar la UI desde XAML y dinstinguir entre Windows Phone y Windows?.

La clase OnPlatform no nos facilita la gestión completa. Siempre nos queda la posibilidad de hacer lo necesario desde código C# pero aspectos directamente relacionados con la UI sería fantástico poder llevarlo a cabo desde la propia UI o lo que es lo mismo desde código XAML.

CustomOnPlatform

Aquí es donde entra en juego un pequeños helper creado en código C#:

public sealed class CustomOnPlatform<T>
{
     public CustomOnPlatform()
     {
         Android = default(T);
         iOS = default(T);
         WinPhone = default(T);
         Windows = default(T);
     }

     public T Android { get; set; }

     public T iOS { get; set; }

     public T WinPhone { get; set; }

     public T Windows { get; set; }

     public T Other { get; set; }

     public static implicit operator T(CustomOnPlatform<T> onPlatform)
     {
         switch (Xamarin.Forms.Device.OS)
         {
             case Xamarin.Forms.TargetPlatform.Android:
                 return onPlatform.Android;
             case Xamarin.Forms.TargetPlatform.iOS:
                 return onPlatform.iOS;
             case Xamarin.Forms.TargetPlatform.WinPhone:
                 return onPlatform.WinPhone;
             case Xamarin.Forms.TargetPlatform.Windows:
                 if (Xamarin.Forms.Device.Idiom == Xamarin.Forms.TargetIdiom.Desktop)
                     return onPlatform.Windows;
                 else
                     return onPlatform.WinPhone;
         }
     }
}

Sencillo. Utilizamos la propiedad OS de la clase Device para determinar la plataforma en la que estamos. En el caso de Windows, dado que nos indica el mismo valor en UWP (tanto si estamos en el Desktop como Mobile) hacemos uso combinado con la propiedad Idiom.

La propiedad Idiom tiene como objetivo fundamental determinar si la aplicación se ejecuta en teléfono, tableta o escritorio. Puede contener unos de los siguientes valores de la enumeración TargetIdiom:

  • Desktop: Detecta aplicación UWP en Windows 10.
  • Phone: La aplicación corre en iPhone, iPod Touch, móvil Android o Windows Phone.
  • Tablet: iPad, tableta Android o Windows 8.1.

De esta forma, además de detectar cuando se ejecuta en iOS y Android, podremos detectar cuando se ejecuta en Windows Phone y en Windows (UWP) tanto en escritorio como móvil.

Para usar el helper desde XAML, definimos el namespace:

xmlns:helpers="clr-namespace:CustomOnPlatform.Helpers;assembly=CustomOnPlatform"

Y lo usamos tal y como usaríamos OnPlatform:

<BoxView 
     HorizontalOptions="Center"
     VerticalOptions="Center"
     HeightRequest="100"
      WidthRequest="100">
     <BoxView.BackgroundColor>
         <helpers:CustomOnPlatform  
             x:TypeArguments="Color"
             Android="Red"
             iOS="Blue"
             WinPhone="Yellow"            
             Windows="Green"/>
     </BoxView.BackgroundColor>
</BoxView>

El resultado de la ejecución anterior es, cuadrado rojo en Android, azul en iOS, amarillo en Windows Phone (Silverlight) y Windows Mobile (UWP) siendo por último, verde en Windows escritorio.

Tenéis el código fuente del ejemplo utilizado disponible en GitHub:

Ver GitHub

Recuerda, cualquier tipo de duda o sugerencia es bienvenida en los comentario del artículo.

Más información

[Material] Microsoft Tech Summit

El evento

El pasado 05 y 06 de Octubre, en el Palacio Municial de Congresos de Madrid tenía lugar el Microsoft tech Summit 2016, un evento donde se daban cita clientes, partners y desarrolladores de tecnologías Microsoft.

Microsoft Tech Summit
Microsoft Tech Summit

Con 10 Tracks organizados en diferentes categorías, varias deceneas de sesiones, un par de talleres y cientos de asistentes, un gran evento donde volver a ver a grandes amigos, conocer a otros nuevos, compartir y aprender mucho.

A continuación, una pequeña galería con algunos momentos del evento:

El material

Pude participar en el evento con una sesión en el track de Mobile Apps & Cross-Platform junto a un taller.

En la sesión me centré en dar a conocer todas las implicaciones y conceptos necesarios para desarrollar una aplicación UWP para Xbox One. Aspectos como la gestión del Gamepad, sonido o la adaptación a TV además de cómo adaptar la UI, sacar partido con efectos visuales y elementos 3D.

App UWP para Xbox One
App UWP para Xbox One

La presentación utilizada la tenéis disponible a continuación:

En cuanto a las demos técnicas realizadas, las tenéis disponible en GitHub:

Ver GitHub

Además de la sesión, por la tarde, disfrutamos de un divertido taller de 2,5h sobre Xamarin.

Realizamos paso a paso desde cero una aplicación meteorológica realizada con Xamarin.Forms y utilizando MVVM. Muy divertido!

Me gustaría terminar agradeciendo a Microsoft, sponsors, ponentes y asistentes por hacer el posible el evento. Ante la cantidad y calidad de sesiones, el networking, sorpresas, concursos y grandes momentos, no puedo terminar de otra forma que no sea…

¿Para cuándo el próximo?

Más información

[Material CartujaDotNet] Sevilla Windows REBUILD

El evento

El pasado 21 de Abril, desde el grupo CartujaDotNet, grupo de usuarios .NET de Sevilla, organizábamos el Sevilla Windows REBUILD. Un evento donde recopilamos algunas de las principales novedades a nivel de desarrollo en la plataforma Universal Windows además de tener momentos para el networking y algun que otro detalle con los asistentes.

Novedades desarrollo UWP presentadas en el BUILD
Novedades desarrollo UWP presentadas en el BUILD

El material

Pude participar en el evento con múltiples sesiones. Tras una breve introducción a la agenda del evento comenzamos viendo las últimas novedades introducidas en Visual Studio (VS 2015 Update 2 y VS 15) destinadas para el desarrollo de UWP. Novedades en el editor XAML o Editar y Continuar en XAML:

Continuamos utilizando Desktop App Converter (aka Project Centennial) viendo como convertir una App Win32 en UWP.

En la siguiente sesión nos centramos en el desarrollo de aplicaciones UWP para Xbox One. Vimos desde como activar cualquier Xbox One para el modo desarrollador hasta como adaptar nuestras aplicaciones para ofrecer la mejor experiencia posible (gestión del gamepad, foco, TV safe area o sonidos).

En cuanto a las demos técnicas realizadas, las tenéis disponible en GitHub:

Ver GitHub

Quisiera terminar añadiendo algunos agradecimientos a Josué Yeray por participar junto a un servidor en las sesiones y por supuesto, muchas gracias a todos los asistentes.

Más información