Con questo post desidero iniziare una serie di trattazioni, seppur introduttiva, relativa all’ormai famoso pattern Model-View-ViewModel da un punto di vista dello sviluppatore Visual Basic 2010, anche in considerazione di due cose:
- c’è sicuramente molto materiale, soprattutto in inglese e soprattutto di livello già avanzato. Ma ci vuole anche un punto per cominciare!
- a parte il grande webcast di Corrado, un articolo di Cristian e gli sforzi di Mauro, c’è ben poco a livello introduttivo in italiano e questo è il problema principale per chi vuole iniziare M-V-VM
- su M-V-VM con Visual Basic non c’è praticamente nulla :-)
Premesso che M-V-VM si applica sia a WPF che Silverlight, con alcune differenze tra le 2 tecnologie, in questi miei post mi riferirò a WPF 4. Farò altre considerazioni, alcune anche di carattere personale, nel corso della discussione.
Cos’è
Se sviluppate con Windows Presentation Foundation e Silverlight, molto probabilmente avete sentito parlare del pattern Model-View-ViewModel. Si tratta di un pattern la cui finalità è quella di fornire uno strato di separazione, il più possibile elevato, tra dati e strato di presentazione, di modo che all’interno di quest’ultimo ci sia esclusivamente codice relativo alla gestione della user interface, ma non della gestione dei dati. Questo è possibile grazie al potente motore di data-binding di WPF che consente a uno strato intermedio (ViewModel) che si pone tra dati (Model) e interfaccia (View) di eseguire le operazioni richieste attraverso binding di oggetti e tecniche di commanding.
M-V-VM è un pattern, quindi un insieme di linee guida che perseguono un obiettivo. Il che significa che non è “la” regola assoluta né che bisogna utilizzarlo sempre e comunque. Anzi, ci sono scenari in cui M-V-VM non è il massimo.
Benefici
M-V-VM è particolarmente utile per i seguenti motivi:
- completa separazione tra dati e interfaccia grafica
- completa separazione tra i ruoli di grafico e sviluppatore (uno degli scopi primari che ha portato alla nascita di XAML)
- testabilità: come vedremo in seguito, eseguire unit test nei confronti di un ViewModel ha decisamente senso al contrario della loro esecuzione in contesti di UI
- Model e ViewModel non hanno necessità di cambiare se cambia la UI
- Indipendentemente dal tipo di Model (oggetti business, Entity Data Model ecc.), il ViewModel mantiene la stessa logica
- migliore “Blendability”, ossia la possibilità di lavorare sul progetto con Expression Blend
M-V-VM è anche un pattern tecnicamente complesso. Per esempio, un semplice click su un pulsante che apre una nuova dialog ha una logica ben diversa e complessa rispetto a quanto siamo abituati a fare. Al di fuori dai punti sopra elencati, è necessario fare un’analisi puntuale delle proprie necessità prima di ricorrervi. Esistono degli interessanti toolkit che facilitano la creazione di applicazioni basate su M-V-VM, ma purtroppo supportano in via esclusiva Visual C#. Poco male, scriveremo qualche riga di codice in più ma capiremo meglio i concetti e impareremo da soli a riutilizzare alcuni componenti.
Considerazioni personali
Premetto che non sono un guru di Model-View-ViewModel, affatto. Prendo la scusa di questo blog per condividere il percorso che ho fatto e che sto tuttora facendo nell’approfondimento della tematica in questione. Mi farebbe anzi piacere che i più esperti lascino i propri commenti. Quello che posso dire è che durante gli studi di M-V-VM la cosa che balza agli occhi è che non c’è una via unica di applicarlo. Ci sono molte tecniche, molte scelte, molteplici implementazioni e molti modi di scrivere i propri oggetti. Detto questo, in questo e nei prossimi post seguirò una linea che è dettata da quella che secondo me possiamo definire come “in media stat virtus” :-) e che può essere riadattata a più contesti. Ciò premesso, è bene poi che ognuno di voi interessato alla materia utilizzi un motore di ricerca e vada ad approfondire autonomamente. E ora passiamo alle cose serie.
Come la vedo io
Questo è il primo di una serie di post. In questo verrà creata una semplicissima applicazione che carica dei dati da un documento XML e li mostra in una DataGrid. Poi la sequenza sarà:
- implementazione del “commanding” per aggiungere comandi e pulsanti
- implementazione di funzionalità di navigazione tra dati (Next, Previous ecc.)
- implementazione di validazione dei dati attraverso l’interfaccia IDataErrorInfo
- ripetizione dei punti 1, 2 e 3 nei confronti di un modello basato su ADO.NET Entity Framework invece che su oggetti custom
- utilizzo del M-V-VM con relazioni master-details verso Entity Framework
Per cominciare, però, ci vuole una cosa molto semplice. Intanto la necessità è creare un progetto WPF con Visual Basic 2010. Una volta fatto questo, è tempo di parlare di dati.
Il Model
Il Model rappresenta essenzialmente i nostri dati. Può essere di vario tipo, ad esempio una classe che rappresenti un oggetto business, un modello a oggetti basato su Entity Framework o su LINQ to SQL, una classe POCO (plain-old-CLR-objects). Ipotizziamo di avere una classe Customer, che rappresenti un nostro cliente, e che sia definita in questo modo:
Public Class Customer
Public Property CompanyName As String
Public Property CustomerID As Integer
Public Property Address As String
Public Property Representative As String
End Class
Ipotizziamo poi che tale classe serva a rappresentare dei dati provenienti da un file XML, molto semplificato, come questo:
<?xml version="1.0" encoding="utf-8" ?>
<Customers>
<Customer CompanyName="Del Sole Spa" CustomerID="1" Address="Cremona" Representative="Alessandro Del Sole" />
<Customer CompanyName="RM Consulenza" CustomerID="2" Address="Varese" Representative="Renato Marzaro" />
<Customer CompanyName="Catucci Snc" CustomerID="3" Address="Milano" Representative="Antonio Catucci" />
</Customers>
Infine, implementiamo una collezione chiamata Customers che contenga tutti gli oggetti Customer caricati. Tale classe eredita da ObservableCollection(Of Customer) e definisce un metodo condiviso che si occupa di caricare i dati:
Imports System.Collections.ObjectModel
'Implementa di suo INotifyPropertyChanged
Public Class Customers
Inherits ObservableCollection(Of Customer)
Public Shared Function LoadCustomers() As Customers
Dim customerCollection As New Customers
Dim doc = XDocument.Load("Data\Customers.xml")
Dim query = From cust In doc...<Customer>
Select New Customer With {.Address = cust.@Address, .CompanyName = cust.@CompanyName, .CustomerID = CInt(cust.@CustomerID), .Representative = cust.@Representative}
For Each cust In query
customerCollection.Add(cust)
Next
Return customerCollection
End Function
End Class
L’importanza di utilizzare la ObservableCollection è che questa, come noto, implementa l’interfaccia INotifyPropertyChanged e quindi è in grado di inviare una notifica ogni qual volta il suo contenuto si modifica.
Il ViewModel
Nella maggior parte dei tutorial che ho letto, dopo il Model si parla della View. Non è un approccio che mi piace, quindi ora passo al ViewModel :-) Il suo compito è essenzialmente quello di eseguire operazioni sul Model e inviare notifiche relative alle operazioni eseguite. Il ViewModel invia notifiche, ma non sa chi le riceverà. Affinchè un ViewModel sia in grado di inviare modifiche, la classe che lo definisce deve implementare l’interfaccia INotifyPropertyChanged. Poiché in un progetto potremmo avere decine di ViewModel, una buona idea è quella di costruire una gerarchia di classi riutilizzabili.
Ad esempio, potrei avere un ViewModel che lavori sugli ordini di un’azienda e un’altro ViewModel che lavori sulle anagrafiche dei clienti, ma entrambi avranno delle caratteristiche in comune per cui si è soliti ricorrere all’implementazione di una classe base che poi può essere ereditata da altri ViewModel. Nella demo di questo post, che è molto essenziale, ci sarà un solo ViewModel ma l’approccio serve per capire alcuni passaggi. Definiamo quindi una classe base chiamata per convenzione ViewModelBase, tecnica adottata nella maggior parte dei casi:
Imports System.ComponentModel
Public Class ViewModelBase
Implements INotifyPropertyChanged
Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
Protected Sub OnPropertyChanged(ByVal strPropertyName As String)
If Me.PropertyChangedEvent IsNot Nothing Then
RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(strPropertyName))
End If
End Sub
End Class
L’implementazione di tale classe in questo modo è minimale, ma in seguito vedremo alcuni approfondimenti che ci consentiranno di validare i nomi delle proprietà. Ora ci occorre un ViewModel che esegua le operazioni sul nostro Model e che invii le notifiche alla/e View collegata/e. Tale classe si chiamerà CustomerViewModel ed eredita da ViewModelBase. Il codice è il seguente:
Public Class CustomerViewModel
Inherits ViewModelBase
Private _objCustomer As Customer
Private _customers As Customers
Dim _selectedCustomer As Customer
Public Property Selection() As Customer
Get
Return _selectedCustomer
End Get
Set(ByVal value As Customer)
If value Is _selectedCustomer Then
Return
End If
_selectedCustomer = value
MyBase.OnPropertyChanged("Selection")
End Set
End Property
Public Property Customers As Customers
Get
Return _customers
End Get
Set(ByVal value As Customers)
Me._customers = value
OnPropertyChanged("Customers")
End Set
End Property
Public Property Customer() As Customer
Get
Return _objCustomer
End Get
Set(ByVal Value As Customer)
_objCustomer = Value
MyBase.OnPropertyChanged("Customer")
End Set
End Property
Public Property Address() As String
Get
Return _objCustomer.Address
End Get
Set(ByVal Value As String)
_objCustomer.Address = Value
MyBase.OnPropertyChanged("Address")
End Set
End Property
Public Property CompanyName() As String
Get
Return _objCustomer.CompanyName
End Get
Set(ByVal Value As String)
_objCustomer.CompanyName = Value
MyBase.OnPropertyChanged("CompanyName")
End Set
End Property
Public Property CustomerID() As Int32
Get
Return _objCustomer.CustomerID
End Get
Set(ByVal Value As Int32)
_objCustomer.CustomerID = Value
MyBase.OnPropertyChanged("CustomerID")
End Set
End Property
Public Property Representative() As String
Get
Return _objCustomer.Representative
End Get
Set(ByVal Value As String)
_objCustomer.Representative = Value
MyBase.OnPropertyChanged("Representative")
End Set
End Property
Public Sub New()
Me._customers = Customers.LoadCustomers
End Sub
Public Sub New(ByVal customerCollection As Customers)
Me._customers = customerCollection
End Sub
End Class
Ci sono alcune cose da notare:
- la classe espone una proprietà chiamata Selection e che rappresenta il customer corrente, utile in fase di data-binding con la View
- la classe espone una proprietà Customers che redirige alla View il contenuto della collezione di customer
- la classe espone tante proprietà quante sono quelle del Model, inviando in modo esplicito una notifica invocando il metodo OnPropertyChanged della classe base
- il costruttore della classe si occupa di caricare i dati e popolare le proprietà, di modo che queste possano essere data-bound
Il concetto di fondo è questo: il ViewModel si occupa di caricare i dati e di esporli. In post successivi vedremo anche come si occupi di modificarli. Non è l’interfaccia che fa il lavoro, è il ViewModel. Che, come detto, non sa a chi invia le notifiche e quindi è pienamente svincolato dall’interfaccia stessa. Ora bisogna dare una destinazione a quanto esposto dal ViewModel, ossia una View.
La View
Con il termine View intendiamo lo strato di presentazione nell’ambito di M-V-VM e generalmente è rappresentato da un oggetto Window, quindi una finestra. Tale oggetto espone, come sapete, una proprietà DataContext. Grazie a tale proprietà, tutti i controlli presenti nell’interfaccia andranno a popolarsi leggendo i propri dati collegati partendo proprio dal DataContext. E poiché i dati sono esposti dal ViewModel, questo andrà a costituire il valore del DataContext stesso.
Se andiamo nello XAML della nostra Window principale, possiamo ipotizzare di voler visualizzare in una DataGrid i nostri dati (per questa volta uso la DataGrid solo per semplicità espositiva). Lo XAML è il seguente:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding Path=Customers}" SelectedItem="{Binding Path=Selection, Mode=TwoWay}"
Name="DataGrid1" >
</DataGrid>
</Grid>
</Window>
In sostanza, la proprietà ItemsSource è popolata col contenuto della proprietà Customers del DataContext della Window e lo stesso vale per la proprietà SelectedItem, che è popolata col valore della proprietà Selection del DataContext. Ma come abbiamo detto, il valore del DataContext è proprio il nostro ViewModel. Quindi il costruttore della Window (o se preferite farlo lato XAML, va bene lo stesso) si limiterà ad istanziare il ViewModel e ad assegnarlo al DataContext:
Private Sub MainWindow_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
Dim custViewModel As New CustomerViewModel()
Me.DataContext = custViewModel
End Sub
Come vedete questo è l’unico codice contenuto nella Window. Tutto il lavoro effettivo è fatto nel ViewModel, raggiungendo quindi lo scopo della separazione completa tra strati. Ora potete avviare l'applicazione per vedere i dati caricati all'interno della DataGrid. Quindi è successo che l’interfaccia è semplicemente in binding, ma chi svolge il lavoro vero e proprio è solamente il ViewModel.
Download del codice e fine della prima parte
In questa prima parte abbiamo messo un po’ di carne al fuoco e forse iniziato a capire alcuni concetti fondamentali. L’applicazione di esempio è ovviamente banale, ma nel prossimo post avremo modo di dedicarci all’implementazione di comandi che renderanno più completo il nostro lavoro. Nel frattempo potete scaricare il codice sorgente relativo all’esempio proposto, da questo indirizzo dell’area Download di VB T&T.
Alessandro