Nel post precedente abbiamo iniziato a trattare l'utilizzo del pattern MVVM nei confronti di ADO.NET Entity Framework come Model, all'interno di applicazioni WPF create con Visual Basic 2010.
Quick recap
La volta scorsa abbiamo:
-
creato l'Entity Data Model
-
discusso come questo costituisca il nostro Model, ossia la rappresentazione dei dati
-
implementato regole di validazione lato Model sfruttando IDataErrorInfo
In questo post scriviamo un po' più di codice relativo al pattern in questione, sebbene il grosso debba ancora venire ma impareremo a trovare la soluzione per un grosso problema che vi illustrerò di seguito.
La logica dei comandi
In un precedente post della serie abbiamo parlato di come ritrasmettere la logica di gestione dei command attraverso la classe RelayCommand e la sua variante generica RelayCommand(Of T). Nello scenario che stiamo trattando ora le cose non cambiano minimamente, per cui all'interno della cartella del progetto chiamata Commands, aggiungiamo un nuovo file di codice chiamato RelayCommand.vb e copia-incolliamo il codice relativo a entrambe le versioni della classe citata di cui, per comodità, riporto il codice completo:
Public Class RelayCommand
Implements ICommand
Private ReadOnly _execute As Action(Of Object)
Private ReadOnly _canExecute As Predicate(Of Object)
Public Sub New(ByVal execute As Action(Of Object))
Me.New(execute, Nothing)
End Sub
Public Sub New(ByVal execute As Action(Of Object), ByVal canExecute As Predicate(Of Object))
If execute Is Nothing Then
Throw New ArgumentNullException("execute")
End If
_execute = execute
_canExecute = canExecute
End Sub
<DebuggerStepThrough()> _
Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
Return If(_canExecute Is Nothing, True, _canExecute(parameter))
End Function
Public Custom Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
AddHandler(ByVal value As EventHandler)
AddHandler CommandManager.RequerySuggested, value
End AddHandler
RemoveHandler(ByVal value As EventHandler)
RemoveHandler CommandManager.RequerySuggested, value
End RemoveHandler
RaiseEvent(ByVal sender As System.Object, ByVal e As System.EventArgs)
End RaiseEvent
End Event
Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute
_execute(parameter)
End Sub
End Class
Public Class RelayCommand(Of T)
Implements ICommand
Private ReadOnly _execute As Action(Of T)
Private ReadOnly _canExecute As Predicate(Of T)
Public Sub New(ByVal execute As Action(Of T))
Me.New(execute, Nothing)
End Sub
Public Sub New(ByVal execute As Action(Of T), ByVal canExecute As Predicate(Of T))
If execute Is Nothing Then
Throw New ArgumentNullException("execute")
End If
_execute = execute
_canExecute = canExecute
End Sub
<DebuggerStepThrough()> _
Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
Return If(_canExecute Is Nothing, True, _canExecute(CType(parameter, T)))
End Function
Public Custom Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
AddHandler(ByVal value As EventHandler)
AddHandler CommandManager.RequerySuggested, value
End AddHandler
RemoveHandler(ByVal value As EventHandler)
RemoveHandler CommandManager.RequerySuggested, value
End RemoveHandler
RaiseEvent(ByVal sender As System.Object, ByVal e As System.EventArgs)
End RaiseEvent
End Event
Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute
_execute(CType(parameter, T))
End Sub
End Class
Un grosso problema: aprire finestre
Se vi ricordate la figura nel post precedente che illustra l'applicazione completa che stiamo creando, notate la presenza di un pulsante chiamato View Details il cui scopo è lanciare una nuova Window all'interno della quale visualizzare tutti i dettagli dell'ordine selezionato nella finestra principale. In un contesto non-MVVM, niente di più semplice: gestisco l'evento Click del pulsante, istanzio la Window, la mostro, la distruggo. In un contesto MVVM, invece, è tutto molto più complicato. Non posso gestire l'evento Click di un pulsante, perchè il pulsante è collegato a un comando esposto dal ViewModel. Al tempo stesso, quando si invoca tale comando, non posso istanziare una View da un ViewModel e mostrarla da lì perché questo è totalmente contrario ai principi di MVVM, secondo cui nel ViewModel non si gestisce la UI e secondo cui nelle View ci deve essere solo codice che gestisca la UI. Non solo: come potrei rimandare alla View il risultato di una dialog attraverso un ViewModel?
La soluzione si realizza attraverso un principio basato su un sistema di scambio di messaggi che vengono inviati, intercettati e gestiti. Per capire meglio il discorso, proviamo a pensare agli eventi classici: clicco un pulsante sulla View, a questo punto un comando nel ViewModel scatena un evento. La View intercetta il verificarsi dell'evento e a questo punto istanzia e mostra una dialog secondaria. Stessa cosa potrebbe avvenire al chiudersi della dialog secondaria. In realtà, sebbene funzionante e semplicistico, l'approccio basato su eventi funziona ma non è il top. I maggiori esponenti e conoscitori di MVVM implementano un pattern chiamato Mediator che funziona in questo modo:
- al suo avvio, l'applicazione definisce alcuni messaggi che saranno inviati agli oggetti
- le View (in genere, ma non necessariamente solo loro) si "iscrivono" al servizio di messaggi cosicchè saranno informate quando i messaggi stessi saranno inviati
- i ViewModel, quando i comandi vengono eseguiti, inviano il messaggio desiderato
- le View vengono notificate dell'invio del messaggio e agiscono di conseguenza
Con un esempio "empirico" riferito alla nostra applicazione, la View principale si iscrive al servizio di messaggi per sapere quando è stato cliccato il pulsante. Quando viene fatto clic, viene invocato l'apposito comando nel ViewModel il cui compito sarà quello di inviare il messaggio. Quando la View riceve la notifica del messaggio inviato, mostra la dialog secondaria attraverso un delegate che viene invocato in tale momento. E' più difficile a dirsi che a farsi, come vedremo parlando attraverso il codice.
La classe Messenger
Sebbene l'idea di fondo sia quella del pattern Mediator, anche in questo caso ci sono tante implementazioni e differenze. Personalmente mi rifaccio a quelle fornite da Josh Smith e Karl Shifflett, due veri guru di MVVM e WPF. Il sistema di messaggistica viene implementato attraverso la definizione di una classe chiamata Messenger e che agisce come un c.d. message broker. Tra i vari membri e le varie sotto-classi che questa espone, due sono quelli fondamentali:
- metodo Register
- metodo NotifyToColleagues
Il primo si invoca per iscriversi al servizio di ricezione di un certo messaggio, il secondo si usa per inviare il messaggio stesso. Per necessità espositiva riporto il codice dell'intera classe, ma non la descriverò per intero. A tal fine vi segnalo il blog di Karl Shifflett che ha proposto tale classe nel suo framework chiamato Ocean. Ecco il codice, di cui faremo qualche commento dopo:
Imports System.Reflection
''' <summary>
''' Provides loosely-coupled messaging between
''' various colleague objects. All references to objects
''' are stored weakly, to prevent memory leaks.
''' </summary>
Public Class Messenger
Public Sub New()
End Sub
''' <summary>
''' Registers a callback method to be invoked when a specific message is broadcasted.
''' </summary>
''' <param name="message">The message to register for.</param>
''' <param name="callback">The callback to be called when this message is broadcasted.</param>
Public Sub Register(ByVal message As String, ByVal callback As [Delegate])
If String.IsNullOrEmpty(message) Then
Throw New ArgumentException("'message' cannot be null or empty.")
End If
If callback Is Nothing Then
Throw New ArgumentNullException("callback")
End If
Dim parameters As ParameterInfo() = callback.Method.GetParameters()
If parameters IsNot Nothing AndAlso parameters.Length > 1 Then
Throw New InvalidOperationException("The registered delegate can have no more than one parameter.")
End If
Dim parameterType As Type = If((parameters Is Nothing OrElse parameters.Length = 0), Nothing, parameters(0).ParameterType)
_messageToActionsMap.AddAction(message, callback.Target, callback.Method, parameterType)
End Sub
''' <summary>
''' Notifies all registered parties that a message is being broadcasted.
''' </summary>
''' <param name="message">The message to broadcast.</param>
Public Sub NotifyColleagues(ByVal message As String)
If String.IsNullOrEmpty(message) Then
Throw New ArgumentException("'message' cannot be null or empty.")
End If
Dim actions = _messageToActionsMap.GetActions(message)
If actions IsNot Nothing Then
actions.ForEach(Function(action) action.DynamicInvoke())
End If
End Sub
''' <summary>
''' Notifies all registered parties that a message is being broadcasted.
''' </summary>
''' <param name="message">The message to broadcast</param>
''' <param name="parameter">The parameter to pass together with the message</param>
Public Sub NotifyColleagues(ByVal message As String, ByVal parameter As Object)
If String.IsNullOrEmpty(message) Then
Throw New ArgumentException("'message' cannot be null or empty.")
End If
Dim actions = _messageToActionsMap.GetActions(message)
If actions IsNot Nothing Then
actions.ForEach(Function(action) action.DynamicInvoke(parameter))
End If
End Sub
''' <summary>
''' This class is an implementation detail of the Messenger class.
''' </summary>
Private Class MessageToActionsMap
' Stores a hash where the key is the message and the value is the list of callbacks to invoke.
ReadOnly _map As New Dictionary(Of String, List(Of WeakAction))()
Friend Sub New()
End Sub
''' <summary>
''' Adds an action to the list.
''' </summary>
''' <param name="message">The message to register.</param>
''' <param name="target">The target object to invoke, or null.</param>
''' <param name="method">The method to invoke.</param>
''' <param name="actionType">The type of the Action delegate.</param>
Friend Sub AddAction(ByVal message As String, ByVal target As Object, ByVal method As MethodInfo, ByVal actionType As Type)
If message Is Nothing Then
Throw New ArgumentNullException("message")
End If
If method Is Nothing Then
Throw New ArgumentNullException("method")
End If
SyncLock _map
If Not _map.ContainsKey(message) Then
_map(message) = New List(Of WeakAction)()
End If
_map(message).Add(New WeakAction(target, method, actionType))
End SyncLock
End Sub
''' <summary>
''' Gets the list of actions to be invoked for the specified message
''' </summary>
''' <param name="message">The message to get the actions for</param>
''' <returns>Returns a list of actions that are registered to the specified message</returns>
Friend Function GetActions(ByVal message As String) As List(Of [Delegate])
If message Is Nothing Then
Throw New ArgumentNullException("message")
End If
Dim actions As List(Of [Delegate])
SyncLock _map
If Not _map.ContainsKey(message) Then
Return Nothing
End If
Dim weakActions As List(Of WeakAction) = _map(message)
actions = New List(Of [Delegate])(weakActions.Count)
For i As Integer = weakActions.Count - 1 To -1 + 1 Step -1
Dim weakAction As WeakAction = weakActions(i)
If weakAction Is Nothing Then
Continue For
End If
Dim action As [Delegate] = weakAction.CreateAction()
If action IsNot Nothing Then
actions.Add(action)
Else
' The target object is dead, so get rid of the weak action.
weakActions.Remove(weakAction)
End If
Next
' Delete the list from the map if it is now empty.
If weakActions.Count = 0 Then
_map.Remove(message)
End If
End SyncLock
Return actions
End Function
End Class
''' <summary>
''' This class is an implementation detail of the MessageToActionsMap class.
''' </summary>
Private Class WeakAction
ReadOnly _delegateType As Type
ReadOnly _method As MethodInfo
ReadOnly _targetRef As WeakReference
''' <summary>
''' Constructs a WeakAction.
''' </summary>
''' <param name="target">The object on which the target method is invoked, or null if the method is static.</param>
''' <param name="method">The MethodInfo used to create the Action.</param>
''' <param name="parameterType">The type of parameter to be passed to the action. Pass null if there is no parameter.</param>
Friend Sub New(ByVal target As Object, ByVal method As MethodInfo, ByVal parameterType As Type)
If target Is Nothing Then
_targetRef = Nothing
Else
_targetRef = New WeakReference(target)
End If
_method = method
If parameterType Is Nothing Then
_delegateType = GetType(Action)
Else
_delegateType = GetType(Action(Of )).MakeGenericType(parameterType)
End If
End Sub
''' <summary>
''' Creates a "throw away" delegate to invoke the method on the target, or null if the target object is dead.
''' </summary>
Friend Function CreateAction() As [Delegate]
' Rehydrate into a real Action object, so that the method can be invoked.
If _targetRef Is Nothing Then
Return [Delegate].CreateDelegate(_delegateType, _method)
Else
Try
Dim target As Object = _targetRef.Target
If target IsNot Nothing Then
Return [Delegate].CreateDelegate(_delegateType, target, _method)
End If
Catch
End Try
End If
Return Nothing
End Function
End Class
ReadOnly _messageToActionsMap As New MessageToActionsMap()
End Class
Essenzialmente il metodo Register riceve il messaggio del quale gli oggetti "iscritti" riceveranno le notifiche e un delegate il cui compito è quello di eseguire l'azione desiderata al verificarsi dell'invio del messaggio. Il metodo NotifyToColleagues, invece, invia il messaggio a tutti gli oggetti in ascolto. Lo vedremo comunque in pratica nei post successivi, per cui non temete se al momento il discorso non vi sembra chiaro.
Fine della sesta parte
Per ora ci fermiamo qui. Abbiamo implementato una classe Messenger per lo scambio di messaggi tra oggetti "colleghi" e la classe RelayCommand. Ci manca ancora un bel po', per realizzare quella che è un'applicazione M-V-VM abbastanza ben strutturata e nel prossimo post inizieremo a parlare di uno strato di servizi che permetterà al ViewModel di dialogare con i dati, senza sapere che al di sotto c'è Entity Framework. Cose decisamente interessanti.
Alessandro