Condividi tramite


Esecuzione di comandi

Browse sample. Esplorare l'esempio

In un'app .NET multipiattaforma (.NET MAUI) che usa il modello Model-View-ViewModel (MVVM), i data binding vengono definiti tra le proprietà nel modello di visualizzazione, che in genere è una classe che deriva da INotifyPropertyChangede proprietà nella visualizzazione, che in genere è il file XAML. A volte un'app ha esigenze che vanno oltre queste associazioni di proprietà richiedendo all'utente di avviare comandi che influiscono su un elemento nel modello di visualizzazione. Questi comandi vengono in genere segnalati dal clic su un pulsante o dal tocco con un dito e in genere vengono elaborati nel file code-behind in un gestore per l'evento Clicked dell'elemento Button o per l'evento Tapped di un elemento TapGestureRecognizer.

L'interfaccia di esecuzione dei comandi consente un approccio alternativo all'implementazione di comandi decisamente più adatto all'architettura MVVM. Il modello di visualizzazione può contenere comandi, ovvero metodi eseguiti in reazione a un'attività specifica nella visualizzazione, ad esempio un Button clic. Tra questi comandi e l'elemento Button vengono definiti data binding.

Per consentire un data binding tra un Button oggetto e un modello di visualizzazione, definisce Button due proprietà:

Per usare l'interfaccia del comando, si definisce un data binding destinato alla Command proprietà dell'oggetto Button in cui l'origine è una proprietà nel modello di visualizzazione di tipo ICommand. Il modello di visualizzazione contiene il codice associato a tale ICommand proprietà eseguita quando si fa clic sul pulsante. È possibile impostare la CommandParameter proprietà su dati arbitrari per distinguere tra più pulsanti se sono associati alla stessa ICommand proprietà nel modello di visualizzazione.

Molte altre visualizzazioni definiscono Command anche le proprietà e CommandParameter . Tutti questi comandi possono essere gestiti all'interno di un modello di visualizzazione usando un approccio che non dipende dall'oggetto interfaccia utente nella visualizzazione.

ICommands

L'interfaccia ICommand è definita nello spazio dei nomi System.Windows.Input ed è costituita da due metodi e da un evento:

public interface ICommand
{
    public void Execute (Object parameter);
    public bool CanExecute (Object parameter);
    public event EventHandler CanExecuteChanged;
}

Per usare l'interfaccia del comando, il modello di visualizzazione deve contenere proprietà di tipo ICommand:

public ICommand MyCommand { private set; get; }

Il modello di visualizzazione deve anche fare riferimento a una classe che implementa l'interfaccia ICommand . Nella visualizzazione la Command proprietà di un Button oggetto è associata a tale proprietà:

<Button Text="Execute command"
        Command="{Binding MyCommand}" />

Quando l'utente preme l'elemento Button, l'elemento Button chiama il metodo Execute nell'oggetto ICommand associato alla sua proprietà Command.

Se il binding viene prima definito nella proprietà Command dell'elemento Button, quando il data binding cambia, l'elemento Button chiama il metodo CanExecute nell'oggetto ICommand. Se CanExecute restituisce false, l'elemento Button si disabilita automaticamente. Ciò indica che in quel momento il comando specificato non è disponibile o non è valido.

L'elemento Button, poi, associa un gestore all'evento CanExecuteChanged di ICommand. L'evento viene generato dall'interno del modello di visualizzazione. Quando viene generato l'evento, viene Button nuovamente chiamato CanExecute . L'elemento Button viene abilitato automaticamente se CanExecute torna true e viene disabilitato automaticamente se CanExecute torna false.

Avviso

Se si usa l'interfaccia di comando, non usare la proprietà IsEnabled di Button.

Quando il modello di visualizzazione definisce una proprietà di tipo ICommand, il modello di visualizzazione deve contenere o fare riferimento a una classe che implementa l'interfaccia ICommand . Questa classe deve contenere o fare riferimento ai metodi Execute e CanExecute e generare l'evento CanExecuteChanged ogni volta che il metodo CanExecute può restituire un valore diverso. È possibile usare la Command classe o Command<T> inclusa in .NET MAUI per implementare l'interfaccia ICommand . Queste classi consentono di specificare il corpo dei metodi Execute e CanExecute nei costruttori delle classi.

Suggerimento

Utilizzare Command<T> quando si usa la CommandParameter proprietà per distinguere tra più visualizzazioni associate alla stessa ICommand proprietà e la Command classe quando non è un requisito.

Comandi di base

Gli esempi seguenti illustrano i comandi di base implementati in un modello di visualizzazione.

La PersonViewModel classe definisce tre proprietà denominate Name, Agee Skills che definiscono una persona:

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 illustrata di seguito crea nuovi oggetti di tipo PersonViewModel e consente all'utente di compilare i dati. A tale scopo, la classe definisce IsEditing, di tipo bool, e PersonEdit, di tipo PersonViewModel, proprietà . La classe, poi, definisce tre proprietà di tipo ICommand e una proprietà, Persons, di tipo 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));
    }
}

In questo esempio, le modifiche apportate alle tre ICommand proprietà e la Persons proprietà non comportano PropertyChanged la generazione di eventi. Queste proprietà sono tutte impostate quando la classe viene creata per la prima volta e non cambia.

L'esempio seguente mostra il codice XAML che usa :PersonCollectionViewModel

<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>

In questo esempio la proprietà della BindingContext pagina è impostata su PersonCollectionViewModel. Grid Contiene un Button oggetto con il testo New con la relativa Command proprietà associata alla NewCommand proprietà nel modello di visualizzazione, una maschera di immissione con proprietà associate alla IsEditing proprietà , nonché le proprietà di PersonViewModele due pulsanti associati alle SubmitCommand proprietà e CancelCommand del modello di visualizzazione. Visualizza ListView la raccolta di persone già immesse:

Lo screenshot seguente mostra il pulsante Invia abilitato dopo l'impostazione di un'età:

Person Entry.

Quando l'utente preme per la prima volta il pulsante Nuovo , viene abilitato il modulo di immissione, ma viene disabilitato il pulsante Nuovo . L'utente immette quindi un nome, l'età e le competenze. In qualsiasi momento durante il processo di modifica, l'utente può selezionare il pulsante Cancel (Annulla) per ricominciare da capo. Il pulsante Submit (Invia) viene abilitato solo dopo l'immissione di un nome e di un'età validi. La selezione del pulsante Submit (Invia) trasferisce la persona nella raccolta visualizzata dall'elemento ListView. Quando si seleziona Cancel (Annulla) o Submit (Invia), il modulo di immissione viene cancellato e il pulsante New (Nuovo) viene abilitato di nuovo.

Tutta la logica per i pulsanti New (Nuovo), Submit (Invia) e Cancel (Annulla) viene gestita in PersonCollectionViewModel tramite la definizione delle proprietà NewCommand, SubmitCommand e CancelCommand. Il costruttore della classe PersonCollectionViewModel imposta queste tre proprietà su oggetti di tipo Command.

Un costruttore della Command classe consente di passare argomenti di tipo Action e Func<bool> corrispondenti ai Execute metodi e CanExecute . Questa azione e funzione possono essere definite come funzioni lambda nel Command costruttore:

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();
    }
    ···
}

Quando l'utente fa clic sul pulsante New (Nuovo), viene eseguita la funzione execute passata al costruttore Command. Ciò crea un nuovo oggetto PersonViewModel, imposta un gestore per l'evento PropertyChanged di tale oggetto, imposta IsEditing su true e chiama il metodo RefreshCanExecutes definito dopo il costruttore.

Oltre all'implementazione dell'interfaccia ICommand, la classe Command definisce anche un metodo denominato ChangeCanExecute. Un modello di visualizzazione deve chiamare ChangeCanExecute per una ICommand proprietà ogni volta che si verifica un evento che potrebbe modificare il valore restituito del CanExecute metodo. Una chiamata a ChangeCanExecute fa sì che la classe Command attivi il metodo CanExecuteChanged. All'elemento Button è associato un gestore di tale evento. Il pulsante risponde chiamando di nuovo CanExecute e quindi abilitandosi automaticamente in base al valore restituito dal metodo.

Quando il metodo execute di NewCommand chiama RefreshCanExecutes, la proprietà NewCommand ottiene una chiamata a ChangeCanExecute e l'elemento Button chiama il metodo canExecute, che ora restituisce false perché la proprietà IsEditing ora è true.

Il PropertyChanged gestore per il nuovo PersonViewModel oggetto chiama il ChangeCanExecute metodo di 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 funzione canExecute per SubmitCommand viene chiamata ogni volta che viene cambiata una proprietà nell'oggetto PersonViewModel in corso di modifica. Restituisce true solo quando la proprietà Name è lunga almeno un carattere e Age è maggiore di 0. A quel punto, il pulsante Submit (Invia) viene abilitato.

La execute funzione per Submit rimuove il gestore modificato dalla proprietà da PersonViewModel, aggiunge l'oggetto all'insieme Persons e restituisce tutti gli elementi allo stato iniziale.

La funzione execute per il pulsante Cancel (Annulla) esegue le stesse operazioni del pulsante Submit (Invia), ad eccezione dell'aggiunta dell'oggetto alla raccolta:

public class PersonCollectionViewModel : INotifyPropertyChanged
{
    ···
    public PersonCollectionViewModel()
    {
        ···
        CancelCommand = new Command(
            execute: () =>
            {
                PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
                PersonEdit = null;
                IsEditing = false;
                RefreshCanExecutes();
            },
            canExecute: () =>
            {
                return IsEditing;
            });
    }
    ···
}

Il metodo canExecute restituisce true in qualsiasi momento durante la modifica di un elemento PersonViewModel.

Nota

Non è necessario definire i metodi execute e canExecute come funzioni lambda. È possibile scriverli come metodi privati nel modello di visualizzazione e farvi riferimento nei Command costruttori. Tuttavia, questo approccio può comportare molti metodi a cui viene fatto riferimento una sola volta nel modello di visualizzazione.

Uso dei parametri di comando

A volte è utile per uno o più pulsanti o altri oggetti dell'interfaccia utente condividere la stessa ICommand proprietà nel modello di visualizzazione. In questo caso, è possibile utilizzare la CommandParameter proprietà per distinguere tra i pulsanti.

È possibile continuare a usare la classe Command per queste proprietà ICommand condivise. La classe definisce un costruttore alternativo che accetta execute metodi e canExecute con parametri di tipo Object. Questo è il modo in cui CommandParameter viene passato a questi metodi. Tuttavia, quando si specifica un oggetto CommandParameter, è più semplice usare la classe generica Command<T> per specificare il tipo dell'oggetto impostato su CommandParameter. I metodi execute e canExecute specificati hanno parametri di quel tipo.

Nell'esempio seguente viene illustrata una tastiera per l'immissione di numeri decimali:

<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>

In questo esempio, la pagina BindingContext è un oggetto DecimalKeypadViewModel. La Entry proprietà di questo modello di visualizzazione è associata alla Text proprietà di un oggetto Label. Tutti gli Button oggetti sono associati ai comandi nel modello di visualizzazione: ClearCommand, BackspaceCommande DigitCommand. Gli 11 pulsanti per le 10 cifre e il separatore decimale condividono un'associazione a DigitCommand. CommandParameter consente di distinguere questi pulsanti. Il valore impostato su CommandParameter è in genere uguale al testo visualizzato dal pulsante ad eccezione del separatore decimale, che ai fini della chiarezza viene visualizzato con un carattere punto centrale:

Decimal keyboard.

Definisce DecimalKeypadViewModel una Entry proprietà di tipo string e tre proprietà di tipo 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; }
}

Il pulsante corrispondente a ClearCommand è sempre abilitato e imposta nuovamente la voce su "0":

public class DecimalKeypadViewModel : INotifyPropertyChanged
{
    ···
    public DecimalKeypadViewModel()
    {
        ClearCommand = new Command(
            execute: () =>
            {
                Entry = "0";
                RefreshCanExecutes();
            });
        ···
    }

    void RefreshCanExecutes()
    {
        ((Command)BackspaceCommand).ChangeCanExecute();
        ((Command)DigitCommand).ChangeCanExecute();
    }
    ···
}

Poiché il pulsante è sempre abilitato, non è necessario specificare un argomento canExecute nel costruttore Command.

Il pulsante Backspace viene abilitato solo se la lunghezza dell'immissione è maggiore di 1 o se Entry non è uguale alla stringa "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 logica della funzione execute per il pulsante Backspace assicura che Entry corrisponda almeno alla stringa "0".

La proprietà DigitCommand è associata a 11 pulsanti, ognuno dei quali identifica se stesso con la proprietà CommandParameter. l'oggetto DigitCommand è impostato su un'istanza della Command<T> classe . Quando si usa l'interfaccia di comando con XAML, le CommandParameter proprietà sono in genere stringhe, ovvero il tipo dell'argomento generico. Le funzioni execute e canExecute hanno quindi argomenti di tipo 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("."));
            });
    }
    ···
}

Il metodo execute accoda l'argomento stringa alla proprietà Entry. Se tuttavia il risultato inizia con uno zero (ma non con uno zero e un separatore decimale), lo zero iniziale deve essere rimosso tramite la funzione Substring. Il metodo canExecute restituisce false solo se l'argomento è il separatore decimale (che indica che è stato premuto il separatore decimale) ed Entry contiene già un separatore decimale. Tutti i metodi execute chiamano RefreshCanExecutes, che a sua volta chiama ChangeCanExecute sia per DigitCommand che per ClearCommand. Ciò garantisce che i pulsanti Backspace e del separatore decimale siano abilitati o disabilitati in base alla sequenza corrente delle cifre immesse.