Faire un Hello World c’est sympa, mais faire un tour plus
réaliste incluant le testing et le reste c’est encore plus sympa !
Suivez-moi pour une série / cours de 5 papiers autour d’une App, de A à Z
Plus loin qu’un Hello World
Au début de cette saga sur MAUI je vous ai montré l’inévitable
Hello World (ref papier #3 23/9/22), il fallait bien se mettre en jambes. Mais
si on allait un peu plus loin ? Par exemple en voyant tous les aspects
d’une App réelle, mais tout en restant simple.
Un cours presque complet. Il ne
sera pas possible de tout voir en détail, mais cette série en 5 épisodes est en
réalité un mini cours plutôt complet qui abordera tous les aspects essentiels
de la création d’un projet MAUI réaliste. MVVM, l’injection de dépendance, les
tests unitaires, etc, nous verrons tout cela et bien plus encore. Alors
accrochez vos ceintures, on décolle !
Le projet de base
Le point de départ de notre saga est bien entendu la création
d’un projet MAUI utilisant le template par défaut fourni par Microsoft. Pour
cela pas de redite inutile je vous renvoie aux articles précédents ainsi qu’à
celui consacré à l’Hello World, on ne va pas le refaire ici, c’est quelque
chose d’acquis. Je renvoie ainsi ceux qui aurait loupé cet épisode au papier du
Hello World.
Passer la vitesse supérieure
Bon, nous avons notre projet de base mais c’est assez peu
réaliste, il marche, il fait des choses (le compteur qui s’incrémente) mais il
ne respecte pas les bonnes pratiques et est trop simple pour prétendre être un
Hello World véritablement sérieux.
Il nous manque plein de choses. Mais on va garder un code
ultra simple pour chacune. C’est plutôt la structure générale et les bonnes
pratiques que nous allons voir pour créer un « projet zéro ». Par
exemple il nous manque tout ce qui concerne MVVM, ou bien le Unit Testing. Bref
ce qui est nécessaire pour se rapprocher un peu plus d’une véritable App MAUI.
Je ne vous parlerai pas de tout à la fois, bien d’autres articles viendront
compléter ce premier pas, déjà ceux de la série lancée aujourd’hui de 5
billets, puis tous ceux à venir et même tous ceux déjà existant !
MVVM
Je parlais de MVVM à l’instant et de papiers déjà écrits…
coïncidence (ou pas) j’en ai écrit plein sur ce sujet notamment les récents sur
le Microsoft MVVM Toolkit (série
en 5 parties) et beaucoup d’autres avant traitant de ce sujet, et même de
la comparaison
entre MVVM et MVU dont vous avez peut-être entendu parlé dans le cadre de
MAUI… Tous ces articles sont tagués MVVM dans Dot.Blog et pour en voir toute la
liste il suffit de cliquer sur ce lien https://www.e-naxos.com/Blog/?tag=/mvvm
.
Pour les paresseux qui ne veulent pas tout reprendre à zéro
(il a tout de même environ 70 articles dans Dot.Blog tagués MVVM !!!)
voici en quelques mots de quoi il s’agit :
Dans une App bien écrite nous devons séparer notre code pour
améliorer la maintenance et les tests (plus d'informations sur les tests dans cette
série de 5 articles). Un pattern très utilisé dans le monde C#/XAML est MVVM
né à l’époque de WPF et qui a connu des tas d’implémentations différentes dans
des Toolkits célèbres comme Mvvm Light, Caliburn, Prism et bien d’autres. Ce
pattern assez proche de Model-View-Controller (MVC), et qui signifie
Model-View-ViewModel, nous dit que :
- Le Modèle (Model, le premier M de MVVM) contient nos
informations brutes sur le domaine qui peuvent provenir de diverses sources de
données et nos règles métier.
- Le ViewModel (le second V et le dernier M de MVVM) récupère
généralement le Modèle via un service pour assurer encore plus l’isolation du
code. Le ViewModel prépare ensuite les informations à afficher et réagit
aux commandes de l’utilisateur qui lui sont passées par la Vue. Il contient
donc la « glu » entre le Modèle et la Vue, c’est-à-dire la gestion
des flux de données entre ces deux pôles et celle des interactions avec
l’utilisateur.
- La Vue (View, le premier V de MVVM) affiche ce qui se trouve
dans le ViewModel et transmet les interactions de l’utilisateur à ce dernier.
La séparation des préoccupations ici est
que
- La Vue ne sait pas comment le ViewModel obtient ses
informations.
- Le ViewModel ne sait pas comment le Model obtient ses
informations.
- Le ViewModel ne sait pas quelle Vue lui est connectée.
- La Vue ne peut pas contenir de code ni de règles métier.
Elle ne peut contenir que du code lié à l’UI.
- Le ViewModel ni le Modèle ne peuvent intervenir sur l’UI.
Cela nous permet d'apporter des modifications de code à la Vue,
au ViewModel et au Modèle de façon indépendante autorisant des équipes
différentes à travailler sur chacune de ces parties sans risque de bogues et
cela facilite grandement le Unit Testing de chaque partie.
Au départ l’idée était surtout de
séparer la partie UI de la partie code proprement dit, donc d’un côté des
graphistes construisant l’UI et de l’autre des développeurs créant le code.
Naissait un nouveau métier, Intégrateur, un développeur un minimum doué pour
l’UI qui s’occupait de brancher tout cela. C’était un schéma idyllique jamais
appliqué dans la réalité en raison du profil incongru de l’Intégrateur (qui dit
rare dit cher et les patrons n’aiment pas ça…). Mais des débuts de MVVM il nous
reste surtout aujourd’hui l’idée principale d’un couplage lâche. Ce
dernier concernant autant le code que les Vues, les ViewModels, les Modèles,
les Services, etc.
Un aspect important de MVVM est ainsi de savoir comment lier
les champs de la Vue au ViewModel car le circuit de l’information n’est pas
unidirectionnel. L'utilisateur interagit avec la vue, par exemple en
cliquant sur un bouton pour incrémenter un compteur. Cela met à jour les
données stockées dans le ViewModel (et éventuellement transmises au Modèle).
Cependant, nous pouvons également mettre à jour les valeurs
par programmation directement dans le ViewModel, et nous voulons que ces valeurs
s'affichent dans la vue. Il s'agit d'une liaison bidirectionnelle. Autre
situation semblable : le Modèle peut changer (un SGBD, un capteur de
smartphone…) et ce changement doit être reflété par la Vue. D’ailleurs dans sa
version « souple » MVVM autorise une Vue à se connecter directement à
un Modèle sans passer par un ViewModel s’il n’y a aucune interaction de
l’utilisateur. Ce cas est très rare et bien entendu dans une App bien
structurée toutes les Vues utilisent un ViewModel même s’il est vide (ce qui
est exceptionnel donc).
Pour assurer ce découplage fort dans nos projets nous
n’allons pas réinventer la roue à chaque fois. Et même au sein d’un même projet
de nombreuses choses vont devenir répétitives. Pour cela XAML nous offre un
système de liaison de données (Data Binding) puissant. Mais ce n’est pas assez
pour assurer une couverture totale de MVVM. Il nous faut ainsi un Toolkit qui
va proposer tout le nécessaire. Il y a eu beaucoup de ces Toolkits dans
l’histoire de XAML, celui que nous allons utiliser avec MAUI est le Microsoft
MVVM Toolkit. Il a été conçu pour remplacer Mvvm Light qui était le plus
utilisé, tout en le rendant plus performant et plus moderne (avec l’aide de son
créateur qui a participé aux premiers pas du Toolkit Microsoft).
Bien entendu dans cette petite série de 5 billets pour
réaliser un Hello World plus réaliste nous allons balayer plein de concepts
comme MVVM, il n’est pas possible de détailler chacun. Même la séparation en 5 parties
ne le permet pas. Je renvoie donc le lecteur aux articles déjà écrits sur le
sujet, de façon générale ou spécifique (comme la présentation du Toolkit
Microsoft).
Ajouter un ViewModel
Cette digression autour de MVVM nous amène en toute logique
à l’ajout d’un ViewModel à notre App de base (celle créée au départ par le
template de Visual Studio).
Que fait l’App pour le moment ? Pas grand-chose c’est
certain, mais c’est un Hello World… Quand l’utilisateur clique sur le bouton,
le compteur est incrémenté, ce que la Vue reflète.
Nota : le code source zippé
sera diffusé avec l’épisode 3 ainsi que le 5.
Comment le fait-elle ? Mal ! Et doublement
même ! D’une part la Vue connecte l’évènement du clic du bouton
directement à son propre code behind (MainPage.xaml.cs), d’autre part ce
code agit directement sur la Vue sans passer par un ViewModel. Mais ce n’est
qu’un template.
Il nous faut avant toute autre chose casser ce couplage fort
entre l’UI et le code, donc ajouter un ViewModel. Ce dernier va être construit
avec les briques fournies par le Microsoft MVVM Toolkit.
Pour commencer il faut ajouter le paquet du Toolkit à notre
projet.
On notera que le Microsoft.Toolkit.Mvvm,
au départ isolé, a été intégré au Community Toolkit, toutefois il reste
sous la forme d’un paquet séparé car chacun peut choisir d’utiliser un autre
Toolkit MVVM sans pour autant se passer des services du Community Toolkit ni
avoir deux Toolkits MVVM dans l’App dont l’un ne servirait pas…
C’est donc aujourd’hui le paquet CommunityToolkit.Mvvm
qu’il faut ajouter au projet.
Vous savez ajouter un paquet à un projet, je vais vous
laisser faire on gagnera un peu de temps.
Armés du bon Toolkit nous pouvons enfin ajouter notre
ViewModel. Dans un projet bien structuré les ViewModels sont tous groupés dans
un ou plusieurs sous-répertoires du projet. Il est donc nécessaire de créer au
minimum un tel sous-répertoire que nous appellerons tout simplement ViewModels.
Là encore je vous laisse le faire c’est de la manipulation classique de Visual
Studio.
A l’intérieur de ce nouveau dossier nous allons maintenant
ajouter un fichier de code appelé Counter.cs. One more time, je vous
laisse jouer seul.
Il existe un débat sans fin que
je ne vais pas clore d’une seule phrase aujourd’hui et qui concerne le nommage
des ViewModels. Depuis longtemps beaucoup de développeurs ont pris l’habitude
de suffixer leur nom par ViewModel. Dans cette logique il faudrait créer le
fichier de code CounterViewModel.cs. Cette habitude provient aussi de
certains toolkits qui effectuaient la liaison automatiquement entre Vues et
ViewModels en se basant sur leur nom et ce suffixe. Comme nous avons placé
notre ViewModel dans un répertoire ViewModels, ce qui de facto
créé un namespace du même nom, il semble redondant d’ajouter un suffixe
identique. Mais chacun fera en fonction de ce qu’il juge bon pour son
projet !
Le code de ce fichier source est le suivant :
using
CommunityToolkit.Mvvm.ComponentModel;
using
CommunityToolkit.Mvvm.Input;
namespace
RealisticHelloWorld.ViewModels;
public partial class Counter
: ObservableObject
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CountText))]
[NotifyCanExecuteChangedFor(nameof(IncreaseCounterCommand))]
private int count;
public string CountText
{
get
{
string text = "Click me";
if (count > 0)
{
text = $"Clicked {count}
" + (count == 1 ? "time" : "times");
}
SemanticScreenReader.Announce(text);
return text;
}
}
[RelayCommand(CanExecute =
nameof(CanIncreaseCounter))]
private void IncreaseCounter()
{
Count++;
}
private bool CanIncreaseCounter() =>
count >= 0 && count < 10;
}
Le CommunityToolkit.Mvvm comprend des générateurs de code
pour créer tout le code de liaison bidirectionnelle à l'aide des attributs.
C’est un progrès important puisqu’il évite d’écrire un code répétitif. Ici nous
pouvons voir ces générateurs à l’œuvre par le biais d’attributs posés à
certains endroits :
- La propriété Count. Créée par l’attribut ObservableProperty
posé sur le champ count. La mise en majuscule de la première lettre est effectuée
par le générateur. Il faudra penser, au moment de liaison XAML, à utiliser la
graphie avec majuscule.
- La propriété CountText est créé de façon classique
mais ne possède qu’un getter et dépend totalement de Count. C’est pour cela que
le champ « count » fait appel à un autre attribut NotifyPropertyChangedFor
qui demande à ce que CountText soit notifié des changements de Count. Ainsi le
texte affiché à l’écran (le compteur) sera mis à jour automatiquement.
- Comme nous souhaitons (pour la démo) que le compteur ne
puisse prendre que les valeurs de 0 à 10, il est important d’activer ou
désactiver la commande (voir ci-après) selon la valeur de la propriété Count.
Pour ce faire on utilise ici un autre attribut sur « count » qui est NotifyCanExecuteChangedFor
en spécifiant le nom de la commande concernée.
- La commande IncreaseCounterCommand est créée en
plaçant l’attribut RelayCommand sur la méthode privée IncreaseCounter.
La commande (publique) sera générée par le toolkit en ajoutant le suffixe
Command. Il s’agira bien à la fin de deux méthodes différentes.
On peut voir aussi dans ce code l’appel à SemanticScreenReader,
je vous avais prévenu nous balayons en peu de code beaucoup de choses
différentes… Il s’agit ici de rendre notre application accessible (ce qui peut
être une obligation légale dans certains contextes et qui reste une obligation
morale dans tous les cas, d’autant que cela peut aider même des utilisateurs
sans aucun handicap). Le toolkit nous propose tout un ensemble de
fonctionnalités pour satisfaire cette exigence sans écrire des pages et des
pages de code. L’appel au lecteur d’écran et sa méthode d’annonce permet
« tout simplement » d’énoncer le texte lorsque la valeur de la
propriété changera. Magique ! Mais là encore impossible d’en dire beaucoup
plus, ceci est un article par un livre… Mais je vous enjoins à visiter https://docs.microsoft.com/en-us/dotnet/maui/fundamentals/accessibility
pour tout savoir sur les propriétés sémantiques offertes par le toolkit
Community Essentials intégré à MAUI (il était séparé dans les Xamarin.Forms).
Pour que la classe que nous avons créé puisse être utilisée
comme ViewModel il faut aussi :
- Qu’elle soit écrite sous la forme d’une classe partielle
- Qu’elle hérite de ObservableObject
- Que ce soit bien la propriété générée Count qui soit
incrémentée et non le champ count (valable pour toutes les situations
identiques peu importe le nom et le type de la propriété cela va sans dire).
Connecter le ViewModel et la Vue
Il existe plusieurs façons de mettre en relation la Vue avec
son ViewModel. Certaines peuvent être automatique (à l’aide d’un toolkit en
général), d’autres se fondent sur le pattern Service Locator ou d’autres façons
encore. Ici nous allons rester simples.
La première chose à faire est de faire reconnaître à la Vue
le type du ViewModel pour que le XAML puisse être compilé. Attention ! Si
MVVM interdit au ViewModel de connaître la Vue, le contraire est autorisé. On
peut toutefois vouloir pousser plus loin le découplage en ajoutant un Locator,
en utilisant des Interfaces, etc.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="RealisticHelloWorld.MainPage"
xmlns:viewModels="clr-namespace:RealisticHelloWorld.ViewModels"
x:DataType="viewModels:Counter"
>
Le code ci-dessus provient de la Vue
« MainPage.xaml », il montre (deux dernières lignes) comment le
namespace ViewModels est ajouté à la ContentPage et comment le type de notre
ViewModel (Counter) est déclaré pour que les propriétés et commandes qu’il expose
soient reconnues par le code XAML de la page.
On peut maintenant modifier le code du bouton pour qu’il
utilise la commande et le texte du ViewModel :
<Button
Text="{Binding CountText}"
SemanticProperties.Hint="Counts the number of times you click"
Command="{Binding IncreaseCounterCommand}"
HorizontalOptions="Center" />
On remarque que le nom du bouton a été supprimé
(x :Name) et que son texte et sa commande sont maintenant reliés par « Binding »
aux propriétés du ViewModel.
Il reste encore une chose : supprimer tout le code
devenu inutile dans le code-behind de la ContentPage (cela violait MVVM). On
ajoute aussi la création du ViewModel qu’on affecte à la propriété
BindingContext de la page :
using RealisticHelloWorld.ViewModels;
namespace RealisticHelloWorld;
public partial class
MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
BindingContext = new Counter();
}
}
Comme dit plus haut, cette façon de lier la Vue à son
ViewModel peut sembler « brutale » à certains. Elle respecte MVVM
mais les puristes voudront certainement passer par un service ou autre pour que
la Vue n’ait pas à connaître le type exact du ViewModel. Cela peut poser des
problèmes pour le code XAML qui utilise ce dernier. Il faudra alors
éventuellement déclarer une Interface pour remplacer ce type afin que la Vue ne
connaisse pas la classe concrète qui l’implémentera. Il s’agit ici d’une vision
assez extrême de MVVM à laquelle je ne souscris pas totalement mais qui peut
s’avérer payante dans certains cas. A chacun de juger selon les impératifs de
son projet…
Exécution
Il est temps d’exécuter notre App. En dehors peut-être du
texte du bouton qui n’est pas tout à fait comme l’original (nous l’avons fixé
dans le ViewModel et non dans XAML) tout le reste est identique et tout
fonctionne comme avant, ce qui est plutôt une bonne chose !
Conclusion de la partie 1/5
L’idée de découper ce petit Hello World réaliste en 5 parties
m’apparait maintenant encore plus judicieux que je ne l’aurais pensé !
Cette première partie est assez longue (certaines qui suivent le seront aussi
je le crains) mais il faut bien expliquer un minimum sinon autant vous laisser
prendre la doc Microsoft…
Donc prochaine étape, les tests unitaires… mais pas que.
Stay Tuned !