Commandes
Dans une application d’application multiplateforme .NET (.NET MAUI) qui utilise le modèle Model-View-ViewModel (MVVM), les liaisons de données sont définies entre les propriétés du viewmodel, qui est généralement une classe qui dérive de INotifyPropertyChanged
, et des propriétés dans la vue, qui est généralement le fichier XAML. Parfois, une application a besoin d’aller au-delà de ces liaisons de propriétés en obligeant l’utilisateur à 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
Pour utiliser l’interface de commande, vous définissez une liaison de données qui cible la Command
propriété de l’emplacement Button où la source est une propriété dans le viewmodel de type ICommand. Le viewmodel contient du code associé à cette ICommand propriété exécutée lorsque le bouton est cliqué. Vous pouvez définir la CommandParameter
propriété sur des données arbitraires pour faire la distinction entre plusieurs boutons s’ils sont tous liés à la même ICommand propriété dans le viewmodel.
De nombreuses autres vues définissent également et CommandParameter
propriétésCommand
. Toutes ces commandes peuvent être gérées dans un viewmodel à l’aide d’une approche qui ne dépend pas de l’objet d’interface utilisateur dans la vue.
ICommands
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 :
public interface ICommand
{
public void Execute (Object parameter);
public bool CanExecute (Object parameter);
public event EventHandler CanExecuteChanged;
}
Pour utiliser l’interface de commande, votre viewmodel doit contenir des propriétés de type ICommand:
public ICommand MyCommand { private set; get; }
Le viewmodel doit également référencer une classe qui implémente l’interface ICommand . Dans la vue, la Command
propriété d’un Button est liée à cette propriété :
<Button Text="Execute command"
Command="{Binding MyCommand}" />
Lorsque l’utilisateur appuie sur le Button, le Button appelle la méthode Execute
dans l’objet ICommand lié à sa propriété Command
.
Lorsque la liaison est initialement définie sur la propriété Command
du Button, quand la liaison de données est modifiée d’une façon ou d’une autre, le Button appelle la méthode CanExecute
dans l’objet ICommand. Si CanExecute
retourne false
, le Button se désactive. Cela indique que la commande particulière est actuellement indisponible ou non valide.
Le Button attache également un gestionnaire sur l’événement CanExecuteChanged
de l’objet ICommand. L’événement est déclenché à partir du viewmodel. Lorsque cet événement est déclenché, les Button appels CanExecute
sont de nouveau effectués. Le Button s’active lui-même si CanExecute
retourne true
et se désactive si CanExecute
retourne false
.
Avertissement
N’utilisez pas la propriété IsEnabled
de Button si vous utilisez l’interface de commande.
Lorsque votre viewmodel définit une propriété de type ICommand, le viewmodel doit également contenir ou référencer une classe qui implémente l’interface ICommand . Cette classe doit contenir ou référencer les méthodes Execute
et CanExecute
, et déclencher l’événement CanExecuteChanged
chaque fois que la méthode CanExecute
peut retourner une valeur différente. Vous pouvez utiliser la ou Command<T>
la Command
classe incluse dans .NET MAUI pour implémenter l’interfaceICommand. Ces classes vous permettent de spécifier les corps des méthodes Execute
et CanExecute
dans les constructeurs de classe.
Conseil
Utilisez Command<T>
quand vous utilisez la propriété pour faire la CommandParameter
distinction entre plusieurs vues liées à la même ICommand propriété et la Command
classe quand cela n’est pas obligatoire.
Commandes de base
Les exemples suivants illustrent les commandes de base implémentées dans un viewmodel.
La PersonViewModel
classe définit trois propriétés nommées Name
, Age
et Skills
qui définissent une personne :
public class PersonViewModel : INotifyPropertyChanged
{
string name;
double age;
string skills;
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
set { SetProperty(ref name, value); }
get { return name; }
}
public double Age
{
set { SetProperty(ref age, value); }
get { return age; }
}
public string Skills
{
set { SetProperty(ref skills, value); }
get { return skills; }
}
public override string ToString()
{
return Name + ", age " + Age;
}
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
La PersonCollectionViewModel
classe ci-dessous crée de nouveaux objets de type PersonViewModel
et permet à l’utilisateur de remplir les données. À cet effet, la classe définit IsEditing
, de type bool
et PersonEdit
, de type PersonViewModel
, des propriétés. En outre, la classe définit trois propriétés de type ICommand et une propriété nommée Persons
de type IList<PersonViewModel>
:
public class PersonCollectionViewModel : INotifyPropertyChanged
{
PersonViewModel personEdit;
bool isEditing;
public event PropertyChangedEventHandler PropertyChanged;
···
public bool IsEditing
{
private set { SetProperty(ref isEditing, value); }
get { return isEditing; }
}
public PersonViewModel PersonEdit
{
set { SetProperty(ref personEdit, value); }
get { return personEdit; }
}
public ICommand NewCommand { private set; get; }
public ICommand SubmitCommand { private set; get; }
public ICommand CancelCommand { private set; get; }
public IList<PersonViewModel> Persons { get; } = new ObservableCollection<PersonViewModel>();
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Dans cet exemple, les modifications apportées aux trois ICommand propriétés et à la Persons
propriété n’entraînent PropertyChanged
pas de levée d’événements. Ces propriétés sont toutes définies lorsque la classe est créée pour la première fois et ne changent pas.
L’exemple suivant montre le code XAML qui consomme les PersonCollectionViewModel
é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:DataBindingDemos"
x:Class="DataBindingDemos.PersonEntryPage"
Title="Person Entry">
<ContentPage.BindingContext>
<local:PersonCollectionViewModel />
</ContentPage.BindingContext>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- New Button -->
<Button Text="New"
Grid.Row="0"
Command="{Binding NewCommand}"
HorizontalOptions="Start" />
<!-- Entry Form -->
<Grid Grid.Row="1"
IsEnabled="{Binding IsEditing}">
<Grid BindingContext="{Binding PersonEdit}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Text="Name: " Grid.Row="0" Grid.Column="0" />
<Entry Text="{Binding Name}"
Grid.Row="0" Grid.Column="1" />
<Label Text="Age: " Grid.Row="1" Grid.Column="0" />
<StackLayout Orientation="Horizontal"
Grid.Row="1" Grid.Column="1">
<Stepper Value="{Binding Age}"
Maximum="100" />
<Label Text="{Binding Age, StringFormat='{0} years old'}"
VerticalOptions="Center" />
</StackLayout>
<Label Text="Skills: " Grid.Row="2" Grid.Column="0" />
<Entry Text="{Binding Skills}"
Grid.Row="2" Grid.Column="1" />
</Grid>
</Grid>
<!-- Submit and Cancel Buttons -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Text="Submit"
Grid.Column="0"
Command="{Binding SubmitCommand}"
VerticalOptions="Center" />
<Button Text="Cancel"
Grid.Column="1"
Command="{Binding CancelCommand}"
VerticalOptions="Center" />
</Grid>
<!-- List of Persons -->
<ListView Grid.Row="3"
ItemsSource="{Binding Persons}" />
</Grid>
</ContentPage>
Dans cet exemple, la propriété de BindingContext
la page est définie sur le PersonCollectionViewModel
. Contient un texte New avec sa Command
propriété liée à la NewCommand
propriété dans le viewmodel, un formulaire d’entrée avec des propriétés liées à la IsEditing
propriété, ainsi que des propriétés de PersonViewModel
, et deux autres boutons liés aux propriétés et CancelCommand
aux SubmitCommand
propriétés du viewmodel.ButtonGrid Affiche ListView la collection de personnes déjà entrées :
La capture d’écran suivante montre le bouton Envoyer activé une fois qu’un âge a été défini :
Lorsque l’utilisateur appuie d’abord sur le bouton Nouveau , cela active le formulaire d’entrée, mais désactive le bouton Nouveau . L’utilisateur entre alors un nom, un âge et des compétences. À tout moment pendant la saisie, l’utilisateur peut appuyer sur le bouton Annuler pour repartir de zéro. Une fois seulement qu’un nom et un âge valides ont été entrés, le bouton Submit (Envoyer) est activé. L’appui sur le bouton Submit transfère la personne vers la collection affichée par le ListView. Après un appui sur le bouton Cancel (Annuler) ou Submit (Envoyer), le formulaire d’entrée est effacé et le bouton New (Nouveau) est activé de nouveau.
Toute la logique pour les boutons New, Submit et Cancel est gérée dans PersonCollectionViewModel
via les définitions des propriétés NewCommand
, SubmitCommand
et CancelCommand
. Le constructeur du PersonCollectionViewModel
définit ces trois propriétés sur des objets de type Command
.
Un constructeur de la Command
classe vous permet de passer des arguments de type Action
et Func<bool>
correspondant aux méthodes et CanExecute
aux Execute
méthodes. Cette action et cette fonction peuvent être définies en tant que fonctions lambda dans le Command
constructeur :
public class PersonCollectionViewModel : INotifyPropertyChanged
{
···
public PersonCollectionViewModel()
{
NewCommand = new Command(
execute: () =>
{
PersonEdit = new PersonViewModel();
PersonEdit.PropertyChanged += OnPersonEditPropertyChanged;
IsEditing = true;
RefreshCanExecutes();
},
canExecute: () =>
{
return !IsEditing;
});
···
}
void OnPersonEditPropertyChanged(object sender, PropertyChangedEventArgs args)
{
(SubmitCommand as Command).ChangeCanExecute();
}
void RefreshCanExecutes()
{
(NewCommand as Command).ChangeCanExecute();
(SubmitCommand as Command).ChangeCanExecute();
(CancelCommand as Command).ChangeCanExecute();
}
···
}
Lorsque l’utilisateur clique sur le bouton New, la fonction execute
passée au constructeur Command
est exécutée. Ceci crée un nouvel objet PersonViewModel
, définit un gestionnaire sur l’événement PropertyChanged
de cet objet, définit IsEditing
sur true
et appelle la méthode RefreshCanExecutes
définie après le constructeur.
Outre l’implémentation de l’interface ICommand, la classe Command
définit également une méthode nommée ChangeCanExecute
. Un viewmodel doit appeler ChangeCanExecute
une ICommand propriété chaque fois que quelque chose se produit qui peut modifier la valeur de retour de la CanExecute
méthode. Un appel à ChangeCanExecute
provoque le déclenchement de la méthode CanExecuteChanged
par la classe Command
. Le Button dispose d’un gestionnaire joint pour cet événement et répond en appelant CanExecute
à nouveau, puis en s’activant lui-même sur la base de la valeur renvoyée par cette méthode.
Lorsque la méthode execute
de NewCommand
appelle RefreshCanExecutes
, la propriété NewCommand
reçoit un appel à ChangeCanExecute
et le Button appelle la méthode canExecute
, qui retourne maintenant false
, car la propriété IsEditing
est désormais égale à true
.
Le PropertyChanged
gestionnaire du nouvel PersonViewModel
objet appelle la ChangeCanExecute
méthode de SubmitCommand
:
public class PersonCollectionViewModel : INotifyPropertyChanged
{
···
public PersonCollectionViewModel()
{
···
SubmitCommand = new Command(
execute: () =>
{
Persons.Add(PersonEdit);
PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
PersonEdit = null;
IsEditing = false;
RefreshCanExecutes();
},
canExecute: () =>
{
return PersonEdit != null &&
PersonEdit.Name != null &&
PersonEdit.Name.Length > 1 &&
PersonEdit.Age > 0;
});
···
}
···
}
La fonction canExecute
pour SubmitCommand
est appelée chaque fois qu’il existe une propriété modifiée dans l’objet PersonViewModel
en cours de modification. Elle retourne true
uniquement lorsque la propriété Name
contient au moins un caractère et que la propriété Age
est supérieure à 0. À ce stade, le bouton Submit devient actif.
La execute
fonction de Submit supprime le gestionnaire modifié par la propriété de la PersonViewModel
collection, ajoute l’objet à la Persons
collection et retourne tout à son état initial.
La fonction execute
pour le bouton Cancel fait tout ce que le bouton Submit fait, si ce n’est ajouter l’objet à la collection :
public class PersonCollectionViewModel : INotifyPropertyChanged
{
···
public PersonCollectionViewModel()
{
···
CancelCommand = new Command(
execute: () =>
{
PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
PersonEdit = null;
IsEditing = false;
RefreshCanExecutes();
},
canExecute: () =>
{
return IsEditing;
});
}
···
}
La méthode canExecute
retourne true
lorsqu’un PersonViewModel
est en cours de modification.
Remarque
Il n’est pas nécessaire de définir les méthodes execute
et canExecute
en tant que fonctions lambda. Vous pouvez les écrire en tant que méthodes privées dans le viewmodel et les référencer dans les Command
constructeurs. Toutefois, cette approche peut entraîner de nombreuses méthodes référencées une seule fois dans le viewmodel.
Utilisation des paramètres de commande
Il est parfois pratique pour un ou plusieurs boutons, ou d’autres objets d’interface utilisateur, de partager la même ICommand propriété dans le viewmodel. Dans ce cas, vous pouvez utiliser la propriété pour faire la CommandParameter
distinction entre les boutons.
Vous pouvez continuer à utiliser la classe Command
pour ces propriétés ICommand partagées. La classe définit un constructeur alternatif qui accepte execute
et canExecute
méthodes avec des paramètres de type Object
. C’est ainsi que CommandParameter
est passé à ces méthodes. Toutefois, lors de la spécification d’un CommandParameter
, il est plus simple d’utiliser la classe générique Command<T>
pour spécifier le type de l’objet défini CommandParameter
sur . Les méthodes execute
et canExecute
que vous spécifiez ont des paramètres de ce type.
L’exemple suivant illustre un clavier permettant d’entrer des nombres décimaux :
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DataBindingDemos"
x:Class="DataBindingDemos.DecimalKeypadPage"
Title="Decimal Keyboard">
<ContentPage.BindingContext>
<local:DecimalKeypadViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<Style TargetType="Button">
<Setter Property="FontSize" Value="32" />
<Setter Property="BorderWidth" Value="1" />
<Setter Property="BorderColor" Value="Black" />
</Style>
</ContentPage.Resources>
<Grid WidthRequest="240"
HeightRequest="480"
ColumnDefinitions="80, 80, 80"
RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto"
ColumnSpacing="2"
RowSpacing="2"
HorizontalOptions="Center"
VerticalOptions="Center">
<Label Text="{Binding Entry}"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"
Margin="0,0,10,0"
FontSize="32"
LineBreakMode="HeadTruncation"
VerticalTextAlignment="Center"
HorizontalTextAlignment="End" />
<Button Text="CLEAR"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
Command="{Binding ClearCommand}" />
<Button Text="⇦"
Grid.Row="1" Grid.Column="2"
Command="{Binding BackspaceCommand}" />
<Button Text="7"
Grid.Row="2" Grid.Column="0"
Command="{Binding DigitCommand}"
CommandParameter="7" />
<Button Text="8"
Grid.Row="2" Grid.Column="1"
Command="{Binding DigitCommand}"
CommandParameter="8" />
<Button Text="9"
Grid.Row="2" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="9" />
<Button Text="4"
Grid.Row="3" Grid.Column="0"
Command="{Binding DigitCommand}"
CommandParameter="4" />
<Button Text="5"
Grid.Row="3" Grid.Column="1"
Command="{Binding DigitCommand}"
CommandParameter="5" />
<Button Text="6"
Grid.Row="3" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="6" />
<Button Text="1"
Grid.Row="4" Grid.Column="0"
Command="{Binding DigitCommand}"
CommandParameter="1" />
<Button Text="2"
Grid.Row="4" Grid.Column="1"
Command="{Binding DigitCommand}"
CommandParameter="2" />
<Button Text="3"
Grid.Row="4" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="3" />
<Button Text="0"
Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2"
Command="{Binding DigitCommand}"
CommandParameter="0" />
<Button Text="·"
Grid.Row="5" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="." />
</Grid>
</ContentPage>
Dans cet exemple, la page BindingContext
est un DecimalKeypadViewModel
. La Entry propriété de ce viewmodel est liée à la Text
propriété d’un Label. Tous les Button objets sont liés à des commandes dans le viewmodel : ClearCommand
, BackspaceCommand
et DigitCommand
. Les 11 boutons pour les 10 chiffres et la virgule décimale partagent une liaison à DigitCommand
. La propriété CommandParameter
fait la distinction entre ces boutons. La valeur définie CommandParameter
est généralement la même que le texte affiché par le bouton, à l’exception du point décimal, qui à des fins de clarté est affiché avec un caractère de point central :
Définit DecimalKeypadViewModel
une Entry propriété de type string
et trois propriétés de type ICommand:
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
string entry = "0";
public event PropertyChangedEventHandler PropertyChanged;
···
public string Entry
{
private set
{
if (entry != value)
{
entry = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Entry"));
}
}
get
{
return entry;
}
}
public ICommand ClearCommand { private set; get; }
public ICommand BackspaceCommand { private set; get; }
public ICommand DigitCommand { private set; get; }
}
Le bouton correspondant à celui-ci ClearCommand
est toujours activé et définit l’entrée sur « 0 » :
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
···
public DecimalKeypadViewModel()
{
ClearCommand = new Command(
execute: () =>
{
Entry = "0";
RefreshCanExecutes();
});
···
}
void RefreshCanExecutes()
{
((Command)BackspaceCommand).ChangeCanExecute();
((Command)DigitCommand).ChangeCanExecute();
}
···
}
Comme le bouton est toujours actif, il n’est pas nécessaire de spécifier un argument canExecute
dans le constructeur Command
.
Le bouton Backspace (Retour arrière) est actif uniquement lorsque la longueur de l’entrée est supérieure à 1, ou si Entry n’est pas égal à la chaîne « 0 » :
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
···
public DecimalKeypadViewModel()
{
···
BackspaceCommand = new Command(
execute: () =>
{
Entry = Entry.Substring(0, Entry.Length - 1);
if (Entry == "")
{
Entry = "0";
}
RefreshCanExecutes();
},
canExecute: () =>
{
return Entry.Length > 1 || Entry != "0";
});
···
}
···
}
La logique de la fonction execute
pour le bouton Backspace garantit que Entry est au moins une chaîne de « 0 ».
La propriété DigitCommand
est liée à 11 boutons, dont chacun s’identifie à la propriété CommandParameter
. La DigitCommand
valeur est définie sur une instance de la Command<T>
classe. Lorsque vous utilisez l’interface de commande avec XAML, les CommandParameter
propriétés sont généralement des chaînes, qui est le type de l’argument générique. Les fonctions execute
et canExecute
ont alors des arguments de type string
:
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
···
public DecimalKeypadViewModel()
{
···
DigitCommand = new Command<string>(
execute: (string arg) =>
{
Entry += arg;
if (Entry.StartsWith("0") && !Entry.StartsWith("0."))
{
Entry = Entry.Substring(1);
}
RefreshCanExecutes();
},
canExecute: (string arg) =>
{
return !(arg == "." && Entry.Contains("."));
});
}
···
}
La méthode execute
ajoute l’argument de chaîne à la propriété Entry. Toutefois, si le résultat commence par un zéro (mais pas par un zéro et une virgule décimale), ce zéro initial doit être supprimé à l’aide de la fonction Substring
. La méthode canExecute
retourne false
uniquement si l’argument est la virgule décimale (indiquant un appui sur la virgule décimale) et que Entry contient déjà une virgule décimale. Toutes les méthodes execute
appellent RefreshCanExecutes
, qui appelle à son tour ChangeCanExecute
pour DigitCommand
et ClearCommand
. Cela garantit que les boutons de virgule décimale et retour arrière sont actifs ou désactivés en fonction de la séquence actuelle de chiffres saisis.