DevM

Pierre Ferrari

[TOC]

Introduction

Pèse personne:

3 à 4 développeurs ?

Utilisation d’un framework multiplateforme pour ne pas avoir à développer 4 fois la même application.

Choix basé sur:

… and the winner is … MAUI 🎉 🥳 !

Marketing vs réalité

Les promesses de MAUI sont alléchantes. On peut développer une application pour Android, iOS, Mac OS et Windows à partir d’une seule base de code. C’est vrai, mais …

Pour toutes les applications standards telles que les applications qui utilisent des labels, des boutons, des listes, des images, des bases de données locales, des services REST, etc. MAUI est parfait. Mais dès qu’on sort de ces sentiers battus, on se retrouve vite confronté à des problèmes.

Par exemple:

C’est la même chose pour les cartes géographiques, les espaces de dessin etc. Par exemple pour le dessin, vous avez accès à un GraphicsView qui vous fournit une interface ICanvas. Vous pouvez donc dessiner sur la plateforme de votre choix, mais le comment sera décidé par la plateforme. Et ça donne des trucs bizarre tel que:

float rotate = 90f;
canvas.Rotate(rotate, center.X, center.Y);

Sur Windows, à chaque appel de méthode, le canvas tourne de 90°. Par exemple, 0° -> 90° -> 180° etc. Sur Android, à chaque appel de la méthode, le canvas se positionne à 90°. Par exemple, 0° -> 90° -> 90° etc.

On doit donc utiliser des directives de compilation pour adapter le code à chaque plateforme.

#if WINDOWS
    rotate = ...
#elif ANDROID
    rotate = ...
#endif
canvas.Rotate(rotate, center.X, center.Y);

C’est donc un peu plus compliqué que ce que l’on veut bien nous dire et parfois, on en perd notre latin.

Il y a aussi ça Issues 3.7k.

De plus, un framework tel que celui-là est tributaire:

Vous êtes confronté à des choix constants. Par exemple, pour le problème cité plus haut avec ICanvas, vous pouvez:

Projets

.NET MAUI

.NET Multi-platform App UI (.NET MAUI) est un Framework multiplateformes permettant la création d’application aussi bien mobile que bureau.

À l’aide de .NET MAUI, vous pouvez développer des applications qui peuvent s’exécuter sur Android, iOS, macOS et Windows à partir d’une base de code partagée unique.

maui

Le langage utilisé et le C# et les interfaces sont décrites en XAML. Ca permet la séparation des tâches entre le Designer et le développeur. Dans la partie XAML on retrouve une mise en page et des styles proches de ce qui se fait en CSS. La navigation entre les pages de l’application peut également être définie en terme de liens avec des passages de valeur de type query string. En ayant donc un bagage C# + développement Web (http / CSS), le développeur .NET MAUI aura une courbe d’apprentissage relativement douce.

Le Framework .NET MAUI permet l’accès natif aux fonctionnalités et composants du smartphone tels que:

Comme il s’agit d’un Framework open source ouvert à la communauté, on trouve également un ensemble de librairie basé sur le gestionnaire NuGet telle que la librairie d’authentification biométrique.

Remarque

Le grand absent de .NET MAUI c’est Linux. Pourtant au vue de l’architecture proposée pour une application .NET MAUI, on devrait pouvoir générer une application Linux GTK+ au travers du runtime Mono qui est exploité au niveau des couches Android, iOS et Mac OS.

architecture

Un projet existe déjà sur github et il appartient à un des acteurs principaux du Framework. On peut donc s’attendre à pouvoir générer des applications Linux stable dans un avenir plus ou moins proche.

Quelques lectures supplémentaires

Installation

La création d’un projet .NET MAUI sera toujours identique. Ci-dessous, vous trouverez la base de la mise en route pour ne pas oublier le détail qui fait que votre programme ne fonctionne pas.

Selon les indications fournies par Microsoft:

Installer Visual Studio

Le développement se fera avec Microsoft Visual Studio Community|Professional|Enterprise 2022. À l’heure où ces lignes sont rédigées, la version minimale est la 17.3. Mais bien entendu, ça va évoluer. Vous pouvez installer la version de votre choix en sachant que la version Community Edition fera parfaitement l’affaire. Peut-être que par le biais de l’école vous pouvez également obtenir une version Entreprise moyennant l’obtention d’une clé via le Microsoft Azure portal.

Lors de l’installation, il faudra veiller à installer le SDK .NET MAUI

vs-workloads

Cette option installera automatiquement les workloads et les templates nécessaires

$ dotnet workload list

ID de la charge de travail installée      Version de manifeste      Source de l’installation
--------------------------------------------------------------------------------------------
android                                   35.0.7/9.0.100            VS 17.12.35707.178      
ios                                       18.1.9163/9.0.100         VS 17.12.35707.178      
maccatalyst                               18.1.9163/9.0.100         VS 17.12.35707.178      
maui-windows                              9.0.14/9.0.100            VS 17.12.35707.178

$ dotnet new list maui
Ces modèles correspondent à votre entrée : 'maui'.

Nom du modèle                        Nom court           Langue  Balises
-----------------------------------  ------------------  ------  -----------------------------------------------------------------------------
.NET MAUI Blazor Hybrid and Web App  maui-blazor-web     [C#]    MAUI/Android/iOS/macOS/Mac Catalyst/Windows/Tizen/Blazor/Blazor Hybrid/Mobile
.NET MAUI ContentPage (C#)           maui-page-csharp    [C#]    MAUI/Android/iOS/macOS/Mac Catalyst/WinUI/Tizen/Xaml/Code
.NET MAUI ContentPage (XAML)         maui-page-xaml      [C#]    MAUI/Android/iOS/macOS/Mac Catalyst/WinUI/Tizen/Xaml/Code
.NET MAUI ContentView (C#)           maui-view-csharp    [C#]    MAUI/Android/iOS/macOS/Mac Catalyst/WinUI/Tizen/Xaml/Code
.NET MAUI ContentView (XAML)         maui-view-xaml      [C#]    MAUI/Android/iOS/macOS/Mac Catalyst/WinUI/Tizen/Xaml/Code
.NET MAUI Multi-Project App          maui-multiproject   [C#]    MAUI/Android/iOS/macOS/Mac Catalyst/Windows/Mobile
.NET MAUI ResourceDictionary (XAML)  maui-dict-xaml      [C#]    MAUI/Android/iOS/macOS/Mac Catalyst/WinUI/Xaml/Code
.NET MAUI Window (C#)                maui-window-csharp  [C#]    MAUI/Android/iOS/macOS/Mac Catalyst/WinUI/Tizen/Xaml/Code
.NET MAUI Window (XAML)              maui-window-xaml    [C#]    MAUI/Android/iOS/macOS/Mac Catalyst/WinUI/Tizen/Xaml/Code
Application .NET MAUI                maui                [C#]    MAUI/Android/iOS/macOS/Mac Catalyst/Windows/Tizen/Mobile
Application .NET MAUI Blazor         maui-blazor         [C#]    MAUI/Android/iOS/macOS/Mac Catalyst/Windows/Tizen/Blazor/Blazor Hybrid/Mobile
Bibliothèque de classes .NET MAUI    mauilib             [C#]    MAUI/Android/iOS/macOS/Mac Catalyst/Windows/Tizen/Mobile

Vous êtes libre d’installer d’autres composants tels que .NET desktop development ou encore, dans les composants individuels, le concepteur graphique de classe.

concepteur_classe

Création d’un projet

La création d’un projet est classique. On choisit un squelette .NET MAUI App et pour vous simplifiez l’arborescence, vous pouvez cocher la case ☑ Place solution and project in the same directory

Vous devrez également mettre votre installation Windows en mode développeur.

Android Emulator

L’étape suivante consiste à créer un Émulateur Android. L’émulateur est en réalité une machine virtuelle de type QEMU et sa création va prendre quelques minutes…

Vous pouvez également sélectionner votre smartphone Android si vous l’avez branché au PC. Vous devez au préalable avoir activé le mode developer sur votre smartphone.

personal_smartphone

Hello World

La création de votre première application est résumée sur la page de Microsoft suivante : First App

Le Hello World par défaut de MAUI se présente ainsi:

.NET 8 .NET 9

NuGet

NuGet est le gestionnaire de paquets de la plate forme de développement Microsoft .NET. C’est un logiciel libre et open source principalement développé par Microsoft.

Il est distribué sous forme d’extension des environnements de développement Visual Studio, SharpDevelop et Visual Studio Code. Il vise à simplifier le processus d’intégration de bibliothèques externes à une application développée dans .NET en automatisant certaines tâches répétitives : télécharger et installer une bibliothèque, modifier les paramètres de configuration, et répéter la même opération pour les autres bibliothèques auxquelles la première fait appel.

source: wikipedia

Lors du développement d’application .NET MAUI, on utilisera toujours le CommunityToolkit.MVVM. Il va énormément simplifier le développement de projet MAUI. Une fois votre futur projet crée, en cliquant avec le bouton droit de la souris sur l’entrée Dépendances de votre projet, vous pourrez vous rendre dans le gestionnaire de paquet NuGet

nuget

Et ajouter :

toolkits

Là aussi, la version à certainement déjà évolué.

MS-Learn path

Microsoft, et plus précisément James Montemagno, propose une série de 7 cours pour bien débuter avec .NET MAUI.

Votre travail consiste à suivre chacun de ces cours et réaliser les exercices pratiques liés.

Il ne faut pas 🏃 dans ces cours mais bien prendre le temps de les réaliser correctement, en lisant bien la théorie. Ce sont tous des concepts que vous devrez mettre en pratique dans toutes vos applications à venir.

Pour chaque cours listé ci-dessous, vous trouverez un lien vers le cours ainsi que quelques explications supplémentaires ou quelques questions.

1. Créer une interface utilisateur dans une application .NET MAUI avec XAML

Ça se passe ici : Create User Interface.

But

Dans ce tutoriel il y a l’explication du langage de balisage XAML, des descriptions de langage avec leur URI, les espaces de nom, notamment l’explication du x:Name, du code behind et des gestionnaires d’événements.

Exercice 1a (Create your first XAML page)

💬 Question: où se trouve le fichier notes.txt ?

%UserProfile%/AppData/Local/Packages/0A39EF64-0606-4941-8C45-EAB3CF9DE53D_9zz4h110yvjzm/LocalState/notes.txt”>

2. Créer une application multiplateforme avec .NET MAUI

Ça se passe ici : Create a cross-platform app with .NET MAUI.

But

Dans ce tutoriel il y a l’explication de la structure des fichiers et à quoi ils servent. On peut retrouver ces fichiers dans le projet Hello World crée précédemment.

On trouve également l’explication de la pile de développement MAUI

L’implémentation des interfaces natives. Noté au passage l’utilisation des interfaces de programmation orientée objet.

Exercice 2 (the phone number translator app)

Pour réaliser l’exercice du tutoriel vous n’avez pas besoin du précédent projet Phoneword. Vous pouvez partir de zéro.

You’ll continue with the Phoneword solution you created in the previous exercise

Exercice 3

Après avoir suivit ces deux tutoriels, vous êtes à même de changer:

Personnalisez votre application avec une icône, une police et un splashscreen de votre choix. Les images doivent être au format svg. Vous trouverez des polices libre de droits chez Google Fonts.

.NET MAUI converts SVG files to PNG files. Therefore, when adding an SVG file to your .NET MAUI app project, it should be referenced from XAML or C# with a .png extension.

3. Personnaliser la disposition dans les pages XAML .NET MAUI

Ça se passe ici : Customize XAML pages and Layout

But

Sont abordés dans cette partie, les différents Controls, Layouts, Pages et les Views.

Exercices mises en page

Il y a trois exercices de mise en page contenu dans l’unité. Ils mettent en pratique la mise en page des composants et l’utilisation des différents Layout.

Lors d’une mise en page de composant, pour bien comprendre ce qui se passe, il est utile de définir une couleur de fond pour l’objet et ainsi voir ses limites.

4. Concevoir des pages XAML .NET MAUI cohérentes à l’aide de ressources partagées

Ça se passe ici : Use shared resources

But

traite de l’utilisation des styles pour permettre l’obtention d’une application qui possède la même allure sur toutes les plateformes mais qui offre également la possibilité de facilement changer l’apparence des interfaces en modifiant le code à un seul endroit. L’unité traite aussi des ressources partagées qui évitent l’utilisation des nombres magiques.

5. Créer des applications .NET MAUI multi-pages avec onglet et navigation volante (flyout)

Ça se passe ici : Create multi-page apps

But

Comment créer une application contenant de multiple pages. Comment naviguer de page en page. C’est l’objectif de cette unité.

6. Consommer des services web REST dans les applications .NET MAUI

Ça se passe ici : Consume REST services

But

De nos jours, les services REST sont incontournables au même titre qu’une base de données. Pour consommer des services REST, vous devez savoir si votre connexion Internet est fonctionnelle. C’est le contenu de cette unité.

Cette unité est liée à ce dépôt git

Ce programme est plus compliqué que les précédents

Cet exercice utilise un programme qui fonctionne en deux parties.

Azure

Le backend proposé utilise un serveur chez Microsoft Azure. Normalement, vous devriez pouvoir réaliser cette partie gratuitement par l’intermédiaire de votre compte école.

7. Stocker des données locales avec SQLite dans une application .NET MAUI

Ça se passe ici : Store local data

But

Difficile d’imaginer une application pour laquelle rien n’est jamais stocké.

Imaginez que vous soyez entrain de remplir votre cadis virtuel, vous perdez la connexion réseau en passant dans un tunnel, est-ce que le cadis est perdu ? Faut-il tout recommencer ? Est-ce que vous devrez vous ré-authentifier ?

Dans cette unité, vous allez voir le stockage de valeurs dans

Un peu plus loin

Si on fouille dans l’emulateur

## On liste les devices
vous@machine:~$ adb devices
List of devices attached
emulator-5554   device
## On entre dans l'emulateur
vous@machine:~$ adb shell
## On passe en root (impossible sur un smartphone s'il n'est pas rooté)
generic_x86_arm:/ $ su
generic_x86_arm:/ #

On peut trouver l’application que l’on vient d’installer. Elle se trouve dans /data/app/com.companyname.people-avdl9o-iOgq06WkLV2rZHA==/. Le nom du dossier se termine par une valeur aléatoire encodée en base64. Se nom sera donc différent chez vous.

Dans le dossier on trouve l’apk qui est l’application elle-même empaquetée ainsi qu’un dossier lib dans lequel on trouve les librairies nécessaires au fonctionnement de l’application. On voit par exemple la librairie libe_sqlite3.so nécessaire à l’utilisation de la base de donnée SQLite.

-rwxr-xr-x 1 system system  849560 2022-06-29 07:38 libSystem.IO.Compression.Native.so
-rwxr-xr-x 1 system system   87788 2022-06-29 07:38 libSystem.Native.so
-rwxr-xr-x 1 system system  129040 2022-06-29 07:38 libSystem.Security.Cryptography.Native.Android.so
-rwxr-xr-x 1 system system 1657160 2022-06-29 07:38 libe_sqlite3.so
-rwxr-xr-x 1 system system  292604 2022-06-29 07:38 libmono-component-debugger.so
-rwxr-xr-x 1 system system  157384 2022-06-29 07:38 libmono-component-hot_reload.so
-rwxr-xr-x 1 system system  403048 2022-06-29 07:38 libmonodroid.so
-rwxr-xr-x 1 system system 3468836 2022-06-29 07:38 libmonosgen-2.0.so
-rwxr-xr-x 1 system system 1827304 2022-06-29 07:38 libxamarin-app.so
-rwxr-xr-x 1 system system   31388 2022-06-29 07:38 libxamarin-debug-app-helper.so

La base de donnée se trouve elle dans la sandbox

Some of this data might be sensitive, and you don’t want to save it to a location where it could be easily accessed by other apps or users. .NET MAUI apps provide the app sandbox. The app sandbox is a private area your application can work with. By default, no other applications can access this area other than the operating system. You can access the sandbox using the AppDataDirectory static property of the FileSystem class:

C#

string path = FileSystem.AppDataDirectory;

In this code, the path variable contains the file path to the location where you can store files for the application to use. You can read and write data to files in this folder using the techniques shown in the section How to use the file system.

Sur Android ça se passe ici

generic_x86_arm:/ # ls -la data/data/
drwxrwx--x 112 system         system         4096 2022-06-29 07:10 .
drwxrwx--x  38 system         system         4096 2022-06-15 22:04 ..
drwx------   4 system         system         4096 2022-06-15 22:04 android
...
drwx------   5 u0_a86         u0_a86         4096 2022-06-29 07:38 com.companyname.people

Le nom com.companyname est paramétrable dans le projet. On voit que le dossier est réglé avec des droits stricts. Pour chaque application, un utilisateur et un groupe sont crées au niveau du système. Ensuite, seul cet utilisateur à les droits de rwx pour ce dossier.

Dans le dossier on trouve entre autre un répertoire files et un lien sur les librairies (vues plus haut dans ce document).

generic_x86_arm:/ # ls -la data/data/com.companyname.people/
total 28
drwx------   5 u0_a86 u0_a86       4096 2022-06-29 07:38 .
drwxrwx--x 112 system system       4096 2022-06-29 07:10 ..
drwxrws--x   2 u0_a86 u0_a86_cache 4096 2022-06-29 07:10 cache
drwxrws--x   2 u0_a86 u0_a86_cache 4096 2022-06-29 07:10 code_cache
drwxrwxrwx   6 u0_a86 u0_a86       4096 2022-06-29 07:11 files
lrwxrwxrwx   1 root   root           65 2022-06-29 07:38 lib -> /data/app/com.companyname.people-avdl9o-iOgq06WkLV2rZHA==/lib/x86

Dans le dossier files on trouve la base de donnée

generic_x86_arm:/ # ls -la data/data/com.companyname.people/files/
total 56
drwxrwxrwx 6 u0_a86 u0_a86  4096 2022-06-29 07:11 .
drwx------ 5 u0_a86 u0_a86  4096 2022-06-29 07:38 ..
...
-rw------- 1 u0_a86 u0_a86 16384 2022-06-29 07:11 people.db3

Là encore, les droits sont stricts. Seul l’application à les droits de lecture et d’écriture sur la base. C’est au niveau du système de fichier que la restriction est effectuée.

Si on copie la base de donnée localement sur notre machine de développement, par exemple en passant par le répertoire /sdcard/Download/, on peut examiner son contenu à l’aide d’un client sqlite ainsi

## On tire la base sur la machine local
vous@machine:~$ adb pull /sdcard/Download/people.db3 .
/sdcard/Download/people.db3: 1 file pulled. 9.4 MB/s (16384 bytes in 0.002s)
## On l'ouvre avec un client SQLite
vous@machine:~$ sqlite3 people.db3
SQLite version 3.37.2 2022-01-06 13:25:41
Enter ".help" for usage hints.
## On examine son contenu
sqlite> .schema
CREATE TABLE IF NOT EXISTS "people" (
"Id" integer primary key autoincrement not null ,
"Name" varchar(250) );
CREATE TABLE sqlite_sequence(name,seq);
CREATE UNIQUE INDEX "people_Name" on "people"("Name");
## On peut lister le contenu de la table
sqlite> SELECT * FROM people;
1|Pierre
2|Paul
3|Jacques
sqlite> .quit

Ce qui correspond à l’application

.NET MAUI Workshop

Plusieurs acteurs du Framework, dont principalement James Montemagno se sont réunit pour mettre en place ce Workshop. Vous y apprendrez à réaliser une application du début à la fin.

Le Workshop peut-être réalisé en mode lecture. Pour cela, il suffit de cloner le dépôt git et d’avancer dans les parties 1 à 6.

Il est également possible (recommander) de suivre le cours vidéo et de reproduire le code petit à petit.

Encore une fois, ne courez pas. Prenez bien le temps de comprendre le code que vous réalisez.

Weather app

Après avoir réaliser ces applications en vous faisant tenir par la main, il est temps de réaliser votre propre application.

Le sujet de cette application est imposé. Il s’agit d’une application météo utilisant les données du site openweathermap. Vous devrez faire la demande pour obtenir une clé d’API et ainsi être en mesure de faire des requêtes REST sur le service

https://api.openweathermap.org/data/2.5/weather

Vous réaliserez cette application en 2 temps.

Première partie

Afin de vous familiariser avec l’API du service REST, vous réaliserez une simple application Console. Cette application affichera les données météos en utilisant une géolocalisation codée en dur. Le programme principal se résume à

using WeatherApp;

RestService restService = new RestService();

string url = String.Format($"{Constants.OpenWeatherMapEndpoint}?lat=46.992979&lon=6.931933&units=metric&appid={Constants.OpenWeatherMapAPIKey}");

WeatherData? weatherData = await restService.GetWeatherData(url);

if (weatherData != null)
{
    Console.WriteLine($"Where : {weatherData.Title}");
    Console.WriteLine($"------------------------");
    Console.WriteLine($"Clouds: {weatherData.Weather?[0].Description}");
}

Seconde partie

Maintenant que votre service REST est fonctionnel et que vous avez compris comment utiliser le résultat de l’API, vous pouvez passer au développement de l’application en utilisant le Framework .NET MAUI. L’application devra être fonctionnelle sur Android et sur Windows.

L’application mettra en pratique le concept MVVM

graph LR;
    A[Service]-->B[Model]-->C[ViewModel]-->D[UI simple];

Méthodologie

On peut aborder le projet de deux manière.

Technique First

On s’occupe d’abords de la partie technique, le service, le modèle, le viewModel et on s’occupera de l’interface graphique plus tard.

Cette approche nous fera gérer les problèmes tels que

Par exemple, si le service REST nous fournit une valeur telle que (string) "23.456", comment afficher cette valeur avec une seule décimale ainsi 23.4°C. Faut-il déjà prévoir une conversion de string à double ?

Comment préparer la valeur pour pouvoir plus tard, la binder dans un Label de l’interface XAML.

l’API REST nous fournit un sunset sous la forme d’un timestamp, soit, le nombre de secondes écoulées depuis le 1.1.1970 à minuit, par exemple 1665421742. Elle fournit aussi une valeur timezone, par exemple 7200. Comme afficher cette valeur sous la forme d’une date telle que “10 octobre 2022 à 17.09.02 GMT” ou mieux encore, avec le respect du fuseau horaire pour la suisse “10 octobre 2022 à 19.09.02 Europe/Zurich”.

Le problème, lorsqu’on commence par l’aspect technique, c’est qu’on risque de perdre du temps à gérer l’affichage permettant de valider le fonctionnement de notre MVVM pour qu’il soit pas trop moche alors que l’interface graphique finale n’est pas encore décidée. Il faut rapidement s’arrêter dans la gestion de l’affichage à quelque chose de potable telle que:

Une fois que toutes les valeurs sont correctement récupérées, alors on pourra s’attacher à réaliser une interface graphique jolie en relation avec les besoins du client (s’il existe un client).

UI First

Dans cette approche, on utilisera des valeurs codées en dur. Par exemple, pour un Label devant afficher la température

<Label
    x:Name="lblTemp"
    Text="23.4°C" />

On définira précisément ce qui doit être affiché et comment. On se chargera de l’aspect technique plus tard.

Par exemple, si on souhaite indiquer la direction du vent avec une rose des vents

On commencera avec une image fixe, à la bonne place, de la bonne taille et on verra plus tard comment animer l’aiguille.

Pour concevoir l’interface graphique, il n’y a pas de logiciel dédié. C’est le rechargement à chaud (hot reload) qui pourra vous être utile. Le principe consiste à démarrer le programme, placer la fenêtre du programme sur le côté et faire les modifications dans le code XAML jusqu’à obtenir l’interface souhaitée.

Attention, le rechargement à chaud à ses limites. Parfois, il y a déclenchement d’exception qui le font planter et après cela, les modifications n’apparaissent plus. On se retrouve alors à faire des modifications sans qu’elles apparaissent à l’écran et on fini par ne plus rien comprendre 🤪. Il faut alors simplement redémarrer le programme.

UI Exemple

L’enseignant à réalisé cette application. Vous pouvez remarquer que l’aiguille de la rose de vent est animée 😎. À vous de réaliser votre application avec vos idées.

Questions à se poser

Pour que votre application fonctionne correctement, vous devez vous posez les questions suivantes:

Application Map

On continue avec l’application Weather-App et ajoute la possibilité de sélectionner une météo en choisissant l’endroit grâce à une map.

Background

Utiliser une map dans .NET MAUI est un peu compliqué parce qu’à la base, les développeurs du framework ont choisi de binder l’élément Map à la librairie native

The Map control uses the native map control on each platform…

Et la librairie native fonctionne avec Google Map. Or, Google Map nécessite une clé d’API et cette clé d’API est payante. Il est possible de l’obtenir gratuitement mais il faut donner des informations de carte de crédit et ni vous ni moi ne voulons donner ces informations.

Il existe une alternative à Google Map, c’est OpenStreetMap. OpenStreetMap est une carte libre et gratuite. Elle est utilisée par exemple dans l’application OsmAnd.

Il est possible de l’utiliser dans .NET MAUI mais il faut passer par une librairie tierce. Tout cela est expliqué ici.

Bluetooth BLE

Projet Bluetooth BLE

Informations complémentaires

Ci-dessous, vous pouvez trouver des informations complémentaires. Elles ne sont pas absolument nécessaire pour réaliser des petites applications mais elles vous permettront de mieux comprendre le fonctionnement de .NET MAUI.

Cycle de vie

Pourquoi c’est la MainPage qui est affichée en premier ? Pourquoi le titre de la page est Home ? Qu’est-ce qui démarre l’application ?

Toutes ces questions se résume à une seule réponse: le cycle de vie de l’application.

cycle_vie

En regardant le cycle de vie de l’application, on peut comprendre pourquoi il y a le titre Home sur le haut de l’écran. Il a simplement été définit au niveau du ShellContent.

Conception graphique

Dans .NET MAUI, le contrôle principal utilisé pour créer des interfaces utilisateurs est la Page. La Page se place généralement en pleine écran.

pages

Tous ces types de Page héritent de la classe VisualElement.

class_diagram

La Page contient un Layout

layouts

Qui contient à son tour des Vues et possiblement d’autres Layout. Parmi les Vues on trouve :

Nom Exemple
Frame frame-card
Shapes dashed-rectangle
Entry entry-clearbutton
TableView menu

Et une multitude d’autres composants permettant la création d’interfaces riches.

La conception

La conception graphique peut se faire par code mais la méthode recommandée et le XAML.

Prenons comme exemple l’application de base fournie avec le template .NET MAUI dans Visual Studio.

Le code XAML correspondant est le suivant. On y retrouve facilement tous les éléments.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiAppBasic.MainPage">
    <ScrollView>
        <VerticalStackLayout
            Spacing="25"
            Padding="30,0"
            VerticalOptions="Center">

            <Image
                Source="dotnet_bot.png"
                SemanticProperties.Description="Cute dot net bot waving hi to you!"
                HeightRequest="200"
                HorizontalOptions="Center" />

            <Label
                Text="Hello, World!"
                SemanticProperties.HeadingLevel="Level1"
                FontSize="32"
                HorizontalOptions="Center" />

            <Label
                Text="Welcome to .NET Multi-platform App UI"
                SemanticProperties.HeadingLevel="Level2"
                SemanticProperties.Description="Welcome to dot net Multi platform App U I"
                FontSize="18"
                HorizontalOptions="Center" />

            <Button
                x:Name="CounterBtn"
                Text="Click me"
                SemanticProperties.Hint="Counts the number of times you click"
                Clicked="OnCounterClicked"
                HorizontalOptions="Center" />

        </VerticalStackLayout>
    </ScrollView>
</ContentPage>

Malheureusement, il n’existe pas de concepteur graphique pour .NET MAUI tel que celui que l’on trouve pour les applications Windows Forms ou WPF. Les développeurs de Microsoft ont longuement expliqué pourquoi dans une vidéo conférence. En résumé, il est très difficile de créer un concepteur graphique qui soit multiplateforme. Pour compenser ce problème, ils ont fait le choix de fournir un Hot Reload qui permet de voir les modifications apportées au code XAML en temps réel. Ainsi, on code l’interface pendant que le programme fonctionne et on voit directement les modifications s’opérer.

{Binding}

Binder signifie faire la liaison de données entre la Vue et le code behind, ou un service ou encore un ViewModel. Le binding est nécessaire car la Vue est construite en XAML et à priori, le designer qui s’occupe de la Vue ne sait pas comment les données sont remplies. Il sait qu’elles existent, de quelles type elles sont et ce qu’elles contiennent. Il doit alors indiquer au code XAML comment aller les chercher. C’est le Binding.

Prenons l’exemple de base fournit avec .NET MAUI. Il y a un bouton qui déclenche un événement. Dans le gestionnaire d’événement code behind on incrémente un attribut et on met à jour le texte du bouton. Tout se fait grâce au code, il n’y a pas de binding.

private void OnCounterClicked(object sender, EventArgs e)
{
    count++;

    if (count == 1)
        CounterBtn.Text = $"Clicked {count} time";
    else
        CounterBtn.Text = $"Clicked {count} times";
}

Ajoutons un Label nommé LabelCounter sur la vue et affichons le nombre de clique dans la propriété Text. On peut facilement le faire à la code behind ainsi

XAML

<Label
    x:Name="LabelCounter"
    Text=""
    FontSize="32"
    HorizontalOptions="Center" />

C#

if (count == 1)
{
    CounterBtn.Text =  $"Clicked {count} time";
}
else
{
    CounterBtn.Text = LabelCounter.Text = $"Clicked {count} times";
}

LabelCounter.Text = $"{count}";

On peut constater que le Label se met bien à jour à chaque click du bouton.

Si on ne souhaite pas faire de code behind il faudra alors Binder la valeur dans le Label. Ce sera le cas lorsque, par exemple, la valeur ne proviendra d’un service REST. Ce n’est pas le code behind qui changera la valeur mais le Service qui nous fournira une valeur provenant d’Internet.

Essayons de reproduire simplement ce cas de figure. Premièrement, on crée une propriété car seul les propriétés qui sont public, sont visibles en dehors de la classe.

int count = 0;
public int Count
{
    get { return count; }
    set { count = value; }
}
private void OnCounterClicked(object sender, EventArgs e)
{
    Count++; // On travail avec la propriété !!! C'est bien un 'C' majuscule.
}

Ensuite, on Bind la valeur côté XAML. Pour ce faire, on peut procéder de différente manière.

Différentes manières de binder

Au niveau du Label

Le BindingContext donne le chemin où aller chercher la valeur. Dans ce cas, on choisi un chemin relatif et on va chercher le ContentPage. En effet, le ContentPage est notre MainPage et c’est dans la classe MainPage que se trouve la propriété Count. On Bind ensuite la valeur directement au Text.

<Label
   BindingContext="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}}"
   Text="{Binding Count}" />

Au niveau du ContentPage

Cette fois, on donne un BindingContext directement dans le ContentPage. On réalise un Binding sur lui-même. Dans le Label on bind seulement la propriété au Text

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             BindingContext="{Binding Source={RelativeSource Self}}"
             x:Class="MauiBLE.MainPage">

    <Label
         Text="{Binding Count}" />

Avec une référence

On donne un nom à la source qui contient la donnée et on l’utilise dans le Binding

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiBLE.MainPage"
             x:Name="ThisPage"> <!--- Ici -->

     <Label
         BindingContext="{Binding Source={x:Reference ThisPage}}"
         Text="{Binding Count}" />

All in one

On peut réunir le BindingContext et la propriété en une seule ligne ainsi

<Label
    Text="{Binding Source={x:Reference ThisPage}, Path=Count}" />

Assembly (rien à voir avec Count mais utile à savoir)

On peut Binder une valeur qui se trouve dans un assembly ainsi

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiBLE.MainPage"
             xmlns:sys="clr-namespace:System;assembly=netstandard"> <!-- on référence l'assembly ici -->
    <Label
        Text="{Binding Source={x:Static sys:DateTime.Now}, StringFormat='{0:HH\:mm\:ss}'}" />

Test de fonctionnement

J’ai choisi le binding suivant

<Label
    BindingContext="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}}"
    Text="{Binding Count}"
    FontSize="32"
    HorizontalOptions="Center" />

On peut constater que la propriété apparaît dans le Label au démarrage de l’application, lors du chargement de la Vue mais lorsqu’on clique sur le bouton, il ne se passe rien dans le Label 🤔. On peut vérifié ce fait en plaçant la valeur 10 dans count. Il y a bien la valeur 10 dans le Label mais cette valeur reste à 10 et elle ne change pas.

public MainPage()
{
    InitializeComponent();
    Count = 10;
}

Ce n’est pas un problème de Binding mais simplement le fait que la Vue n’est pas informée du changement de valeur dans la propriété.

Il faut bien utiliser la propriété et pas l’attribut! C’est bien Count et pas count (C majuscule) !

OnPropertyChange

Pour que la vue soit notifiée du changement de valeur, on doit ajouté le code qui le fait.

int count = 0;

public MainPage()
{
    InitializeComponent();
    Count = 10;
}

public int Count
{
    get { return count; }
    set
    {
        count = value;
        OnPropertyChanged(nameof(Count)); // On notifie la Vue du changement de valeur
    }
}

private void OnCounterClicked(object sender, EventArgs e)
{
    Count++;
}

binding

Remarque

OnPropertyChanged est utilisable dans ce cas car MainPage implémente déjà l’interface INotifyPropertyChanged.

On peut le valider en regardant l’aide de la classe ContentPage :

Inheritance: ObjectBindableObjectElementNavigableElementVisualElementPageTemplatedPageContentPage

Et parmi cette suite d’héritage on trouve le BindableObject qui lui :

Implements: IDynamicResourceHandler, INotifyPropertyChanged

La classe BindableObject défini une méthode

public abstract class BindableObject : INotifyPropertyChanged, IDynamicResourceHandler
{
    ...

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)

Cette méthode est donc accessible à qui hérite de cette classe et comme elle est virtuelle, on peut la réécrire. C’est exactement ce qu’on fait.

Entry

On peut également ajouter un Entry pour valider les changements. On se rend compte que si on change la valeur dans l’Entry ça change aussi directement dans le Label

<Frame
    BackgroundColor="LightSkyBlue"
    BorderColor="BlueViolet"
    HasShadow="True"
    Margin="0"
    Padding="0,0,0,10">
    <Entry
        x:Name="InputCount"
        Text="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=Count}" />
</Frame>

<Label
    Text="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=Count}" />

binding_entry

MVVM

Le modèle-vue-vue modèle (en abrégé MVVM, de l’anglais Model View ViewModel) est une architecture et une méthode de conception utilisée dans le génie logiciel.

Apparu en 2004, MVVM est originaire de Microsoft et adapté pour le développement des applications basées sur les technologies Windows Presentation Foundation et Silverlight via l’outil MVVM Light par exemple. Cette méthode permet, tel le modèle MVC (modèle-vue-contrôleur), de séparer la vue de la logique et de l’accès aux données en accentuant les principes de liaison et d’événement.

source: wikipedia

Anatomie

Dans l’arborescence des fichiers d’un projet .NET MAUI, on retrouve différent répertoires. Ci-dessous, ceux de l’application du Workshop, Monkey Finder.

structure_projet

On trouve dans cette structure, entre autre, les dossiers View, ViewModel, Model et Services. Ces dossiers vont nous permettre de respecter l’anatomie MVVM.

Model

Son rôle est la représentation des données. Par exemple, dans l’application Monkey Finder qui représente des singes, le modèle correspond à

public class Monkey
{
    public string Name { get; set; }
    public string Location { get; set; }
    public string Details { get; set; }
    public string Image { get; set; }
    public int Population { get; set; }
    public double Latitude { get; set; }
    public double Longitude { get; set; }
}

On voit qu’il représente les données caractérisant un singe.

Service

Le modèle sera remplit par l’intermédiaire d’un service. Par exemple, le service réalisera une requête https pour obtenir les données depuis un serveur distant. C’est le cas de l’application Monkey Finder.

public class MonkeyService
{
    private List<Monkey> monkeyList;
    private HttpClient httpClient;

    public MonkeyService()
    {
        monkeyList = new List<Monkey>();
        httpClient = new HttpClient();
    }

    public async Task<List<Monkey>> GetMonkeys()
    {
        if (monkeyList?.Count > 0)
        {
            return monkeyList;
        }

        HttpResponseMessage response = await httpClient.GetAsync("https://www.montemagno.com/monkeys.json");

        if (response.IsSuccessStatusCode)
        {
            monkeyList = await response.Content.ReadFromJsonAsync<List<Monkey>>();
        }

        return monkeyList;
    }
}

C’est via la méthode GetMonkeys qu’on obtiendra une List<Monkey>.

ViewModel

C’est la passerelle entre la Vue et le Model. C’est lui qui ira chercher les informations dans le Model et qui les exposera à la Vue pour que cette dernière soit à même de les afficher grâce au DataBinding.

Le ViewModel doit être indépendant de la Vue. C’est-à-dire qu’il doit pouvoir être utiliser avec d’autre Vue. Par exemple, si le ViewModel met à disposition une propriété pour obtenir les singes, cette propriété doit pouvoir être utilisée par différentes Vue et ne pas être liée, par exemple, à la manière d’afficher les données dans une Vue en particulier.

Dans l’application Monkey Finder, le ViewModel pourrait se résumer à

public partial class MonkeysViewModel : BaseViewModel
{
    private List<Monkey> monkeys;           // la collection de singes

    public List<Monkey> Monkeys => monkeys;

    public MonkeysViewModel(MonkeyService monkeyService) // On obtient la référence du service grâce au DI
    {
        Title = "Monkey Finder";
        this.monkeys = new List<Monkey>();

        List<Monkey> monkeysFromService = await monkeyService.GetMonkeys();

        Monkeys.Clear();

        foreach (Monkey monkey in monkeysFromService)
        {
            Monkeys.Add(monkey);
        }
    }

Il fournit bien une liste de singes via la propriété public Monkeys. Une Vue qui souhaite obtenir les singes n’a qu’à instancier un objet MonkeysViewModel et aller les chercher dans la propriété Monkeys.

Vue

C’est le rendu des informations. Le XAML décrit le comment afficher les informations. C’est dans la Vue qu’on va choisir d’afficher des Label, des ListView etc. Le couplage de la Vue et du ViewModel se fait avec le BindingContext. Par exemple :

BindingContext = viewModel;

RelayCommand

La gestion des interactions entre la Vue et le ViewModel passera par les RelayCommand. Le ViewModel exposera des RelayCommand auxquels la Vue pourra s’abonner. Par exemple, dans l’application Monkey Finder, le ViewModel fournit la RelayCommand suivante (simplifiée)

[RelayCommand]
public async Task GetMonkeysAsync()
{
    try
    {
        List<Monkey> monkeysFromService = await monkeyService.GetMonkeys();
        Monkeys.Clear();

        foreach (Monkey monkey in monkeysFromService)
        {
            Monkeys.Add(monkey);
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"Unable to get monkeys: {ex.Message}");
        await Shell.Current.DisplayAlert("Error!", ex.Message, "OK");
    }
}

Et la Vue s’est abonner à cette commande dans le bouton libellé Get Monkeys

<Button
     Text="Get Monkeys"
     Command="{Binding GetMonkeysCommand}" />

Les RelayCommand font appel au CommunityToolkit.MVVM. C’est ce kit qui va générer le code automatiquement. Par exemple pour le RelayCommand ci-dessus, le code généré sera:

// <auto-generated/>

namespace MonkeyFinder.ViewModel
{
    partial class MonkeysViewModel
    {
    /// <summary>The backing field for <see cref="GetMonkeysASyncCommand"/>.</summary>
    [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.0.0.0")]
    private global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand? getMonkeysASyncCommand;
    /// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand"/> instance wrapping <see cref="GetMonkeysASync"/>.</summary>
    [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.0.0.0")]
    [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    public global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand GetMonkeysASyncCommand => getMonkeysASyncCommand ??= new global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand(new global::System.Func<global::System.Threading.Tasks.Task>(GetMonkeysASync));
    }
}

Il se trouve ici:

Dependency Injection

Commençons par un rappel. Il existe plusieurs façon de lié des classes entre elles.

Relations

Notation A ┈┈> B A ───> B A ◊───> B A ⬧───> B
Liaison Dépendance Association Agrégation Composition
Explication A a besoin de B pour fonctionner A connaît B B fait partie de A. B peut exister sans A. B est une partie de A et disparait avec A si A est détruit
Exemple A utilise une méthode de B Le chauffeur sait quel est son camion mais ne fait pas partie du camion. La batterie fait partie du téléphone et on peut la récupérer si on détruit le téléphone. Les chambres d’hôtel font parties de l’hôtel mais elles sont détruites avec lui.

Association

class Teacher { }

class Student
{
    List<Teacher> teachers;
}

Dans cette relation, l’étudiant possède une liste d’enseignants. Il connaît donc ses enseignants. Mais il n’a aucune dépendance aux enseignants. Les deux classes peuvent existé indépendamment l’une de l’autre.

Agrégation

class CellBattery { }

class CellPhone
{
    CellBattery _battery;

    void CellPhone (CellBattery battery)
    {
       _battery = battery;
    }
}

public static Main(string[] args)
{
    CellBattery battery = new CellBattery();
    CellPhone phone = new CellPhone(battery);
}

Dans cette relation, la batterie fait partie du téléphone mais si le téléphone est défectueux, par exemple avec un écran cassé, je peux récupérer la batterie pour la placer dans un autre téléphone. Dans le constructeur, on reçoit une batterie qui existe déjà et on va simplement copier sa référence dans l’attribut. Si le téléphone est détruit, la batterie existe toujours.

Composition

class Hotel
{
    List<Room> rooms = new List<Room>();
    public void AddRoom(string name, string numero)
    {
        rooms.Add(new Room(name, numero));
    }
}

class Room
{
    public string Name { get; set; }
    public string Numero { get; set; }
    public Room(string name, string numero) { Name = name; Numero = numero; }
}

Dans cette relation, les chambres font parties intégrantes de l’hôtel. Si on détruit l’hôtel, on détruit aussi les chambres.

Injection de dépendance

class GreetingSender
{
    EmailSender _emailSender;
    void SendGreetings(EmailSender emailSender)
    {
        _emailSender = emailSender;
        _emailSender.SendEmail();
    }
}

class EmailSender
{
    public void SendEmail()
    {
       //Send Email
    }
}

On retrouve ici une relation d’agrégation mais l’idée qui se cache derrière l’injection de dépendance c’est qu’on aimerait utiliser une fonctionnalité d’un autre objet dans notre classe. Ici la classe GreetingSender aimerait utiliser la fonctionnalité d’envoi SendEmail de la classe EmailSender.

Dans sa version la plus simple, le programme va devoir instancier un objet EmailSender grâce à l’opérateur new et ensuite, passer la référence à la construction de l’objet GreetingSender. On parle d’injection par constructeur

EmailSender sender = new EmailSender();
GreetingSender greeting = new GreetingSender(sender);

Cette implémentation permet d’informer, l’utilisateur de la classe GreetingSender des dépendances à utiliser et de s’assurer que celles-ci soient instanciées en même temps que la création de l’objet.

💀 Problème 💀

Dans cet exemple, il y a un problème. Si le développeur de la classe EmailSender apporte une modification à sa méthode, par exemple en ajoutant un paramètre, toutes les classes qui utilisent cette méthode seront cassées. Dans cette exemple, la classe EmailSender contrôle la manière dont GreetingSender fonctionne. La liaison entre ces deux classes est trop forte.

Pour réduire cette liaison, on va faire une liaison abstraite entre les deux classes. La liaison sera de type je peux envoyer un message mais il n’y aura pas de liaison sur le comment envoyer le message. Le comment sera définit plus tard. On crée donc une interface ISender ainsi

interface ISender
{
    void Send();
}

Pour définir un comment, une classe implémentera l’interface. Par exemple, pour un envoi par email

public class EmailSender: ISender
{
    void Send()
    {
        Mailer mailer = new Mailer();
        mailer.Send();
    }
}

Mais pourquoi pas par SMS

public class SMSSender : ISender
{
    void Send()
    {
       SMS sms = new SMS();
       sms.Send();
    }
 }

L’injection de dépendance se fait sur l’interface et plus sur l’objet. On injecte la fonctionnalité mais pas le comment

class GreetingSender
{
    ISender _sender;
    public GreetingSender(ISender sender)
    {
        _sender = sender;
    }
    void SendGreetings()
    {
        _sender.Send();
    }
}

On peut alors utiliser l’injection de dépendance ainsi

//Send Greeting through Email
EmailSender emailSender = new EmailSender();
GreetingSender greetingsEmailSender = new GreetingSender(emailSender);
greetingsEmailSender.SendGreetings();

//Send Greeting through SMS
SMSSender smsSender = new SMSSender();
GreetingSender greetingsSMSSender = new GreetingSender(smsSender);
greetingsSMSSender.SendGreetings();

😎 Et le problème 😎

Si le développeur de la classe EmailSender apporte une modification à sa méthode, par exemple en ajoutant un paramètre, il le fera sur sa méthode interne mailer.Send(newParameter);. Il peut ainsi modifier le comment son objet travail sans impacter les classes qui l’utilisent.

public class EmailSender: ISender
{
    void Send()
    {
        Mailer mailer = new Mailer();
        mailer.Send(token);             // <- ajout d'un nouveau paramètre
    }
}

class GreetingSender
{
    ISender _sender;
    public GreetingSender(ISender sender)
    {
        _sender = sender;
    }
    void SendGreetings()
    {
        _sender.Send();                 // <- ça ne change rien sur la manière de l'utiliser
    }
}

Il nous reste un problème à résoudre. Chaque fois qu’on veut utiliser un objet dépendant d’un autre, on doit au préalable instancier cet autre objet via l’opérateur new. C’est donc à nous de gérer les dépendances. On doit aussi faire face au fait que certaines instances doivent être instanciés qu’une seule fois. C’est les singleton. Tout cela peut vite devenir un casse-tête. C’est là qu’intervient l’IOC (Inversion Of Control).

IOC, Inversion Of Control

L’IOC se défini comme un conteneur qui détermine ce qui doit être instancié et retourné au client pour éviter que ce dernier appel explicitement le constructeur avec l’opérateur new. En résumé, c’est un objet qui agit comme un cache pour les instances dont nous avons besoin dans les diverses parties de notre application.

.NET MAUI

Lorsqu’on travail dans le Framework .NET MAUI, on se trouve confronté à ce genre de pile.

graph TD
Page --> ViewModel
ViewModel --> RESTService

Une Page contient une référence au ViewModel. Le ViewModel contient une référence sur le service RESTService qui lui permettra de récupérer des valeurs via une requête https.

Exemple pour le ViewModel

public class MyViewModel : BaseViewModel
{
    MyService myService;
    public MyViewModel(MyService myService)
    {
        Title = "My Title";
        this.myService = myService;
    }
}

Comme on peut le voir, on est dans de l’injection de dépendance. La question qu’on peut légitimement se poser c’est où est-ce que l’objet MyService a été instancié ?

Services

.NET MAUI fournit une collections permettant de stocker les références et ainsi faire de l’injection de dépendance facilement. Pour reprendre l’exemple ci-dessus où on souhaite injecter la dépendance MyService à la classe ViewModel, on le fait simplement dans le fichier MauiProgram.cs :

namespace MauiAppBasic;

using MauiAppBasic.Services;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

        // Add this
        // ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓
        builder.Services.AddSingleton<MyService>();
        builder.Services.AddSingleton<MyViewModel>();

        return builder.Build();
    }
}

Si on souhaite injecter, par exemple un modèle dans la page principale, il faudrait pouvoir modifier le constructeur de ça

public MainPage()

à ça

public MainPage(MyViewModel viewModel)

C’est pour cette raison qu’on devra également injecter la MainPage

builder.Services.AddSingleton<MainPage>();

Singleton vs Transient

La méthode AddSingleton permet d’ajouter une référence qui se sera instanciée qu’une seule fois. AddTransient permet d’ajouter une référence qui pourra être instancier plusieurs fois.

Par exemple, si vous injectez la MainPage, et que cette page ne doit être instanciée qu’une seule fois au démarrage de l’application, on utilise un Singleton

builder.Services.AddSingleton<MainPage>();

Si au contraire, on injecte une page de détails pour voir les détails d’un magasin et que cette page sera instanciée à chaque fois que l’on clique sur un magasin, on choisira le mode Transient ainsi

builder.Services.AddTransient<StoreDetailsPage>();

Références

Tips

Utilisation des ToDo

On peut facilement gérer une liste de tâches dans un IDE. C’est très utile lorsqu’on doit penser à faire quelque chose. Par exemple, vous êtes en pleine session de débogage et vous placez un Debug.WriteLine pour vous aider à comprendre ce qui ne fonctionne pas correctement. Bien entendu, il faudra supprimer cette ligne de débogage avant de publier l’application et pour cela, il faudra s’en souvenir ! Alors on peut ajouter une tâche ainsi

catch(TaskCanceledException ex)
{
    //ToDo remove debug writeline in release
    Debug.WriteLine(ex.Message, this.GetType().Name);

La ligne de commentaire ToDo ajoute une tâche qu’on peut visualiser dans la fenêtre du même nom

Compilation conditionnelle

On peut choisir les lignes de codes qui doivent être compilées en fonction de certaines conditions. Par exemple, on pourrait souhaiter compiler certaines lignes de codes uniquement lorsqu’on est en mode Debug.

catch (Exception ex)
{
#if DEBUG
    Debug.WriteLine(ex.Message, this.GetType().Name);
#endif
}

Dans cette exemple, la ligne Debug.WriteLine ne sera exécutée que si on est en mode Debug et simplement ignorée si on est en mode Release. On peut d’ailleurs constater que l’IDE met cette ligne en grisée lorsqu’on est en mode Release.

Debug.WriteLine

Lorsqu’on utilise la fonction Debug.WriteLine on trouve l’affichage dans l’onglet Sortie

[monodroid-assembly] open_from_bundles: failed to load assembly System.ComponentModel.Primitives.dll
Loaded assembly: /data/data/com.companyname.weatherapp/files/.__override__/System.ComponentModel.Primitives.dll [External]
[0:] WeatherViewModel: Latitude: 41.890265, Longitude: 12.492141666666665, Altitude: 0
[monodroid-assembly] open_from_bundles: failed to load assembly System.Net.Http.dll

En utilisant la fonction WriteLine ainsi

Debug.WriteLine($"Latitude: {WhereAmI.Latitude}, Longitude: {WhereAmI.Longitude}, Altitude: {WhereAmI.Altitude}", this.GetType().Name);

On pourra trouver la classe qui a émit le message, ici c’est la classe WeatherViewModel

    |     CLASSE      |                 MESSAGE
[0:] WeatherViewModel: Latitude: 41.890265, Longitude: 12.492141666666665, Altitude: 0

Erreur HttpClient

Lors de l’utilisation des requêtes avec le HttpClient, on peut être confronté à une Exception lors du second click sur le bouton Request Datas. L’exception n’est pas très explicite puisque à l’écran on voit uniquement ceci

Pour comprendre d’où vient l’erreur, il faut impérativement utiliser des blocks try/catch

try
{
    NetworkAccess accessType = Connectivity.Current.NetworkAccess;
    if (accessType == NetworkAccess.Internet)
    {
        client.DefaultRequestHeaders.Accept.Clear();
        client.Timeout = TimeSpan.FromSeconds(10.0);

        Task<Stream> streamTask = client.GetStreamAsync(query);

        weatherData = await JsonSerializer.DeserializeAsync<WeatherData>(await streamTask);
    }
    else
    {
        weatherData = GetDefault();
    }
}
catch (Exception ex)
{
    Debug.WriteLine(ex.Message);
}

En faisant cela, on verra apparaître dans la fenêtre de Sortie le message d’erreur

[0:] This instance has already started one or more requests. Properties can only be modified before sending the first request.

L’erreur provient de la redéfinition des propriétés DefaultRequestHeaders et Timeout alors que le client est instancié une seule fois. On a donc deux choix :

  1. Créer un nouvel objet HttpClient à chaque requête, c’est-à-dire, dans la méthode qui en fait l’usage et on Dispose() l’objet à la fin en ajoutant une clause finally
  2. On instancie l’objet HttpClient dans le constructeur et on définit une seule fois les deux propriétés directement dans le constructeur

Erreur qui n’en n’est pas une

Cette erreur est particulière. Lors de mes essais, je me suis trouvé dans la situation suivante. Mon ordinateur portable n’a pas de connexion Internet mais l’émulateur Android pense que oui . Résultat, lors du passage par le test ci-dessous

NetworkAccess accessType = Connectivity.Current.NetworkAccess;
if (accessType == NetworkAccess.Internet)

Le test est toujours vrai malgré que le portable ne peut pas atteindre Internet et on tombe finalement dans le timeout du client HttpClient.

Ce n’est pas une vraie erreur et il ne faut pas passer des heures à déboguer ce faux problème. On peut simplement le contourner en mettant une valeur par défaut au départ du code. Cette valeur sera écrasée par la valeur reçue avec la requête web si cette dernière abouti.

On peut également intercepter l’Exception pour avertir l’utilisateur du fait que les données affichées ne sont pas celles souhaitées mais celles par défaut.

try
{
    weatherData = GetDefault(); // If there is no connection we use default values

    ...
}
catch(TaskCanceledException ex)
{
    Debug.WriteLine(ex.Message, this.GetType().Name);
    await Shell.Current.DisplayAlert("Timeout reached", $"The timeout is reached.{Environment.NewLine}Default values are used!", "OK");
}

.NET 9 et champ field

Le field mot clé est une fonctionnalité d’aperçu en C# 13. Vous devez utiliser .NET 9 et définir votre élément preview dans votre fichier projet afin d’utiliser le field mot clé contextuel.

Lorsqu’on utilise le CommunityToolkit.MVVM dans la version .NET 9, on est confronté à un [warning mvvmtk0045]https://learn.microsoft.com/fr-fr/dotnet/communitytoolkit/mvvm/generators/errors/mvvmtk0045) car la nouvelle version utilise le mot clé field pour accéder à l’attribut d’une propriété automatique. Pour résoudre ce problème on doit déclarer les champs observables ainsi:

Avant

using CommunityToolkit.Mvvm.ComponentModel;

namespace MyApp;

public partial class SampleViewModel : ObservableObject
{
    [ObservableProperty]
    private string? name;
}

Après

using CommunityToolkit.Mvvm.ComponentModel;

namespace MyApp;

public partial class SampleViewModel : ObservableObject
{
    [ObservableProperty]
    public partial string? Name { get; set; }
}

Links

what-is-maui

github-maui

github-workshop

Youtube channel

James Montemagno

Gerald Versluis

Useful links

Full course 4h

Introducing .NET MAUI

Jetbrain Webinar

Créer des applications mobiles MAUI

Application settings

Mobile game

ToDo App SQlite

Weather app

Awesome .NET MAUI

Programmez N°252