Alessandro Del Sole's Blog

{ A programming space about Microsoft® .NET® }
posts - 1904, comments - 2047, trackbacks - 352

My Links

News

Your host

This is me! Questo spazio è dedicato a Microsoft® .NET®, di cui sono molto appassionato :-)

Cookie e Privacy

Disabilita cookie ShinyStat

Microsoft MVP

My MVP Profile

Microsoft Certified Professional

Microsoft Specialist

Xamarin Certified Mobile Developer

Il mio libro su VB 2015!

Pre-ordina il mio libro su VB 2015 Pre-ordina il mio libro "Visual Basic 2015 Unleashed". Clicca sulla copertina per informazioni!

Il mio libro su WPF 4.5.1!

Clicca sulla copertina per informazioni! E' uscito il mio libro "Programmare con WPF 4.5.1". Clicca sulla copertina per informazioni!

These postings are provided 'AS IS' for entertainment purposes only with absolutely no warranty expressed or implied and confer no rights.
If you're not an Italian user, please visit my English blog

Le vostre visite

I'm a VB!

Guarda la mia intervista a Seattle

Follow me on Twitter!

Altri spazi

GitHub
I miei progetti open-source su GitHub

Article Categories

Archives

Post Categories

Image Galleries

Privacy Policy

WPF: Introduzione al pattern Model-View-ViewModel con Visual Basic 2010 - parte 3

Nei primi due post di questa serie dedicata a Model-View-ViewModel con VB 2010, abbiamo visto come creare applicazioni che, molto semplicemente, caricano, presentano e salvano dati attraverso un ViewModel che espone dati e comandi alla View. In questo post facciamo un ulteriore passo in avanti, implementando una tecnica di navigazione tra i dati che ci permetterà di andare avanti e indietro all’interno dei dati stessi. Ragionando per immagini, questo è il risultato da raggiungere:

Come vedete, quindi, rispetto alle precedenti volte cambia l’interfaccia. C’è una visualizzazione di dettaglio del singolo elemento più dei pulsanti per l’aggiunta, salvataggio e spostamento tra elementi. In fondo alla pagina trovate il link per scaricare il progetto completo.

 

Il Model

I nostri dati rimangono invariati rispetto ai precedenti post, quindi entrambe le classi Customer e Customers nonché il file di dati Customers.xml, tutto esattamente come prima.

 

La classe RelayCommand generica

Nel post precedente abbiamo introdotto la classe RelayCommand e abbiamo discusso come questa si occupi di ritrasmettere la logica di esecuzione dei command. La classe descritta la volta scorsa aveva un’implementazione non generica, quindi che lavora nei confronti del tipo Object. In realtà, è possibile anche riscrivere la classe affinché sia fortemente tipizzata e abbia un’implementazione generica. Riscriviamo quindi la classe RelayCommand come RelayCommand(Of T) in questo modo:

 

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

 

Sostanzialmente il lavoro della classe non cambia, quello che cambia è l’approccio generico e quindi il codice introduce alcune conversioni esplicite. A breve vedremo come, nel ViewModel, vada utilizzata tale classe.

 

Il ViewModel

Per quanto riguarda lo strato intermedio del ViewModel, possiamo riprendere il lavoro svolto nel precedente post e fare qualche lieve modifica. La classe ViewModelBase rimane invariata. Questa, infatti, espone dei membri base riutilizzabili. Alla classe CustomerViewModel dobbiamo invece aggiungere qualche campo e proprietà. Questi i campi:

 

    Private customersView As ICollectionView

    Private _cmdMoveNextCommand As ICommand

    Private _cmdMovePreviousCommand As ICommand

 

 

Il primo campo occorre per memorizzare la c.d. view dei dati (da non confondere con la View di M-V-VM che rappresenta lo strato di presentazione) e che consentirà di spostarsi al loro interno. Gli altri due campi servono invece come supporto per le due seguenti proprietà, che si occupano di esporre i comandi di spostamento avanti e indietro:

 

    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

 

Notate come si faccia utilizzo della versione generica della classe RelayCommand, il cui parametro è l’oggetto esposto dal Model. Ora bisogna prevedere i metodi di esecuzione e verifica dello stato di eseguibilità per i sopra esposti comandi. Eccoli:

 

    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

 

Se avete già lavorato in WPF con le view, non c’è niente di complicato. Si richiamano i metodi MoveCurrentToNext e MoveCurrentToPrevious sulla view per spostarsi. I comandi possono essere eseguiti solo se la posizione corrente lo consente, in base alla presenza o meno di altri elementi dopo o prima di quello selezionato. Notate come ora il parametro dei metodi non sia più di tipo Object, ma di tipo Customer. Questo è possibile grazie all’utilizzo della RelayCommand(Of T); questo richiede di modificare anche le firme dei metodi scritti la volta scorsa (più avanti riproporrò il codice dell’intera classe, rivisto). C’è ancora un passaggio da eseguire. Poiché lavoriamo sulla view, bisogna prima ottenerne l’istanza. Modifichiamo quindi di poco il costruttore del ViewModel in questo modo:

 

    Public Sub New()

        Me._customers = Customers.LoadCustomers

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

    End Sub

 

In questo modo l’istanza della view viene ottenuta in fase di caricamento dei dati. A seguito delle modifiche intervenute sul ViewModel, per comodità riporto il codice completo della classe 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 cust As New 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

 

La View

Vista la semplicità del risultato da ottenere, per costruire l’interfaccia della View mi sono aiutato col data-binding drag and drop introdotto da Visual Studio 2010 per WPF. Lo XAML che definisce la finestra, come rappresentata dalla figura all’inizio del post, è questo:

 

<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>

 

Nulla di trascendentale, è più lungo a scriverlo che a spiegarlo. Le note di rilievo sono relative alle proprietà Text delle TextBox, che sono in binding con le proprietà di interesse del ViewModel, così come le proprietà Command di ciascun pulsante che puntano ai comandi appropriati (come già discusso la scorsa volta). Da un punto di vista dell’istanza e assegnazione del ViewModel al DataContext della Window non cambia assolutamente nulla rispetto ai precedenti post:

 

    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

 

Avviando quindi l’applicazione otterremo il risultato mostrato nella figura iniziale, dimostrando ancora una volta il livello di separazione tra strati raggiunto grazie a M-V-VM. Tutto viene eseguito dal ViewModel, la View si occupa solo di porre in essere il data-binding.

 

Download del codice e cosa ci aspetta

Il codice sorgente a corredo di questo post è disponibile nell’area Download di Visual Basic Tips & Tricks, a questo indirizzo. Nel prossimo post vedremo come implementare tecniche di validazione dei dati nei confronti dell’applicazione creata oggi, sfruttando l’interfaccia IDataErrorInfo.

 

Alessandro

Print | posted on martedì 15 giugno 2010 15:03 | Filed Under [ Visual Basic Windows Presentation Foundation Visual Studio 2010 ]

Powered by:
Powered By Subtext Powered By ASP.NET