DEV Community

Chris Key
Chris Key

Posted on

Building an alert service in Blazor (Client-side)

I've been mostly playing with AngularJS and Angular for the last few years at Packt as a dev manager, but now I'm moving to a new role where they've chosen to build a platform with Blazor.

I'm totally brand new to Blazor, and haven't used .Net since 4.0 came out (original framework, Core didn't exist then). I'm going to write up some of my discoveries as part of this migration to be used as a reference for anyone else starting out.

Why an Alert Service?

All the tutorials I've seen so far are fairly static, with minimal interaction between pages and components (hint, pages are components). A UI often needs a way of letting a user know something has happened. It may be directly related to the component they are interacting with, in which case popping up a message is pretty easy, but doing it for every component can mean being repetitive. A background service may need to let a user know something has happened, or a component that is going out of focus. In this case, as a component out of focus can't display anything, nor can a service, we need something else to manage messages.

My requirements were pretty simple - ability to show at any time, in a range of colours (following Bootstrap) and automatically disappear after some seconds.

I originally looked at a component in the MainLayout that was accessible down the stack through the use of CascadingValue but it got messy around the redraw and having events for making messages disappear.

The code structure

= Shared
  = Interfaces
      IAlert.cs
  = Classes
      Alert.cs
  = Enums
      Alerts.cs
  = Services
      AlertService.cs
  MainLayout.razor
= Components
    AlertComponent.razor
= wwwroot
  = css
      alert-service.css
  index.html
Program.cs
Enter fullscreen mode Exit fullscreen mode

The Code Files

We have an interface to define the type for the alerts being passed around, not absolutely necessary but useful for mocking tests later on:

public interface IAlert {
    string message { get; set; }
    Alerts alert { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

A class is then used to actually hold the data. Coming from TypeScript, this is unnecessary as anonymous classes do the job perfectly well.

public class Alert : IAlert
{
    public Alert() {}
    public Alert(string message, Alerts alert) {
        this.message = message;
        this.alert = alert;
    }

    public string message { get; set; }
    public Alerts alert { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

The enum is used to hold all the different types of alert: Primary, Secondary, Danger, Warning, Info etc.

public enum Alerts {
    Primary,
    Secondary,
    Success,
    Info,
    Danger,
    Warning,
    Dark,
    Light
}
Enter fullscreen mode Exit fullscreen mode

The AlertService defines our actual code:

using System;
using System.Collections.Generic;

public class AlertService {
    public List<IAlert> messages { get; private set; }
    public event Action RefreshRequested;

    public AlertService() {
        this.messages = new List<IAlert>();
    }

    public void addMessage(Alert alert) {
        this.messages.Add(alert);
        System.Console.WriteLine("Message count: {0}", this.messages.Count);
        RefreshRequested?.Invoke();

        // pop message off after a delay
        new System.Threading.Timer((_) => {
            this.messages.RemoveAt(0);
            RefreshRequested?.Invoke();
        }, null, 8000, System.Threading.Timeout.Infinite);
    }
}
Enter fullscreen mode Exit fullscreen mode

There is a private property to hold the messages in a list, and a public method to add to the list.

There is also an event setup for when data changes. Then anything that wants to use the data from the service can be notified when changes occur. This event is referenced twice in theaddMessage method - once when the alert is added to the list, and once when it is removed.

You can see we have a timer set to 8 seconds to remove the top item in the list. If for some reason the list is empty, this could potentially cause problems - ideally some defences should be added here.


The AlertComponent has the physical layout and rendering method:

@inject AlertService AlertService

<div class="alert-service">
    @for (int c = 0 ; c < AlertService.messages.Count ; c++)
    {
        <span class="alert @getAlertClass(AlertService.messages[c])">@AlertService.messages[c].message</span>
    }
</div>

@code {
    protected override void OnInitialized() {
        Console.WriteLine("AlertComponent:Initializied");
        AlertService.RefreshRequested += Refresh;
    }

    private String getAlertClass(IAlert alert) {
        return String.Format("alert-{0}", alert.alert.ToString().ToLower());
    }

    private void Refresh() {
        Console.WriteLine("AlertComponent:refreshing");
        StateHasChanged();
    }
}
Enter fullscreen mode Exit fullscreen mode

Firstly, we inject the AlertService into the component. There are changes in the Program.cs below that make this possible.

We then have some HTML with a razor iterator to build the messages. This is slightly inefficient as every update will recreate all the DOM elements, rather than adding and removing as necessary. This would be much worse if we had a more complex structure, but as it is, it isn't completely terrible. I did try using a BlazorStrap Alert but it had issues with reading data out of sync (no idea why yet).

The code is mostly just helpers. When the component is initialised, the AlertService RefreshRequested event is subscribed and wired to the Refresh() method.

The getAlertClass() method performs the required manipulation to build the CSS class from the Alert object containing the enum.


We put our component into MainLayout with simply:

<AlertComponent />
Enter fullscreen mode Exit fullscreen mode

In alert-service.css we have some fairly simple CSS:

.alert-service {
    position: absolute;
    top: 4rem;
    right: 2rem;
}

.alert-service span {
    display: block;
    width: 20rem;
    line-height: 2.2rem;
    text-align: right;
    padding: 0.8rem;
    margin: 0.4em;
}


.alert {
    color: #fff;
    border-style: solid;
    border-width: 0 1px 4px 1px
}

.alert-primary {
    background-color: #158CBA;
    border-color: #127ba3
}

...snip...
Enter fullscreen mode Exit fullscreen mode

Finally, in Program.cs we have to register our service to make it available to components. We add the following line next to where the App is registered (shown together):

builder.RootComponents.Add<App>("app");
builder.Services.AddSingleton<AlertService>();
Enter fullscreen mode Exit fullscreen mode

Now, we can use the AlertService in any component.

For example, I added it to the default Counter page/component:

@page "/counter"
@inject AlertService AlertService
<PageTitle Title="Counter" @ref="PageTitle" />

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private PageTitle PageTitle;
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
        this.PageTitle.Title = String.Format("Counter: {0}", currentCount);
        Console.WriteLine("Incrementing");
        AlertService.addMessage(new Alert(currentCount.ToString(), Alerts.Info));
    }
}
Enter fullscreen mode Exit fullscreen mode

You can see we inject the AlertService at the top, as in the AlertComponent. We can then reference it in the IncrementCount() method based on the definition in the injection. We add our message, and it shows on screen for 8 secs.

Future changes for this setup would allow for persistent, manually dismissable messages, and to ensure HTML safety in the messages displayed to ensure no XSS capability.

PageTitle is another problem I solved....

Top comments (2)

Collapse
 
icodeintx profile image
Scott

I'm curious about the AddSingleton lifecycle of the service. If a message is added to the service is that message displayed to all users who are logged in? I'm looking to add a simple alert to my Blazor (server) and I like this setup.

Collapse
 
euankirkhope profile image
EuanK
private async void Refresh()
    {
        Console.WriteLine("AlertComponent:refreshing");
        await InvokeAsync(StateHasChanged);
    }
Enter fullscreen mode Exit fullscreen mode


`