Single Instance Overlay in Blazor

blazor asp.net core

Overall, Blazor server-side has been good for internal apps and prototyping. I tried to see how far I could get in a project without any third party component libraries but couldn’t do much. There are some good functional and official components like the data grid QuickGrid, but soon I found myself needing more components. I eventually switched to MudBlazor which has been pretty good at replicating what you might see in the vast wealth of JS front end libs.

I recently wanted to add an overlay to my app and conceptually to me, it was to be just something that there is only one of and is either “on” or “off”. Though the way the MudBlazor Overlay doc reads is that it’s just an element you add wherever needed on your DOM - meaning you add multiple of them across many components.

But with this being Blazor, the front end is all C#. That means you can tie your UI components in with a dependency injection scope. Using DI, I was able to manage state of a single overlay. So rather than adding overlay components all over, I simply had to call OverlayService.Show().

The setup is pretty simple:

1. Define an overlay service contract:

public interface IOverlayService
{
    Task<Overlay> Show();

    Task Hide();

    void RegisterOverlay(Overlay overlay);
}

2. …and an implementation

public sealed class SingleInstanceOverlayService : IOverlayService
{
    /// <summary>
    /// The single <see cref="Overlay"/> instance. Assign this in a component that lives in MainLayout during OnInitialized
    /// </summary>
    private Overlay Overlay { get; set; } = null!;

    public Task<Overlay> Show()
    {
        Overlay.Show();
        return Task.FromResult(Overlay);
    }

    public Task Hide()
    {
        Overlay.Close();
        return Task.CompletedTask;
    }

    public void RegisterOverlay(Overlay overlay)
    {
        Overlay = overlay;
    }
}

3. Make sure to register the service as a scoped dependency

The service should be injected as a with a scoped lifetime because we want it to persist during Blazor server-side websocket sessions. Singleton doesn’t work because there will be multiple users.

4. Define the Overlay UI component

The UI component should now be able to have a dependency on IOverlayService and will be tied to the scope DI scope of the request. In my case, I used the MudBlazor overlay but you can really use any custom component so long as it has an on/off state or equivalent.


@inject IOverlayService OverlayService

<MudOverlay @bind-Visible="Visible" DarkBackground AutoClose="false" OnClosed="Close"/>

@code {
    [Parameter] public bool Visible { get; set; }
    [Parameter] public EventCallback<bool> VisibleChanged { get; set; }

    protected override void OnInitialized()
    {
        OverlayService.RegisterOverlay(this); // This lets the overlay service have a reference to the Overlay
        base.OnInitialized();
    }

    public void Show()
    {
        Visible = true;
        VisibleChanged.InvokeAsync(Visible);
        InvokeAsync(StateHasChanged);
    }

    public void Close()
    {
        Visible = false;
        VisibleChanged.InvokeAsync(Visible);
        InvokeAsync(StateHasChanged);
    }

}

5. Add the Overlay component to the MainLayout

Now to add the Overlay to the “DOM”, you just reference that component in MainLayout. This is the one and only time you add it:

<MudLayout>
    ...
    <Overlay @bind-Visible="_overlayOpen"/>
</MudLayout>

@code {

    private bool _overlayOpen;
    ...

}

6. Show the overlay

To show the overlay, you can either directly call Show if you have a reference to the overlay component. But the more convenient method is to inject the same IOverlayService dependency in another component, and show it whenever needed.

In my case, I had a popover with an overlay background:

@inject IOverlayService OverlayService

<MudPopover>
...
</MudPopover>

@code {
    [Parameter] public bool IsOpen { get; set; }
    [Parameter] public EventCallback OnClose { get; set; }
    ...

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (IsOpen)
        {
            await OverlayService.Show();
            StateHasChanged();
        }

        await base.OnAfterRenderAsync(firstRender);
    }

    public async Task Open()
    {
        IsOpen = true;
        await OverlayService.Show();
        await InvokeAsync(StateHasChanged);
    }

    public async Task Close()
    {
        IsOpen = false;
        await OverlayService.Hide();
        await OnClose.InvokeAsync();
        await InvokeAsync(StateHasChanged);
    }

}

Now, whenever I need to show the Popover, I automatically get the Overlay as well.