MVVM in VB.Net - Lesson 3: The Core

Overview

The core of the MVVM design contains abstract base classes that will be used throughout your application. The core will always contain the ObserverableObject. This class will implement the INotifyPropertyChanged interface that will notify the UI that a data bound property has changed, and needs to update its data.

Place the following classes in the Core folder.

ObserverableObject

The ObserverableObject is a base class that contains essential methods used in ViewModels and to update the View’s data bound properties.

ObserverableObject.vb

Imports System.ComponentModel

''' <summary>
''' This is the abstract base class for any object that provides property change notifications.  
''' </summary>
<Serializable()> _
Public MustInherit Class ObservableObject
    Implements INotifyPropertyChanged
    
#Region "OnPropertyChanged"

    ''' <summary>
    ''' Raises this object's PropertyChanged event.
    ''' </summary>
    ''' <param name="propertyName">The property that has a new value.</param>
    Protected Sub OnPropertyChanged(ByVal propertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub

#End Region

#Region "Debugging Aides"

    ''' <summary>
    ''' Warns the developer if this object does not have
    ''' a public property with the specified name. This 
    ''' method does not exist in a Release build.
    ''' </summary>
    <Conditional("DEBUG")> _
    <DebuggerStepThrough()> _
    Public Sub VerifyPropertyName(ByVal propertyName As String)

        ' If you raise PropertyChanged and do not specify a property name,
        ' all properties on the object are considered to be changed by the binding system.
        If [String].IsNullOrEmpty(propertyName) Then
            Return
        End If

        ' 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 ArgumentException(msg)
            Else
                Debug.Fail(msg)
            End If
        End If
    End Sub

    ''' <summary>
    ''' Returns whether an exception is thrown, or if a Debug.Fail() is used
    ''' when an invalid property name is passed to the VerifyPropertyName method.
    ''' The default value is false, but subclasses used by unit tests might 
    ''' override this property's getter to return true.
    ''' </summary>
    Protected Property ThrowOnInvalidPropertyName() As Boolean
        Get
            Return m_ThrowOnInvalidPropertyName
        End Get
        Private Set(ByVal value As Boolean)
            m_ThrowOnInvalidPropertyName = value
        End Set
    End Property
    Private m_ThrowOnInvalidPropertyName As Boolean

#End Region

#Region "INotifyPropertyChanged Members"

    ''' <summary>
    ''' Raised when a property on this object has a new value.
    ''' </summary>
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

#End Region

End Class

There are 3 regions:

  • OnPropertyChanged – This Sub is what you will use to raise the PropertyChanged event.
  • PropertyChanged – When this event is raised. WPF’s UI will update it’s data bound data.
  • Debugging Aides – These methods are here to be sure that when you call the OnPropertyChanghed method, that the parameter provided matches a property in the class.

ObserverableDataObject

The ObserverableDataObject is used as a base class for data oriented classes, such as model classes. It inherits from the ObserverableObject class, and implements the IDataErrorInfo interface. This interface allows you to show data errors in the view’s data bound properties. The data errors notify the user that there is a problem with the data entered, and it must be corrected before the data is accepted.

ObserverableDataObject.vb

Public Class ObserverableDataObject
    Inherits ObservableObject
    Implements ComponentModel.IDataErrorInfo
    Implements ComponentModel.IChangeTracking

#Region "IsChageTracking Implementation"     

    ''' <summary>
    ''' Local storage.
    ''' </summary>
    Private Changes As New List(Of String)

    ''' <summary>
    ''' Adds a column that has changed from the original data.
    ''' </summary>
    ''' <param name="columnName">The column name.</param>
    Protected Sub AddChange(ByVal columnName As String)
        If Not Changes.Contains(columnName) Then
            Changes.Add(columnName)             

            ' triggers the UI to reevaluate the IsChanged Property
            OnPropertyChanged("IsChanged")
        End If
    End Sub

    ''' <summary>
    ''' Removes a column that is the same as the original data.
    ''' </summary>
    ''' <param name="columnName">The column name.</param>
    Protected Sub RemoveChange(ByVal columnName As String)
        If Changes IsNot Nothing AndAlso Changes.Contains(columnName) Then
            Changes.Remove(columnName)

            ' triggers the UI to reevaluate the IsChanged Property
            OnPropertyChanged("IsChanged")
        End If
    End Sub

    ''' <summary>
    ''' Clears the changes, and notifies that the data is clean.
    ''' </summary>
    Private Sub AcceptChanges() Implements System.ComponentModel.IChangeTracking.AcceptChanges
        Changes.Clear()

        ' triggers the UI to reevaluate the IsChanged Property
        OnPropertyChanged("IsChanged")
     End Sub

    ''' <summary>
    ''' Exposes a Boolean value to the View specifying the data has changes and should be saved.
    ''' You can use this property to toggle a reset button.
    ''' </summary>
    ''' <value>Boolean</value>
    ''' <returns>True if the data has changes; otherwise false.</returns>
    Public ReadOnly Property IsChanged As Boolean Implements System.ComponentModel.IChangeTracking.IsChanged
        Get
            Return (Changes.Count > 0)
        End Get
    End Property

#End Region


#Region "IDataErrorInfo"

    ''' <summary>
    ''' Raises when the error count has changed.
    ''' </summary>
    ''' <param name="HasErrors">Returns a Boolean value indicating whether or not this class has data errors.</param>
    Public Event HasErrorsChanged(ByVal HasErrors As Boolean)


    'This dictionary contains a list of our validation errors for each field
    Private _validationErrors As New Dictionary(Of String, String)
    Public ReadOnly Property ValidationErrors As Dictionary(Of String, String)
        Get
            Return _validationErrors
        End Get
    End Property

    ''' <summary>
    ''' Adds an error to the dictionary.
    ''' </summary>
    ''' <param name="columnName">The property that has the error.</param>
    ''' <param name="msg">The message to display to the user.</param>
    Protected Sub AddError(ByVal columnName As String, ByVal msg As String)
        If Not ValidationErrors.ContainsKey(columnName) Then
            ValidationErrors.Add(columnName, msg)
        End If
    End Sub

    ''' <summary>
    ''' Removes a specified error from the dictionary, if one exists.
    ''' </summary>
    ''' <param name="columnName">The property that no longer has an error.</param>
    Protected Sub RemoveError(ByVal columnName As String)
        If ValidationErrors.ContainsKey(columnName) Then
            ValidationErrors.Remove(columnName)
        End If
    End Sub

    ''' <summary>
    ''' A property that returns whether or not this class has errors.
    ''' </summary>
    ''' <returns>A Boolean value indicating if this class has errors.</returns>
    Public Overridable ReadOnly Property HasErrors() As Boolean
        Get
            Return (ValidationErrors.Count > 0)
        End Get
    End Property

    ''' <summary>
    ''' A property that returns whether or not this class is valid. This works in opposition to HasErrors Property.
    ''' </summary>
    ''' <returns>a Boolean value indicating if this class is valid.</returns>
    Public ReadOnly Property IsValid As Boolean
        Get
            Return _validationErrors.Count = 0
        End Get
    End Property

    ''' <summary>
    ''' Gets an error message indicating what is wrong with this object. 
    ''' </summary>
    ''' <returns>An error message or nothing is no error exists.</returns>
    Public ReadOnly Property [Error]() As String _
        Implements System.ComponentModel.IDataErrorInfo.Error
        Get
            If ValidationErrors.Count > 0 Then
                Return String.Format("{0} data is invalid.", TypeName(Me))
            Else
                Return Nothing
            End If
        End Get
    End Property

    ''' <summary>
    ''' Gets the error message for the property with the given name. 
    ''' </summary>
    ''' <param name="columnName">The property that has a data error.</param>
    ''' <returns>A error message or nothing if no error exists.</returns>
    Default Public ReadOnly Property Item(ByVal columnName As String) As String _
        Implements System.ComponentModel.IDataErrorInfo.Item
        Get
            If ValidationErrors.ContainsKey(columnName) Then
                Return ValidationErrors(columnName).ToString
            Else
                Return Nothing
            End If
        End Get
    End Property

#End Region

End Class

RelayCommand

The RelayCommand class will be used to push the command of our buttons on the views to the ViewModels. Remember, since the ViewModels and the Views are in separate layers. They can only communicate through data binding. This class allows us to communicate to the button through the ViewModel.

RelayCommand.vb

''' <summary>
''' A command whose sole purpose is to 
''' relay its functionality to other
''' objects by invoking delegates. The
''' default return value for the CanExecute
''' method is 'true'.
''' </summary>
Public Class RelayCommand
    Implements ICommand

#Region "Fields"

    Private ReadOnly _execute As Action
    Private ReadOnly _canExecute As Func(Of Boolean)

#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)
        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, ByVal canExecute As Func(Of Boolean))
        If execute Is Nothing Then
            Throw New ArgumentNullException("execute")
        End If

        _execute = execute
        _canExecute = canExecute
    End Sub

#End Region

#Region "ICommand Members"

    <DebuggerStepThrough()> _
    Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
        Return If(_canExecute Is Nothing, True, _canExecute())
    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()
    End Sub

#End Region ' ICommand Members

End Class

RelayCommandWithParameter

Sometimes you need to pass a parameter to the view model. For example, you want to edit a specific customer in a list. We can pass the customer object in the parameter.

RelayCommandWithParameter.vb

''' <summary>
''' A command whose sole purpose is to 
''' relay its functionality to other
''' objects by invoking delegates. The
''' default return value for the CanExecute
''' method is 'true'.
''' </summary>
Public Class RelayCommandWithParamerter
    Implements ICommand

#Region "Fields"

    Private ReadOnly _execute As Action(Of Object)
    Private ReadOnly _canExecute As Func(Of Boolean)

#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 Func(Of Boolean))
        If execute Is Nothing Then
            Throw New ArgumentNullException("execute")
        End If

        _execute = execute
        _canExecute = canExecute
    End Sub

#End Region

#Region "ICommand Members"

    <DebuggerStepThrough()> _
    Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
        Return If(_canExecute Is Nothing, True, _canExecute())
    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

This finished Core folder should look like this:

More by this Author


Comments 3 comments

David 4 years ago

Hi,

The ObserverableDataObject class includes a definition for an HasErrorsChanged event, however this event isn't raised in this class. Should this event be raised during the AddError and RemoveError methods if the ValidationErrors.count changes to or from 0? Is this event even required for MVVM?

Kind regards,

Dave.


Maligui profile image

Maligui 4 years ago from Elkhart, IN USA Author

The event is supposed to be raised when an error is added to removed. It is simply used for notifying the ViewModel of the changes.

In more advanced MVVM the event is not captured in the ViewModel, though. So it is safe to remove the event if you like.


elhuus 2 years ago

hello

can you please reupload the zip file

does this work with .net 3.5 (the type ttributes.NeedsValidationAttribute is missing) is it just some reference or is it uncompatible with 3.5

and verify where part 6 of this series is located in ModelBase.vb

thank you

    Sign in or sign up and post using a HubPages Network account.

    0 of 8192 characters used
    Post Comment

    No HTML is allowed in comments, but URLs will be hyperlinked. Comments are not for promoting your articles or other sites.


    Click to Rate This Article
    working