Introduction
Dans les applications web modernes, de nombreuses opérations nécessitent un temps de traitement significatif : import de fichiers volumineux, génération de rapports complexes, traitement par lots, migrations de données, etc. L'expérience utilisateur lors de ces opérations longues est cruciale : un utilisateur ne sachant pas ce qui se passe côté serveur aura l'impression que l'application est figée ou cassée.
Le système LongTaskNotifier pour Blazor Server répond à ce besoin en fournissant un mécanisme de notification en temps réel, permettant à l'utilisateur de suivre précisément l'avancement d'une tâche longue avec :
- Affichage en temps réel de la progression
- Multiples barres de progression simultanées
- Messages informatifs, warnings et erreurs
- Possibilité d'annuler une tâche en cours
- Historique détaillé des événements
Pourquoi la notification temps réel est essentielle ?
1. Transparence et confiance
Lorsqu'un utilisateur lance une opération qui prend plusieurs minutes, le silence de l'application génère de l'anxiété. Il se pose des questions :
- Est-ce que ça fonctionne vraiment ?
- Combien de temps cela va-t-il prendre ?
- L'application a-t-elle planté ?
- Puis-je fermer l'onglet ?
Une notification en temps réel répond à toutes ces questions en affichant clairement :
- Le statut actuel ("Traitement de l'élément 45/200")
- L'étape en cours ("Validation des données")
- Le temps écoulé
- La possibilité d'annuler
2. Productivité de l'utilisateur
Sans feedback visuel, l'utilisateur est bloqué à attendre devant son écran, ne sachant pas combien de temps l'opération prendra. Avec une notification claire :
- Il peut estimer le temps restant grâce aux barres de progression
- Il peut continuer à travailler sur d'autres tâches si l'opération est longue
- Il peut identifier et corriger rapidement les problèmes (via les warnings/erreurs)
- Il peut annuler une opération qui prend trop de temps
3. Détection précoce des problèmes
Les notifications en temps réel permettent de détecter les anomalies pendant l'exécution :
- Warnings sur des données incohérentes
- Erreurs non bloquantes à corriger
- Ralentissements inattendus
- Échecs partiels nécessitant une intervention
4. Expérience utilisateur professionnelle
Une application qui communique clairement avec l'utilisateur pendant les traitements longs donne une impression de professionnalisme et de fiabilité. C'est la différence entre une application "amateur" et une application "production-ready".
Architecture du système
Diagramme de classes
classDiagram
class ILongTaskNotifier~TCategory~ {
<<interface>>
+Notify(LongTaskNotification) Task
+RegisterCancellationTokenSource(Guid, CancellationTokenSource) void
}
class LongTaskNotifier~TCategory~ {
-LongTaskContainerService _containerService
-ILogger _logger
-Dictionary~Guid, CancellationTokenSource~ _ctsList
+Notify(LongTaskNotification) Task
+RegisterCancellationTokenSource(Guid, CancellationTokenSource) void
}
class LongTaskNotification {
+Guid TaskId
+DateTime CreationDate
+LongTaskNotificationType Type
+string Subject
+string Body
+int? TotalCount
+int? Index
+string? ProgressBarCode
+string? ErrorMessage
+List~string~ BrokenRuleList
}
class LongTaskNotificationType {
<<enumeration>>
LongTaskStart
LongTaskWrite
LongTaskWarning
LongTaskError
LongTaskStartProgress
LongTaskProgress
LongTaskEndProgress
LongTaskCompleted
LongTaskFailed
LongTaskCanceled
}
class LongTaskContainerService {
-ConcurrentDictionary~Guid, LongTaskDisplayItem~ _activeTasks
+event Func~Task~ OnChange
+IReadOnlyList~LongTaskDisplayItem~ ActiveTasks
+ProcessNotificationAsync(LongTaskNotification, CancellationTokenSource?) Task
+RemoveTaskAsync(Guid) Task
+CancelTaskAsync(Guid) Task
}
class LongTaskDisplayItem {
+Guid TaskId
+string Title
+LongTaskStatus Status
+DateTime StartedAt
+DateTime? CompletedAt
+Dictionary~string, ProgressBarItem~ ProgressBars
+List~LongTaskLogEntry~ LogEntries
+bool IsCancellable
+CancellationTokenSource CancellationTokenSource
}
class LongTaskContainer {
<<Blazor Component>>
+LongTaskPosition Position
+int MaxLogEntries
-LongTaskContainerService LongTaskService
+OnAfterRender(bool) void
+CloseTask(Guid) Task
+CancelTask(Guid) Task
}
class ProgressBarItem {
+string Code
+string Label
+bool IsContinuous
+int Value
+int Max
+int Percentage
+string? CurrentMessage
}
ILongTaskNotifier~TCategory~ <|.. LongTaskNotifier~TCategory~
LongTaskNotifier~TCategory~ --> LongTaskContainerService : utilise
LongTaskNotifier~TCategory~ --> LongTaskNotification : crée
LongTaskNotification --> LongTaskNotificationType : référence
LongTaskContainerService --> LongTaskDisplayItem : gère
LongTaskDisplayItem --> ProgressBarItem : contient
LongTaskContainer --> LongTaskContainerService : injecte
LongTaskContainer ..> LongTaskDisplayItem : afficheDiagramme de séquence - Cycle de vie complet d'une tâche longue
sequenceDiagram
actor User as Utilisateur
participant UI as Blazor UI
participant Notifier as LongTaskNotifier
participant Service as LongTaskContainerService
participant Container as LongTaskContainer
participant BG as Tâche Background
User->>UI: Clic sur "Lancer le traitement"
UI->>UI: Créer CancellationTokenSource
UI->>Notifier: RegisterCancellationTokenSource(taskId, cts)
UI->>BG: Task.Run(() => ExecuteTask(taskId, token))
activate BG
BG->>Notifier: Start(taskId, "Import de données")
Notifier->>Service: ProcessNotificationAsync(notification, cts)
Service->>Service: Créer LongTaskDisplayItem
Service->>Container: OnChange Event
Container->>Container: StateHasChanged()
Container-->>User: 📦 Affichage "Import de données - En cours"
Note over BG: Phase 1 : Chargement
BG->>Notifier: StartContinuousProgress(taskId, "load", "Chargement du fichier")
Notifier->>Service: ProcessNotificationAsync(...)
Service->>Container: OnChange Event
Container-->>User: 🔄 Barre de progression continue
BG->>BG: await Task.Delay(5000, token)
BG->>Notifier: EndContinuousProgress(taskId, "load")
Note over BG: Phase 2 : Traitement
BG->>Notifier: StartProgress(taskId, "process", "Traitement", 100)
Notifier->>Service: ProcessNotificationAsync(...)
Service->>Container: OnChange Event
Container-->>User: 📊 Barre 0/100 (0%)
loop Pour chaque élément
BG->>BG: Traiter l'élément
BG->>Notifier: Progress(taskId, "process", "Élément 25", 25)
Notifier->>Service: ProcessNotificationAsync(...)
Service->>Container: OnChange Event
Container-->>User: 📊 Barre 25/100 (25%)
alt Élément avec warning
BG->>Notifier: Warning(taskId, "Anomalie", "Données incomplètes")
Notifier->>Service: ProcessNotificationAsync(...)
Service->>Container: OnChange Event
Container-->>User: ⚠️ Warning affiché
end
end
BG->>Notifier: EndProgress(taskId, "process")
Note over BG: Finalisation
BG->>Notifier: Completed(taskId, "Terminé", "100 éléments traités")
Notifier->>Service: ProcessNotificationAsync(...)
Service->>Service: task.Status = Completed
Service->>Container: OnChange Event
Container-->>User: ✅ "Terminé - 100 éléments traités"
deactivate BG
User->>Container: Clic sur "Fermer"
Container->>Service: RemoveTaskAsync(taskId)
Service->>Container: OnChange Event
Container-->>User: Notification disparueCe composant doit etre séparé en 2 assembiles
Une assembly UI qui permet réellement les notifications pour l'utilisateur, et une assembly Abstraction qui n'expose que l'interface ILongTaskNotification<T>
Le backend doit reférencer l'abstraction tandis que l'UI reférence le composant graphique.
2. Ajout du composant dans le layout
Dans MainLayout.razor, il faut ajoutez le composant container :
@using LongTaskNotifier.Components
@inherits LayoutComponentBase
<div class="page">
<main>
<article class="content">
@Body
</article>
</main>
</div>
<!-- Container de notifications en bas à droite -->
<LongTaskContainer Position="LongTaskPosition.BottomRight" MaxLogEntries="100" />
Par exemple dans un service on lance une longue tache :
public class MyService(ILongTaskNotification<MyService> notifier)
{
public async Task MyLongProcess()
{
var taskId = Guid.NewGuid(); // <- peut etre utilisé pour savoir s'il n'y a pas déjà un traitement en cours
// Créer un CancellationTokenSource pour permettre l'annulation
var cts = new CancellationTokenSource();
notifier.RegisterCancellationTokenSource(taskId, cts);
// Lancer la tâche en arrière-plan
_ = Task.Run(async () => await ExecuteInternal(taskId, cts.Token));
}
public async Task ExecuteInternal(Guid taskId, CancellationToken cancellationToken = default)
{
try
{
// 1. Démarrage
await notifier.Start(
taskId,
"Import de données",
"Début du traitement"
);
// 2. Phase de chargement (barre continue)
await notifier.StartContinuousProgress(
taskId,
"loading",
"Chargement du fichier"
);
// Simuler le chargement
await Task.Delay(3000, cancellationToken);
await notifier.EndContinuousProgress(taskId, "loading");
// 3. Phase de traitement (barre déterminée)
var items = GetItemsToProcess(); // 500 éléments
await notifier.StartProgress(
taskId,
"processing",
"Traitement des données",
items.Count
);
for (int i = 0; i < items.Count; i++)
{
// Vérifier l'annulation
cancellationToken.ThrowIfCancellationRequested();
// Traiter l'élément
var result = await ProcessItemAsync(items[i]);
// Mettre à jour la progression
await notifier.Progress(
taskId,
"processing",
$"Traitement de {items[i].Name}",
i + 1
);
// Gérer les cas spéciaux
if (result.HasWarning)
{
await notifier.Warning(
taskId,
"Données incomplètes",
$"L'élément {items[i].Name} contient des données manquantes"
);
}
if (result.HasError)
{
await LongTaskNotifier.Error(
taskId,
"Erreur de validation",
$"L'élément {items[i].Name} ne respecte pas les règles de validation"
);
}
// Petite pause pour éviter de surcharger le système
await Task.Delay(50, cancellationToken);
}
await notifier.EndProgress(taskId, "processing");
// 4. Phase de finalisation
await notifier.StartProgress(
taskId,
"finalize",
"Finalisation",
3
);
await notifier.Progress(taskId, "finalize", "Sauvegarde en base", 1);
await SaveToDatabase();
await Task.Delay(1000, cancellationToken);
await notifier.Progress(taskId, "finalize", "Mise à jour des index", 2);
await UpdateIndexes();
await Task.Delay(1000, cancellationToken);
await notifier.Progress(taskId, "finalize", "Notification des utilisateurs", 3);
await NotifyUsers();
await Task.Delay(1000, cancellationToken);
await notifier.EndProgress(taskId, "finalize");
// 5. Complétion
await notifier.Completed(
taskId,
"Import terminé avec succès",
$"{items.Count} éléments importés"
);
}
catch (OperationCanceledException)
{
// L'utilisateur a annulé la tâche
await notifier.Canceled(taskId);
}
catch (Exception ex)
{
// Erreur fatale
await notifier.Failed(
taskId,
$"Erreur lors de l'import : {ex.Message}"
);
}
finally
{
_isRunning = false;
await InvokeAsync(StateHasChanged);
}
}
}
Voici un exemple de ce que ça donne comme affichage :
Annulation de tâches longues avec CancellationTokenSource
Pourquoi l'annulation est cruciale ?
L'annulation d'une tâche longue est une fonctionnalité essentielle pour plusieurs raisons :
- Correction d'erreur : L'utilisateur réalise qu'il a lancé la mauvaise opération
- Attente trop longue : L'opération prend plus de temps que prévu
- Modification des priorités : Une tâche plus urgente doit être effectuée
- Économie de ressources : Libérer des ressources serveur pour d'autres traitements
Diagramme de séquence - Annulation d'une tâche
sequenceDiagram
actor User as Utilisateur
participant Container as LongTaskContainer
participant Service as LongTaskContainerService
participant BG as Tâche Background
participant Notifier as LongTaskNotifier
Note over User,Notifier: Tâche en cours d'exécution...
User->>Container: Clic sur bouton "Annuler"
Container->>Service: CancelTaskAsync(taskId)
activate Service
Service->>Service: task.CancellationTokenSource.CancelAsync()
Service->>Service: task.Status = Canceled
Service->>Service: AddLogEntry("Arrêt demandé")
Service->>Container: OnChange Event
Container-->>User: 🚫 "Arrêt en cours..."
deactivate Service
Note over BG: Dans la boucle de traitement
BG->>BG: cancellationToken.ThrowIfCancellationRequested()
BG->>BG: OperationCanceledException lancée
activate BG
BG->>Notifier: Canceled(taskId)
Notifier->>Service: ProcessNotificationAsync(LongTaskCanceled)
Service->>Service: task.Status = Canceled
Service->>Container: OnChange Event
Container-->>User: 🚫 "Tâche annulée"
deactivate BG
Note over User: L'utilisateur peut fermer la notificationConclusion
Avoir un système d'affichage de la progression des très longues tâches comme LongTaskNotifier pour Blazor Server offre une solution complète et professionnelle pour gérer les notifications de tâches longues. Ses points forts :
✅ Transparence totale : L'utilisateur sait toujours ce qui se passe
✅ Expérience utilisateur optimale : Barres de progression, messages, warnings
✅ Contrôle : Annulation possible à tout moment
✅ Performance : Optimisé pour les tâches très longues
✅ Flexibilité : Multiples barres de progression, positionnement configurable
✅ Production-ready : Gestion complète des erreurs et des cas limites
En implémentant ce système dans vos applications Blazor, vous transformerez l'expérience utilisateur lors d'opérations longues, passant d'un état d'incertitude et d'attente anxieuse à une expérience transparente, informative et contrôlable. N'hésitez pas pour nous contacter