Liaison de données et MVVM
Le modèle Model-View-ViewModel (MVVM) applique une séparation entre trois couches logicielles : l’interface utilisateur XAML, appelée vue, données sous-jacentes, appelées modèle et intermédiaire entre la vue et le modèle, appelée viewmodel. La vue et le viewmodel sont souvent connectés par le biais de liaisons de données définies en XAML. La BindingContext
vue est généralement une instance du viewmodel.
Important
L’interface utilisateur de l’application .NET multiplateforme (.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 viewmodel 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 viewmodels, mais pas des modèles, pour 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 MVVM dans .NET MAUI, consultez Model-View-ViewModel (MVVM) dans les modèles d’application d’entreprise à l’aide de .NET MAUI. Pour obtenir un didacticiel qui vous aide à implémenter le modèle MVVM, consultez Mettre à niveau votre application avec des concepts MVVM.
Simple MVVM
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 x:Static
balisage pour obtenir la date et l’heure actuelles de la propriété statique DateTime.Now
dans l’espace System
de noms :
<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 sur BindingContext
un StackLayout. Lorsque vous définissez l’élément BindingContext
sur un élément, il est hérité par tous les enfants de cet élément. Cela signifie que tous les enfants de l’objet 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.
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. Lorsque vous pensez en termes de MVVM, le modèle et le viewmodel 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 le viewmodel par le biais de liaisons de données. Dans MVVM, un modèle ignore le viewmodel et un viewmodel ignore la vue. Toutefois, vous personnalisez souvent les types exposés par le viewmodel 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 un viewmodel lié 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));
}
Viewmodels implémente 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 PropertyChanged
événement afin qu’il puisse être averti lorsqu’une propriété change et conserve la cible mise à jour avec la nouvelle valeur. Dans l’exemple de code précédent, la OnPropertyChanged
méthode gère le déclenchement de l’événement tout en déterminant automatiquement le nom de la source de propriété : DateTime
.
L’exemple suivant montre le code 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 l’utilisation ContentPageBindingContext
des balises d’élément de propriété. Sinon, le fichier code-behind peut instancier le viewmodel.
Extension Binding
de balisage sur la Text
propriété des Label formats de la DateTime
propriété. La capture d’écran suivante montre le résultat :
En outre, il est possible d’accéder aux propriétés individuelles de la DateTime
propriété du viewmodel en séparant les propriétés par des points :
<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >
Interactive MVVM
La machine virtuelle virtuelle est souvent utilisée 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 la HslViewModel
conversion d’une Color valeur en Hue
valeurs , Saturation
et Luminosity
les valeurs, puis de nouveau :
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 au Hue
, Saturation
et Luminosity
les propriétés entraînent la modification de la Color
propriété et les modifications apportées à la Color
propriété entraînent la modification des trois autres propriétés. Cela peut ressembler à une boucle infinie, sauf que le viewmodel n’appelle pas l’événement PropertyChanged
, sauf si la propriété a changé.
L’exemple XAML suivant contient une BoxView propriété dont Color
la propriété est liée à la Color
propriété du viewmodel, et trois Slider et trois Label vues liées aux Hue
propriétés , Saturation
et Luminosity
aux propriétés :
<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 chacun Label est la valeur par défaut OneWay
. Il doit uniquement afficher la valeur. Toutefois, la liaison par défaut sur chacune d’elles Slider est TwoWay
. Cela permet l’initialisation Slider à partir du viewmodel. Lorsque le viewmodel est instancié, sa Color
propriété est définie sur Aqua
. Modification d’une Slider nouvelle valeur pour la propriété dans le viewmodel, qui calcule ensuite une nouvelle couleur :
Commandes
Parfois, une application a besoin d’aller au-delà des liaisons de propriétés en demandant à l’utilisateur de lancer des commandes qui affectent quelque chose dans le viewmodel. 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. Le viewmodel peut contenir des commandes, qui sont des méthodes exécutées en réaction à une activité spécifique dans l’affichage, comme un Button clic. Des liaisons de données sont définies entre ces commandes et le Button.
Pour autoriser une liaison de données entre un viewmodel et un Button viewmodel, les Button deux propriétés définies sont les suivantes :
Command
de typeSystem.Windows.Input.ICommand
CommandParameter
de typeObject
Remarque
De nombreux autres contrôles définissent Command
également et CommandParameter
propriétés.
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
Le viewmodel peut définir des propriétés de type ICommand. Vous pouvez ensuite lier ces propriétés à la Command
propriété de chacun Button ou d’un autre élément, ou peut-être une vue personnalisée qui implémente cette interface. Vous pouvez éventuellement définir la CommandParameter
propriété pour identifier des objets individuels Button (ou d’autres éléments) liés à cette propriété viewmodel. En interne, les Button appels de la Execute
méthode chaque fois que l’utilisateur appuie sur , Buttonpassant à la Execute
méthode son CommandParameter
.
La méthode et CanExecuteChanged
l’événement CanExecute
sont utilisés pour les cas où un Button appui peut être actuellement non valide, auquel cas il Button doit se désactiver. Appels ButtonCanExecute
lorsque la propriété est définie pour la Command
première fois et chaque fois que l’événement CanExecuteChanged
est déclenché. Si CanExecute
elle est retournée false
, le Button désactive lui-même et ne génère Execute
pas d’appels.
Vous pouvez utiliser la ou Command<T>
la Command
classe incluse dans .NET MAUI pour implémenter l’interfaceICommand. Ces deux classes définissent plusieurs constructeurs plus une ChangeCanExecute
méthode que le viewmodel peut appeler pour forcer l’objet Command
à déclencher l’événement CanExecuteChanged
.
L’exemple suivant montre un viewmodel 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 et CanExecute
les Execute
méthodes des commandes sont définies en tant que fonctions lambda dans le constructeur. Le viewmodel part du principe que la AddCharCommand
propriété est liée à la Command
propriété 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 InputString
propriété, qui est ensuite mise en forme comme numéro de téléphone pour la DisplayText
propriété. Il existe également une deuxième propriété de type ICommand nommée DeleteCharCommand
. Cela est lié à un bouton d’espacement arrière, mais le bouton doit être désactivé s’il n’y a pas de caractères à supprimer.
L’exemple suivant montre le code XAML qui consomme les KeypadViewModel
éléments suivants :
<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 Command
propriété du premier Button lié au DeleteCharCommand
. Les autres boutons sont liés à l’objet AddCharCommand
avec un CommandParameter
caractère identique au caractère qui apparaît sur le Button:
Commentaires
https://aka.ms/ContentUserFeedback.
Bientôt disponible : Tout au long de 2024, nous allons supprimer progressivement GitHub Issues comme mécanisme de commentaires pour le contenu et le remplacer par un nouveau système de commentaires. Pour plus d’informations, consultezEnvoyer et afficher des commentaires pour