Dot.Blog

C#, XAML, Xamarin, UWP/Android/iOS

MVVM, Chronomètre et Illustrator

[new:15/02/2011]MVVM, j’en ai parlé souvent et à travers de longs articles à télécharger, mais qu’est le rapport entre MVVM, un chronomètre et Illustrator ? Aucun. Si ce n’est qu’une fois associés, les trois permettent de voir comment construire une application MVVM tout en démontrant la fonction d’importation Photoshop/Illustrator de Expression Blend et quelques autres avantages de ce logiciel incontournable. Let’s Go !

Voir le projet finalisé

Il est toujours important de voir le projet fini avant de le décortiquer, on comprend mieux où on va...

Cliquez sur le lien suivant et jouez avec le chronomètre et ses deux boutons (en haut : start / stop, à gauche : split /reset, comme sur un “vrai”) :

http://www.e-naxos.com/SLSamples/Chronos/TestPage.html

MVVM Light

J’ai présenté cette bibliothèque de code au sein d’un gros article (Cf. “appliquer la pattern MVVM avec MVVM Light”) dont je vous recommande la lecture si ce n’est déjà fait. Je ne donnerai pas ici de précisions sur ce toolkit pour éviter les redites. Mais le code démontré utilise MVVM Light. Il est donc temps de faire un crochet par l’article indiqué avant d’aller plus loin Sourire

Expression Blend

Expression Blend est un outil fantastique pour concevoir des applications Silverlight ou WPF. C’est l’un des rares EDI qui m’a autant enthousiasmé ces dernières années, en dehors de Visual Studio. Expression Blend n’est pas un gadget dont on peut se passer. Bien que VS ait intégré un designer pour Silverlight dans les dernières versions, il ne permet tout simplement pas de tout faire en XAML. De plus VS n’a pas été conçu globalement comme un outil pour le design artistique, c’est un IDE de codeur. VS reste donc a sa place et Blend ne peut le concurrencer sur ce point (même si on peut éditer du code avec Blend). Pour tout ce qui est mise en place du visuel d’une application, seul Blend est l’outil vraiment adapté.

Tout naturellement c’est dans Expression Blend que se fera l’essentiel de ce billet. Notamment l’importation Adobe Illustrator qui n’existe pas dans VS.

Vous pouvez télécharger une version d’essai ici : Microsoft Store Expression Blend

L’objet graphique du projet

On pourrait utiliser un graphique bitmap issu de Photoshop, mais un vectoriel de Illustrator sera généralement plus conforme à l’esprit XAML, vectoriel par nature. Avec un objet Illustrator on peut retravailler plus facilement le dessin, l’adapter, le modifier car il reste en vectoriel, là où un bitmap ne peut être corrigé sous Blend. Mais en fait cela ne change pas grand-chose du point de vue de la manipulation que je vais vous faire voir. En vous baladant sur le Web vous trouverez de nombreux sites offrant des dessins gratuits dans l’un ou l’autre de ces formats. Ici je vais utiliser un chronomètre que j’ai récupéré sur le site oneter (un fichier EPS que j’ai mis au format Illustrator “ai” pour l’importer sans problème). Pour les bitmaps il y a aussi le site “psdGraphics” qui propose des JPG ou des fichiers PSD de Photoshop. Il y a des tonnes de sites de ce genre, je vous laisse surfer...

Création du projet

Dans Expression Blend, donc, Fichier / Nouveau Projet. Là il faut avoir installé le toolkit MVVM Light car il faut choisir un projet de type “MvvmLight (SL4)” comme le montre l’écran ci-dessous:

image

Une fois le projet créé nous obtenons une base simple qui a le mérite d’avoir une page d’accueil et son ViewModel déjà créé et recensé dans le ViewModelLocator. Pour un projet d’une seule page il n’y a rien à ajouter, et cela tombe bien pour cet exemple...

Je vais ouvrir directement la page “MainPage.xaml” en édition, sachant que le template de projet a déjà lié son DataContext au ViewModel correspondant (ViewModel/MainViewModel.cs).

J’ai fais le nettoyage de la page (MVVM Light ajoute un TextBlock lié à une propriété du ViewModel pour démontrer le mécanisme, on peut tout enlever et ne conserver que le LayoutRoot puis ajouter les éléments dont on a réellement besoin).

L’importation du graphique

Comme je l’ai dit, il s’agit d’un graphique vectoriel récupéré sur le Web en format EPS puis transformé en “ai” Adobe Illustrator. Cela m’a permis de simplifier le dessin original et de lui enlever tout ce qui n’était pas vraiment nécessaire (comme une seconde copie du chronomètre avec d’autres couleurs). Bref, ce qui est important, c’est de disposer au départ d’un dessin, bitmap ou vecteur, provenant de Photoshop ou Illustrator.

Maintenant dans le menu Fichier, cliquons sur “Importer fichier Adobe Illustrator”. La boite de dialogue d’ouverture de fichiers est affichée, ne reste plus qu’à sélectionner celui qu’on désire importer et de cliquer sur Ok.

C’est tout...

Si le fichier ne contient qu’un seul layer, il n’y a pas de question à se poser. Dans le cas où le fichier comporte plusieurs layers, Blend affiche un dialogue intermédiaire permettant de choisir ceux qu’on souhaite importer. Par exemple, avec un fichier PSD (Photoshop) le dialogue ressemble à cela :

image

Sur le côté droit on voit la liste des layers, tous sélectionnés par défaut. Imaginons qu’on souhaite ici importer la petite sacoche, il est évident qu’on préfèrera supprimer les layers affichant le texte et la miniature en haut à droite. Cela est purement conjoncturel et dépend uniquement de la façon dont le graphiste a travaillé... D’où l’importance pour des projets clients de s’être bien mis d’accord avec lui sur la façon d’organiser ses dessins et sur l’éventuelle signification des différentes couches (layers).

Un seul fichier Photoshop ou Illustrator peut ainsi contenir plusieurs illustrations différentes ou des variantes d’une même illustration, l’essentiel est que les choses soient séparées sur des layers différents pour rendre l’importation rapide et facile...

Mais revenons à notre chronomètre.

La fichier source n’ayant qu’un seul layer, nous n’avons pas eu de dialogue intermédiaire et Blend a placé directement l’image vectorielle dans un groupe à l’intérieur de LayoutRoot :

image

L’arbre visuel est le suivant :

image

J’avais commencé à placer une grille, le groupe “stopwatch” (nom issu du fichier original) a été ajouté par Blend en dessous. Il va falloir replacer le tout dans la mise en page que j’avais prévue et éventuellement modifier quelques éléments importés.

Tout cela n’étant pas très important, je vous fais grâce de ces étapes.

Faire marcher le chronomètre

 

Etat des lieux

Jusque là, nous avons importer une image vectorielle directement depuis un fichier Illustrator et nous l’avons placé correctement dans notre page. C’est peu de chose, cela a été très rapide, mais tous ceux qui doivent travailler avec un infographiste pour créer leurs applications Silverlight et WPF auront compris à quel point cela est important... Blend a transformé le fichier “ai” en un groupe correctement nommé contenant des Path contenus dans un Canvas. C’est magique, le chronomètre est désormais un objet XAML constitué de chemins modifiables (forme, couleur, etc.).

Ajustements

Pour faire marcher le chronomètre nous allons avoir besoin d’un peu de code, mais avant tout il faut s’assurer que les “morceaux” du dessin que nous voulons manipuler sont bien accessibles, bien positionnés dans le Z-order, etc. Notamment il va falloir mettre l’aiguille “à zéro”. Comme elle constituée de plusieurs Paths qui ne sont ni nommés ni groupés (le graphiste n’aurait pas fait son travail correctement s’il s’agissait d’un travail effectué pour un projet Silverlight/WPF) nous allons devoir modifier tout cela. D’où, une fois encore l’intérêt de Expression Blend qui nous offre des outils vectoriels comme Expression Design ou Adobe Illustrator. Bien entendu Visual Studio ne propose pas de tels outils trop spécifiques au monde de l’infographie.

Un petit détail concernant l’aiguille : pour la mettre “à zéro” il suffit de lui appliquer une rotation. Certes. Mais il ne faut surtout pas oublier de modifier le centre de rotation de l’objet avant. Ici il faut faire coïncider “à l’œil” ce centre de rotation et le rond de l’aiguille qui simule son axe.

Comme il y a fort peu de chance pour que le dessin d’origine soit uniquement conçu avec des chiffres ronds en pixels (les outils de dessin vectoriels ne travaillent pas en pixels, et les graphistes ne font pas la petite fixette que nous faisons sur la beauté des puissances de deux !) il sera nécessaire de placer ce point “à l’œil”. Il faut donc l’avoir bon, mais surtout savoir se servir du zoom et ne pas hésiter à zoomer à 3000% si cela est nécessaire.

Idem pour la rotation qui va amener l’aiguille sur midi. Le faire à la main n’est pas très précis mais vous pouvez approcher la perfection en travaillant en deux temps : d’abord utiliser la palette de transformation de Blend et changer la rotation en bougeant la souris afin d’approcher au plus près le centrage désiré, puis, à la main, modifier l’angle indiqué en y ajoutant des décimales jusqu’à ce que tout soit ok (en ayant bien zoomé !). Par exemple dans ce projet, à -120° l’aiguille était trop à droite, et à -121° elle était trop à gauche... Le zoom étant à 1600% pour bien visualiser la zone. A la main et selon une approche de type recherche dichotomique (n’en parlez pas à l’infographiste Sourire) je suis arrivé rapidement sur la bonne position : –120,85%.

Le principe de mise en page

J’ai ici opté pour une mise en page des plus simples : le LayoutRoot de la page principal (et unique) est une grille en mode 100% automatique sur les deux axes. Elle remplira donc tout l’espace dédié au plugin Silverlight dans la page Web. Le chronomètre a été importé directement dans un Canvas, je l’ai laissé ainsi. En revanche j’ai correctement retaillé ce dernier et je l’ai paramétré pour qu’il soit centré sur les deux axes.

C’est très simple, et cela permettra que le chronomètre apparaissent centré quelle que soit la taille de la fenêtre du browser.

Le temps précis

Il manque l’affichage du temps précis écoulé. Notamment quand le chronomètre aura fait plus d’un tour il sera difficile de se rappeler exactement combien de fois l’aiguille aura tourné et encore moins de faire le calcul de tête (l’utilisateur n’est pas un informaticien !).

Pour ce faire j’ai ajouté un TextBlock sous l’aiguille. Il est de taille fixe, et c’est le texte qui est en mode centré. L’objet est placé convenablement dans l’arbre visuel pour qu’il soit sous l’aiguille, bien évidemment.

J’ai donné un nom à l’objet texte, c’est surtout pour le repérer facilement, nous allons voir plus loin qu’en MVVM aucune référence à ce texte n’est nécessaire et qu’il peut rester sans nom.

Des boutons qui marchent

Le chronomètre possède deux boutons, je vais essayer de faire simple pour cette démonstration... Le bouton du haut est le Start/Stop. Facile. Le bouton de gauche est le “Split/Reset”. Quand le chronomètre tourne (Start) cela permet de figer l’affichage sans arrêter le mécanisme (temps intermédiaire). Un second appui relâche l’aiguille qui se remet à compter normalement. Quand le chronomètre est à l’arrêt, le bouton Split/Reset ramène l’aiguille à zéro et efface le temps écoulé. Il s’agit d’un automate classique et très simple pour le lequel je ne vous ferai pas un diagramme UML...

En revanche, du point de vue graphique, le dessinateur n’avait certainement pas prévu qu’un jour quelqu’un se serve de son fichier pour faire une application Silverlight. De fait les boutons ne sont pas formés de “pièces” qui peuvent être facilement déplacées. De plus les deux boutons sont assez différents.

La solution va consister ici à simplifier le dessin et à transformer l’un des boutons en contrôle templaté puis à le dupliquer, chaque copie recevant une orientation convenable.

image

L’image ci-dessus montre la différence entre les deux boutons originaux. Il va falloir choisir lequel garder. Chacun règlera ce dilemme selon ses gouts...

Les boutons sont ici les éléments qui comptent le plus de Path ou presque. Toutes les petites rayures sont autant de chemins dessinés. Les sélectionner un à un pour les grouper ou les supprimer (ce que nous devons faire pour l’un et l’autre respectivement) peut être très compliqué malgré le zoom et les facilités de sélection de Blend. Je vous conseille plutôt l’astuce suivante : dans l’arbre visuel chaque objet est doté d’un œil, ce qui permet de le cacher ou de le montrer (uniquement en conception, aucun effet sur le rendu final), cachez tous les objets, un par un, jusqu’à ce que seuls restent les objets qui vous intéressent. On contrôle mieux visuellement ce qu’on fait, c’est plus clair, et il est plus facile ensuite de sélectionner les éléments visibles pour les grouper ou les supprimer.

j’ai choisi de supprimer le bouton de gauche et de conserver celui du haut. Aucune raison esthétique dans ce choix, juste de la logique : celui du haut est déjà dessiné “verticalement”. En faire une copie orientée autrement pour recréer le bouton de gauche sera facile. Partir du bouton de gauche obligerait d’abord à le “verticaliser” ce qui sera par force approximatif (même en zoomant et en le faisant bien). L’expérience parle, croyez moi, c’est plus facile en partant de celui qui est déjà bien vertical...

Il me reste donc uniquement le bouton supérieur. Le bouton de gauche a été totalement supprimé. Le bouton du haut Il a été groupé dans un Canvas. Le Canvas remplace la notion de “layer” sous Blend. N’hésitez jamais a regrouper des éléments de dessin dans des Canvas et à les nommer si le graphiste ne l’a pas fait, on s’y retrouve mieux.

Les pièces composant le bouton étant groupées, il me suffit maintenant de sélectionner le groupe (dans l’arbre visuel. C’est souvent plus simple de manipuler les objets dans celui-ci que directement dans l’espace de travail dès que la scène est complexe) puis de faire un clic droit et de choisir “make into control” (je ne sais pas comment cela a été traduit dans la version FR que je n’utilise pas mais vous devriez trouver !). Ce qui ouvre le dialogue suivant :

image

Je fais un “make into control” et non un “make into UserControl” car ce qui m’intéresse n’est pas d’inventer un contrôle mais bien de transformer mon bouton visuel en un bouton fonctionnel. Bien entendu la classe Button est la plus proche de mes besoins et c’est elle que je choisi.

Automatiquement Blend transforme mon Canvas en un Button qu’il place exactement au même endroit. Rien à faire donc... Visual Studio est bien incapable de telles prouesses malgré ses qualités énormes pour tout ce qui touche le code.

En fait cela va si vite qu’on peut se demander si quelque chose s’est passé. Mais à mieux y regarder on comprend que : d’une part le Canvas (ou le ou les objets sélectionnés) a été supprimé de l’arbre visuel, qu’un bouton a bien été créé à la place (on voit le texte du ContentPresenter en surimpression) et que Blend a immédiatement basculé en mode templating (l’arbre visuel l’indique et de nombreuses petites choses ont changé pour qui connait bien Blend).

En réalité aucun nouveau “composant” n’est créé, nous sommes juste en train de donner un nouveau look à la classe Button. Ce nouveau look, ce nouvel aspect c’est le dessin formé par tous les éléments que avons sélectionnés avant de faire “make into control”. Et tout cela n’est rien d’autre qu’un simple template pour la classe Button. Une ressource XAML qui est appliquée à une instance de Button, celle qui a remplacé le dessin original...

Travailler avec Blend est magique et agréable. Ceux qui prétendent pouvoir s’en passer ont certainement loupé quelque chose dans la notion de User Experience et dans l’obligation de créer des interfaces utilisateurs un peu différentes de celles qu’on faisait en Windows Forms. Peut-être arriverai-je, avec le temps, à en convaincre quelques uns !

A partir de maintenant nous sommes donc en mode templating sur un bouton. L’idée générale est de séparer proprement la partie supérieure de l’axe du bouton visuel afin de pouvoir faire bouger le capuchon lorsqu’on “appuiera” dessus (lorsqu’on cliquera dessus). Une fois le travail de séparation effectué c’est en jouant avec les différents états du Visual State Manager que nous donnerons l’illusion de ce “clic” un peu spécial.

image

le VSM sur l’état “Normal” et le bouton en position haute par défaut.

image

L’état “Pressed” est sélectionné, j’ai déplacé le capuchon vers le bas. On notera les petits réglages du VSM. Tous les états vers “Pressed” provoque un changement assez rapide, il ne faut pas donner l’impression que le logiciel est “mou”. J’utilise souvent un temps à zéro pour les boutons. Ici j’ai mis quelques centièmes de secondes pour simuler la course d’un bouton mécanique. Dans le sens “Pressed” vers tous les autres états la durée est légèrement supérieure, c’est visuellement plus agréable (mais toujours faire attention à la sensation de molesse !). Le tout est agrémenté d’un easout cubique.

Ajouter du son

Pour rendre le tout plus sympathique et moins plat il faudrait ajouter du son. Je dispose d’une énorme banque de données de sons divers (que j’utilise principalement pour la musique je compose) dans laquelle j’ai trouvé deux sons intéressants, l’un pour le clic du bouton et l’autre qui sera joué pour imiter le bruit du chronomètre qui tourne.

Le son est-ce important ? pour un musicien, certainement Sourire Mais dans le cadre d’un tel développement qu’est-ce que cela peut apporter ?

Tout est une question de dosage et d’à propos. Un léger bruit de clic quand on appuie sur les boutons ajoutera du réalisme et plongera l’utilisateur plus facilement dans notre univers virtuel. Ce n’est pas intempestif, ça reste léger, ponctuel et on choisira un son assez doux (pas un bip électronique qui casse les oreilles...).

Le second son que je vais utiliser est la simulation du “tic tac”. Est-ce utile ? Je pourrais reprendre le même argument : cela plonge l’utilisateur dans l’univers virtuel du logiciel autrement que par la vue, en associant l’ouïe qui n’est sollicitée que dans les jeux en fait (univers conçus pour être prenant justement). Alors attention, notre application n’est pas un méga jeu, c’est une application utile. Mais utiliser les mêmes astuces que les jeux qui savent captiver l’utilisateur n’est pas un pêché... Et puis ici notre chronomètre simule le fonctionnement d’un véritable objet. Par exemple lorsque l’utilisateur figera l’affichage (temps intermédiaire) avec le bouton de gauche, comment faire sentir que le chronomètre n’est pas “en panne”, pas “arrêté” ? On pourrait ajouter un message (beurk !) ou une animation... Avez-vous déjà vu une animation sur un chronomètre mécanique ? (en fait oui, des petits mouvements de balancier en général). Mais notre modèle n’a pas été dessiné ainsi. Alors, que peut-il y avoir de plus naturel que le tic tac régulier pour signifier à l’utilisateur “mon affichage est figé mais, écoutez le son, oui, je suis toujours en marche !”...

Les sons, en activant un sens peu utilisé en informatique classique peut aider à transmettre des informations utiles sur l’état du logiciel qui ne viennent pas brouiller la vue trop souvent sollicitée. Il faut s’en servir correctement, passer parfois des heures à écouter des banques de sons avant de trouver le bon, celui qui ira dans le contexte, parfois même il faudra utiliser des logiciels particuliers pour le traiter même sommairement (ce qui a été fait ici d’ailleurs, à l’aide Sound Forge de Sony, un excellent logiciel de traitement du son).

Maintenant que nous savons pourquoi nous utiliserons du son (deux pour être précis), comment les faire jouer ?

On conçoit facilement que pour le tic tac il suffira de jouer le son toutes les secondes ou demi secondes et qu’un simple timer, celui qui fera avancer l’aiguille par exemple, sera suffisant. En revanche pour le clic du bouton c’est moins évident.

En effet nous sommes au sein d’un template, pas un bout de code auquel se raccrocher. Que du XAML rangé dans un dictionnaire de ressources. Damned !

La feinte consiste repérer (ou à créer s’il le faut) un élément du dessin qui occupe l’espace cliquable. Le bouton étant déjà dans un Canvas rectangulaire qui couvre toute sa surface c’est lui que je choisirai. Sinon il aurait fallu ajouter un Canvas ou une Rectangle ou autre, peu importe, pour couvrir la région cliquable. Mais attention, le Canvas qui entoure l’objet n’a pas de Background. Vous pouvez toujours essayer de détecter le clic, ça ne marchera pas. Il lui faut une couleur transparente pour que le clic soit attrapé...

Seconde astuce, comment créer une simple couleur totalement transparente et est-ce nécessaire ? D’une part il n’est pas nécessaire de créer une couleur pour cela et d’autre part il existe une astuce : un clic sur le carré des options de la propriété Background affiche un menu (celui par lequel on effectue un data binding), il suffit de choisir l’entrée “Custom expression” (expression personnalisée). Une petite boite de saisie s’affiche, et là on tape “Transparent” puis Return (avec la majuscule). Et voici un Canvas avec une transparence totale, sans sélectionner de couleur mais qui, désormais, peut attraper le clic... C’est un pas en avant.

Mais comment gérer le clic dans un template qui ne possède aucun code-behind ? On ne le fait pas, tout simplement (ou alors on utilise des triggers, ce qui n’est pas très pratique). Encore une autre astuce : on place sur le Canvas qui nous sert “d’attrape clic” un Behavior : PlaySoundAction. L’évènement déclencheur sera le LeftMouseButtonDown, et la source du son sera un fichier mp3 préalablement ajouté au projet et qui apparaitra dans la combobox “source” du Behavior :

image

C’est tout bête mais il faut y penser...

On peut déjà exécuter l’application et, miracle, le bouton fonctionne bien, il s’abaisse et se relève comme prévu, le tout accompagné d’un joli son !

Il ne reste plus qu’à sortir du mode de templating, ce qui nous fait revenir à la page principale, puis de faire un copier / coller du bouton du haut pour recréer le second bouton qui était sur la gauche (un peu de placement, une petite rotation et l’affaire est jouée).

Deux choses à prendre en compte pour se faciliter le travail : d’une part après avoir fait le copier / coller le nouveau bouton se trouve en haut du Z-Order, il faut le ramener au même niveau que le premier bouton pour respecter les “couches” du dessin et que le bas de l’axe du bouton soit caché par le cadran. D’autre part, pour effectuer le placement il faut agir avec doigté ! Le faire à la sauvage en déplaçant l’objet et en espérant qu’on sera aligné sur le cadran qui est un objet rond, c’est possible, mais bien délicat.

Pour cela il faut procéder autrement. Il suffit de déplacer le point qui marque le centre des transformations pour le faire coïncider avec le centre du cadran, là où nous avons positionné le centre de l’aiguille il y a un moment. Une fois ce point placé correctement au centre du chronomètre, il ne reste plus qu’à appliquer une rotation à notre bouton : il tournera autour du cadran, on s’arrête quand l’angle nous plait, c’est aussi simple et imparable que ça !

Placé correctement le point est donc la clé de la manipulation. Pour cela je n’hésite pas à zoomer au maximum, à 6400% sur la zone centrale. Il ne faut pas mesquiner sur le zoom !

Autre détail : si vous regardez bien, vous verrez que le bouton du haut possède une ombre qui laisse supposer que la lumière arrive directement depuis le haut. Ce n’est d’ailleurs pas très homogène avec le reste de l’objet mais visuellement ça passe. Les graphistes comme les peintres se laissent souvent aller à de telles libertés contraires aux lois de la physique mais qui “passent” visuellement. Pour un scientifique c’est un crime, pour un graphiste, c’est normal.

En tout cas, la fameuse ombre, comme le graphiste n’est plus là, il va bien falloir la simuler sous le second bouton placé sur le côté... Ici aussi, tout est question d’astuce.

image

Puisque l’ombre du bouton haut laisse supposer que la lumière vient directement du dessus, il faut que l’ombre du bouton de gauche soit légèrement déformée. Un graphiste ferait peut-être autrement, moi et mon esprit cartésien on pense qu’il serait bon de l’incliner un poil. La première étape (après avoir copier / coller l’originale) consiste à appliquer un Skew sur l’ombre pour l’étirer à gauche. Ensuite on place son point de référence des transformations sur le centre du chronomètre et on applique une rotation. Simple aussi donc. Reste à jouer sur sa transparence, l’ombre va se trouver sur une zone plus brillante, elle doit être atténuée.

On fait un Run, et la magie s’opère à nouveau : les deux boutons fonctionnent à merveille, avec leur petit son. Le vectoriel s’est beau, XAML c’est très beau.

Fin de la partie graphique

Graphiquement nous avons fait le tour de la question. Nous avons récupéré un objet Illustrator par importation directe dans Blend, nous l’avons modifié pour nos besoins en ajoutant un texte (qui contiendra le temps écoulé écrit en clair), nous avons transformé une partie du dessin en template de bouton que nous avons dupliqué pour reproduire l’objet original. Comble du raffinement les boutons s’enfoncent et se relâchent avec douceur (VSM plus easout) et jouent un son quand ils sont cliqués (behavior et un mp3). L’aiguille est bien à zéro (sur midi) et l’application compile. Que demander de plus ? !

Et bien... il faudrait que ça marche vraiment. Ca serait quand même mieux.

Il reste donc à faire en sorte que les boutons agissent vraiment sur l’objet, que l’aiguille tourne et que le TextBlock affichant le temps écoulé soit mis à jour.

J’ai choisi de développer ce petit projet en mode MVVM parce que tout doit être développé en MVVM. Nous allons voir maintenant comment donner vie à notre chronomètre en respectant la plomberie !

Le code

Nous suivons la pattern MVVM et utilisons le toolkit MVVM Light. Tout va se jouer ainsi dans le VIewModel de la page principale et unique de notre applicaton. Le fichier MainViewModel.cs.

Par défaut le template de projet MVVM Light contient déjà le ViewModel Locator, et la page principale (le DataContext de la Vue) est déjà connectée à son ViewModel via le Locator. Nous n’avons qu’à ajouter du code au ViewModel puis à faire quelques bindings dans la Vue et l’affaire sera réglée.

Deux boutons, deux commandes

Notre interface comporte deux boutons, nous retrouverons ainsi deux commandes dans le ViewModel. MainButtonCommand, le bouton du haut (start / stop) et LefButtonCommand, le bouton de gauche (split / reset).

Le code source complet du projet est fourni est fin d’article, je ne montrera ici que des extraits pour illustrer le propos. Par exemple la commande MainButtonCommand sert uniquement à basculer le chronomètre en mode marche / arrêt. Cela est représenté par une propriété State (état) qui est de type enum et qui accepte les valeurs MainState.Stopped et MainState.Running.

La commande MainButtonCommand sera ainsi initialisée dans le constructeur du ViewModel :

MainButtonCommand = new RelayCommand(()=>
 {
    State = State == MainState.Stopped ? MainState.Running : MainState.Stopped;
 });

La commande étant déclarée comment suit :

/// <summary>
/// Gets or sets the main button command.
/// </summary>
/// <value>The main button command.</value>
public ICommand MainButtonCommand { get ; private set; }

C’est bien entendu la propriété State qui, en changeant de valeur va déclencher ou arrêter l’horloge.

Qui dit horloge dit Timer. Dans une application qui doit être très précise il faudrait utiliser des timers qui le sont aussi. Ici j’utilise un DispatcherTimer qui évite de se poser des questions sur d’éventuels problèmes de threads (car seul le thread de l’UI peut mettre à jour l’UI). Lorsqu’on utilise que des data binding une telle précaution n’est pas obligatoire, le Framework le gère. Mais je vais aussi utiliser des Messages pour la démonstration et je veux éviter d’avoir à vérifier les invocations. En revanche avec un DispatcherTimer nous aurons des temps approximativement correctes. Le chronomètre avancera par pas de 500 ms, et comme le prouvera le TextBlock affichant le temps écoulé, les millisecondes tomberont rarement juste ! (ce qui ne serait pas très beau dans une application réelle).

Une fois le chronomètre démarré, le Timer se met en route. Regardons le code de son évènement Tick :

void timer_Tick(object sender, EventArgs e)
{
    Messenger.Default.Send(new GenericMessage<string>("TICK"));
    if (substate == SubState.Splitting) return;
    var old = SecondAngle;
    var delta = DateTime.Now - startDate;
    var ms = (delta.Seconds*1000d) + delta.Milliseconds;
    SecondAngle = (ms / 60000d) * 360d;
    RaisePropertyChanged("SecondAngle", old, SecondAngle, true);
    RaisePropertyChanged("ElapsedTime");
}

La première chose qui est faire est l’envoi d’un message MVVM Light, un message générique transportant une chaine de caractère indiquant “TICK”. A chaque Tick il faudra bien jouer le son prévu.

Mais comment un ViewModel peut-il jouer du son alors qu’il n’a aucun lien avec l’UI ?

Le plus simple est d’envoyer un message. L’UI qui possèdera un MediaElement n’aura qu’à répondre au message “TICK” pour jouer le son. Sans savoir d’où vient le message, sans couplage fort entre les classes et sans astuces alambiquée. Un mécanisme simple et sain donc, qui respecte la pattern.

Notre chronomètre gère un “sous état”, qui est déclenché par le bouton de gauche. Lorsque le chronomètre est en marche, un appui sur ce dernier suspend l’affichage ou le reprend. C’est la raison du test qui suit le message: si nous sommes en mode suspension, le tick ne fera rien d'autre que d’envoyer le message pour jouer le son.

Sinon, l’angle de l’aiguille est calculé. Rien de bien complexe, je voulais seulement que l’aiguille avance par pas d’une demi seconde, d’où les calculs en ms et non en secondes.

Enfin, le tick déclenche deux PropertyChanged (légèrement modifiés dans MVVM Light). Concernant l’aiguille, c’est la partie transmission d’un message en plus du PropertyChanged qui nous intéresse. En effet, le zéro de l’aiguille a été obtenu en début de projet en appliquant une rotation sur le dessin original. Pour rendre les choses plus propres encore, cette rotation “originelle” a été transformée en ressource stockée dans la Vue.

De deux choses l’une, soit nous dupliquons cette valeur dans le ViewModel afin qu’il retourne un angle directement utilisable, soit le ViewModel ne retourne qu’un angle “pur”, à l’interface de faire l’adaptation.

Bien entendu c’est la seconde solution qui est retenue. Stocker des valeurs propres à l’interface dans le ViewModel est une hérésie... Le ViewModel calcule un angle idéal, propre. Le même ViewModel pourrait servir à plusieurs Vues, chacune possédant ses propres “bricolages” d’affichage.

Du coup, on ne peut pas binder directement la valeur de la rotation de l’aiguille à la propriété angle du ViewModel. Il faudra adapter cette valeur avant de s’en servir. Un convertisseur alourdirait la programmation sans vraiment être une solution satisfaisante.

Nous utilisons ainsi la propriété de RaisePropertuChanged de MVVM Light de pouvoir (ce n’est pas obligé) transmettre un message en même temps que d’appeler PropertyChanged. La Vue va s’abonner à ce message et sera ainsi notifiée comme par un binding mais en gardant la main pour adapter la valeur reçue.

Regardons le code intégré à la Vue. Il n’y a que celui-là, placé dans le constructeur. La Vue peut posséder du code du moment que celui sert à traiter l’interface, c’est le but ici :

public MainPage()
       {
           InitializeComponent();
           Messenger.Default.Register<GenericMessage<string>>(this, 
                m=>
                   {
                     if (m.Content != "TICK") return;
                     this.meTick.Stop();
                     this.meTick.Play();
                   });
           Messenger.Default.Register<PropertyChangedMessage<double>>(this,
                m=>
                   {
                     if (m.PropertyName != "SecondAngle")return;
                     ((CompositeTransform)
                        Needle.RenderTransform).Rotation =
                           (double)Resources["InitialNeedleAngle"] +
                           m.NewValue;
                   });
       }

Il s’agit du constructeur de la Vue. Les deux messages auxquels elles s’abonnent sont :

GenericMessage<string> pour le message de “TICK” qui déclenche le MediaElement

et PropertyChangedMessage<double> pour la valeur de l’angle.

On voit que les messages sont filtrés, le message générique utilise une chaine “TICK” pour s’assurer qu’il va bien traiter le message qui lui est destiné. Le message de changement de propriété est filtré par le type qu’il reçoit (un double) et ensuite par contrôle du nom de la propriété qui vient de changer (“SecondAngle”).

Le calcul de la position de l’aiguille est simple : on récupère la valeur transmise par le message, on l’ajoute à l’offset stocké en ressource (InitialNeedleAngle) et on le stocke dans la Rotation du CompositeTransform du RenderTransform de l’objet Needle (aiguille).

Pour ce qui est du texte affiché sous l’aiguille, le temps écoulé, j’ai choisi de vous montrer la méthode la plus directe : le ViewModel expose une propriété ElapsedTime, de type string qui est calculée à la volée en fonction de la date de départ du chronomètre (initialisée par le passage à l’état Running) et la date courante puis mise en forme par un String.Format.

Comme cette propriété n’est pas mise à jour mais qu’elle calcule sa valeur quand on la lit, elle ne peut pas émettre elle-même de signal PropertyChanged... C’est pourquoi vous voyez une telle notification en fin du code de la méthode timer_Tick un peu plus haut. C’est à chaque battement d’horloge qu’on simulera un property changed de Elapsed time pour indiquer au data binding de la Vue qu’il faut venir lire une nouvelle valeur...

Et bien entendu, dans ce cas, la propriété Text du TextBlock est bien liée par data binding à la propriété ElapsedTime du ViewModel selon la méthode classique :

<TextBlock x:Name="txtElapsed" 
  Text="{Binding ElapsedTime, Mode=OneWay}" 
  ..... />

 

Conclusion

Il faut bien conclure un jour... Mai sil reste des choses à découvrir dans cette petite application !

C’est pourquoi je vous fourni son code complet (mais à vous d’installer MVVM Light), mais aussi le fichier EPS d’origine, dans le cas où, par volonté d’auto formation, vous souhaiteriez parcourir à nouveau le chemin mais seul, en partant de zéro pour vérifier que vous avez bien intégré tous mes conseils !

Le code source: MvvmChrono.zip (92,88 kb)

Le fichier EPS original : stopwatch.ai (441,01 kb)

Et Stay Tuned !

blog comments powered by Disqus