Almacenando datos locales en una aplicación híbrida de Blazor .NET MAUI usando IndexedDB - Parte 1

Luis Beltran - Dec 4 '23 - - Dev Community

Esta publicación forma parte del Calendario de Adviento .NET MAUI 2023, una iniciativa liderada por Héctor Pérez, Alex Rostan, Pablo Piovano y Luis Beltrán. Consulta este enlace para obtener más artículos interesantes sobre .NET MAUI creados por la comunidad.

La opción más utilizada para almacenar datos estructurados localmente en una aplicación .NET MAUI es la base de datos SQLite. Sin embargo, debido a que es posible crear una aplicación .NET MAUI Blazor Hybrid, se puede considerar una nueva opción: IndexedDB, una base de datos integrada en un navegador.

Definición

Tomada de aquí:

IndexedDB es una forma de almacenar datos de forma persistente dentro del navegador. Debido a que le permite crear aplicaciones web con capacidades de consulta enriquecidas independientemente de la disponibilidad de la red, sus aplicaciones pueden funcionar tanto en línea como fuera de línea.

Antes de escribir esta publicación, no sabía si era posible usar IndexedDB en una aplicación Blazor Hybrid. En teoría lo es, así que pensé que sería un caso interesante de explorar. No estoy seguro de si existen ventajas o desventajas sobre SQLite, pero sí sé que existe el beneficio típico de Blazor Hybrid: Si ya tienes una aplicación web que almacena datos locales utilizando IndexedDB, puedes traer tu código a un aplicación móvil que utiliza Blazor Hybrid.

Demostremos esto. Además, voy a utilizar el componente experimental MauiHybridWebView que fue presentado hace unos meses y destacado durante .NET Conf 2023. Este componente te permite usar JavaScript en su aplicación .NET MAUI; además, hace posible la comunicación entre el código en WebView (JavaScript) y el código que aloja WebView (C#/.NET) para que puedas tener por ejemplo una aplicación React JS alojada en una aplicación nativa .NET MAUI. ¡Suena increíble!

Paso 1. Crear y configurar el proyecto

En primer lugar, crea una aplicación Blazor Hybrid .NET MAUI. Debes utilizar .NET 7 como mínimo para que funcione el componente MauiHybridWebView.

Creando un proyecto

Agrega el paquete NuGet EJL.MauiHybridWebView en tu aplicación:

Agregando el paquete NuGet

Usando el Explorador de soluciones, abre la carpeta Resources de tu proyecto. Dentro de la carpeta "raw", crea una nueva carpeta llamada "hybrid_root". Luego, crea dos archivos nuevos allí: index.html y dbscript.js:

Archivos web

Paso 2. Agrega el código .NET MAUI C#/XAML (parte móvil):

Abre MauiProgram.cs. Agrega el soporte a HybridWebView en el método CreateMauiApp, justo antes de devolver la instancia construida MauiApp:



...
public static MauiApp CreateMauiApp()
{
...
  builder.Services.AddHybridWebView();
  return builder.Build();
}


Enter fullscreen mode Exit fullscreen mode

Ahora, abre MainPage.xaml y elimina los controles (solo conserva las definiciones de ContentPage ubicadas en la parte superior). Luego, modifícalo de acuerdo con las siguientes instrucciones:

  • Agrega una referencia al ensamblado HybridWebView.
  • Agrega un componente HybridWebView en la sección Content de ContentPage. Establece las siguientes propiedades y valores:
    • Coloca Name en hwv.
    • Configura la propiedad HybridAssetRoot a la carpeta hybrid_root creada en el Paso 1.
    • Establece la propiedad MainFile en el archivo index.html creado en el Paso 1.
    • Establece RawMessageReceived en un método OnJsRawMessageReceived que se creará en el código C#.

El siguiente código muestra lo anterior:



<?xml version="1.0" encoding="utf-8" ?>
<ContentPage 
  ... 
  xmlns:ejl="clr-namespace:HybridWebView;assembly=HybridWebView"
  ...>

    <ejl:HybridWebView x:Name="hwv" 
                       HybridAssetRoot="hybrid_root" 
                       MainFile="index.html" 
                       RawMessageReceived="OnJsRawMessageReceived" />

</ContentPage>


Enter fullscreen mode Exit fullscreen mode

Ahora, abre MainPage.xaml.cs. Aquí realiza lo siguiente:

  • Agrega el espacio de nombres HybridWebView.
  • Habilita las herramientas de desarrollo web desde el componente HybridWebView en el constructor después del método InitializeComponent.
  • Crea un método asíncrono llamado OnJsRawMessageReceived que muestra un mensaje enviado mediante código JavaScript. Se utiliza un Dispatcher para una interacción segura con la interfaz de usuario. El mensaje se incluye en el argumento HybridWebViewRawMessageReceivedEventArgs del componente HybridWebView.

Este es el código:



using HybridWebView;

namespace NetMauiIndexedDb
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();

            hwv.EnableWebDevTools = true;
        }

        private async void OnJsRawMessageReceived(object sender, HybridWebView.HybridWebViewRawMessageReceivedEventArgs e)
        {
            await Dispatcher.DispatchAsync(async () =>
            {
                await DisplayAlert("JavaScript message", e.Message, "OK");
            });
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Paso 3. Agrega el código HTML/JS (parte web):

Para la página index.html, puedes:

  • Definir una página HTML5 básica que haga referencia a dos scripts: HybridWebView.js (del paquete NuGet) y dbscript.js (que incluye el código para manejar la base de datos IndexedDB).
  • Agregar un botón: Cuando se presiona, ejecutará un método load_data definido en dbscript.js.
  • Agregar una lista ordenada: Mostrará dinámicamente los datos almacenados en una tabla student de la base de datos IndexedDB.

Considera que la referencia dbscript.js se agrega antes de que termine el body, porque primero necesitamos crear los elementos HTML.

Este es el código:



<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
    <script src="_hwv/HybridWebView.js"></script>
</head>
<body>
    <div>
        <button onclick="load_data()">Load Data</button>
    </div>

    <br />

    <div>
        <h2>Students</h2>
        <ol></ol>
    </div>

    <script src="dbscript.js"></script>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode

Finalmente, para dbscript.js, se crean dos variables locales: students_table (para el nombre de la tabla que existirá en nuestra base de datos) y students (la lista ordenada que mostrará el contenido de la tabla).

Además, se definen algunos métodos:

  • load_data: Simplemente invoca el método init_database.
  • init_database: En primer lugar, comprueba si indexedDB es soportado por el navegador. Si es así, intenta configurar una conexión a una base de datos IndexedDB schoolDB. Si aún no existe, se crea. El evento onupgradeneeded se invoca una vez que se crea la base de datos por primera vez, y aquí creamos la tabla students (almacén de objetos). El evento onsuccess se activa una vez que nos hemos conectado exitosamente a la base de datos, y aquí el método insert_student se llama dos veces.
  • insert_student: Como es de esperar, este método agrega una entrada en la tabla students.
  • show_students: Este método primero elimina todo el contenido de la lista ordenada. Luego, obtiene todas las entradas de la tabla "estudiantes". Un objeto Cursor itera sobre cada registro de la tabla para crear contenido HTML dinámico para mostrar cada entrada.

Por cierto, el método SendRawMessageToDotNet (del componente HybridWebView) se utiliza para enviar un mensaje desde JavaScript a código .NET.



let students_table = 'Students';
let students = document.querySelector("ol");

function load_data() {
init_database();
}

function init_database() {
if (!window.indexedDB) {
HybridWebView.SendRawMessageToDotNet("Your browser doesn't support IndexedDB");
return;
}

let db;
const request = indexedDB.open('schoolDB', 1);

request.onerror = (event) =&gt; {
    HybridWebView.SendRawMessageToDotNet("Database error: " + event.target.errorCode);
};

request.onsuccess = (event) =&gt; {
    db = event.target.result;

    insert_student(db, {
        name: 'John Doe',
        faculty: 'FAI'
    });

    insert_student(db, {
        name: 'Jane Doe',
        faculty: 'FAME'
    });

    show_students(db);
};

request.onupgradeneeded = (event) =&gt; {
    db = event.target.result;

    let store = db.createObjectStore(students_table, {
        autoIncrement: true,
        keyPath: 'id' 
    });
};
Enter fullscreen mode Exit fullscreen mode

}

function insert_student(db, student) {
const txn = db.transaction(students_table, 'readwrite');
const store = txn.objectStore(students_table);

let query = store.put(student);

query.onsuccess = function (event) {
    console.log(event);
};

query.onerror = function (event) {
    console.log(event.target.errorCode);
}

txn.oncomplete = function () {
    db.close();
};
Enter fullscreen mode Exit fullscreen mode

}

function show_students(db) {
while (students.firstChild) {
students.removeChild(students.firstChild);
}

const txn = db.transaction(students_table, 'readwrite');

const store = txn.objectStore(students_table);
store.openCursor().addEventListener('success', e =&gt; {
    const pointer = e.target.result;

    if (pointer) {
        const listItem = document.createElement('li');
        const h3 = document.createElement('h3');
        const pg = document.createElement('p');
        listItem.appendChild(h3);
        listItem.appendChild(pg);
        students.appendChild(listItem);

        h3.textContent = pointer.value.name;
        pg.textContent = pointer.value.faculty;
        listItem.setAttribute('data-id', pointer.value.id);

        pointer.continue();
    } else {
        if (!students.firstChild) {
            const listItem = document.createElement('li');
            listItem.textContent = 'No Students.'
            students.appendChild(listItem);
        }

        HybridWebView.SendRawMessageToDotNet("Data has been loaded");
    }
});
Enter fullscreen mode Exit fullscreen mode

}

Enter fullscreen mode Exit fullscreen mode




Step 4. Prueba la aplicación

Ahora debería poder construir el proyecto y probarlo sin ningún problema. Primero lo probaré en una aplicación de Windows.

Aquí está el contenido inicial de nuestra aplicación. Como puedes observar, sólo se muestran los componentes HTML (un botón y un título), ya que realmente no agregamos ninguna interfaz de usuario XAML excepto la vista web que aloja los elementos web:

Vista inicial de la aplicación

Haz clic en el botón. Se ejecuta el código JavaScript que interactúa con una base de datos IndexedDB en el navegador y se presenta el siguiente resultado:

Datos cargados

¡Éxito! Nuestra aplicación ha creado una base de datos local con una tabla y dos entradas, que se muestran en la aplicación. Además, se pasó un mensaje JavaScript a la parte C# y esta comunicación es posible gracias al componente HybridWebView. ¿Sería posible pasar los datos en lugar de un simple mensaje para que podamos crear una interfaz de usuario usando XAML? Supongo que acabo de encontrar un nuevo tema sobre el cual escribir, así que exploraré este escenario pronto =)

Finalmente, no olvides que como habilitamos Web DevTools, podemos traerlas para depurar o, mejor aún, para ver nuestra base de datos:

Contenido de IndexedDB

¡Excelente! El almacenamiento IndexedDB contiene la base de datos schoolDB que creamos en nuestra aplicación (en la sección Application --> Storage). Luego, hay una tabla "Estudiantes" que contiene dos entradas, por lo que todo funciona como se esperaba.

Antes de continuar, dos situaciones a comentar:

  • Puede suceder que cuando ejecutas la aplicación, se muestre vacía, sin contenido. No estoy seguro si esto es un error de HybridWebView (no olvides que es un componente experimental) o si sucede porque las Web DevTools estaban habilitadas. Simplemente ejecuta la aplicación nuevamente y debería funcionar (inténtalo nuevamente, si no, eventualmente aparecerá). Noté que cuando comento la línea que habilita las herramientas, la aplicación funciona sin problemas, así que probablemente esto sea una situación a considerar. Haré un poco de exploración al respecto.

  • Si vuelves a ejecutar la aplicación, las entradas se duplicarán. Esto es algo obvio, ya que los insertamos después de que la conexión a la base de datos sea exitosa. Puede borrar la base de datos/tabla utilizando Web DevTools en cualquier momento, por supuesto.

¡Bueno, eso es todo! Puedes encontrar el código fuente de este proyecto en mi

  • Si vuelves a ejecutar la aplicación, las entradas se duplicarán. Esto es algo obvio, ya que los insertamos después de que la conexión a la base de datos es exitosa. Puedes borrar la base de datos/tabla utilizando Web DevTools en cualquier momento, por supuesto.

¡Bueno, eso es todo! Puedes encontrar el código fuente de este proyecto en mi repositorio de GitHub.

Espero que esta publicación te haya sido útil. Recuerda seguir el resto de publicaciones interesantes del Calendario de Adviento .NET MAUI 2023.

Gracias por leer. ¡Hasta la próxima!

Luis

. . . . . . . . . . . . . . . . . . . . . . . . . . .