Practical .NET

Working with Local Storage in a Blazor Progressive Web App

Thanks to Chris Sainty and Remi Bourgarel, working with local storage from a Blazor application running either in the browser or out of it is relatively easy. Testing your code can be equally easy but only if you set up support the real world of network connections.

In a previous article, I discussed how to create a Blazor application that could be installed on the user's computer and be accessed without starting a web browser (a "Progressive Web App" or PWA). Because the application can be accessed without starting the web browser, you may want to also enable that application to run without a network connection. If that's the case, then you'll need to leverage local storage to replace data retrieved from web services.

Accessing Local Storage
Fortunately, accessing local storage is easy once you've added Chris Sainty's Blazored.LocalStorage NuGet Package to your application (the project and its documentation can be found on GitHub).

Before anything else, to use Sainty's package, you need to add it your project's Services collection. Normally, I'd do that in my project's Startup class but the Visual Studio template for a PWA doesn't include a Startup class. So, in a PWA, you'll need to add Sainty's package to the Services collection in the Program.cs file.

The Program.cs file in the PWA template already includes code to add an HttpClient to the Services collection. You can add Sainty's package by tacking on a call to his AddBlazoredLocalStorage extension method, like this:

builder.Services.AddTransient(sp => new HttpClient { 
                             BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)})
                .AddBlazoredLocalStorage();

To support this extension method, you'll need this using statement:

using Blazored.LocalStorage;

After that, you need to inject Sainty's local storage object into your Razor component by adding this code at the start of your Razor page:

@inject Blazored.LocalStorage.ILocalStorageService ls

That code gets you the asynchronous version of the object's API. If you use ISyncLocalStorageService, you'll get a synchronous API. The API for both is rich and includes these methods (these are, obviously, the methods for the asynchronous interface):

  • SetItemAsync()
  • GetItemAsync()
  • GetItemAsStringAsync
  • RemoveItemAsync()
  • ClearAsync()
  • LengthAsync()
  • KeyAsync()
  • ContainsKeyAsync()

Storing and retrieving an object with local storage can be as simple as this code that saves and retrieves a Customer object:

Customer cust;
await biStorage.SetItem(cust.CustomerId, cust);
cust = await biStorage.GetItem<Customer>("B456");

It's easy to check to see what's in local storage: Press F12 to bring up the Developer's tools panel in either the browser or PWA version of your app, click on the Application tab (which may be hidden under the tools overflow menu icon), and select Storage from the left-hand list.

While the code is straightforward, I found debugging the resulting application ... problematic. First, if I simply started debugging my application by pressing F5, my browser would hang on either a blank page or the "Loading ... " prompt in the Index.html page that hosts my Blazor app. Using Start Without Debugging worked every time, however (and, besides, it's not like Visual Studio's server-side debugger is going to be much help with your code, anyway).

Determining If You're Offline (and Testing)
My next problem was determining if my application was offline and testing its behavior when it wasn't connected. In JavaScript code, I can use the browser's Navigator object's onLine property to determine my connection status. Except, of course, I want to access the onLine property from my Blazor code.

My implemented solution may seem over-complicated, but I'll explain why I adopted it as I go. First, I defined a Boolean field a that I set to reflect my online status:

private bool onLine = true;

To support accessing the onLine property, I added the NuGet package for Remi Bourgarel's BrowserInterop package. BrowserInterop provides Blazor-enabled access to a ton of DOM options, including the window.Navigator object and its onLine property (the package also includes support for LocalStorage, but I preferred Chris Sainty's package).

To use BrowserInterop, I also needed to add this script tag to the Index.html page that hosts my Blazor application:

<script src="_content/BrowserInterop/scripts.js"></script>

BrowserInterop also requires Blazor's JavaScript runtime package so I added this to the top of my app's Razor page:

@inject IJSRuntime jsr;

In my application's constructor, I used this code to retrieve the Navigator object and wire up a method (called OnConnectionStateChanged) to be called when my browser's connection status changes. After that, I called my OnConnectionStateChanged method, passing the Navigator object, to initialize my online status:

WindowNavigator biNavigator;
protected async override Task OnAfterRenderAsync(bool firstRender)    
{
  if (firstRender)
  {
    WindowInterop biWindow = await jsr.Window();
    WindowNavigator biNavigator = await biWindow.Navigator();
    await biNavigator.Connection.OnChange(async () => 
      OnConnectionStateChanged(biNavigator.onLIne));
    OnConnectionStateChanged(biNavigator.Online);
  }
}

In this code, the Window method called from the JavaScriptRuntime object is another extension method and requires this using statement:

using BrowserInterop.Extensions;

In my OnConnectionStateChanged method, I set my internal onLine field ... which leads to one of the reasons that I picked this approach. I discovered that, as my application changed from online to offline mode, there were some actions I needed to take to support the change that were independent of any specific network-dependent method. My OnConnectionStateChange method provided a good place to put that code, driven by my onLine field. This change can trigger UI changes, so I also call StateHasChanged in this method:

protected async ValueTask OnConnectionStateChanged(bool onlineState)
{
  
  if (onLine != onlineState && onLineState)
  {
    // ... code to support switching to online mode ... 
  }
  if (onLine != onlineState && !onLineState)
  {
    // ... code to support switching to offline mode ... 
  }
  if (onLine != onlineState)
  {
    StateHasChanged();
  }
  onLine = onlineState; 
  return await Task.CompletedTask;
}

This design also gives me the ability to let the user switch between offline and online mode -- just wire a button or menu item up to a method like this:

protected async void ToggleState()
{
  await OnConnectionStateChanged(!onLine);   
}

And, for testing, while I could use Developers' Tools (which has an option on the Network tab to take the browser online or offline), a button like this is the easiest way to test my code in offline mode.

Reality Intrudes
While this design facilitates testing, the real reason that I implemented it is because, in production, I don't use the Navigator object's onLine property to make the decision about making network calls. The onLine property may report, correctly, that you're connected ... but you may have a connection that's sufficiently erratic or so slow as to make you, effectively, offline.

In production, I wrap all my HttpClient calls (i.e. all the places where I need a network connection) inside an object that implements the Circuit Breaker and Retry patterns. That wrapper object takes a much stricter view of what counts as "online" than the Navigator object does. In production, it's from that object that I call my OnConnectionChanged method to manage my onLine field.

And now, thanks to the ecosystem growing up around Blazor, when your user's connection fails, rather than failing, you can consider extending your PWA to provide some functionality.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

Subscribe on YouTube