Dot.Blog

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

Utiliser une machine à état pour améliorer les ViewModels et les UI (version Xamarin.Forms)

Découpler la logique des états et transitions d’un ViewModel en faisant gérer les Commandes par une Machine à Etats Finis apporte un nouveau niveau d’abstraction aussi important que l’est MVVM lui-même. Êtes-vous prêt à gérer correctement le workflow de vos applications et en améliorer l’ergonomie ?

Nouvelle Version Xamarin.Forms

J’ai déjà développé ce sujet à propos de WPF il y a cinq ans… Le sujet est toujours aussi passionnant, toujours aussi révolutionnaire et toujours aussi peu populaire ! Beaucoup de raisons qui justifient de vous en présenter aujourd’hui une version rénovée, utilisant les dernières versions des outils de développement et le tout sous Xamarin.Forms ! Bonne lecture, en espérant que cette fois-ci l’idée fasse encore un autre bout de chemin vers son adoption…

Le problème des commandes

Avant d’entrer dans les détails posons le problème :

Gérer des commandes en MVVM est très simple. Une commande n’est qu’une propriété de type ICommand du ViewModel que l’on attache (binding) à une propriété de même type d’un objet d’UI (type bouton par exemple). Lorsque l’utilisateur déclenche la commande via l’objet d’UI la propriété ICommand du ViewModel déclenche sa méthode Execute().

Tout est simple et clair grâce à XAML, son binding et le pattern MVVM. Les librairies annexes comme MVVM Light simplifient encore plus la tâche et proposent en général un objet RelayCommand plus complet et plus pratique à utiliser que l’interface brute ICommand. L’implémentation d’origine a été créée par Josh Smith et on la retrouve dans de nombreuses librairies MVVM dont Prism sous ce nom ou un autre. Xamarin.Forms vient avec sa propre implémentation appellée Command, supportant ICommand de la même façon, il n’y a donc ici aucune toolbox supplémentaire à utiliser.

L’interface ICommand est fort simple et se compose de peu de choses. Dont une méthode CanExecute(Object).

Mais tout cela ne change rien à la véritable question qu’on doit se poser pour créer un logiciel bien programmé : comment gérer proprement le CanExecute ?

Je vois beaucoup de code dans lequel CanExecute n’est tout simplement pas géré… J’en conclue que beaucoup d’entre vous se diront “mais pourquoi se prend-t-il la tête avec ce ‘détail’ ?

Parce que ce n’est pas un détail justement… CanExecute reflète de façon assez fidèle le workflow du ViewModel, ce qui est possible de faire ou non à un moment donné, donc l’état de l’automate qu’est un logiciel

CanExecute est donc l’émanation programmatique de l’essence même de ce qu’est l’informatique : créer des automates, des machines de Turing. Comment cela pourrait-il être un simple “détail” ?

Plus important encore car c’est la partie visible pour l’utilisateur, la bonne gestion de CanExecute permet à l’UI de mieux le guider et donc d’améliorer l’ergonomie de vos applications.

Quand CanExecute n’est pas géré

Quand le développeur ne gère pas le CanExecute des commandes celles-ci peuvent donc s’exécuter à tout moment dans n’importe quel ordre quel que soit l’état de l’automate porte ouverte à tous les bogues. Autant dire que c’est le boxon pour être poli. A moins que le développeur ne barde en entrée les méthodes exécutées de tests divers et variés pour vérifier si le code peut ou non être exécuté justement. Ce n’est pas l’endroit où le faire, le CanExecute sert à cela… à moins de travailler par Aspect mais c’est un autre débat. Pour l’utilisateur c’est forcément le chaos à l’arrivée après un chemin semé de doutes… Sans parler des incohérences, plantages, et autres désynchronisation code/UI.

Quand CanExecute est géré

Bien ! Vous gérez le CanExecute de vos commandes je vous en félicite ! Mais de ce que je constate le plus souvent, même dans ce cas où le développeur à fait son job, c’est qu’hélas les tests de CanExecute sont soit trop succincts soit se compliquent tellement qu’on en perd le fil. Quant à comprendre ce qui est vraiment autorisé ou non dans tel ou tel état et comment maintenir ces tests … il faudrait lire en un bloc tous les tests de tous les CanExecute et resynthétiser tout cela en un schéma. D’ailleurs quels sont les états d’un ViewModel donné ? Il est rarissime qu’un développeur envisage les choses sous cet angle et des incohérences apparaissent vite entrainant une UX pénible pour l’utilisateur et des bogues difficiles à supprimer (sans parler de la dette technique qui augmente !). Le plus souvent on teste donc ce qui semble interdire une commande mais sans avoir fait l’effort de lister tous les états possibles du ViewModel… Bref même quand le CanExecute est géré on est loin de la perfection. Très loin.

Il ne s’agit bien entendu pas d’académisme. La recherche d’une perfection illusoire, d’une orthodoxie tatillonne. Non, il s’agit bien d’une préoccupation majeure, un programme est une machine à états et ne pas matérialiser clairement ces états c’est prendre de gros risques dont le plus grave de tous est de perdre l’utilisateur dans un dédale illogique. Tout comme MVVM et le découplage fort permettent de minimiser les erreurs et les mélanges entre UI et logique, entre services et utilisateurs de services, gérer les états d’un ViewModel minimise un risque majeur, celui de pondre du code spaghetti au cœur même de la logique de l’application !

Comment gérer correctement CanExecute ?

La critique est facile, l’art est plus difficile, c’est connu. Mais vous me connaissez, plus je critique plus ma réponse est longue ! Donc comment gérer correctement CanExecute ? En gérant bien les états de l’automate qu’est le ViewModel. Certes. Et comment ?

En utilisant un outil adapté à cette situation : une machine à états finis.

Il y a de nombreux avantages à utiliser cette stratégie, notamment celui de créer un découplage entre d’une part la logique des états possibles et d’autre part les transitions entre ceux-ci ainsi que le code du ViewModel. Cette abstraction supplémentaire simplifie le ViewModel, le rend plus maintenable tout en rendant lisible, cohérent et facilement modifiable le workflow. Le ViewModel devient l’emplacement du code des actions, la machine à états finis le mode d’emploi des commandes. L’UI déjà découplée par MVVM tire profit de ce nouveau niveau de découplage de façon automatique grâce à la machine à états finis qui gère automatiquement le CanExecute() qui guide l’utilisateur dans sa progression. Développeur et utilisateur sont gagnants…

Qu’est-ce qu’une machine à états finis ?

Une Finite State Machine, ou Machine à Etats Finis modélise le comportement séquentiel d’un objet. Une telle machine possède donc un nombre fini d’états et ne peut avoir qu’un seul état à la fois. Les machines hiérarchiques autorisent la notion de sous-états et sont très utilisées car plus conformes à la complexité des mécanismes modélisés en général (l’héritage de la configuration de l’état maitre par un sous-état simplifie son paramétrage et donc celui de toute la machine).

On notera qu’on appelle Automate Fini ce type de machine mais j’utilise ici la traduction littérale de l’anglais “Machine à états finis” dans le sens d’un code spécifique (une librairie) gérant les états d’une partie d’application afin de le différencier de l’Automate Fini qu’est l’application elle-même prise comme un tout. Cette nuance est artificielle et n’a de sens que dans le contexte de cet article afin de séparer les deux choses.

Une telle machine se définie donc par :

  • Des états généralement nommés par un adjectif indiquant une action en cours (“en train de”, le “ing” à la fin d’un verbe anglais), par exemple : Listening, Waiting, Editing, Printing, etc… (on peut le faire en français aussi bien que je conseille par cohérence avec le langage de rester toujours en anglais dans les noms de champs, méthodes etc, sinon on obtient un franglais abominable je trouve).
  • Des évènements qui sont des conditions extérieures comme une action utilisateur. Ces évènements sont eux aussi nommés avec des conventions diverses mais indiquant une action comme Save, Refresh ou Print ou autre dans cet esprit. Ces évènement peuvent déclencher une sortie de l’état courant vers un nouvel état dit final.

Au final l’association de l’état courant, de l’état final, de l’évènement et de l’action forme ce qu’on appelle une transition.

On retrouve cette notion de machine à états finis dans toute l’informatique puisqu’elle en est la base même et c’est sans surprise que le langage de modélisation UML propose un diagramme états-transitions dans le groupe des diagrammes comportementaux par exemple. Mais sans aller chercher loin, tout le monde connait les logigrammes ou ordinogrammes qui servent à décrire un algorithme à l’aide de boites, de flèches et de losanges de prise de décision. Sans être tout à fait identiques ces diagrammes “primitifs” représentent eux aussi en quelque sorte des états et des transitions d’un automate.

Stateless State Machine

Stateless, “sans état”, est un code créé par Nicholas Blumhardt qui permet facilement de coder une machine à états finis hiérarchique. Le nom est étrange mais on peut certainement l’expliquer par le fait que justement c’est cette machine qui gère les états et non plus le code principal de l’application qui devient alors “sans états”. Mais bon c’est une supposition et cela reste tiré par les cheveux ! Heureusement ce n’est pas le nom qui nous intéresse mais ce que fait “Stateless”.

Stateless se présente sous la forme de paquet Nuget à installer dans la partie Xamarin.Forms, pas besoin de le placer dans les parties natives puisqu’il s’agit de pur code C# destiné à l’App (pas de renderer par exemple). Les dernières versions sont des 5.x. On trouve le package dans Nuget :

image

Code minimaliste d’une machine à états finis

Pour mieux comprendre Stateless commençons par coder “à la main” (sans utiliser le paquet donc) une machine à états finis vraiment minimale. Elle ne connait que 2 états, On et Off, qu’un seul déclencheur dans la méthode Transition, la commande “on” (le reste étant compris comme la commande “off”) :

public class BasicStateMachine
    {
        public string State { get; private set; }

        public void Transition(string state)
        {
            switch (state)
            {
                case "on":
                    State = "ON";
                    OnSwitchedOn();
                    break;
                default:
                    State = "OFF";
                    OnSwitchedOff();
                    break;
            }
        }

        private void OnSwitchedOn()
        {
            // Do something
        }

        private void OnSwitchedOff()
        {
            // Stop doing something
        }
    }

Ce code est réellement direct et déjà contient les germes de la problématique des commandes, sujet principal de cet article ne l’oublions pas même si je suis obligé de vous présenter tout cela avant d’y arriver !

En effet, un choix a été fait ici dans l’implémentation, le switch bascule vers On dans le cas où la commande “on” est envoyée et renvoie vers l’état Off dans tous les autres cas. C’est un choix qu’il faut assumer. La machine doit-elle s’éteindre si je lui commande “toto” ou _uniquement_ si je lui commande “off”  ? Un autre aspect est négligé :  dans quel état se trouve l’automate au départ ?

Sans diagramme on se rend compte que même dans un cas aussi simple les problèmes se posent. Alors imaginez un peu ce qui se passe dans le code réel d’un ViewModel un peu complexe où cet aspect n’est pas même géré !!!

Schématisons un peu le code ci-dessus :

image

L’état initial n’est pas précisé, première erreur qui devient évidente quand on représente le schéma. L’état final aussi n’est pas précisé, la machine est donc à fonctionnement infini, il n’y pas de sortie, ce qui n’est pas une erreur mais un choix.

Enfin on note les deux états On et Off et les transitions. La première va de On vers Off et n’est pas définie, n’importe quel évènement fera donc basculer vers Off. La seconde va de Off à On et ne fonctionne que sur l’envoi de la commande “on” ce qui est très restrictif mais qui peut s’admettre.

Bref, une fois le diagramme posé on s’aperçoit des manques et incohérences.

On notera au passage que je parle de “commandes” pour passer d’un état à l’autre ce n’est pas une terminologie tout à fait exacte (on parle d’évènements ou de déclencheurs) mais cela est à mettre en rapport avec le sujet de l’article qui concerne les commandes qui sont bien à la source des évènements qui causent les transitions. Pour approfondir toute la rigueur de la notation UML des diagrammes d’états transitions je renvoie le lecteur intéressé à la nombreuse littérature technique sur le sujet.

Si on modélise la machine de façon plus sensée (en y réfléchissant donc) on voit les états, les transitions, ce qui donne plutôt cela :

image

Comme on visualise le diagramme on s’aperçoit qu’il faut bien un état initial, et ici il est à “Off”. Ensuite s’il faut une commande “on” pour passer à “On”, il faut aussi pour être rigoureux une commande “off” pour passer à “Off”. Ce n’est pas innocent comme choix…

Si n’importe quoi fait basculer à Off comme dans le code de départ, cela peut être voulu, une machine outil par exemple est dangereuse, dans la précipitation d’un accident l’opérateur peut par affolement appuyer n’importe où et il est préférable dans ce contexte que cela passe à “Off”, mieux vaut arrêter la machine pour rien qu’arracher un bras... Si maintenant c’est un système de circulation extracorporelle utilisé durant les pontages coronariens (qui remplace le cœur pendant qu’on le “démonte”), il est préférable que la commande “Off” soit au contraire hyper verrouillée avec même une clé à tourner, un code à saisir, etc.

il est vraiment essentiel de comprendre que toutes ces petites choses apparemment de l’ordre du détail ont en réalité un impact gigantesque sur l’application et sa raison d’être, sa finalité, ses contraintes. Et que tout cela OBLIGE a se poser des questions qui ne viendrait jamais en tête sans cet effort de formalisation !

Il n’y a pas d’état final dans l’exemple corrigé, c’est une machine à états finis mais à fonctionnement infini, pourquoi pas. On pourrait en revanche pousser plus loin le diagramme en indiquant un état “Error” si une commande différente de “on” ou “off” est envoyée ou bien représenter une boucle sur les états “On” et “Off” qui indiquerait que toute commande différente de celle attendue fait rester la machine dans son état courant.

Là encore rien n’est de l’ordre du détail, les questions soulevées par ces deux boites, ce point et ces deux flèches vont creuser très loin dans l’analyse fonctionnelle du logiciel, bien plus loin qu’on ne l’imagine au départ. Les diagrammes en général, même de SGBD sont des outils essentiels en cela que la présence ou non d’une flèche, d’une orientation, d’une cardinalité, aussi insignifiant que cela paraisse soulève des questions d’une grande profondeur dans la compréhension du mécanisme modélisé, questions qui sont rarement posées autrement.

Nous n’irons pas trop loin dans la modélisation de notre exemple et regardons simplement l’impact du diagramme corrigé succinctement sur le code :

public class BasicStateMachine
    {
        const string offState = "OFF";
        const string onState = "ON";
        private string state = offState;
        public string State { 
                              get { return state;} 
                              private set { state=value;}
                             }

        public void Transition(string state)
        {
            switch (state.ToUpper())
            {
                case "ON":
                    State = onState;
                    OnSwitchedOn();
                    break;
	        case "OFF":
                    State = offState;
                    OnSwitchedOff();
                    break;
            }
        }

        private void OnSwitchedOn()
        {
            // Do something
        }

        private void OnSwitchedOff()
        {
            // Stop doing something
        }
    }

La machine ainsi codée possède un état initial, “Off”, et ne bascule de l’un à l’autre que si les commandes “on” et “off”, peu importe la casse, sont envoyées. Tout autre commande laisse la machine dans son état courant.

Finalement nous avons un code plus rigoureux mais s’il fallait représenter une véritable machine à états finis et la faire évoluer il faut avouer que cela deviendrait vite fastidieux avec une telle programmation “manuelle”.

Stateless pour coder la machine à états

Avec Stateless la même machine se codera de la façon suivante :

public class StatelessStateMachine
    {
        private readonly StateMachine stateMachine;

        public enum Trigger
        {
            TurnOn,
            TurnOff
        }

        public enum State
        {
            On,
            Off
        }

        public State Current => stateMachine.State;

        public StatelessStateMachine()
        {
            stateMachine = new StateMachine(State.Off);
            stateMachine.Configure(State.Off)
                .Permit(Trigger.TurnOn, State.On)
                .OnEntry(OnSwitchedOff);

            stateMachine.Configure(State.On)
                .Permit(Trigger.TurnOff, State.Off)
                .OnEntry(OnSwitchedOn);
        }

        public bool Transition(Trigger trigger)
        {
            if (!stateMachine.CanFire(trigger))
                return false;

            stateMachine.Fire(trigger);

            return true;
        }
        private void OnSwitchedOn()
        {
            // Do something clever
        }

        private void OnSwitchedOff()
        {
            // Stop doing something clever
        }

    }

C’est un peu long pour si peu vous direz-vous et c’est normal, notre machine est tellement bête et simple que le code pour l’exprimer est forcément long par rapport à son utilité réelle, ce n’est qu’une sorte de Hello Word pour Stateless…

Mais si vous lisez attentivement ce code vous vous apercevrez rapidement qu’il est bien plus clair et bien plus structuré. Et surtout qu’il est bien plus souple, chaque état est défini, chaque évènement aussi, et l’API de type “fluent” qui permet de définir tout cela est aussi simple que puissante.

Modifier un état, une transition, ajouter des actions d’entrées et de sorties à une transition, etc, tout cela devient limpide.

Stateless utilise des méthodes génériques et le type des états ou des triggers est totalement libre. Ici on utilise des énumérations mais des objets plus complexes sont utilisables si cela le demande.

Et le rapport avec les commandes en MVVM ?

C’est en fait tout le sujet de l’article.

Maintenant vous avez une idée de ce qu’est une machine à états finis et de ce qu’est la librairie Stateless et comment elle s’utilise (au moins vous en avez une idée).

C’était un préambule nécessaire. Le “véritable” article commence ici.

“La continuité c’est maintenant !” (enfin un slogan politique qui ne ment pas !).

Retour à la problématique des commandes

Revenons sur le problème que pose les commandes. On a bien compris qu’elles se fondent sur l’interface ICommand (même si on utilise un RelayCommand ou une Command) et que les méthodes principales sont Execute() qui exécute l’action, CanExecute() qui teste si la commande peut s’exécuter et CanExecuteChanged() un évènement qui permet à l’UI de savoir si CanExecute() à changé de valeur.

L’intérêt d’une classe comme RelayCommand ou Command est de proposer non pas une interface qu’il faut implémenter à chaque fois mais une classe dont on peut créer des instances immédiatement ce qui est bien plus pratique. Autre apport de RelayCommand / Command, la méthode RaiseCanExecuteChanged() qui déclenche l’évènement CanExecuteChanged(). En effet, si les possibilités d’exécution changent et bien qu’il existe un évènement ad hoc auquel s’abonner dans ICommand rien ne permet dans cette interface de forcer cet évènement et donc de prévenir l’UI que “l’exécutabilité” de la commande vient de changer.

Je n’irai pas trop loin sur ce point bien qu’essentiel car j’ai déjà écrit de très nombreux articles et livres sur MVVM et notamment sur la façon de gérer les commandes. Le lecteur intéressé saura retrouver tout cela sur Dot.Blog et dans la collection “All Dot.Blog” j’en suis certain.

Comme toujours l’enfer se loge dans les fameux détails. Le vrai chalenge n’est pas dans l’exécution de la commande, le code derrière Execute(), mais dans le fait de s’assurer qu’il s’exécute quand le ViewModel est dans un état valide pour l’action donnée.

Typiquement le code du CanExecute(), quand il est géré, est implémenté sous la forme d’expressions conditionnelles qui utilisent des variables locales de type “isLoading” “isPrinting” “CanDoxxx” “objectMachin!=null” etc…

Chaque nouvel état nécessite de coder des conditions supplémentaires qui nécessitent une évaluation, de nouvelles variables locales, etc, ce qui très rapidement devient d’une complexité impossible à maitriser.

Sous XAML on dispose d’un outil qui permet de gérer les états visuels, le Visual State Manager, c’est une aide précieuse pour clarifier les choses. Mais si l’UI est découplée du code, ses états reposent sur ceux du code. Il ne peut y avoir une représentation de l’état “attente” dans le visuel XAML s’il n’y a pas un état correspondant dans le ViewModel. Tout tient donc sur la bonne gestion des états de la machine à états finis qui permet de construire un code solide et cohérent aussi bien en C# que côté UI en XAML. Une raison de plus d’adopter l’approche que je vous propose dans le présent article !

Le rôle de la machine à états finis

La machine à états finis va permettre de modéliser tous les états du ViewModel, donc ce qui est autorisé ou non de faire selon l’état en cours.

En construisant des commandes à partir du mécanisme de la machine et en laissant cette dernière décider des actions et de la gestion du CanExecute il sera même possible d’automatiser les réactions de l’UI puisque les objets supportant ICommand savent réagir au CanExecute en modifiant leur aspect visuel (qui peut de plus être retravaillé grâce à la grande souplesse de XAML, Xamarin.Forms n’implémentant quelque chose que pour le Button à l’heure actuelle).

On créé ainsi un nouveau découpage intéressant : l’UI d’une part dont le rôle ne change pas, le ViewModel qui devient un magasin de code (les actions) et d’autre part la machine à états finis qui joue le chef d’orchestre afin de garantir la cohérence de l’état du ViewModel et de l’UI.

Un exemple simple

Pour tenter d’être concret nous allons partir d’un exemple simple. Une pseudo gestion du personnel qui permet de faire des recherches, d’afficher la liste des personnes filtrées et bien entendu la modification d’une fiche sélectionnée. Pour rendre tout cela encore plus évident et éviter d’avoir trop de code parasite le mode de recherche se limitera à faire un refresh de la liste et simulera un temps d’attente (pour voir l’état visuel de l’application dans son mode “busy”).

Afin de comprendre les changements d’états et bien que cela sera absent d’une application réelle, nous allons adjoindre l’affichage de l’état courant ainsi qu’une liste qui va se remplir au fur et à mesure de tous les états qui se succèdent.

Il serait un peu stupide de recoder un nouvel exemple puisque les principes sont identiques, je renvoie ainsi le lecteur à l’exemple WPF original.

Téléchargez directement le Projet WPF VS 2013 du Code Démo (pointe le répertoire partagé des exemples Dot.Blog, répertoire Dropbox, chercher le fichier DemoMachineEtats.7z).

Un bon exercice sera de le transformer en Xamarin.Forms, mais l’utilisation de la Datagrid qui n’existe pas de base ni à l’identique, le ruban supérieur qu’il faudra simuler, la mise en page forcément pas adaptée à un smartphone tout cela fait qu’il y aura du travail ! Le mieux serait de penser une nouvelle démo adaptée à un smartphone. Si cela pourrait me prendre plus de temps que d'écrire l'article, en revanche cela vous ferait un bon entraînement, que vous pourriez éventuellement partager avec nous ! Un courageux ou une courageuse pour se dévouer ?
Pour ceux qui ne veulent pas télécharger le code WPF et le faire tourner, voici un gif animé qui en fait une démonstration assez complète :

 

Conclusion

Il faut bien terminer un jour… J’ai passé trois jours sur l’article original et malgré la similitude je viens de passer tout un dimanche après-midi à créer cette version Xamarin.Forms, conclure prend donc ici toute sa signification !

J’espère que vous avez compris le principe et surtout les intérêts nombreux qu’il y a à suivre ce pattern de l’Automate Fini en l’appliquant à MVVM et XAML.

Non seulement les états du ViewModel deviennent clairs et maintenables, cohérents et inviolables, mais cette approche force à penser le workflow, les états et transitions du ViewModel très tôt dans le développement de celui-ci. C’est un gain énorme qui permet de lever des loups avant même de coder, de s’apercevoir que le cahier des charges ne prévoit rien dans telle ou telle situation par exemple. Car noyée dans une prose rébarbative une analyse cache souvent beaucoup de choses !

Le code exemple complet est téléchargeable en fin d’article, amusez-vous bien ! (car développer correctement avec les bonnes méthodes est un plaisir).

Le sujet est vaste alors voici quelques références intéressantes :

Tout d’abord Tarquin Vaughan-Scott à qui j’ai emprunté originellement une partie de l’article et du code de démo qu’il avait publié dans un article en 2014 dans MSDN qui m’avait séduit et dont je voulais absolument vous parler. Mieux vaut tard que jamais ! Et j’ai réussi à faire mieux que de vous en parler, en tout cas je l’espère, en écrivant ce long article qui enrichit beaucoup l’original et le rend plus accessible à ceux d’entre-vous qui ne sont pas des fans de l’anglais…

Cet article de Bob Nystrom (en anglais) qui présente assez bien le concept de machines à états.

Tom Anderson et sa rapide introduction à “Stateless” dont est issu le premier exemple de l’article.

On pourrait certainement encore allonger cette liste car le sujet est vaste dès qu’on parle de MVVM, de son système de commande et lorsqu’on y ajoute les automates finis ! Mais j’ajouterais forcément la collection All.Dot.Blog dont certains tomes sont particulièrement orientés XAML ou MVVM.

Bref, il y a de quoi lire. Et surtout de quoi réfléchir…

Stay Tuned !

blog comments powered by Disqus