Blazor -  Toast Notifications (sem usar javascript)


Hoje veremos como exibir notificações toast no Blazor sem usar JavaScript. Vamos usar somente código C#, HTML e CSS.

Este artigo foi totalmente baseado no original de Chris Sainty com pequenas alterações e ajustes.

Uma notificação toast fornece um feedback simples sobre uma operação em uma pequena janela pop-up, e, ocupa a quantidade de espaço necessária para a mensagem; a atividade atual continua visível e interativa desaparecendo automaticamente após um tempo limite.  É um recurso muito usando em aplicações Androids.

Atualmente existem diversas bibliotecas que usam JavaScript para emitir toast notifications que podemos usar com o Blazor.( veja aqui e aqui )

Hoje veremos como implementar este recurso em um projeto Blazor Server sem usar JavaScript mas apenas código C#, e umas pitadas de CSS e HTML.

O objetivo do projeto é dar uma idéia de como implementar este recurso sem usar JavaScript.

Recursos usados:

Criando o projeto Blazor Server no VS Community 2019

Abra o VS 2019 Community (versão mínima 16.4) e selecione a opção Create a New Project;

A seguir selecione a opção Blazor app e clique em next;

Informe o nome do projeto :  Blazor_ToastNotification1, a localização e clique em Create;

A seguir teremos uma janela com duas opções :

  1. Blazor Server App
  2. Blazor WebAssembly App

Selecione a primeira opção - Blazor Server App. Não vamos usar autenticação e vamos habilitar o https.

Clique no botão Create para criar o projeto.

Antes de prosseguir vamos limpar o projeto excluindo os arquivos abaixo e suas referências:

Vamos também ajustar o arquivo NavMenu.razor deixando apenas a opção Toast Notifications e a opção Demo Toast conforme o código a seguir:

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Toast Notifications
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="demo">
                <span class="oi oi-plus" aria-hidden="true"></span> Demo Toast
            </NavLink>
        </li>
    </ul>
</div>

Agora com o projeto criado e os ajustes feitos vamos criar o serviço de notificação Toast.

Criando o serviço de Notificação toast

Crie uma pasta Services na raiz do projeto (Project->New Folder) e nesta pasta crie o arquivo ToastLevel onde vamos definir uma enumeração com os tipos de mensagens toast a serem exibidas.

    public enum ToastLevel
    {
        Info,
        Success,
        Warning,
        Error
    }

Crie também o arquivo ToastService onde vamos definir o serviço para emitir notificações:

using System;
using System.Timers;
namespace Blazor_ToastNotification1.Services
{
    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 Timer(5000);
                Countdown.Elapsed += HideToast;
                Countdown.AutoReset = false;
            }
        }
        private void HideToast(object source, ElapsedEventArgs args)
        {
            OnHide?.Invoke();
        }
        public void Dispose()
        {
            Countdown?.Dispose();
        }
    }
}

A classe ToastService vai permitir emitir as notificações toast e possui um único método público, ShowToast() que recebe a mensagem a ser exibida o e nível da mensagem toast.

O serviço também possui dois eventos, OnShow e OnHide, e um temporizador, Countdown. Nosso componente toast se inscreverá nos eventos e os usará para exibir  e ocultar as notificações.

O timer é usado internamente pelo serviço e esta definido inicialmente com um delay de 5 segundos, após esse período o evento OnHide será invocado.

Criando o componente Toast.razor e o code-behind ToastBase.cs

Agora vamos criar o componente Toast e aqui vamos separar a lógica do componente do código de marcação usando a experiência code-behind.

Para isso vamos criar um componente Toast.razor na pasta Shared com o código abaixo:

@inherits ToastBase
<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>

Note que usamos a declaração @inherits ToastBase no início do arquivo para indicar que ele herda da classe ToastBase que iremos criar a seguir.

Por definição o arquivo code-behind tem que ter nome do componente Toast seguido do prefixo Base e tem que herdar de ComponentBase.

O código da classe ToastBase é dado abaixo:

using Blazor_ToastNotification1.Services;
using Microsoft.AspNetCore.Components;
using System;
namespace Blazor_ToastNotification1.Shared
{
    public 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;
            InvokeAsync(StateHasChanged);
        }
        private void HideToast()
        {
            IsVisible = false;
            InvokeAsync(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;
        }
    }
}

Vamos entender o código:

Estamos injetando o serviço ToastService no componente e definindo algumas propriedades que serão usadas na parte de marcação do componente.

A seguir, estamos substituindo um dos eventos do ciclo de vida do componente Blazor, OnInitialized onde estamos conectando os eventos que definimos no ToastService aos manipuladores no componente.

Depois, temos os manipuladores de eventos ShowToast e HideToast. ShowToast pega a mensagem e o nível de toast e os passa para o método BuildToastSettings onde definmos vários nomes de classes CSS, o cabeçalho e a mensagem.

A propriedade IsVisible é definida no componente e StateHasChanged é chamado usando o InvokeAsync. O método HideToast apenas define IsVisible como false e chama StateHasChanged.

Precisamos chamar StateHasChanged pois o componente precisa renderizar novamente quando seu estado for alterado. Quando essa atualização vem do próprio componente ou via um valor passado para o componente usando a diretiva [Parameter], ou seja, algo que o componente conhece e pode monitorar , então  uma nova renderização é acionada automaticamente.

No entanto, se ocorrer uma atualização no estado dos componentes de uma fonte externa, por exemplo, um evento, esse processo automático é ignorado e uma chamada manual deve ser feita para que o componente saiba que algo mudou. É aqui que entra o StateHasChanged.

No nosso caso, estamos atualizando os valores dos componentes com base em um evento externo, OnShow, do ToastService. Isso significa que precisamos chamar StateHasChanged para informar ao componente que ele precisa ser renderizado novamente.

Agora para concluir precisamos definir o código CSS que iremos usar para exibir as notificações com base em seu nível.

Inclua o código abaixo no arquivo site.css que esta na pasta wwwroot do projeto:

.toast {
    display: none;
    padding: 1.5rem;
    color: #fff;
    z-index: 1;
    position: absolute;
    width: 25rem;
    top: 2rem;
    border-radius: 1rem;
    left: 50%;
}
.toast-icon {
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 0 1rem;
    font-size: 2.5rem;
}
.toast-body {
    display: flex;
    flex-direction: column;
    flex: 1;
    padding-left: 1rem;
}
    .toast-body p {
        margin-bottom: 0;
    }
    .toast-visible {
        display: flex;
        flex-direction: row;
        animation: fadein 1.5s;
    }
@keyframes fadein {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

Registrando o serviço

Agora precisamos registrar o serviço no ConfigureServices da classe Startup :

 public void ConfigureServices(IServiceCollection services)
 {
            services.AddScoped<ToastService>();
            services.AddRazorPages();
            services.AddServerSideBlazor();
 }

Precisamos adicionar o componente <Toast/> ao arquivo MainLayout.razor :

@inherits LayoutComponentBase
<Toast />
<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">
        @Body
    </div>
</div>

Inclua também um link para referenciar FontAwesome na tag head do arquivo _Host.cshtml da pasta Pages:

...
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Blazor_ToastNotification1</title>
    <base href="~/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />

    <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">

    <link href="css/site.css" rel="stylesheet" />
</head>
...
...
Aqui estamos usando uma referência CDN.
Abra o arquivo _Imports.razor  do projeto e inclua  uma referência a pasta Services do projeto:
@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using Blazor_ToastNotification1
@using Blazor_ToastNotification1.Shared
@using Blazor_ToastNotification1.Services

Usando o componente Toast

Agora vamos usar o serviço criado para emitir notificações toast.

No arquivo Index.razor substitua o código original pelo código abaixo:

@page "/"

@inject ToastService toastService

<h3>Toast Notification</h3>

<button class="btn btn-info" @onclick="@(() => toastService.ShowToast("Mensagem de INFO", ToastLevel.Info))">Info Toast</button>

<button class="btn btn-success" @onclick="@(() => toastService.ShowToast("Mensagem de SUCESSO", ToastLevel.Success))">Success Toast</button>

<button class="btn btn-warning" @onclick="@(() => toastService.ShowToast("Mensagem de ALERTA", ToastLevel.Warning))">Warning Toast</button>

<button class="btn btn-danger" @onclick="@(() => toastService.ShowToast("Mensagem de ERRO", ToastLevel.Error))">Error Toast</button>
 

Aqui apenas injetamos o nosso serviço e estamos definindo em cada evento onClick de cada botão a chamada a um tipo de notificação toast.

Crie um novo componente chamado Demo.razor na pasta Pages e inclua o código a seguir:

@page "/demo"
<h3>Demo Toast</h3>
<Aviso Exibir="Exibir">
    <h5>@Mensagem</h5>
</Aviso>
@code {
    [Inject] protected ToastService ToastService { get; set; }
    bool Exibir = false;
    [Parameter]
    public string Mensagem { get; set; }
    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        await Task.Run(() => ToastService.ShowToast("Componente Initializado!", ToastLevel.Info));
        ExibeAviso();
    }
    private void ExibeAviso()
    {
        Exibir = true;
        Mensagem = "Exemplo de utilização do componente <Toast> para exibir notificações";
    }
}

Aqui também injetamos o serviço no componente e no evento OnInitializedAsync emitimos uma notificação para informar que o componente foi inicializado. A seguir exibimos uma mensagem usando o componente Aviso.

Para concluir vamos criar o componente Aviso.razor na pasta Shared que estamos usando

@if (Exibir)
{
    <div class="alert alert-secondary mt-4" role="alertdialog">
        @ChildContent
    </div>
}

@code {
    [Parameter]
    public bool Exibir { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }

}

Agora é só alegria...

Executando o projeto teremos o resultado abaixo:

Pegue o projeto aqui:  Blazor_ToastNotification1.zip (sem as referências)

"Levantarei os meus olhos para os montes, de onde vem o meu socorro.
O meu socorro vem do Senhor que fez o céu e a terra."

Salmos 121:1,2

Referências:


José Carlos Macoratti