In this chapter, we continue to look at managing state. Most applications manage state in some form.
A state is simply information that is persisted in some way. It can be data stored in a database, session states, or even something stored in a URL.
The user state is stored in memory either in the web browser or on the server. It contains the component hierarchy and the most recently rendered UI (render tree). It also contains the values or fields and properties in the component instances as well as the data stored in service instances in dependency injection.
If we make JavaScript calls, the values we set are also stored in memory. Blazor Server relies on the circuit (SignalR connection) to hold the user state, and Blazor WebAssembly relies on the browser’s memory. If we reload the page, the circuit and the memory will be lost. Managing state is not about handling connections or connection issues but rather how we can keep the data even if we reload the web browser.
Saving state between page navigations or sessions improves the user experience and could be the difference between a sale and not. Imagine reloading the page, and all your items in the shopping cart are gone; chances are you won’t shop there again.
Now imagine returning to a page a week or month later, and all those things are still there.
In this chapter, we will cover the following topics:
Some of these things we have already talked about and even implemented. Let’s take this opportunity to recap the things we have already talked about, as well as introduce some new techniques.
Make sure you have followed the previous chapters or use the Chapter10
folder as a starting point.
You can find the source code for this chapter’s end result at https://github.com/PacktPublishing/Web-Development-with-Blazor-Second-Edition/tree/main/Chapter11.
If you are jumping into this chapter using the code from GitHub, make sure you have added Auth0 account information in the settings files. You can find the instructions in Chapter 8, Authentication and Authorization.
There are many different ways in which to store data on the server side. The only thing to remember is that Blazor WebAssembly will always need an API. Blazor Server doesn’t need an API since we can access the server-side resources directly.
I have had discussions with many developers regarding APIs or direct access, which all boils down to what you intend to do with the application. If you are building a Blazor Server application and have no interest in moving to Blazor WebAssembly, I would probably go for direct access, as we have done in the MyBlog project.
I would not do direct database queries in the components, though. I would keep it in an API, just not a Web API. As we have seen, exposing those API functions in an API, as we did in Chapter 7, Creating an API, does not require a lot of steps. We can always start with direct server access and move to an API if we want to.
When it comes to storing data, we can save it in blob storage, key-value storage, a relational database, a document database, table storage, etc.
There is no end to the possibilities. If .NET can communicate with the technology, we will be able to use it.
At first glance, this option might sound horrific, but it’s not. Data, in this case, can be the blog post ID or the page number if we use paging. Typically, the things you want to save in the URL are things you want to be able to link to later on, such as blog posts, in our case.
To read a parameter from the URL, we use the following syntax:
@page "/post/{BlogPostId:int}"
The URL is post
followed by Id
of the post.
To find that particular route, BlogPostId
must be an integer; otherwise, the route won’t be found.
We also need a public
parameter with the same name:
[Parameter]
public int BlogPostId{ get; set; }
If we store data in the URL, we need to make sure to use the OnParametersSet
or OnParametersSetAsync
method;, otherwise, the data won’t get reloaded if we change the parameter. If the parameter changes, Blazor won’t run OnInitializedAsync
again.
This is why our post.razor
component loads the things that change based on the parameter in the URL in OnParametersSet
, and load the things that are not affected by the parameter in OnInitializedAsync
.
We can use optional parameters by specifying them as nullable like this:
@page "/post/{BlogPostId:int?}"
When we specify what type the parameter should be, this is called a route constraint. We add a constraint so the match will only happen if the parameter value can be converted into the type we specified.
The following constraints are available:
bool
datetime
decimal
float
guid
int
long
The URL elements will be converted to a C# object. Therefore, it’s important to use an invariant culture when adding them to a URL.
So far, we have only talked about routes that are specified in the page
directive, but we can also read data from the query string.
NavigationManager
gives us access to the URI, so by using this code, we can access the query string parameters:
@inject NavigationManager Navigation
@code{
var query = new Uri(Navigation.Uri).Query;
}
We won’t dig deeper into this, but now we know that it is possible to access query string parameters if we need to.
We can also access the query parameter using an attribute like this:
[Parameter, SupplyParameterFromQuery(Name = "parameterName")] public string ParameterFromQuery { get; set; }
This syntax is a bit nicer to work with.
Some scenarios might not be as common, but I didn’t want to leave them out of the book completely since I have used them in some of my implementations. I want to mention them in case you might run into the same requirements as I did.
By default, Blazor will assume that a URL that contains a dot is a file and will try and serve the user a file (and will probably not find one if we are trying to match a route).
By adding the following in Startup.cs
to the Blazor WebAssembly server project (a server-hosted WebAssembly project), the server will redirect the request to the index.html
file:
app.MapFallbackToFile("/example/{param?}", "index.html");
If the URL is example/some.thing
, it will redirect the request to the Blazor WebAssembly entry point, and the Blazor routes will take care of it. Without it, the server would say file not found.
The routing, including a dot in the URL, will work, and to do the same, we would need to add the following to Startup.cs
in our Blazor Server project:
app.MapFallbackToPage("/example/{param?}", "/_Host");
We are doing the same thing here, but instead of redirecting to index.html
, we are redirecting to _Host
, which is the entry point for Blazor Server.
The other scenario that is not that common is handling routes that catch everything. Simply put, we are catching a URL that has multiple folder boundaries, but we are catching them as one parameter:
@page "/catch-all/{*pageRoute}"
@code {
[Parameter]
public string PageRoute{ get; set; }
}
The preceding code will catch "/catch-all/OMG/Racoons/are/awesome"
and the pageRoute
parameter will contain "OMG/Racoons/are/awesome"
. This is what we are using for our MyBlog
project, because if we change the implementation to use RavenDB, for example, the default behavior is that the Id
is collectionname/Id
. This would be interpreted as a path, not a value, and would not hit our route unless we use the *
in the route.
I used both techniques when I created my own blog in order to be able to keep the old URLs and make them work even though everything else (including the URLs) had been rewritten.
Having data in the URL is not really storing the data. If we navigate to another page, we need to make sure to include the new URL; otherwise, it would be lost. We can use the browser storage instead if we want to store data that we don’t need to include every time in the URL.
The browser has a bunch of different ways of storing data in the web browser. They are handled differently depending on what type we use. Local storage is scoped to the user’s browser window. The data will still be saved if the user reloads the page or even closes the web browser.
The data is also shared across tabs. Session storage is scoped to the Browser tab; if you reload the tab, the data will be saved, but if you close the tab, the data will be lost. SessionsStorage
is, in a way, safer to use because we avoid risks with bugs that may occur due to multiple tabs manipulating the same values in storage.
To be able to access the browser storage, we need to use JavaScript. Luckily, we won’t need to write the code ourselves.
In .NET 5, Microsoft introduced Protected Browser Storage, which uses data protection in ASP.NET Core and is not available in WebAssembly. We can, however, use an open-source library called Blazored.LocalStorage
, which can be used by both Blazor Server and Blazor WebAssembly.
But we are here to learn new things, right?
So, let’s implement an interface so that we can use both versions in our app, depending on which hosting model we are using.
First, we need an interface that can read and write to storage:
Interfaces
.IBrowserStorage.cs
.namespace Components.Interfaces;
public interface IBrowserStorage
{
Task<T?> GetAsync<T>(string key);
Task SetAsync(string key, object value);
Task DeleteAsync(string key);
}
Now we have an interface containing get
, set
, and delete
methods.
For Blazor Server, we will use protected browser storage:
Services
.BlogProtectedBrowserStorage.cs
.
(I realize the naming is overkill, but it will be easier to tell the Blazor Server and the Blazor WebAssembly implementation apart because we will soon create another one.)
using
statements:
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Components.Interfaces;
public class BlogProtectedBrowserStorage : IBrowserStorage
{
ProtectedSessionStorage Storage { get; set; }
public BlogProtectedBrowserStorage(ProtectedSessionStorage storage)
{
Storage = storage;
}
public async Task DeleteAsync(string key)
{
await Storage.DeleteAsync(key);
}
public async Task<T?> GetAsync<T>(string key)
{
var value = await Storage.GetAsync<T>(key);
if (value.Success)
{
return value.Value;
}
else
{
return default(T);
}
}
public async Task SetAsync(string key, object value)
{
await Storage.SetAsync(key, value);
}
}
The BlogProtectedBrowserStorage
class implements the IBrowserStorage
interface for protected browser storage. We inject a ProtectedSessionStorage
instance and implement the set
, get
, and delete
methods.
Program.cs
, add the following namespaces:
using Components.Interfaces;
using BlazorServer.Services;
builder.Services.AddScoped<IBrowserStorage,BlogProtectedBrowserStorage>();
We are configuring Blazor to return an instance of BlogProtectedBrowserStorage
when we inject IBrowserStorage
.
This is the same as we did with the API. We inject different implementations depending on the platform.
For Blazor WebAssembly, we will use Blazored.SessionStorage
:
Blazored.SessionStorage
.Services
.BlogBrowserStorage.cs
.using Blazored.SessionStorage;
using Components.Interfaces;
namespace BlazorWebAssembly.Client.Services;
public class BlogBrowserStorage : IBrowserStorage
{
ISessionStorageService Storage { get; set; }
public BlogBrowserStorage(ISessionStorageService storage)
{
Storage = storage;
}
public async Task DeleteAsync(string key)
{
await Storage.RemoveItemAsync(key);
}
public async Task<T?> GetAsync<T>(string key)
{
return await Storage.GetItemAsync<T>(key);
}
public async Task SetAsync(string key, object value)
{
await Storage.SetItemAsync(key, value);
}
}
The implementations of ProtectedBrowserStorage
and Blazored.SessionStorage
are pretty similar to one another. The names of the methods are different, but the parameters are the same.
Program.cs
file, add the following namespaces:
using Blazored.SessionStorage;
using Components.Interfaces;
using BlazorWebAssembly.Client.Services;
await builder.Build().RunAsync();
:
builder.Services.AddBlazoredSessionStorage();
builder.Services.AddScoped<IBrowserStorage, BlogBrowserStorage>();
The AddBlazoredSessionStorage
extension method hooks up everything so that we can start using the browser session storage.
Then we add our configuration for IBrowserStorage
, just as we did with the server, but in this case, we return BlogBrowserStorage
when we ask the dependency injection for IBrowserStorage
.
We also need to implement some code that calls the services we just created:
Pages/Admin/BlogPostEdit.razor
. We are going to make a couple of changes to the file.IBrowserStorage
:
@inject Components.Interfaces.IBrowserStorage _storage
OnAfterRender
method, let’s create an OnAfterRenderMethod
:
override protected async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && string.IsNullOrEmpty(Id))
{
var saved = await _storage.GetAsync<BlogPost>("EditCurrentPost");
if (saved != null)
{
Post = saved;
StateHasChanged();
}
}
await base.OnAfterRenderAsync(firstRender);
}
When we load the component and Id
is null
, this means we are editing a new file and then we can check whether we have a file saved in browser storage.
This implementation can only have one file in the drafts and only saves new posts. If we were to edit an existing post, it would not save those changes.
Here is more information on handling protected browser storage with prerender:https://docs.microsoft.com/en-us/aspnet/core/blazor/state-management?view=aspnetcore-7.0&pivots=server.
UpdateHTML
method to become async. Change the method to look like this:
protected async Task UpdateHTMLAsync()
{
if (Post.Text != null)
{
markDownAsHTML = Markdig.Markdown.ToHtml(Post.Text, pipeline);
if (string.IsNullOrEmpty(Post.Id))
{
await _storage.SetAsync("EditCurrentPost", Post);
}
}
}
If Id
on the blog post is null
, we will store the post in the browser storage. Make sure to change all the references from UpdateHTML
to UpdateHTMLAsync
.
Make sure to await the call as well in the OnParametersSetAsync
method like this:
await UpdateHTMLAsync();
We are done. Now it’s time to test the implementation:
You should see one post with the key EditCurrentPost
, and the value of that post should be an encrypted string, as seen in Figure 11.1:
Figure 11.1: The encrypted protected browser storage
Let’s test Blazor WebAssembly next:
You should see one post with the key EditCurrentPost
, and the value of that post should be a JSON string, as seen in Figure 11.2.
If we were to change the data in the storage, it would also change in the application, so keep in mind that this is plain text, and the end user can manipulate the data:
Figure 11.2: Browser storage that is unprotected
Now we have implemented protected browser storage for Blazor Server and session storage for Blazor WebAssembly.
We only have one way left to go through, so let’s make it the most fun.
When it comes to in-memory state containers, we simply use dependency injection to keep the instance of the service in memory for the predetermined time (scoped, singleton, transient).
In Chapter 4, Understanding Basic Blazor Components, we discussed how the scope of dependency injections differs from Blazor Server and Blazor WebAssembly. The big difference for us in this section is the fact that Blazor WebAssembly runs inside the web browser and doesn’t have a connection to the server or other users.
To show how the in-memory state works, we will do something that might seem a bit overkill for a blog, but it will be cool to see. When we edit our blog post, we will update all the web browsers connected to our blog in real time (I did say overkill).
We will have to implement that a bit differently, depending on the host. Let’s start with Blazor Server.
The implementation for Blazor Server can also be used for Blazor WebAssembly. Since WebAssembly is running in our browser, it would only notify the users connected to the site, which would be just you. But it might be good to know that the same way works in Blazor Server as well as Blazor WebAssembly:
IBlogNotificationService.cs
.using Data.Models;
namespace Components.Interfaces;
public interface IBlogNotificationService
{
event Action<BlogPost>? BlogPostChanged;
Task SendNotification(BlogPost post);
}
We have an action that we can subscribe to when the blog post is updated and a method we can call when we update a post.
BlazorServerBlogNotificationService.cs
.
It might seem unnecessary to give the class a name that includes BlazorServer
, but it makes sure we can easily tell the classes apart.
Replace the content with the following code:
using Components.Interfaces;
using Data.Models;
namespace BlazorServer.Services;
public class BlazorServerBlogNotificationService : IBlogNotificationService
{
public event Action<BlogPost>? BlogPostChanged;
public Task SendNotification(BlogPost post)
{
BlogPostChanged?.Invoke(post);
return Task.CompletedTask;
}
}
The code is pretty simple here. If we call SendNotification
, it will check whether anyone is listening for the BlogPostChanged
action and whether to trigger the action.
Program.cs
, add the dependency injection:
builder.Services.AddSingleton<IBlogNotificationService, BlazorServerBlogNotificationService>();
Whenever we ask for an instance of the type IBlogNotificationService
, we will get back an instance of BlazorServerBlogNotificationService
.
We add this dependency injection as a Singleton
. I can’t stress this enough. When using Blazor Server, this will be the same instance for ALL users, so we must be careful when we use Singleton
.
In this case, we want the service to notify all the visitors of our blog that the blog post has changed.
Post.razor
.@using Components.Interfaces
@inject IBlogNotificationService notificationService
@implements IDisposable
We add dependency injection for IBlogNotificationService
and we also need to implement IDisposable
to avoid any memory leaks.
At the top of the OnInitializedAsync
method, add the following:
notificationService.BlogPostChanged += PostChanged;
We added a listener to the event so we know when we should update the information.
PostChanged
method, so add this code:
private async void PostChanged(BlogPost post)
{
if (BlogPost?.Id == post.Id)
{
BlogPost = post;
await InvokeAsync(()=>this.StateHasChanged());
}
}
If the parameter has the same ID as the post we are currently viewing, then replace the content with the post in the event and call StateHasChanged
.
Since this is happening on another thread, we need to call StateHasChanged
using InvokeAsync
so that it runs on the UI thread.
The last thing in this component is to stop listening to the updates by implementing the Dispose
method. Add the following:
void IDisposable.Dispose()
{
notificationService.BlogPostChanged -= PostChanged;
}
We remove the event listener to avoid any memory leaks.
Pages/Admin/BlogPostEdit.Razor
file.@using Components.Interfaces
@inject IBlogNotificationService notificationService
We add a namespace and inject our notification service.
UpdateHTMLAsync
method, add the following just under the Post.Text!=null if
statement:
await notificationService.SendNotification(Post);
Every time we change something, it will now send a notification that the blog post changed. I do realize that it would make more sense to do this when we save a post, but it makes for a much cooler demo.
In the first window, open a blog post (doesn’t matter which one), and in the second window, log in and edit the same blog post.
When we change the text of the blog post in the second window, the change should be reflected in real time in the first window.
I am constantly amazed how a feature that would be a bit tricky to implement without using Blazor only requires 10 steps (not counting the test), and if we didn’t prepare for the next step, it would take even fewer steps.
Next, we will implement the same feature for Blazor WebAssembly, but Blazor WebAssembly runs inside the user’s web browser. There is no real-time communication built in, as with Blazor Server.
We already have a lot of things in place. We only need to add a real-time messaging system. Since SignalR is both easy to implement and awesome, let’s use that.
The first time I used SignalR, my first thought was, wait, it can’t be that easy. I must have forgotten something, or something must be missing. Hopefully, we will have the same experience now.
Let’s see whether that still holds true today:
Hubs
.BlogNotificationHub.cs
.using Data.Models;
using Microsoft.AspNetCore.SignalR;
namespace BlazorWebAssembly.Server.Hubs;
public class BlogNotificationHub : Hub
{
public async Task SendNotification(BlogPost post)
{
await Clients.All.SendAsync("BlogPostChanged", post);
}
}
The class inherits from the Hub
class. There is a method called SendNotification
. Keep that name in mind; we will come back to that.
We call Clients.All.SendAsync
, which means we will send a message called BlogPostChanged
with the content of a blog post.
The name BlogPostChanged
is also important, so keep that in mind as well.
Program.cs
file add the following:
builder.Services.AddSignalR();
This adds SignalR.
using BlazorWebAssembly.Server.Hubs;
app.MapFallbackToFile("index.html")
, add:
app.MapHub<BlogNotificationHub>("/BlogNotificationHub");
Here, we configure what URL BlogNotificationHub
should use. In this case, we are using the same URL as the name of the hub.
The URL here is also important. We will use that in just a bit.
BlazorWebAssembly.Client
add a reference to the NuGet package Microsoft.AspNetCore.SignalR.Client
.BlazorWebAssemblyBlogNotificationService.cs
.
In this file, we will implement the SignalR communication.
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.SignalR.Client;
using Data.Models;
using Components.Interfaces;
public class BlazorWebAssemblyBlogNotificationService : IBlogNotificationService, IAsyncDisposable
{
public BlazorWebAssemblyBlogNotificationService(NavigationManager navigationManager)
{
_hubConnection = new HubConnectionBuilder()
.WithUrl(navigationManager.ToAbsoluteUri("/BlogNotificationHub"))
.Build();
_hubConnection.On<BlogPost>("BlogPostChanged", (post) =>
{
BlogPostChanged?.Invoke(post);
});
_hubConnection.StartAsync();
}
private readonly HubConnection _hubConnection;
public event Action<BlogPost>? BlogPostChanged;
public async Task SendNotification(BlogPost post)
{
await _hubConnection.SendAsync("SendNotification", post);
}
public async ValueTask DisposeAsync()
{
await _hubConnection.DisposeAsync();
}
}
A lot is happening here. The class is implementing IBlogNotificationService
and IAsyncDisposable
.
In the constructor, we use dependency injection to get NavigationManager
, so we can figure out the URL to the server.
Then we configure the connection to the hub. Then we specify the URL to the hub; this should be the same as we specified in step 7.
Now we can configure the hub connection to listen for events. In this case, we listen for the BlogPostChanged
event, the same name we specified in step 3. When someone sends the event, the method we specify will run.
The method, in this case, triggers the event we have in IBlogNotificationService
. Then we start the connection. Since the constructor can’t be async, we won’t await the StartAsync
method.
IBlogNotificationService
also implements the SendNotification
method, and we trigger the event with the same name on the hub, which will result in the hub sending the BlogPostChanged
event to all connected clients.
The last thing we do is make sure that we dispose of the hub connection.
Program.cs
file, we need to configure dependency injection. Just above await builder.Build().RunAsync();
, add the following:
builder.Services.AddSingleton<IBlogNotificationService, BlazorWebAssemblyBlogNotificationService>();
Set the BlazorWebAssembly.Server project as Startup Project and run the project by pressing Ctrl + F5.
In the first window, open a blog post (it doesn’t matter which one), and in the second window, log in and edit the same blog post.
When we change the text of the blog post in the second window, the change should be reflected in real time in the first window.
In 11 steps (not counting testing), we have implemented real-time communication between the server and client, a Blazor WebAssembly client with .NET code running inside the web browser.
And no JavaScript!
In this chapter, we learned how we can handle state in our application and how we can use local storage to store data, both encrypted and not. We looked at different ways of doing that, and we also made sure to include SignalR to be able to use real-time communication with the server.
Almost all applications need to save data in some form. Perhaps it can be settings or preferences. The things we covered in the chapter are the most common ones, but we should also know that there are many open-source projects we can use to persist state. We could save the information using IndexedDB.
In the next chapter, we will take a look at debugging. Hopefully, you won’t have needed to know how to debug yet!