[new:10/07/2010]Silverlight 4 ajoute la gestion des commandes à certains objets comme les boutons ce qui simplifie la mise en œuvre de la pattern MVVM. Le lien entre Interface et ViewModel s’en trouve amélioré même si cela semble toujours un peu nébuleux pour le débutant. Il est vrai que programmer de la sorte impose de raisonner autrement. Nous allons le voir au travers d’un exemple.
Le but
Je ne vais pas me lancer dans un cours sur MVVM d’autant que j’ai déjà écrit un très long article sur la question il y a peu de temps sans compter les billets où j’y fais référence directement ou non. Le but de ce billet est plutôt de partir d’un cas particulier pour voir la mise en œuvre d’un besoin courant sous MVVM, les commandes. Après les visions globales, notamment celle de l’article évoqué, pénétrer par une petite porte et découvrir le labyrinthe est une approche complémentaire, parallèle, pouvant certainement éclairer d’une autre façon le sujet.
Structure simplifiée d’une application MVVM
Plutôt que des redites je renvoie ainsi le lecteur vers les billets et articles suivants pour qu’il puisse faire le point sur MVVM :
un résumé
Pour résumer ici, une application suivant la pattern Model-View-ViewModel est une application qui sépare très fortement l’interface et son design du code applicatif. Cette séparation s’effectue en écrivant d’une part une interface (Xaml), la Vue, et d’autre par un ViewModel (Modèle de Vue) qui est connecté à l’interface via le DataContext de l’élément le plus haut(le contrôle de base) de cette dernière.
Toutes les données nécessaires au fonctionnement de l’application sont placées dans un ou plusieurs “modèles”. Seuls les Modèles de Vue (ViewModel) ont accès aux modèles. Les Vues n’effectuent aucun traitement, elles ne font que consommer des données retournées par le Modèle de Vue qui leur est associé.
La pattern MVVM accepte qu’une Vue dialogue directement avec un Modèle (les données) s’il n’y a aucun traitement (par exemple une simple fiche listant des données). C’est un cas extrême car même dans une telle situation il a beaucoup de chances qu’il soit nécessaire d’avoir au moins un bouton de sélection, le déclenchement de l’affichage d’un détail, ou autre action qui nécessitera en réalité la présente d’un Modèle de Vue.
De même, en MVVM il n’est pas “interdit” de placer du code dans la Vue. Bien au contraire. Mais ce code ne doit être que du code de présentation (gestion des objets visuels, animations, etc).
Donc, la structure d’une application MVVM ce sont des Vues (des interfaces Xaml) reliées par leur propriété DataContext à des ViewModels (Modèles de Vue) se chargeant de tout le travail. Ces derniers puisent leurs données depuis Internet le plus souvent (web services, Ria services…) mais il peut aussi s’agir de données locales (images, sons, flux webcam…). Lorsque les données sont suffisamment complexes, les ViewModels ne les gèrent pas directement, ils les consomment depuis des Modèles qui implémentent les accès aux sources d’information.
Informations mais aussi commandes
A la limite, si MVVM ne concernait que les informations à afficher, cela serait certainement plus simple à comprendre. Faire un data binding n’est pas une chose nouvelle, cela existe depuis les Windows Forms dans les premières versions de .NET. Relier la valeur d’une propriété à une source de données est trivial. Cette source de données étant ce qu’on veut (une liste, voire une simple instance d’un objet).
Le “volet information” de MVVM reprend ainsi des recettes éprouvées. Seul le mécanisme de séparation entre la vue et son Modèle de Vue peut troubler un peu au départ.
Là où les choses deviennent un peu plus difficile à comprendre, c’est lorsqu’on prend conscience qu’une interface n’est pas qu’un tableau noir utilisant la craie provenant d’un ViewModel pour créer son affichage. Un tableau noir est passif, il reçoit l’information et l’affiche aux yeux de tous, mais sans aucune interactivité.
Plutôt qu’un tableau noir d’école, la Vue est un Tablet PC, un IPad : elle affiche des données mais l’utilisateur peut interagir avec ces dernières. L’interface contient ainsi des éléments permettant d’envoyer des commandes à l’application.
Une commande est, in fine, une action réalisée par du code. Comme le code applicatif ne peut être placé que dans le ViewModel, c’est donc lui qui contiendra les méthodes réalisant ces actions.
Si certaines actions peuvent être “muettes”, la grande majorité implique un changement d’état de l’automate application. Tout changement d’état doit être visualisé pour que l’utilisateur sache “où il en est”. Cela peut se matérialiser par un simple changement de couleur d’une Led passant du vert au rouge pour indiquer une erreur comme par l’affichage de nouvelles données, la recréation d’un graphique, voire un changement de page ou de site Web.
Les commandes et leurs traitements impliquent donc qu’il existe un moyen :
- de déclencher le code utile se trouvant dans le ViewModel depuis l’interface;
- de récupérer les changements d’état du ViewModel pour mettre l’interface en accord visuellement avec ces derniers.
Un exemple live
Le principe est simple mais la chaîne d’action pour y arriver en respectant MVVM n’est pas forcément évidente : Un rectangle ayant un bord rouge et, au départ, aucun remplissage.
Deux boutons, l’un permettant de remplir le rectangle (en noir) et l’autre permettant de le vider (intérieur transparent). Selon l’état, le bouton qui n’a pas de sens fonctionnel est disabled.
Testez par vous-mêmes (ça devrait être rapide) on se retrouve au paragraphe suivant !
[silverlight:source=/SLSamples/Mvvm1/SLmvvm01.xap;width=300;height=300]
L’interface ICommand
La solution de la gestion des commandes passe par la définition d’une interface, ICommand, qui représente une méthode pouvant être exécutée. Comme ICommand est un type comme un autre, il est possible de définir des variables de ce type. Je peux écrire “ICommand maCommande;” définissant ainsi une variable “maCommande” de type ICommand.
Si c’est une variable, elle peut être exposée comme n’importe quel autre champ via une propriété du ViewModel. Finalement il n’y a aucune différence avec un string ou un entier.
Et puisque le ViewModel peut exposer des commandes sous la forme de propriétés de type ICommand, la Vue qui est data bound au ViewModel peut consommer ces variables comme n’importe quelle autre.
Cela signifie que les objets d’interface pouvant déclencher une commande, comme le Bouton par exemple, exposent une propriété “Commande” de type ICommand.
C’est justement ce que Silverlight 4 fait.
ICommand
Pour être complet disons que l’interface ICommand définit peu de choses :
1: public interface ICommand
2: {
3: // Events
4: event EventHandler CanExecuteChanged;
5:
6: // Methods
7: bool CanExecute(object parameter);
8: void Execute(object parameter);
9: }
10:
11:
- CanExecute, qui permet de savoir si la commande peut être exécutée (selon l’état de l’application)
- CanExecuteChanged, un événement qui permet de savoir quand l’état de la commande change (ce qui permet par exemple de mettre un bouton Enabled/Disabled automatiquement)
- Execute, qui exécute l’action représentée par la commande.
Rien de bien complexe dans tout ça donc. Reste, pour chaque commande, à coder le nécessaire. On notera que sous WPF l’ensemble est un peu plus sophistiqué avec un CommandManager qui se charge entre autre de vérifier le CanExecute pour mettre l’interface sans cesse en accord avec l’état de l’application. Cela n’existe pas sous Silverlight et nous verrons comment, malgré tout, arriver à un résultat identique (dans l’exemple cela concerne l’état disabled des boutons).
Le Circuit d’une commande
La commande est ainsi définie dans le ViewModel comme une propriété de type ICommand. Dans le constructeur du ViewModel (généralement) le développeur initialise la valeur de ces propriétés un peu particulières. S’il s’agissait d’un entier on écrirait “maCommande = 12;” par exemple pour l’initialiser à “12”.
Mais comme ICommand est un type décrivant un code à exécuter, un delegate (ou plus exactement un System.Action<> ou un Predicate pour le CanExecute), ce n’est pas une valeur comme “12” que le programme fixera pour la variable maCommande mais tout simplement une Action, donc un bout de code. Ce bout de code peut être un méthode vers laquelle la commande pointera, un delegate, ou bien une expression lambda, ce qui est très souvent utilisé.
Nous disposons maintenant d’une propriété “maCommande” de type ICommand qui “pointe” vers une Action (un code), l’initialisation de la variable se faisant dans le constructeur du ViewModel. La propriété est bien entendu en mode read only c’est à dire qu’elle dispose d’un getter public mais pas d’un setter (ou bien ce dernier est private, ou la propriété utilise un backing field retourné par le getter et éventuellement modifié en interne).
Comme “maCommande” est publique, elle s’appellera plutôt “MaCommande”. Et comme il est préférable de différencier les commandes des autres propriétés (c’est plus lisible) on ajoute le suffixe '”Command” en général. “MaCommande” cela ne veut rien dire, alors devenons plus concret. Reprenons notre exemple live avec un Rectangle qui peut être soit plein soit vide. On exposera deux commandes “FillRectangleCommand” et “EmptyRectangleCommand”.
Une commande étant souvent visualisée par un bouton ou autre élément de ce type, on a pour habitude aussi de déclarer pour chacune une propriété string qui retourne le texte à afficher (texte qui pourra de plus être facilement localisé dans le ViewModel).
Ici nous aurons ainsi 4 propriétés, 2 commandes et 2 textes de commande :
- FillRectangleCommand, EmptyRectangleCommand
- FillRectangleCommandText, EmptyRectangleCommandText
le suffixe “Text” se rajoute à “Command”. Il est ainsi très facile de voir qui est la commande et qui est le texte du nom de la commande. Cela sera bien pratique lors de l’établissement du data binding.
1: public RelayCommand EmptySquareCommand { get; private set; }
2: public string EmptySquareCommandText { get { return "Empty Square"; } }
3: public RelayCommand FillSquareCommand { get; private set; }
4: public string FillSquareCommandText { get { return "Fill Square"; } }
Vous noterez que les commandes sont déclarées de type RelayCommand et non ICommand. Cela est lié au fait que l’exemple utilise la librairie MVVM Light. RelayCommand supporte ICommand bien entendu et pour ce qui nous intéresse ici on pourra considérer que cela ne fait aucune différence.
Bref. Nous disposons d’un ViewModel exposant des commandes et des textes. Ce ViewModel est lié à la Vue par le DataContext de cette dernière.
On notera au passage que le lien Vue –> Modèle de Vue ne s’effectue pas directement sous MVVM Light, ce qui est une bonne chose et créé un nouveau niveau de découplage. Il existe un ServiceLocator ayant pour propriétés l’ensemble des ViewModels existants. La Vue se lie ainsi à son ViewModel en passant par la propriété ad hoc du Service Locator. L’implémentation est triviale (vous le verrez dans le code source du projet). De fait, le code Xaml de l’application est celui-ci :
1: <UserControl x:Class="SLmvvm01.MainPage"
8: . . .
9: DataContext="{Binding Main, Source={StaticResource Locator}}">
Le Service Locator s’appelle Locator, la fiche de l’exemple fait pointer son DataContext sur la propriété Main de ce Locator, propriété qui retourne l’instance du ViewModel associé à cette page.
Il est donc maintenant possible d’établir des data binding entre les éléments de la Vue, des boutons par exemple, et les propriétés du ViewModel.
Le titre de l’application est ainsi fourni par le ViewModel et n’est pas une chaîne fixée dans l’interface. Son code Xaml est le suivant (en supprimant le code de présentation):
1: <TextBlock Text="{Binding Title}" ... />
“Title” est une propriété du ViewModel associé à la Vue.
Pour les boutons il y a deux bindings, un pour le texte, l’autre pour l’action :
1: <Button Content="{Binding Path=EmptySquareCommandText}"
2: Command="{Binding Path=EmptySquareCommand}" />
3: <Button Content="{Binding Path=FillSquareCommandText}"
4: Command="{Binding Path=FillSquareCommand}" />
Le circuit commence à se former : la Vue expose un bouton, celui-ci voit sa propriété Command reliée par data binding à la commande xxx exposée par le ViewModel. Donc un clic sur le bouton exécutera le code se trouvant dans le ViewModel. Parfait !
Ci-dessous on voit comment, sous VS2010, le binding entre l’un des boutons et sa commande dans le ViewModel s’effectue facilement (notammant grâce au Service Locator qui instancie le ViewModel) :
Oui mais il manque le circuit retour, le nécessaire feedback : l’action change l’état de l’application et l’interface doit refléter ce changement !
Comment marche ce circuit de feedback ?
Feedback des commandes
Il y a plusieurs options, mais les deux plus utilisées sont les suivantes :
- le data binding lui-même
- un système de messages
Le data binding peut largement suffire à assurer ce feedback entre le ViewModel et sa Vue. Poursuivons l’exemple du rectangle qui peut être vide ou plein. Il suffit que sa propriété “Fill” soit data bound à une propriété “SquareFillColor” de type SolidColorBrush du ViewModel et l’affaire est jouée ! Enfin presque… Il ne faut pas oublier dans le code des commandes de lever un PropertyChanged de SquareFillColor.
Dès lors, quand on clic sur le bouton “remplir rectangle”, cela invoque la commande correspondante se trouvant dans le ViewModel. Cette commande modifie la propriété SquareFillColor et, d’une façon ou d’une autre (directement ou indirectement) déclenche l’événement PropertyChanged de cette dernière. Comme l’objet Rectangle de la Vue a sa propriété Fill reliée par data binding à SquareFillColor du ViewModel, tout changement de cette dernière (repéré par le ProperyChanged) mettra automatiquement à jour l’affichage… Magique!
1: <Rectangle Height="66" Stroke="Red" StrokeThickness="3" Width="87"
2: RadiusX="5" RadiusY="5"
3: Fill="{Binding Path=SquareFillColor}" />
On voit ci-dessus comment la propriété Fill du Rectangle est liée à la propriété SquareFillColor du DataContext du UserControl (pointant, via la propriété Main du Locator, le ViewModel associé). Tout déclenchement du PropertyChanged de SquareFillColor entraînera la mise à jour de la propriété Fill du rectangle. Ce n’est pas du MVVM ici, juste du Binding…
Dans certains cas le changement d’état qu’entraîne l’action nécessite un traitement différent. Un simple data binding entre le Fill d’un rectangle et une propriété Brush n’est pas suffisant. On met alors en place un procédé de messagerie : au lieu d’un PropertyChanged, c’est un message particulier (par exemple “L’état de l’application vient de passer à xxx”) qui va être diffusé. Toutes les instances de toutes les classes participant au fonctionnement de l’application pourront s’abonner à cette messagerie, voire à ce message particulier et réagir en conséquence (mise à jour d’affichage si c’est une Vue, mise à jour de l’état d’un autre objet, etc) ce qui peut bien entendu déclencher d’autres PropertyChanged ou l’émission de d’autres messages… Les messages peuvent même transiter d’un ViewModel à un autre, être diffusés ou traités par les Modèles, etc. La gestion d’une messagerie apporte une grande souplesse et permet de régler de nombreux problèmes que nous ne voyons pas l’exemple du jour (par exemple l’affichage d’un message : déclenché par une action utilisateur, une commande, le ViewModel peut avoir à afficher un dialogue, ce qu’il ne peut pas – ne doit pas – faire, il faut ainsi qu’il demande cet affichage à un objet d’interface mais aussi qu’il puisse receueillir le résultat… un cas que je traiterais dans un prochain article sur MVVM Light).
Un tel système de message n’est pas implémenté directement sous Silverlight. Mais il existe des solutions (voir les billets cités en introduction) sous la forme de librairie plus ou moins grosses.
Personnellement, et après les avoir toutes essayées (les principales en tout cas) j’en suis arrivé à la conclusion que la seule qui soit vraiment abordable pour une majorité de développeur est MVVM Light de Laurent Bugnion. Quand je dis “abordable” cela ne veut pas dire que je considère les développeurs comme des idiots. Non, mais je sais à quel point la charge de travail qui pèse sur eux limite de façon drastique le temps qu’ils peuvent investir dans la formation à une librairie. Prism est géniale, très riche et répond à d’autres problématiques, mais Prism réclame beaucoup d’investissement. Et quand on maîtrise peu ou mal un framework quel qu’il soit, on fait fatalement des bêtises et on perd du temps.
Je suis donc partisan des petits frameworks qui font peu de choses et qui s’apprennent vite. On les maîtrise de bout en bout et on s’en sert bien. C’est une vision réaliste du monde donc, et non un manque de confiance en vos compétences !
MVVM Light
MVVM Light est une librairie d’un seul homme. Elle est donc simple et se concentre sur une chose : MVVM. Laurent Bugnion, un MVP aussi, est un développeur compétent plein de bonnes idées et surtout adepte, comme je le suis, des choses simples qui s’apprennent vite. Son framework est donc totalement dédié et uniquement dédié aux mécanismes permettant d’écrire facilement des applications MVVM sous WPF, Silverlight et Phone 7. Des templates sont fournis ainsi que des snippets pour aller encore plus vite.
Bien entendu, Laurent est un peu charrette comme nous tous… Il fait beaucoup de choses et si la librairie est bien écrite et évolue très régulièrement, c’est comme toujours la doc qui trinque un peu… Mais le code source est simple, lisible, et c’est comme cela qu’on apprend le mieux à quoi il sert : en l’étudiant ! Il existe aussi des billets ou articles épars mais il est vrai que pour un grand débutant l’approche sera un peu rude (surtout que tout est en anglais).
Je prépare d’ailleurs un article sur MVVM Light pour rendre tout cela plus abordable.
Pour l’instant vous pouvez bien entendu charger et installer MVVM Light (ce qui sera nécessaire pour faire tourner l’exemple) : http://www.galasoft.ch/mvvm/getstarted
L’initialisation des commandes
On en a parlé, mais on ne l’a pas vu ! Les commandes sont des propriétés de type RelayCommand (ICommand) qu’on initialise généralement dans le constructeur du ViewModel. Cela peut se faire de plusieurs façons mais les expressions Lambda simplifient et allègent le code. Voici l’initialisation des deux commandes de l’exemple :
1: EmptySquareCommand = new RelayCommand((Action)(() => IsSquareEmpty = true),
2: () => !isSquareEmpty);
3: FillSquareCommand = new RelayCommand((Action)(() => IsSquareEmpty = false),
4: () => isSquareEmpty);
Les commandes se bornent à basculer la propriété IsSquareEmpty, un booléen qui est défini comme suit :
1: private bool isSquareEmpty = true;
2: public bool IsSquareEmpty
3: {
4: get { return isSquareEmpty; }
5: set
6: {
7: isSquareEmpty = value;
8: RaisePropertyChanged("IsSquareEmpty");
9: }
10: }
On notera bien entendu l’appel à RaisePropertyChanged, une méthode de MVVM Light qui propage le changement d’une propriété comme PropertyChanged.
Nous disposons aussi d’une autre propriété, celle qui est liée au Fill du Rectangle :
1: public SolidColorBrush SquareFillColor
2: {
3: get { return isSquareEmpty ? null : new SolidColorBrush(Colors.Black); }
4: }
Normalement, IsSquareEmpty, dans son setter, devrait appeler le RaiseNotifyChanged de SquareFillColor. En effet, c’est cette dernière propriété qui est liée au Fill du Rectangle, pas le booléen que nous utilisons en interne dans le code… Même si ce booléen comporte bien un RaisePropertyChanged cela ne peut avertir l’interface que SquareFillColor vient de changer.
Dans un tel cas les PropertyChanged doivent se suivre, mais une petite astuce de MVVM Light nous en empêche. L’enfer étant pavé de bonnes intentions, MVVM Light, en mode Debug, vérifie que la chaîne de caractères passée dans le RaisePropertyChanged correspond bien au nom de la propriété en cours. C’est une sécurité vraiment géniale qui efface un peu l’aberration de la présence d’une chaîne de caractères non controlée dans un si beau langage fortement typé qu’est C#… Hélas cette astuce fait que si nous plaçons le second RaisePropertyChanged sous celui du booléen, MVVM Light lèvera une exception car le nom (le second) ne correspond pas à celui de la propriété en cours. Fâcheux dans ce cas précis.
Pour s’en sortir il faut définir dans le ViewModel un gestionnaire de son propre PropertyChanged, et dans le code de ce gestionnaire il faut vérifier si c’est la propriété booléenne qui vient de changer. Dans l’affirmative on effectue le RaisePropertyChanged manquant (sans erreur car nous ne somme pas dans le code de définition d’une property).
Le CommandManager manquant
Comme je l’i dit plus haut, WPF offre un CommandManager qui s’occupe d’exécuter les CanExecute de toutes les commandes pour s’assurer que les éléments d’interface connectés aux commandes passent bien en disabled / enabled selon le cas.
Or Silverlight n’offre pas cette classe. Il en existe des substituts sur CodePlex, mais ici nous ferons sans autre code tiers.
Il convient donc, quand nous savons qu’une action modifie la possibilité d’exécuter ou non une commande d’appeler pour la ou les commandes concernées leur méthode RaiseCanExecuteChanged, une méthode fournie justement par RelayCommand et qui n’existe pas dans l’interface ICommand.
Ainsi, lorsque IsSquareEmpty est modifiée nous prenons en charge dans un gestionnaire d’événement PropertyChanged du ViewModel l’ensemble des cas de figure :
1: void MainViewModel_PropertyChanged(object sender,
2: System.ComponentModel.PropertyChangedEventArgs e)
3: {
4: if (e.PropertyName == "IsSquareEmpty")
5: {
6: RaisePropertyChanged("SquareFillColor");
7: EmptySquareCommand.RaiseCanExecuteChanged();
8: FillSquareCommand.RaiseCanExecuteChanged();
9: }
10: }
Et voilà ! Lorsque la propriété IsSquareEmpty change, notre gestionnaire effectue les tâches suivantes :
- RaisePropertyChanged de la propriété SquareFillColor, ce qui entraîne la mise à jour de l’affichage dans la Vue (grâce au binding entre le Fill du rectangle et cette propriété)
- RaiseCanExecuteChanged est invoqué pour les deux commandes qui dépendent de l’état du booléen. Le code exécuté est celui défini par les expressions Lambda initialisée dans le constructeur du ViewModel. L’interface réagit aux événements CanExecuteChanged de chaque commande et les boutons se placent en mode enabled / disabled tous seuls.
Conclusion
Voir MVVM non plus de façon globale mais au travers d’un exemple simple et d’une problématique courante, la gestion des commandes, m’a semblé être une approche complémentaire aux articles et billets que j’ai déjà écrits sur la question.
Est-ce que cela vous aura aidé à mieux comprendre MVVM ? A vous de le dire !
En tout cas, comme je reparlerais en détail de MVVM Light dans quelques temps, si le sujet vous intéresse une seule solution :
Stay tuned !
Le source du projet (VS2010 + MVVM Light) : SLmvvm01.zip (12,58 kb)