Storing local data in a .NET MAUI Blazor Hybrid App using IndexedDB - Part 1

Luis Beltran - Dec 1 '23 - - Dev Community

This publication is part of the .NET MAUI Advent Calendar 2023, an initiative led by Héctor Pérez, Alex Rostan, Pablo Piovano, and Luis Beltrán. Check this link for more interesting articles about .NET MAUI created by the community.

The most commonly used option for storing structured data locally in a .NET MAUI app is SQLite database. However, because it is possible to create a .NET MAUI Blazor Hybrid app, a new option can be considered: IndexedDB, a database that is built into a browser,

Definition

Taken from here:

IndexedDB is a way to persistently store data inside the browser. Because it lets you create web applications with rich query abilities regardless of network availability, your applications can work both online and offline.

Before writing this post, I didn't know if it was possible or not to use IndexedDB in a Blazor Hybrid app. In theory, it is possible, so I thought it would be an interesting case to explore. I'm not sure if there are advantages or disadvantages over SQLite, but I do know that there is the typical benefit around Blazor Hybrid: If you already have a web application which stores local data using IndexedDB, then you can bring your code to a mobile app using Blazor Hybrid.

Let's demonstrate this. Moreover, I am going to use the experimental MauiHybridWebView component that was presented a few months ago and highlighted during the .NET Conf 2023. This component allows you to use JavaScript in your .NET MAUI app, it also enables communication between the code in the WebView (JavaScript) and the code that hosts the WebView (C#/.NET), so you can have a React JS app hosted in a .NET MAUI native app. It's amazing!

Step 1. Create & configure the project

First of all, create a Blazor Hybrid .NET MAUI app. You must use .NET 7 at minimum for the MauiHybridWebView component to work.

Creating a project

Add the EJL.MauiHybridWebView NuGet package in your application:

Adding the NuGet package

Using the Solution Explorer, open the Resources folder of your project. Inside the raw folder, create a new folder named hybrid_root. Then, create two new files there: index.html and dbscript.js:

Web files

Step 2. Add the .NET MAUI C#/XAML mobile code:

Open MauiProgram.cs. Add the HybridWebView support in the CreateMauiApp method, just before returning the MauiApp built instance:



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


Enter fullscreen mode Exit fullscreen mode

Now, open MainPage.xaml and delete the controls (just keep the top ContentPage definitions). Then, modify it according to the following instructions:

  • Add a reference to the HybridWebView assembly.
  • Add a HybridWebView component in the Content section of the ContentPage. Set the following properties and values:
    • Set Name to hwv.
    • Set HybridAssetRoot to the hybrid_root folder created in Step 1.
    • Set MainFile to the index.html file created in Step 1.
    • Set RawMessageReceived to an OnJsRawMessageReceived method that will be created in the C# code.

This is a sample implementation code:



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

Then, open MainPage.xaml.cs. Here:

  • Add the HybridWebView namespace.
  • Enable the Web Dev Tools from the HybridWebView component in the constructor after InitializeComponent method.
  • Create an async method called OnJsRawMessageReceived which displays a message sent by JavaScript code. It uses a Dispatcher for a safe interaction with the UI. The message is included in the HybridWebViewRawMessageReceivedEventArgs argument from the HybridWebView component.

This is the code:



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

Step 3. Add the HTML/JS web code:

For the index.html page, you can:

  • Define a basic HTML5 page that references two scripts: HybridWebView.js (from the NuGet package) and dbscript.js (which includes the code to handle the IndexedDB database).
  • Add a button. When it is pressed, it will execute a load_data method defined in dbscript.js.
  • Add an ordered list. This one will dynamically show the data stored in a student table from the IndexedDB database.

Please note, the dbscript.js reference is added before the body ends, because we need the HTML elements to be created first.

This is the code:



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

Finally, for dbscript.js, two local variables are created: students_table (for the name of the table that will exist in our database) and students (the ordered list that will display the table content).

Moreover, some methods are defined:

  • load_data: It simply invokes init_database method.
  • init_database: First of all, it checks if indexedDB is supported in the browser. If so, then it tries to set up a connection to a schoolDB IndexedDB database. If it doesn't exist yet, it is created. The onupgradeneeded event is invoked once the database is created for the first time, and here we create the students table (object store). The onsuccess event is fired once we have successfully connected to the database, and here the insert_student method is called twice.
  • insert_student: As you can expect, this method adds an entry in the students table.
  • show_students: This method firstly removes all the content of the ordered list. Then, it gets all entries from the students table. A Cursor object iterates over each record in the table to create dynamic HTML content to display each entry.

By the way, the SendRawMessageToDotNet method (from the HybridWebView component) is used to send a message from JavaScript to .NET code.



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. Test the app.

Now, you should be able to build the project and test it without any issues. I will test it in a Windows app first.

Here is the initial content of our app. As you can observe, only HTML components (a button and a title) are displayed, since we didn't really add any XAML UI except the Web View that hosts the web elements:

Initial view of the app

Click on the button. The JavaScript code that interacts with an IndexedDB database in the browser is executed, and the following output is presented:

Data loaded

Success! Our app has created a local database with one table and two entries, which are displayed in the app. Moreover, a JavaScript message was passed to the C# part, and this communication is possible thanks to the HybridWebView component. Would it be possible to pass the data instead of a simple message so we can build a UI using XAML? I guess I've just found a new topic to write about, so I will explore this scenario soon =)

Finally, don't forget that since we enabled the Web DevTools, we can bring them to debug or, even better, to see our database:

IndexedDB content

Great! The IndexedDB storage contains the schoolDB database that we created in our app (under Application --> Storage section). Then, a Students table containing two entries is there, so everything is working as expected.

Before we go, two disclaimers:

  • It might happen that when you run the app, the window is empty. I'm not sure if this is a bug of the HybridWebView (don't forget that it is an experimental component) or if it happens because the Web DevTools were enabled. Simply run the app again and it should work (try again if not, it will eventually show up). I noticed that when I comment the line that enables the tools, the app works without any issues, so probably this is an issue. I will do some exploration.

  • If you run the app again, the entries will be duplicated. This is kind of obvious, since we insert them after the connection to the database is successful. You can clear the database/table by using the Web DevTools at any time, of course.

Well, that's it! You can find the source code of this project in my GitHub repo.

I hope this post was helpful for you. Remember to follow the rest of the interesting publications of the .NET MAUI Advent Calendar 2023.

Thank you for reading. Until next time!

Luis

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