Dot.Blog

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

Un « projet zéro » MAUI réaliste partie 1/5 – Création du projet et MVVM

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 !

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