Jul 28 2020

State Management and Error Recovery in Blazor WebAssembly part 3

Category: Blazor | Asp.net coreFrancesco @ 05:35

State Management and Error Recovery in Blazor WebAssembly part 2

State Management and Error Recovery in Blazor WebAssembly part 1

 

In this third an last part we will analyze how to save automatically the application state when the browser or the Blazor application tab are closed by the user.

We will also add the possibility to ask confirmation of exiting the application to the user in case there are still open tasks. Since this can only be done via JavaScript

we are forced to write some script and to connect it to C# code thanks to Blazor-JavaScript Interoperability (see here, and here). As a first step we must prepare to process and deploy JavaScript files.

Processing and deploying JavaScript files

JavaScript files contained in a library must be minimized and merged in a single file that will be deployed as a library internal resource. Minimization and merging of CSS and JavaScript files can be carried out with the help of the “BuildBundlerMinifier” Nuget package. Let install its last stable version in our “StateManager” project. After that let add a “js” folder in the project root, and let add a JavaScript file named “stateManager.js” inside of it. This file must be minimized and placed inside the library project wwwroot folder.

“BuildBundlerMinifier” is informed about which file to process and how to process them through a json configuration file that must be called “bundleconfig.json” and must be placed in the library project root. Let add it and let place the following code in it:

[
    {
        "outputFileName": "wwwroot/stateManager.min.js",
        "inputFiles": [
            "js/stateManager.js"
        ],
        "minify": {
            "enabled": true,
            "renameLocals": true
        },
        "sourceMap": true
    }
]

Each object specifies how to create an output file, in our case "wwwroot/stateManager.min.js”. The input files to merge are listed in the “inputFiles” string array, “minify” specifies the minimization options, and finally “sourceMap” whether to create or not a source-map that maps instructions in the minimized file into their source instructions in the input files, in order to allow an easy debug.

If now we right click on the state project and choose “rebuild”, the library project is rebuilt and our input JavaScript file is minimized and placed in the wwwroot folder. Clearly since our input file is empty also its minimized counterpart will be empty.

Before starting our code let finish our configuration by recalling the minimized version of our JavaScript file in main program wwwroot/index.html file:

<script src="_framework/blazor.webassembly.js"></script>
    //below the newly added JavaScript reference
    <script src="_content/StateManager/stateManager.min.js?v=6"></script>
</body>

In general a JavaScript file placed as a resource in a library can be referenced as “_content/<library name>/<file name>”.

Now we need to design both our JavaScript side and our WebAssembly side code.

We need a JavaScript function that adds both an event handler for the “beforeunload” event and for the “unload” event. We will call this JavaScript function from C# to enable the auto-save and exit confirmation features. Both JavaScript handlers, in turn, will call C# methods that will handle the events exploiting  all information available on the WebAssembly side.

The C# side code

The best place where to add our C# handler is the TaskStateService class. The unload handler must save the state in a default location, and possibly call a custom event. The code below do the job:

public event Func<Task> BeforeUnload;
public string  UnloadKey { get; set; }
[JSInvokable]
public async  Task OnUnload()
{

    if (BeforeUnload != null)
        await BeforeUnload.Invoke();
    if (IsDirty())
        await Save(UnloadKey);
}

In the UnloadKey the user can place the name of the localStorage where to store the application state, BeforeUnload is an event where the developer can place some custom unload code, and finally the OnUnload method is the handler that will be called by JavaScript. The “[JSInvokable]” attribute makes it callable from JavaScript.

OnUnload invokes custom events, if any, and then, if the application state is not empty, saves it to local storage.

The beforeunload handler must return a not null, and not empty string if the user must be prompted for confirmation of leaving the application. In old browsers the string returned is used as prompt, but modern browser, instead, use a standard message:

public string UnloadPrompt { get; set; }
[JSInvokable]
public  string OnBeforeUnload()
{
    if (IsDirty()) return UnloadPrompt;
    else return string.Empty;
}

Both the above methods must be called by JavaScript handlers installed with “window.addEventListener”, as we will see in the next section.

The JavaScript side code

The JavaScript code consists in a “stateManager.AddUnloadListeners” function that receives as input a .Net reference to a TaskStateService instance and installs the JavaScript handlers that call the TaskStateService methods we defined in the previous section on this instance:

(function () {
    window["stateManager"] = {
        "AddUnloadListeners": function (netObject) {
        try {
            window.addEventListener("beforeunload",
                function (event) {
                    var res = null
                    try {
                        res = netObject.invokeMethod('OnBeforeUnload');
                    }
                    catch (ex) {
                        console.error(ex);
                    }
                    if (res) {
                        event.preventDefault();
                        event.returnValue = res;
                    }
                    else delete event["returnValue"];
                });
            window.addEventListener("unload",
                function () {
                    try {
                        netObject.invokeMethod('OnUnload');
                    }
                    catch (ex) {
                        console.error(ex);
                    }

                });
        } catch (e) {
            console.error(e);
        }
    }
    }
})();

The code above is quite standard, the only peculiar snippets are the ones used to call the two .Net instance methods:

res = netObject.invokeMethod('OnBeforeUnload');

 

netObject.invokeMethod('OnUnload');

 

Done! We need just to define a .Net function that invokes “stateManager.AddUnloadListeners”. We can define it as an extension method in the “Extensions/StateHandling.cs” file:

public static async Task<IServiceProvider>
    EnableUnloadEvents(this IServiceProvider services)
{
    var state = services.GetRequiredService<TasksStateService>();
    IJSRuntime jSRuntime = services.GetRequiredService<IJSRuntime>();
    await jSRuntime.InvokeVoidAsync(
            "stateManager.AddUnloadListeners", state.JsTeference);
    return services;
}

We are now ready to test our library.

Putting everything together

As a first step we must call the EnableUnloadEvents from the program.cs file of the main application:

 

await built.Services.EnableUnloadEvents();
await built.RunAsync();

 

Now in the same file we need to update our InitializeState method, so that it set also the UnloadKey and the prompt for asking the user if he would like

to quit the application or not :

private  async static Task InitializeState(IServiceProvider services,
    string errorStateKey,
    string exitConfirm)
{
    var state=services.GetRequiredService<TasksStateService>();
    state.ErrorKey = errorStateKey;
    state.UnloadKey = errorStateKey;
    state.UnloadPrompt = exitConfirm;
    if (await state.Load(errorStateKey))
    {
        await state.Delete(errorStateKey);
    }

}

 

After this change the main becomes:

public static async Task Main(string[] args)
{
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    builder.RootComponents.Add<App>("app");
            
    builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
    builder.Services.AddStateManagemenet();
    var built = builder.Build();
    await InitializeState(built.Services,
        "stateSaved", "There are unsaved changes. Quit anyway?");
    await built.Services.EnableUnloadEvents();
    await built.RunAsync();
}

Now just start the application, increment the counter, and then close the browser. You will be prompted for confirmation, confirm. The reopen the browser, you will see the same count you left before!

It works!

 

The updated code is available on Github here.

 

Important: Some mobile browsers for smart phones are bugged and do not trigger the unload event when a browser tab is closed but only when you move to a different site. Therefore, when you target these devices you must provide also manual save.

 

Here, you can see a complex application that shows what you can do with the state management technique described. The demo is based on the Blazor Controls Toolkit, which uses basically the same code described in these posts for its management. The trick for achieving similar results  is a careful design of the component state during each component design.

 

That’s all for now! The next post will be about a new interesting subject.

Francesco

Tags: ,