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).

Unleash Blazor's Potential

Blazor promises to make it much easier (and faster) to build modern, responsive web applications, using the tools you already know and understand.

Subscribe to my Practical ASP.NET Blazor newsletter and get instant access to the vault.

In there you'll find step-by-step tutorials, source code and videos to help you get up and running using Blazor's component model.

I respect your email privacy. Unsubscribe with one click.

    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.

    Unleash Blazor's Potential

    Blazor promises to make it much easier (and faster) to build modern, responsive web applications, using the tools you already know and understand.

    Subscribe to my Practical ASP.NET Blazor newsletter and get instant access to the vault.

    In there you'll find step-by-step tutorials, source code and videos to help you get up and running using Blazor's component model.

    I respect your email privacy. Unsubscribe with one click.

      Next up

      Finally! Improved Blazor Server reconnection UX
      .NET 9 changes how your Blazor Server app behaves when server connection is lost
      .NET 9 improves JavaScript module importing for Blazor
      .NET 9 ensures your users always get the latest version of your JS modules
      How to use .NET 9 to ensure users always get the latest version of your stylesheets
      .NET 9 changes how static files are served, and it solves a long-standing problem