Almacenando datos locales en una aplicación híbrida de Blazor .NET MAUI usando IndexedDB - Parte 1
Tags: dotnet, blazor, dotnetmaui, indexeddb
Published: 2023-12-04 12:38:34

_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](https://elcamino.dev/calendario-adviento-net-maui-espanol-23/)** 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í](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB):

> 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**](https://github.com/Eilon/MauiHybridWebView) que fue [presentado hace unos meses](https://www.youtube.com/ watch?v=NxFunRzi-tc) y [destacado durante .NET Conf 2023](https://www.youtube.com/watch?v=u30XwO9-10Q). 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/57tvxfep8qvc7yx3ie1s.png)

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



![Agregando el paquete NuGet](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b34o3qir0ybgzzh2mfkj.png)


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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rt589h89atokmfp2ei60.png)

## 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();
}
```

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>
```

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");
            });
        }
    }
}
```

## 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>
```

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) => {
        HybridWebView.SendRawMessageToDotNet("Database error: " + event.target.errorCode);
    };

    request.onsuccess = (event) => {
        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) => {
        db = event.target.result;

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

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();
    };
}

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 => {
        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");
        }
    });
}
```


## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/as7vp8ww1x34kmfduigo.png)


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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/csl94u1gf9rrzekgb794.png)


¡É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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e846pbj8ij7kjl4m7udj.png)

¡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](https://github.com/icebeam7/NetMauiIndexedDb).

Espero que esta publicación te haya sido útil. Recuerda seguir el resto de publicaciones interesantes del [Calendario de Adviento .NET MAUI 2023](https://elcamino.dev/calendario-adviento-net-maui-espanol-23/).

Gracias por leer. ¡Hasta la próxima!

Luis