Introduction
Dans une application Blazor Server, je peux proposer une interface très riche sans déplacer toute ma logique dans le navigateur. En contrepartie, l'interface dépend d'un circuit actif entre le navigateur et le serveur. Dès que le Wi-Fi change, qu'un mobile sort de veille ou qu'un déploiement redémarre le processus, l'utilisateur peut se retrouver devant un écran figé sans savoir s'il doit attendre ou recommencer.
Pendant longtemps, je me suis contenté du comportement par défaut : une bannière de reconnexion, puis un rechargement si les tentatives échouaient. Sur une application métier, ce n'est pas suffisant. Un utilisateur qui remplit un panier, une fiche client ou un formulaire d'audit doit comprendre ce qui se passe :
- est-ce une coupure temporaire ?
- est-ce que l'application essaie encore de revenir ?
- est-ce que le circuit serveur a été perdu ?
- est-ce que recharger la page risque d'effacer sa saisie ?
Avec .NET 10, le template Blazor Web App fournit un composant ReconnectModal personnalisable et une notification d'état components-reconnect-state-changed. Dans cet article, je montre comment je m'en sers pour construire une expérience de déconnexion propre, mesurable et compréhensible.
Je vais couvrir :
- Les différents états d'une reconnexion Blazor Server.
- Le réglage de la durée de conservation d'un circuit côté serveur.
- Une
ReconnectModalclaire pour l'utilisateur. - Le JavaScript nécessaire pour retenter ou recharger sans dépendre du serveur déconnecté.
- Un
CircuitHandlerC# pour journaliser les coupures et retours de connexion. - Les tests que j'applique avant une mise en production.
1. Une déconnexion n'est pas toujours un incident applicatif
Un circuit Blazor Server repose sur une connexion SignalR. Quand celle-ci tombe, le navigateur tente de rejoindre le circuit conservé en mémoire sur le serveur. Plusieurs situations sont possibles :
| Etat | Ce que cela signifie pour moi | Réponse utilisateur adaptée |
|---|---|---|
show |
La connexion vient d'être perdue et le dialogue apparaît. | J'indique que les données à l'écran ne doivent pas être ressaisies tout de suite. |
retrying |
Blazor est en train d'effectuer une nouvelle tentative. | J'affiche le nombre de tentatives en cours. |
hide |
Le même circuit est à nouveau accessible. | Je ferme le dialogue et l'utilisateur continue. |
failed |
Les tentatives automatiques n'ont pas abouti, souvent à cause du réseau. | Je propose une nouvelle tentative manuelle. |
rejected |
Le serveur a répondu mais refuse l'ancien circuit : son état n'existe plus. | Je demande un rechargement, en avertissant qu'un brouillon doit être restauré par un autre mécanisme. |
paused |
Le circuit a été volontairement suspendu. | J'explique que la session est en pause plutôt que de parler de panne. |
Voici le flux que je veux obtenir côté interface :
stateDiagram-v2
[*] --> Connecte
Connecte --> ConnexionPerdue: reseau coupe
ConnexionPerdue --> Tentative: show / retrying
Tentative --> Connecte: hide
Tentative --> EchecReseau: failed
EchecReseau --> Tentative: Blazor.reconnect()
Tentative --> CircuitPerdu: rejected
CircuitPerdu --> PageRechargee: location.reload()
Connecte --> EnPause: paused
EnPause --> Connecte: reprise du circuitLe point important est de ne pas confondre failed et rejected. Dans le premier cas, je peux encore récupérer le circuit si le réseau revient. Dans le second, le serveur ne dispose plus de l'état du circuit initial : répéter la même tentative ne résout pas le problème.
2. Configurer combien de temps je conserve un circuit déconnecté
Une reconnexion ne peut fonctionner que si le serveur conserve le circuit déconnecté assez longtemps. Par défaut, je pourrais laisser la configuration standard ; en production, je préfère expliciter mon compromis entre confort utilisateur et mémoire serveur.
Dans Program.cs, je configure les composants interactifs serveur :
using Microsoft.AspNetCore.Components.Server;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents(options =>
{
// Je garde un circuit deconnecte trois minutes.
// Au-dela, l'utilisateur devra recharger ou restaurer un brouillon.
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(3);
// Je borne le nombre de circuits deconnectes conserves en memoire.
options.DisconnectedCircuitMaxRetained = 250;
});
var app = builder.Build();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
Je choisis ces valeurs à partir du parcours réel : dans un back-office utilisé sur un réseau fixe, trois minutes sont généralement confortables. Pour un outil terrain utilisé sur téléphone, je peux augmenter la durée, mais je le fais en surveillant le nombre de circuits conservés et la consommation mémoire.
Ce paramétrage ne remplace pas la sauvegarde d'un brouillon. Si un utilisateur remplit un formulaire de quinze minutes, je mets en place une sauvegarde métier ou la persistance d'état de circuit .NET 10 ; la modal de reconnexion n'est que la partie visible de l'expérience.
3. Régler les tentatives automatiques côté navigateur
Le serveur décide combien de temps un circuit reste disponible. Le navigateur décide à quel rythme il tente de le rejoindre. Dans une Blazor Web App, je peux démarrer Blazor manuellement et préciser les options de reconnexion :
@* App.razor ou le document hote selon l'organisation du projet *@
<script src="@Assets["_framework/blazor.web.js"]" autostart="false"></script>
<script>
Blazor.start({
circuit: {
reconnectionOptions: {
maxRetries: 5,
retryIntervalMilliseconds: 2000
}
}
});
</script>
Dans cet exemple, j'autorise cinq tentatives espacées de deux secondes. J'évite une séquence trop longue : attendre silencieusement pendant trente secondes donne l'impression que l'application est bloquée. Mon interface doit rapidement dire à l'utilisateur ce qui est en cours, puis lui donner un choix.
sequenceDiagram
participant U as Utilisateur
participant N as Navigateur Blazor
participant S as Serveur
U->>N: Travaille sur un formulaire
N-xS: Coupure de connexion
N-->>U: Affiche ReconnectModal
loop Jusqu'a maxRetries
N->>S: Tentative de reconnexion
S-->>N: Circuit disponible ?
end
alt Circuit retrouve
S-->>N: Connexion acceptee
N-->>U: Ferme la modal, saisie conservee
else Reseau toujours indisponible
N-->>U: Etat failed, bouton Reessayer
else Circuit supprime ou serveur redemarre
S-->>N: Reconnexion rejetee
N-->>U: Etat rejected, bouton Recharger
end4. Personnaliser la ReconnectModal fournie par .NET 10
Le template Blazor Web App .NET 10 créé en mode interactif Server ou Auto inclut un composant Components/Layout/ReconnectModal.razor, accompagné de ses fichiers CSS et JavaScript colocalisés. Je pars de ce composant et je remplace le message technique par un dialogue utile dans mon contexte métier.
Blazor pose automatiquement des classes components-reconnect-* sur l'élément dont l'identifiant est components-reconnect-modal. Je peux donc afficher un contenu différent selon l'état sans faire d'aller-retour serveur.
@* Components/Layout/ReconnectModal.razor *@
<dialog id="components-reconnect-modal" class="reconnect-dialog" data-nosnippet>
<section class="components-reconnect-show reconnect-content">
<h2>Connexion interrompue</h2>
<p>
Je conserve votre écran pendant que l'application tente de se reconnecter.
Ne rechargez pas encore la page.
</p>
</section>
<section class="components-reconnect-retrying reconnect-content">
<h2>Reconnexion en cours</h2>
<p>
Tentative
<strong id="components-reconnect-current-attempt"></strong>
sur
<strong id="components-reconnect-max-retries"></strong>.
</p>
</section>
<section class="components-reconnect-failed reconnect-content">
<h2>Le réseau ne répond pas</h2>
<p>Vérifiez votre connexion, puis relancez une tentative.</p>
<button id="retry-connection" type="button" class="btn btn-primary">
Réessayer
</button>
</section>
<section class="components-reconnect-rejected reconnect-content">
<h2>La session a expiré</h2>
<p>
Le serveur ne peut plus reprendre cet écran.
Rechargez la page pour continuer.
</p>
<button id="reload-page" type="button" class="btn btn-primary">
Recharger la page
</button>
</section>
<section class="components-reconnect-paused reconnect-content">
<h2>Session mise en pause</h2>
<p>La connexion reprendra lorsque vous reviendrez sur cette page.</p>
</section>
</dialog>
Je garde volontairement deux messages différents : « le réseau ne répond pas » autorise une nouvelle tentative, tandis que « la session a expiré » prévient l'utilisateur qu'un simple retry ne retrouvera pas l'ancien circuit.
Une feuille de style simple
/* Components/Layout/ReconnectModal.razor.css */
.reconnect-dialog {
border: 0;
border-radius: 1rem;
padding: 0;
max-width: 30rem;
box-shadow: 0 1rem 3rem rgb(0 0 0 / 22%);
}
.reconnect-dialog::backdrop {
background: rgb(15 23 42 / 52%);
}
.reconnect-content {
display: none;
padding: 1.75rem;
}
.components-reconnect-show .components-reconnect-show,
.components-reconnect-retrying .components-reconnect-retrying,
.components-reconnect-failed .components-reconnect-failed,
.components-reconnect-rejected .components-reconnect-rejected,
.components-reconnect-paused .components-reconnect-paused {
display: block;
}
.NET 10 s'appuie sur ce dialogue et ses classes au lieu d'injecter dynamiquement le style de l'ancienne interface de reconnexion. Pour moi, c'est plus propre à personnaliser et cela facilite une politique Content Security Policy stricte.
5. Le piège à éviter : un @onclick Razor ne peut pas réparer une connexion coupée
La première fois que je personnalise une modal de reconnexion, la tentation est forte d'écrire ceci :
@* A ne pas faire dans la modal de deconnexion *@
<button @onclick="RetryAsync">Réessayer</button>
@code {
private Task RetryAsync()
{
// Cette méthode C# nécessite précisément le circuit qui est indisponible.
return Task.CompletedTask;
}
}
Ce bouton dépend du circuit serveur pour appeler la méthode C#. Or, lorsque j'affiche le bouton, le problème est justement que ce circuit n'est plus joignable. L'action de secours doit donc s'exécuter dans le navigateur, en JavaScript.
Dans le fichier JavaScript colocalisé de ReconnectModal, j'écoute le changement d'état officiel et je branche mes boutons :
// Components/Layout/ReconnectModal.razor.js
const modal = document.getElementById("components-reconnect-modal");
const retryButton = document.getElementById("retry-connection");
const reloadButton = document.getElementById("reload-page");
modal.addEventListener("components-reconnect-state-changed", handleStateChanged);
function handleStateChanged(event) {
const state = event.detail.state;
if (state === "show") {
modal.showModal();
} else if (state === "hide") {
modal.close();
} else if (state === "failed") {
retryButton.disabled = false;
} else if (state === "rejected") {
reloadButton.disabled = false;
}
}
retryButton.addEventListener("click", async () => {
retryButton.disabled = true;
const restored = await Blazor.reconnect();
if (!restored) {
location.reload();
}
});
reloadButton.addEventListener("click", () => location.reload());
La logique est volontairement minimale :
- sur
failed, je permets une tentative supplémentaire avecBlazor.reconnect(); - sur
rejected, je recharge, car Microsoft indique que l'état du circuit serveur est perdu ; - je n'appelle aucun service C# pour afficher ou déclencher ces actions de secours.
6. Instrumenter les déconnexions avec un CircuitHandler en C#
L'interface explique le problème à l'utilisateur. Côté serveur, j'ai également besoin de savoir si les déconnexions sont anecdotiques ou symptomatiques d'un problème de production.
J'implémente un CircuitHandler qui journalise l'ouverture du circuit, la perte de connexion, son retour et sa fermeture :
using Microsoft.AspNetCore.Components.Server.Circuits;
public sealed class ConnectionLoggingCircuitHandler(
ILogger<ConnectionLoggingCircuitHandler> logger) : CircuitHandler
{
public override Task OnCircuitOpenedAsync(
Circuit circuit,
CancellationToken cancellationToken)
{
logger.LogInformation(
"Circuit {CircuitId} opened",
circuit.Id);
return Task.CompletedTask;
}
public override Task OnConnectionDownAsync(
Circuit circuit,
CancellationToken cancellationToken)
{
logger.LogWarning(
"Connection lost for circuit {CircuitId}",
circuit.Id);
return Task.CompletedTask;
}
public override Task OnConnectionUpAsync(
Circuit circuit,
CancellationToken cancellationToken)
{
logger.LogInformation(
"Connection restored for circuit {CircuitId}",
circuit.Id);
return Task.CompletedTask;
}
public override Task OnCircuitClosedAsync(
Circuit circuit,
CancellationToken cancellationToken)
{
logger.LogInformation(
"Circuit {CircuitId} closed",
circuit.Id);
return Task.CompletedTask;
}
}
Puis je l'enregistre dans le conteneur d'injection de dépendances :
builder.Services.AddScoped<CircuitHandler, ConnectionLoggingCircuitHandler>();
Je peux ensuite envoyer ces logs vers mon outil d'observabilité et suivre plusieurs indicateurs :
- le nombre de pertes de connexion par période ;
- le nombre de connexions restaurées ;
- la proportion de circuits qui finissent fermés après une coupure ;
- une hausse des incidents après un déploiement ou un changement réseau.
Je fais attention à ne pas journaliser les contenus saisis par l'utilisateur. L'identifiant technique du circuit suffit pour établir des mesures fiables, sans introduire de données métier ou personnelles dans les logs.
7. Ce que je fais lorsqu'une saisie utilisateur est importante
Une modal de reconnexion bien conçue limite l'incompréhension, mais elle ne garantit pas la conservation des données. Si le serveur redémarre ou si le circuit expire, l'état en mémoire peut avoir disparu.
Pour un formulaire court, j'accepte généralement ce risque. Pour un parcours long ou critique, je combine trois niveaux :
| Besoin | Solution que j'applique |
|---|---|
| Coupure réseau très courte | Reconnexion automatique au circuit conservé. |
| Onglet suspendu ou reprise temporaire | Persistance de l'état de circuit avec .NET 10 et [PersistentState]. |
| Brouillon qui doit survivre à un redémarrage ou plusieurs jours | Sauvegarde métier en base de données. |
Ainsi, je ne promets jamais à l'utilisateur que la reconnexion protège une donnée qui n'a jamais été sauvegardée. J'utilise la modal pour être transparent : tant que le circuit est retrouvé, le travail continue ; s'il est rejeté, je recharge et restaure uniquement ce que mon architecture a réellement enregistré.
8. Ma check-list de validation avant production
Avant de livrer une application Blazor Server avec une expérience de reconnexion personnalisée, je vérifie systématiquement les scénarios suivants :
- Je remplis un formulaire, coupe le réseau quelques secondes, puis vérifie que l'état
hidepermet de continuer sans perte. - Je maintiens la coupure jusqu'à l'état
failed, puis vérifie que le bouton « Réessayer » appelle bienBlazor.reconnect()côté navigateur. - Je redémarre l'application pendant qu'un écran est ouvert, puis vérifie que l'état
rejectedpropose clairement un rechargement. - Je confirme qu'aucun bouton de secours ne dépend d'un
@onclickserveur lorsque le circuit est perdu. - Je vérifie la durée de
DisconnectedCircuitRetentionPeriodpar rapport à l'usage réel. - Je mesure les logs de
CircuitHandlerdans l'environnement de recette. - Je contrôle qu'une saisie critique dispose d'un brouillon persistant et ne dépend pas uniquement du circuit.
- Je teste l'affichage mobile : la déconnexion arrive souvent lorsque l'appareil change de réseau ou sort de veille.
Conclusion
Gérer proprement les déconnexions Blazor Server ne consiste pas à masquer une bannière technique derrière un design plus joli. Je dois distinguer les états réels du circuit, conserver côté serveur ce qui peut raisonnablement être repris, donner une action correcte à l'utilisateur et mesurer ce qui se passe en production.
Avec la ReconnectModal de .NET 10, les classes components-reconnect-* et l'événement components-reconnect-state-changed, je dispose enfin d'un point d'intégration propre pour cette expérience. Le principe que je retiens est simple : une coupure temporaire doit rester discrète, une erreur réseau doit offrir une nouvelle tentative, et un circuit définitivement perdu doit conduire à un rechargement honnête, accompagné d'une vraie stratégie de sauvegarde lorsqu'une saisie est précieuse.
Aucun commentaire publié pour le moment.
Ajouter un commentaire