Alessandro Del Sole's Blog

{ A programming space about Microsoft® .NET® }
posts - 1893, 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

Disabilita cookie ShinyStat

Microsoft MVP

My MVP Profile

Microsoft Certified Professional

Microsoft Specialist

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 - sesta parte

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:

  1. creato l'Entity Data Model
  2. discusso come questo costituisca il nostro Model, ossia la rappresentazione dei dati
  3. 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:

  1. al suo avvio, l'applicazione definisce alcuni messaggi che saranno inviati agli oggetti
  2. le View (in genere, ma non necessariamente solo loro) si "iscrivono" al servizio di messaggi cosicchè saranno informate quando i messaggi stessi saranno inviati
  3. i ViewModel, quando i comandi vengono eseguiti, inviano il messaggio desiderato
  4. 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:

  1. metodo Register
  2. 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

Print | posted on martedì 27 luglio 2010 04:01 | Filed Under [ Visual Basic Windows Presentation Foundation Visual Studio 2010 ]

Powered by:
Powered By Subtext Powered By ASP.NET