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.