M-V-VM: master-details
L’ultima volta, troppo tempo fa, abbiamo parlato del primo approccio a Model View ViewModel. Andiamo avanti e prima di addentrarci nelle vere problematiche legate all’implementazione di questo pattern vediamo di capire come gestire un modello che sia un filo più complesso di quello dell’ultima volta:
la solita solfa, un bel master-detail “tipicamente tipico”, prossimamente faremo anche un esempio N-N e non sempre e solo 1-N; fissiamo degli obiettivi per questo giro:
- essere in grado di visualizzare la lista degli indirizzi;
- essere in grado di modificare un indirizzo;
- essere in grado di eliminare un indirizzo;
- abbiamo la necessità di capire come visualizzare una lista di elementi in una view;
- abbiamo la necessità di capire come tenere traccia dell’elemento selezionato nella lista degli indirizzi;
- abbiamo la necessità di capire come visualizzare i dati dell’elemento selezionato;
- abbiamo la necessità di capire come gestire un’azione, la cancellazione in questo caso;
i primi due punti sono un “problema” che deleghiamo volentieri al motore di data binding di wpf, ma prima di vedere come preoccupiamoci di aggiornare il nostro ViewModel al fine di soddisfare i nuovi requisiti:
Refactoring
La prima cosa che notiamo è un massiccio refactoring dall’ultima volta:
il MainViewModel innanzitutto comincia a fare il MainViewModel e non rappresenta più una Person, ruolo che non gli si addiceva, abbiamo introdotto un AbstractViewModel che faccia da classe base per tutti e abbiamo introdotto i ViewModel sia per Person che per Address. In perfetta sintonia con il concetto di Separation of Concern il modello è completamente mediato dai ViewModel e non è mai esposto direttamente alla View. In questo esempio la gerarchia dei ViewModel è speculare alla gerarchia del modello ma non deve essere necessariamente così: il modello ha lo scopo di modellare il nostro mondo, la realtà che vogliamo rappresentare, mentre il ViewModel ha lo scopo di adattare il modello ad uno specifico Use Case.
Questa nuova struttura ci permette di soddisfare il primo requisito.
“List” Data Binding
Aggiorniamo la nostra UI:
con questo semplice xaml:
Abbiamo aggiunto alla nostra View una ListView, nulla di trascendentale, ci sono però un paio di cose degne di nota:<ListView Grid.ColumnSpan="2" ItemsSource="{Binding Path=Person.Addresses}" Grid.Row="4" HorizontalAlignment="Stretch" Margin="12" VerticalAlignment="Stretch"> <ListView.View> <GridView> <GridViewColumn DisplayMemberBinding="{Binding Converter={StaticResource streetAddressConverter}}" Header="Street" /> <GridViewColumn Header="City" DisplayMemberBinding="{Binding Path=City}" /> <GridView> <ListView.View> <ListView>
- per agganciare la lista degli indirizzi alla ListView mattiamo in binding la collection Addresses esposta da PersonViewModel con la proprietà ItemsSource:
- se andiamo a spulciare notiamo che Addresses è una ObservableCollection
, una ObservableCollection è una collection apposita per Wpf che ha la capacità di notificare alla UI le variazioni interne alla collection, una sorta di INotifyPropertyChanged per le liste;
- se andiamo a spulciare notiamo che Addresses è una ObservableCollection
- per “realizzare” le colonne ci limitiamo a mettere in binding una GridViewColumn con il dato che ci interessa, anche qui una cosa nuova degna di nota: in uno dei casi utilizziamo un converter. Avete inoltre notato che non stiamo specificando esplicitamente un Path verso una proprietà questo significa che il motore di binding deve usare tutta l’entità e non una specifica proprietà, quindi in questo caso la colonna sarà in binding con l’intera istanza dell’AddressViewModel.
Un converter è una qualsiasi classe che implementa l’interfaccia IValueConverter:
un converter si inserisce nella binding pipeline e ha la possibilità di manipolare il contenuto del binding secondo questo schema:public class StreetAddressConverter : IValueConverter { public object Convert( object value, Type targetType, object parameter, CultureInfo culture ) { if( value != null ) { var address = ( AddressViewModel )value; return String.Format( "{0}, {1}", address.Street, address.Number ); } return null; } public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture ) { throw new NotImplementedException(); } }
Per completare il tutto quello che ci resta da fare è modificare il nostro MainViewModel così:
Qui facciamo una cosa sola: abbiamo cablato, per ora, la creazione del modello per tenere bassa la complessità.public class MainViewModel : AbstractViewModel { public MainViewModel() { var person = new Model.Person( new DateTime( 1973, 1, 10 ) ) { FirstName = "Mauro", LastName = "Servienti" }; person.Addresses.Add ( new Model.Address( person ) { Street = "v. Nonzo", Number = "12/B", City = "Città del Mondo", ZipCode = "10101", Province = "SW", Country = "Italy" } ); person.Addresses.Add ( new Model.Address( person ) { Street = "v. Schiaffino", Number = "11", City = "Milano", ZipCode = "20100", Province = "MI", Country = "Italy" } ); this.Person = new PersonViewModel( person ); } private PersonViewModel _person = null; public PersonViewModel Person { get { return this._person; } set { if( value != this.Person ) { this._person = value; this.OnPropertyChanged( () => this.Person ); } } } }
Creiamo l’istanza di Person la popoliamo con le proprietà del caso e con un paio di indirizzi giusto per avere dei dati da visualizzare, infine creaimo il ViewModel per mediare il rapporto tra la Person e la View.
I ViewModel che wrappano le nostre entità non fanno nulla di trascendentale, l’unica cosa degna di nota è il costruttore del PersonViewModel che a sua volta crea la lista degli indirizzi:
Per ora direi che ci possiamo fermare qui, abbiamo messo un bel po’ di carne al fuoco, al prossimo giro vediamo di completare i requisiti che ci siamo dati ad inizio post:public PersonViewModel( Model.Person person ) { this.person = person; this.FirstName = this.person.FirstName; this.LastName = this.person.LastName; this.BornDate = this.person.BornDate; this.Age = this.EvalAge( this.BornDate ); this.Addresses = this.person.Addresses .Aggregate( new ObservableCollection<AddressViewModel>(), ( list, obj ) => { var avm = new AddressViewModel( obj ); list.Add( avm ); return list; } ); }
essere in grado di visualizzare la lista degli indirizzi;- essere in grado di modificare un indirizzo;
- essere in grado di eliminare un indirizzo;
.m