Blazor by Example - A dismissable banner
February 17, 2020 · 7 minute read · Tags: blazor
There are some components you end up building time and again, for all kinds of different web applications.
One of those is the humble banner, oft used to impart important (and, let’s face it, sometimes not so important) information to your users.
The requirement
Users will be shown a banner (perhaps it’s an advert, or some kind of notification about their account). Once they’re done reading, they can click a button to dismiss it (so they don’t have to keep seeing the same information over and over as they navigate the application).
How might we approach this requirement using Blazor?
Markup first
The banner itself is straightforward enough. In this case I’m using Bootstrap to get something up and running.
I’ll put this component in the Shared folder in my Blazor project, making it straightforward to reference from a shared layout page later.
Shared/Banner.razor
<div class="col-md-12 banner">
<button @onclick="@Dismiss" class="float-right btn">x</button>
A very important message
</div>
@code {
private void Dismiss()
{
}
}
Apart from the message itself, the other point of interest is the button, which will be used to dismiss the banner (we’ll wire that up in a moment).
Add a little css to wwwroot/css/site.css
.banner {
padding: 1em;
text-align: center;
vertical-align: center;
background-color: orangered;
color: white;
font-size: 1.2em;
}
.banner .btn {
color: white;
}
We’ll want this banner on every page so a shared layout seems like the logical place to render it.
The Blazor new project template ships with a shared MainLayout page, let’s render it there.
Shared/MainLayout.razor
@inherits LayoutComponentBase
<div class="sidebar">
<NavMenu/>
</div>
<div class="main">
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>
<div class="content px-4">
<Banner />
@Body
</div>
</div>
Give this a spin in the browser and you’ve got… a pretty ugly looking banner!
How very dismissive of you
Now what about that requirement to make the banner dismissable?
Well the simplest first step would probably be to have a boolean flag, and use that to control whether the banner is visible or not…
@if (_visible)
{
<div class="col-md-12 banner">
<button @onclick="@Dismiss" class="float-right btn">x</button>
A very important message
</div>
}
@code {
private bool _visible = true;
private void Dismiss()
{
_visible = false;
}
}
Try this now, hit the x
button and you’ll find the banner disappears.
Job done, everyone’s happy…
Except, if you try reloading the page, the pesky banner comes back!
Stateful
At this point, we need a way to persist this “dismissed” state for the user, so they won’t keep seeing the banner after they’ve dismissed it.
If we had a backend at this point we could figure out who the user is and persist this state to a database.
But, what if they’re an anonymous user, or we don’t want to have to track things like this in a database?
As with any application running in the browser, we have two other options, namely:
- Local Storage
- Session Storage
Local Storage is scoped to the user’s browser, meaning if they reload the page, or close and reopen the browser the state will still be there.
Session Storage is scoped to the user’s browser tab, meaning it will not be shared between tabs and will be lost if they close and reopen their browser.
In this case, Local Storage feels like the best fit, so that the user won’t keep seeing the banner across multiple browsing sessions.
To interact with LocalStorage via Blazor Server we need to employ a little (whisper it quietly) javascript interop.
We can use the existing javascript APIs to read/write from/to the browser’s local storage, but call them from Blazor.
Happily, we don’t have to write this ourselves if we use the extremely convenient Blazored/LocalStorage NuGet package.
Install via NuGet…
Install-Package Blazored.LocalStorage
Add it to ConfigureServices
in startup.cs…
public void ConfigureServices(IServiceCollection services)
{
services.AddBlazoredLocalStorage();
}
With that in place we’ll want to write to local storage when the user dimisses the banner, and read from local storage to determine if the banner has already been dismissed.
@inject Blazored.LocalStorage.ILocalStorageService localStorage
@if (_visible)
{
<div class="col-md-12 banner">
<button @onclick="@Dismiss" class="float-right btn">x</button>
A very important message
</div>
}
@code {
private bool _visible = false;
private async Task Dismiss()
{
_visible = false;
await localStorage.SetItemAsync("bannerDismissed", true);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
_visible = !await localStorage.GetItemAsync<bool>("bannerDismissed");
StateHasChanged();
}
}
Now this code might look OK, but it has a major flaw so read on to see why this code is bad (very, very bad)…
DO NOT COPY THE CODE ABOVE AND USE IT!
A few things stand out here.
First, I’ve defaulted _visible
to false, so the banner will not be shown until we’ve checked whether it should be. This avoids any ugly flashes of the banner before we can check if it’s been dismissed already.
Secondly, JS Interop can’t be executed earlier than during OnAfterRenderAsync
. This has to do with how the blazor component is being pre-rendered on the server. So we have to make local storage calls during or after OnAfterRenderAsync
.
Thirdly, because OnAfterRenderAsync
runs after the component has been rendered (the clue’s in the name) we have to force a re-render using StateHasChanged
.
That last point is the killer.
When we trigger StateHasChanged
we effectively tell Blazor it must re-render the component.
At which point it will hit OnAfterRenderAsync
again, which will check local storage again, and then call StateHasChanged
again, which will force another re-render…
We have an infinite loop…
Now, in exploring this I’m not sure if there is an official “correct” way to handle this, and it’s worth noting this JS Interop limitation applies to Blazor Server only (Blazor WASM won’t have the issue), but let’s take a stab at handling it.
The facts:
- We can’t check local storage any sooner than
OnAfterRenderAsync
- We need to manually call
StateHasChanged
after we intentionally change the state of_visible
- We only want to do this once, to get us in sync with local storage
That last point is the key, let’s see if we can express that inside OnAfterRenderAsync
.
private bool _visible = false;
private async Task Dismiss()
{
_visible = false;
await localStorage.SetItemAsync("bannerDismissed", true);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_visible = !await localStorage.GetItemAsync<bool>("bannerDismissed");
StateHasChanged();
}
}
When overloading OnAfterRenderAsync
we get access to a handy boolean indicating whether this is the first render of the component.
Essentially, every Blazor component is going to be rendered at least once, and then again as state changes (buttons are clicked etc).
This firstRender
boolean is handy if we need to do something once, but not every time the component re-renders.
In this case, the component will keep track of its own state (when the user clicks a button and we set the visible
to false), but on that first render we want to sync the component’s _visible
value with whatever we’ve stored in local storage (if anything).
Now, everything works and we won’t fall into an infinite loop, because we only call StateHasChanged
once.
So what do we think? Is there a simpler way to handle this integration with Local Storage? Hit me up on Twitter and let me know!
Just before you go, you can see the source for this example here.