Dopo aver fatto una pausa più o meno lunga sulla trattazione del pattern Model-View-ViewModel con WPF 4 e Visual Basic 2010, riprendiamo il discorso al punto in cui vi avevo promesso di continuare, ossia parlare di questo pattern nell’ambito di un contesto sicuramente più interessante come quello di adottarlo nei confronti di ADO.NET Entity Framework. Prima di addentrarci nel codice, sono opportune alcune illustrazioni discorsive.
Dove vogliamo arrivare
Alla fine della serie di post arriveremo a realizzare questo:
Quindi una semplice applicazione master-details, che permetterà di lanciare anche una finestra. Banale, indubbiamente, se si ragiona per gestori di eventi e click invece che per command binding, data binding e ViewModel. Ci renderemo infatti conto di come sia complesso questo pattern ma anche del perché ha i suoi benefici. Parleremo di message broker, di service locators e altro. Non ho l'obiettivo nè tanto meno la pretesa di illustrare un'applicazione reale. L'obiettivo che mi pongo, non solo come "relatore" ma come "studioso" del pattern, se volete, è quello di capirne i concetti, capire come funziona, capire il livello di astrazione da raggiungere. E' una precisazione che tenevo a fare.
Non sono esente da errori
Considerato quanto affermato al paragrafo precedente, non sono esente da errori. Ribadisco di non essere un guru di MVVM, semplicemente condivido con chi ha voglia di seguirmi quello che è il percorso che sto personalmente facendo nello studio del pattern. Per tale ragione non tutto potrebbe essere perfetto e ben vengano le correzioni e i feedback di chi, invece, padroneggia la materia.
Quante possibilità, quante "non regole"...
Già in precedenza abbiamo detto che MVVM è un pattern, non una regola. E' un insieme di linee guida che ha una finalità ben precisa, certo, ma ci sono tante, tantissime varianti e questo può portare a confusione. La verità è che nel momento in cui stavo per iniziare la serie di post dedicata a MVVM con Entity Framework, ho capito che non era poi così tutto scontato e studi ulteriori mi hanno permesso di capire quanto invece fosse il vero lavoro da fare per arrivare a un certo livello di astrazione, in questo aiutato dalle indicazioni di Corrado, Andrea, Raf, Davide, Karl Shifflett. Banalmente basterebbe far interagire il ViewModel con il DAL; funziona, senz'altro, ma non è "pulito". Ho scovato esempi su Internet in cui si faceva in questo modo, oppure esempi molto più complessi e "astrattivi" ma poco calzanti, a mio avviso. Mi sono convinto della bontà delle indicazioni ricevute dai "colleghi" MVP e su questa falsariga sono andato avanti. Il tutto, ripeto, può contenere degli errori che prontamente saranno fixati. Ora basta ciance, mettiamoci all'opera.
Creazione del progetto
Dopo aver avviato Microsoft Visual Studio 2010, creiamo un nuovo progetto WPF 4 con Visual Basic 2010. In questo primo post ci sarà poco di MVVM, prepareremo infatti la base di quanto ci servirà nei prossimi post. All'interno di Solution Explorer creiamo le seguenti folders: Models (che conterrà i nostri dati), ViewModels (per tutti i ViewModel), Views (per tutte le View), Commands (che conterrà le logiche di implementazione dei comandi), Helpers (che conterrà classi di supporto) e Services (che conterrà interfacce e classi di servizio che vedremo successivamente).
L'Entity Data Model, ovvero il nostro Model
ADO.NET Entity Framework è una tecnologia complessa per l'accesso ai dati. Non ci addentriamo in discorsi di tipo "domani model" et similia, semplicemente utilizzeremo Entity Framework come strato di accesso ai dati nei confronti del database dimostrativo Northwind, basato su SQL Server, del quale mapperemo come oggetti .NET tre tabelle tipiche: Customers, Orders e Order Details. Ciò premesso, tasto destro sul nome del progetto -> Add|New Item -> scheda Data e template ADO.NET Entity Data Model. Assegnamo all'EDM il nome Northwind.edmx e procediamo tramite il wizard selezionando il db Northwind e lasciando inalterate le impostazioni di default, accertandoci di selezionare le tabelle richieste come in figura:
Al termine della procedura guidata il designer di Entity Framework mostrerà in Visual Studio 2010 la rappresentazione grafica del mapping ottenuto, visibile in figura:
A questo punto abbiamo delle classi che rappresentano i nostri dati e per tale ragione possono essere considerate il nostro Model.
Considerazioni sulle Entities come Model
Come detto, le entità possono essere considerate come il nostro Model. Di default le entità implementano l'interfaccia INotifyPropertyChanged e questa è una ragione in più per considerarle come Model. In realtà, nei primi post su MVVM tale interfaccia è stata implementata nel ViewModel al fine di inviare notifiche alle View. Non è di certo un male se essa è implementata anche nel Model, fornendo un punto di notifica eventuale anche verso oggetti esterni come può essere per l'appunto un ViewModel.
Implementare la validazione dei dati
In questo post non parlerò di ViewModel. Piuttosto passeremo ad un qualcosa che precedentemente abbiamo visto solo nell'ultimo post, ossia prevedere delle regole di validazione. Facendo riferimento alla figura iniziale dell'applicazione che vogliamo realizzare, notiamo come sia possibile gestire gli ordini di ciascun cliente. Ipotizziamo che un ordine, nuovo o esistente, possa essere accettato solo qualora venga immesso lo Stato di destinazione, che nel nostro codice è rappresentato dalla proprietà ShipCountry della classe Order. Se avete un po' di dimestichezza con Entity Framework, sapete che il runtime genera due metodi utili allo scopo, OnXXXChanging e OnXXXChanged dove XXX è il nome della proprietà da validare. Nel nostro caso, avremo OnShipCountryChanging e OnShipCountryChanged. A noi interessa in modo particolare quest'ultimo, che viene richiamato quando la proprietà subisce una modifica. Poiché è buona regola di programmazione quella di non toccare i file autogenerati da Visual Studio, possiamo sfruttare le caratteristiche delle classi parziali e dei metodi parziali. In parole povere, creiamo una classe parziale Order che estenda quella autogenerata. Detto ciò, nella cartella Models del nostro progetto aggiungiamo una nuova classe e chiamiamo il file di codice come Order.vb. In questa classe:
-
l'intestazione sarà di Partial Class
-
implementeremo l'interfaccia IDataErrorInfo per la validazione dei dati
-
implementeremo la logica di validazione
-
all'interno del metodo OnShipCountryChanged invocheremo l'aggiunta o la rimozione di un errore alla collezione degli errori di validazione, a seconda della validità del valore immesso.
Il codice della classe è il seguente, opportunamente commentato:
Imports System.ComponentModel
Partial Public Class Order
Implements IDataErrorInfo
'Collezione di errore/descrizione
Private m_validationErrors As New Dictionary(Of String, String)
'Aggiunge un errore alla collezione, se non già presente
'con la stessa chiave
Private Sub AddError(ByVal columnName As String, ByVal msg As String)
If Not m_validationErrors.ContainsKey(columnName) Then
m_validationErrors.Add(columnName, msg)
End If
End Sub
'Rimuove un errore dalla collezione, se presente
Private Sub RemoveError(ByVal columnName As String)
If m_validationErrors.ContainsKey(columnName) Then
m_validationErrors.Remove(columnName)
End If
End Sub
'Estende la classe con una proprietà che determina
'se l'istanza ha errori di validazione
Public ReadOnly Property HasErrors() As Boolean
Get
Return m_validationErrors.Count > 0
End Get
End Property
'Restituisce un messaggio di errore
'In questo caso è un messaggio generico, che viene
'restituito se l'elenco di errori contiene elementi
Public ReadOnly Property [Error] As String Implements System.ComponentModel.IDataErrorInfo.Error
Get
If m_validationErrors.Count > 0 Then
Return "Order data is invalid"
Else
Return Nothing
End If
End Get
End Property
Default Public ReadOnly Property Item(ByVal columnName As String) As String _
Implements System.ComponentModel.IDataErrorInfo.Item
Get
If m_validationErrors.ContainsKey(columnName) Then
Return m_validationErrors(columnName).ToString
Else
Return Nothing
End If
End Get
End Property
'Metodo parziale che esegue la validazione
Private Sub OnShipCountryChanged()
If String.IsNullOrEmpty(Me.ShipCountry) Then
Me.AddError("ShipCountry", "You need to specify the ship country")
Else
Me.RemoveError("ShipCountry")
End If
End Sub
Public Sub New()
'Gestendo quest'evento possiamo notificare alla UI
'che l'associazione con il Customer è cambiata
AddHandler Me.CustomerReference.AssociationChanged, AddressOf Customer_AssociationChanged
'Imposta un valore di default
Me.ShipCountry = String.Empty
'Aggiunge subito l'errore quando un nuovo ordine viene creato,
'di modo che l'utente sia obbligato a fixare il problema
Me.AddError("ShipCountry", "You need to specify the ship country")
End Sub
'Evento che viene generato nel caso in cui l'utente cambia
'l'associazione tra ordine e cliente
Private Sub Customer_AssociationChanged(ByVal sender As Object, _
ByVal e As CollectionChangeEventArgs)
If e.Action = CollectionChangeAction.Remove Then
OnPropertyChanging("Customer")
Else
If e.Action = CollectionChangeAction.Add Then
Me.RemoveError("Customer")
End If
OnPropertyChanged("Customer")
End If
End Sub
End Class
Di interessante rileviamo il gestore di evento Customer.AssociationChanged che ci permette di verificare che sia cambiata l'associazione tra ordine corrente e cliente.
Fine della prima parte
Per il momento ci fermiamo qui, potendo dire di aver completato l'aggiunta del Model. Nel prossimo post vedremo come implementare uno strato di servizi che consenta ai ViewModel di lavorare con i dati, pur non sapendo che sta interagendo con Entity Framework, al fine di favorire un adeguato livello di astrazione. Come sempre, vi invito a lasciare i vostri commenti/suggerimenti/critiche/segnalazioni di imprecisioni. Non rilascio codice da scaricare, lo farò al termine della serie di post con il download dell'applicazione completa, così vi costringo a seguirmi :-)
Alessandro