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:
-
created the Entity Data Model
-
discussed how this can constitute our Model, representing the data
-
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:
- when launched, the application defines some messages that will be sent to objects
- Views (typically they're not the only ones) subscribe the messaging service so that they will be notified when messages are sent
- ViewModels send the desired messages when commands are invoked
- 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:
- method: Register
- 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