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
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; }
}
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; }
}
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
}
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);
}
}
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();
}
}
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 />
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...
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>();
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));
}
}
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)
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.
`