Alessandro Del Sole's Blog

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

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: Creare una libreria riutilizzabile per il pattern Model-View-ViewModel

Nella serie di post dedicata al pattern Model-View-ViewModel in WPF 4 con Visual Basic 2010, abbiamo studiato due differenti scenari, osservando come in entrambi abbiamo utilizzato alcune classi dello stesso tipo. Per capirci, sia negli esempi su oggetti custom che in quello su Entity Framework abbiamo utilizzato le classi RelayCommand e ViewModelBase.

Questo ci può portare a fare la seguente considerazione: è conveniente creare un framework per MVVM riutilizzabile in tutti i nostri progetti. In questo post creeremo un framework che è il caso di definire “light”, nel senso che conterrà i componenti riutilizzabili più comuni, anche se MVVM a volte consente (e richiede) l’utilizzo di altre tipologie di oggetti.

 

Personalmente ho appena pubblicato un nuovo progetto su CodePlex, chiamato Neptune MVVM, che consiste in un framework MVVM “light” per WPF e scritto con VB 2010, che potete scaricare da qui e che potete prendere come esempio da seguire per il post odierno.

 

Creeremo dapprima la libreria di classi, quindi un template di progetto riutilizzabile.

 

Creazione della libreria

La prima operazione da fare è creare una nuova libreria di classi in Visual Basic 2010 e aggiungere i seguenti riferimenti agli assembly di WPF:

 

1.    WindowsBase.dll

2.    PresentationCore.dll

3.    PresentationFramework.dll

 

Fatto questo si può tranquillamente rimuovere il file Class1.vb. Le classi che faranno parte del nostro Framework sono le seguenti:

 

1.    ViewModelBase, la classe base per tutti i ViewModel che potrà essere ereditata in altri progetti per la definizione di ViewModel specifici

2.    Messenger, la classe che offre un servizio di scambio messaggi e che agisce da Message broker. Come detto a suo tempo, tale classe è ripresa dal framework Ocean di Karl Shifflett.

3.    RelayCommand, la classe che si occupa di smistare la logica di esecuzione dei comandi, anche nella sua versione generica

4.    ServiceLocator, la classe che consente di registrare e ottenere le istanze di classi di servizio nel momento in cui abbiamo la necessità di implementare un service layer

 

La descrizione dettagliata delle citate classi la potete trovare nei post della serie su MVVM. Quindi al progetto bisogna aggiungere i 4 file di codice di cui, per comodità, riporto l’implementazione di seguito.

 

Questa è la classe ViewModelBase:

 

Imports System.ComponentModel

 

Public MustInherit Class ViewModelBase

    Implements INotifyPropertyChanged

 

    Dim myServiceLocator As New ServiceLocator

 

    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

 

    Private privateThrowOnInvalidPropertyName As Boolean

    Protected Overridable Property ThrowOnInvalidPropertyName() As Boolean

        Get

            Return privateThrowOnInvalidPropertyName

        End Get

        Set(ByVal value As Boolean)

            privateThrowOnInvalidPropertyName = value

        End Set

    End Property

 

    <Conditional("DEBUG"), DebuggerStepThrough()> _

    Public Sub VerifyPropertyName(ByVal propertyName As String)

        ' Verify that the property name matches a real, 

        ' public, instance property on this object.

        If TypeDescriptor.GetProperties(Me)(propertyName) Is Nothing Then

            Dim msg As String = "Invalid property name: " & propertyName

 

            If Me.ThrowOnInvalidPropertyName Then

                Throw New Exception(msg)

            Else

                Debug.Fail(msg)

            End If

        End If

    End Sub

 

    Private privateDisplayName As String

    Public Overridable Property DisplayName() As String

        Get

            Return privateDisplayName

        End Get

        Protected Set(ByVal value As String)

            privateDisplayName = value

        End Set

    End Property

 

    Public Function ServiceLocator() As ServiceLocator

        Return Me.myServiceLocator

    End Function

 

    Public Function GetService(Of T)() As T

        Return myServiceLocator.GetService(Of T)()

    End Function

End Class

 

 

La classe Messenger:

 

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

 

#Region "Constructor"

 

    Public Sub New()

    End Sub

 

#End Region

 

#Region "Register"

 

    ''' <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

 

#End Region

 

#Region "NotifyColleagues"

 

    ''' <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

 

#End Region

 

#Region "MessageToActionsMap [nested class]"

 

    ''' <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

 

#End Region

 

#Region "WeakAction [nested 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

 

#End Region

 

#Region "Fields"

 

    ReadOnly _messageToActionsMap As New MessageToActionsMap()

 

#End Region

 

End Class

 

 

La classe RelayCommand/RelayCommand(Of T):

 

Imports System.Windows.Input

 

Public Class RelayCommand

    Implements ICommand

#Region "Fields"

 

    Private ReadOnly _execute As Action(Of Object)

    Private ReadOnly _canExecute As Predicate(Of Object)

 

#End Region ' Fields

 

#Region "Constructors"

 

    ''' <summary>

    ''' Creates a new command that can always execute.

    ''' </summary>

    ''' <param name="execute">The execution logic.</param>

    Public Sub New(ByVal execute As Action(Of Object))

        Me.New(execute, Nothing)

    End Sub

 

    ''' <summary>

    ''' Creates a new command.

    ''' </summary>

    ''' <param name="execute">The execution logic.</param>

    ''' <param name="canExecute">The execution status logic.</param>

    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

 

#End Region ' Constructors

 

#Region "ICommand Members"

 

    <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 Region ' ICommand Members

End Class

 

Public Class RelayCommand(Of T)

    Implements ICommand

 

    Private ReadOnly _execute As Action(Of T)

    Private ReadOnly _canExecute As Predicate(Of T)

 

    ''' <summary>

    ''' Creates a new command that can always execute.

    ''' </summary>

    ''' <param name="execute">The execution logic.</param>

    Public Sub New(ByVal execute As Action(Of T))

        Me.New(execute, Nothing)

    End Sub

 

    ''' <summary>

    ''' Creates a new command.

    ''' </summary>

    ''' <param name="execute">The execution logic.</param>

    ''' <param name="canExecute">The execution status logic.</param>

    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

 

La classe ServiceLocator:

 

Public Class ServiceLocator

    Implements IServiceProvider

 

    Private services As New Dictionary(Of Type, Object)()

 

    Public Function GetService(Of T)() As T

        Return CType(GetService(GetType(T)), T)

    End Function

 

    Public Function RegisterService(Of T)(ByVal service As T, ByVal overwriteIfExists As Boolean) As 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 T) As Boolean

        Return RegisterService(Of T)(service, True)

    End Function

 

    Public Function GetService(ByVal serviceType As Type) As Object Implements IServiceProvider.GetService

        SyncLock services

            If services.ContainsKey(serviceType) Then

                Return services(serviceType)

            End If

        End SyncLock

        Return Nothing

    End Function

End Class

 

A questo punto possiamo compilare il nostro progetto per avere una dll funzionante e che possiamo riutilizzare nei nostri progetti WPF senza dover tutte le volte riscrivere o copiare da altre parti il codice.

Per semplificarci la vita, però, possiamo anche creare un template di progetto riutilizzabile che ogni volta definisca gli elementi principali e referenzi automaticamente la nostra libreria.

 

Creazione di un template di progetto

Creiamo in primo luogo un nuovo progetto WPF con Visual Basic 2010. Per prima cosa aggiungiamo un riferimento alla libreria di classi appena creata. Se il progetto lo aggiungete alla soluzione contenente anche il progetto della libreria, fate attenzione ad aggiungere il riferimento alla dll e non al progetto, altrimenti il template punterà al progetto e non alla dll. Quindi creiamo le seguenti cartelle di progetto, che in realtà non riempiremo ma che saranno utili quando svilupperemo i progetti creati attraverso il template:

 

1.    Models

2.    ViewModels

3.    Services

4.    Views

 

Nella cartella Views spostiamo il file MainWindow.xaml, ricordando di andare in My Project e aggiornare la proprietà Startup URI. Per completare l’opera, nel file Application.xaml.vb definiamo l’istanza della classe Messenger:

 

    Shared ReadOnly _messenger As New Messenger()

 

    Friend Shared ReadOnly Property Msn As Messenger

        Get

            Return _messenger

        End Get

    End Property

 

    Private Sub Application_DispatcherUnhandledException(ByVal sender As System.Object, ByVal e As System.Windows.Threading.DispatcherUnhandledExceptionEventArgs)

        MessageBox.Show(e.Exception.Message)

        e.Handled = True

    End Sub

 

Questo codice richiederà una direttiva Imports che punta al namespace che avete scelto per la libreria, nel caso del mio progetto Neptune il namespace è DelSole.Neptune. Ora possiamo esportare il template. Selezioniamo File|Export template e nella prima dialog assicuriamoci di selezionare Project Template nella prima combo e il nome di quest’ultimo progetto nella seconda. Andando avanti, imposteremo le proprietà del template in questo modo:

 

 

Al termine il template di progetto sarà disponibili tra quelli installati con Visual Studio, come si vede dalla seguente figura:

 

 

Conclusioni

In questo post abbiamo visto come sia possibile creare una libreria riutilizzabile per il pattern MVVM in WPF, con VB 2010 sebbene, come detto, questo sia in forma molto “light”. Il codice è disponibile su CodePlex, nella pagina della mia libreria Neptune.

 

Alessandro

Print | posted on martedì 17 agosto 2010 14:23 | Filed Under [ Visual Basic Windows Presentation Foundation Visual Studio 2010 ]

Powered by:
Powered By Subtext Powered By ASP.NET