Condividi tramite


VB.NET Utilizzo di ICommand su pattern MVVM (it-IT)


Finalità

In questo articolo, si intende mostrare un'implementazione di base dell'interfaccia ICommand, per gestire - secondo il pattern MVVM - la definizione di comandi svincolati dalla UI su cui saranno utilizzati, realizzando quindi la separazione tra View e Model tipica di tale metodologia

Introduzione

Come brevemente accennato in un precedente articolo (cfr. VB.NET Cenni ed esempi di base sul pattern MVVM (it-IT)),il paradigma fondamentale di MVVM è quello di mantenere separate la parte grafica di una applicazione (tipicamente, la parte che effettua la presentazione dei dati) dalla sua business logic, ovvero il modo, il codice, tramite cui i dati sono reperiti, inizializzati, ed esposti. Nell'articolo linkato sopra, si è visto come ottenere ciò in relazione alle proprietà di un oggetto, introducendo un layer intermedio (il cosiddetto ViewModel), responsabile di rendere disponibili alla View (la UI, nel senso più generale) i dati (le proprietà) del Model (la classe contenente i dati veri e propri). Vedremo qui come la stessa cosa possa essere fatta anche per i comandi, ovvero come si possa superare la gestione ad eventi di un tasto, per muoversi invece verso una separazione netta tra l'oggetto cliccabile dall'utente, ed il codice che sarà poi effettivamente eseguito.

A questo fine, sarà opportuno preparare un ambiente adatto, ovvero una classe contenente dei dati da presentare, la classe intermedia per la loro esposizione, e la vista, che anche in questo caso sarà una Window WPF.

Una classe dati di base

Al pari di come abbiamo fatto per gli esempi passati, inizieremo con la stesura di una semplice classe che rappresenti l'elemento dati. Si tratta di un esempio in questo caso molto banale, dove andremo a progettare un elemento che espone avente due proprietà: un testo, identificato dalla proprietà Text, ed uno sfondo, ovvero la proprietà Background. Iniziando già ad ipotizzare come utilizzare tale classe in una vista, possiamo anticipare che tale classe verà bindata ad un controllo TextBox, e più precisamente collegando le due proprietà Text per il contenuto, e Background per il colore di sfondo.

Public Class  ItemData
    Dim _text As String
    Dim _backg As Brush = Nothing
 
    Public Property  Text As  String
        Get
            Return _text
        End Get
        Set(value As  String)
            _text = value
        End Set
    End Property
 
    Public Property  Background As  Brush
        Get
            Return _backg
        End Get
        Set(value As  Brush)
            _backg = value
        End Set
    End Property
 
    Public Sub  New()
        _text = "RANDOM TEXT"
        _backg = New  SolidColorBrush(Colors.Green)
    End Sub
End Class

Tale classe, nel suo costruttore, inizializza molto semplicemente le due proprietà con i valori di "RANDOM Text" per quando riguarda Text, ed il colore di sfondo creando un oggetto SolidColorBrush di colore verde. Nell'ottica del suo utilizzo, sappiamo che sarà opportuno - come accennato nel primo articolo - valorizzare il DataContext della nostra View. Dovremo pertanto creare ora una classe che funga da ViewModel, ovvero che - dato un ItemData - ne esponga le proprietà fondamentali (o, quantomeno, quelle che si desidera effettivamente utilizzare in ambito operativo).

La ViewModel, prima versione

Iniziamo con una ViewModel ridottissima, quanto basta per l'inizializzazione di un DataContext. Qui a seguire riporto una classe adatta allo scopo, le cui funzionalità sono la creazione di un nuovo ItemData, e l'esposizione - attraverso opportune proprietà - delle proprietà dell'ItemData stesso. Il nocciolo fondamentale di una qualsiasi ViewModel è, come abbiamo visto, l'implementazione dell'interfaccia INotifyPropertyChanged, per tracciare le modifiche effettuate alle proprietà esposte, e poter quindi realizzare efficacemente il binding dei dati.

Imports System.ComponentModel
 
Public Class  ItemDataViewModel
    Implements INotifyPropertyChanged
 
    Dim _item As New  ItemData
 
    Public Property  Text As  String
        Get
            Return _item.Text
        End Get
        Set(value As  String)
            _item.Text = value
            NotifyPropertyChanged()
        End Set
    End Property
 
    Public Property  Background As  Brush
        Get
            Return _item.Background
        End Get
        Set(value As  Brush)
            _item.Background = value
            NotifyPropertyChanged("Background")
        End Set
    End Property
 
    Public Event  PropertyChanged As  PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    Private Sub  NotifyPropertyChanged(Optional propertyName As String  = "")
        RaiseEvent PropertyChanged(Me, New  PropertyChangedEventArgs(propertyName))
    End Sub
End Class

In questa classe, si nota che il primo passo è costituito dalla creazione di un'istanza di ItemData: chiamandone il costruttore, verranno inizalizzate le proprietà di base dell'oggetto, come visto sopra. Si creano quindi le proprietà utiliti ad esporre "sotto-proprietà", ovvero le proprietà della nostra fonte dati. In questo caso, per semplicità, ho mantenuto nel ViewModel lo stesso nome delle proprietà di ItemData. Attraverso di esse, tramite i metodi Get/Set, si realizzano le modifiche ai dati veri e propri.

La View, prima versione

Infine, una View di base: come anticipato, si tratta qui di fare in modo che il DataContext di una ipotetica Window sia il nostro ViewModel, e che su tale finestra sia presente un TextBox, bindato alle proprietà esposte dalla ViewModel. Potremo quindi scrivere una cosa di questo tipo:

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplication1"
    Title="MainWindow" Height="350" Width="525">
     
    <Window.DataContext>
        <local:ItemDataViewModel/>
    </Window.DataContext>
     
    <Grid>
        <TextBox HorizontalAlignment="Left" Height="25" Margin="30,53,0,0" Text="{Binding Text}" Background="{Binding Background}" VerticalAlignment="Top" Width="199"/>
    </Grid>
</Window>

Si noti la dichiarazione del namespace local, che referenzia l'applicazione stessa: senza tale clausola, non sarà possibile, tramite istruzione Window.DataContext, puntare alla classe ItemDataViewModel, definita appunto nel progetto corrente. Successivamente, il TextBox subisce un binding sulle proprietà Text e Background. Le proprietà sorgente sono, naturalmente, quelle del ViewModel. A questo punto, abbiamo tutti gli elementi utili a produrre un primo risultato. Anche senza eseguire il programma, l'IDE ci mostrerà una Window modificata secondo le nostre aspettative, per effetto del binding realizzato. Ovvero, un TextBox contenente il testo "RANDOM TEXT", su sfondo verde.

Ora abbiamo la base che ci serve per iniziare a parlare dei comandi.

L'interfaccia ICommand

Lo scopo generico dei comandi, è quello di mantenere separata la semantica del codice dall'oggetto che ne farà uso. Da un lato, quindi, avremo una rappresentazione grafica del comando stesso (p.es.: un Button), mentre nella nostra ViewModel risiederà il set di istruzioni vero e proprio, eseguito - se si verificano le condizioni stabilite dallo sviluppatore - quando l'utente invia un input al comando grafico. In questo esempio, arricchiremo quindi la ViewModel con alcune sottoclassi, utili ad esporre i comandi per il binding alla View: nulla però vieterebbe di creare classi dedicate, da referenziare in seguito all'interno del modello visuale.

L'implementazione di ICommand prevede quattro elementi fondamentali: un costruttore, le funzioni CanExecute() ed Execute(), e la gestione dell'evento CanExecuteChanged. Il suo esempio più basilare può essere quindi espresso così:

Public Class  Command
     Implements ICommand
 
     Private _action As Action
 
     Sub New(action As  action)
         _action = action
     End Sub
 
     Public Function  CanExecute(parameter As Object) As  Boolean Implements  ICommand.CanExecute
         Return True
     End Function
 
     Public Event  CanExecuteChanged(sender As Object, e As  EventArgs) Implements ICommand.CanExecuteChanged
 
     Public Sub  Execute(parameter As  Object) Implements ICommand.Execute
         _action()
     End Sub
End Class

Definiamo cioè una classe che chiameremo Command, che implementa ICommand ed i suoi elementi caratteristici ed obbligatori. Le variabili di tipo Action rappresentano, secondo la natura di tale tipo, degli incapsulamenti di metodi (Delegati). Detto in altri termini, quando inizializzeremo il costruttore del nostro Command, dovremo indicargli quale sub o funzione richiamare, e a questo scopo gli passeremo appunto una referenza al Delegato desiderato. Tale referenza verrà eseguita su chiamata del metodo Execute(). Il metodo CanExecute() è utile quanto si desidera definire le condizioni in cui un tale comando è disponibile. Nell'esempio, ritornando un valore True, supponiamo lo sia sempre.

Definita questa basilare implementazione, dovremo integrarla al nostro ViewModel: in questo esempio, supponiamo di voler introdurre un comando alla cui esecuzione lo sfondo dell'elemento diventa rosso. Sulla View, ciò sarà demandato alla pressione di un tasto. Dunque, potremo modificare il nostro ViewModel come segue:

Imports System.ComponentModel
Public Class  ItemDataViewModel
    Implements INotifyPropertyChanged
 
    Dim _item As New  ItemData
 
    Public Property  Text As  String
        Get
            Return _item.Text
        End Get
        Set(value As  String)
            _item.Text = value
            NotifyPropertyChanged()
        End Set
    End Property
 
    Public Property  Background As  Brush
        Get
            Return _item.Background
        End Get
        Set(value As  Brush)
            _item.Background = value
            NotifyPropertyChanged("Background")
        End Set
    End Property
 
    Public Event  PropertyChanged As  PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    Private Sub  NotifyPropertyChanged(Optional propertyName As String  = "")
        RaiseEvent PropertyChanged(Me, New  PropertyChangedEventArgs(propertyName))
    End Sub
 
    Dim _cmd As New  Command(AddressOf  MakeMeRed)
 
    Public ReadOnly  Property FammiRosso As ICommand
        Get
            Return _cmd
        End Get
    End Property
 
    Private Sub  MakeMeRed()
        Me.Background = New  SolidColorBrush(Colors.Red)
    End Sub
 
    Public Class  Command
        Implements ICommand
 
        Private _action As Action
 
        Sub New(action As  action)
            _action = action
        End Sub
 
        Public Function  CanExecute(parameter As Object) As  Boolean Implements  ICommand.CanExecute
            Return True
        End Function
 
        Public Event  CanExecuteChanged(sender As Object, e As  EventArgs) Implements ICommand.CanExecuteChanged
 
        Public Sub  Execute(parameter As  Object) Implements ICommand.Execute
            _action()
        End Sub
    End Class
End Class

Si noti come la classe Command sia stata inserita direttamente come sottoclasse (procedura non obbligatoria, ma utile per me a presentare un esempio meno dispersivo). La ViewModel dichiara una variabile _cmd come nuovo Command, passando ad esso un delegato (AddressOf) della Sub privata MakeMeRed. Cosa fa questa sub? Semplicemente, come si vede, prende la proprietà Background dell'oggetto corrente (a sua volta collegata a quella dell'ItemData sottostante), e la valorizza con un Brush rosso. Come da prassi per MVVM, il comando deve essere esposto come proprietà: ecco quindi creata la proprietà FammiRosso, che espone il nostro Command all'esterno del ViewModel.

Si può quindi procedere all'introduzione del comando sulla View. Modifichiamo quindi lo XAML della nostra finestra in questo modo:

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplication1"
    Title="MainWindow" Height="350" Width="525">
     
    <Window.DataContext>
        <local:ItemDataViewModel/>
    </Window.DataContext>
     
    <Grid>
        <TextBox HorizontalAlignment="Left" Height="25" Margin="30,53,0,0" Text="{Binding Text}" Background="{Binding Background}" VerticalAlignment="Top" Width="199"/>
        <Button Content="Button" Command="{Binding FammiRosso}" HorizontalAlignment="Left" Height="29" Margin="30,99,0,0" VerticalAlignment="Top" Width="199"/>
    </Grid>
</Window>

In un controllo Button, il binding ad un comando si effettua sulla proprietà Command. Si noti come sia sufficiente, dal momento che il DataContext della finestra è su ItemDataViewModel, indicare la proprietà appena creata legata al comando: Command="{Binding FammiRosso} è tutto ciò che serve. Se eseguiamo ora il nostro esempio, vedremo inizialmente un colore di sfondo TextBox verde, dato dal costruttore di classe. Alla pressione del nostro Button, il colore del controllo testuale cambierà:

A questo punto è utile notare, ancora una volta, l'indipendenza tra i controlli grafici e la logica sottostante. Con il consueto modello di programmazione ad eventi, avremmo dovuto scrivere le istruzioni di cambio colore nel code-behind della finestra, mentre, grazie a MVVM, i files che definiscono la UI possono viaggiare indipendentemente da quelli che stabiliscono la logica di funzionamento, evitando che una modifica in una delle due determini la necessità di modificare anche la controparte (a meno che non si vada a variare un elemento di contatto fondamentale: per esempio, la rinomina di una proprietà utilizzata fino a quel momento. Ad ogni modo, anche in questo caso i successivi interventi di adeguamento saranno senz'altro più contenuti).

Comandi parametrizzati

In un qualsiasi scenario, un comando statico potrebbe non essere sufficiente a soddisfare i requisiti operativi. Pensiamo ad un comando che debba svolgere operazioni differenti a seconda dello stato di un altro controllo. Nasce quindi l'esigenza di poter creare dei comandi parametrizzati, ovvero che accettino parametri dalla UI, e che sulla base di questo possano eseguire compiti diversi.

Nel nostro esempio, vogliamo implementare un bottone aggiuntivo. Anch'esso dovrà far cambiare colore allo sfondo del TextBox, ma tale colore dovrà essere quello indicato dall'utente nella TextBox. La proprietà Text dell'ItemData deve cioè essere passata al nostro comando come parametro. L'implementazione di base di un comando parametrizzato differisce veramente poco da quella vista sopra. Potremo riassumerla così:

Public Class  ParamCommand
    Implements ICommand
 
    Private _action As Action(Of String)
 
    Sub New(action As  Action(Of String))
        _action = action
    End Sub
 
    Public Function  CanExecute(parameter As Object) As  Boolean Implements  ICommand.CanExecute
       Return True
    End Function
 
    Public Event  CanExecuteChanged(sender As Object, e As  EventArgs) Implements ICommand.CanExecuteChanged
 
    Public Sub  Execute(parameter As  Object) Implements ICommand.Execute
       _action(parameter.ToString)
    End Sub
End Class

Si noti che l'unica differenza dalla precedente implementazione (oltre al nome della classe), risiede nell'aver specificato un tipo per il delegato Action. Ora abbiamo un Action(Of String), sia nella dichiarazione della variabile, che - ovviamente - nel costruttore e nella sintassi di lancio nel metodo Execute. Si dichiara cioè un delegato di un qualsiasi tipo desiderato (perchè io abbia scelto String sarà chiaro tra poco), che farà riferimento ad un metodo i cui argomenti sono congruenti a tale tipo.

Allo stesso modo dell'esempio precedente, necessiteremo quindi che nella nostra ItemDataViewModel venga realizzata l'implementazione di tale comando, e la sua esposizione a mezzo proprietà. Aggiungendo quindi alla classe ViewModel la nuova classe riferita al comando parametrizzato, avremo:

Imports System.ComponentModel
 
Public Class  ItemDataViewModel
    Implements INotifyPropertyChanged
 
    Dim _item As New  ItemData
 
    Public Property  Text As  String
        Get
            Return _item.Text
        End Get
        Set(value As  String)
            _item.Text = value
            NotifyPropertyChanged()
        End Set
    End Property
 
    Public Property  Background As  Brush
        Get
            Return _item.Background
        End Get
        Set(value As  Brush)
            _item.Background = value
            NotifyPropertyChanged("Background")
        End Set
    End Property
 
    Public Event  PropertyChanged As  PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    Private Sub  NotifyPropertyChanged(Optional propertyName As String  = "")
        RaiseEvent PropertyChanged(Me, New  PropertyChangedEventArgs(propertyName))
    End Sub
 
    Dim _cmd As New  Command(AddressOf  MakeMeRed)
    Dim _cmdP As New  ParamCommand(AddressOf EmitMessage)
 
    Public ReadOnly  Property FammiRosso As ICommand
        Get
            Return _cmd
        End Get
    End Property
 
    Public ReadOnly  Property Message As ICommand
        Get
            Return _cmdP
        End Get
    End Property
 
    Private Sub  MakeMeRed()
        Me.Background = New  SolidColorBrush(Colors.Red)
    End Sub
 
    Private Sub  EmitMessage(message As String)
        Try
            Me.Background = New  SolidColorBrush(CType(ColorConverter.ConvertFromString(message), Color))
        Catch
            MakeMeRed()
        End Try
        MessageBox.Show(message)
    End Sub
 
    Public Class  Command
        Implements ICommand
 
        Private _action As Action
 
        Sub New(action As  action)
            _action = action
        End Sub
 
        Public Function  CanExecute(parameter As Object) As  Boolean Implements  ICommand.CanExecute
            Return True
        End Function
 
        Public Event  CanExecuteChanged(sender As Object, e As  EventArgs) Implements ICommand.CanExecuteChanged
 
        Public Sub  Execute(parameter As  Object) Implements ICommand.Execute
            _action()
        End Sub
    End Class
 
    Public Class  ParamCommand
        Implements ICommand
 
        Private _action As Action(Of String)
 
        Sub New(action As  Action(Of String))
            _action = action
        End Sub
 
        Public Function  CanExecute(parameter As Object) As  Boolean Implements  ICommand.CanExecute
            Return True
        End Function
 
        Public Event  CanExecuteChanged(sender As Object, e As  EventArgs) Implements ICommand.CanExecuteChanged
 
        Public Sub  Execute(parameter As  Object) Implements ICommand.Execute
            _action(parameter.ToString)
        End Sub
    End Class
End Class

Si noti la dichiarazione della classe ParamCommand, la dichiarazione di una variabile _cmdP di tipo ParamCommand, con riferimento al delegato EmitMessage, subroutine che accetta un argomento di tipo String. Tale routine cerca di convertire l'argomento in un colore, e di assegnarlo alla proprietà Background della classe ItemData. Allo stesso tempo, si occuperà dell'emissione di una MessageBox, contenente l'argomento stesso. L'esposizione del comando parametrizzato avverrà mediante la proprietà Message. È ora chiaro che il parametro del comando, passato alla routine, dovrà essere - per quanto ci siamo proposti - la proprietà esposta Text, in modo che essa venga digitata dall'utente nella TextBox, recepita dal comando parametrizzato, ed utilizzata nel contesto della routine delegata.

Per realizzare tale binding, implementiamo il tasto sulla View, il cui codice XAML finale diventerà:

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplication1"
    Title="MainWindow" Height="350" Width="525">
     
    <Window.DataContext>
        <local:ItemDataViewModel/>
    </Window.DataContext>
     
    <Grid>        
        <TextBox HorizontalAlignment="Left" Height="25" Margin="30,53,0,0" Text="{Binding Text}" Background="{Binding Background}" VerticalAlignment="Top" Width="199"/>
        <Button Content="Button"  Command="{Binding FammiRosso}" HorizontalAlignment="Left" Height="29" Margin="30,99,0,0" VerticalAlignment="Top" Width="199"/>
        <Button Content="Button" Command="{Binding Message}" CommandParameter="{Binding Text}" HorizontalAlignment="Left" Margin="30,133,0,0" VerticalAlignment="Top" Width="199" Height="29"/>
    </Grid>
</Window>

Si noti, come in precedenza, il binding su Command della proprietà che, in ViewModel, espone il comando parametrizzato. Per passare il nostro argomento, sarà sufficiente valorizzare la proprietà CommandParameter del Button con una referenza di binding alla proprietà da collegare. In questo caso, CommandParameter="{Binding Text}" indica che il parametro passato al comando dovrà essere ricavato dalla proprietà bindata Text.

Possiamo quindi eseguire il codice, ed inserire diversi valori nella TextBox (purchè appartenenti alla classe System.Windows.Media.Colors, ovvero Red, Black, Green, ecc.), premendo ad ogni volta il tasto, e constatando la modifica della proprietà Background e l'emissione della MessageBox. In caso di valore non valido, come si nota dal codice del metodo EmitMessage(), il colore imposto sarà il rosso.