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 3

In the first two articles of this series about the Model-View-ViewModel pattern with VB 2010 we saw hot to build applications that load, show and save data through a ViewModel exposing data and commands to the View. In this blog post we'll make a further step, implementing a navigation technique between data that will allow us to browse the data themselves. Since images can speak better than words, this is the objective that we'll reach today:

 

 

As you can see the user interface is different from the previous sample application. Now we have a details view for each item plus some buttons for adding, saving and browsing data. At the bottom of this page you will find the link for downloading the source code. 

 

The Model

Again, data are unchanged from the previous posts meaning that both the Customer and Customers classes and the Customers.xml data file are exactly the same.

 

The generic version of RelayCommand

In the previous post I introduced the RelayCommand class discussing how it is responsible of relaying the commands execution logic. The class as it was described in the previous post had a non-generic implementation, working against the type Object. By the way it is possible rewriting the class in order to make it strongly-typed via a generic implementation. With that said, let's rewrite it as RelayCommand(Of T) like this:

 

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

 

Basically the work of the class is the same, but now you can use custom objects instead of Object. This is the reason why the code performs some explicit conversions. In the ViewModel I will show how to utilize such a class.

 

The ViewModel

About the intermediate layer known as ViewModel, we can retake the work we did in the previous post and making some slight modification. The ViewModelBase class remains unchanged since it exposes reusable members. We need to add some fields and properties to the CustomerViewModel class. New fields:

 

    Private customersView As ICollectionView

    Private _cmdMoveNextCommand As ICommand

    Private _cmdMovePreviousCommand As ICommand

 

 

The first field is required in order to store the so-called data view (pay attention to not confuse it with the View in M-V-VM which represents the presentation layer) and that will allow browsing data. The other two fields are instead the infrastructure for the following properties exposing navigation commands:

 

    Public ReadOnly Property MoveNextCommand As ICommand

        Get

            If _cmdMoveNextCommand Is Nothing Then

                _cmdMoveNextCommand = New RelayCommand(Of Customer)(AddressOf MoveNext, AddressOf CanMoveNext)

 

            End If

            Return _cmdMoveNextCommand

        End Get

    End Property

 

    Public ReadOnly Property MovePreviousCommand As ICommand

        Get

            If _cmdMovePreviousCommand Is Nothing Then

                _cmdMovePreviousCommand = New RelayCommand(Of Customer)(AddressOf MovePrevious, AddressOf CanMovePrevious)

 

            End If

            Return _cmdMovePreviousCommand

        End Get

    End Property

 

Notice how the code uses the generic version of RelayCommand, whose parameter is the object exposed by the Model. Now we need to implement execution and check methods for the above commands. Here's the code:

 

    Private Sub MoveNext(ByVal param As Customer)

        If Me.CanMoveNext(param) Then

            Me.customersView.MoveCurrentToNext()

        End If

    End Sub

 

    Private Function CanMoveNext(ByVal param As Customer) As Boolean

        Return Me.customersView.CurrentPosition < _

                           CType(Me.customersView, CollectionView).Count - 1

    End Function

 

    Private Sub MovePrevious(ByVal param As Customer)

        If Me.CanMovePrevious(param) Then

            Me.customersView.MoveCurrentToPrevious()

        End If

    End Sub

 

    Private Function CanMovePrevious(ByVal param As Customer) As Boolean

        Return Me.customersView.CurrentPosition > 0

    End Function

 

If you are existing WPF developers and worked with the views, there's nothing new. The code invokes the MoveCurrentToNext and MoveCurrentToPrevious methods against the view to browse data. Commands can be executed only if the current position is valid (if there are valid elements before or after the current one). Also notice how the methods argument's type is no longer of type Object but Customer. This is possible due to the usage of the generic version of RelayCommand(Of T); this also requires to change also the signatures of methods written in the previous post (later I'll provide the full revisited code). There's also another step. Since we work against the view, first we need to get its instance. Let's replace the ViewModel's constructor with the following one:

 

    Public Sub New()

        Me._customers = Customers.LoadCustomers

        Me.customersView = CollectionViewSource.GetDefaultView(Me._customers)

    End Sub

 

The view's instance is retrieved when data are loading. In order to have a full vision of the edits, this is the complete revisited code for CustomerViewModel:

 

Imports System.ComponentModel

 

Public Class CustomerViewModel

    Inherits ViewModelBase

 

    Private _cmdAddCommand As ICommand

    Private _cmdRemoveCommand As ICommand

    Private _cmdSaveCommand As ICommand

 

    Private _objCustomer As Customer

    Private _customers As Customers

    Private _selectedCustomer As Customer

 

    Private customersView As ICollectionView

    Private _cmdMoveNextCommand As ICommand

    Private _cmdMovePreviousCommand As ICommand

 

 

    Public Property Selection() As Customer

        Get

            Return _selectedCustomer

        End Get

        Set(ByVal value As Customer)

            If value Is _selectedCustomer Then

                Return

            End If

 

            _selectedCustomer = value

            MyBase.OnPropertyChanged("Selection")

        End Set

    End Property

 

    Public Property Customers As Customers

        Get

            Return _customers

        End Get

        Set(ByVal value As Customers)

            Me._customers = value

            OnPropertyChanged("Customers")

        End Set

    End Property

 

    Public Property Customer() As Customer

        Get

            Return _objCustomer

        End Get

        Set(ByVal Value As Customer)

            _objCustomer = Value

            MyBase.OnPropertyChanged("Customer")

        End Set

    End Property

 

    Public Property Address() As String

        Get

            Return _objCustomer.Address

        End Get

        Set(ByVal Value As String)

            _objCustomer.Address = Value

            MyBase.OnPropertyChanged("Address")

        End Set

    End Property

 

    Public Property CompanyName() As String

        Get

            Return _objCustomer.CompanyName

        End Get

        Set(ByVal Value As String)

            _objCustomer.CompanyName = Value

            MyBase.OnPropertyChanged("CompanyName")

        End Set

    End Property

 

    Public Property CustomerID() As Int32

        Get

            Return _objCustomer.CustomerID

        End Get

        Set(ByVal Value As Int32)

            _objCustomer.CustomerID = Value

            MyBase.OnPropertyChanged("CustomerID")

        End Set

    End Property

 

    Public Property Representative() As String

        Get

            Return _objCustomer.Representative

        End Get

        Set(ByVal Value As String)

            _objCustomer.Representative = Value

            MyBase.OnPropertyChanged("Representative")

        End Set

    End Property

 

    Public Sub New()

        Me._customers = Customers.LoadCustomers

        Me.customersView = CollectionViewSource.GetDefaultView(Me._customers)

    End Sub

 

    Public Sub New(ByVal customerCollection As Customers)

        Me._customers = customerCollection

        Me.customersView = CollectionViewSource.GetDefaultView(Me._customers)

    End Sub

 

    Public Sub New(ByVal objCustomer As Customer)

        _objCustomer = objCustomer

    End Sub

 

    Public ReadOnly Property MoveNextCommand As ICommand

        Get

            If _cmdMoveNextCommand Is Nothing Then

                _cmdMoveNextCommand = New RelayCommand(Of Customer)(AddressOf MoveNext, AddressOf CanMoveNext)

 

            End If

            Return _cmdMoveNextCommand

        End Get

    End Property

 

    Public ReadOnly Property MovePreviousCommand As ICommand

        Get

            If _cmdMovePreviousCommand Is Nothing Then

                _cmdMovePreviousCommand = New RelayCommand(Of Customer)(AddressOf MovePrevious, AddressOf CanMovePrevious)

 

            End If

            Return _cmdMovePreviousCommand

        End Get

    End Property

 

 

    Public ReadOnly Property AddCommand() As ICommand

        Get

            If _cmdAddCommand Is Nothing Then

                _cmdAddCommand = New RelayCommand(Of Customer)(AddressOf AddExecute, AddressOf CanAddExecute)

            End If

            Return _cmdAddCommand

        End Get

    End Property

 

 

    Public ReadOnly Property RemoveCommand() As ICommand

        Get

            If _cmdRemoveCommand Is Nothing Then

                _cmdRemoveCommand = New RelayCommand(Of Customer)(AddressOf Remove, AddressOf CanRemove)

            End If

            Return _cmdRemoveCommand

        End Get

    End Property

 

    Public ReadOnly Property SaveCommand() As ICommand

        Get

            If _cmdSaveCommand Is Nothing Then

                _cmdSaveCommand = New RelayCommand(Of Customer)(AddressOf Save, AddressOf CanSave)

            End If

            Return _cmdSaveCommand

        End Get

    End Property

 

    Private Function CanAddExecute(ByVal param As Customer) As Boolean

        Return True

    End Function

 

    Private Sub AddExecute(ByVal param As Customer)

        Dim lv As ListCollectionView = CType(Me.customersView, ListCollectionView)

        lv.AddNew()

        lv.CommitNew()

    End Sub

 

 

    Private Function CanRemove(ByVal param As Customer) As Boolean

        Return Me.Selection IsNot Nothing

    End Function

 

    Private Sub Remove(ByVal param As Customer)

        Me.Customers.Remove(Me.Selection)

    End Sub

 

    Private Function CanSave(ByVal param As Customer) As Boolean

        Return True

    End Function

 

    Private Sub Save(ByVal param As Customer)

        Dim doc = <?xml version="1.0"?>

                  <Customers>

                      <%= From cust In Me.Customers

                          Select <Customer CompanyName=<%= cust.CompanyName %>

                                     CustomerID=<%= cust.CustomerID %>

                                     Address=<%= cust.Address %>

                                     Representative=<%= cust.Representative %>/>

                      %>

                  </Customers>

 

        doc.Save(AppDomain.CurrentDomain.BaseDirectory + "Data\Customers.xml")

    End Sub

 

    Private Sub MoveNext(ByVal param As Customer)

        If Me.CanMoveNext(param) Then

            Me.customersView.MoveCurrentToNext()

        End If

    End Sub

 

    Private Function CanMoveNext(ByVal param As Customer) As Boolean

        Return Me.customersView.CurrentPosition < _

                           CType(Me.customersView, CollectionView).Count - 1

    End Function

 

    Private Sub MovePrevious(ByVal param As Customer)

        If Me.CanMovePrevious(param) Then

            Me.customersView.MoveCurrentToPrevious()

        End If

    End Sub

 

    Private Function CanMovePrevious(ByVal param As Customer) As Boolean

        Return Me.customersView.CurrentPosition > 0

    End Function

 

End Class

 

The View

The result we want to obtain is pretty simple, so I used the new drag'n'drop data-binding feature in VS 2010 in order to build the user interface. Following is the XAML code defining the Window as it was represented at the beginning of this blog post:

 

<Window x:Class="MainWindow"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    Title="MainWindow" Height="350" Width="525" mc:Ignorable="d" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

        xmlns:my="clr-namespace:_6___Model_View_ViewModel_with_Navigation">

    <Window.Resources>

        <CollectionViewSource x:Key="CustomerViewSource" d:DesignSource="{d:DesignInstance my:Customer, CreateList=True}" />

    </Window.Resources>

    <Grid>

        <Grid DataContext="{Binding Path=Customers}"

              HorizontalAlignment="Left" Margin="12,74,0,0" Name="Grid1" VerticalAlignment="Top">

            <Grid.ColumnDefinitions>

                <ColumnDefinition Width="Auto" />

                <ColumnDefinition Width="Auto" />

            </Grid.ColumnDefinitions>

            <Grid.RowDefinitions>

                <RowDefinition Height="Auto" />

                <RowDefinition Height="Auto" />

                <RowDefinition Height="Auto" />

                <RowDefinition Height="Auto" />

            </Grid.RowDefinitions>

            <Label Content="Address:" Grid.Column="0" Grid.Row="0" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />

            <TextBox Grid.Column="1" Grid.Row="0" Height="23" HorizontalAlignment="Left" Margin="3" Name="AddressTextBox" Text="{Binding Path=Address, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />

            <Label Content="Company Name:" Grid.Column="0" Grid.Row="1" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />

            <TextBox Grid.Column="1" Grid.Row="1" Height="23" HorizontalAlignment="Left" Margin="3" Name="CompanyNameTextBox" Text="{Binding Path=CompanyName, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />

            <Label Content="Customer ID:" Grid.Column="0" Grid.Row="2" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />

            <TextBox Grid.Column="1" Grid.Row="2" Height="23" HorizontalAlignment="Left" Margin="3" Name="CustomerIDTextBox" Text="{Binding Path=CustomerID, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />

            <Label Content="Representative:" Grid.Column="0" Grid.Row="3" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />

            <TextBox Grid.Column="1" Grid.Row="3" Height="23" HorizontalAlignment="Left" Margin="3" Name="RepresentativeTextBox" Text="{Binding Path=Representative, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />

        </Grid>

        <Button Content="Save" Height="33" Command="{Binding SaveCommand}"

                HorizontalAlignment="Left" Margin="336,72,0,0" Name="Button1" VerticalAlignment="Top" Width="104" />

        <Button Content="Next" Height="33" Command="{Binding MoveNextCommand}"

                HorizontalAlignment="Left" Margin="336,112,0,0" Name="Button2" VerticalAlignment="Top" Width="104" />

        <Button Content="Previous" Height="33" Command="{Binding MovePreviousCommand}"

                HorizontalAlignment="Left" Margin="336,152,0,0" Name="Button3" VerticalAlignment="Top" Width="104" />

        <Button Content="Insert" Height="33" Command="{Binding AddCommand}"

                HorizontalAlignment="Left" Margin="336,192,0,0" Name="Button4" VerticalAlignment="Top" Width="104" />

    </Grid>

</Window>

 

Nothing difficult. Just notice how the Text property in each TextBox is data-bound to the appropriate ViewModel's properties, as well as Command properties for each button, pointing to the appropriate commands. Nothing changes instead about assigning the ViewModel instance to the View:

 

    Private Sub Window_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded

        Dim custViewModel As New CustomerViewModel

        Me.DataContext = custViewModel

    End Sub

 

If you now run the application you get the result shown in the above figure, demonstrating once again the abstraction level between layers thanks to M-V-VM. The ViewModel runs everything, the View is only responsible for the data-binding. 

 

Downloading the source code and what's next

The companion code for this article is available here. In next post I will show how to implement data validation techniques using the IDataErrorInfo interface against the application shown today.

 

Alessandro

Print | posted on domenica 27 giugno 2010 15:19 | Filed Under [ Visual Studio 2010 Visual Basic Windows Presentation Foundation ]

Feedback

No comments posted yet.

Post Comment

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

Powered by:
Powered By Subtext Powered By ASP.NET