Nella puntata scorsa sono stati introdotti i concetti di base relativi il pattern Model-View-VievModel. Ad una breve introduzione storica, è seguito un esempio nel quale si è dimostrato come attingere ai dati esposti dal model per visualizzarli nell'interfaccia utente, e viceversa come intercettare e gestire l'iterazione dell'utente mediante dei semplici comandi.
Pur nella sua estrema semplicità l'esempio ha sostanzialmente riassunto il funzionamento del pattern MVVM dimostrando come separare la user-interface da logica e modello.
Tuttavia quanto illustrato non è sufficiente a completare una applicazione in tutte le sue parti. Se è vero che siamo stati in grado di popolare una semplice ListBox
, intercettare la pressione di un pulsante e aggiornare di conseguenza la lista, è altrettanto vero che in una applicazione reale ci sono logiche molto più complesse e raffinate da gestire. Potremmo ad esempio voler abilitare e disabilitare elementi dell'interfaccia in seguito alle azioni dell'utente, oppure gestire comandi di diversa natura rispetto il semplice click di un pulsante.
CommandParameter
Gli esempi di codice della scorsa puntata mostrano come collegare un comando con il click di un pulsante. Per mettere in atto questo collegamento abbiamo fatto uso di un costrutto particolare di XAML, cioè di una attached property.
La proprietà - del cui intimo funzionamento parleremo poco oltre - consente di specificare due parametri; il primo è il comando stesso, per il quale si fa uso del databinding, e l'altro è il CommandParameter
, ovvero un parametro che potremo associare all'esecuzione del comando.
Proviamo nel nostro esempio ad associare una stringa che ci consenta di discriminare tra due pulsanti collegati al medesimo DelegateCommand
. Innanzitutto occorre cambiare il tipo generico del DelegateCommand
da object
a string
. Il tipo così specificato è appunto il tipo del parametro che dovremmo immettere nel CommandParameter
:
this.SearchCommand = new DelegateCommand(this.Search, this.CanExecuteSearch);
A questo punto dobbiamo per forza modificare anche il metodo Search
che riceverà in input il valore del parametro. Ecco come:
private void Search(string action)
{
if (action == "Clear")
{
this.SearchKey = string.Empty;
this.Products.Clear();
}
else if (action == "Search")
this.Load(this.SearchKey);
}
È chiaro che l'assegnazione di una stringa vuota alla proprietà SearchKey
, provocherà la modifica del contenuto della casella di testo solamente se essa è implementata facendo uso di INotifyPropertyChanged
. Grazie a questa interfaccia infatti il databinding sarà in grado di rilevare la modifica e aggiornare la TextBox
.
A questo punto non ci rimane che modificare lo XAML aggiungendo un pulsante "Clear" e modificando il precedente con "Search":
<Button Grid.Column="1" Margin="0,3,3,3" Content="Search"
cmd:Click.Command="{Binding SearchCommand}"
cmd:Click.CommandParameter="Search" />
<Button Grid.Column="2" Margin="0,3,3,3" Content="Clear"
cmd:Click.Command="{Binding SearchCommand}"
cmd:Click.CommandParameter="Clear" />
Così facendo il metodo Search
riceverà la stringa di CommandParameter
. Questa tecnica trova svariate applicazioni nel pattern ViewModel
, qualora sia necessario specificare dei valori accessori che sono necessari per un comando.
La proprietà 'CanExecute'
Pensando in termini astratti ad un comando è intuibile che in esso può essere individuato uno stato. Esistono infatti condizioni nelle quali un comando può essere eseguito piuttosto che situazioni in cui esso non è significativo - ad esempio perchè mancano i dati che ne consentono il funzionamento - la sua esecuzione potrebbe essere persino dannosa.
Se consideriamo l'esempio della scorsa puntata, eseguire il comando di ricerca quando la casella di testo è vuota è perfettamente inutile a potrebbe essere anche fonte di errori. A nostra tutela in tale situazione possiamo seguire due strade: possiamo innanzitutto validare la chiave prima di eseguire la ricerca e quindi di conseguenza interrompere l'esecuzione, oppure possiamo disabilitare il pulsante qualora la TextBox
non sia ancora stata compilata. Questo seconda strategia ha il pregio di suggerire all'utente che deve ancora fornire ulteriori informazioni prima di compiere la sua ricerca.
L'interfaccia ICommand
, implementata dalla classe DelegateCommand<T>
che abbiamo impiegato la volta scorsa, fornisce gli strumenti necessari a questo scopo. Vediamo la sua definizione:
public interface ICommand
{
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}
L'interfaccia in questione espone un metodo CanExecute
e un evento CanExecuteChanged
che hanno rispettivamente lo scopo di verificare se il comando può essere eseguito rispetto ad un eventuale parametro e di notificare che lo stato di "eseguibilità" è cambiato. Per mezzo della classe DelegateCommand
possiamo facilmente trarre vantaggio di questa interfaccia come segue; dapprima forniamo al costruttore un metodo che restituisce un boolean che indica se il comando può essere eseguito:
public ProductViewModel(IProductRepository repository)
{
this.Repository = repository;
this.Products = new ObservableCollection<Product>();
this.SearchCommand = new DelegateCommand<object>(this.Search, this.CanExecuteSearch);
this.Load(string.Empty);
}
L'implementazione del metodo deve valutare se le condizioni attuali del ViewModel
sono adeguate all'esecuzione del comando. Nel nostro caso il comando può essere eseguito qualora la proprietà SearchKey
sia diversa da string.Empty
.
public bool CanExecuteSearch(object state)
{
return !string.IsNullOrEmpty(this.SearchKey);
}
L'attached property Click.Command
che abbiamo usato per collegare il DelegateCommand
al Button
è in grado autonomamente di gestire l'interfaccia ICommand
e adeguare lo stato abilitato e disabilitato del pulsante al risultato del nostro metodo. A noi rimane il compito di notificare al DelegateCommand
che lo stato del ViewModel è cambiato - ad esempio quando la proprietà SearchKey
cambia - e di conseguenza chiedere una nuova valutazione:
public string SearchKey
{
get { return searchKey; }
set
{
searchKey = value;
this.SearchCommand.RaiseCanExecuteChanged();
}
}
Ogni qualvolta la proprietà SearchKey
sia cambiata dall'utente, digitando nella casella di testo, il metodo RaiseCanExecuteChanged
si occuperà di informare che è necessario verificare nuovamente se il comando può essere eseguito. In risultato è ovvio in questo caso: il nostro pulsante cambierà di stato e diverrà abilitato.
Comandi custom
Un'applicazione in cui sia possibile gestire esclusivamente il Click
di pulsanti ha sicuramente vita breve. Ciononostante, la attuale versione di Prism mette a disposizione solamente questa implementazione, perciò se come spesso capita dobbiamo gestire qualcosa di più complesso ecco che è necessario scrivere un po' di codice aggiuntivo. Il codice che vedremo è abbastanza complesso, tuttavia una volta che si è preparato lo snippet di codice è sufficiente copiarlo con poche modifiche e i nuovi comandi nasceranno in pochi istanti.
Supponiamo perciò di voler aggiungere una nuova feature al nostro esempio. Mediante una ComboBox
è possibile selezionare una categoria di ricerca per filtrare con più efficacia i prodotti. La selezione di questa ComboBox
tuttavia deve applicare il filtro immediatamente, senza attendere una ulteriore pressione del pulsante Search
. Questo comportamento normalmente lo si ottiene gestendo l'evento SelectionChanged ma in un'ottica MVVM dovremo scrivere un apposito comando.
La prima cosa da fare è creare una classe estendendo CommandBehaviorBase<T>
. Il tipo generico indica il controllo a cui si deve applicare il behavior. Nel nostro caso potrebbe essere riferito alla ComboBox ma dato che l'evento SelectionChanged prende origine dal controllo primitivo Selector possiamo rendere il comando più ampiamente utilizzabile se lo riferiamo a quest'ultimo.
public class SelectionChangedCommandBehavior : CommandBehaviorBase<Selector>
{
public SelectionChangedCommandBehavior(Selector targetObject) : base(targetObject)
{
targetObject.SelectionChanged += (s, e) => base.ExecuteCommand();
}
}
Lo scopo di questa classe è evidente. Essa è incaricata di agganciare il giusto evento e di notificare l'esecuzione del comando. Versioni più complesse potrebbero ad esempio valorizzare il CommandParameter
con un valore preso dal controllo di riferimento. L'unico limite è dato dalla fantasia di chi scrive il codice. Aggiungendo questo metodo, ad esempio, possiamo fare in modo che il controllo sia nascosto quando il comando non può essere eseguito, anziché disabilitato:
protected override void UpdateEnabledState()
{
if(this.Command.CanExecute(this.CommandParameter))
this.TargetObject.Visibility = Visibility.Visible;
else
this.TargetObject.Visibility = Visibility.Collapsed;
}
A questo punto siamo pronti per scrivere tre attached properties. Le prime due sono le ovvie Command e CommandParameter
che hanno lo scopo di valorizzare questi parametri per mezzo di XAML. La terza proprietà, che sarà invisibile a livello di markup, ha il solo scopo di memorizzare l'istanza di behavior creata per questo elemento. Creiamo le proprietà:
public static class SelectionChanged
{
public static readonly DependencyProperty SelectionChangedBehaviorProperty =
DependencyProperty.RegisterAttached("SelectionChangedBehaviorProperty",
typeof(SelectionChangedCommandBehavior),
typeof(SelectionChangedCommandBehavior),
null);
#region CommandProperty
public static readonly DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command", typeof(ICommand),
typeof(SelectionChanged),
new PropertyMetadata(CommandProperty_Changed));
public static ICommand GetCommand(DependencyObject obj)
{
return (ICommand)obj.GetValue(CommandProperty);
}
public static void SetCommand(DependencyObject obj, ICommand value)
{
obj.SetValue(CommandProperty, value);
}
private static void CommandProperty_Changed(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs e)
{
Selector targetObject = dependencyObject as Selector;
if (targetObject != null)
GetOrCreateBehavior(targetObject).Command = e.NewValue as ICommand;
}
#endregion
#region CommandParameterProperty
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.RegisterAttached("CommandParameter", typeof(object),
typeof(SelectionChanged),
new PropertyMetadata(CommandParameterProperty_Changed));
public static ICommand GetCommandParameter(DependencyObject obj)
{
return (ICommand)obj.GetValue(CommandParameterProperty);
}
public static void SetCommandParameter(DependencyObject obj, ICommand value)
{
obj.SetValue(CommandParameterProperty, value);
}
private static void CommandParameterProperty_Changed(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs e)
{
Selector targetObject = dependencyObject as Selector;
if (targetObject != null)
GetOrCreateBehavior(targetObject).CommandParameter = e.NewValue;
}
#endregion
private static SelectionChangedCommandBehavior GetOrCreateBehavior(Selector targetObject)
{
SelectionChangedCommandBehavior behavior =
targetObject.GetValue(SelectionChangedBehaviorProperty) as
SelectionChangedCommandBehavior;
if (behavior == null)
{
behavior = new SelectionChangedCommandBehavior(targetObject);
targetObject.SetValue(SelectionChangedBehaviorProperty, behavior);
}
return behavior;
}
}
Il metodo GetOrCreateBehavior
, riportato verso la fine della classe, verrò chiamato ogni volta le proprietà Command
e CommandParameter
sono valorizzate. In questo modo la prima volta il metodo crea una istanza del behavior e di conseguenza aggancia gli eventi del controllo.
Le volte successive l'istanza del behavior viene recuperata dalla apposita attached property dove è stato memorizzato precedentemente. In questo modo, quanto l'evento SelectionChanged
viene sollevato, il relativo comando viene eseguito. Ora non resta che inserire il comando nel markup e scrivere il codice che lo gestisca:
<ComboBox Grid.Row="1" Grid.ColumnSpan="3" Margin="3,0,3,3"
ItemsSource="{Binding Categories}"
code:SelectionChanged.Command="{Binding CategoryChangedCommand}"
SelectedItem="{Binding SelectedCategory, Mode=TwoWay}"
DisplayMemberPath="Name" />
Il comando apparirà a tutti gli effetti come quello di Click dei pulsanti, anche se ovviamente nel namespace che rappresenta il nostro codice. Lascio al codice allegato a questo articolo la parte di implementazione del ViewModel che a questo punto dovrebbe essere abbastanza chiara.
Come dicevo in precedenza il codice da scrivere per ogni comando è molto e soprattutto ripetitivo. Per questo motivo è un ottimo candidato per uno snippet di VisualStudio. Nel link riportato in [1] potrete trovare lo snippet già confezionato, pronto da essere importato in Visual Studio.