M-V-VM: Commanding
Posted by mauro in Software Mason on Thursday 02 September 2010 at 11:40 AM
Finalmente ci siamo, dopo anni luce dall’ultima volta, siamo all’ultimo step della nostra parte introduttiva su Model View ViewModel. Il bello inzia adesso :-)
L’ultimo requisito che abbiamo, per questa parte, è la possibilità di cancellare un indirizzo dalla lista degli indirizzi della persona corrente.
Commanding
Prima di addentrarci nei tecnicismi dell’implementazione introduciamo una delle “novità”, alla faccia della novità… 6 anni sono passati, di Wpf, l’accoppiata ICommand/ICommandSource.
ICommand
Un command è una classe che rappresenta un’azione, concetto semplicissimo per chi arriva da Delphi in cui esistono proprio le Action. Per Wpf un command è una qualsiasi classe che implementa l’interfaccia ICommand che è caratterizzata da:
- Boolean CanExecute( Object ): l’infrastruttura di Wpf chiama CanExecute per sapere se in quel determinato momento il comando debba essere considerato “disponibile”;
- void Execute( Object ): Execute viene chiamato per effettivamente invocare il comando e permettergli di eseguire l’azione per cui è pensato, Execute verrà invocato se e solo se CanExecute ritorna true;
- EventHanlder CanExecuteChanged: Un comando può sollevare questo evento per notificare al motore di Wpf che il suo stato è cambiato ed è quindi il caso che Wpf venga a controllare nuovamente con una chiamata a CanExecute;
Un esempio tipico potrebbe essere un ipotetico SaveCommand che a seguito di un chiamata a CanExecute ritorna true se il grafo è effettivamente modificato, lo stesso command può notificare a Wpf che il suo stato è cambiato quando lo stato del grafo cambia ed infine il command si preoccuperà di innescare la logica per il salvataggio all’atto dell’execute.
n.d.r.: CanExecute ed Execute prendono in ingresso un parametro di tipo object che possiamo usare sulla falsa riga di un command argument che potrebbe essere utile per pilotare dall’esterno parte della logica interna de command.
ICommandSource
La controparte di un ICommand è ICommandSource. Una classe che implementa ICommandSource è qualcosa, tipicamente un controllo della UI, che è in grado di invocare un comando. ICommandSource è così definita:
- ICommand Command{ get; set; }: è la dependency property, quindi con pieno supporto per data binding, che ci consente di impostare quale sia il comando che vogliamo venga eseguito;
- Object CommandParameter{ get; set; }: è la dependency property che ci consente di impostare l’eventuale parametro da passare al comando;
- IInputElement CommandTarget{ get; set; }: Ha senso, e viene utilizzata, solo ed esclusivamente se il comando è un RoutedCommand, tipologia di comando che non ci interessa trattare;
Anche in questo caso un esempio tipico è un bottone, quando impostiamo la proprietà command la prima cosa che succede è che il bottone fa una chiamata a CanExecute per capire lo stato del comando e nel caso in cui l’esito sia negativo automaticamente si disabilita. Allo stesso modo quando il comando scatena l’evento CanExecuteChanged il bottone fa nuovamente una chiamata a CanExecute per rivalutare il suo stato e in caso affermativo si riabilita.
Ogni controllo è libero di implementare a suo piacimento la logica e il comportamento da seguire a seguito della variazione di stato di un comando, come abbiamo detto, ad esempio, il bottone si disabilita.
DelegateCommand
Wpf mette a disposizione un largo set di command già pronti all’uso, questi sono tutti di tipo RoutedCommand che, in un mondo basato su Model View ViewModel, ha una serie di difetti: in primis quello di non avere nessuna possibilità di controllo sul processo di notifica della variazione di stato del comando. I limiti di RoutedCommand e di RoutedUICommand hanno portato alla nascita di svariate implementazioni dell’interfaccia ICommand, implementazioni che tipicamente cadono sotto il nome di DelegateCommand o RelayCommand.
Il tutto si “limita” ad un’implementazione più o meno stilisticamente bella che consente di generare un comando agganciando al volo dei delegati, tipicamente facendo largo uso di una sintassi basata sulle lambda, che verranno eseguiti sia per determnare lo stato del comando (CanExecute) sia per eseguire il comando (Execute); inoltre tutti espongono un meccamismo per poter triggherare la notifica della varizione di stato del comando stesso.
Anche Radical non è da meno e offre un’interessante implementazione.
AddressDeleteCommand
Adesso che sappiamo un po’ meglio la teoria non ci resta che metterla in pratica. La prima cosa che ci interessa fare è esporre la funzionalità dal nostro MainViewModel, semplicemente esponendo una proprietà di tipo ICommand:
public ICommand DeleteAddressCommand { get; private set; }
contestualmente agganciando, via DataBinding, la nuova proprietà ad un nuovo elemento della UI:
<Button Content="Delete" Command="{Binding Path=DeleteAddressCommand}" Margin="0,0,317,0" />
l’implementazione del nostro comando è decisamente semplice anche se dietro le quinte succedono parecchie cose:
this.DeleteAddressCommand = DelegateCommand.Create() .OnCanExecute( o => this.SelectedAddress != null ) .OnExecute( o => { var add = this.SelectedAddress; this.SelectedAddress = null; this.Person.Addresses.Remove( add ); } ) .AddMonitor ( PropertyObserver.For( this ) .Observe( vm => vm.SelectedAddress ) );
Radical offre un entry point statico, e anche un costruttore volendo, per la creazione di un nuovo comando. La cosa comoda è il supporto per le fluent interface che vi permette di definire le caratteristiche del comando in maniera decisamente intuitiva:
- OnCanExecute si aspetta un Func
- OnExecute accetta invece come parametro una Action
La parte veramente interessante, di cui ho già parlato (sintassi a parte che è leggermente cambiata), è AddMonitor che vi permette di agganciare dei “trigger” al command, trigger che altro non fanno che automatizzare il processo di notifica, alla UI, della variazione di stato del comando stesso.
Fatto tutto ciò se mandiamo in esecuzione la nostra bellissima applicazione (bellissima è auto-consolatorio
Come tradizione allego i sorgenti: Mvvm.Application 1.3
Il prossimo requisito? essere in grado di creare una nuova “Person” con la sua simpatica lista di “Address”.
.m
) quello che otteniamo è questo:
#1 da Omar Damiani Thursday September 2010 alle 12:08
Bel post...spiega davvero molto bene tutto il "giro del fumo" dei command...
grazie.
#2 da dancerjude Sunday September 2010 alle 07:27
"...concetto semplicissimo per chi arriva da Delphi in cui esistono proprio le Action..."
eh si, le TAction... bei tempi andati... mi è scesa una lacrimuccia...
pensare che dopo 7 anni di Delphi sfrenato sono passato nel 2002 su .NET che poi è stato fatto da Anders Hejlsberg che aveva progettato guardacaso proprio Delphi...
peccato che Borland/Inprise/Borland/CodeGear/Embarcadero Delphi non abbia avuto gli onori che meritava, anche se in giro per il mondo sono sempre tantissimi che lo usano ancora... e quello che rimane della Borland è stato comprato diversi anni fa da Micro Focus, per intenderci quella del "Micro Focus Cobol"... mah
#3 da Mauro D. Tuesday September 2010 alle 11:21
Mauro, spulciando il tuo esempio noto come nel costruttore del VM copi le proprietà della tua entity sulle proprietà del VM. Chiaramente il tuo esempio è base per dimostrare quello che scrivi nel post, ma usando questa stessa metodologia unita a Nhibernate ho evidenti problemi nella persistenza della stessa entità.
) .. hai qualche input da darmi?Nhibernate mi lancia un eccezione se nel metodo di salvataggio copio il valore di tutte le proprietà nelle proprietà della entità sorgente (soprattutto se stiamo parlando di IList
#4 da Mauro Servienti Tuesday September 2010 alle 12:42
Ciao Mauro,
from somewhere...
();
le liste e tutt i "reference" che per NH sono ei proxy devi sincronizzarli non puoi sostituirli ovviamente.
io mi sono fatto un extension method che fa più o meno questo:
var source = //IEnumerable
var dest = new List
source.SyncTo( dest );
.m