In the first post of this series we discussed the various options available to represent state information in Blazor WebAssembly. Our conclusion was that the best option is to represent state information in data structures held in Blazor Dependency Injection engine, and to store them on disk (IndexedDb, localStorage) only when the user leaves the application or when the application is about to crash.
We gave also a way to catch unhandled exceptions in order to save the application state before the application crashes. However, are we sure there are no other events that might cause the in-memory state be lost? Of course there are! User might inadvertently navigate to an external page, or he/she might close the application browser page / tab. Luckily, in all these circumstances we can exploit the onbeforeunload and onunload events. More specifically, in case there are uncompleted tasks and the user is about to leave the application we can use the onbeforeunload event to ask confirmation, and in case the user decides to proceed we can use the onunload event to save the state of all uncompleted tasks.
So now we have a complete plan! We need to decide just how to represent all state tasks and how to save them on disk.
Representing Tasks State
We have seen that it is convenient to enclose each Task state in a containe classr, so each processor can easily detect when a task is being processed just by looking if the container is empty or not. In case a container is empty a new task can be created from scratch and put in the container so that also other pages can find it.
Let modify the code of the first post to add a TasksStateService class. As a first step let create a State folder in the StateManager library project. Then let move the StateContainer.cs file that is in the client project to this new folder. After that, let modify the file namespace to “StateManager” to yield:
namespace StateManager
{
public abstract class StateContainer
{
public abstract object RoughState {get;}
}
public class StateContainer<T> : StateContainer
{
public T State { get; set; }
public override object RoughState => State;
}
}
We can add also a method that verify if a task is executing:
namespace StateManager
{
public abstract class StateContainer
{
public abstract object RoughState {get;}
public abstract bool IsRunning {get;}
}
public class StateContainer<T> : StateContainer
{
public T State { get; set; }
public override bool IsRunning { get {
return State != null && !State.Equals(default); } }
public override object RoughState => State;
}
}
We can index all task States with a dictionary enclosed in the TasksStateService class. Let add this class to the same state folder and let replace the scaffolded code with the following code:
namespace StateManager
{
public class TasksStateService
{
private Dictionary<string, StateContainer>
OverallState
= new Dictionary<string, StateContainer>();
public bool IsRunning(string x)
{
return OverallState.TryGetValue(x, out var state)
&& state.IsRunning;
}
public bool IsDirty()
{
return OverallState.Values
.Any(x => x.IsRunning);
}
}
}
The IsRunning method returns whether the Task indexed by string x was started and not yet completed, while IsDirty returns whether there is or not at least
a task that is running and needs to be saved.
Now we can add also a method to get an existing state or create it if it doesn’t exist:
public T Get<T>(string x, bool createNew)
{
if (OverallState.TryGetValue(x, out var state))
{
if (state.IsRunning) return (T)state.RoughState;
else if (createNew)
{
var res = Activator.CreateInstance<T>();
((StateContainer<T>)state).State = res;
return res;
}
else return default;
}
else if (createNew)
{
var container = new StateContainer<T>
{
State = Activator.CreateInstance<T>()
};
OverallState[x] = container;
return container.State;
}
else return default;
}
Finally, we need a method that finishes a task, thus resetting its container:
public void Finish<T>(string x)
{
if (OverallState.TryGetValue(x, out var state))
((StateContainer<T>)state).State = default;
}
With all this in place we can start using our TasksStateService, it is enough to add it to the Dependency Injection of the application project.
For this purpose it is enough to add it to the AddStateManagemenet extension method in the “Extensions/StateHandling.cs” file of the StateManager
library project:
public static IServiceCollection AddStateManagemenet(
this IServiceCollection services)
{
services.AddSingleton<IErrorHandler, DefaultErrorHandler>();
services.AddLogging(builder => builder.CustomLogger());
//add the line below
services.AddSingleton<TasksStateService>();
return services;
}
At this point the only missing piece is a state saving/reloading facility.
Saving and reloading the application state
Now we have to decide where and how to save the application state: localStorage can only store strings, while IndexedDb can store also object graphs that satisfy certain constraint.Unluckily, we can’t take advantage of the better opportunity offered by IndexedDb since we can’t move object graphs from WebAssembly to JavaScript, since Blazor interop features just cope with JSON-serializable graphs, that are essentially object trees.
Unluckily, in the general case the complete state of a .Net Core Task is not a tree but a more complex graph. Therefore, we are left with the only option sketched below:
- Serialize state information with the powerful .Net Core binary serializer.
- Transform bytes returned by the binary serializer into a BASE64 string that can be moved to JavaScript as a string.
- Once we have state information represented as a string the simplest and more efficient storage option is localStorage.
We can proceed as follows:
- When application start if there is a saved state we load it. We can also improve this step by asking the user if he would like to restore a previously saved state.
- Once the user decides to load or discard the previous state we delete the stored state to prevent the application to load it again at next application start, in case no new state is saved.
Of course our library project will provide just the save, load, and delete methods that the developer can use as he/she prefer to build a custom state management strategy.
Serialization and Deserialization
As a first step let write two protected methods that perform serialization and deserialization of the application state. It is not convenient to serialize the whole dictionary containing state information, it is better to filter just KeyValue pairs containing uncompleted tasks. The serialize method code is straightforward:
protected string Serialize()
{
var toSerialize=OverallState
.Where(m => m.Value.IsRunning)
.ToList();
string result=null;
using (var stream = new MemoryStream())
{
var formatter = new BinaryFormatter();
formatter.Serialize(stream, toSerialize);
stream.Flush();
result=Convert.ToBase64String(stream.ToArray());
}
return result;
}
The deserialize method is the exact converse:
protected void Deserialize(string s)
{
byte[] binary = Convert.FromBase64String(s);
using (var stream = new MemoryStream(binary))
{
var formatter = new BinaryFormatter();
var res = formatter.Deserialize(stream)
as List<KeyValuePair<string, StateContainer>>;
OverallState = res.ToDictionary(m => m.Key, m => m.Value);
}
}
BinaryFormatter requires that all classes to be serialized/de-serialized be marked with the [Serializable] attribute. Therefore we must add it to both StateContainer, and StateContainer<T>:
[Serializable]
public abstract class StateContainer
[Serializable]
public class StateContainer<T> : StateContainer
Now we miss just the routines to store retrieve, and delete serialized strings from localStorage.
Interacting with the localStorage
We need to call methods of window.localStorage from TasksStateService C# code, so we must inject the IJSRuntime interface in its constuctor (see here for more information about the usage of the IJSRuntime interface). Moreover, TasksStateService needs also an instance of the IErrorHandler interface to add an event handler to its OnException event that saves the state in case of errors.
Therefore, let add both interfaces to the TasksStateService constructor, and let save both interfaces in private variables:
IJSRuntime JSRuntime;
IErrorHandler ErrorHandler;
public TasksStateService(IJSRuntime jSRuntime,
IErrorHandler errorHandler)
{
JSRuntime = jSRuntime;
ErrorHandler = errorHandler;
ErrorHandler.OnException += SaveError;
}
SaveError is the error handler that saves the state in case of exceptions. Let write just its skeleton, we will complete it in a short time:
public async Task SaveError(Exception ex)
{
}
The SaveError handler must be detached when TasksStateService is destroyed, so TasksStateService must implement IDisposable:
public class TasksStateService: IDisposable
public void Dispose()
{
ErrorHandler.OnException -= SaveError; ;
}
Now we have everything in place to write our Save method:
public async Task Save(string key)
{
try
{
var s = Serialize();
await JSRuntime
.InvokeVoidAsync("window.localStorage.setItem", key, s);
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
In case of errors we just write the error message in the browser console.
Load is completely analogous:
public async Task<bool> Load(string key)
{
try
{
var s = await JSRuntime
.InvokeAsync<string>("window.localStorage.getItem", key);
if (s != null)
{
Deserialize(s);
return true;
}
else return false;
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
return false;
}
}
In case no state is found in local storage, Load returns false.
We need also a Delete method to delete a previously recorder state after its recovery:
public async Task Delete(string key)
{
await JSRuntime
.InvokeVoidAsync("window.localStorage.removeItem", key);
}
Finally, we need a default name for the location where to store the saved state in case of unhandled exceptions, so we can fill also the SaveError method skeleton:
public string ErrorKey { get; set; }
public async Task SaveError(Exception ex)
{
await Save(ErrorKey);
}
Our Save/Load state Ready! Now, we need just to modify our test application so we can test our TasksStateService class.
Testing the overall state management / error recovery logic
As a firs step let add the initialization logic of our state management/recovery to the program.cs file of our Blazor client application. For this purpose let add the following method to the Program class:
private async static Task InitializeState(IServiceProvider services,
string errorStateKey)
{
var state=services.GetRequiredService<TasksStateService>();
state.ErrorKey = errorStateKey;
if (await state.Load(errorStateKey))
{
await state.Delete(errorStateKey);
}
}
It gets the TasksStateService singleton, sets the name chosen for saving the state in case of crashes, and then tries to load a previously saved state. If it succeeds the Load method returns true, and the saved state is removed from localStorage.
This method must be called, at application start as soon as the application host has been built. Therefore, we need to replace the default code scaffolded by Visual Studio below:
await builder.Build().RunAsync()
With:
var built = builder.Build();
await InitializeState(built.Services,
"stateSavedBeforeError");
await built.RunAsync();
We can test everything in the Counter page. As a first Step we must create a state class for the state of this page:
namespace StateManagement.Client
{
[Serializable]
public class CounterStatus
{
public int Counter { get; set; }
}
}
State types MUST be reference types so that when the a component modifies a copy it gets from TasksStateService, the copy held in the TasksStateService is automatically updated.
Moreover, each state object must have a default constructor, so it can be created, when needed, by the TasksStateService with the Activator class. Finally, it must be marked with the [Serializable] attribute for our serialization methods to work properly. Optionally, we may replace the [Serializable] attribute with the implementation of the ISerializable interface in order to provide a custom serialization logic.
When the page starts it must require a state instance to the TasksStateService singleton that must be injected in the page with something like:
private CounterStatus currentCount;
protected override void OnInitialized()
{
currentCount = tasksStateService
.Get<CounterStatus>("counter", true);
}
If a state object has been already created, that one is returned, otherwise a fresh one is created because we set the createNew parameter to true. The page will have a button to increment the counter and a button that throws an exception, so we can verify the state recovery capabilities of the application. The full code of the page is shown below:
@page "/counter"
@inherits ComponentBase
@inject StateManager.TasksStateService tasksStateService
<h1>Counter</h1>
<p>Current count: @currentCount.Counter</p>
<button class="btn btn-primary"
@onclick="IncrementCount">
Click me
</button>
<button class="btn btn-primary"
@onclick="TryException">
TryException
</button>
@code {
private CounterStatus currentCount;
protected override void OnInitialized()
{
currentCount = tasksStateService
.Get<CounterStatus>("counter", true);
}
private void IncrementCount()
{
currentCount.Counter++;
}
private void TryException()
{
throw new Exception();
}
}
Now let start the application, go to the counter page, increment a few times the counter, and then let click the “TryException” button. The application crashes and we are prompted to reload the browser page. When we reload the page the value of the counter is retained! We were able to recovery the application state.
If you don’t want to mark all state objects with a [Serializable] attribute, you can mark the Serialize and Deserialize methods as virtual, and override them with third parties serializer. Among them a good one is SharpSerializer. It overcomes the needs for the [Serializable] attribute but has other limitations.
Thats all for now! The code respository of the first post has been updated with the new code.
In the next post of this series we will add support for the onbeforeunload and onunload events.
Francesco
Tags: Blazor, Blazor Controls Toolkit