Esecuzione di comandi
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 INotifyPropertyChanged
e 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à:
Command
di tipoSystem.Windows.Input.ICommand
CommandParameter
di tipoObject
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
, Age
e 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 PersonViewModel
e 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à:
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="⇦"
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>
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
, BackspaceCommand
e 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:
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.