Dot.Blog

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

Comment une Machine à états finis peut améliorer vos ViewModels et vos UI ? (WPF Version)

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 ? …!

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.

Mais tout cela ne change rien à la véritable question d’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…

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. 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.

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 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 son code original ou bien sous une forme adaptée à .NET 4.0. On trouve les deux packages dans Nuget, Stateless-4.0 étant le plus récent et celui que nous utiliserons.

Code minimaliste d’une machine à états finis

Pour mieux comprendre Stateless commençons par coder “à la main” 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 la représente. 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.

Lorsqu’on modélise la même machine même d’instinct on fait mieux car 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. 

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, etc, 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 en l’état.

Au final 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.

Stateless pour coder une machine

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 { get { return _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.

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) 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 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, 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élisé 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).

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.

Regardons pour commencer l’animation ci-dessous, je la commenterai ensuite :

DemoWpfFSM

L’animation peut être vue en 100% en cliquant dessus, cela marche pour les images sur Dot.Blog et pour ce GIF aussi.

En gros nous avons une application qui propose une barre de commande contenant un bouton de recherche (limité ici à un resfresh simulant une attente pour l’accès aux données) et un bouton d’édition pour modifier une fiche salarié.

Sous cette barre se trouve une liste des employés. On peut sélectionné une ligne et cliquer sur le bouton d’édition.

Cela amène un dialogue de modification qui grise le fond. De même les commandes de refresh et d’édition sont grisées et interdites. Seule la commande de confirmation de saisie fonctionne (par simplification il n’y a pas de bouton annuler ici).

Pour les besoins de la démonstration j’ai ajouté dans la barre de commande un indicateur d’état courant de la machine à états. Il est directement bindé à la propriété State de la machine. J’ai aussi ajouté une liste à droite qui montre l’évolution dans le temps des états de la machine. Certains états sont évident car stables et longs (comme Start tant qu’on ne fait rien ou Selected tant qu’une fiche est sélectionnée) et d’autres plus fugaces comme SearchCompleted. Certains états comme Searching ou Editing entraînent des modifications majeures de l’UI, l’apparition du cadre de saisie pour Editing ou celle de la petite horloge animée pour matérialiser l’attente de Searching.

En réalité cette démonstration ne comporte aucun code pour gérer ces changements de l’UI qui sont liés directement à l’état CanExecute des commandes ou à l’état de la machine…

Pourtant tout est clair, sans faille possible car tout est codifié dans la machine à états !

Schématiser les états

Avant de coder la machine à états, plutôt la configurer puisque nous utilisons ici un code existant simulant une machine à états finis, il est absolument nécessaire de modéliser par un diagramme, même simple, les différents états, les évènements qu’il faut prendre en compte et les actions à entreprendre.

Je ne vais pas entrer dans le formalisme UML et sa rigueur, mais je vais utiliser un diagramme de type Etats-Transitions UML sans trop forcer sur l’académisme, l’essentiel étant de comprendre que ce schéma est indispensable même si vous le faite au crayon sur une nappe en papier au resto…

image

 

Les états de la machine sont représentés par des rectangles portant un nom blanc sur vert (Start, Searching, SearchCompleted…).

Les évènements ou triggers qui permettent de passer d’un état à l’autre sont matérialisés par des flèches directionnelles et sont accompagnées du nom de l’évènement (Search qui va de l’état Start à l’état Searching par exemple).

Les interventions de l’utilisateur au travers de l’UI sont matérialisées par un bonhomme vert pointant l’évènement concerné du diagramme. Il est accompagné du nom de la commande telle qu’elle est vue par lui (“Search Command” par exemple).

On notera que les évènements peuvent être déclenchés par l’utilisateur ou par le changement d’état d’autres automates dans l’application. Par exemple Search Succeed et Search Failed (réussite ou échec de la recherche) sont déclenchés par la partie du programme qui effectue la recherche des données.

Se rappeler : Le programme est un utilisateur comme les autres.

Comme je le disais on ne pinaillera pas sur la beauté académique du diagramme lui-même, la notation UML est plus rigoureuse et un peu plus difficile à lire que mon schéma pour celui qui ne connait pas sa syntaxe particulière. De même l’état Start n’est sémantiquement pas très futé, le démarrage est symbolisé par le cercle plein c’est lui le vrai “start”, il doit pointer un état considéré comme initial (auquel on a le droit de revenir), on aurait donc pu appeler cette état Idle (au repos) plutôt que Start. Mais ne nous encombrons pas de tout cela qui est totalement accessoire ici.

En revanche nous aurions pu aller plus loin pour représenter les sous-états, ceux qui héritent de leur parent. La syntaxe UML aurait encore plus compliqué la lisibilité mais tentons malgré tout, en pensée, de visualiser ces sous-états s’ils existent :

Par exemple l’état SearchCompleted, lorsque la recherche est terminée n’est qu’un sous-état de l’état Start (qu’on aurait pu appeler Idle, “au repos” ce qui prend tout son sens ici).

De même finalement Selected est un sous-état de SearchCompleted. Il ne peut y avoir de sélection (ou de non sélection) que si la recherche est terminée.

On doit représenter cette notion de sous-états dans le diagramme car le paramétrage de la machine est plus simple et il est plus fidèle à la réalité. Certains états ne peuvent exister que si la machine est passée par un état parent. Sortir de ces sous-états est généralement limité à ce que le parent autorise.

Paramétrer la machine à états finis

Comme je l’ai expliqué au début de cet article en présentant les machines à états finis nous n’avons heureusement pas à coder nous-mêmes la machine, nous allons utiliser un code tout fait, un package Nuget dont nous avons déjà vu comment il se programme.

Mais il reste à adapter tout cela à notre exemple WPF / MVVM.

Une fois le schéma établi coder les états et les triggers ou évènements n’est pas compliqué. On pourrait utiliser des objets complexes pour les représenter, ici nous choisirons le plus simple : des énumérations.

Ainsi états et triggers seront codés de cette façon :

public enum States
{
  Start, Searching, SearchComplete, Selected, NoSelection, Editing
}
public enum Triggers
{
  Search, SearchFailed, SearchSucceeded, Select, DeSelect, Edit, EndEdit
}

 

Une fois que les états et triggers sont posés il ne reste plus qu’à configurer la machine.

Celle-ci est créée comme héritant de StateMachine<State,Trigger> et elle supportera au passage INPC pour récupérer correctement les changements d’états quand ils interviennent (ce qui permet de construire la liste des états de la démo notamment).

Ce type dont nous allons hériter est bien entendu fourni par Stateless. Et comme je le disais états et triggers sont des types génériques qui nous permettent d’utiliser ce que nous voulons.

Le code complet de la machine utilisée dans la démo est ainsi :

public class StateMachine : StateMachine<States, Triggers>, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public StateMachine(Action searchAction)
            : base(States.Start)
        {
            Configure(States.Start)
              .Permit(Triggers.Search, States.Searching);

            Configure(States.Searching)
              .OnEntry(searchAction)
              .Permit(Triggers.SearchSucceeded, States.SearchComplete)
              .Permit(Triggers.SearchFailed, States.Start)
              .Ignore(Triggers.Select)
              .Ignore(Triggers.DeSelect);

            Configure(States.SearchComplete)
              .SubstateOf(States.Start)
              .Permit(Triggers.Select, States.Selected)
              .Permit(Triggers.DeSelect, States.NoSelection);

            Configure(States.Selected)
              .SubstateOf(States.SearchComplete)
              .Permit(Triggers.DeSelect, States.NoSelection)
              .Permit(Triggers.Edit, States.Editing)
              .Ignore(Triggers.Select);

            Configure(States.NoSelection)
              .SubstateOf(States.SearchComplete)
              .Permit(Triggers.Select, States.Selected)
              .Ignore(Triggers.DeSelect);

            Configure(States.Editing)
              .Permit(Triggers.EndEdit, States.Selected);

            OnTransitioned
              (
                t =>
                {
                    onPropertyChanged("State");
                    CommandManager.InvalidateRequerySuggested();
                }
              );

            //used to debug commands and UI components
            OnTransitioned
              (
                t => Debug.WriteLine
                  (
                    "State Machine transitioned from {0} -> {1} [{2}]",
                    t.Source, t.Destination, t.Trigger
                  )
              );
        }

        private void onPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }


    }

 

On note la présence de INPC qui n’est pas forcément indispensable ainsi que la surcharge de OnTransitioned qui est ajoutée pour lister les transitions dans la console de débogue. Toutefois ces “extensions” au système de paramétrage des états rend Stateless très souple et véritablement attrayant.

La programmation de l’automate passe par une API dite “fluent”, c’est à dire dont les méthodes retournent un objet modifié ce qui permet d’enchainer les appels de façon très proche d’une description fonctionnelle. La “vraie” syntaxe de LINQ marche de cette façon (celle qui utilise des méthodes et non celle qui ressemble à du SQL).

Les méthodes de la machine

Les principales méthodes disponibles sont :

  • Permit(TTrigger,TState) qui autorise un état à faire une transition vers l’état cible en passant par le trigger;
  • Ignore(Ttrigger) qui bloque une transition depuis l’état quand le trigger indiqué se déclenche;
  • SubstateOf(TState) qui permet de créer un lien enfant / parent entre un état celui indiqué;
  • OnEntry(Action) qui permet d’enregistrer une action exécutée à l’entrée de l’état;
  • OnExit(Action) qui dans l’autre sens définit une action exécutée juste avant la sortie de l’état.

 

La machine passe en erreur lorsqu’un trigger est déclenché dans un état non valide ou non configuré.

Prenons en détails la configuration de l’état Searching :

Configure(States.Searching)
  .OnEntry(searchAction)
  .Permit(Triggers.SearchSucceeded, States.SearchComplete)
  .Permit(Triggers.SearchFailed, States.Start)
  .Ignore(Triggers.Select)
  .Ignore(Triggers.DeSelect);

 

La méthode Configure(TState) indique le début de la configuration de l’état passé en paramètre, ici States.Searching. L’appel à OnEntry déclenche l’action “searchAction” qui accède à la base de données.

Dans cet état deux seules transitions sont possibles, elles sont matérialisées par les deux méthodes Permit. Le premier paramètre est le trigger, le second est l’état d’arrivée. Pour l’état Searching ici configuré cela signifie qu’il accepte les triggers SeachSucceeded et SearchFailed qui le font transiter respectivement vers SearchComplete ou vers l’état d’attente Start.

En revanche les deux méthodes Ignore() sont utilisées ici pour pallier un problème de la plupart des listes WPF qui déclenchent leur Select lorsque la liste est bindée à la source. Nous ne voulons pas de ces transitions et les interdisons. Plus exactement elles seront donc ignorées.

La machine propose aussi deux autres méthodes qui sont utilisées dans le ViewModel principalement :

  • void Fire(TTriger) qui déclenche la transition associée à l’état en cours, au trigger et à l’état d’arrivé configuré. C’est comme cela que le ViewModel pilote la machine à états.
  • bool CanFire(TTriger) qui retourne True si le trigger indiqué est autorisé dans l’état en cours.

 

Binder la machine aux commandes MVVM

La machine à états finis “Stateless” n’est pas conçue spécialement pour fonctionner avec MVVM ou n’importe quoi d’autre. C’est une machine à états finis, c’est tout. Elle peut servir à moult choses bien différentes. Il n’y a donc aucune relation “naturelle” entre “Stateless” et MVVM ou même avec le système de commandes ICommand de WPF ou WinRT. De même n'oubliez pas que les commandes ne sont ici qu'un prétexte pour parler tout cela, il n'y a pas que ICommand qui nous intéresse, la démo le montre (par la fonction Edit ou l'horloge d'attente), la machine est utile bien au-delà des commandes.

Pourtant dans une application suivant MVVM nous avons besoin de déclarer dans le ViewModel des commandes ICommand qui seront bindées à des objets de l’UI, soit directement s’ils le supportent, soit en utilisant l’astuce d’un Behavior EventToCommand qui transforme un évènement en déclencheur d’une commande ICommand.

Une telle commande possède donc au minimum un Execute(), cela n’est pas compliqué à fournir, et un CanExecute() dont nous avons vu toute l’importance.

Tout l’objet de cet article et de la technique démontrée est justement d’éviter d’écrire le code de CanExecute() pour en confier la gestion à une machine à états finis. Or “Stateless” n’est pas conçu pour cela “out of the box”.

Qu’à cela ne tienne ! Ecrivons une méthode d’extension qui permettra d’ajouter une machine la capacité de créer des commandes reliées à son fonctionnement interne :

public static ICommand CreateCommand<TState, TTrigger>(
  this StateMachine<TState, TTrigger> stateMachine, TTrigger trigger)
    {
      return new RelayCommand
        (
          () => stateMachine.Fire(trigger),
          () => stateMachine.CanFire(trigger)
        );
    }

 

Une nouvelle commande de type RelayCommand est créée en utilisant comme action le Fire() du trigger et le CanFire() de ce dernier. Nous obtenons bien une commande reliée à l’état interne de la machine.

Et c’est de cette façon que les commandes du ViewModel sont créées :

SearchCommand = StateMachine.CreateCommand(Triggers.Search);
EditCommand = StateMachine.CreateCommand(Triggers.Edit);
EndEditCommand = StateMachine.CreateCommand(Triggers.EndEdit);

 

Connexion à l’UI

Selon le modèle MVVM le ViewModel expose les commandes (ci-dessus) et l’UI se binde sur celles-ci. C’est ce qui se passe dans la barre de commande de notre application démo :

<Button ToolTip="Search" VerticalAlignment="Center" 
  Style="{StaticResource ButtonStyle}"
  Command="{Binding SearchCommand}">
  <Image Source="Images\Search.png"></Image>
</Button>

 

Le lien est donc fait entre UI et Commande, et entre Commande et machine à états finis… Ce qui par transitivité relie l’UI à la machine à états donc.

De fait les boutons seront désactivés ou activés automatiquement suivant si l’état courant de la machine autorise ou non les triggers correspondants ! Zéro code ici, tout s’enchaine parfaitement.

Notre démo montre d’autres connexions entre UI et machine à états que les commandes. Par exemple vous avez du remarquer dans l’animation plus haut que lorsque le ViewModel est occupé à la recherche des données une petite horloge animée s’affiche à la place de la liste des résultats.

Ici pas de ICommand à connecter… Comment s’opère la magie ?

En ajoutant un convertisseur de valeur qui va traduire l’état courant de la machine en un booléen. Et pas n’importe comment, juste en passant à True si l’état est “busy” donc lorsque la machine est en mode recherche de données.

Une fois ce convertisseur créé on peut ajouter une couche à l’affichage de notre application, couche possédant la petite horloge d’attente et qui sera visible ou non selon l’état de la machine :

public class StateMachineVisibilityConverter : IValueConverter
  {
    public object Convert(object value, Type targetType,
      object parameter, CultureInfo culture)
    {
      string state = value != null ? 
        value.ToString() : String.Empty;
      string targetState = parameter.ToString();
      return state == targetState ? 
        Visibility.Visible : Visibility.Collapsed;
    }
 }

 

Le code du convertisseur est bien entendu un peu plus générique que ce que je viens d’en dire car cela serait dommage d’avoir à se répéter pour chaque état qu’on souhaiterait tester (DRY !). Il est donc conçu pour recevoir en paramètre l’état à tester. De même il ne retourne pas un booléen mais quelque chose de directement exploitable par WPF, une valeur de type Visibility.

L’horloge d’attente est ainsi bindée à l’état de la machine en XAML en utilisant le convertisseur avec le paramètre “Searching” qui est le nom de l’état qui doit afficher une attente :

<local:AnimatedGIFControl Visibility="{Binding StateMachine.State,
                          Converter={StaticResource StateMachineConverter},
                          ConverterParameter=Searching}"/>

 

“StateMachine” est le nom de la propriété de notre ViewModel qui expose la machine à états finis à l’extérieur. Et “State” est l’état courant d’une machine “Stateless”. Grâce au convertisseur et à son paramètre la visibilité de l’horloge est désormais directement et automatiquement contrôlé par l’état de recherche en cours de la machine !

Pas de code superflu, pas de répétition de tests éparpillés partout et difficiles à maintenir, rien d’autre qu’une machine à états finis qui sert de point central pour contrôler tous les états du ViewModel et leur incidence sur les états visuels de l’UI… C’est beau non ?

Le même principe est appliqué à une autre grille contenant un fond gris et la fenêtre d’édition… Quand l’état bascule à Editing, cette grille ayant l’avant-plan dans le Z-Order devient visible, cachant tout le reste et interdisant au passage le clic sur ce qui est en dessous… Les boutons de la barre de commande se grisent eux aussi mais cela par le biais de CanExecute() déjà relié à la machine.

Conclusion

Il faut bien terminer un jour… Cela fait trois jours que je suis sur cet article l’expression 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é 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.

Josh Smith et son RelayCommand repris par MVVM Light et d’autres et qui est utilisé dans le code de démo (qui ne fait usage d’aucun framework MVVM juste du nécessaire).

MSDN et son article de présentation du système de commande de WPF.

L’article de Laurent Bugnion, créateur de MVVM Light, présentant RelayCommand et EventToCommand.

Prism pour WPF et l’implémentation de MVVM avec ce framework.

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 comme je le dis souvent, “si je le connais, c’est que je l’ai lu quelque part”. Croire qu’on peut générer de la connaissance pure tout seul dans son coin est soi être mégalo soit inconscient de l’importance de la communication et du partage de la connaissance. Nous ne sommes rien sans le savoir des autres. Il n’y a qu’en faisant circuler le savoir qu’on peut aider à l’émergence du progrès… Il faudrait milles vies pour tout lire sur tout, lisez déjà Dot.Blog ça pourra vous servir Sourire

Et Stay Tuned !

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)

Faites des heureux, partagez l'article !
blog comments powered by Disqus