Dans StockAsso.FrontWebApp, j’avais besoin d’afficher des notifications globales après certaines opérations : confirmer l’ajout d’un produit, signaler un avertissement métier ou informer l’utilisateur d’une erreur.
Je n’ai pas créé de IToastService. J’ai réutilisé le mécanisme déjà présent dans l’application : ReloadComponentNotificationService.
L’implémentation réelle repose sur :
ViewModels.ToastetToastSeverity;ReloadComponentNotificationService;- le composant global
Toast.razor; - son intégration dans
App.razor; - Bootstrap et le module
toast.js.
flowchart LR
A["Composant métier"] -->|"NotifyAsync(new Toast(...))"| B["ReloadComponentNotificationService"]
B -->|"EventCallback"| C["Toast.razor"]
C -->|"Rendu d'un élément .toast"| D["DOM"]
E["Bootstrap"] --> DLe modèle de notification
Le modèle se trouve dans ViewModels/Toast.cs :
namespace StockAsso.FrontWebApp.ViewModels;
public enum ToastSeverity
{
Info,
Success,
Warning,
Error
}
public record Toast(string Subject, string Body, ToastSeverity Severity = ToastSeverity.Info, int? DurationInMs = null);
Le record transporte le titre, le corps, la sévérité et une éventuelle durée d’affichage.
| Propriété | Rôle |
|---|---|
Subject |
titre de la notification |
Body |
contenu du message |
Severity |
type et couleur de la notification |
DurationInMs |
délai éventuel avant la fermeture automatique |
Le service existant
Le projet utilise Services/ReloadComponentNotificationService.cs :
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Components;
namespace StockAsso.FrontWebApp.Services;
public class ReloadComponentNotificationService
{
private readonly ConcurrentDictionary<string,ComponentChangedSubscription> _changedSubscriptions = new();
private sealed class ComponentChangedSubscription(ReloadComponentNotificationService Owner, EventCallback Callback, string Key) : IDisposable
{
public Task NotifyAsync(object? obj = null) => Callback.InvokeAsync(obj);
public void Dispose() => Owner._changedSubscriptions.TryRemove(Key, out var bye);
}
public IDisposable AddSubscription(string subscriberName,EventCallback callback)
{
var subscription = new ComponentChangedSubscription(this, callback, subscriberName);
_changedSubscriptions.AddOrUpdate(subscriberName, subscription, (key, oldValue) => subscription);
return subscription;
}
public Task NotifyAsync(object? obj = null)
=> Task.WhenAll(_changedSubscriptions.Select(i => i.Value.NotifyAsync(obj)));
}
Ce service n’est pas spécifique aux Toasts. Il est également utilisé pour demander à d’autres composants de se recharger, notamment des composants du panier et du compte.
Son fonctionnement est simple :
- un composant s’abonne avec
AddSubscription; - le service conserve son
EventCallbackdans unConcurrentDictionary; NotifyAsyncinvoque tous les callbacks enregistrés ;- l’objet passé à
NotifyAsyncest transmis aux abonnés.
sequenceDiagram
participant P as Composant métier
participant S as ReloadComponentNotificationService
participant T as Toast.razor
T->>S: AddSubscription("Toast", callback)
P->>S: NotifyAsync(new Toast(...))
S->>T: Callback.InvokeAsync(obj)
T->>T: DisplayToast(obj)
T->>T: Nouveau renduLe service est enregistré en Scoped dans StartupExtensions.cs :
builder.Services.AddScoped<Services.ReloadComponentNotificationService>();
Le composant Toast.razor
Le composant situé dans Pages/Shared/Toast.razor injecte le service :
@inject Services.ReloadComponentNotificationService ReloadComponentNotificationService
Il s’abonne ensuite sous la clé "Toast" :
@code {
ViewModels.Toast? toast = null;
string bgClass = "bg-success";
protected override void OnInitialized()
{
ReloadComponentNotificationService.AddSubscription("Toast", EventCallback.Factory.Create(this, DisplayToast));
}
void DisplayToast(object displayToast)
{
if (displayToast as ViewModels.Toast is not null)
{
toast = displayToast as ViewModels.Toast;
@if (toast!.Severity == ViewModels.ToastSeverity.Error)
{
bgClass = "bg-danger";
}
else if (toast.Severity == ViewModels.ToastSeverity.Warning)
{
bgClass = "bg-warning";
}
else if (toast.Severity == ViewModels.ToastSeverity.Info)
{
bgClass = "bg-info";
}
else
{
bgClass = "bg-success";
}
}
}
}
EventCallback.Factory.Create relie DisplayToast au composant. Quand le service invoque ce callback, Blazor exécute la méthode puis actualise le rendu.
Le test de type est important :
if (displayToast as ViewModels.Toast is not null)
Comme le service est partagé avec d’autres composants, un appel à NotifyAsync() sans objet ne doit pas afficher de Toast.
flowchart TD
A["NotifyAsync(obj)"] --> B["Tous les abonnés"]
B --> C["Toast.razor"]
C --> D{"obj est un ViewModels.Toast ?"}
D -->|Oui| E["Mise à jour du Toast"]
D -->|Non| F["Aucune modification"]La correspondance des couleurs Bootstrap
Le composant traduit la sévérité en classe Bootstrap :
ToastSeverity |
Classe Bootstrap |
|---|---|
Error |
bg-danger |
Warning |
bg-warning |
Info |
bg-info |
Success |
bg-success |
flowchart LR
A["Error"] --> B["bg-danger"]
C["Warning"] --> D["bg-warning"]
E["Info"] --> F["bg-info"]
G["Success"] --> H["bg-success"]Le balisage Bootstrap rendu
Le composant ne rend rien tant que toast vaut null. Lorsqu’une notification est reçue, il produit ce balisage :
@if (toast is not null)
{
var toastId = $"toast-notifier-{Guid.NewGuid()}";
<div class="toast fade show" id="@toastId" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="@(toast.DurationInMs > 0)" data-bs-delay="@toast.DurationInMs" data-bs-animation="true">
<div class="toast-header @bgClass text-@bgClass">
<strong class="me-auto toast-subject">@toast.Subject</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Fermer"></button>
</div>
<div class="toast-body @bgClass text-@bgClass ">
@toast.Body
</div>
</div>
}
Un identifiant est généré à chaque rendu :
var toastId = $"toast-notifier-{Guid.NewGuid()}";
La classe show rend la notification visible. Le bouton de fermeture utilise l’attribut Bootstrap :
data-bs-dismiss="toast"
La fermeture automatique dépend de DurationInMs :
data-bs-autohide="@(toast.DurationInMs > 0)"
data-bs-delay="@toast.DurationInMs"
Sans durée, DurationInMs vaut null et data-bs-autohide reçoit false. Avec une durée positive, Bootstrap reçoit true et le délai correspondant.
Le positionnement global dans App.razor
Je ne répète pas le composant dans chaque page. Il est présent une seule fois dans Pages/App.razor :
<div class="container">
<div class="toast-container position-fixed bottom-0 start-50 translate-middle-x mb-5">
<Toast />
</div>
</div>
Les classes Bootstrap placent le conteneur en position fixe, en bas de l’écran et centré horizontalement.
flowchart TB
A["App.razor"] --> B["Routes"]
A --> C["SectionOutlet modals"]
A --> D["toast-container"]
D --> E["Toast.razor"]L’intégration JavaScript
Le fichier wwwroot/js/toast.js contient :
/**
* Toast
* @requires https://getbootstrap.com
*/
const toast = (() => {
let toastElList = [].slice.call(document.querySelectorAll('.toast'));
let toastList = toastElList.map((toastEl) => new bootstrap.Toast(toastEl));
})();
export default toast;
Ce module recherche les éléments .toast présents dans le document et crée une instance bootstrap.Toast pour chacun.
Il est importé dans wwwroot/js/stockasso.js :
import toast from './toast.js';
En développement, App.razor charge Bootstrap avant le module principal :
<script src="lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="lib/simplebar/simplebar.min.js"></script>
<script src="lib/tiny-slider/tiny-slider.min.js"></script>
<script src="lib/drift-zoom/Drift.min.js"></script>
<script src="js/stockasso.js" type="module"></script>
En production et en staging, l’application charge les bundles :
<script src="@Assets["dist/vendor.min.js"]"></script>
<script src="@Assets["dist/stockasso.min.js"]"></script>
flowchart LR
A["Bootstrap ou vendor.min.js"] --> B["stockasso.js ou stockasso.min.js"]
B --> C["Import de toast.js"]
C --> D["new bootstrap.Toast(toastEl)"]Le déclenchement réel depuis SupplierProduct.razor
Une utilisation concrète existe dans Pages/Catalog/SupplierProduct.razor.
Le composant injecte le service :
@inject Services.ReloadComponentNotificationService ReloadComponentNotificationService
Afficher un avertissement
await ReloadComponentNotificationService.NotifyAsync(new ViewModels.Toast("Attention", "Tu es présent dans plusieurs associations, il faut passer par l'interface d'admin pour ajouter des produits à l'une des boutiques", ToastSeverity.Warning));
Afficher une erreur
await ReloadComponentNotificationService.NotifyAsync(new ViewModels.Toast("Erreur", "Une erreur est survenue lors de l'ajout du produit", ToastSeverity.Error));
Afficher une confirmation
await ReloadComponentNotificationService.NotifyAsync(new ViewModels.Toast("Produit ajouté", "Ce produit a bien été ajouté à ta boutique", ToastSeverity.Success));
Le chemin complet est donc le suivant :
sequenceDiagram
autonumber
actor U as Utilisateur
participant P as SupplierProduct.razor
participant S as ReloadComponentNotificationService
participant T as Toast.razor
participant B as Bootstrap
U->>P: Ajoute un produit
P->>P: Exécute le traitement
P->>S: NotifyAsync(new Toast(...))
S->>T: Invoque DisplayToast
T->>T: Stocke le Toast
T->>T: Sélectionne bgClass
T-->>U: Rend l'élément .toast
B-->>U: Gère sa fermetureSynthèse
Mon système de Toast fonctionne ainsi :
ViewModels.Toasttransporte les données de la notification ;Toast.razors’abonne àReloadComponentNotificationServicesous la clé"Toast";- un composant métier construit un
ViewModels.Toast; - il transmet cet objet avec
NotifyAsync; DisplayToastvérifie son type et choisit une classe Bootstrap ;- Blazor actualise le composant ;
- le Toast apparaît dans le conteneur global défini dans
App.razor; - Bootstrap fournit le style et la fermeture.
L’appel réellement utilisé dans le projet reste direct :
await ReloadComponentNotificationService.NotifyAsync(
new ViewModels.Toast(
"Produit ajouté",
"Ce produit a bien été ajouté à ta boutique",
ToastSeverity.Success));
Cette mise en place s’appuie uniquement sur le code existant de StockAsso.FrontWebApp : Toast.cs, ReloadComponentNotificationService.cs, Toast.razor, App.razor, toast.js, stockasso.js, StartupExtensions.cs et les appels présents dans SupplierProduct.razor.
Aucun commentaire publié pour le moment.
Ajouter un commentaire