Blazor Series- Managing State with Blazor SSR Interactive Components

Managing State with Blazor SSR Interactive Components

Share This Article
This entry is part 2 of 2 in the series MudBlazor

So you just spun up a new project and used the new Blazor Template. You expect you will get some MudBlazor goodness working in all its glory. The page loads, and your anticipation heightens as the elements fill the screen. Then, you realize that the Menu is static, and the drawer never goes away. You quickly glance back at the layout, add a button, and tie it to the toggle method already present in the design. You reload the browser and take your mouse to the new button you created…

Nothing happens. The button does nothing at all when you click it. We forgot that this page was being rendered on the server, so changing the DOM is difficult. Don’t worry, though; in this article, we will cover how to bring back the interactive goodness of your MudDrawer. So let’s get started.

The Default Template

When you use the MudBlazor Blazor Template for .NET 8, it replaces the built-in components with Muddy Awesomeness. Aside from building out the Client and main projects, little has changed since .NET 7. It still builds your layout precisely as before, with a tiny exception. By default, It’s no longer interactive.

@inherits LayoutComponentBase

<MudThemingProvider/>
<MudLayout>
    <MudAppBar Elevation="1">
        <MudText Typo="Typo.h5" Class="ml-3">Application</MudText>
    </MudAppBar>
    <MudDrawer @bind-Open="_drawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2">
        <NavMenu/>
    </MudDrawer>
    <MudMainContent Class="mt-16 pa-4">
        @Body
    </MudMainContent>
</MudLayout>

@code {
    private bool _drawerOpen = true;

    private void DrawerToggle()
    {
        _drawerOpen = !_drawerOpen;
    }

}

To solve this problem, the MudAppBar is static and always open, but let’s make a small change to allow its state to be changed.

@inherits LayoutComponentBase

<MudThemingProvider/>
<MudLayout>
    <MudAppBar Elevation="1">
        <MudText Typo="Typo.h5" Class="ml-3">Application</MudText>
        <MudIconButton Icon="@Icons.Material.Filled.Menu" OnClick="ToggleMenu"></MudIconButton>
    </MudAppBar>
    <MudDrawer @bind-Open="_drawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2">
        <NavMenu/>
    </MudDrawer>
    <MudMainContent Class="mt-16 pa-4">
        @Body
    </MudMainContent>
</MudLayout>

@code {
    private bool _drawerOpen = true;

    private void DrawerToggle()
    {
        _drawerOpen = !_drawerOpen;
    }

    private void ToggleMenu()
    {
        DrawerToggle();
    }

}

Now we’re getting somewhere. However, when I click the menu button, nothing happens!

This layout is rendered on the server and does not directly support interactivity. We could move the Layout to be rendered on the client, but then nothing would use SSR, and that’s a waste. Let’s use reactive magic and client-side coding to rub some interactive funk on it!

Stateful Services in Blazor

We need a place to hold the value and decide whether our drawer should be opened. We could create a variable on the Layout itself, but there would be no way for that state to be saved between page loads, so it’s not really what we’re looking for.

Instead, let’s take a page from my Angular development days and create a stateful service. We’ll start by creating a new interface for our client project. In the days of yesteryear, I would not have made an interface, but it’s a new age, and we want to use the auto-render mode, so let’s go all out on this one. Create a new folder called Abstraction in our client project, and add the ILayoutService Interface:

namespace MyProject.Client.Abstraction;

public interface ILayoutService
{
    public IObservable<bool> DrawerOpen { get; }
    
    public void ToggleDrawer();
}

We’ll implement this interface with a concrete class in our client project. We’ll call this the UnifiedLayoutService and store it in a folder called Services:

using System.Reactive.Subjects;
using MyProject.Client.Abstraction;

namespace MyProject.Client.Services;

public class UnifiedLayoutService: ILayoutService
{
    private readonly BehaviorSubject<bool> _drawerOpen = new(false);

    public IObservable<bool> DrawerOpen => _drawerOpen;

    public void ToggleDrawer()
    {
        _drawerOpen.OnNext(!_drawerOpen.Value);
    }
}

You’re probably wondering why I created an interface and called this the Unified Layout Service. Those are fair questions.

When creating interactive components with the render mode set to auto, Blazor SSR will load them as Blazor Server components over a WebSocket the first time they are rendered. It will also download and cache the files and framework needed to use WASM on subsequent loads. Interactive WASM components that load data will typically use an API, whereas the Server components might not use the same API and might instead run logic directly in the application. If we’re building a small enough application, we might build the API into the SSR Application. Adding API Endpoints is quite trivial. Therefore, we created the interface to allow us to implement it differently depending on our render mode.

We can implement it once with a unified service in the Client code because we’re not doing anything different on the Server versus WASM code. You must register the service in the DI for both projects, but it can be the same service. In the future, if we want to change how it works for Server-renewed components, we need to refactor the name and create a new concretion for the main project. Then, we can change the service we register for the main project to use our new one.

Interactive Components

We will create two new components in our Client project and mark them for auto-render mode. First, let’s move the menu itself out to its component. We’ll use a render fragment to help make this work.

@using Estate.Client.Abstraction
@rendermode InteractiveAuto
@inject ILayoutService LayoutService

<MudDrawer @bind-Open="_drawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2">
    @ChildContent
</MudDrawer>

@code {
    [Parameter] public RenderFragment ChildContent { get; set; }
    
    private bool _drawerOpen = false;
    public bool DrawerOpen
    {
        get => _drawerOpen;
        set
        {
            if(_drawerOpen != value)
                LayoutService.ToggleDrawer();
            StateHasChanged();
        }
    }
    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        LayoutService.DrawerOpen.Subscribe(open =>
        {
            _drawerOpen = open;
            StateHasChanged();
        });
    }

}

This component injects an ILayoutService to help manage the state of the drawer. We’re subscribing to the DrawerOpen Observable that a BehaviorSubject backs. The BehaviorSubject is of type bool, which tracks the state of a boolean variable. An observable in an of it’s self can’t really do that, but the Behavior subject can. When a new subscriber attaches, it is immediately sent the current value in the BehaviorSubject. Each time the value changes, the BehaviorSubject will broadcast those changes to all subscribed listeners.

Now we need a way to change the value so the BehaviorSubject can tell the drawer to open. Let’s move our button into a component. We’ll call it the OldAppDrawerButton, and place it in the same directory.

@using Estate.Client.Abstraction
@rendermode InteractiveAuto
@inject ILayoutService LayoutService

<MudIconButton Icon="@Icons.Material.Filled.Menu" OnClick="ToggleMenu"></MudIconButton>

@code {

    private void ToggleMenu()
    {
        LayoutService.ToggleDrawer();
    }

}

Here, we will use the same service, and when the button is clicked, we’ll tell the service to update the AppDrawer by toggling it open or closed. On both components, we’ve set the @rendermode to InteractiveAuto, telling blazor to load it as a server component at first, so that it loads fast and then to use WASM on subsequent pages.

Now, let’s update our Layout to use these new components…

@inherits LayoutComponentBase

<MudThemingProvider/>
<MudLayout>
    <MudAppBar Elevation="1">
        <MudText Typo="Typo.h5" Class="ml-3">Application</MudText>
        <MudSpacer/>
        <OldAppDrawerButton />
    </MudAppBar>
    <OldAppDrawer>
        <NavMenu/>
    </OldAppDrawer>
    <MudMainContent Class="mt-16 pa-4">
        @Body
    </MudMainContent>
</MudLayout>

@code {
    
}

Notice that all the configuration happens in our new components so that we can clean out the code block for our layout. Let’s spin it up and see what happens.

One would expect that everything would work out fine, but instead we’re greeted with this message:

An unhandled exception occurred while processing the request.
InvalidOperationException: Cannot pass the parameter 'ChildContent' to component 'OldAppDrawer' with rendermode 'InteractiveAutoRenderMode'. This is because the parameter is of the delegate type 'Microsoft.AspNetCore.Components.RenderFragment', which is arbitrary code and cannot be serialized.

Well, that’s interesting. It turns out that you can not pass a static rendered component to an interactive component as a RenderFragment. To resolve this issue, we’ll need to move the code for the menu into an interactive component as well. We could keep it separate from the Drawer component, but there’s not really a need to do that, so, let’s go ahead and copy it into our OldAppDrawer component.

@using Estate.Client.Abstraction
@rendermode InteractiveAuto
@inject ILayoutService LayoutService

<MudDrawer @bind-Open="_drawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2">
    <MudNavMenu>
        <MudNavLink Href="" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home">Home</MudNavLink>
        <MudNavLink Href="counter" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Add">Counter</MudNavLink>
        <MudNavLink Href="weather" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">Weather</MudNavLink>
        <MudNavLink Href="auth" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">Auth Required</MudNavLink>

        <AuthorizeView>
            <Authorized>
                <MudNavLink Href="Account/Manage" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">@context.User.Identity?.Name</MudNavLink>
            </Authorized>
            <NotAuthorized>
                <MudNavLink Href="Account/Register" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">Register</MudNavLink>
                <MudNavLink Href="Account/Login" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">Login</MudNavLink>
            </NotAuthorized>
        </AuthorizeView>

    </MudNavMenu>
</MudDrawer>

@code {
    private bool _drawerOpen = false;
    public bool DrawerOpen
    {
        get => _drawerOpen;
        set
        {
            if(_drawerOpen != value)
                LayoutService.ToggleDrawer();
            StateHasChanged();
        }
    }
    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
		LayoutService.DrawerOpen.Subscribe(open =>
        {
            _drawerOpen = open;
            StateHasChanged();
        });
    }

}

That allowed us to drop the RenderFragment for ChildContent, and now we need to apply this change to our layout…

@inherits LayoutComponentBase

<MudThemingProvider/>
<MudLayout>
    <MudAppBar Elevation="1">
        <MudText Typo="Typo.h5" Class="ml-3">Application</MudText>
        <MudSpacer/>
        <OldAppDrawerButton />
    </MudAppBar>
    <OldAppDrawer/>
    <MudMainContent Class="mt-16 pa-4">
        @Body
    </MudMainContent>
</MudLayout>

@code {
    
}

Now, let’s reload the app and see our update menu system…

Conclusion

We could work on some styling, but our toggle works, which was the goal. Remember that we’ve also been able to glimpse a bigger truth about working at Blazor SSR. We must be mindful of the render context we’re using for interactivity. We can take advantage of the fact that our application runs server-side now and include the API in the same project. That’s handy for small applications, but we’ll still want to use separate APIs for larger projects, and Blazor SSR can handle that as well.

No matter how you approach getting data to your front end, you must ensure that your services are registered in both the main project and the client. Sometimes, you may want to use different services for the same interface.

Series Navigation<< Blazor SSR & MudFields: EditContext Issues

Leave a Reply

Your email address will not be published. Required fields are marked *