The Practical Client

Creating a Progressive Web App with Blazor WebAssembly

Not surprisingly, it's dead easy to create an app in Blazor that runs outside of the browser window and (potentially) in an offline mode. Before you get carried away, though, there are some key design decisions to make.

When people talk about a Progressive Web App (PWA) they typically refer to an app that can (a) run in the browser but (b) can also function as a desktop app, even in (c) offline mode, and do that while (d) looking as much like a native app as possible. Blazor WebAssembly (formerly Client Side Blazor) makes creating a running PWA that hits most of those marks relatively easy. But, of course, just creating something that runs isn't enough: You have to create something useful which, if you want to support running offline, can be ... challenging.

Creating a Running App
Your first step to creating a PWA in Visual Studio 2019 (I used the Community Edition Preview) is to select the Blazor App template in the File | New Project dialog. After starting the Blazor App wizard and giving your project a name, on the Create a new Blazor app page, select the Blazor WebAssembly app choice and check the Progressive Web App checkbox before clicking the Create button. You've created a PWA.

To test your app in the web browser, just press F5 as you would normally. To run your app as a PWA, just pick the install option provided by your browser ... an option which will vary from one browser to another. In my current version of Chrome, for example, the overflow menu on the toolbar (the three vertical dots) includes an "Install <app name>" option. When I clicked that I got another dialog box with an Install button. Clicking that that button not only installed my app and added the app to my Windows menu, it opened my app in its own window.

Your app's pages are downloaded to the user's computer and held in a local cache to be used by the app. The pages that make up your app aren't converted from HTML so your PWA's UI doesn't change (i.e. your app will still look like a web app rather than a native app, unless you use some clever CSS to manage your page's appearance).

Working with the Server
Your project includes JavaScript files called service-worker.js and service-worker.published.js that manage the relationship between the PWA and server-side resources. For example, if your app has pages that should always be fetched from the server you can enhance the server-worker file.

By default, as the user navigates from one page to another in your app, the Blazor PWA retrieves the pages from a cache on the user's computer. However, you can force the app to get specific pages from the server by modifying the onFetch method in the service-worker file (fetched pages are still displayed in your app's window -- altering this code won't suddenly throw the user back into their web browser).

In the onFetch method, the app looks for navigate events and tests the requested page's URL to set the shouldServerIndexHTML variable. When this variable is true, pages are fetched from the local cache. When you want to get pages from the server, you just need to extend this test to return false when the user attempts to navigate to a page you want fetched from the server. You can find the page's URL in the request.url property of the event parameter passed to the onFetch method (that parameter is cleverly called "event" in the current temple).

This code, for example, ensures that the ContactUs page will always be fetched from the server:

const shouldServeIndexHtml = event.request.mode === 'navigate' &&
                             !event.request.url.includes("ContactUs");

Handling Changes
Pushing changes to your app out to your PWA users is also pretty easy. Every time the user starts the app as a PWA, the service-worker code checks to see if the app's local resources match the server's resource. If the match fails then the app downloads the latest version of the resources from the server. Boiling this down, it means that updating all the copies of your app (provided your PWA is actually being used) just means deploying the new version of the app to your server.

You can, if you wish, override the process used to determine if a new version is to be downloaded by rewriting the service-worker file's onFetch method. You can, also, have the app download assets from a different URL than the app was retrieved from.

That doesn't mean that there aren't some issues, though. As Microsoft points out, the new version of the app is downloaded in the background and isn't provided to the user until the next time the user uses the app ... and you don't know how long any particular user may go before they use your app. You have to assume that, at any time, every prior version of your app is about to be used one more time before it's upgraded.

That means, when web services used from the app are changed, you need to make sure that every prior version of your app can continue to work with your service. The simplest way to ensure that is to not make any breaking changes in existing services and, instead, deploy new services to support changed functionality. That way the existing, deployed versions of the app can continue to call the old versions of your services. Your other options is, when the user's version of the app isn't compatible with your current services, to disable the app until the user shuts down and restarts.

Designing Functionality
The biggest challenges in designing your PWA are based on why you're creating a PWA. There are, at least, three reasons for creating a PWA.

One is that you simply want your app to start faster (presumably a local copy of the app will start faster for your user than navigating to your site and downloading the app). Another good reason for creating a PWA is to reduce demands on the server: Because navigation between pages is handled locally, the demand on your server is reduced to just the REST requests that your app may make (especially if new versions of your app are downloaded from a different server, further reducing demand on your application server).

A third reason (and probably the one that you're thinking of) is to enable your app to work even when the user doesn't have a network connection. Be aware: That option opens you up to a potential world of hurt.

To begin with you need to realize that, while the user is offline, you won't be able to authenticate your user. You'll have to decide what functionality you'll provide to a "non-authenticated user" -- all you know about "non-authenticated" users is that they have successfully logged onto the device that your app is running on.

Next, you'll need to decide what display functionality you'll provide. To support that, you need to ensure that all the required data is kept locally and that the local data isn't "too stale" when compared with the server data.

The simplest solution for that option is to take the same "cache-first" approach that Blazor does to your web pages: At start up, if you have a network connection, fill local storage with data from the server. From then on, have your app always fetch data from local storage (this should also make your app more responsive). If you don't have a network connection then you can support offline processing by checking that the data isn't "too stale" (you'll have to decide what that means) and skipping refreshing the data. If you don't have a connection and the data is "too stale," you'll have to disable the app.

Finally, you'll need to decide what changes (if any) an unauthenticated user is allowed to make in offline mode. If you're going to allow local changes then you must provide a way to synchronize changes made while the app is offline when the app comes back online (including dealing with potential, conflicting changes made while the app was offline).

Those are the issues. I'll be looking at some of the enabling technology in upcoming columns.

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

  • ML.NET Improves Object Detection

    Microsoft improved the object detection capabilities of its ML.NET machine learning framework for .NET developers, adding the ability to train custom models with Model Builder in Visual Studio.

  • More Improvements for VS Code's New Python Language Server

    Microsoft announced more improvements for the new Python language server for Visual Studio Code, Pylance, specializing in rich type information.

  • Death of the Dev Machine?

    Here's a takeaway from this week's Ignite 2020 event: An advanced Azure cloud portends the death of the traditional, high-powered dev machine packed with computing, memory and storage components.

  • COVID-19 Is Ignite 2020's Elephant in the Room: 'Frankly, It Sucks'

    As in all things of our new reality, there was no escaping the drastic changes in routine caused by the COVID-19 pandemic during Microsoft's big Ignite 2020 developer/IT pro conference, this week shifted to an online-only event after drawing tens of thousands of in-person attendees in years past.

  • Visual Studio 2019 v16.8 Preview Update Adds Codespaces

    To coincide with the Microsoft Ignite 2020 IT pro/developer event, the Visual Studio dev team shipped a new update, Visual Studio 2019 v16.8 Preview 3.1, with the main attraction being support for cloud-hosted Codespaces, now in a limited beta.

Upcoming Events