Mettre à niveau votre application avec des concepts MVVM
Cette série de tutoriels est conçue pour poursuivre le tutoriel Créer une application .NET MAUI, qui a créé une application prenant des notes. Dans cette partie de la série, vous découvrirez comment :
- Implémentez le modèle MVVM (modèle-vue-vue modèle).
- Utilisez un style supplémentaire de chaîne de requête pour transmettre des données pendant la navigation.
Nous vous suggérons vivement de suivre d’abord le tutoriel Créer une application .NET MAUI, car le code créé dans ce tutoriel est la base du présent tutoriel. Si vous avez perdu le code ou que vous souhaitez démarrer à nouveau, téléchargez ce projet.
Comprendre MVVM
L’expérience du développeur .NET MAUI implique généralement la création d’une interface utilisateur en XAML, puis l’ajout de code-behind qui fonctionne sur l’interface utilisateur. Des problèmes de maintenance complexes peuvent survenir quand les applications sont modifiées et qu’elles augmentent en taille et en étendue. Ces problèmes incluent le couplage étroit entre les contrôles d’IU et la logique métier, ce qui augmente le coût des modifications de l’IU ainsi que la difficulté d’effectuer des tests unitaires sur ce code.
Le modèle MVVM (modèle-vue-vue modèle) permet de séparer correctement la logique métier et de présentation d’une application de son interface utilisateur (IU). Le maintien d’une séparation correcte entre la logique de l’application et l’interface utilisateur permet de résoudre de nombreux problèmes de développement et facilite le test, la maintenance et l’évolution d’une application. Il peut également améliorer considérablement les opportunités de réutilisation du code et permet aux développeurs et aux concepteurs d’interface utilisateur de collaborer plus facilement lors du développement de leurs parties respectives d’une application.
Le modèle
Il existe trois composants principaux dans le modèle MVVM : le modèle, la vue et le modèle de vue. Chacun sert un objectif distinct. Le schéma suivant représente les relations entre trois composants.
Il est important de comprendre les responsabilités de chaque composant, et il est tout aussi important de comprendre la façon dont elles interagissent. À un niveau supérieur, la vue « connaît » le modèle de vue, et le modèle de vue « connaît » le modèle, mais le modèle ignore l’existence du modèle de vue et le modèle de vue ignore l’existence de la vue. Ainsi, le modèle de vue isole la vue du modèle et permet au modèle d’évoluer indépendamment de la vue.
Pour utiliser efficacement MVVM, vous devez bien comprendre comment factoriser le code d’application dans les classes appropriées, et comment ces classes interagissent.
Affichage
La vue est chargée de définir la structure, la disposition et l’apparence de ce que l’utilisateur voit à l’écran. Dans l’idéal, chaque vue est définie en XAML, avec un code-behind limité qui ne contient pas de logique métier. Toutefois, dans certains cas, le code-behind peut contenir une logique d’IU qui implémente un comportement visuel difficile à exprimer en XAML, par exemple les animations.
Vue modèle
Le modèle de vue implémente des propriétés et des commandes avec lesquelles la vue peut effectuer une liaison aux données. Il notifie la vue de tout changement d’état via des événements de notification de changement. Les propriétés et les commandes fournies par le modèle de vue définissent la fonctionnalité à offrir par l’IU, mais la vue détermine la façon dont cette fonctionnalité doit être affichée.
Le modèle de vue est également responsable de la coordination des interactions de la vue avec les classes de modèle nécessaires. Il existe généralement une relation un-à-plusieurs entre le modèle de vue et les classes de modèle.
Chaque modèle de vue fournit les données d’un modèle sous une forme que la vue peut facilement consommer. Pour ce faire, le modèle de vue effectue parfois une conversion de données. Il est judicieux de placer cette conversion de données dans le modèle de vue, car elle fournit des propriétés auxquelles la vue peut se lier. Par exemple, le modèle de vue peut combiner les valeurs de deux propriétés pour faciliter son affichage par la vue.
Important
.NET MAUI marshale les mises à jour de liaison vers le thread d’interface utilisateur. Lorsque vous utilisez MVVM, cela vous permet de mettre à jour les propriétés de modèle-vue liées aux données à partir de n’importe quel thread, avec le moteur de liaison de .NET MAUI qui apporte les mises à jour au thread d’interface utilisateur.
Modèle
Les classes de modèle sont des classes non visuelles qui encapsulent les données de l’application. Ainsi, le modèle peut être considéré comme une représentation du modèle de domaine de l’application, qui comprend généralement un modèle de données avec une logique métier et une logique de validation.
Mettre le modèle à niveau
Dans cette première partie du tutoriel, vous allez implémenter le modèle MVVM (modèle-vue-vue modèle). Pour commencer, ouvrez la solution Notes.sln dans Visual Studio.
Nettoyer le modèle
Dans le tutoriel précédent, les types de modèles agissaient à la fois comme modèle (données) et en tant que modèle de vue (préparation des données), qui était mappé directement à une vue. Le tableau ci-dessous décrit le modèle :
Fichier de code | Description |
---|---|
Models/About.cs | Le modèle About . Contient des champs en lecture seule qui décrivent l’application elle-même, telles que le titre et la version de l’application. |
Models/Note.cs | Le modèle Note . Représente une note. |
Models/AllNotes.cs | Le modèle AllNotes . Charge toutes les notes sur l’appareil dans une collection. |
En pensant à l’application elle-même, il n’existe qu’un seul élément de données utilisé par l’application, la Note
. Les notes sont chargées à partir de l’appareil, enregistrées sur l’appareil et modifiées via l’interface utilisateur de l’application. Il n’y a vraiment pas besoin des modèles About
et AllNotes
. Supprimez ces modèles du projet :
- Trouvez le volet Explorateur de solutions de Visual Studio.
- Cliquez avec le bouton droit sur le fichier Models\About.cs, puis sélectionnez Supprimer. Sélectionnez OK pour supprimer le fichier.
- Cliquez avec le bouton droit sur le fichier Models\AllNotes.cs, puis sélectionnez Supprimer. Sélectionnez OK pour supprimer le fichier.
Le seul fichier de modèle restant est le fichier Models\Note.cs.
Mettre le modèle à niveau
Le modèle Note
contient :
- Un identificateur unique, qui est le nom de fichier de la note, tel qu’il est stocké sur l’appareil.
- Le texte de la note.
- Une date qui indique la date de création ou de dernière mise à jour de la note.
Actuellement, le chargement et l’enregistrement du modèle étaient effectués via les vues, et dans certains cas, par les autres types de modèles que vous venez de supprimer. Le code dont vous disposez pour le type Note
doit être le suivant :
namespace Notes.Models;
internal class Note
{
public string Filename { get; set; }
public string Text { get; set; }
public DateTime Date { get; set; }
}
Le modèle Note
va être développé pour gérer le chargement, l’enregistrement et la suppression de notes.
Dans le volet Explorateur de solutions de Visual Studio, double-cliquez sur Models\Note.cs.
Dans l’éditeur de code, ajoutez les deux méthodes suivantes à la classe
Note
. Ces méthodes sont basées sur des instances et gèrent l’enregistrement ou la suppression de la note actuelle sur l’appareil, respectivement :public void Save() => File.WriteAllText(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename), Text); public void Delete() => File.Delete(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename));
L’application doit charger des notes de deux façons, charger une note individuelle à partir d’un fichier et charger toutes les notes sur l’appareil. Le code pour gérer le chargement peut être des membres
static
, ce qui n’exige pas qu’une instance de classe s’exécute.Ajoutez le code suivant à la classe pour charger une note par nom de fichier :
public static Note Load(string filename) { filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename); if (!File.Exists(filename)) throw new FileNotFoundException("Unable to find file on local storage.", filename); return new() { Filename = Path.GetFileName(filename), Text = File.ReadAllText(filename), Date = File.GetLastWriteTime(filename) }; }
Ce code prend le nom de fichier en tant que paramètre, génère le chemin d’accès à l’emplacement où les notes sont stockées sur l’appareil et tente de charger le fichier s’il existe.
La deuxième façon de charger des notes consiste à énumérer toutes les notes sur l’appareil et à les charger dans une collection.
Ajoutez le code suivant à la classe :
public static IEnumerable<Note> LoadAll() { // Get the folder where the notes are stored. string appDataPath = FileSystem.AppDataDirectory; // Use Linq extensions to load the *.notes.txt files. return Directory // Select the file names from the directory .EnumerateFiles(appDataPath, "*.notes.txt") // Each file name is used to load a note .Select(filename => Note.Load(Path.GetFileName(filename))) // With the final collection of notes, order them by date .OrderByDescending(note => note.Date); }
Ce code retourne une collection énumérable de types de modèles
Note
en récupérant les fichiers sur l’appareil qui correspondent au modèle de fichier de notes : *.notes.txt. Chaque nom de fichier est transmis à la méthodeLoad
, en chargeant une note individuelle. Enfin, la collection de notes est triée selon la date de chaque note et retournée à l’appelant.Enfin, ajoutez un constructeur à la classe qui définit les valeurs par défaut pour les propriétés, y compris un nom de fichier aléatoire :
public Note() { Filename = $"{Path.GetRandomFileName()}.notes.txt"; Date = DateTime.Now; Text = ""; }
Le code de la classe Note
doit se présenter comme suit :
namespace Notes.Models;
internal class Note
{
public string Filename { get; set; }
public string Text { get; set; }
public DateTime Date { get; set; }
public Note()
{
Filename = $"{Path.GetRandomFileName()}.notes.txt";
Date = DateTime.Now;
Text = "";
}
public void Save() =>
File.WriteAllText(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename), Text);
public void Delete() =>
File.Delete(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename));
public static Note Load(string filename)
{
filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename);
if (!File.Exists(filename))
throw new FileNotFoundException("Unable to find file on local storage.", filename);
return
new()
{
Filename = Path.GetFileName(filename),
Text = File.ReadAllText(filename),
Date = File.GetLastWriteTime(filename)
};
}
public static IEnumerable<Note> LoadAll()
{
// Get the folder where the notes are stored.
string appDataPath = FileSystem.AppDataDirectory;
// Use Linq extensions to load the *.notes.txt files.
return Directory
// Select the file names from the directory
.EnumerateFiles(appDataPath, "*.notes.txt")
// Each file name is used to load a note
.Select(filename => Note.Load(Path.GetFileName(filename)))
// With the final collection of notes, order them by date
.OrderByDescending(note => note.Date);
}
}
Maintenant que le modèle Note
est terminé, les modèles de vues peuvent être créés.
Créer À propos du modèle de vue
Avant d’ajouter des modèles de vues au projet, ajoutez une référence au kit de ressources de la communauté MVVM. Cette bibliothèque est disponible sur NuGet et fournit des types et des systèmes qui aident à implémenter le modèle MVVM.
Dans le volet Explorateur de solutions de Visual Studio, cliquez avec le bouton droit sur le projet Notes >Gérer les packages NuGet.
Sélectionnez l’onglet Parcourir.
Recherchez communitytoolkit mvvm et sélectionnez le package
CommunityToolkit.Mvvm
, qui doit être le premier résultat.Vérifiez qu’au moins la version 8 est sélectionnée. Ce tutoriel a été écrit à l’aide de la version 8.0.0.
Ensuite, sélectionnez Installer et acceptez les invites éventuelles affichées.
Vous êtes maintenant prêt à commencer à mettre à jour le projet en ajoutant des modèles de vues.
Dissocier avec des modèles de vues
La relation modèle-vue-vue modèle repose fortement sur le système de liaison fourni par l’interface utilisateur de l’application multiplateforme .NET (.NET MAUI). L’application utilise déjà la liaison dans les vues pour afficher une liste de notes et présenter le texte et la date d’une seule note. La logique d’application est actuellement fournie par le code-behind de la vue et est directement liée à la vue. Par exemple, lorsqu’un utilisateur modifie une note et appuie sur le bouton Enregistrer, l’événement Clicked
pour le bouton est déclenché. Ensuite, le code-behind du gestionnaire d’événements enregistre le texte de la note dans un fichier et accède à l’écran précédent.
L’utilisation d’une logique d’application dans le code-behind d’une vue peut devenir un problème lorsque la vue change. Par exemple, si le bouton est remplacé par un autre contrôle d’entrée ou si le nom d’un contrôle est modifié, les gestionnaires d’événements peuvent devenir non valides. Quelle que soit la façon dont la vue est conçue, l’objectif de la vue est d’appeler une sorte de logique d’application et de présenter des informations à l’utilisateur. Pour cette application, le bouton Save
enregistre la note, puis revient à l’écran précédent.
Le modèle de vue donne à l’application un emplacement spécifique pour placer la logique de l’application, quelle que soit la façon dont l’interface utilisateur est conçue ou la façon dont les données sont chargées ou enregistrées. Le modèle de vue est la colle qui représente et interagit avec le modèle de données pour le compte de la vue.
Les modèles de vues sont stockés dans un dossier ViewModels.
- Trouvez le volet Explorateur de solutions de Visual Studio.
- Cliquez avec le bouton droit sur le projet Notes, puis sélectionnez Ajouter>Nouveau dossier. Nommez le dossier ViewModels.
- Cliquez avec le bouton droit sur le dossier ViewModels >Ajouter>Classe et nommez-la AboutViewModel.cs.
- Répétez l’étape précédente et créez deux modèles de vues supplémentaires :
- NoteViewModel.cs
- NotesViewModel.cs
La structure de votre projet doit ressembler à ceci :
À propos du modèle de vue et À propos de la vue
À propos de la vue affiche des données à l’écran et accède éventuellement à un site web avec plus d’informations. Étant donné que cette vue n’a pas de données à modifier, comme avec un contrôle d’entrée de texte ou la sélection d’éléments dans une liste, il convient pour illustrer l’ajout d’un modèle de vue. Pour À propos du modèle de vue, il n’existe pas de modèle de stockage.
Créez À propos du modèle de vue :
Dans le volet Explorateur de solutions de Visual Studio, double-cliquez sur ViewModels\AboutViewModel.cs.
Collez le code suivant :
using CommunityToolkit.Mvvm.Input; using System.Windows.Input; namespace Notes.ViewModels; internal class AboutViewModel { public string Title => AppInfo.Name; public string Version => AppInfo.VersionString; public string MoreInfoUrl => "https://aka.ms/maui"; public string Message => "This app is written in XAML and C# with .NET MAUI."; public ICommand ShowMoreInfoCommand { get; } public AboutViewModel() { ShowMoreInfoCommand = new AsyncRelayCommand(ShowMoreInfo); } async Task ShowMoreInfo() => await Launcher.Default.OpenAsync(MoreInfoUrl); }
L’extrait de code précédent contient certaines propriétés qui représentent des informations sur l’application, telles que le nom et la version. Cet extrait de code est exactement le même que À propos du modèle, que vous avez supprimé précédemment. Toutefois, ce modèle de vue contient un nouveau concept, la propriété de commande ShowMoreInfoCommand
.
Les commandes sont des actions pouvant être liées qui appellent du code et constituent un excellent endroit pour placer la logique de l’application. Dans cet exemple, ShowMoreInfoCommand
pointe vers la méthode ShowMoreInfo
, qui ouvre le navigateur web à une page spécifique. La section suivante vous permettra d’en apprendre plus au sujet du système de commandes.
À propos de la vue
À propos de la vue doit être légèrement modifié pour le raccorder au modèle de vue créé dans la section précédente. Dans le fichier Views\AboutPage.xaml, appliquez les modifications suivantes :
- Changez l’espace de noms XML
xmlns:models
enxmlns:viewModels
et ciblez l’espace de noms .NETNotes.ViewModels
. - Modifiez la propriété
ContentPage.BindingContext
sur une nouvelle instance du modèle de vueAbout
. - Supprimez le gestionnaire d’événements
Clicked
du bouton et utilisez la propriétéCommand
.
Mettez à jour À propos de la vue :
Dans le volet Explorateur de solutions de Visual Studio, double-cliquez sur Views\AboutPage.xaml.
Collez le code suivant :
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:Notes.ViewModels" x:Class="Notes.Views.AboutPage"> <ContentPage.BindingContext> <viewModels:AboutViewModel /> </ContentPage.BindingContext> <VerticalStackLayout Spacing="10" Margin="10"> <HorizontalStackLayout Spacing="10"> <Image Source="dotnet_bot.png" SemanticProperties.Description="The dot net bot waving hello!" HeightRequest="64" /> <Label FontSize="22" FontAttributes="Bold" Text="{Binding Title}" VerticalOptions="End" /> <Label FontSize="22" Text="{Binding Version}" VerticalOptions="End" /> </HorizontalStackLayout> <Label Text="{Binding Message}" /> <Button Text="Learn more..." Command="{Binding ShowMoreInfoCommand}" /> </VerticalStackLayout> </ContentPage>
L’extrait de code précédent met en évidence les lignes qui ont changé dans cette version de la vue.
Notez que le bouton utilise la propriété Command
. De nombreux contrôles ont une propriété Command
appelée lorsque l’utilisateur interagit avec le contrôle. Lorsqu’elle est utilisée avec un bouton, la commande est appelée lorsqu’un utilisateur appuie sur le bouton, comme lorsque le gestionnaire d’événements Clicked
est appelé, sauf que vous pouvez lier Command
à une propriété dans le modèle de vue.
Dans cette vue, lorsque l’utilisateur appuie sur le bouton, la Command
est appelée. La Command
est liée à la propriété ShowMoreInfoCommand
dans le modèle de vue, et lorsqu’elle est appelée, elle exécute le code dans la méthode ShowMoreInfo
, qui ouvre le navigateur web à une page spécifique.
Nettoyer le code-behind À propos de
Le bouton ShowMoreInfo
n’utilise pas le gestionnaire d’événements. Le code LearnMore_Clicked
doit donc être supprimé du fichier Views\AboutPage.xaml.cs. Supprimez ce code, la classe ne doit contenir que le constructeur :
Dans le volet Explorateur de solutions de Visual Studio, double-cliquez sur Views\AboutPage.xaml.cs.
Conseil
Vous devrez peut-être développer Views\AboutPage.xaml pour afficher le fichier.
Remplacez tout le code par l’extrait de code suivant :
namespace Notes.Views; public partial class AboutPage : ContentPage { public AboutPage() { InitializeComponent(); } }
Créer le modèle de vue Note
L’objectif de la mise à jour de la vue Note consiste à déplacer autant de fonctionnalités que possible hors du code-behind XAML et à les placer dans le modèle de vue Note.
Modèle de vue Note
En fonction de ce que nécessite la vue Note, le modèle de vue Note doit fournir les éléments suivants :
- Le texte de la note.
- La date/heure de création ou de dernière mise à jour de la note.
- Une commande qui enregistre la note.
- Une commande qui supprime la note.
Créez le modèle de vue Note :
Dans le volet Explorateur de solutions de Visual Studio, double-cliquez sur ViewModels\NoteViewModel.cs.
Remplacez le code dans ce fichier par l’extrait de code suivant :
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.ComponentModel; using System.Windows.Input; namespace Notes.ViewModels; internal class NoteViewModel : ObservableObject, IQueryAttributable { private Models.Note _note; }
Ce code est le modèle de vue
Note
vide dans lequel vous allez ajouter des propriétés et des commandes pour prendre en charge la vueNote
. Notez que l’espace de nomsCommunityToolkit.Mvvm.ComponentModel
est importé. Cet espace de noms fournit leObservableObject
utilisé comme classe de base. L’étape suivante vous permettra d’en apprendre plus au sujet deObservableObject
. L’espace de nomsCommunityToolkit.Mvvm.Input
est également importé. Cet espace de noms fournit certains types de commandes qui appellent des méthodes de manière asynchrone.Le modèle
Models.Note
est stocké en tant que champ privé. Les propriétés et méthodes de cette classe utilisent ce champ.Ajoutez les propriétés suivantes à la classe :
public string Text { get => _note.Text; set { if (_note.Text != value) { _note.Text = value; OnPropertyChanged(); } } } public DateTime Date => _note.Date; public string Identifier => _note.Filename;
Les propriétés
Date
etIdentifier
sont des propriétés simples qui récupèrent simplement les valeurs correspondantes à partir du modèle.Conseil
Pour les propriétés, la syntaxe
=>
crée une propriété get uniquement où l’instruction à droite de=>
doit évaluer une valeur à retourner.La propriété
Text
vérifie d’abord si la valeur définie est une valeur différente. Si la valeur est différente, cette valeur est transmise à la propriété du modèle et la méthodeOnPropertyChanged
est appelée.La méthode
OnPropertyChanged
est fournie par la classe de baseObservableObject
. Cette méthode utilise le nom du code appelant, dans ce cas, le nom de propriété de Texte, et déclenche l’événementObservableObject.PropertyChanged
. Cet événement fournit le nom de la propriété à tous les abonnés aux événements. Le système de liaison fourni par .NET MAUI reconnaît cet événement et met à jour toutes les liaisons associées dans l’interface utilisateur. Pour le modèle de vue Note, lorsque la propriétéText
change, l’événement est déclenché et tout élément d’interface utilisateur lié à la propriétéText
est averti que la propriété a changé.Ajoutez les propriétés de commande suivantes à la classe, qui sont les commandes auxquelles la vue peut être liée :
public ICommand SaveCommand { get; private set; } public ICommand DeleteCommand { get; private set; }
Ajoutez les constructeurs suivants à la classe :
public NoteViewModel() { _note = new Models.Note(); SaveCommand = new AsyncRelayCommand(Save); DeleteCommand = new AsyncRelayCommand(Delete); } public NoteViewModel(Models.Note note) { _note = note; SaveCommand = new AsyncRelayCommand(Save); DeleteCommand = new AsyncRelayCommand(Delete); }
Ces deux constructeurs sont utilisés pour créer le modèle de vue avec un nouveau modèle de stockage, qui est une note vide, ou pour créer un modèle de vue qui utilise l’instance de modèle spécifiée.
Les constructeurs configurent également les commandes pour le modèle de vue. Ajoutez ensuite le code pour ces commandes.
Ajoutez les méthodes
Save
etDelete
:private async Task Save() { _note.Date = DateTime.Now; _note.Save(); await Shell.Current.GoToAsync($"..?saved={_note.Filename}"); } private async Task Delete() { _note.Delete(); await Shell.Current.GoToAsync($"..?deleted={_note.Filename}"); }
Ces méthodes sont appelées par les commandes associées. Elles effectuent les actions associées sur le modèle et permettent à l’application d’accéder à la page précédente. Un paramètre de chaîne de requête est ajouté au chemin de navigation
..
, indiquant quelle action a été effectuée et l’identificateur unique de la note.Ensuite, ajoutez la méthode
ApplyQueryAttributes
à la classe, qui répond aux exigences de l’interface IQueryAttributable :void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { if (query.ContainsKey("load")) { _note = Models.Note.Load(query["load"].ToString()); RefreshProperties(); } }
Lorsqu’une page, ou le contexte de liaison d’une page, implémente cette interface, les paramètres de chaîne de requête utilisés dans la navigation sont passés à la méthode
ApplyQueryAttributes
. Ce modèle de vue est utilisé comme contexte de liaison pour la vue Note. Lorsque la vue Note est accédée, le contexte de liaison de la vue (ce modèle de vue) est transmis aux paramètres de chaîne de requête utilisés pendant la navigation.Ce code vérifie si la clé
load
a été fournie dans le dictionnairequery
. Si cette clé est trouvée, la valeur doit être l’identificateur (nom de fichier) de la note à charger. Cette note est chargée et définie comme objet de modèle sous-jacent de cette instance de modèle de vue.Enfin, ajoutez ces deux méthodes d’assistance à la classe :
public void Reload() { _note = Models.Note.Load(_note.Filename); RefreshProperties(); } private void RefreshProperties() { OnPropertyChanged(nameof(Text)); OnPropertyChanged(nameof(Date)); }
La méthode
Reload
est une méthode d’assistance qui actualise l’objet de modèle de stockage, en le rechargeant à partir du stockage de l’appareilLa méthode
RefreshProperties
est une autre méthode d’assistance pour s’assurer que tous les abonnés liés à cet objet sont avertis que les propriétésText
etDate
ont changé. Étant donné que le modèle sous-jacent (champ_note
) est modifié lorsque la note est chargée pendant la navigation, les propriétésText
etDate
ne sont pas réellement définies sur de nouvelles valeurs. Étant donné que ces propriétés ne sont pas directement définies, toutes les liaisons attachées à ces propriétés ne seraient pas averties, carOnPropertyChanged
n’est pas appelé pour chaque propriété.RefreshProperties
garantit que les liaisons à ces propriétés sont actualisées.
Le code de la classe doit ressembler à l’extrait de code suivant :
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Windows.Input;
namespace Notes.ViewModels;
internal class NoteViewModel : ObservableObject, IQueryAttributable
{
private Models.Note _note;
public string Text
{
get => _note.Text;
set
{
if (_note.Text != value)
{
_note.Text = value;
OnPropertyChanged();
}
}
}
public DateTime Date => _note.Date;
public string Identifier => _note.Filename;
public ICommand SaveCommand { get; private set; }
public ICommand DeleteCommand { get; private set; }
public NoteViewModel()
{
_note = new Models.Note();
SaveCommand = new AsyncRelayCommand(Save);
DeleteCommand = new AsyncRelayCommand(Delete);
}
public NoteViewModel(Models.Note note)
{
_note = note;
SaveCommand = new AsyncRelayCommand(Save);
DeleteCommand = new AsyncRelayCommand(Delete);
}
private async Task Save()
{
_note.Date = DateTime.Now;
_note.Save();
await Shell.Current.GoToAsync($"..?saved={_note.Filename}");
}
private async Task Delete()
{
_note.Delete();
await Shell.Current.GoToAsync($"..?deleted={_note.Filename}");
}
void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.ContainsKey("load"))
{
_note = Models.Note.Load(query["load"].ToString());
RefreshProperties();
}
}
public void Reload()
{
_note = Models.Note.Load(_note.Filename);
RefreshProperties();
}
private void RefreshProperties()
{
OnPropertyChanged(nameof(Text));
OnPropertyChanged(nameof(Date));
}
}
Vue Note
Maintenant que le modèle de vue a été créé, mettez à jour la vue Note. Dans le fichier Views\NotePage.xaml, appliquez les modifications suivantes :
- Ajoutez l’espace de noms XML
xmlns:viewModels
qui cible l’espace de noms .NETNotes.ViewModels
. - Ajoutez un
BindingContext
à la page. - Supprimez les gestionnaires d’événements
Clicked
des boutons supprimer et enregistrer et remplacez-les par des commandes.
Mettez à jour la vue Note :
- Dans le volet Explorateur de solutions de Visual Studio, double-cliquez sur Views\NotePage.xaml pour ouvrir l’éditeur XAML.
- Collez le code suivant :
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:Notes.ViewModels"
x:Class="Notes.Views.NotePage"
Title="Note">
<ContentPage.BindingContext>
<viewModels:NoteViewModel />
</ContentPage.BindingContext>
<VerticalStackLayout Spacing="10" Margin="5">
<Editor x:Name="TextEditor"
Placeholder="Enter your note"
Text="{Binding Text}"
HeightRequest="100" />
<Grid ColumnDefinitions="*,*" ColumnSpacing="4">
<Button Text="Save"
Command="{Binding SaveCommand}"/>
<Button Grid.Column="1"
Text="Delete"
Command="{Binding DeleteCommand}"/>
</Grid>
</VerticalStackLayout>
</ContentPage>
Auparavant, cette vue ne déclarait pas de contexte de liaison, car elle était fournie par le code-behind de la page elle-même. La définition du contexte de liaison directement dans le code XAML fournit deux éléments :
Au moment de l’exécution, lorsque la page est accédée, il affiche une note vide. Cela est dû au fait que le constructeur sans paramètre du contexte de liaison, le modèle de vue, est appelé. Si vous vous souvenez correctement, le constructeur sans paramètre du modèle de vue Note crée une note vide.
Dans l’éditeur XAML, IntelliSense affiche les propriétés disponibles dès que vous commencez à taper la syntaxe
{Binding
. La syntaxe est également validée et vous avertit d’une valeur non valide. Essayez de modifier la syntaxe de liaison deSaveCommand
enSave123Command
. Si vous placez le curseur de la souris sur le texte, vous remarquerez qu’une info-bulle s’affiche pour vous informer que Save123Command est introuvable. Cette notification n’est pas considérée comme une erreur, car les liaisons sont dynamiques, c’est vraiment un petit avertissement qui peut vous aider à remarquer lorsque vous avez tapé une propriété incorrecte.Si vous avez modifié SaveCommand en une autre valeur, restaurez-le maintenant.
Nettoyer le code-behind Note
Maintenant que l’interaction avec la vue a changé des gestionnaires d’événements en commandes, ouvrez le fichier Views\NotePage.xaml.cs et remplacez tout le code par une classe qui contient uniquement le constructeur :
Dans le volet Explorateur de solutions de Visual Studio, double-cliquez sur Views\NotePage.xaml.cs.
Conseil
Vous devrez peut-être développer Views\NotePage.xaml pour afficher le fichier.
Remplacez tout le code par l’extrait de code suivant :
namespace Notes.Views; public partial class NotePage : ContentPage { public NotePage() { InitializeComponent(); } }
Créer le modèle de vue Notes
La paire modèle de vue-vue finale est le modèle de vue Notes et la vue AllNotes. Actuellement, la vue est directement lié au modèle, qui a été supprimé au début de ce tutoriel. L’objectif de la mise à jour de la vue AllNotes consiste à déplacer autant de fonctionnalités que possible hors du code-behind XAML et à les placer dans le modèle de vue. Là encore, l’avantage est que la vue peut modifier sa conception avec peu d’effet sur votre code.
Modèle de vue Notes
En fonction de ce que la vue AllNotes va afficher et de quelles interactions l’utilisateur effectuera, le modèle de vue Notes doit fournir les éléments suivants :
- Une collection de notes.
- Une commande permettant de gérer la navigation vers une note.
- Une commande permettant de créer une note.
- Mettez à jour la liste des notes lorsqu’une note est créée, supprimée ou modifiée.
Créez le modèle de vue Notes :
Dans le volet Explorateur de solutions de Visual Studio, double-cliquez sur ViewModels\NotesViewModel.cs.
Remplacez tout le code de ce fichier par le code suivant :
using CommunityToolkit.Mvvm.Input; using System.Collections.ObjectModel; using System.Windows.Input; namespace Notes.ViewModels; internal class NotesViewModel: IQueryAttributable { }
Ce code est le
NotesViewModel
vide dans lequel vous allez ajouter des propriétés et des commandes pour prendre en charge la vueAllNotes
.Dans le code de la classe
NotesViewModel
, ajoutez les propriétés suivantes :public ObservableCollection<ViewModels.NoteViewModel> AllNotes { get; } public ICommand NewCommand { get; } public ICommand SelectNoteCommand { get; }
La propriété
AllNotes
est uneObservableCollection
qui stocke toutes les notes chargées à partir de l’appareil. Les deux commandes seront utilisées par la vue pour déclencher les actions de création d’une note ou de sélection d’une note existante.Ajoutez un constructeur sans paramètre à la classe, qui initialise les commandes et charge les notes à partir du modèle :
public NotesViewModel() { AllNotes = new ObservableCollection<ViewModels.NoteViewModel>(Models.Note.LoadAll().Select(n => new NoteViewModel(n))); NewCommand = new AsyncRelayCommand(NewNoteAsync); SelectNoteCommand = new AsyncRelayCommand<ViewModels.NoteViewModel>(SelectNoteAsync); }
Notez que la collection
AllNotes
utilise la méthodeModels.Note.LoadAll
pour remplir la collection observable avec des notes. La méthodeLoadAll
retourne les notes comme typeModels.Note
, mais la collection observable est une collection de typesViewModels.NoteViewModel
. Le code utilise l’extension LinqSelect
pour créer des instances de modèle de vue à partir des modèles de note retournés parLoadAll
.Créez les méthodes ciblées par les commandes :
private async Task NewNoteAsync() { await Shell.Current.GoToAsync(nameof(Views.NotePage)); } private async Task SelectNoteAsync(ViewModels.NoteViewModel note) { if (note != null) await Shell.Current.GoToAsync($"{nameof(Views.NotePage)}?load={note.Identifier}"); }
Notez que la méthode
NewNoteAsync
ne prend pas de paramètre, contrairement àSelectNoteAsync
. Les commandes peuvent éventuellement avoir un paramètre unique fourni lorsque la commande est appelée. Pour la méthodeSelectNoteAsync
, le paramètre représente la note sélectionnée.Enfin, implémentez la méthode
IQueryAttributable.ApplyQueryAttributes
:void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { if (query.ContainsKey("deleted")) { string noteId = query["deleted"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note exists, delete it if (matchedNote != null) AllNotes.Remove(matchedNote); } else if (query.ContainsKey("saved")) { string noteId = query["saved"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note is found, update it if (matchedNote != null) matchedNote.Reload(); // If note isn't found, it's new; add it. else AllNotes.Add(new NoteViewModel(Note.Load(noteId))); } }
Le modèle de vue Note créé à l’étape précédente du tutoriel, utilisait la navigation lorsque la note était enregistrée ou supprimée. Le modèle de vue revenait à la vue AllNotes à laquelle ce modèle de vue est associé. Ce code détecte si la chaîne de requête contient la clé
deleted
ousaved
. La valeur de la clé est l’identificateur unique de la note.Si la note a été supprimée, cette note est trouvée dans la collection
AllNotes
grâce à l’identificateur fourni, puis supprimée.Il existe deux raisons possibles pour lesquelles une note est enregistrée. La note vient d’être créée ou une note existante a été modifiée. Si la note se trouve déjà dans la collection
AllNotes
, il s’agit d’une note mise à jour. Dans ce cas, l’instance de note de la collection doit simplement être actualisée. Si la note est manquante dans la collection, il s’agit d’une nouvelle note et elle doit être ajoutée à la collection.
Le code de la classe doit ressembler à l’extrait de code suivant :
using CommunityToolkit.Mvvm.Input;
using Notes.Models;
using System.Collections.ObjectModel;
using System.Windows.Input;
namespace Notes.ViewModels;
internal class NotesViewModel : IQueryAttributable
{
public ObservableCollection<ViewModels.NoteViewModel> AllNotes { get; }
public ICommand NewCommand { get; }
public ICommand SelectNoteCommand { get; }
public NotesViewModel()
{
AllNotes = new ObservableCollection<ViewModels.NoteViewModel>(Models.Note.LoadAll().Select(n => new NoteViewModel(n)));
NewCommand = new AsyncRelayCommand(NewNoteAsync);
SelectNoteCommand = new AsyncRelayCommand<ViewModels.NoteViewModel>(SelectNoteAsync);
}
private async Task NewNoteAsync()
{
await Shell.Current.GoToAsync(nameof(Views.NotePage));
}
private async Task SelectNoteAsync(ViewModels.NoteViewModel note)
{
if (note != null)
await Shell.Current.GoToAsync($"{nameof(Views.NotePage)}?load={note.Identifier}");
}
void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.ContainsKey("deleted"))
{
string noteId = query["deleted"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note exists, delete it
if (matchedNote != null)
AllNotes.Remove(matchedNote);
}
else if (query.ContainsKey("saved"))
{
string noteId = query["saved"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note is found, update it
if (matchedNote != null)
matchedNote.Reload();
// If note isn't found, it's new; add it.
else
AllNotes.Add(new NoteViewModel(Note.Load(noteId)));
}
}
}
Vue AllNotes
Maintenant que le modèle de vue a été créé, mettez à jour la vue AllNotes pour qu’elle pointe vers les propriétés du modèle de vue. Dans le fichier Views\AllNotesPage.xaml, appliquez les modifications suivantes :
- Ajoutez l’espace de noms XML
xmlns:viewModels
qui cible l’espace de noms .NETNotes.ViewModels
. - Ajoutez un
BindingContext
à la page. - Supprimez l’événement
Clicked
du bouton de barre d’outils et utilisez la propriétéCommand
. - Modifiez la
CollectionView
pour lier sonItemSource
àAllNotes
. - Modifiez la
CollectionView
pour utiliser les commandes pour réagir lorsque l’élément sélectionné change.
Mettez à jour la vue AllNotes :
Dans le volet Explorateur de solutions de Visual Studio, double-cliquez sur Views\AllNotesPage.xaml.
Collez le code suivant :
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:Notes.ViewModels" x:Class="Notes.Views.AllNotesPage" Title="Your Notes"> <ContentPage.BindingContext> <viewModels:NotesViewModel /> </ContentPage.BindingContext> <!-- Add an item to the toolbar --> <ContentPage.ToolbarItems> <ToolbarItem Text="Add" Command="{Binding NewCommand}" IconImageSource="{FontImage Glyph='+', Color=Black, Size=22}" /> </ContentPage.ToolbarItems> <!-- Display notes in a list --> <CollectionView x:Name="notesCollection" ItemsSource="{Binding AllNotes}" Margin="20" SelectionMode="Single" SelectionChangedCommand="{Binding SelectNoteCommand}" SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}"> <!-- Designate how the collection of items are laid out --> <CollectionView.ItemsLayout> <LinearItemsLayout Orientation="Vertical" ItemSpacing="10" /> </CollectionView.ItemsLayout> <!-- Define the appearance of each item in the list --> <CollectionView.ItemTemplate> <DataTemplate> <StackLayout> <Label Text="{Binding Text}" FontSize="22"/> <Label Text="{Binding Date}" FontSize="14" TextColor="Silver"/> </StackLayout> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> </ContentPage>
La barre d’outils n’utilise plus l’événement Clicked
et utilise plutôt une commande.
CollectionView
prend en charge les commandes avec les propriétés SelectionChangedCommand
et SelectionChangedCommandParameter
. Dans le code XAML mis à jour, la propriété SelectionChangedCommand
est liée au SelectNoteCommand
du modèle de vue, ce qui signifie que la commande est appelée lorsque l’élément sélectionné change. Lorsque la commande est appelée, la valeur de propriété SelectionChangedCommandParameter
est passée à la commande.
Examinez la liaison utilisée pour CollectionView
:
<CollectionView x:Name="notesCollection"
ItemsSource="{Binding AllNotes}"
Margin="20"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectNoteCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
La propriété SelectionChangedCommandParameter
utilise la liaison Source={RelativeSource Self}
. Self
référence l’objet actuel, qui est le CollectionView
. Notez que le chemin de liaison est la propriété SelectedItem
. Lorsque la commande est appelée en modifiant l’élément sélectionné, la commande SelectNoteCommand
est appelée et l’élément sélectionné est passé à la commande en tant que paramètre.
Nettoyer le code-behind AllNotes
Maintenant que l’interaction avec la vue a changé des gestionnaires d’événements en commandes, ouvrez le fichier Views\AllNotesPage.xaml.cs et remplacez tout le code par une classe qui contient uniquement le constructeur :
Dans le volet Explorateur de solutions de Visual Studio, double-cliquez sur Views\AllNotesPage.xaml.cs.
Conseil
Vous devrez peut-être développer Views\AllNotesPage.xaml pour afficher le fichier.
Remplacez tout le code par l’extrait de code suivant :
namespace Notes.Views; public partial class AllNotesPage : ContentPage { public AllNotesPage() { InitializeComponent(); } }
Exécuter l’application
Vous pouvez maintenant exécuter l’application et tout fonctionne. Toutefois, il existe deux problèmes avec le comportement de l’application :
- Si vous sélectionnez une note, ce qui ouvre l’éditeur, appuyez sur Enregistrer, puis essayez de sélectionner la même note, cela ne fonctionne pas.
- Chaque fois qu’une note est modifiée ou ajoutée, la liste des notes n’est pas réorganisées pour afficher les dernières notes en haut.
Ces deux problèmes sont résolus à l’étape suivante du tutoriel.
Corriger le comportement de l’application
Maintenant que le code de l’application peut être compilé et exécuté, vous remarquerez probablement qu’il existe deux défauts avec le comportement de l’application. L’application ne vous permet pas de resélectionner une note déjà sélectionnée, et la liste des notes n’est pas réorganisée après la création ou la modification d’une note.
Afficher les notes en haut de la liste
Tout d’abord, corrigez le problème de réorganisation de la liste des notes. Dans le fichier ViewModels\NotesViewModel.cs, la collection AllNotes
contient toutes les notes à présenter à l’utilisateur. Malheureusement, l’inconvénient de l’utilisation de ObservableCollection
est qu’elle doit être triée manuellement. Pour afficher les éléments nouveaux ou mis à jour en haut de la liste, procédez comme suit :
Dans le volet Explorateur de solutions de Visual Studio, double-cliquez sur ViewModels\NotesViewModel.cs.
Dans la méthode
ApplyQueryAttributes
, examinez la logique de la clé de chaîne de requête enregistrée.Quand
matchedNote
n’est pasnull
, la note est mise à jour. Utilisez la méthodeAllNotes.Move
pour déplacermatchedNote
à l’indice 0, qui est le haut de la liste.string noteId = query["saved"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note is found, update it if (matchedNote != null) { matchedNote.Reload(); AllNotes.Move(AllNotes.IndexOf(matchedNote), 0); }
La méthode
AllNotes.Move
prend deux paramètres pour déplacer la position d’un objet dans la collection. Le premier paramètre est l’indice où se trouve l’objet à déplacer, et le deuxième paramètre est l’indice où déplacer l’objet. La méthodeAllNotes.IndexOf
récupère l’index de la note.Lorsque
matchedNote
estnull
, la note est nouvelle et est ajoutée à la liste. Au lieu de l’ajouter, ce qui ajoute la note à la fin de la liste, insérez la note à l’indice 0, qui est le haut de la liste. Changez la méthodeAllNotes.Add
enAllNotes.Insert
.string noteId = query["saved"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note is found, update it if (matchedNote != null) { matchedNote.Reload(); AllNotes.Move(AllNotes.IndexOf(matchedNote), 0); } // If note isn't found, it's new; add it. else AllNotes.Insert(0, new NoteViewModel(Models.Note.Load(noteId)));
La méthode ApplyQueryAttributes
doit se présenter comme l’extrait de code suivant :
void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.ContainsKey("deleted"))
{
string noteId = query["deleted"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note exists, delete it
if (matchedNote != null)
AllNotes.Remove(matchedNote);
}
else if (query.ContainsKey("saved"))
{
string noteId = query["saved"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note is found, update it
if (matchedNote != null)
{
matchedNote.Reload();
AllNotes.Move(AllNotes.IndexOf(matchedNote), 0);
}
// If note isn't found, it's new; add it.
else
AllNotes.Insert(0, new NoteViewModel(Models.Note.Load(noteId)));
}
}
Permettre de sélectionner deux fois une note
Dans la vue AllNotes, CollectionView
liste toutes les notes, mais ne vous permet pas de sélectionner deux fois la même note. Il existe deux façons dont l’élément reste sélectionné : lorsque l’utilisateur modifie une note existante et lorsque l’utilisateur navigue de force vers l’arrière. Le cas où l’utilisateur enregistre une note est résolu avec le changement de code de la section précédente qui utilise AllNotes.Move
, de sorte que vous n’avez pas à vous soucier de ce cas.
Le problème que vous devez maintenant résoudre est lié à la navigation. Quelle que soit la façon dont la vue Allnotes est accédée, l’événement NavigatedTo
est déclenché pour la page. Cet événement est un endroit idéal pour désélectionner de force l’élément sélectionné dans CollectionView
.
Toutefois, avec le modèle MVVM appliqué ici, le modèle de vue ne peut pas déclencher quelque chose directement sur la vue, comme l’effacement de l’élément sélectionné après l’enregistrement de la note. Alors comment y arriver ? Une bonne implémentation du modèle MVVM réduit le code-behind dans la vue. Il existe plusieurs façons de résoudre ce problème pour prendre en charge le modèle de séparation MVVM. Toutefois, il est également possible de placer du code dans le code-behind de la vue, en particulier lorsqu’il est directement lié à la vue. MVVM a de nombreuses conceptions et concepts exceptionnels qui vous aident à compartimenter votre application, à améliorer la facilité de maintenance et à faciliter l’ajout de nouvelles fonctionnalités. Toutefois, dans certains cas, vous pouvez constater que MVVM encourage l’ingénierie excessive.
N’utilisez pas une ingénierie excessive pour ce problème et utilisez simplement l’événement NavigatedTo
pour effacer l’élément sélectionné dans CollectionView
.
Dans le volet Explorateur de solutions de Visual Studio, double-cliquez sur Views\AllNotesPage.xaml.
Dans le code XAML de
<ContentPage>
, ajoutez l’événementNavigatedTo
:<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:Notes.ViewModels" x:Class="Notes.Views.AllNotesPage" Title="Your Notes" NavigatedTo="ContentPage_NavigatedTo"> <ContentPage.BindingContext> <viewModels:NotesViewModel /> </ContentPage.BindingContext>
Vous pouvez ajouter un gestionnaire d’événements par défaut en cliquant avec le bouton droit sur le nom de la méthode d’événement,
ContentPage_NavigatedTo
, puis en sélectionnant Atteindre la définition. Cette action ouvre Views\AllNotesPage.xaml.cs dans l’éditeur de code.Remplacez le code du gestionnaire d’événements par l’extrait de code suivant :
private void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e) { notesCollection.SelectedItem = null; }
Dans le code XAML,
CollectionView
a le nomnotesCollection
. Ce code utilise ce nom pour accéder àCollectionView
et définirSelectedItem
surnull
. L’élément sélectionné est effacé chaque fois que la page est accédée.
À présent, exécutez votre application. Essayez d’accéder à une note, appuyez sur le bouton Précédent, puis sélectionnez la même note une deuxième fois. Le comportement de l’application est résolu !
Explorez le code de ce tutoriel.. Si vous voulez télécharger une copie du projet terminé pour y comparer votre code, téléchargez ce projet.
Félicitations !
Votre application utilise désormais des modèles MVVM !
Étapes suivantes
Les liens suivants fournissent plus d’informations sur certains des concepts que vous avez appris dans ce tutoriel :
Vous avez un défi avec cette section ? Si c'est le cas, faites-nous part de vos commentaires pour que nous puissions l'améliorer.