Last updated on April 16th, 2023
Estimated reading time: 13 minutes
Ever wonder how you can show user-friendly notification in Blazor when a record has been updated/saved or loading list of records on page load. In this article, you will learn to create fully functional toast notifications component for your Blazor web assembly application. By the end of this article, you’ll be able to show different toasts messages depending on the level of importance (information, success, warning, and error). All the coding would only be done in c# without using JavaScript code.
Note: Basic working knowledge of Blazor web assembly and .NET Core is required for this working demo. If you are new to Blazor read article on creating a fully functional application in Blazor web assembly with Add/Edit/Delete operations.
Output
When home page is loaded all employees list would be displayed. “Info toast” notification would be showed when all the employees have been loaded.
Success notification would be displayed when employee information is updated or if new employee has been added.
If record update operation fails Toast level Error could be displayed
Blazor Project Structure
Employee.Client project
This is a Blazor Web Assembly Project and all the client-side code will reside in this project.
It would include the below items,
- Blazor components
- Razor pages
- Client-side css libraries and Master Layout
- Services (Services are used to call .NET Core API)
Blazor components : All the Blazor Components for this project would reside in this folder. Each Blazor component will have a Razor File to define component UI and Program.cs file to write program logic.
Razor pages: These are razor pages.
EmployeePortal.Server
This is the ASP.NET Core project. All required APIs and repositories will reside in this project.
It will also contain all the necessary configurations to connect to the SQL server.
EmployeePortal.Shared
This project will include data models and it will be shared by our client and server projects.
Employee Razor Page
@page "/"
@using EmployeePortal.Client.Components;
<h1 class="page-title">All employees</h1>
@if (Employees == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Employee ID</th>
<th>First name</th>
<th>Last name</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var employee in Employees)
{
<tr>
<td>@employee.EmployeeId</td>
<td>@employee.FirstName</td>
<td>@employee.LastName</td>
<td>
<a href="@($"detail/{employee.EmployeeId}")" class="btn btn-primary-details table-btn">
<i class="fas fa-info-circle"></i>
Details
</a>
<a Edit href="@($"edit/{employee.EmployeeId}")" class="btn btn-primary-edit table-btn">
<i class="fas fa-edit"></i>
Edit
</a>
</td>
</tr>
}
</tbody>
</table>
}
<button @onclick="QuickAddEmployee" class="btn btn-dark table-btn quick-add-btn">Â Â +Â Â </button>
<a href="@($"api/employee/export")" class="btn btn-primary-details table-btn">
<i class="fas fa-info-circle"></i>
Export
</a>
<AddEmployeeDialog @ref="AddEmployeeDialog" CloseEventCallback="@AddEmployeeDialog_OnDialogClose"></AddEmployeeDialog>
Employee Page.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EmployeePortal.Shared;
using EmployeePortal.Client.Services;
using Microsoft.AspNetCore.Components;
using EmployeePortal.Client.Components;
using ResourceDemandOpens.Client.Services;
namespace EmployeePortal.Client.Pages
{
public partial class EmployeePage:ComponentBase
{
public IEnumerable<EmployeePortal.Shared.Employee> Employees { get; set; }
[Inject]
public IEmployeeDataService EmployeeDataService { get; set; }
[Inject]
public ToastService ToastService { get; set; }
public AddEmployeeDialog AddEmployeeDialog { get; set; }
protected async override Task OnInitializedAsync()
{
Employees = (await EmployeeDataService.GetAllEmployees()).ToList();
if (Employees != null)
{
ToastService.ShowToast("Employee data loaded", ToastLevel.Info);
}
}
protected void QuickAddEmployee()
{
AddEmployeeDialog.Show();
}
public async void AddEmployeeDialog_OnDialogClose()
{
Employees = (await EmployeeDataService.GetAllEmployees()).ToList();
StateHasChanged();
}
}
}
Edit.razor
@page "/edit/{EmployeeId}"
@using EmployeePortal.Shared;
@if (Employee == null)
{
<p><em>Loading...</em></p>
}
else
{
@if (!Saved)
{
<section class="employee-edit">
<h1 class="page-title">Details for @Employee.FirstName @Employee.LastName</h1>
<EditForm Model="@Employee" OnValidSubmit="@HandleValidSubmit"
OnInvalidSubmit="@HandleInvalidSubmit">
<DataAnnotationsValidator />
<ValidationSummary></ValidationSummary>
<div class="form-group row">
<label for="firstName" class="col-sm-3">First name: </label>
<InputText id="firstName" class="form-control col-sm-8" @bind-Value="@Employee.FirstName" placeholder="Enter first name"></InputText>
<ValidationMessage class="offset-sm-3 col-sm-8" For="@(() => Employee.FirstName)" />
</div>
<div class="form-group row">
<label for="lastName" class="col-sm-3">Last name: </label>
<InputText id="lastName" class="form-control col-sm-8" @bind-Value="@Employee.LastName" placeholder="Enter last name"></InputText>
<ValidationMessage class="offset-sm-3 col-sm-8" For="@(() => Employee.LastName)" />
</div>
<div class="form-group row">
<label for="email" class="col-sm-3">Email: </label>
<InputText id="email" class="form-control col-sm-8" @bind-Value="@Employee.Email" placeholder="Enter email"></InputText>
<ValidationMessage class="offset-sm-3 col-sm-8" For="@(() => Employee.Email)" />
</div>
<div class="form-group row">
<label for="street" class="col-sm-3">Street: </label>
<InputText id="street" class="form-control col-sm-8" @bind-Value="@Employee.Street" placeholder="Enter street"></InputText>
</div>
<div class="form-group row">
<label for="zip" class="col-sm-3">Zip code: </label>
<InputText id="zip" class="form-control col-sm-8" @bind-Value="@Employee.Zip" placeholder="Enter zip code"></InputText>
</div>
<div class="form-group row">
<label for="city" class="col-sm-3">City: </label>
<InputText id="city" class="form-control col-sm-8" @bind-Value="@Employee.City" placeholder="Enter city"></InputText>
</div>
<div class="form-group row">
<label for="phonenumber" class="col-sm-3">Phone number: </label>
<InputText id="phonenumber" class="form-control col-sm-8" @bind-Value="@Employee.PhoneNumber" placeholder="Enter phone number"></InputText>
</div>
<div class="form-group row">
<label for="comment" class="col-sm-3">Comment: </label>
<InputTextArea id="comment" class="form-control col-sm-8" @bind-Value="@Employee.Comment" placeholder="Enter comment"></InputTextArea>
<ValidationMessage class="offset-sm-3 col-sm-8" For="@(() => Employee.Comment)" />
</div>
<button type="submit" class="btn btn-primary edit-btn">Save employee</button>
<a class="btn btn-danger" @onclick="@DeleteEmployee">
Delete
</a>
<a class="btn btn-outline-primary" @onclick="@NavigateToOverview">Back to overview</a>
</EditForm>
</section>
}
else
{
<div class="alert @StatusClass">@Message</div>
}
}
Edit.cs
using System.Linq;
using System.Threading.Tasks;
using EmployeePortal.Shared;
using EmployeePortal.Client.Services;
using Microsoft.AspNetCore.Components;
using EmployeePortal.Client.Components;
using ResourceDemandOpens.Client.Services;
namespace EmployeePortal.Client.Pages
{
public partial class Edit
{
[Inject]
public IEmployeeDataService EmployeeDataService { get; set; }
[Parameter]
public string EmployeeId { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
[Inject]
public ToastService ToastService { get; set; }
public EmployeePortal.Shared.Employee Employee { get; set; }
//used to store state of screen
protected string Message = string.Empty;
protected string StatusClass = string.Empty;
protected bool Saved;
protected override async Task OnInitializedAsync()
{
Saved = false;
int.TryParse(EmployeeId, out var employeeId);
if (employeeId == 0) //new employee is being created
{
Employee = new Employee {};
}
else
{
Employee = await EmployeeDataService.GetEmployeeDetails(int.Parse(EmployeeId));
}
}
protected async Task HandleValidSubmit()
{
Saved = false;
if (Employee.EmployeeId == 0)
{
var addedEmployee = await EmployeeDataService.AddEmployee(Employee);
if (addedEmployee != null)
{
//StatusClass = "alert-success";
//Message = "New employee added successfully.";
Saved = true;
ToastService.ShowToast("Employee addded Successfully", ToastLevel.Success);
}
else
{
//StatusClass = "alert-danger";
//Message = "Something went wrong adding the new employee. Please try again.";
Saved = false;
ToastService.ShowToast("Something went wrong adding the new employee. Please try again", ToastLevel.Error);
}
}
else
{
ToastService.ShowToast("Employee updated Successfully", ToastLevel.Success);
await EmployeeDataService.UpdateEmployee(Employee);
//StatusClass = "alert-success";
//Message = "Employee updated successfully.";
Saved = true;
}
}
protected void HandleInvalidSubmit()
{
StatusClass = "alert-danger";
Message = "There are some validation errors. Please try again.";
}
protected async Task DeleteEmployee()
{
await EmployeeDataService.DeleteEmployee(Employee.EmployeeId);
StatusClass = "alert-success";
Message = "Deleted successfully";
Saved = true;
}
protected void NavigateToOverview()
{
NavigationManager.NavigateTo("/");
}
}
}
Creating Toast notification service and registering the service
We would first build the required Toast Service.
The first thing is to create an enum called ToastLevel inside the services folder.
public enum ToastLevel {
Info,
Success,
Warning,
Error
}
There are 4 different types of toast.
As per requirement, we can call the required toast level.
Now we will create a new class called ToastService.
The toast service will bind any component that issues a toast with a toast component that displays the toast.
The ShowToast method takes the message that needs to be displayed along with the level of toast as parameters.
The service has two events, OnShow and OnHide, and a timer, Countdown. Toast component will subscribe to the events and use them to show and hide. The timer is used internally by the service and is set at 5 seconds. When it elapses it invokes the OnHide event,
public class ToastService: IDisposable {
public event Action < string, ToastLevel > OnShow;
public event Action OnHide;
private Timer Countdown;
public void ShowToast(string message, ToastLevel level) {
OnShow?.Invoke(message, level);
StartCountdown();
}
private void StartCountdown() {
SetCountdown();
if (Countdown.Enabled) {
Countdown.Stop();
Countdown.Start();
} else {
Countdown.Start();
}
}
private void SetCountdown() {
if (Countdown == null) {
Countdown = new System.Timers.Timer(10000);
Countdown.Elapsed += HideToast;
Countdown.AutoReset = false;
}
}
private void HideToast(object source, ElapsedEventArgs args) {
OnHide?.Invoke();
}
public void Dispose() {
Countdown?.Dispose();
}
}
Building Notification Toast component
The toast component will work with the service to display toast on the screen or in UI.
Base class for logic – Toast.razor.cs .Inherit it from the ComponentBase class so Blazor knows that this is a component.
public partial class ToastBase: ComponentBase, IDisposable {
[Inject] ToastService ToastService {
get;
set;
}
protected string Heading {
get;
set;
}
protected string Message {
get;
set;
}
protected bool IsVisible {
get;
set;
}
protected string BackgroundCssClass {
get;
set;
}
protected string IconCssClass {
get;
set;
}
protected override void OnInitialized() {
ToastService.OnShow += ShowToast;
ToastService.OnHide += HideToast;
}
private void ShowToast(string message, ToastLevel level) {
BuildToastSettings(level, message);
IsVisible = true;
StateHasChanged();
}
private void HideToast() {
IsVisible = false;
StateHasChanged();
}
private void BuildToastSettings(ToastLevel level, string message) {
switch (level) {
case ToastLevel.Info:
BackgroundCssClass = "bg-info";
IconCssClass = "info";
Heading = "Info";
break;
case ToastLevel.Success:
BackgroundCssClass = "bg-success";
IconCssClass = "check";
Heading = "Success";
break;
case ToastLevel.Warning:
BackgroundCssClass = "bg-warning";
IconCssClass = "exclamation";
Heading = "Warning";
break;
case ToastLevel.Error:
BackgroundCssClass = "bg-danger";
IconCssClass = "times";
Heading = "Error";
break;
}
Message = message;
}
public void Dispose() {
ToastService.OnShow -= ShowToast;
}
}
Integrating the toast component inside our existing Employee Blazor app
Inject the ToastService into the component.
In the OnInitialized event we’re wiring up the events we defined in the ToastService to handlers in the component.
We have the event handlers, ShowToast and HideToast. ShowToast takes the message and the toast level and passes them to BuildToastSettings. This then sets various CSS class names, the heading, and the message. The IsVisible property is then set on the component and StateHasChanged is called. HideToast just sets IsVisible to false and calls StateHasChanged.
<div class="toast @(IsVisible ? "toast-visible" : null) @BackgroundCssClass">
<div class="toast-icon">
<i class="fa fa-@IconCssClass" aria-hidden="true"></i>
</div>
<div class="toast-body">
<h5>@Heading</h5>
<p>@Message</p>
</div>
</div>
Razor markup – Toast.razor
.toast {
display: none;
padding: 1.5 rem;
color:
#fff;
z - index: 1;
position: absolute;
width: 25 rem;
top: 2 rem;
border - radius: 1 rem;
left: 50 % ;
}.toast - icon {
display: flex;
flex - direction: column;
justify - content: center;
padding: 0 1 rem;
font - size: 2.5 rem;
}.toast - body {
display: flex;
flex - direction: column;
flex: 1;
padding - left: 1 rem;
}.toast - body p {
margin - bottom: 0;
}.toast - visible {
display: flex;
flex - direction: row;
animation: fadein 1.5 s;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
Add the following CSS in app.css folder
Register our ToastService in Program.cs of EmployeePortal.Client project
public static async Task Main(string[] args) {
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add < App > ("app");
//builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddHttpClient < IEmployeeDataService, EmployeeDataService > (client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
builder.Services.AddScoped < ToastService > ();
await builder.Build().RunAsync();
}
Register the ToastService in Startup.cs of EmployeePortal.Server Project,
public void ConfigureServices(IServiceCollection services) {
services.AddDbContext < AppDbContext > (options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddControllersWithViews();
services.AddRazorPages();
services.AddScoped < IEmployeeRepository, EmployeeRepository > ();
services.AddScoped < ToastService > ();
SpreadsheetInfo.SetLicense("FREE-LIMITED-KEY");
}
Add the toast component to the main layout,
@inherits LayoutComponentBase
@using EmployeePortal.Client.Components;
<div class="sidebar">
<NavMenu />
</div>
<ToastBase></ToastBase>
<div class="main">
<div class="top-row px-4">
<a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
</div>
<div class="content px-4">
@Body
</div>
</div>
Add link for FontAwesome in the head tag of Index.html in EmployeePortal.Client project,
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>EmployeePortal</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" integrity="sha384-gfdkjb5BdAXd+lj+gudLWI+BXq4IuLW5IT+brZEZsLFm++aCMlF1V92rMkPaX4PP" crossorigin="anonymous">
</head>
Run the application and we are good to go.
When the employees are loaded on the home screen we would see the info notification.
When we edit any employee and the save button is clicked success notification would be displayed.
When new employee is added success notification would be displayed in green color.
If there are any errors while updating records Error notification would be displayed in Red color
Toast notifications colors and display position can be modified using above toast css classes
Notification time can be increased or decreased as per requirement in SetCountdown Method