11

Managing State – Part 2

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:

  • Storing data on the server side
  • Storing data in the URL
  • Implementing browser storage
  • Using an in-memory state container service

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.

Technical requirements

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.

Storing data on the server side

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.

Storing data in the URL

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?}"

Route constraints

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.

Using a query string

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.

Scenarios that are not that common

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.

Implementing browser storage

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.

Creating an interface

First, we need an interface that can read and write to storage:

  1. In the Components project, create a new folder called Interfaces.
  2. In the new folder, create a new class called IBrowserStorage.cs.
  3. Replace the content in the file with the following code:
    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.

Implementing Blazor Server

For Blazor Server, we will use protected browser storage:

  1. In the BlazorServer project, add a new folder called Services.
  2. In the new folder create a new class called 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.)

  1. Open the new file and add the following using statements:
    using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
    using Components.Interfaces;
    
  2. Replace the class with this one:
    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.

  1. In Program.cs, add the following namespaces:
    using Components.Interfaces;
    using BlazorServer.Services;
    
  2. Add the following:
    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.

Implementing WebAssembly

For Blazor WebAssembly, we will use Blazored.SessionStorage:

  1. In the BlazorWebAssembly.Client, add a NuGet reference to Blazored.SessionStorage.
  2. Add a new folder called Services.
  3. In the new folder, create a new class called BlogBrowserStorage.cs.
  4. Open the new file and replace the content with the following code:
    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.

  1. In the Program.cs file, add the following namespaces:
    using Blazored.SessionStorage;
    using Components.Interfaces;
    using BlazorWebAssembly.Client.Services;
    
  2. Add the following code just above 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.

Implementing the shared code

We also need to implement some code that calls the services we just created:

  1. In the Components project, open Pages/Admin/BlogPostEdit.razor. We are going to make a couple of changes to the file.
  2. Inject IBrowserStorage:
    @inject Components.Interfaces.IBrowserStorage _storage
    
  3. Since we can only run JavaScript calls when doing an action (like a click) or in the 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.

  1. We need our 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:

  1. Set the BlazorServer project as Startup Project, and run the project by pressing Ctrl + F5.
  2. Log in to the site (so we can access the admin tools).
  3. Click Blog posts followed by New blog post.
  4. Type anything in the boxes, and as soon as we type something in the text area, it will save the post to storage.
  5. Click Blog posts (so we navigate away from our blog post).
  6. Click New blog post and all the information will still be there.
  7. Press F12 to see the browser developer tools. Click Application | Session storage | https://localhost:portnumber.

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

Figure 11.1: The encrypted protected browser storage

Let’s test Blazor WebAssembly next:

  1. Set the BlazorWebAssembly.Server project as Startup Project and run the project by pressing Ctrl + F5.
  2. Log in to the site (so we can access the admin tools).
  3. Click Blog posts and then New blog post.
  4. Type anything in the boxes, and as soon as we type something in the text area, it will save the post to storage.
  5. Click Blog posts (so we navigate away from our blog post).
  6. Click New blog post and all the information should still be there.
  7. Press F12 to see the browser developer tools. Click Application | Session storage | https://localhost:portnumber.

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

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.

Using an in-memory state container service

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.

Implementing real-time updates on 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:

  1. In the Components project, in the Interfaces folder, create an interface called IBlogNotificationService.cs.
  2. Add the following code:
    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.

  1. In the BlazorServer project, in the Services folder, add a new class called 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.

  1. In 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.

  1. In the Components project, open Post.razor.
  2. Add the following code at the top (or close to the top) of the page:
    @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.

  1. We also need the 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.

  1. Open the Pages/Admin/BlogPostEdit.Razor file.
  2. When we make changes to our blog post, we need to send a notification as well. At the top of the file, add the following:
    @using Components.Interfaces
    @inject IBlogNotificationService notificationService
    

    We add a namespace and inject our notification service.

  1. In the 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.

  1. Set the BlazorServer project as Startup Project and run the project by pressing Ctrl + F5.
  2. Copy the URL and open another web browser. We should now have two web browser windows open showing us the blog.

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.

Implementing real-time updates on Blazor WebAssembly

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:

  1. In the BlazorWebAssembly.Server project, add a new folder called Hubs.
  2. In the new folder create a class called BlogNotificationHub.cs.
  3. Replace the code with the following:
    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.

  1. In the Program.cs file add the following:
    builder.Services.AddSignalR();
    

    This adds SignalR.

  1. Add the following namespace:
    using BlazorWebAssembly.Server.Hubs;
    
  2. Just above 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.

  1. In the BlazorWebAssembly.Client add a reference to the NuGet package Microsoft.AspNetCore.SignalR.Client.
  2. In the Services folder, create a class called BlazorWebAssemblyBlogNotificationService.cs.

    In this file, we will implement the SignalR communication.

  1. Add the following namespaces:
    using Microsoft.AspNetCore.Components;
    using Microsoft.AspNetCore.SignalR.Client;
    using Data.Models;
    using Components.Interfaces;
    
  2. Add this class:
    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.

  1. In the Program.cs file, we need to configure dependency injection. Just above await builder.Build().RunAsync();, add the following:
    builder.Services.AddSingleton<IBlogNotificationService, BlazorWebAssemblyBlogNotificationService>();
    
  2. Now, it’s time to carry out testing, and we do that the same way as for the Blazor Server project.

    Set the BlazorWebAssembly.Server project as Startup Project and run the project by pressing Ctrl + F5.

  1. Copy the URL and open another web browser. We should now have two web browser windows open showing us the blog.

    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!

Summary

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!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset