Partager via


Commandes

Browse sample. Parcourir l’exemple

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 :

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, Ageet 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 boolet 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. Button Grid 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 :

Person Entry.

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 trueet 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 PersonViewModelcollection, 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 CommandParametersur . 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="&#x21E6;"
                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="&#x00B7;"
                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, BackspaceCommandet 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 :

Decimal keyboard.

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.