Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Xamarin.Forms, Validation des données et Mvvm–Partie 2

Après l’exposé du mécanisme de validation en partie 1 nous allons voir comment mettre en œuvre tout cela par code C#, XAML et en exploitant les effets natifs Android, iOS et UWP…

Résumé

Je renvoie bien entendu le lecteur à la Partie 1 pour comprendre de quoi je parle et voir l’animation en fin d’article qui montre tout cela en action. C’est tellement mieux de savoir où on va…

Rappel du principe

Le but du jeu consiste à valider des données saisies par l’utilisateur et ce d’une façon générique réutilisable facilement tout en offrant un retour visuel clair à l’utilisateur pour éviter ses frustrations et le guider sans qu’il n’ait à lire un manuel de 300 pages. Cela convoque de nombreuses compétences tant en C# qu’en XAML et qu’en injection de code natif pour chaque plateforme si l’on veut faire les choses correctement sur le plan visuel.

Rappel du fonctionnement

Le coeur du système de validation que je vous ai montré est basé sur une classe un peu spéciale qui contient à la fois la donnée à la valider (et à exposer à l’utilisateur) et less règles de validation. Le schéma était le suivant :


Maintenant que vous avez lu ou relu la partie 1, nous sommes prêt pour discuter de la mise en œuvre du système de validation…

Allez, parce que c’est vous, la petite animation de la partie 1 qui nous rappelle à quoi sert tout cela :

INPC

Partons de la base, le support de INPC. Comme je le disais dans la partie 1 la plupart des toolkits MVVM proposent une classe de base qui offre le support de INPC, vous pouvez utiliser cette classe si cela vous convient et que créer une dépendance forte entre votre code et une librairie externe ne vous fait pas peur ! Si demain vous transportez votre système de validation dans un autre projet qui n’utilise pas le même toolkit MVVM vous serez obligé de revoir une partie de votre code.

C’est pour cela que le mécanisme de validation sera basé sur une classe séparée du toolkit MVVM, SimpleObservableObject donc voici le code :

using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace DataValidationDemo.Inpc
{
    public class SimpleObservableObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        protected virtual bool Set<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(field, value)) return false;
            field = value;
            // ReSharper disable once ExplicitCallerInfoArgument
            OnPropertyChanged(propertyName);
            return true;
        }

        public void RaisePropertyChanged([CallerMemberName] string property = null)
        {
            // ReSharper disable once ExplicitCallerInfoArgument
            OnPropertyChanged(property);
        }
    }
}


Comme on peut le constater c’est simple, mais pratique. La méthode Set<T> permet de modifier une propriété en testant la valeur précédente (pas de changement si égalité) tout en déclenchant INPC. Il n’est donc pas utile d’ajouter un appel à RaisePropertyChanged() sauf si on souhaite notifier une propriété en dehors de son setter.

ValidatableObject<T>

C’est le coeur du système de validation, et il n’est pas très gros :

using System.Collections.Generic;
using System.Linq;
using DataValidationDemo.Inpc;


namespace DataValidationDemo.Validation
{
    public class ValidatableObject<T> : SimpleObservableObject, IValidity
    {
        private List<string> errors = new List<string>();
        private T innerValue;
        private bool isValid=true;

        public ValidatableObject(bool autoValidation)
        {
            AutoValidation = autoValidation;
        }

        public List<IValidationRule<T>> Validations { get; } = new List<IValidationRule<T>>();

        public List<string> Errors
        {
            get => errors;
            set
            {
                if (Set(ref errors, value)) 
                    // ReSharper disable once ExplicitCallerInfoArgument
                    RaisePropertyChanged(nameof(FirstError));
            } 
        }

        public string FirstError => Errors.FirstOrDefault();

        public bool AutoValidation { get; set; }

        public T Value
        {
            get => innerValue;
            set
            {
                if (value.Equals(innerValue))
                {
                    var i = errors.Count;
                }
                if (Set(ref innerValue, value) && AutoValidation) Validate();
            }
        }

        public bool IsValid
        {
            get => isValid;
            set => Set(ref isValid, value);
        } 
        public bool Validate()
        {
            Errors.Clear();
            Errors = Validations.Where(v => !v.Check(Value))
                    .Select(v => v.ValidationMessage).ToList();
            IsValid = !Errors.Any();

            return IsValid;
        }
    }
}


Première constatation cette classe hérite de SimpleObservableObject donc elle participe au système INPC et elle peut ainsi être bindée à des éléments d’UI qui seront avertis des changements de valeur des propriétés. C’est un détail mais il est essentiel pour concevoir un système de validation dynamique et visuel.

La seconde chose qu’on remarque est le support de IValidity, cette interface est encore plus simple :

public interface IValidity
{
   bool IsValid { get; set; }
}

Elle permet d’étendre le découplage en permettant à un objet de type ValidatableObject d’être interrogé sur l’état de sa validité sans rien connaître d’autre. Selon les circonstances cela peut s’avérer très pratique et il ne coûte rien de l’ajouter puisque de toute façon il nous faudra implémenter la propriété IsValid qui jouera un jeu important au niveau de l’UI.

Enfin le troisième constat que nous pouvons faire est celui de la nature générique de la classe.

Pourquoi générique ? Car cette classe rappelez-vous va venir remplacer le type de toute propriété qui doit être validée. Or il est important de connaître le type de cette dernière. Et il ne semble plus question aujourd’hui d’écrire plusieurs classes, l’une pour supporter string, l’autre pour supporter double, etc, alors même que la généricité nous évite ce travail et cette répétition de code.

Ensuite nous trouvons les propriétés de ValidatableObject<T>, elles sont peu nombreuses mais ont toutes une importance cruciale.

Value par exemple est la donnée elle-même, celle qui serait utilisée directement comme propriété dans le ViewModel. C’est elle qui sera validée.

IsValid, déjà présentée, qui indique en permanence l’état de Value vis-à-vis des règles de validation. True, Value est valide, False, le contenu de Value ne répond pas aux règles de validation.

Validations est une liste de IValidationRule<T>, c’est à dire de règles qui permettront de valider le contenu de Value.

Errors, on le comprend facilement, est une liste d’erreurs, plus exactement de messages d’erreur. Lorsqu’une règle de validation est appliquée à Value si elle déclenche une erreur elle ajoute le message d’erreur qu’elle contient à cette liste. L’UI peut réagir aux changement de cette liste.

FirstError est une simplification de Errors qui ne retourne que la première erreur de la liste. Il est souvent peu réalisable d’afficher un ListView sous chaque champ pour afficher les “n” erreurs potentielles. Le plus souvent on ne peut afficher qu’une ligne, donc se limiter à la première erreur sera bien plus simple à gérer. Lorsque l’utilisateur aura corrigé sa saisie et que cette première erreur disparaitra, s’il en existe une seconde elle deviendra automatiquement FirstError à son tour et ainsi de suite. L’utilisateur suivra ainsi les consignes données par l’UI au fur et à mesure de ses corrections.

Il est tout à fait envisageable d’afficher Errors,  la liste complète des erreurs, tout dépendra du cas pratique précis et de la place disponible à l’écran ou même du temps qu’on désire passer au design XAML de la page (la liste peut apparaître uniquement dans le cas où il y a plus d’une erreur et uniquement lorsque le champ concerné possède le focus par exemple).

D’autres petits détails ont leur importance. Par exemple le fait de positionner IsValid à True dès l’initialisation de la classe. Cela est laissé à votre appréciation. Il faut brosser l’utilisateur dans le sens du poil, et psychologiquement il faut dire qu’arriver sur une fiche où tout est en rouge alors qu’on n’a pas commencé à saisir quoi que ce soit a quelque chose de violent, de négatif. En considérant que le champ est valide aucun avertissement ne sera affiché lorsque la page sera construite. Mais dès que l’utilisateur commencer sa saisie il sera averti de la première erreur qu’il commettra. C’est plus “soft”. Vous pouvez penser que cela comporte une faille, si l’utilisateur valide toute sa saisie sans rien avoir saisi du tout tous les champs seront considérés comme valides et le bogue n’est pas loin… Non, car nous verrons qu’avant de se terminer le ViewModel de la page forcera tous les champs à se valider pour savoir s’il peut ou non considérer l’ensemble de la saisie comme correct… Si on admet ce petit travail supplémentaire alors l’effet d’une fiche vierge sans erreur est tout à fait envisageable. D’ailleurs la principale erreur ici étant qu’un champ obligatoire reste vide. Or la validation forcée des champs en cas de validation de la page par l’utilisateur déclenchera les mécanismes visuels indiquant cette erreur.

Un détail sur lequel on pourrait donc parler longtemps… Mais passons à l’autre détail, la propriété AutoValidation qui doit être passée au constructeur.

Selon l’approche qu’on préférera et puisque tous les champs sont validés “de force” en sortie de la page, on pourrait se dire qu’il est inutile de valider chaque champ à chaque caractère tapé. Dans ce cas l’utilisateur découvrira ses erreurs à la fin de la saisie de toute façon. En passant AutoValidation à False nous autoriserons ce comportement.

Toutefois dans la majorité des cas il ne faut pas différer l’affichage des erreurs. Psychologie oblige là aussi. Rien n’est plus frustrant que d’avoir saisi toute une page de données et de recevoir un message d’erreur concernant le premier champ tout en haut et peut être hors de vue au moment de valider la page. Donc sauf cas exceptionnels les erreurs doivent être affichées immédiatement. C’est tout l’intérêt du système de validation proposé ici, sa réactivité et son rôle de guide pour l’utilisateur. Pour respecter cette dynamique AutoValidation sera ainsi laissé le plus souven tà True.

IValidationRule<in T>

Notre classe de validation et de transport de données (puisqu’elle contient à la fois la donnée et ses règles de validation) repose sur l’application de règles de validation à la donnée stockée “Value”. Les règles peuvent être de n’importe quel type, nous n’avons aucune raison de limiter leur héritage en en imposant un. C’est une excellente règle de développement…. Laissez toujours le choix si cela est possible au développeur '”final” (même si c’est vous) d’hériter ses classes de ce qu’il veut. L’héritage étant simple (non multiple) se servir de l’héritage bloque toute tentative d’hériter d’autre chose. Lorsqu’on conçoit certaines classes d’une application très liées aux aspects spécifiques de cette dernière l’héritage peut être un choix acceptable, mais lorsqu’on conçoit une classe très générique pouvant être utilisée potentiellement dans plusieurs applications il vaut mieux laisser la seule et unique possibilité d’hériter à la discrétion du développeur final. En choisissant d’utiliser une interface plutôt qu’une classe “de base” et puisque l’héritage multiple des interfaces est supporté, nous ne “grillons” pas la seule possibilité d’hériter qui existe.

C’est pourquoi puisque cela n’est pas obligatoire les règles de validation ne fixerons aucun héritage particulier en dehors du support de l’interface IvalidationRule<T>. A l’extrême certains futés pourrait se dire qu’une classe de type Person et une classe de type Car pourront chacune être une règle de validation pour leur propre type... Cela peut sembler être un avantage. Une classe définissant une entité, par exemple Person pourrait ainsi supporter IValidationRule<Person> ce qui permettrait pour valider une instance de Person de passer cette même instance comme règle de validation… Dans un tel cas toutefois la règle serait “complexe” puisqu’elle serait unique et devrait valider toute l’entité. Mais c’est concevable après tout. Sauf que…

Le sacro-saint principe du découplage doit s’entendre au sens le plus large possible. Et les bonnes pratiques de la Programmation Objet nous imposent la règle de “Single Reponsability” (ou SRP, Single Responsabilité Principle), la responsabilité unique. Une classe = une responsabilité. Ce principe de responsabilité unique impose donc de séparer les règles de validation des entités à valider. Et ce principe est tellement important qu’il est la première lettre du principe plus général appelé par son acronyme : SOLID. je renvoie ici le lecteur à mon article “Que savez-vous de S.O.L.I.D. ?” Une lecture saine et édifiante !

Bref, nous n’imposerons pas d’héritage, et nous ne pourrons pas éviter non plus les petits malins de faire du code spaghetti s’ils le veulent. Les lecteurs de Dot.Blog sont à l’abri de cette malédiction j’en suis convaincu !

Retournons à notre interface :

namespace DataValidationDemo.Validation
{
   public interface IValidationRule<in T>
   {
       string ValidationMessage { get; set; }
       bool Check(T value);
   }
}


Elle expose une méthode Check(T value) retournant True si la donnée passée est valide, False dans le cas contraire. Elle contient aussi une propriété ValidationMessage, une string qui sera fixée lors de la création de la règle. C’est ce message qui sera ajouté à la liste Errors de ValidatableObject et qui ainsi sera présenté à l’utilisateur. Le fait qu’il soit libre laisse la possibilité à l’application de traduire les messages au runtime par exemple.

Aucune disposition n’est prise en ce sens mais il est tout à fait possible de complexifier un peu les choses en laissant la possibilité d’utiliser une chaîne de format comme message. Dans ce cas il faudra modifier le code de ValidatableObject afin qu’il utilise String.Format en passant les bons paramètres. On peut penser à la valeur elle-même, mais elle est déjà sous les yeux de l’utilisateur… C’est pourquoi cette solution n’a pas été retenue ici. Mais dans d’autres contextes on pourrait explorer cette direction pour obtenir des messages plus précis tout en conservant la possibilité de les traduire en plusieurs langues.

Les plus attentifs auront remarqué le '”in” devant le T de IValidationRule<in T>. Il s’agit d’autoriser ici la contravariance parce qu’il n’y a pas de raison de se le refuser… L’octothorpe ça vous dit quelque chose ? Non ? … Alors lisez ce vieil article sur les nouveautés de C# 4.0 et vous saurez tout la covariance et la contravariance ainsi que sur l’octothorpe ! Un beau moyen de briller en société et de couper la chique à l’autre, là, machin truc, qui n’arrête pas de rouler des mécaniques à la machine à café !

Des exemples de règles

Dans notre application exemple nous simulons la saisie d’un login avec son mot de passe. Nous allons utiliser deux règles : l’une qui s’assure que la donnée n’est pas vide, cette règle sera utilisée par les deux champs, et une règle spécifique au champ Password qui s’assurera que ce dernier compte bien 8 caractères au minimum (c’est le cadre de l’exemple, les règles peuvent valider la donnée comme elles l’entendent).

Champ non null ni vide

Cette règle sera donc utilisée deux fois, avec deux instances différences, et deux messages différents (d’où l’intérêt d’un message librement fixé et non pas lié en dur à la règle). La classe de cette règle ne possède aucun héritage sauf celui de l’interface de validation :

namespace DataValidationDemo.Validation
{
    public class IsNotNullOrEmptyRule<T> : IValidationRule<T>
    {
        public string ValidationMessage { get; set; }

        public bool Check(T value)
        {
            if (value == null) return false;
            return !string.IsNullOrWhiteSpace(value as string);
        }
    }
}


Arrivé ici, il faut dire qu’il n’y a plus grand chose à coder… Surtout que tester une chaîne sur sa nullité ou sa “vidité” (abominable néologisme, pire que l’écriture inclusive !), .NET le fait déjà pour nous avec IsNullOrWhiteSpace.

Hériant de IValidationRule cette classe se contente d’implémenter le stricte nécessaire, la propriété ValidationMessage, et la méthode Check dont on ne commentera pas le code.

Toi ! Bon Mot de passe hugh !

C’est un peu la traduction en français du nom de la classe IsGoodPassword que nous allons voir ici… De l’anglais au rabais je m’en excuse.

namespace DataValidationDemo.Validation
{
    public class IsGoodPassword<T> : IValidationRule<T>
    {
        public string ValidationMessage { get; set; }

        public bool Check(T value)
        {
            if (value == null) return false;
            if (!(value is string)) return false;
            var v = value as string;
            if (string.IsNullOrWhiteSpace(v)) return false;
            if (v.Length < 8) return false;
            return true;
        }
    }
}


Que dire ici si ce n’est que c’est la même chose que précédemment, à la différence que le test compte les caractères et retourne False si le champ en contient moins de 8 ou qu’il est vide. Cela fait un peu double emploi avec la règle précédente mais “Au moins 8 caractères” oblige à tester aussi 0 caractère.

Conclusion de la partie 2

Et oui, il y aura une partie 3 ! je vous le disais en fin de partie 1, malgré la grande simplicité de tout cela il faut beaucoup expliquer, et même s’il n’y a rien de sorcier, il y a plein de petites choses qui comptent.

C’est long à lire, mais dîtes vous que c’est encore plus long à écrire !

Donc on se retrouve pour la prochaine partie, avec peut-être d’autres news plus courtes avant… de bonnes raisons de ….

Stay Tuned !

blog comments powered by Disqus