[TOC]
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;
.Rotate(rotate, center.X, center.Y); canvas
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
.Rotate(rotate, center.X, center.Y); canvas
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 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.
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:
text-to-speech
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.
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.
les liens présent en bas dans la section Useful links
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:
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
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.
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.
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.
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 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
Et ajouter :
Là aussi, la version à certainement déjà évolué.
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.
Ç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.
💬 Question: où se trouve le fichier notes.txt ?
%UserProfile%/AppData/Local/Packages/0A39EF64-0606-4941-8C45-EAB3CF9DE53D_9zz4h110yvjzm/LocalState/notes.txt”>
Ç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.
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
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.
Ç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.
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.
Ç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.
Ç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é.
Ç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
Cet exercice utilise un programme qui fonctionne en deux parties.
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.
Ç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
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 theFileSystem
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
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.
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.
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;
= new RestService();
RestService restService
string url = String.Format($"{Constants.OpenWeatherMapEndpoint}?lat=46.992979&lon=6.931933&units=metric&appid={Constants.OpenWeatherMapAPIKey}");
? weatherData = await restService.GetWeatherData(url);
WeatherData
if (weatherData != null)
{
.WriteLine($"Where : {weatherData.Title}");
Console.WriteLine($"------------------------");
Console.WriteLine($"Clouds: {weatherData.Weather?[0].Description}");
Console}
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];
On peut aborder le projet de deux manière.
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).
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.
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.
Pour que votre application fonctionne correctement, vous devez vous posez les questions suivantes:
openweathermap
ne me
retourne pas de données valides ? Par exemple, si le site est en
maintenance.openweathermap
. En effet, chaque
fois que vous lancer votre programme pour, par exemple, tester
l’affichage graphique de la température, une requête est envoyée chez
openweathermap
et votre nombre de requête journalière est
limité. De plus, si vous travailler sur le réseau de l’école, vous
sortez tous avec la même adresse IP. Du point de vue
d’openweathermap
, il n’y a qu’une personne qui fait
beaucoup de requêtes…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.
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.
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.
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.
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
.
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.
Tous ces types de Page héritent de la classe
VisualElement
.
La Page contient un Layout
Qui contient à son tour des Vues et possiblement d’autres Layout. Parmi les Vues on trouve :
Nom | Exemple |
---|---|
Frame | ![]() |
Shapes | ![]() |
Entry | ![]() |
TableView | ![]() |
Et une multitude d’autres composants permettant la création d’interfaces riches.
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.
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)
.Text = $"Clicked {count} time";
CounterBtnelse
.Text = $"Clicked {count} times";
CounterBtn}
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)
{
.Text = $"Clicked {count} time";
CounterBtn}
else
{
.Text = LabelCounter.Text = $"Clicked {count} times";
CounterBtn}
.Text = $"{count}"; LabelCounter
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)
{
++; // On travail avec la propriété !!! C'est bien un 'C' majuscule.
Count}
Ensuite, on Bind la valeur côté XAML. Pour ce faire, on peut procéder de différente manière.
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();
= 10;
Count }
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é.
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();
= 10;
Count }
public int Count
{
get { return count; }
set{
= value;
count OnPropertyChanged(nameof(Count)); // On notifie la Vue du changement de valeur
}
}
private void OnCounterClicked(object sender, EventArgs e)
{
++;
Count}
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: Object
→ BindableObject
→
Element
→ NavigableElement
→
VisualElement
→ Page
→
TemplatedPage
→ ContentPage
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}" />
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
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.
On trouve dans cette structure, entre autre, les dossiers
View
, ViewModel
, Model
et
Services
. Ces dossiers vont nous permettre de respecter
l’anatomie MVVM.
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.
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()
{
= new List<Monkey>();
monkeyList = new HttpClient();
httpClient }
public async Task<List<Monkey>> GetMonkeys()
{
if (monkeyList?.Count > 0)
{
return monkeyList;
}
= await httpClient.GetAsync("https://www.montemagno.com/monkeys.json");
HttpResponseMessage response
if (response.IsSuccessStatusCode)
{
= await response.Content.ReadFromJsonAsync<List<Monkey>>();
monkeyList }
return monkeyList;
}
}
C’est via la méthode GetMonkeys
qu’on obtiendra une
List<Monkey>
.
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
{
= "Monkey Finder";
Title this.monkeys = new List<Monkey>();
<Monkey> monkeysFromService = await monkeyService.GetMonkeys();
List
.Clear();
Monkeys
foreach (Monkey monkey in monkeysFromService)
{
.Add(monkey);
Monkeys}
}
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
.
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 :
= viewModel; BindingContext
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
{
<Monkey> monkeysFromService = await monkeyService.GetMonkeys();
List.Clear();
Monkeys
foreach (Monkey monkey in monkeysFromService)
{
.Add(monkey);
Monkeys}
}
catch (Exception ex)
{
.WriteLine($"Unable to get monkeys: {ex.Message}");
Debug.Current.DisplayAlert("Error!", ex.Message, "OK");
await Shell}
}
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:
Commençons par un rappel. Il existe plusieurs façon de lié des classes entre elles.
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. |
class Teacher { }
class Student
{
<Teacher> teachers;
List}
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.
class CellBattery { }
class CellPhone
{
;
CellBattery _battery
void CellPhone (CellBattery battery)
{
= battery;
_battery }
}
public static Main(string[] args)
{
= new CellBattery();
CellBattery battery = new CellPhone(battery);
CellPhone phone }
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.
class Hotel
{
<Room> rooms = new List<Room>();
Listpublic void AddRoom(string name, string numero)
{
.Add(new Room(name, numero));
rooms}
}
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.
class GreetingSender
{
;
EmailSender _emailSendervoid SendGreetings(EmailSender emailSender)
{
= emailSender;
_emailSender .SendEmail();
_emailSender}
}
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
= new EmailSender();
EmailSender sender = new GreetingSender(sender); GreetingSender greeting
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()
{
= new Mailer();
Mailer mailer .Send();
mailer}
}
Mais pourquoi pas par SMS
public class SMSSender : ISender
{
void Send()
{
= new SMS();
SMS sms .Send();
sms}
}
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 _senderpublic GreetingSender(ISender sender)
{
= sender;
_sender }
void SendGreetings()
{
.Send();
_sender}
}
On peut alors utiliser l’injection de dépendance ainsi
//Send Greeting through Email
= new EmailSender();
EmailSender emailSender = new GreetingSender(emailSender);
GreetingSender greetingsEmailSender .SendGreetings();
greetingsEmailSender
//Send Greeting through SMS
= new SMSSender();
SMSSender smsSender = new GreetingSender(smsSender);
GreetingSender greetingsSMSSender .SendGreetings(); greetingsSMSSender
😎 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()
{
= new Mailer();
Mailer mailer .Send(token); // <- ajout d'un nouveau paramètre
mailer}
}
class GreetingSender
{
;
ISender _senderpublic GreetingSender(ISender sender)
{
= sender;
_sender }
void SendGreetings()
{
.Send(); // <- ça ne change rien sur la manière de l'utiliser
_sender}
}
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).
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.
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 myServicepublic MyViewModel(MyService myService)
{
= "My Title";
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é ?
.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 =>
{
.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
fonts});
// Add this
// ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
.Services.AddSingleton<MyService>();
builder.Services.AddSingleton<MyViewModel>();
builder
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
.Services.AddSingleton<MainPage>(); builder
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
.Services.AddSingleton<MainPage>(); builder
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
.Services.AddTransient<StoreDetailsPage>(); builder
Références
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
.WriteLine(ex.Message, this.GetType().Name); Debug
La ligne de commentaire ToDo
ajoute une tâche qu’on peut
visualiser dans la fenêtre du même nom
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
.WriteLine(ex.Message, this.GetType().Name);
Debug#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
.
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
.WriteLine($"Latitude: {WhereAmI.Latitude}, Longitude: {WhereAmI.Longitude}, Altitude: {WhereAmI.Altitude}", this.GetType().Name); Debug
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
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
{
= Connectivity.Current.NetworkAccess;
NetworkAccess accessType if (accessType == NetworkAccess.Internet)
{
.DefaultRequestHeaders.Accept.Clear();
client.Timeout = TimeSpan.FromSeconds(10.0);
client
<Stream> streamTask = client.GetStreamAsync(query);
Task
= await JsonSerializer.DeserializeAsync<WeatherData>(await streamTask);
weatherData }
else
{
= GetDefault();
weatherData }
}
catch (Exception ex)
{
.WriteLine(ex.Message);
Debug}
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
:
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
HttpClient
dans le constructeur et
on définit une seule fois les deux propriétés
directement dans le constructeurCette 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
= Connectivity.Current.NetworkAccess;
NetworkAccess accessType 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
{
= GetDefault(); // If there is no connection we use default values
weatherData
...
}
catch(TaskCanceledException ex)
{
.WriteLine(ex.Message, this.GetType().Name);
Debug.Current.DisplayAlert("Timeout reached", $"The timeout is reached.{Environment.NewLine}Default values are used!", "OK");
await Shell}
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; }
}