MVVM Implementation Discussion

MattP

Well-known member
Joined
Feb 29, 2008
Messages
1,206
Location
WY, USA
Programming Experience
5-10
Thought I'd share the MVVM implementation that I've been using lately. I didn't want to use a pre-rolled framework so I could learn how things fit together.

Hopefully this will help those that are looking to learn about implementing MVVM in your projects.

Using MEF won't be included in the original posting and I'm not using Prism because it's not compatible with Reactive Extensions (RX).

If anybody has any additional information to add or suggestions for improvement please do so as this is meant to be a discussion furthering the forums understanding of the pattern.
 
The first class to discuss is the base class that all of your ViewModels will inherit from. The main purpose of this class is implementing the INotifyPropertyChanged Interface.

VB.NET:
Imports System.ComponentModel
Imports System.ServiceModel.DomainServices.Client
Imports System.Reflection
Imports System.Linq.Expressions

Public MustInherit Class ViewModelBase
    Implements INotifyPropertyChanged

    Public Event PropertyChanged(ByVal sender As Object, ByVal e As PropertyChangedEventArgs) _
        Implements INotifyPropertyChanged.PropertyChanged

    Sub RaisePropertyChanged(ByVal propertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub

    Protected Sub RaisePropertyChanged(Of T)(ByVal propertyExpresssion As Expression(Of Func(Of T)))
        Dim propertyName = GetPropertyName(propertyExpresssion)
        RaisePropertyChanged(propertyName)
    End Sub

    Private Function GetPropertyName(Of T)(ByVal propExpresssion As Expression(Of Func(Of T))) As String
        If propExpresssion Is Nothing Then
            Throw New ArgumentNullException("propExpression")
        End If

        Dim mbrExpression = TryCast(propExpresssion.Body, MemberExpression)
        If mbrExpression Is Nothing Then
            Throw New ArgumentException("The expression is not a member access expression.", "propExpression")
        End If

        Dim prop = TryCast(mbrExpression.Member, PropertyInfo)
        If prop Is Nothing Then
            Throw New ArgumentException("The member access expression does not access a property.", "propExpression")
        End If

        If Not prop.DeclaringType.IsAssignableFrom(Me.GetType()) Then
            Throw New ArgumentException("The referenced property belongs to a different type.", "propExpression")
        End If

        Dim getMethod = prop.GetGetMethod(True)
        If getMethod Is Nothing Then
            Throw New ArgumentException("The referenced property does not have a get method.", "propExpression")
        End If

        If getMethod.IsStatic Then
            Throw New ArgumentException("The referenced property is a static property.", "propExpression")
        End If

        Return mbrExpression.Member.Name
    End Function

End Class

Most examples you see for a ViewModelBase class have a RaisePropertyChanged Sub accepting a string property name. I tend to make mistakes typing out property names so I wanted an overload that would allow me to use Intellisense, hence the Expression(Of Func(Of T)) overload.

For an example suppose my ViewModel has a property FirstName that I would like to notify clients that it's been updated.

With the original RaisePropertyChanged Sub:

VB.NET:
        Private _firstName As String
        Public Property FirstName As String
            Get
                Return _firstName
            End Get
            Set(ByVal value As String)
                _firstName = value
                RaisePropertyChanged("FirstName")
            End Set
        End Property

With the overloaded Sub:

VB.NET:
        Private _firstName As String
        Public Property FirstName As String
            Get
                Return _firstName
            End Get
            Set(ByVal value As String)
                _firstName = value
                RaisePropertyChanged(Function() Me.FirstName)
            End Set
        End Property
 
The second topic to cover when talking about MVVM is Commanding.

Here's the implementation I've been using. The original implementation by John Papa can be found here: 5 Simple Steps to Commanding in Silverlight : JohnPapa.net. I've modified it slightly to overload newing up a DelegateCommand so I don't need to provide CanExecute if I know it will always be true.

The MSDN page on the ICommand Interface can be found here.

VB.NET:
Public Class DelegateCommand
    Implements ICommand

    Private _executeAction As Action(Of Object)
    Private _canExecuteCache As Boolean
    Private _canExecute As Func(Of Object, Boolean)

    ''' <summary>
    ''' Initializes a new instance of the <see cref="DelegateCommand"/> class.
    ''' </summary>
    ''' <param name="executeAction">The execute action.</param>
    ''' <remarks></remarks>
    Public Sub New(ByVal executeAction As Action(Of Object))

        Initialize(executeAction,
                   Function()
                       Return True
                   End Function)

    End Sub

    ''' <summary>
    ''' Initializes a new instance of the <see cref="DelegateCommand"/> class.
    ''' </summary>
    ''' <param name="executeAction">The execute action.</param>
    ''' <param name="canExecute">Whether it can execute.</param>
    Public Sub New(ByVal executeAction As Action(Of Object), ByVal canExecute As Func(Of Object, Boolean))

        Initialize(executeAction, canExecute)

    End Sub

    Public Sub Initialize(ByVal executeAction As Action(Of Object), ByVal canExecute As Func(Of Object, Boolean))

        _executeAction = executeAction
        _canExecute = canExecute

    End Sub

    ''' <summary>
    ''' Determines whether the command can execute in its current state.
    ''' </summary>
    ''' <param name="parameter">Data used by the command.</param>
    ''' <returns>True if the command can be executed; False otherwise.</returns>
    Public Function CanExecute(ByVal parameter As Object) As Boolean _
        Implements ICommand.CanExecute

        Dim temp = _canExecute(parameter)
        If _canExecuteCache <> temp Then
            _canExecuteCache = temp
            RaiseEvent CanExecuteChanged(Me, New EventArgs)
        End If
        Return _canExecuteCache

    End Function

    Public Sub RaiseCanExecuteChanged()
        OnCanExecuteChanged(EventArgs.Empty)
    End Sub

    Protected Overridable Sub OnCanExecuteChanged(ByVal e As EventArgs)
        RaiseEvent CanExecuteChanged(Me, e)
    End Sub

    ''' <summary>
    ''' Occurs when changes happen that affects whether the command should execute.
    ''' </summary>
    Public Event CanExecuteChanged(ByVal sender As Object, ByVal e As System.EventArgs) _
        Implements ICommand.CanExecuteChanged

    ''' <summary>
    ''' Defines method to call when the command is invoked.
    ''' </summary>
    ''' <param name="parameter">Data used by the command.</param>
    Public Sub Execute(ByVal parameter As Object) _
        Implements ICommand.Execute

        _executeAction(parameter)

    End Sub
End Class

An example of using DelegateCommand:

VB.NET:
        Private _savePersonCommand As DelegateCommand
        Public ReadOnly Property SavePersonCommand As DelegateCommand
            Get
                If _savePersonCommand Is Nothing Then
                    _savePersonCommand = New DelegateCommand(AddressOf OnSavePersonCommandExecute, AddressOf CanSavePerson)
                End If
                Return _savePersonCommand
            End Get
        End Property

        Private Sub OnSavePersonCommandExecute()
            SavePerson()
        End Sub

        Private Function CanSavePerson() As Boolean
            Return Not SelectedPerson.HasValidationErrors
        End Function

Of course you can clean this up a bit:

VB.NET:
        Private _savePersonCommand As DelegateCommand
        Public ReadOnly Property SavePersonCommand As DelegateCommand
            Get
                If _savePersonCommand Is Nothing Then
                    _savePersonCommand = _
                        New DelegateCommand(Sub()
                                                SavePerson()
                                            End Sub,
                                            Function()
                                                Return Not SelectedProcess.HasValidationErrors
                                            End Function)
                End If
                Return _savePersonCommand
            End Get
        End Property
 
Last edited:
To communicate between ViewModels an implementation of the Mediator Pattern is used. The implementation I use takes advantage of Reactive Extensions for .NET (Rx) for push-based observable collections.

The original implementation I based mine off of: Reactive Extensions (Rx) and MVVM

IMessenger.vb:

VB.NET:
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text

Namespace Messaging
    Public Interface IMessenger
        Inherits IObservable(Of IMessage)

        Sub Send(ByVal message As IMessage)

    End Interface
End Namespace

IMessage.vb:

VB.NET:
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
Imports System

Namespace Messaging
    Public Interface IMessage
    End Interface
End Namespace

Messenger.vb:

VB.NET:
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
Imports System

Namespace Messaging
    Public Class Messenger
        Implements IMessenger

        Private ReadOnly _subject As New Subject(Of IMessage)()

        Public Function Subscribe(ByVal observer As IObserver(Of IMessage)) As IDisposable _
            Implements IMessenger.Subscribe

            Return _subject.Subscribe(observer)

        End Function

        Public Sub Send(ByVal message As IMessage) _
            Implements IMessenger.Send

            _subject.OnNext(message)

        End Sub

        Private Shared ReadOnly _default As New Messenger()
        Public Shared ReadOnly Property [Default]() As Messenger
            Get
                Return _default
            End Get
        End Property
    End Class
End Namespace

ICollectionExtensions:

VB.NET:
Imports System.Linq
Imports System.Text
Imports System.Runtime.CompilerServices

Namespace Messaging
    Public Module ICollectionExtensions
        Sub New()
        End Sub

        <Extension()> _
        Public Function Remove(Of T)(ByVal collection As ICollection(Of T), ByVal predicate As Func(Of T, Boolean)) As Integer
            Dim removals = collection.Where(predicate).ToList()
            removals.ForEach(Function(r) collection.Remove(r))
            Return removals.Count
        End Function

    End Module
End Namespace

IObservableExtensions.vb:

VB.NET:
Imports System.Runtime.CompilerServices
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text

Namespace Messaging
    Public Module IObservableExtensions
        Sub New()
        End Sub

        ' Filter an IObservable for the specified type or derivatives
        <Extension()> _
        Public Function OfType(Of TResult)(ByVal source As IObservable(Of IMessage)) As IObservable(Of TResult)
            Return OfType(Of TResult)(source, Function(o) TypeOf o Is TResult)
        End Function

        ' Filter an IObservable for the specified type not including derivatives
        <Extension()> _
        Public Function OfTypeExact(Of TResult)(ByVal source As IObservable(Of IMessage)) As IObservable(Of TResult)
            Return OfType(Of TResult)(source, Function(o) o.[GetType]() Is GetType(TResult))
        End Function

        <Extension()> _
        Private Function OfType(Of TResult)(ByVal source As IObservable(Of IMessage), ByVal typeCheck As Func(Of IMessage, Boolean)) As IObservable(Of TResult)
            Dim subj = New Subject(Of TResult)()
            source.Subscribe(Sub(o)
                                 If typeCheck(o) Then
                                     subj.OnNext(DirectCast(o, TResult))
                                 End If
                             End Sub,
                             Sub(ex)
                                 subj.OnError(ex)
                             End Sub,
                             Sub()
                                 subj.OnCompleted()
                             End Sub)
            Return subj.AsObservable
        End Function
    End Module
End Namespace

Here's an example from the project I'm currently working on:

SelectedDepartmentMessage.vb:

VB.NET:
    Public Class SelectedDepartmentMessage
        Implements IMessage

        Public Property deptID As Integer
        Public Property deptName As String

    End Class

Sending the message from my DepartmentViewModel:

VB.NET:
        Public Sub SendSelectedDepartmentChanged()
            If SelectedDepartment Is Nothing Then
                Exit Sub
            End If
            Messenger.Default.Send(New SelectedDepartmentMessage() With
                    {
                        .deptID = SelectedDepartment.ID,
                        .deptName = SelectedDepartment.Name
                    })
        End Sub

Subscribing to the message in my FunctionViewModel:

VB.NET:
        Public Sub SubscribeSelectedDepartmentChanged()
            Messenger.[Default].OfType(Of SelectedDepartmentMessage) _
                .Subscribe(Sub(param As SelectedDepartmentMessage)
                               SelectedDepartmentID = param.deptID
                               SelectedDepartment = param.deptName
                               LoadFunctions(param.deptID)
                           End Sub)
        End Sub
 
Last edited:
Since it won't let me edit the post above I'll make the example here.

For the purpose of the example I'll be using WCF RIA services. I'll also have a Department entity with a related Department_Contact entity.

Department:

VB.NET:
        Public Property ID As Integer

        <Display(Name:="Business Unit")>
        Public Property BusinessUnit As String

        <Display(Name:="Department Name")>
        Public Property Name As String

        <Display(Name:="Location")>
        Public Property Location As String

        <Include()>
        Public Property Department_Contact As EntityCollection(Of Department_Contact)

Service call to get departments:

VB.NET:
    Public Function GetDepartments() As IQueryable(Of Department)
        Return Me.ObjectContext.Departments
    End Function

Department_Contact:

VB.NET:
        Public Property DeptID As Integer

        <Key()>
        Public Property ID As Integer

        <Required()>
        Public Property Contact As String
        
        <Required()>
        Public Property Email As String

        <Required()>
        Public Property Extension As String
        
        <Required()>
        Public Property Phone As String

        Public Property Department As Department

Service call to get related Department_Contacts:

VB.NET:
    Public Function GetDepartmentContactByID(ByVal id As Integer) As IQueryable(Of Department_Contact)
        Return Me.ObjectContext.Department_Contact.Where(Function(dc) dc.DeptID = id)
    End Function
 
DepartmentViewModel:

VB.NET:
Imports System.ServiceModel.DomainServices.Client
Imports System.ComponentModel.DataAnnotations
Imports System.Runtime.CompilerServices
Imports System.Reflection
Imports System.Windows.Data
Imports System.ComponentModel
Imports BusinessImpact.Messaging

Namespace ViewModel
    Public Class DepartmentViewModel
        Inherits ViewModelBase

#Region "Ctor"
        Public Sub New()
            LoadDepartments()
        End Sub
#End Region

#Region "Mediator"
        Public Sub SendSelectedDepartmentChanged()
            If SelectedDepartment Is Nothing Then
                Exit Sub
            End If
            Messenger.Default.Send(New SelectedDepartmentMessage() With
                    {
                        .deptID = SelectedDepartment.ID,
                        .deptName = SelectedDepartment.Name
                    })
        End Sub
#End Region

#Region "Loading Data"
        Private Sub LoadDepartments()
            ctx.Load(ctx.GetDepartmentsQuery)
        End Sub
#End Region

#Region "Entities"
        Public ReadOnly Property Departments As EntitySet(Of Department)
            Get
                Return ctx.Departments
            End Get
        End Property

        Public ReadOnly Property PCVDepartments As PagedCollectionView
            Get
                PCVDepartments = New PagedCollectionView(Departments)
                PCVDepartments.GroupDescriptions.Add(New PropertyGroupDescription("BusinessUnit"))
                PCVDepartments.SortDescriptions.Add(New SortDescription("Name", ListSortDirection.Ascending))
            End Get
        End Property

        Private _selectedDepartment As Department = Nothing
        Public Property SelectedDepartment As Department
            Get
                Return _selectedDepartment
            End Get
            Set(ByVal value As Department)
                _selectedDepartment = value
                RaisePropertyChanged(Function() Me.SelectedDepartment)
                ViewFunctionsCommand.RaiseCanExecuteChanged()
                SendSelectedDepartmentChanged()
            End Set
        End Property
#End Region

#Region "Commanding"
        Private _changeSelectedDepartmentCommand As DelegateCommand
        Public ReadOnly Property ChangeSelectedDepartmentCommand As DelegateCommand
            Get
                If _changeSelectedDepartmentCommand Is Nothing Then
                    _changeSelectedDepartmentCommand = _
                        New DelegateCommand(Sub(param As Object)
                                                SelectedDepartment = DirectCast(param, Department)
                                                RaisePropertyChanged(Function() Me.SelectedDepartment)
                                            End Sub)
                End If
                Return _changeSelectedDepartmentCommand
            End Get
        End Property
#End Region

    End Class
End Namespace

DepartmentContactViewModel:

VB.NET:
Imports System.ServiceModel.DomainServices.Client
Imports System.ComponentModel.DataAnnotations
Imports System.Runtime.CompilerServices
Imports System.Reflection
Imports BusinessImpact.Messaging
Imports System.Windows.Data

Namespace ViewModel
    Public Class DepartmentContactViewModel
        Inherits ViewModelBase

#Region "Ctor"
        Public Sub New()
            SubscribeSelectedDepartmentChanged()
        End Sub
#End Region

#Region "Mediator"
        Public Sub SubscribeSelectedDepartmentChanged()
            Messenger.[Default].OfType(Of SelectedDepartmentMessage) _
                .Subscribe(Sub(param As SelectedDepartmentMessage)
                               SelectedDepartmentID = param.deptID
                               LoadDepartmentContacts(param.deptID)
                           End Sub)
        End Sub
#End Region

#Region "Loading Data"
        Private Sub LoadDepartmentContacts(ByVal id As Integer)
            If id <> 0 Then
                ctx.Load(ctx.GetDepartmentContactByIDQuery(id),
                         Sub()
                             RaisePropertyChanged(Function() Me.DepartmentContacts)
                             RaisePropertyChanged(Function() Me.PCVDepartmentContacts)
                             SelectedDepartmentContact = DepartmentContacts.Where(Function(dc) dc.DeptID = id).FirstOrDefault
                             If PCVDepartmentContacts.ItemCount <> 0 Then
                                 SelectedDepartmentContact = PCVDepartmentContacts.Item(0)
                             End If
                         End Sub,
                         Nothing)
            End If
        End Sub
#End Region

#Region "Entities"
        Private Property _selectedDepartmentID As Integer = 0
        Public Property SelectedDepartmentID As Integer
            Get
                Return _selectedDepartmentID
            End Get
            Set(ByVal value As Integer)
                _selectedDepartmentID = value
                RaisePropertyChanged(Function() Me.SelectedDepartmentID)
            End Set
        End Property

        Public ReadOnly Property DepartmentContacts As EntitySet(Of Department_Contact)
            Get
                Return ctx.Department_Contacts
            End Get
        End Property

        Public ReadOnly Property PCVDepartmentContacts As PagedCollectionView
            Get
                PCVDepartmentContacts = New PagedCollectionView(DepartmentContacts)
                PCVDepartmentContacts.Filter = New Predicate(Of Object)(Function(dc As Department_Contact)
                                                                            Return dc.DeptID = SelectedDepartmentID
                                                                        End Function)
            End Get
        End Property

        Private _selectedDepartmentContact As Department_Contact = Nothing
        Public Property SelectedDepartmentContact As Department_Contact
            Get
                Return _selectedDepartmentContact
            End Get
            Set(ByVal value As Department_Contact)
                _selectedDepartmentContact = value
                RaisePropertyChanged(Function() Me.SelectedDepartmentContact)
            End Set
        End Property
#End Region

    End Class
End Namespace

SelectedDepartmentMessage:

VB.NET:
Imports BusinessImpact.Messaging

Namespace ViewModel
    Public Class SelectedDepartmentMessage
        Implements IMessage

        Public Property deptID As Integer
        Public Property deptName As String

    End Class
End Namespace
 
DepartmentUserControl xaml:

VB.NET:
    <UserControl.Resources>
        <vm:DepartmentViewModel x:Key="DepartmentViewModelDataSource" d:IsDataSource="True"/>
        <Style x:Key="DataGridTextColumnStyle" TargetType="TextBlock">
            <Setter Property="TextWrapping" Value="Wrap" />
        </Style>
    </UserControl.Resources>

    <UserControl.DataContext>
        <Binding Source="{StaticResource DepartmentViewModelDataSource}"/>
    </UserControl.DataContext>

    <Grid x:Name="LayoutRoot">
        <Border  BorderThickness="2" CornerRadius="5" Padding="3" Margin="3" BorderBrush="#FF999999" Background="#0C000000">
            <StackPanel Orientation="Vertical">
                <sdk:DataGrid x:Name="dataGrid" 
                              ItemsSource="{Binding PCVDepartments, Mode=OneWay}"
                              AutoGenerateColumns="False">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="SelectionChanged">
                            <i:InvokeCommandAction Command="{Binding ChangeSelectedDepartmentCommand}"
                                               CommandParameter="{Binding SelectedItem, ElementName=dataGrid}"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                    <sdk:DataGrid.Columns>
                        <sdk:DataGridTextColumn Header="Department Name"
                                                Width="1*"
                                                Binding="{Binding Name, Mode=TwoWay}" 
                                                ElementStyle="{StaticResource DataGridTextColumnStyle}" />
                        <sdk:DataGridTextColumn Header="Location" 
                                                Width="2*"
                                                Binding="{Binding Location, Mode=TwoWay}"
                                                ElementStyle="{StaticResource DataGridTextColumnStyle}"/>
                    </sdk:DataGrid.Columns>
                </sdk:DataGrid>
            </StackPanel>
        </Border>
    </Grid>

DepartmentContactUserControl xaml:

VB.NET:
    <UserControl.Resources>
        <vm:DepartmentContactViewModel x:Key="DepartmentContactViewModelDataSource" d:IsDataSource="True"/>
    </UserControl.Resources>

    <UserControl.DataContext>
        <Binding Source="{StaticResource DepartmentContactViewModelDataSource}"/>
    </UserControl.DataContext>

    <Grid x:Name="LayoutRoot">
        <Border  BorderThickness="2" CornerRadius="5" Padding="3" Margin="3" BorderBrush="#FF999999" Background="#0C000000">
            <Grid Margin="10" DataContext="{Binding SelectedDepartmentContact}">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="60" />
                    <ColumnDefinition Width="240" />
                </Grid.ColumnDefinitions>
                <sdk:Label Content="Contact:" HorizontalAlignment="Right" Grid.Column="0" Grid.Row="0" />
                <sdk:Label Content="Phone:" HorizontalAlignment="Right" Grid.Column="0" Grid.Row="1" />
                <sdk:Label Content="Extension:" HorizontalAlignment="Right" Grid.Column="0" Grid.Row="2" />
                <sdk:Label Content="Email:" HorizontalAlignment="Right" Grid.Column="0" Grid.Row="3" />
                <TextBox Text="{Binding Contact, Mode=TwoWay}" 
                         Grid.Column="1" Grid.Row="0" 
                         Margin="3"
                         IsReadOnly="True" />
                <TextBox Text="{Binding Phone, Mode=TwoWay}" 
                         Grid.Column="1" Grid.Row="1" 
                         Margin="3"
                         IsReadOnly="True"/>
                <TextBox Text="{Binding Extension, Mode=TwoWay}" 
                         Grid.Column="1" Grid.Row="2" 
                         Margin="3"
                         IsReadOnly="False"/>
                <TextBox Text="{Binding Email, Mode=TwoWay}" 
                         Grid.Column="1" Grid.Row="3" 
                         Margin="3"
                         IsReadOnly="True"/>
            </Grid>
        </Border>
    </Grid>
 
Back
Top