[Xamarin Forms] Acceso a bases de datos de tipo SqLite

SqLite permite que las aplicaciones mobiles usen una base de datos local.
Vamos a dedicar este articulo a crear una aplicacion para Android, IOS y UWP usando Xamarin Forms.

Por defecto, SqlLite esta incluido en Android e IOS. Para UWP, es necesario instalar la extensión

SQLite for Universal Windows Platform

Nota: Otra forma de instalarlo, es usando la opción de menú “Herramientas->Extensiones y Actualizaciones” de Visual Studio.

Una vez instalada, vamos a crear un nuevo proyecto Cross Platorm App usando la plantilla de libreria portable.

Una vez creada la solución Xamarin Forms, vamos a añadir el paquete NuGet sqlite-net-plc con la versión 1.5.166-beta

Mientras se escribía este articulo la versión estable era la 1.4.118.0, pero esta versión nos daba un error en el proyecto UWP

System.IO.FileLoadException: 'Could not load file or assembly 'SQLite-net, Version=1.4.118.0, Culture=neutral, PublicKeyToken=null'

Antes de empezar a codificar, vamos recompilar la solucion Xamarin Forms para comprobar que todo este OK.

Nota: si no dispones de un Mac (local o remoto) para compilar el proyecto IOS, desactiva la compilación del proyecto IOS en las propiedades de la solución

Preparar la conexión a la base de datos

Como pasa con cualquier base de datos, es necesario usar una cadena de conexión. Debido a que la base de datos de SqLite esta alojada en un fichero, la cadena de conexión contendrá la ruta fisica, la cual es diferente en cada plataforma.

La mejor forma de configurar la conexión SqLite es usar Dependency Injection.

Añadimos la interfaz ISqLiteConexion al proyecto de libreria portable

using SQLite;

namespace XamarinSqLite.Servicios
{
    public interface ISqLiteConexion
    {
        SQLiteConnection ConexionBaseDatos();
    }
}

El método ConexionBaseDatos debe ser implementado en cada una de las plataformas.

  1. Android
    using SQLite;
    using XamarinSqLite.Servicios;
    using System.IO;
    using XamarinSqLite.Droid.Servicios;
    
    [assembly: Xamarin.Forms.Dependency(typeof(SqLiteConexionAndroid))]
    namespace XamarinSqLite.Droid.Servicios
    {
        public class SqLiteConexionAndroid : ISqLiteConexion
        {
            public SQLiteConnection ConexionBaseDatos()
            {
                string nombreBaseDatos = "ListaTareas.db3";
                string rutaFisica = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), nombreBaseDatos);
                return new SQLiteConnection(rutaFisica);
            }
        }
    }
    

    Del codigo anterior cabe destacar:

    • El atributo Dependency indica que el tipo/clase especificado es registrado para el Dependency Injection
    • La extensión “.db3” es asociada a base de datos SqLite
    • Como ubicación debemos usar el “Personal Folder”, la cual es una carpeta privada (files) dentro del paquete de nuestra App.
    • Por último, la ruta es pasada como parametro al constructor de la clase SQLiteConnection.
  2. IOS
    using XamarinSqLite.iOS.Servicios;
    using XamarinSqLite.Servicios;
    using SQLite;
    using System.IO;
    
    [assembly: Xamarin.Forms.Dependency(typeof(SqLiteConexionIOS))]
    namespace XamarinSqLite.iOS.Servicios
    {
        public class SqLiteConexionIOS : ISqLiteConexion
        {
            public SQLiteConnection ConexionBaseDatos()
            {
                string nombreBaseDatos = "ListaTareas.db3";
                string rutaFisica = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal),"..","Library",nombreBaseDatos);
                return new SQLiteConnection(rutaFisica);
            }
        }
    }
    

    El código anterior es similar al de android, lo unico que hemos cambiado es la ubicacion de la base de datos para que apunte a la ruta predeterminada de la carpeta Library

  3. UWP
    using Windows.Storage;
    using XamarinSqLite.Servicios;
    using System.IO;
    using XamarinSqLite.UWP.Servicios;
    
    [assembly: Xamarin.Forms.Dependency(typeof(SqLiteConexionUWP))]
    namespace XamarinSqLite.UWP.Servicios
    {
        public class SqLiteConexionUWP : ISqLiteConexion
        {
            public SQLiteConnection ConexionBaseDatos()
            {
                string nombreBaseDatos = "ListaTareas.db3";
                string rutaFisica = Path.Combine(ApplicationData.Current.LocalFolder.Path, nombreBaseDatos);
                return new SQLiteConnection(rutaFisica);
            }
        }
    }
    

    En UWP la base de datos debe guardarse en la carpeta local de la App.

Definición del Modelo de Datos

Para este articulo, vamos a trabajar con una simple lista de tareas.

En el proyecto portable vamos a crear una clase que represente la entidad Tarea.

using SQLite;

namespace XamarinSqLite.Modelo
{

    [Table("Tareas")]
    public class Tarea
    {
        private int m_Id;
        [PrimaryKey, AutoIncrement]
        public int Id
        {
            get
            {
                return m_Id;
            }
            set
            {
                this.m_Id = value;
            }
        }

        private string m_Descripcion;
        [MaxLength(200),NotNull]
        public string Descripcion
        {
            get
            {
                return m_Descripcion;
            }
            set
            {
                this.m_Descripcion = value;
            }
        }

        private bool m_Completada;
        [NotNull]
        public bool Completada
        {
            get
            {
                return m_Completada;
            }
            set
            {
                this.m_Completada = value;
            }
        }
    }
}

Hemos incluido una serie de atributos (Table, PrimaryKey, AutoIncrement, MaxLength, NotNull …) para mapear la clase a una tabla de SqLite.

Operaciones sobre la base datos

Para realizar las operaciones CRUD sobre la tabla Tareas de la base de datos, vamos crear una nueva clase llamada AccesoDatosSqLite en el proyecto portable.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SQLite;
using System.Collections.ObjectModel;
using Xamarin.Forms;
using XamarinSqLite.Servicios;
using System.Diagnostics;

namespace XamarinSqLite.Modelo
{
    public class AccesoDatosSqLite:IDisposable
    {
        private SQLiteConnection m_conexion;
        private static object m_ControlBloqueo;
        static AccesoDatosSqLite()
        {
            m_ControlBloqueo = new object();
        }

        public AccesoDatosSqLite()
        {
            try
            {
                m_conexion = DependencyService.Get<ISqLiteConexion>().ConexionBaseDatos();
                m_conexion.CreateTable<Tarea>();
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
        }

        public List<Tarea> Tareas { get; set; }

        public List<Tarea> ObtenerTareas()
        {
            List<Tarea> resultado = null;
            if (m_conexion != null)
            {
                lock (m_ControlBloqueo)
                {
                    try
                    {
                        resultado = new List<Tarea>(m_conexion.Table<Tarea>());
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.Message);
                    }
                }
            }

            return resultado;
        }

        public List<Tarea> ObtenerTareasPendientes()
        {
            List<Tarea> resultado = null;
            if (m_conexion != null)
            {
                lock (m_ControlBloqueo)
                {
                    try
                    {
                        IEnumerable<Tarea> tareasFiltradas = from t in m_conexion.Table<Tarea>()
                                                             where t.Completada == false
                                                             select t;

                        resultado = new List<Tarea>(tareasFiltradas);
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.Message);
                    }
                }
            }

            return resultado;
        }

        public List<Tarea> ObtenerTareaId(int id)
        {
            List<Tarea> resultado = null;
            if (m_conexion != null)
            {
                lock (m_ControlBloqueo)
                {
                    try
                    {
                        IEnumerable<Tarea> tareasFiltradas = from t in m_conexion.Table<Tarea>()
                                                             where t.Id == id
                                                             select t;

                        resultado = new List<Tarea>(tareasFiltradas);
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.Message);
                    }
                }

            }

            return resultado;
        }
        public int GuardarTarea(Tarea tarea)
        {
            int id = 0;
            int numReg = 0;
            int nErrores = 0;
            if ((m_conexion != null) && (tarea!=null))
            {
                try
                {
                    if (tarea.Id != 0)
                    {
                        lock (m_ControlBloqueo)
                        {
                            numReg = m_conexion.Update(tarea);
                        }

                        if (numReg == 0)
                        {
                            nErrores++;
                        }
                        else
                        {
                            id = tarea.Id;
                        }
                    }
                    else
                    {
                        lock (m_ControlBloqueo)
                        {
                            numReg = m_conexion.Insert(tarea);
                        }

                        if (numReg == 0)
                        {
                            nErrores++;
                        }
                        else
                        {
                            id = tarea.Id;
                        }
                    }
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.Message);
                }
            }

            if (nErrores == 0)
            {
                return id;
            }
            else
            {
                return 0;
            }
        }

        public bool EliminarTarea(int id)
        {
            int numReg = 0;
            int nErrores = 0;
            if ((m_conexion != null) && (id>0))
            {
                lock (m_ControlBloqueo)
                {
                    try
                    {
                        numReg = m_conexion.Delete<Tarea>(id);
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.Message);
                    }
                }
                    
                if (numReg == 0)
                {
                    nErrores++;
                }
            }

            if (nErrores == 0)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        public bool LimpiarTablaTareas()
        {
            int numReg = 0;
            int nErrores = 0;
            if (m_conexion != null)
            {
                lock (m_ControlBloqueo)
                {
                    try
                    {
                        numReg = m_conexion.DeleteAll<Tarea>();
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.Message);
                    }
                }
            }

            if (nErrores == 0)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        public bool GuardarTareas(IEnumerable<Tarea> tareas)
        {
            int nErrores = 0;
            int id = 0;
            if ((m_conexion != null) && (tareas != null))
            {
                foreach(Tarea tarea in tareas)
                {
                    id = GuardarTarea(tarea);

                    if (id == 0)
                    {
                        nErrores++;
                    }
                }
            }

            return (nErrores==0);
        }

        #region IDisposable Support
        private bool disposedValue = false; // Para detectar llamadas redundantes

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    if (m_conexion != null)
                    {
                        m_conexion.Dispose();
                        m_conexion = null;
                    }
                }

                // TODO: libere los recursos no administrados (objetos no administrados) y reemplace el siguiente finalizador.
                // TODO: configure los campos grandes en nulos.

                disposedValue = true;
            }
        }

        // TODO: reemplace un finalizador solo si el anterior Dispose(bool disposing) tiene código para liberar los recursos no administrados.
        // ~AccesoDatosSqLite() {
        //   // No cambie este código. Coloque el código de limpieza en el anterior Dispose(colocación de bool).
        //   Dispose(false);
        // }

        // Este código se agrega para implementar correctamente el patrón descartable.
        public void Dispose()
        {
            // No cambie este código. Coloque el código de limpieza en el anterior Dispose(colocación de bool).
            Dispose(true);
            // TODO: quite la marca de comentario de la siguiente línea si el finalizador se ha reemplazado antes.
            // GC.SuppressFinalize(this);
        }
        #endregion
    }
}

En esta clase se realizan las siguientes operaciones:

  • En el constructor de la clase realizamos 2 operaciones:
    1. Ceamos una nueva instancia de tipo SQLiteConnection usando el servicio de inyeccion de dependencias
    2. Invocamos al método CreateTable, para crear la tabla de tareas. Si la tabla ya existe, no se realizará ninguna operacion.
  • El método Table permite acceder al contenido de la tabla y con Linq podemos filtrar los resultados.
  • Para crear un nuevo registro hemos usado el método Insert pasandole como parámetro la instancia de la tarea a guardar. Este método retorna el numero de registros insertados.
    Nota: una vez instertado el registro, podemos recuperar el Id de la misma entidad.
  • La modificación de una tarea se realiza usado el método Update pasandole como parámetro la instancia de la tarea a editar. Este método retorna el numero de registros actualizados.
  • La eliminación se puede realizar invocando al método Delete y pasando como parámetro del Id de la tarea a borrar.

Interfaz

Para el diseño de la interfaz vamos a usar el patron MVVM de forma que la implementación solo la realizaremos en el proyecto portable.

En nuestro proyecto de ejemplo, vamos a crear 2 Views con sus 2 ViewModels correspondientes.

La pantalla inicial muestra un listado con las tareas pendientes

Para cada tarea hay 2 opciones

  • ver: se muestra la vista de creación/edición
  • Finalizar: marca la tarea como completada

y un botón en la toolbar para crear una nueva tarea

En el siguiente enlace se puede descargar el proyecto completo compilado para Android y UWP