Alessandro Del Sole's Blog

{ A programming space about Microsoft® .NET® }
posts - 1909, comments - 2047, trackbacks - 352

My Links

News

Your host

This is me! Questo spazio è dedicato a Microsoft® .NET®, di cui sono molto appassionato :-)

Cookie e Privacy

Microsoft MVP

My MVP Profile

Microsoft Certified Professional

Microsoft Specialist

Xamarin Certified Mobile Developer

Il mio libro su VB 2015!

Pre-ordina il mio libro su VB 2015 Pre-ordina il mio libro "Visual Basic 2015 Unleashed". Clicca sulla copertina per informazioni!

Il mio libro su WPF 4.5.1!

Clicca sulla copertina per informazioni! E' uscito il mio libro "Programmare con WPF 4.5.1". Clicca sulla copertina per informazioni!

These postings are provided 'AS IS' for entertainment purposes only with absolutely no warranty expressed or implied and confer no rights.
If you're not an Italian user, please visit my English blog

Le vostre visite

I'm a VB!

Guarda la mia intervista a Seattle

Follow me on Twitter!

Altri spazi

GitHub
I miei progetti open-source su GitHub

Article Categories

Archives

Post Categories

Image Galleries

Privacy Policy

WPF: Introduzione al pattern Model-View-ViewModel per sviluppatori Visual Basic 2010 - settima parte

Riprendiamo la nostra avventura nello studio del pattern MVVM in WPF 4, con Visual Basic 2010, nei confronti di ADO.NET Entity Framework. La volta scorsa ci siamo salutati dopo aver implementato la classe Messenger e aver descritto a cosa serve, anche se è ancora presto per vederla in pratica. In questo post scriveremo un po' di codice, ancora legato all'accesso ai dati, che ci serve per capire quali complesse problematiche può portare l'utilizzo di MVVM ma anche quali benefici.

Problema del giorno: il ViewModel con chi parla?

Vi ho confessato, un paio di post fa, che ho dovuto rimandare l'inizio della serie di post su MVVM ed Entity Framework perché studiando mi sono accorto che c'era qualcosa che non andava nel mio approccio e stavo sbagliando delle cose. Noi siamo giunti ad un punto in cui abbiamo dei dati, uno strato di accesso ai dati, un modello a oggetti che ci permetta di interagire coi dati stessi. Teoricamente basterebbe scrivere un ViewModel, come avevamo fatto all'inizio, che sia in grado di eseguire le operazioni di nostro interesse attraverso appositi command.

E qui nasce il problema: pensando per logica di astrazione, se il ViewModel interagisse direttamente con l'Entity Data Model, avrebbe una dipendenza stretta dall'EDM stesso. Poiché una delle finalità del pattern MVVM è quella di favorire al massimo la separazione tra layer, questo approccio non può andar bene. Il ViewModel deve essere talmente "astratto", in linea generale, da poter essere in grado di dialogare con qualunque sorgente dati senza scossoni nel caso in cui la sorgente stessa venga modificata. Mi spiego meglio: se oggi lavoro con XML e domani decidessi di lavorare con SQL Server, dovrei avere un ViewModel così versatile da non cambiare a seconda dei miei dati e, soprattutto, in linea di massima questo consente anche alle View collegate di continuare a lavorare a dovere. E' fattibile tutto questo? Si, certo. Ci vuole un po' di lavoro però :-)

Ragioniamo per servizi

Lo scopo del gioco è: come faccio a creare un ViewModel che non sappia con quale origine dati abbia a che fare, ma che sia in grado di modificare o leggere l'origine dati stessa? Il problema si risolve aggiungendo uno strato di servizi che funziona in questo modo:

  1. Si definisce un'interfaccia che per convenzione prende il nome di IxxxDataService, dove XXX è il nome del dato da rappresentare (es. ICustomerDataService). Quest'interfaccia definisce i membri che andranno ad interagire direttamente con l'origine dati, quindi query, insert/update/delete, ecc.
  2. Si dichiara una classe, che per convenzione si chiama xxxDataService, dove XXX è il nome del dato da rappresentare (es. CustomerDataService), che implementa l'interfaccia di cui al punto precedente e che esegue il lavoro effettivo sui dati.
  3. Si scrive una classe chiamata ServiceLocator, che si occupa di passare agli oggetti chiamanti tutti gli strumenti per agire nei confronti della sorgente dati, separando il tutto.
  4. Si scrive il ViewModel, che invocherà i membri della classe xxxDataService, senza così sapere qual è l'origine dati sottostante.

Qual è il vantaggio del fare tutto questo "casino"? Beh, il vantaggio è questo: immaginate di avere un comando nel ViewModel che deve salvare i dati. Il ViewModel esporrà un comando chiamato Save, che invocherà analogo metodo nella classe di servizio. In questo modo il ViewModel non dipende strettamente dall'origine dati perchè non sa qual è la sorgente sottostante, al tempo stesso espone un comando di tipo "convenzionale" per così dire ed è in grado di raggiungere l'obiettivo proposto.

Ora che vi ho confusi a dovere :-), vediamo come implementare lo strato di servizi. Dal prossimo post cominceremo a parlare di ViewModel e tutto sarà definitivamente (spero) chiaro. Una precisazione: generalmente si aggiungono tante interfacce/classi di servizio per quanti sono i Model da prendere in considerazione. Vedremo anche questo in pratica.

Refactoring? Si certo, non ora però :-)

Probabilmente i più esperti, al termine di questo post, avranno da obiettare che si può riorganizzare il codice facendo refactoring e sfruttando ereditarietà. Ho però in mente di scrivere un post interamente dedicato al refactoring di tutto il codice che stiamo scrivendo, per cui preferisco per ora esporre tutte le cose in modo "ruspante" per poi riorganizzare e sistemare il tutto.

Implementare la classe ServiceLocator

Come detto, è necessario implementare una classe che per convenzione si chiama ServiceLocator e che offre la possibilità di "registrare" le interfacce dello strato di servizi e grazie alla quale sarà possibile ottenere le istanze delle classi di servizio, al fine di interagire con i dati. Si tratta di una classe che viene spiegata anche nella documentazione di Prism, alla quale vi rimando per i dettagli. In questa sede mi interessa esporre l'implementazione della classe e descrivere sommariamente i suoi metodi. Aggiungiamo quindi una nuova classe così chiamata all'interno della cartella di progetto chiamata Services (che avevamo creato in precedenza) e scriviamo il seguente codice:

Public Class ServiceLocator
    Implements IServiceProvider
 
    
Private services As New Dictionary(Of TypeObject)()
 
    
Public Function GetService(Of T)() As T
        Return CType(GetService(GetType(T)), T)
    
End Function
 
    
Public Function RegisterService(Of T)(ByVal service As TByVal overwriteIfExists As BooleanAs Boolean
        SyncLock services
            
If Not services.ContainsKey(GetType(T)) Then
                services.Add(GetType(T), service)
                
Return True
            ElseIf overwriteIfExists Then
                services(GetType(T)) = service
                
Return True
            End If
        End SyncLock
        Return False
    End Function
 
    
Public Function RegisterService(Of T)(ByVal service As TAs Boolean
        Return RegisterService(Of T)(service, True)
    
End Function
 
    
Public Function GetService(ByVal serviceType As TypeAs Object Implements IServiceProvider.GetService
        
SyncLock services
            
If services.ContainsKey(serviceType) Then
                Return services(serviceType)
            
End If
        End SyncLock
        Return Nothing
    End Function
End Class

Essenzialmente tale classe, che implementa l'interfaccia IServiceProvider, espone due metodi: RegisterService, che registra un tipo come "fornitore" di servizi, e GetService che ottiene l'istanza della classe di servizio che esegue l'effettivo accesso ai dati. Una volta che abbiamo questa classe di infrastruttura, possiamo passare a definire le interfacce. Appena prima, però, c'è un passaggio intermedio che ci serve per istanziare l'ObjectContext

Dichiarare l'ObjectContext

Dobbiamo trovare un luogo in cui istanziare l'ObjectContext che, nel nostro progetto, si chiama NorthwindEntities. Chiaramente questo non può essere fatto nel ViewModel, per salvaguardare l'astrazione. Teoricamente può essere fatto anche nello strato di servizi, ma vogliamo escluderlo anche da qui per cui possiamo creare una variabile raggiungibile all'interno del progetto, definendola in un modulo. Nella cartella di progetto chiamata Helpers, aggiungiamo un modulo chiamato Helper e scriviamo il seguente codice:

Module Helper
    Public Northwind As New NorthwindEntities
End Module

Ora siamo pronti per le interfacce di servizio.

Implementare interfacce IDataService

Se ricordate la figura in cui mostravo come si presenterà la nostra applicazione completa, ricorderete che sulla sinistra è presente un elenco di clienti che otterremo dalla tabella Customers del database. A livello di gestione dei clienti, non dobbiamo fare altro. Quindi è sufficiente definire un'interfaccia che a sua volta definisca un metodo che estragga l'elenco dei clienti. All'interno della cartella di progetto chiamata Services, aggiungiamo un'interfaccia chiamata ICustomerDataService il cui codice è il seguente:

Public Interface ICustomerDataService

    
Function GetAllCustomers() As IQueryable(Of Customer
)
End Interface

Come vedete l'interfaccia definisce un banale metodo chiamato GetAllCustomers che, una volta implementato, restituirà l'elenco completo di clienti sotto forma di IQueryable(Of Customer). Ora facciamo un paio di considerazioni: il metodo restituisce banalmente IQueryable invece che, ad esempio, un ObjectQuery(Of T) che è tipico di Entity Framework. Questo è opportuno perché non lega il risultato a una tecnologia sottostante. Inoltre, un domani potrei sostituire il tipo Customer che ora mi proviene da Entity Framework con un oggetto Customer personalizzato, che magari proviene da un'altra sorgente dati, ma in questo modo il ViewModel che riceve tale risultato non rimane influenzato in alcun modo.

Come detto poco fa, almeno in linea teorica per ogni Model ci dev'essere un'interfaccia di servizio. Noi lavoriamo su clienti e ordini, per cui dobbiamo definire un'interfaccia per quest'ultimo tipo di dati. Qui l'interfaccia si fa più interessante atteso che dobbiamo implementare:

  1. un metodo per ottenere l'elenco degli ordini. Ci saranno due overload, uno per ottenere l'elenco come IQueryable e uno per ottenere l'elenco degli ordini di uno specifico Customer, restituito come ObservableCollection dal momento che a noi interessa anche la modifica dei dati.
  2. un metodo per il salvataggio
  3. un metodo per l'inserimento di ordini
  4. un metodo per l'eliminazione di ordini
  5. metodi per lo spostamento tra ordini, quindi la navigazione

Ciò premesso, la nuova interfaccia si chiamerà IOrderDataService e la aggiungiamo ancora nella cartella Services. Il codice è il seguente:

Imports System.Collections.ObjectModel

Public Interface IOrderDataService
    Function GetOrderDetailsByOrderId(ByVal ID As IntegerAs ObservableCollection(Of Order_Detail)

    
Function GetAllOrders() As IQueryable(Of Order
)
    
Function GetAllOrders(ByVal customerID As StringAs ObservableCollection(Of Order
)
    
Sub
 Save()

    
Sub Delete(ByVal dataSource As Object
)
    
Sub Insert(ByVal dataSource As ObjectByVal selectedCustomer As Customer
)
    
Sub MoveToNext(ByVal dataSource As Object
)
    
Sub MoveToPrevious(ByVal dataSource As Object
)
End Interface

Alcuni dettagli li vedremo in fase di implementazione delle interfacce, al momento voglio evidenziare che il primo overload di GetAllOrders, che restituisce un IQueryable, è implementato a futura utilità anche se non utilizzato in quest'applicazione. Altri metodi ricevono, come argomento, la sorgente dati su cui lavorare. Questo è fatto attraverso parametri Object, che favoriscono la più completa genericità. Le conversioni appropriate saranno fatte tra breve nelle classi di servizio.

Implementare classi di servizio

Una volta che abbiamo definito le interfacce, ci servono anche delle classi che le implementino. In primo luogo, creiamo una classe CustomerDataService che implementi l'interfaccia correlata. Aggiungiamo tale classe alla cartella Services e scriviamo il seguente codice:

Public Class CustomerDataService
    Implements ICustomerDataService
      Public Function GetAllCustomers() As IQueryable(Of CustomerImplements ICustomerDataService.GetAllCustomers
        
Return Northwind.Customers.Include("Orders"
)
    
End Function
End Class

Forse l'idea di base può cominciare ad essere più chiara. Il metodo restituisce un normalissimo tipo .NET, IQueryable, sarà recepito dal ViewModel. Il corpo del metodo ora restituisce il risultato di una query svolta nei confronti dell'Entity Data Model, ma un domani potrebbe restituire analoga query svolta nei confronti di un file XML. Il ViewModel continuerà a ricevere un IQueryable senza sapere cos'è cambiato nella sorgente dati sottostante. Mentre questa classe è molto semplice, quella relativa all'interfaccia IOrderDataService è più complessa anche se l'idea di fondo è analoga. Ciò premesso, aggiungiamo nella medesima locazione una nuova classe chiamata OrderDataService il cui codice è il seguente:

Imports System.Data, System.Windows.Data
Imports System.Data.Objects
Imports System.ComponentModel
Imports System.Collections.ObjectModel
 
Public Class OrderDataService
    Implements IOrderDataService
 
    
Public Function GetOrderDetailsByOrderID(ByVal ID As IntegerAs ObservableCollection(Of Order_DetailImplements IOrderDataService.GetOrderDetailsByOrderId
        
Dim OrderDetailsQuery As System.Data.Objects.ObjectQuery(Of Order_Detail) = CType((From det In Northwind.Order_Details
                                                                                   
Where det.OrderID = ID
                                                                                   
Select det),
                                                                                   
Global.System.Data.Objects.ObjectQuery(Of Order_Detail))
 
        
Return New ObservableCollection(Of Order_Detail)(OrderDetailsQuery)
    
End Function
 
    
Public Sub Insert(ByVal dataSource As ObjectByVal customer As CustomerImplements IOrderDataService.Insert
        
Dim newOrder As Order
        Dim tp = dataSource.GetType
 
        
Select Case tp.Name
            
Case Is = "ListCollectionView"
                Dim source = CType(dataSource, ListCollectionView)
 
                newOrder = 
CType(source.AddNew, Order)
                newOrder.Customer = customer
 
                
'Aggiunge il nuovo ordine cosicchè il binding
                'faccia sì che l'ErrorTemplate venga richiamato
                source.CommitNew()
            
Case Is = "BindingListCollectionView"
                Dim source = CType(dataSource, BindingListCollectionView)
 
                newOrder = 
CType(source.AddNew, Order)
                newOrder.Customer = customer
 
                
'Aggiunge il nuovo ordine cosicchè il binding
                'faccia sì che l'ErrorTemplate venga richiamato
                source.CommitNew()
            
Case Else
                Throw New InvalidOperationException("Data source is of a type CollectionView which does not support adding items")
        
End Select
 
    
End Sub
 
    
Public Sub Save() Implements IOrderDataService.Save
        
Try
            Northwind.SaveChanges()
        
Catch ex As OptimisticConcurrencyException
            'Gestione concorrenza
            Northwind.Refresh(Objects.RefreshMode.ClientWins,
                                        Northwind.Orders)
            Northwind.SaveChanges()
        
Catch ex As Exception
            Throw
        End Try
    End Sub
 
    
Public Function GetAllOrders() As IQueryable(Of OrderImplements IOrderDataService.GetAllOrders
        
Return Northwind.Orders
    
End Function
 
    
Public Function GetAllOrders(ByVal customerID As StringAs ObservableCollection(Of OrderImplements IOrderDataService.GetAllOrders
        
Dim query = From ord In Northwind.Orders.Include("Customer")
                  
Where ord.CustomerID = customerID
                  
Select ord
 
        
Return New ObservableCollection(Of Order)(query)
    
End Function
 
    
Public Sub MoveToNext(ByVal dataSource As ObjectImplements IOrderDataService.MoveToNext
        
CType(dataSource, CollectionViewSource).View.MoveCurrentToNext()
    
End Sub
 
    
Public Sub MoveToPrevious(ByVal dataSource As ObjectImplements IOrderDataService.MoveToPrevious
        
CType(dataSource, CollectionViewSource).View.MoveCurrentToPrevious()
    
End Sub
 
    
Public Sub Delete(ByVal dataSource As ObjectImplements IOrderDataService.Delete
        
Dim tp = dataSource.GetType
 
        
Select Case tp.Name
            
Case Is = "ListCollectionView"
                Dim source = CType(dataSource, ListCollectionView)
                source.Remove(source.CurrentItem)
            
Case Is = "BindingListCollectionView"
                Dim source = CType(dataSource, BindingListCollectionView)
                source.Remove(source.CurrentItem)
            
Case Else
                Throw New InvalidOperationException("Data source is of a type that does not support removing items")
        
End Select
 
    
End Sub
End Class

Andiamo con ordine:

  1. Il metodo GetOrderDetailsByOrderId restituisce l'elenco degli Order Details per l'ordine richiesto. In teoria un metodo del genere andrebbe implementato in un servizio specifico per gli Order Details, ma siccome è l'unica operazione che svolgiamo verso tale model, lo mettiamo qui. Il risultato è di tipo ObservableCollection al fine di poter supportare nel modo appropriato il data-binding
  2. Il metodo Insert permette di aggiungere un nuovo ordine ai dati esistenti. Il primo argomento è l'origine dati a cui aggiungere il nuovo ordine. E' di tipo Object e, come vedete nel corpo del metodo, viene effettuata un analisi sul tipo di sorgente ed eseguita la conversione appropriata in ListCollectionView o BindingListCollectionView a seconda del chiamante. Questo perché comunque sappiamo che lavoreremo in modo specifico su oggetti di questo tipo, provenienti dal ViewModel. E' chiaro che il codice è estendibile e si potrebbe prevedere l'azione nei confronti di una ObservableCollection ma lo lascio a voi come esercizio. Lavorare sulle View è conveniente perché ci sono membri specifici per le operazioni sui dati. Il secondo argomento del metodo è solamente l'istanza del cliente a cui va associato il nuovo ordine.
  3. L'implementazione degli overload del metodo GetAllOrders è abbastanza semplice. In particolare, il secondo interroga l'elenco ordini appartenenti al cliente specificato e restituisce il risultato come ObservableCollection. E' questo l'overload che utilizzeremo nel ViewModel.
  4. Il metodo Save è banale. Salva i dati nel db, verifica scenari di concorrenza e solleva le eccezioni del caso.
  5. Il metodo Delete si comporta in modo analogo a Insert, solo che rimuove l'ordine specificato dall'origine specificata.
  6. I metodi MoveToNext e MoveToPrevious, che permettono di andare avanti e indietro nell'elenco degli ordini, assumono che l'origine dati sia "sfogliabile", ossia di tipo ICollectionView. Per tale ragione viene fatta la conversione diretta con invocazione dei metodi di spostamento.

Ripeto che faremo refactoring più avanti, anche perché per oggi direi che abbiamo scritto molto codice e imparato molti nuovi concetti.

Fine della puntata

Questa settima parte è stata molto intensa. Abbiamo:

  1. Descritto come il ViewModel dovrà essere "astratto" rispetto all'origine dati, agendo su di essa senza sapere di che tipo sia
  2. Descritto come questo sia possibile attraverso uno strato di servizi
  3. Implementato una classe chiamata ServiceLocator che si occuperà di registrare i servizi e ottenerne le istanze
  4. Implementato interfacce e classi di servizio che permetteranno di operare sui dati, restituendo il risultato al ViewModel, ignaro di come il tutto avvenga

Nella prossima parte inizieremo finalmente a scrivere i nostri ViewModel. Non manca molto al completamento del lavoro, anche se un po' di sforzo va ancora fatto. Ci aggiorniamo presto :-)

Alessandro

Print | posted on lunedì 2 agosto 2010 02:21 | Filed Under [ Visual Basic Windows Presentation Foundation Visual Studio 2010 ]

Powered by:
Powered By Subtext Powered By ASP.NET