Liaison de données et MVVM
Le modèle modèle-vue-vue modèle (MVVM) applique une séparation entre trois couches logicielles : l’interface utilisateur XAML, appelée vue, les données sous-jacentes, appelées modèle, et un intermédiaire entre la vue et le modèle, appelée vue modèle. La vue et la vue modèle sont souvent connectése par le biais de liaisons de données définies dans XAML. La vue BindingContext
est généralement une instance de la vue modèle.
Important
.NET Multi-platform App UI (.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.
Il existe plusieurs approches pour implémenter le modèle MVVM, et cet article se concentre sur une approche simple. Il utilise des vues et des vues modèles, mais pas des modèles, pour se concentrer sur la liaison de données entre les deux calques. Pour obtenir une explication détaillée de l’utilisation du modèle MVVM dans .NET MAUI, consultez modèle-vue-vue modèle (MVVM) dans les modèles d’application d’entreprise à l’aide de .NET MAUI. Pour obtenir un tutoriel qui vous aide à implémenter le modèle MVVM, consultez Mettre à niveau votre application avec des concepts MVVM.
MVVM simple
Dans les extensions de balisage XAML, vous avez vu comment définir une nouvelle déclaration d’espace de noms XML pour permettre à un fichier XAML de référencer des classes dans d’autres assemblys. L’exemple suivant utilise l’extension de balisage x:Static
pour obtenir la date et l’heure actuelles de la propriété statique DateTime.Now
dans l’espace de noms System
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard"
x:Class="XamlSamples.OneShotDateTimePage"
Title="One-Shot DateTime Page">
<VerticalStackLayout BindingContext="{x:Static sys:DateTime.Now}"
Spacing="25" Padding="30,0"
VerticalOptions="Center" HorizontalOptions="Center">
<Label Text="{Binding Year, StringFormat='The year is {0}'}" />
<Label Text="{Binding StringFormat='The month is {0:MMMM}'}" />
<Label Text="{Binding Day, StringFormat='The day is {0}'}" />
<Label Text="{Binding StringFormat='The time is {0:T}'}" />
</VerticalStackLayout>
</ContentPage>
Dans cet exemple, la valeur récupérée DateTime
est définie comme le BindingContext
sur un StackLayout. Lorsque vous définissez le BindingContext
sur un élément, il est hérité par tous les enfants de cet élément. Cela signifie que tous les enfants du StackLayout ont le même BindingContext
, et qu’ils peuvent contenir des liaisons à des propriétés de cet objet :
Toutefois, le problème est que la date et l’heure sont définies une fois lorsque la page est construite et initialisée, et ne change jamais.
Avertissement
Dans une classe qui dérive de BindableObject, seules les propriétés du type BindableProperty peuvent être liées. Par exemple, VisualElement.IsLoaded et Element.Parent ne peuvent pas être liés.
Une page XAML peut afficher une horloge qui affiche toujours l’heure actuelle, mais nécessite du code supplémentaire. Le modèle MVVM est un choix naturel pour les applications .NET MAUI lors de la liaison de données à partir de propriétés entre les objets visuels et les données sous-jacentes. En termes de MVVM, le modèle et la vue modèle sont des classes écrites entièrement dans le code. La vue est souvent un fichier XAML qui fait référence aux propriétés définies dans la vue modèle par le biais de liaisons de données. Dans MVVM, un modèle ignore la vue modèle et une vue modèle ignore la vue. Toutefois, vous personnalisez souvent les types exposés par la vue modèle aux types associés à l’interface utilisateur.
Remarque
Dans des exemples simples de MVVM, tels que ceux présentés ici, il n’existe souvent aucun modèle, et le modèle implique simplement une vue et une vue modèle liées avec des liaisons de données.
L’exemple suivant montre une vue modèle pour une horloge, avec une propriété unique nommée DateTime
qui est mise à jour toutes les secondes :
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace XamlSamples;
class ClockViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private DateTime _dateTime;
private Timer _timer;
public DateTime DateTime
{
get => _dateTime;
set
{
if (_dateTime != value)
{
_dateTime = value;
OnPropertyChanged(); // reports this property
}
}
}
public ClockViewModel()
{
this.DateTime = DateTime.Now;
// Update the DateTime property every second.
_timer = new Timer(new TimerCallback((s) => this.DateTime = DateTime.Now),
null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
}
~ClockViewModel() =>
_timer.Dispose();
public void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Les vues modèles implémentent généralement l’interface INotifyPropertyChanged
, ce qui permet à une classe de déclencher l’événement PropertyChanged
chaque fois qu’une de ses propriétés change. Le mécanisme de liaison de données dans .NET MAUI attache un gestionnaire à cet événement PropertyChanged
afin d’être averti lorsqu’une propriété change et de mettre à jour la cible avec la nouvelle valeur. Dans l’exemple de code précédent, la méthode OnPropertyChanged
gère le déclenchement de l’événement tout en déterminant automatiquement le nom de la source de la propriété : DateTime
.
L’exemple suivant montre XAML qui consomme ClockViewModel
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.ClockPage"
Title="Clock Page">
<ContentPage.BindingContext>
<local:ClockViewModel />
</ContentPage.BindingContext>
<Label Text="{Binding DateTime, StringFormat='{0:T}'}"
FontSize="18"
HorizontalOptions="Center"
VerticalOptions="Center" />
</ContentPage>
Dans cet exemple, ClockViewModel
est défini sur le BindingContext
de la ContentPage à l’aide des balises d’élément de propriété. Sinon, le fichier code-behind peut instancier la vue modèle.
L’extension de balisage Binding
lie sur la propriété Text
du Label formate la propriété DateTime
. La capture d’écran suivante montre le résultat :
En outre, il est possible d’accéder aux propriétés individuelles de la propriété DateTime
de la vue modèle en séparant les propriétés par des points :
<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >
MVVM interactif
MVVM est souvent utilisé avec des liaisons de données bidirectionnelle pour une vue interactive basée sur un modèle de données sous-jacent.
L’exemple suivant montre le HslViewModel
qui convertit une valeur Color en valeurs Hue
, Saturation
et Luminosity
, et inversement :
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace XamlSamples;
class HslViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private float _hue, _saturation, _luminosity;
private Color _color;
public float Hue
{
get => _hue;
set
{
if (_hue != value)
Color = Color.FromHsla(value, _saturation, _luminosity);
}
}
public float Saturation
{
get => _saturation;
set
{
if (_saturation != value)
Color = Color.FromHsla(_hue, value, _luminosity);
}
}
public float Luminosity
{
get => _luminosity;
set
{
if (_luminosity != value)
Color = Color.FromHsla(_hue, _saturation, value);
}
}
public Color Color
{
get => _color;
set
{
if (_color != value)
{
_color = value;
_hue = _color.GetHue();
_saturation = _color.GetSaturation();
_luminosity = _color.GetLuminosity();
OnPropertyChanged("Hue");
OnPropertyChanged("Saturation");
OnPropertyChanged("Luminosity");
OnPropertyChanged(); // reports this property
}
}
}
public void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Dans cet exemple, les modifications apportées aux propriétés Hue
, Saturation
et Luminosity
entraînent la modification de la propriété Color
et les modifications apportées à la propriété Color
entraînent la modification des trois autres propriétés. Cela peut ressembler à une boucle infinie, sauf que la vue modèle n’appelle pas l’événement PropertyChanged
, sauf si la propriété a changé.
L’exemple XAML suivant contient une BoxView dont la propriété Color
est liée à la propriété Color
de la vue modèle, ainsi que les trois vues Slider et les trois vues Label liées aux propriétés Hue
, Saturation
et Luminosity
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.HslColorScrollPage"
Title="HSL Color Scroll Page">
<ContentPage.BindingContext>
<local:HslViewModel Color="Aqua" />
</ContentPage.BindingContext>
<VerticalStackLayout Padding="10, 0, 10, 30">
<BoxView Color="{Binding Color}"
HeightRequest="100"
WidthRequest="100"
HorizontalOptions="Center" />
<Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Hue}"
Margin="20,0,20,0" />
<Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Saturation}"
Margin="20,0,20,0" />
<Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
HorizontalOptions="Center" />
<Slider Value="{Binding Luminosity}"
Margin="20,0,20,0" />
</VerticalStackLayout>
</ContentPage>
La liaison sur chaque Label est la valeur par défaut OneWay
. Seule la valeur doit être affichée. Toutefois, la liaison par défaut sur chaque Slider est TwoWay
. Le Slider peut ainsi être initialisé à partir de la vue modèle. Lorsque la vue modèle est instanciée, sa propriété Color
est définie sur Aqua
. Une modification du Slider définit une nouvelle valeur pour la propriété dans la vue modèle, qui calcule ensuite une nouvelle couleur :
Commandes
Parfois, une application a des besoins qui vont au-delà de ces liaisons de propriété en exigeant de l’utilisateur qu’il lance des commandes qui affectent un élément dans la vue modèle. Ces commandes sont généralement signalées par des clics de bouton ou des appuis tactiles et, en règle générale, elles sont traitées dans le fichier code-behind dans un gestionnaire pour l’événement Clicked
du Button ou l’événement Tapped
d’un TapGestureRecognizer.
L’interface d’exécution de commandes fournit une alternative à l’implémentation des commandes qui est beaucoup mieux adaptée à l’architecture MVVM. La vue modèle peut contenir des commandes, qui sont des méthodes exécutées en réaction à une activité spécifique dans la vue, comme un clic sur un Button. Des liaisons de données sont définies entre ces commandes et le Button.
Pour permettre une liaison de données entre un Button et une vue modèle, le Button définit deux propriétés :
Command
de typeSystem.Windows.Input.ICommand
CommandParameter
de typeObject
Remarque
De nombreux autres contrôles définissent également les propriétés Command
et CommandParameter
.
L’interface ICommand est définie dans l’espace de noms System.Windows.Input et se compose de deux méthodes et d’un événement :
void Execute(object arg)
bool CanExecute(object arg)
event EventHandler CanExecuteChanged
La vue modèle peut définir des propriétés de type ICommand. Vous pouvez ensuite lier ces propriétés à la propriété Command
de chaque élément Button ou autre, ou peut-être à une vue personnalisée qui implémente cette interface. Vous pouvez éventuellement définir la propriété CommandParameter
pour identifier des objets individuels Button (ou d’autres éléments) liés à cette propriété vue modèle. En interne, le Button appelle la méthode Execute
chaque fois que l’utilisateur appuie sur , le Button, passant à la méthode Execute
son CommandParameter
.
La méthode CanExecute
et l’événement CanExecuteChanged
sont utilisés pour les cas où un clic sur Button peut être actuellement non valide, auquel cas le Button doit se désactiver. Le Button appelle CanExecute
lorsque la propriété Command
est définie pour la première fois et que chaque fois que l’événement CanExecuteChanged
est déclenché. Si CanExecute
retourne false
, le Button se désactive et ne génère pas d’appels Execute
.
Vous pouvez utiliser la classe Command
ou Command<T>
incluse dans .NET MAUI pour implémenter l’interface ICommand. Ces deux classes définissent plusieurs constructeurs plus une méthode ChangeCanExecute
que la vue modèle peut appeler pour forcer l’objet Command
à déclencher l’événement CanExecuteChanged
.
L’exemple suivant montre une vue modèle pour un pavé numérique simple destiné à entrer des numéros de téléphone :
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace XamlSamples;
class KeypadViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _inputString = "";
private string _displayText = "";
private char[] _specialChars = { '*', '#' };
public ICommand AddCharCommand { get; private set; }
public ICommand DeleteCharCommand { get; private set; }
public string InputString
{
get => _inputString;
private set
{
if (_inputString != value)
{
_inputString = value;
OnPropertyChanged();
DisplayText = FormatText(_inputString);
// Perhaps the delete button must be enabled/disabled.
((Command)DeleteCharCommand).ChangeCanExecute();
}
}
}
public string DisplayText
{
get => _displayText;
private set
{
if (_displayText != value)
{
_displayText = value;
OnPropertyChanged();
}
}
}
public KeypadViewModel()
{
// Command to add the key to the input string
AddCharCommand = new Command<string>((key) => InputString += key);
// Command to delete a character from the input string when allowed
DeleteCharCommand =
new Command(
// Command will strip a character from the input string
() => InputString = InputString.Substring(0, InputString.Length - 1),
// CanExecute is processed here to return true when there's something to delete
() => InputString.Length > 0
);
}
string FormatText(string str)
{
bool hasNonNumbers = str.IndexOfAny(_specialChars) != -1;
string formatted = str;
// Format the string based on the type of data and the length
if (hasNonNumbers || str.Length < 4 || str.Length > 10)
{
// Special characters exist, or the string is too small or large for special formatting
// Do nothing
}
else if (str.Length < 8)
formatted = string.Format("{0}-{1}", str.Substring(0, 3), str.Substring(3));
else
formatted = string.Format("({0}) {1}-{2}", str.Substring(0, 3), str.Substring(3, 3), str.Substring(6));
return formatted;
}
public void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Dans cet exemple, les méthodes Execute
et CanExecute
des commandes sont définies en tant que fonctions lambda dans le constructeur. La vue modèle part du principe que la propriété AddCharCommand
est liée à la propriété Command
de plusieurs boutons (ou tout autre contrôle ayant une interface de commande), chacun d’entre eux est identifié par le CommandParameter
. Ces boutons ajoutent des caractères à une propriété, InputString
qui est ensuite mise en forme comme numéro de téléphone pour la propriété DisplayText
. Il existe également une deuxième propriété de type ICommand nommée DeleteCharCommand
. Cela est lié à un bouton de retour arrière, mais le bouton doit être désactivé s’il n’y a pas de caractères à supprimer.
L’exemple suivant montre le XAML qui consomme le KeypadViewModel
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.KeypadPage"
Title="Keypad Page">
<ContentPage.BindingContext>
<local:KeypadViewModel />
</ContentPage.BindingContext>
<Grid HorizontalOptions="Center" VerticalOptions="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80" />
<ColumnDefinition Width="80" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<Label Text="{Binding DisplayText}"
Margin="0,0,10,0" FontSize="20" LineBreakMode="HeadTruncation"
VerticalTextAlignment="Center" HorizontalTextAlignment="End"
Grid.ColumnSpan="2" />
<Button Text="⇦" Command="{Binding DeleteCharCommand}" Grid.Column="2"/>
<Button Text="1" Command="{Binding AddCharCommand}" CommandParameter="1" Grid.Row="1" />
<Button Text="2" Command="{Binding AddCharCommand}" CommandParameter="2" Grid.Row="1" Grid.Column="1" />
<Button Text="3" Command="{Binding AddCharCommand}" CommandParameter="3" Grid.Row="1" Grid.Column="2" />
<Button Text="4" Command="{Binding AddCharCommand}" CommandParameter="4" Grid.Row="2" />
<Button Text="5" Command="{Binding AddCharCommand}" CommandParameter="5" Grid.Row="2" Grid.Column="1" />
<Button Text="6" Command="{Binding AddCharCommand}" CommandParameter="6" Grid.Row="2" Grid.Column="2" />
<Button Text="7" Command="{Binding AddCharCommand}" CommandParameter="7" Grid.Row="3" />
<Button Text="8" Command="{Binding AddCharCommand}" CommandParameter="8" Grid.Row="3" Grid.Column="1" />
<Button Text="9" Command="{Binding AddCharCommand}" CommandParameter="9" Grid.Row="3" Grid.Column="2" />
<Button Text="*" Command="{Binding AddCharCommand}" CommandParameter="*" Grid.Row="4" />
<Button Text="0" Command="{Binding AddCharCommand}" CommandParameter="0" Grid.Row="4" Grid.Column="1" />
<Button Text="#" Command="{Binding AddCharCommand}" CommandParameter="#" Grid.Row="4" Grid.Column="2" />
</Grid>
</ContentPage>
Dans cet exemple, la propriété Command
du premier Button lié à la DeleteCharCommand
. Les autres boutons sont liés à la AddCharCommand
avec un CommandParameter
identique au caractère qui apparaît sur le Button :