Dans le développement d'applications d'entreprise modernes, concevoir un formulaire "Mobile-First" ne se limite pas à rendre des colonnes empilables. Pour moi, le formulaire parfait repose sur trois piliers UX essentiels : guider l'utilisateur visuellement sans le bloquer inutilement, sécuriser sa saisie contre les sorties accidentelles, et distinguer le bloquant (l'erreur) du facultatif (l'avertissement).
Voici, point par point, les grands principes que j'applique dans mes projets Blazor Server en m'appuyant sur la bibliothèque de validation FluentValidation, les styles de validation de Bootstrap 5.3 et une suite de composants dédiés.
Principe 1 : Différencier Erreurs et Warnings
Toutes les validations ne se valent pas. Si un nom de marque manquant doit bloquer l'enregistrement (Erreur), une description trop courte ou l'absence d'un lien externe ne devrait générer qu'une alerte (Avertissement) sans empêcher la sauvegarde.
flowchart TD
A[Soumission du formulaire] --> B[Exécuter le validateur]
B --> C{Présence d'erreurs critiques ?}
C -- Oui --> D[Bloquer la sauvegarde et afficher les erreurs en rouge]
C -- Non --> E{Présence de warnings ?}
E -- Oui --> F[Afficher les warnings en orange et autoriser la sauvegarde]
E -- Non --> G[Sauvegarde standard sans avertissement]En pratique avec FluentValidation
J'exploite la méthode .WithSeverity() de FluentValidation pour séparer ces deux mondes :
// RÈGLE CRITIQUE (Bloque la sauvegarde)
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Le nom de la marque est obligatoire.");
// SIMPLE AVERTISSEMENT (Permet la sauvegarde)
RuleFor(x => x.Description)
.NotEmpty().WithMessage("Conseil : Ajouter une description améliore votre référencement.")
.WithSeverity(Severity.Warning);
Principe 2 : Le cycle de vie de la Sauvegarde (Reset & Nettoyage)
La gestion de l'état du formulaire durant la sauvegarde est cruciale pour éviter les comportements incohérents. Il est capital de respecter un cycle strict : réinitialiser l'état au démarrage pour effacer les résidus des tentatives précédentes, puis nettoyer le formulaire à la fin si la sauvegarde réussit afin de désactiver le verrou de navigation (PreventLostDirty).
Voici le code typique que j'implémente pour ma méthode de sauvegarde :
private async Task SaveAsync()
{
// 1. RÉINITIALISATION AU DÉMARRAGE
_errorMessage = null;
_successText = null;
// On efface les erreurs et avertissements précédents et on repasse temporairement
// le formulaire à l'état non modifié (unmodified) pour préparer la validation serveur
_editForm.EditContext.Clear();
// 2. EXÉCUTION DE LA SAUVEGARDE
var result = await Mediator.Send(new SaveBrandRequest(EditedBrand));
// 3. GESTION DES ERREURS & WARNINGS SERVEUR
if (result.HasError)
{
// En cas d'échec ou d'alertes, on injecte les nouveaux messages dans l'EditContext
_editForm.EditContext.DisplayErrorsAndWarnings(result);
return; // La validation bloque le processus ici
}
// 4. NETTOYAGE FINAL EN CAS DE SUCCÈS
// On met à jour l'entité avec les données retournées par la base (ex: dates de mise à jour, ID)
EditedBrand.ApplySuccessPersistResult(result);
_successText = "Marque enregistrée avec succès.";
// Le formulaire est maintenant synchronisé et sauvegardé. On appelle Clear() :
// - Cela vide les messages de validation restants
// - Cela marque le formulaire comme non modifié (IsModified = false)
// - Cela libère le composant PreventLostDirty pour que l'utilisateur puisse quitter la page librement
_editForm.EditContext.Clear();
if (IsNew)
{
// Redirection sans redéclencher le verrou de navigation
Navigation.NavigateTo($"/admin/marques/{EditedBrand.Id}", false, true);
}
}
Principe 3 : Le retour visuel ciblé (SuperInputText & SuperValidationMessage)
Sur mobile, l'espace d'affichage est restreint. Il faut éviter les fenêtres modales ou les popups qui masquent le formulaire. L'information visuelle doit être localisée directement sur le contrôle concerné en respectant les guides de style de Bootstrap 5.3 validation.
J'utilise des composants de saisie personnalisés qui modifient dynamiquement leur contour selon l'état de l'validation :
- Pas de message : Style classique Bootstrap 5.3 (
form-control). - Erreur détectée : Contour rouge (
is-invalid) et message en rouge sous l'input. - Warning détecté : Contour orange (
is-warning) et message en orange sous l'input.
// Exemple de sélection de classe dans le composant
string GetInputCss()
{
if (messages.Any(m => !m.IsValidationWarning())) return "form-control is-invalid"; // Rouge
if (messages.Any()) return "form-control is-warning"; // Orange
return "form-control";
}
Sous le champ, le composant <SuperValidationMessage> affiche le texte d'erreur avec la bonne couleur, tirant parti de la structure des Floating Labels de Bootstrap 5.3 pour une accessibilité maximale sur écran tactile.
Principe 4 : La synthèse globale (SuperValidationSummary)
En plus du retour individuel sur chaque champ, l'utilisateur a besoin d'une vue d'ensemble, surtout sur les longs formulaires mobiles où certains champs en erreur peuvent être hors de l'écran (scrollés).
Le composant <SuperValidationSummary /> analyse l'état complet du formulaire et affiche un bandeau récapitulatif adapté :
- Si des erreurs bloquantes subsistent : "❌ X erreur(s) empêche(nt) la validation de ce formulaire." (Bandeau rouge)
- S'il n'y a que des alertes : "⚠️ Y avertissement(s) à vérifier dans ce formulaire." (Bandeau orange, mais le bouton "Enregistrer" reste actif)
Ce résumé dynamique évite à l'utilisateur de chercher sur toute la page pourquoi son clic sur "Enregistrer" n'a eu aucun effet.
Principe 5 : Sécuriser la saisie (PreventLostDirty)
Sur smartphone, un glissement de doigt vers la gauche ou un clic involontaire sur le bouton "Retour" physique peut détruire de longues minutes de rédaction.
Pour y pallier, le composant <PreventLostDirty EditForm="_editForm" /> est placé à l'intérieur du formulaire :
- Il s'abonne à l'événement
OnFieldChangedde l'EditContext. - Dès qu'une modification est détectée, le formulaire passe à l'état "Dirty" (
IsModified() == true). - Il active un
<NavigationLock>qui intercepte toute tentative de navigation.
sequenceDiagram
actor U as Utilisateur
participant F as Formulaire Blazor (PreventLostDirty)
participant N as NavigationLock
participant D as Boîte de Dialogue
U->>F: Modifie un champ du formulaire
Note over F: EditContext.IsModified() == true
F->>N: Active le verrouillage de navigation
U->>N: Tente de quitter l'écran (ex: retour arrière)
N->>D: Ouvre la confirmation ("Modifications en cours")
alt Option "Rester" choisie
U->>D: Clic "Rester"
D->>N: Bloque la navigation (l'utilisateur reste)
else Option "Quitter" choisie
U->>D: Clic "Quitter"
D->>N: Autorise la navigation (données perdues)
endPrincipe 6 : Les méthodes d'extension au format moderne .NET 10
Pour enrichir l'EditContext sans surcharger mon code de syntaxe verbeuse, j'utilise la nouvelle syntaxe d'extension C# 13/14 (standardisée pour .NET 10). Au lieu de répéter static et this EditContext source sur chaque méthode, je regroupe tout dans un bloc d'extension d'instance.
Voici comment je structure mes outils d'édition et de validation :
namespace StockAsso2.WebApp.Extensions;
public static class EditFormExtensions
{
public const string WARNING_MESSAGE_PREFIX = "Attention: ";
// Définition de l'extension de type sur EditContext (.NET 10)
extension(EditContext source)
{
// Réinitialise l'état de modification et efface les messages
public void Clear()
{
var validationMessageStore = new ValidationMessageStore(source);
validationMessageStore.Clear();
source.MarkAsUnmodified();
source.NotifyValidationStateChanged();
}
// Ajoute manuellement une erreur sur un champ spécifique
public void AddError(string fieldName, string errorMessage)
{
var validationMessageStore = new ValidationMessageStore(source);
validationMessageStore.Add(new FieldIdentifier(source.Model, fieldName), errorMessage);
source.NotifyValidationStateChanged();
}
// Répartit dynamiquement les erreurs et les avertissements du serveur
public void DisplayErrorsAndWarnings(CommandResult validationResult)
{
var validationMessageStore = new ValidationMessageStore(source);
// Gestion des erreurs critiques
foreach (var brokenRule in validationResult.ErrorBrokenRuleList)
{
foreach (var error in brokenRule.MessageList)
{
validationMessageStore.Add(ResolveFieldIdentifier(source.Model, brokenRule.PropertyName), error);
}
}
// Gestion des avertissements (préfixés pour identification par l'UI)
foreach (var brokenRule in validationResult.WarningBrokenRuleList)
{
foreach (var warning in brokenRule.MessageList)
{
validationMessageStore.Add(ResolveFieldIdentifier(source.Model, brokenRule.PropertyName), WARNING_MESSAGE_PREFIX + warning);
}
}
source.NotifyValidationStateChanged();
}
}
// Méthodes d'extension classiques pour le traitement des messages
public static bool IsValidationWarning(this string message) =>
message.StartsWith(WARNING_MESSAGE_PREFIX, StringComparison.Ordinal);
public static string GetDisplayValidationMessage(this string message) =>
message.IsValidationWarning() ? message[WARNING_MESSAGE_PREFIX.Length..] : message;
}
Pourquoi cette syntaxe .NET 10 est supérieure ?
- Clarté d'écriture : Le bloc
extension(EditContext source)définit la cible de l'extension une seule fois. Les méthodes internes s'écrivent comme si elles faisaient partie de la classeEditContext. - Propreté d'appel : Dans mon composant Blazor, j'appelle simplement
_editForm.EditContext.Clear()ou_editForm.EditContext.DisplayErrorsAndWarnings(result)de façon naturelle.
Conclusion : L'assemblage final
Dans ma page de formulaire (comme BrandEdit.razor), l'assemblage de ces principes s'écrit de manière extrêmement concise et lisible :
<EditForm Model="EditedBrand" @ref="_editForm" OnValidSubmit="SaveAsync">
<!-- 1. Sécurité de navigation -->
<PreventLostDirty EditForm="_editForm" />
<!-- 2. Validation immédiate côté client -->
<DataAnnotationsValidator />
<!-- 3. Synthèse globale -->
<SuperValidationSummary SuccessText="@_successText" />
<!-- 4. Saisie avec retours contextuels automatiques -->
<SuperInputText Label="Nom de la marque" @bind-Value="EditedBrand.Name" Required="true" />
<SuperInputTextArea Label="Description" @bind-Value="EditedBrand.Description" />
<button class="btn btn-primary" type="submit">Enregistrer</button>
</EditForm>
Note
J'ajoute le composant natif <DataAnnotationsValidator /> immédiatement sous l'activation du formulaire. Si mes propriétés de modèle possèdent des attributs de validation de base (comme [Required] ou [StringLength]), ce composant intercepte les saisies invalides directement dans le navigateur. Cela permet une réactivité instantanée pour l'utilisateur mobile et évite un aller-retour (roundtrip) réseau inutile vers le serveur.
Grâce à cette approche modulaire, j'offre une UX mobile-first haut de gamme : le code de chaque page reste épuré, le contrôle de navigation est automatisé, et l'utilisateur sait exactement ce qui est bloquant de ce qui est conseillé.
Aucun commentaire publié pour le moment.
Ajouter un commentaire