Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
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. Le BindingContext de la vue est généralement une instance du viewmodel.
Important
.NET Multi-platform App UI (.NET MAUI) achemine les mises à jour des liaisons 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 modèles de vue, mais pas de modèles, afin de se concentrer sur la liaison de données entre les deux couches. Pour obtenir une explication détaillée de l’utilisation du Modèle-Vue-VueModèle (MVVM) dans .NET MAUI, consultez Modèle-Vue-VueModè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 XAML de balisage, vous avez vu comment définir une nouvelle déclaration d’espace de noms XML afin de permettre à un fichier XAML de référencer des classes dans d’autres assemblies. L’exemple suivant utilise l’extension de balisage x:Static pour obtenir la date et l’heure actuelles à partir 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"
x:DataType="sys:DateTime">
<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 éléments 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 vers 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 le modèle de vue et un modèle de vue 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 un viewmodel 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 modèles de vue implémentent généralement l’interface INotifyPropertyChanged, 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 pour être informé lorsque une propriété change et 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 du XAML qui utilise 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"
x:DataType="local:ClockViewModel">
<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 le ViewModel.
L'extension de balisage Binding sur la propriété Text du Label formate la propriété DateTime. La capture d’écran suivante montre le résultat :
Capture d’écran d’une page affichant la date et l’heure via un ViewModel.
En outre, il est possible d’accéder aux propriétés individuelles de la propriété DateTime du viewmodel 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 retour :
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’invoque pas l’événement PropertyChanged à moins que la propriété n’ait changé.
L’exemple XAML suivant contient une BoxView dont la propriété Color est liée à la propriété Color du ViewModel, ainsi que trois vues Slider et trois vues Label, toutes 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"
x:DataType="local:HslViewModel">
<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 du modèle de vue. Lorsque le viewmodel est instancié, sa propriété Color est définie sur Aqua. Une modification d'un Slider définit une nouvelle valeur pour la propriété dans le ViewModel, qui calcule ensuite une nouvelle couleur :
MVVM utilisant les liaisons de données bidirectionnelles.
Commande
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 déclenchées par des clics de bouton ou des appuis tactiles. Habituellement, elles sont traitées dans le fichier « code-behind » au sein d'un gestionnaire pour l'événement Clicked du Button ou pour 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. Le ViewModel 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. 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 un ViewModel, le Button définit deux propriétés :
-
Commandde typeSystem.Windows.Input.ICommand -
CommandParameterde 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 comprend deux méthodes ainsi qu'un événement :
void Execute(object arg)bool CanExecute(object arg)event EventHandler CanExecuteChanged
Le modèle de vue peut définir des propriétés de type ICommand. Vous pouvez ensuite lier ces propriétés à la propriété Command de chaque Button ou autre élément, 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é modèle de vue. En interne, le Button appelle la méthode Execute chaque fois que l’utilisateur appuie sur le Button, en passant à la méthode Execute son CommandParameter.
La méthode CanExecute et l’événement CanExecuteChanged sont utilisés pour les cas où un tapotement sur Button pourrait être actuellement non valide, auquel cas le Button devrait se désactiver. Le Button appelle CanExecute lorsque la propriété Command est définie pour la première fois et 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 ainsi qu'une méthode ChangeCanExecute que le modèle de vue peut appeler pour forcer l'objet Command à déclencher l'événement CanExecuteChanged.
L’exemple suivant montre un modèle de vue pour un pavé numérique simple destiné à la saisie de 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. Le modèle de vue part du principe que la propriété AddCharCommand est liée à la propriété Command de plusieurs boutons (ou tout autre contrôle disposant d'une interface de commande), chacun étant identifié par le CommandParameter. Ces boutons ajoutent des caractères à la propriété InputString, qui est ensuite formatée en 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 utilise 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"
x:DataType="local:KeypadViewModel">
<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 qui est associée à la DeleteCharCommand. Les autres boutons sont liés au AddCharCommand avec un CommandParameter identique au caractère qui apparaît sur le Button :
Capture d’écran d’une calculatrice utilisant MVVM et des commandes.
Parcourez l'exemple