Alessandro Del Sole's Blog

/* A programming space about Microsoft® .NET® */
posts - 159, 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 2015!

Pre-order VB 2015 Unleashed Pre-order my new book "Visual Basic 2015 Unleashed". Click for more info!

My new book on LightSwitch!

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

Your visits

Follow me on Twitter!

CodePlex download Download my open-source projects from CodePlex!

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 8

Let's go ahead with our discussion about the MVVM pattern in WPF applications with VB 2010. Last time I explained how to implement a service layer; in this post I will focus on ViewModels. Particularly I will show where to place an instance of the Messenger class and then I will show all the required ViewModels. Because of this it will be a quite long work, especially in terms of lines of code since concepts have all been explained in this previous post.

Send me a message!

In all tutorials I found (and studied) declaring an instance of the Messenger class is done at the application level under the form of a read-only property. Methods offered by such a class receive arguments of type string, representing the messages to exchange with colleague objects and that will be intercepted later. Instead of writing strings every time, you can define constants storing messages. So the Application class also becomes the place where you define such constants. With regard to this, we are interested in intercepting two messages: one for opening the Order Details window and one for closing the window itself. With that said this is the code required in the Application.xaml.vb file:

Class Application
 
    ' Application-level events, such as Startup, Exit, and DispatcherUnhandledException
    ' can be handled in this file.
 
    Friend Const VIEW_DETAILS_EXECUTE As String = "ViewDetailsExecute"
    Friend Const VIEW_DETAILS_CLOSE As String = "ViewDetailsClose"
 
    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.ObjectByVal e As System.Windows.Threading.DispatcherUnhandledExceptionEventArgs)
        MessageBox.Show(e.Exception.Message)
        e.Handled = True
    End Sub
End Class

Later in this post I will show an example of using the Messenger class but for now let's go ahead with ViewModels.

Each View owns a ViewModel 

Let's start by this statement: each View (meaning each Window or user control whose job is presenting data) talks to a specific ViewModel. In our application we have two windows: the main window and the window for showing order details. So we need to implement two ViewModels. Like we did previously, we can implement a class name ViewModelBase which will send (via inheritance) some members that are common for all ViewModels. At this point add a new code file to the ViewModels folder, naming it ViewModelBase.vb. I will now show an extended version of the class, adding some members for property validation. Although in the sample application I will not use any special features, these can be useful in the future. Moreover, I'm going to introduce the ServiceLocator and GetService methods which return respectively the instance of the ServiceLocator class (which switches service requests) and the instance of the specified service class. Both will be used later in the derived ViewModels. Now the class looks like this:

Imports System.ComponentModel
Public MustInherit Class ViewModelBase     Implements INotifyPropertyChanged
    Dim myServiceLocator As New ServiceLocator
    Public Event PropertyChanged(ByVal sender As ObjectByVal e As System.ComponentModel.PropertyChangedEventArgsImplements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
    Protected Sub OnPropertyChanged(ByVal strPropertyName As String)         If Me.PropertyChangedEvent IsNot Nothing Then             RaiseEvent PropertyChanged(MeNew 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

Nothing difficult but more has to come in a few moments.

A ViewModel for the Customer/Orders relationship

In the application's main window we want to show a list of customers and when the user selects a customer, the application will show the orders related to the customer and the user will be able of adding, removing and browsing data. So we need to create a ViewModel in order to expose to the View the necessary data over than the appropriate members that will perform the required operations. Also the ViewModel will implement IDataErrorInfo (I described such a technique here) so that it will send data validation errors to the View, which are raised by the Model. Now the point is this: generally we expose data through properties of type ObservableCollection; this is correct because such a collection provides full support to data-binding in WPF. By the way, we need methods which allow browsing and editing data. To accomplish this we can use objects of type ICollectionView and we can expose to Views some objects of type ListCollectionView. This particular kind of object in fact offers advanced functionalities for working against data and supports data-binding. With that said we basically need to implement:

  • Commands for executing tasks 
  • IDataErrorInfo for data validation 
  • Instances of the service classes that will actually access the database
  • Properties coming from the Model that will expose data to Views
  • Objects of type ListCollectionView that, while bound, will allow managing data through the View
  • Properties of type ObservableCollection that return data. Although these properties will not be used effectively, they can be useful for future utilizations and purposes while their backing fields are necessary in order to provide support for ListCollectionViews

Thus after adding a new code file named OrdersViewModel.vb to the project, let's begin by adding some support fields:

Public Class OrdersViewModel
    Inherits ViewModelBase
 
    Implements IDataErrorInfo
 
#Region " Declarations "
 
    'A number of required commands
    Private _cmdDeleteCommand As ICommand
    Private _cmdInsertCommand As ICommand
    Private _cmdNextCommand As ICommand
    Private _cmdPreviousCommand As ICommand
    Private _cmdSaveCommand As ICommand
    Private _cmdViewDetails As ICommand
 
    'A single order
    Private _objOrder As Order
    'The instance of the current customer
    Private _selection As Customer
 
    'Exposing data the common way
    Private _orders As ObservableCollection(Of Order)
    Private _customers As ObservableCollection(Of Customer)
 
    'A "bridge" between data and View
    Private _customerViewSource As New CollectionViewSource
    Private _customerOrdersViewSource As New CollectionViewSource
 
    'Lists of editable objects, both for customers and orders
    Private WithEvents _customersView As ListCollectionView
    Private WithEvents _customerOrdersView As ListCollectionView
 
    'The instance of the service class
    Private orderAccess As IOrderDataService
 
#End Region

At this point we can go to data properties. First we expose CollectionView objects via some properties and the same will be done for ListCollectionView objects; this is important to get the data to work with. Next we have a property representing the instance of the current customer and one representing the instance of the current order. For the sake of completeness we expose also 2 properties of type ObservableCollection for both customers and orders that you can use instead of ListCollectionViews. In the end, the code for properties is the following:

#Region " Properties "
 
    Public ReadOnly Property CustomerViewSource As CollectionViewSource
        Get
            Return Me._customerViewSource
        End Get
    End Property
 
    Public Property CustomerOrdersViewSource As CollectionViewSource
        Get
            Return Me._customerOrdersViewSource
        End Get
        Set(ByVal value As CollectionViewSource)
            Me._customerOrdersViewSource = value
            OnPropertyChanged("CustomerOrdersViewSource")
        End Set
    End Property
 
    Public Property CustomersView As ListCollectionView
        Get
            Return Me._customersView
        End Get
        Set(ByVal value As ListCollectionView)
            Me._customersView = value
            OnPropertyChanged("CustomersView")
        End Set
    End Property
 
    Public Property CustomerOrdersView As ListCollectionView
        Get
            Return Me._customerOrdersView
        End Get
        Set(ByVal value As ListCollectionView)
            Me._customerOrdersView = value
            OnPropertyChanged("CustomerOrdersView")
        End Set
    End Property
 
    'The instance of the current customer
    Public Property Selection As Customer
        Get
            Return Me._selection
        End Get
        Set(ByVal value As Customer)
            If value Is _selection Then
                Return
            End If
            _selection = value
            MyBase.OnPropertyChanged("Selection")
        End Set
    End Property
 
    Public Property Customers As ObservableCollection(Of Customer)
        Get
            Return Me._customers
        End Get
        Set(ByVal value As ObservableCollection(Of Customer))
            Me._customers = value
            OnPropertyChanged("Customers")
        End Set
    End Property
 
    Public Property Orders As ObservableCollection(Of Order)
        Get
            Return Me._orders
        End Get
        Set(ByVal value As ObservableCollection(Of Order))
            Me._orders = value
            OnPropertyChanged("Orders")
        End Set
    End Property
 
    Public Property Order() As Order
        Get
            Return _objOrder
        End Get
        Set(ByVal Value As Order)
            _objOrder = Value
            OnPropertyChanged("Order")
        End Set
    End Property
 
    Public Property Customer() As Customer
        Get
            Return _objOrder.Customer
        End Get
        Set(ByVal Value As Customer)
            _objOrder.Customer = Value
            OnPropertyChanged("Customer")
        End Set
    End Property
 
#End Region

Now we need command properties. Nothing new, I already talked about this here.

#Region " Command Properties "
 
    Public ReadOnly Property DeleteCommand() As ICommand
        Get
            If _cmdDeleteCommand Is Nothing Then
                _cmdDeleteCommand = New RelayCommand(AddressOf DeleteExecute, AddressOf CanDeleteExecute)
            End If
            Return _cmdDeleteCommand
        End Get
    End Property
 
    Public ReadOnly Property InsertCommand() As ICommand
        Get
            If _cmdInsertCommand Is Nothing Then
                _cmdInsertCommand = New RelayCommand(AddressOf InsertExecute, AddressOf CanInsertExecute)
            End If
            Return _cmdInsertCommand
        End Get
    End Property
 
    Public ReadOnly Property NextCommand() As ICommand
        Get
            If _cmdNextCommand Is Nothing Then
                _cmdNextCommand = New RelayCommand(AddressOf NextExecute, AddressOf CanNextExecute)
            End If
            Return _cmdNextCommand
        End Get
    End Property
 
    Public ReadOnly Property PreviousCommand() As ICommand
        Get
            If _cmdPreviousCommand Is Nothing Then
                _cmdPreviousCommand = New RelayCommand(AddressOf PreviousExecute, AddressOf CanPreviousExecute)
            End If
            Return _cmdPreviousCommand
        End Get
    End Property
 
    Public ReadOnly Property SaveCommand() As ICommand
        Get
            If _cmdSaveCommand Is Nothing Then
                _cmdSaveCommand = New RelayCommand(AddressOf SaveExecute, AddressOf CanSaveExecute)
            End If
            Return _cmdSaveCommand
        End Get
    End Property
 
    Public ReadOnly Property ViewDetailsCommand() As ICommand
        Get
            If _cmdViewDetails Is Nothing Then
                _cmdViewDetails = New RelayCommand(AddressOf ViewDetailsExecute, AddressOf CanViewDetailsExecute)
            End If
            Return _cmdViewDetails
        End Get
    End Property
 
#End Region

Now it's time for IDataErrorInfo. Again, I discussed this technique in this previous post:

#Region " IDataErrorInfo members "
    Public ReadOnly Property [Error] As String Implements System.ComponentModel.IDataErrorInfo.Error
        Get
            Return TryCast(Me.Order, IDataErrorInfo).Error
        End Get
    End Property
 
    Default Public ReadOnly Property Item(ByVal columnName As StringAs String Implements System.ComponentModel.IDataErrorInfo.Item
        Get
            Dim [error] As String = Nothing
 
            [error] = (TryCast(Me.Order, IDataErrorInfo))(columnName)
            ' Dirty the commands registered with CommandManager,
            ' such as our Save command, so that they are queried
            ' to see if they can execute now.
            CommandManager.InvalidateRequerySuggested()
 
            Return [error]
        End Get
    End Property
#End Region

Things become more interesting at the point in which we need to write the constructor.

When the game gets harder

Our ViewModel's constructor plays a fundamental role. In fact it will register and instantiate service classes that will do the actual data access work via Entity Framework. First the code, then some considerations.

#Region " Constructors "
 
    Public Sub New()
        Me._customers = New ObservableCollection(Of Customer)
 
        'Register the instance of the CustomerDataService service class
        ServiceLocator.RegisterService(Of ICustomerDataService)(New CustomerDataService)
        'Register the instance of the service class related to orders
        ServiceLocator.RegisterService(Of IOrderDataService)(New OrderDataService)
 
        'Get the instance of both classes
        Dim dataAccess = GetService(Of ICustomerDataService)()
        Me.orderAccess = GetService(Of IOrderDataService)()
 
        'Retrieves the list of customers, adding each of them to the collection
        For Each element In dataAccess.GetAllCustomers
            Me._customers.Add(element)
        Next
 
        'Set the CollectionViewSource
        _customerViewSource.Source = Me.Customers
 
        'Get a View from the CollectionViewSource and perform a conversion to
        'ListCollectionView which supports editing
        Me.CustomersView = CType(Me.CustomerViewSource.View, ListCollectionView)
        Me.CustomersView.MoveCurrentToFirst()
    End Sub

#End Region

The shared method named RegisterService.ServiceLocator here is registering 2 instances for both the CustomerDataService and OrderDataService service classes. Notice that the generic parameter of RegisterService is not a class but the interface. This is the reason why we need to write interfaces and then implement them. The GetService method, exposed by the base class, retrieves the instance of the service classes. The class related to orders is at class level because we will use it inside command methods. The first usage example is the For Each loop that populates the field storing the list of customers; in fact you can notice how the code invokes the GetAllCustomers method exposed by the service class. Here is the cool thing: the ViewModel is getting a list of data but it does not know that such data are coming from anEntity Data Model.

Let's go ahead with command methods

Now it's time to implement command methods, like CanXXXExecute and XXXExecute, where XXX is the name of the action to execute. Nothing difficult, I discussed this topic here. Anyway, it is worth pointing out a couple of things. Every execution method invokes the related one exposed by the service class. So, while in different scenarios we saw how to execute actions against data directly from within such methods, here we are asking the service class to do the work for us. In this way the ViewModel is still able of working against data but being abstracted from the Data Access Layer, which is in this particular case is the Entity Framework that will receive instructions by the OrderDataService class. This means that some day I can change my data store (although with some slight modifications) but my ViewModel remains substantially unchanged. There is also another interesting detail that I will discuss after the code:

#Region " Command Methods "
 
    Private Function CanDeleteExecute(ByVal param As ObjectAs Boolean
        If Me.CustomerOrdersView Is Nothing Then Return False
        Return Me.CustomerOrdersView.CurrentPosition > -1
    End Function
 
    Private Sub DeleteExecute(ByVal param As Object)
        Me.orderAccess.Delete(Me.CustomerOrdersView)
    End Sub
 
    Private Function CanInsertExecute(ByVal param As ObjectAs Boolean
        Return True
    End Function
 
    Private Sub InsertExecute(ByVal param As Object)
        Me.orderAccess.Insert(Me.CustomerOrdersView, Me.Selection)
    End Sub
 
    Private Function CanNextExecute(ByVal param As ObjectAs Boolean
        If Me.CustomerOrdersViewSource.View Is Nothing Then Return False
        Return Me.CustomerOrdersViewSource.View.CurrentPosition <
            CType(Me.CustomerOrdersViewSource.View, CollectionView).Count - 1
    End Function
 
    Private Sub NextExecute(ByVal param As Object)
        If Me.CanNextExecute(param) Then
            Me.orderAccess.MoveToNext(Me.CustomerOrdersViewSource)
        End If
    End Sub
 
    Private Function CanPreviousExecute(ByVal param As ObjectAs Boolean
        If Me.CustomerOrdersViewSource.View Is Nothing Then Return False
        Return Me.CustomerOrdersViewSource.View.CurrentPosition > 0
    End Function
 
    Private Sub PreviousExecute(ByVal param As Object)
        If Me.CanPreviousExecute(param) Then
            Me.orderAccess.MoveToPrevious(Me.CustomerOrdersViewSource)
        End If
    End Sub
 
    Private Function CanSaveExecute(ByVal param As ObjectAs Boolean
        Try
            If CType(Me.CustomerOrdersView.CurrentItem, Order).HasErrors Then
                Return False
            Else
                Return True
            End If
        Catch ex As Exception
 
        End Try
    End Function
 
    Private Sub SaveExecute(ByVal param As Object)
        Me.orderAccess.Save()
    End Sub
 
    Private Function CanViewDetailsExecute(ByVal param As ObjectAs Boolean
        Return Me.Selection IsNot Nothing
    End Function
 
    Private Sub ViewDetailsExecute(ByVal param As Object)
        Application.Msn.NotifyColleagues(Application.VIEW_DETAILS_EXECUTE)
    End Sub
 
#End Region

The last method, ViewDetailsExecute, uses the Messenger class. The NotifyColleagues method is sending the message defined in the VIEW_DETAILS_EXECUTE constant so that when the message is intercepted by the View, the Order Details window will be opened. I will retake this technique in the last post of this series, when talking about Views.

Selection changes

The last step for this OrdersViewModel is handling an event. Since we need to understand when the user chooses a different customer via the UI, we can handle the CurrentChanged event in the CollectionView which exposes the list of Customers. When the event is raised, orders related to the newly selected customer are loaded. All is made by invoking methods from the OrderDataService service class. The code:

#Region " Event handlers"
    '_customersView is the backing field for the CustomersView property
    Private Sub _customersView_CurrentChanged(ByVal sender As ObjectByVal e As System.EventArgsHandles _customersView.CurrentChanged
 
        Dim currentCustomer = CType(Me.CustomersView.CurrentItem, Customer)
 
        Try
            Me.Orders = orderAccess.GetAllOrders(currentCustomer.CustomerID)
            Me.CustomerOrdersViewSource.Source = Me.Orders
            Me.CustomerOrdersView = CType(Me.CustomerOrdersViewSource.View, ListCollectionView)
        Catch ex As Exception
 
        End Try
    End Sub
#End Region
 
End Class

Now let's switch to the Order Details ViewModel.

The OrderDetailsViewModel

The ViewModel for managing Order Details works similarly like the previous one but with minor functionalities. In our application we just need to show a list of order details but we will also implement methods for saving data and closing the view with messages because this can be useful in the future. This ViewModel will also use an instance of the OrderDataService class the same way as for the previous one. All the techniques describe here are not new, so you can check the previous section if something appears difficult. The following is the code for the new OrderDetailsViewModel that you add to the project folder called ViewModels:

Imports System.Collections.ObjectModel
 
Public Class OrderDetailsViewModel
    Inherits ViewModelBase
 
#Region " Declarations "
 
    Private _cmdSaveCommand As ICommand
    Private _cmdCloseCommand As ICommand
 
    Private _objOrder_Detail As Order_Detail
    Private _orderDetails As ObservableCollection(Of Order_Detail)
    Dim _selection As Order_Detail
 
    Private _orderDetailsViewSource As CollectionViewSource
    Private _orderDetailsView As ListCollectionView
 
    Private dataAccess As IOrderDataService
#End Region
 
#Region " Properties "
 
 
    Public Property OrderDetailsViewSource As CollectionViewSource
        Get
            Return Me._orderDetailsViewSource
        End Get
        Set(ByVal value As CollectionViewSource)
            Me._orderDetailsViewSource = value
            OnPropertyChanged("OrderDetailsViewSource")
        End Set
    End Property
 
    Public Property OrderDetailsView As ListCollectionView
        Get
            Return Me._orderDetailsView
        End Get
        Set(ByVal value As ListCollectionView)
            Me._orderDetailsView = value
            OnPropertyChanged("OrderDetailsView")
        End Set
    End Property
 
    Public Property Order_Detail() As Order_Detail
        Get
            Return _objOrder_Detail
        End Get
        Set(ByVal Value As Order_Detail)
            _objOrder_Detail = Value
            OnPropertyChanged("Order_Detail")
        End Set
    End Property
 
    Public Property Order_Details As ObservableCollection(Of Order_Detail)
        Get
            Return Me._orderDetails
        End Get
        Set(ByVal value As ObservableCollection(Of Order_Detail))
            Me._orderDetails = value
            OnPropertyChanged("Order_Details")
        End Set
    End Property


    Public Property Selection As Order_Detail
        Get
            Return Me._selection
        End Get
        Set(ByVal value As Order_Detail)
            If value Is _selection Then
                Return
            End If
            _selection = value
            MyBase.OnPropertyChanged("Selection")
        End Set
    End Property
 
#End Region
 
#Region " Command Properties "
 
    Public ReadOnly Property SaveCommand() As ICommand
        Get
            If _cmdSaveCommand Is Nothing Then
                _cmdSaveCommand = New RelayCommand(AddressOf SaveExecute, AddressOf CanSaveExecute)
            End If
            Return _cmdSaveCommand
        End Get
    End Property
 
    Public ReadOnly Property CloseCommand As ICommand
        Get
            If _cmdCloseCommand Is Nothing Then
                _cmdCloseCommand = New RelayCommand(AddressOf CloseExecute, AddressOf CanCloseExecute)
            End If

            Return _cmdCloseCommand
        End Get
    End Property
#End Region
 
#Region " Constructors "
 
    Public Sub New(ByVal OrderID As Integer)
        Me._orderDetails = New ObservableCollection(Of Order_Detail)
 
        ServiceLocator.RegisterService(Of IOrderDataService)(New OrderDataService)
        Me.dataAccess = GetService(Of IOrderDataService)()

        Me._orderDetails = dataAccess.GetOrderDetailsByOrderId(OrderID)
        Me._orderDetailsViewSource = New CollectionViewSource
        Me.OrderDetailsViewSource.Source = Me.Order_Details
        Me.OrderDetailsView = CType(Me.OrderDetailsViewSource.View, ListCollectionView)
 
    End Sub
#End Region
 
#Region " Command Methods "
 
    Private Function CanSaveExecute(ByVal param As ObjectAs Boolean
        Return True
    End Function
 
    Private Sub SaveExecute(ByVal param As Object)
        Me.dataAccess.Save()
    End Sub
    Private Sub CloseExecute(ByVal param As Object)
        Application.Msn.NotifyColleagues(Application.VIEW_DETAILS_CLOSE)
    End Sub
 
    Private Function CanCloseExecute(ByVal param As ObjectAs Boolean
        Return True
    End Function
 
#End Region
End Class

It's worth mentioning that here the Messenger.NotifyColleagues method is also invoked for closing the object. This can be useful in case we want to let the caller know that the associated View is closing. Now we have an infrastructure that works against data, via a service layer that allows ViewModels to interact with data without knowing who's on the other side. It's cool, isn't it?

End of the story

This is the end of this blog post, after a quite long work. Basically we did the following:

  1. placed a declaration for the instance of the Messenger class
  2. implemented a ViewModelBase class
  3. implemented two ViewModels, one for showing the customer/orders relationship and one for showing the list of order details

The last step is defining Views. But this is something that I will not cover in next post, since I will do this in part 10. In part 9 I will talk about something very interesting: first some code refactoring and, most of all, writing unit test so that we can test code blocks understanding one of the most important benefits provided by the MVVM pattern.

Alessandro

Print | posted on giovedì 12 agosto 2010 16:51 | Filed Under [ Visual Studio 2010 Visual Basic Windows Presentation Foundation ]

Feedback

No comments posted yet.

Post Comment

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

Powered by:
Powered By Subtext Powered By ASP.NET