Alessandro Del Sole's Blog

/* A programming space about Microsoft® .NET® */
posts - 143, comments - 0, trackbacks - 0

My Links

News

Your host

This is me! This space is about Microsoft® .NET® and Microsoft® Visual Basic development. Enjoy! :-)

These postings are provided 'AS IS' for entertainment purposes only with absolutely no warranty expressed or implied and confer no rights.

Microsoft MVP

My MVP Profile

I'm a VB!

Watch my interview in Seattle

My new book on VB 2012

Order my book about VB 2012 on Amazon My book "Visual Basic 2012 Unleashed" is available. Click the cover!

My new book on LightSwitch!

Visual Studio LightSwitch Unleashed My book "Visual Studio LightSwitch Unleashed" is available. Click the cover!

Your visits

campusMVP.NET - Tutored online training for Microsoft developers

Follow me on Twitter!

Messenger me!


CyberInstaller Beta Tester

Download CIS 2008!!

CodePlex download Download my open-source projects from CodePlex!

Search the blog



Article Categories

Archives

Post Categories

.NET Framework

Blogroll

Help Authoring

Microsoft & MSDN

Setup & Deployment

Visual Basic 2005/2008/2010

WPF: Introducing the Model-View-ViewModel pattern for Visual Basic 2010 developers - part 6

ln my previous post I began describing how to use the MVVM pattern against a model based on the ADO.NET Entity Framework inside WPF apps built with Visual Basic 2010.

Quick recap

Last time we: 

  1. created the Entity Data Model
  2. discussed how this can constitute our Model, representing the data
  3. implemented data validation rules on the Model side taking advantage of IDataErrorInfo

In this new post you will see some more code strictly related to the MVVM pattern, although the most complex work has to come yet. By the way, you will also find a solution to an important problem that I will describe in the last part of the article.

Relaying the command logic

Some time ago we discussed the command logic in MVVM and how you relay the command logic via the RelayCommand class and its generic flavor. In the current scenario things remain absolutely unchanged; because of this, inside the Commands project subfolder let's add a new code file name RelayCommand.vb and paste the code for both implementations of the class, as follows:

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

A huge problem: launching new windows

If you remember the figure shown in my previous post which shows the full version of the app we're building, you can notice a button named View Details whose job is starting a new Window that will list all details for the order selected in the main window. In a non-MVVM context, there's nothing simpler: I can handle the button's click event, then I istantiate a new Window, I show it, I dispose it. Instead, in a MVVM scenario everything is more complex. I cannot handle any Click event because the button is bound to a command exposed by the ViewModel. Moreover, if I invoke such a command I cannot instantiate a View from within a ViewModel and show it from there because this is completely incorrect according to the pattern principles because a ViewModel cannot handle the UI and Views must store only code that manages the UI. And finally: how could I send back to the view the result of a second window via the ViewModel?

The solution is pretty interesting and is based on a messaging system that sends, intercepts and manages messages. For a better understanding, think of classic events: you click a button on the View, the bound command in the ViewModel raises an event. The View intercepts the event and at this point launches a secondary window. The same thing could happen when closing the secondary window. In the reality of things, this approach works ok but it's not the best approach. MVVM gurus use to implement a pattern known as Mediator that works like this:

  1. when launched, the application defines some messages that will be sent to objects
  2. Views (typically they're not the only ones) subscribe the messaging service so that they will be notified when messages are sent 
  3. ViewModels send the desired messages when commands are invoked 
  4. Views are notified when a message is sent and they take the appropriate action

Talking about our sample application, the main View subscribes the messaging service in order to know when the View Details button gets clicked. When this is clicked, the appropriate command in the ViewModel is invoked and will send the appropriate message. When the View receives a notification about the message, it creates an instance of the new dialog window via a delegate. Don't worry my friend, it's simpler than you can imagine but I'll show you this later.

The Messenger class

Although the root idea is the Mediator pattern, there are a lot of different implementations and differences, which is not uncommon in MVVM. I personally use the implementation offered by Josh Smith and Karl Shifflett, who are MVVM and WPF gurus. The messaging system is implemented through a class named Messenger which acts like a so-called message broker. Such a class provides a number of members but we are interested into just the most important two of them:

  1. method: Register
  2. method: NotifyToColleagues

The first method is invoked to subscribe the messaging system in order to receive the specified message, while the second one is invoked to send the message. I need to show you the complete code for the class but I won't discuss it completely. With regard to this you can find information on the blog of the original author, Karl Shifflett, who provides the class inside his MVVM framework known as Ocean. Here's the code:

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

Basically the Register method receives as an argument the message that will be sent and a delegate that will execute the specified action whereas NotifyToColleagues actually sends the message to all objects listening to the service. I will explain this in practice in next posts, so don't be afraid if something is not clear. 

The end of part 6

Today we implemented a Messenger to exchange messages between "colleague" objects and the RelayCommand class. We need to do a lot of work in order to create a well structured MVVM application and in next post I will begin discussing a service layer that will allow to a ViewModel to interact with data without knowing that the underlying provider is the Entity Framework. It's interesting, stay tuned! 

Alessandro

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

Feedback

No comments posted yet.

Post Comment

Title  
Name  
Email
Url
Comment   
Please add 8 and 8 and type the answer here:

Powered by:
Powered By Subtext Powered By ASP.NET