Guest post: Il pattern MVVM nelle Universal Windows app - La pratica
Questo post è stato scritto da Matteo Pagani , Support Engineer in Microsoft per il programma AppConsult
Continuiamo il nostro percorso alla scoperta del pattern MVVM nelle Universal Windows app: dopo aver appreso le basi teoriche nel post precedente, ora iniziamo a sporcarci le mani scrivendo un po’ di codice. Come anticipato la volta scorsa, sfrutteremo MVVM Light per aiutarci nell’implementazione del pattern: si tratta, infatti, della libreria più semplice tra quelle disponibili, che ci permetterà di capire meglio i concetti fondamentali durante la creazione dell’applicazione.
La struttura del progetto
MVVM è un pattern che, come obiettivo principale, ha quello di aiutare lo sviluppatore a organizzare al meglio il proprio progetto. Di conseguenza, il primo passaggio è quello di creare una struttura che, anche a livello logico, rispetti questa organizzazione. Il mio suggerimento, perciò, è di creare tre cartelle, che andranno a contenere le classi che compongono le tre componenti:
· Models, in cui includere tutte le entità base.
· ViewModels, in cui includere i ViewModel che faranno da punto di contatto con la View
· Views, in cui includere le View, ovvero le pagine XAML.
Ovviamente, il progetto non conterrà solamente queste tre cartelle: ce ne potranno essere altre per gli asset, per i servizi, per eventuali classi di supporto. Un progetto basato su MVVM, però, tipicamente deve avere almeno queste tre cartelle.
Il secondo passaggio, per il nostro progetto di esempio, è installare la libreria di supporto che abbiamo scelto, nel nostro caso MVVM Light. La troviamo su NuGet, in due varianti:
· Il pacchetto completo che, oltre alle librerie, aggiunge automaticamente una serie di asset, come un ViewModel, la documentazione, ecc.
· Solo la libreria: questo pacchetto andrà ad aggiungere solamente, tra le reference del progetto, le DLL necessarie.
Per il momento il consiglio è di utilizzare il secondo pacchetto, così da non avere nulla di già pronto e poter capire meglio i dettagli implementativi del pattern.
Come progetto di esempio andremo a realizzare un’applicazione molto semplice che, probabilmente, in code-behind sapreste creare in pochissimo tempo: una pagina con una casella di testo, all’interno della quale inserire il vostro nome, e un pulsante che, se premuto, mostrerà un messaggio di saluto.
Nella sua semplicità, però, questo progetto ci permetterà di mettere in pratica tutto quello che abbiamo imparato nel post precedente.
Collegare ViewModel e View
Il primo passo è identificare le tre componenti dell’applicazione: Model, View e ViewModel.
· Il model, in realtà, in questo scenario non è necessario: l’applicazione non ha bisogno di manipolare alcuna entità.
· La view sarà costituita da una sola pagina, che mostrerà all’utente la casella di testo dove inserire il proprio nome e il pulsante per mostrare il messaggio di saluto.
· Il ViewModel sarà costituito dalla classe che andrà a gestire le interazioni con la view: recupererà il nome inserito dall’utente e lo elaborerà per essere mostrato all’interno del messaggio di saluto.
Iniziamo perciò a creare i vari componenti: aggiungiamo, all’interno della cartella Views, la pagina principale della nostra applicazione. Come impostazione predefinita, un progetto per una Universal Windows app ne include già una (il file MainPage.xaml) all’interno della root. Eliminiamola pure e ricreiamola all’interno della cartella Views, facendoci clic con il tasto destro in Solution Explorer e scegliendo Add -> New Item -> Blank page.
Ora creiamo il ViewModel che si farà carico di gestire questa pagina. Come spiegato nel post precedente, il ViewModel non è nient’altro che una normalissima classe: facciamo, perciò, tasto destro in Solution Explorer sulla cartella ViewModels e scegliamo Add -> New Item -> Class.
Ora dobbiamo collegare le due componenti: lo facciamo tramite il DataContext. Vi ricordo, infatti, che alla base dell’implementazione del pattern c’è il fatto che il ViewModel venga assegnato come DataContext di una pagina. In questo modo, i controlli definiti nella View saranno in grado di accedere a tutte le proprietà ed a tutti i comandi definiti nel ViewModel. Ci sono diversi approcci per raggiungere questo scopo. Vediamoli brevemente.
Usare le risorse
Una prima opzione è quella di dichiarare il ViewModel come risorsa dell’applicazione (a livello di pagina o a livello globale) e, dopodiché, assegnarla alla proprietà DataContext della classe Page sfruttando la markup extensione StaticResource, esattamente come fareste con una risorsa di tipo visuale (uno stile, un DataTemplate, ecc.).
Ipotizzando di avere creato un ViewModel chiamato MainViewModel, potremmo dichiararlo all’interno della classe App.xaml nel modo seguente:
<Application
x:Class="MVVMSample.MVVMLight.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModel="using:MVVMSample.MVVMLight.ViewModel">
<Application.Resources>
<viewModel:MainViewModel x:Key="MainViewModel" />
</Application.Resources>
</Application>
Il primo passo è quello di dichiarare, come attributo della classe Application, il namespace che contiene il ViewModel (nell’esempio, è MVVMSample.MVVMLight.ViewModel). Dopodichè, all’interno della collezione Resources della classe Application, abbiamo dichiarato come risorsa una nuova istanza della classe MainViewModel, associandola ad una chiave dal medesimo nome.
Ora possiamo associarla alla proprietà DataContext della nostra pagina grazie alla markup expression StaticResource, come nell’esempio seguente:
<Page
x:Class="MVVMSample.MVVMLight.Views.MainPage"
DataContext="{Binding Source={StaticResource MainViewModel}}"
mc:Ignorable="d">
</Page>
Usare un ViewModelLocator
Il secondo approccio è quello di usare una classe definita locator, che si fa carico di “smistare” i ViewModel alle varie pagine. Invece di registrare tutti i ViewModel come risorse globali dell’applicazione (come nel primo approccio), si registra solamente il locator. I ViewModel saranno esposti, sotto forma di proprietà, dal locator e saranno utilizzati per essere associati al DataContext della relativa pagina. Ecco un esempio di locator:
public class ViewModelLocator
{
public ViewModelLocator()
{
}
public MainViewModel Main
{
get
{
return new MainViewModel();
}
}
}
Ecco, invece, come la proprietàdi nome Main del ViewModelLocator venga assegnata come DataContext della pagina collegata:
<Page
x:Class="MVVMSample.MVVMLight.Views.MainPage"
DataContext="{Binding Source={StaticResource Locator}, Path=Main}"
mc:Ignorable="d">
</Page>
La sintassi è simile a quella del primo approccio, con la differenza che, in questo caso, dato che la classe ViewModelLocator espone più proprietà, dobbiamo specificare tramite l’attributo Path quale vogliamo utilizzare.
Questo approccio è più oneroso da mantenere (perché richiede di gestire una classe in più e, ogni volta che creiamo un nuovo ViewModel, dobbiamo ricordarci di registrarlo), ma ha il vantaggio di garantire la massima flessibilità, dato che ci permette di personalizzare il processo di creazione del ViewModel. Con l’approccio precedente, invece, viene semplicemente creata una nuova istanza del ViewModel, senza la possibilità di passare, ad esempio, dei parametri al costruttore.
Nel prossimo post, quando parleremo di dependency injection, avremo la possibilità di capire meglio i vantaggi nell’utilizzo del ViewModelLocator.
Creare il ViewModel
Indipendentemente dall’approccio che abbiamo scelto, al termine del passaggio precedente avremo raggiunto il risultato di aver collegato la View (la nostra pagina che conterrà il form) al ViewModel (che si farà carico, invece, di gestire le interazioni con l’utente).
Iniziamo dal ViewModel e andiamo a definire le proprietà che ci serviranno per realizzare il nostro obiettivo. Se ricordate quanto spiegato nel post precedente, per gestire correttamente i ViewModel abbiamo bisogno di implementare l’interfaccia INotifyPropertyChanged: in caso contrario, ogni qualvolta andassimo a variare una delle proprietà del ViewModel, la nostra View non se ne accorgerebbe e l’utente non vedrebbe alcun cambiamento nell’interfaccia.
Per semplificare l’implementazione di questa interfaccia, MVVM Light ci mette a disposizione una classe base da cui far ereditare i ViewModel chiamata ViewModelBase.
public class MainViewModel : ViewModelBase
{
}
L’implementazione di tale classe ci mette a disposizione un metodo di nome Set() , che possiamo usare nella definizione delle nostre proprietà per propagare le notifiche verso l’interfaccia utente. Vediamo un esempio con un caso reale. La nostra applicazione deve poter raccogliere il nome che l’utente inserirà all’interno di un controllo TextBox: ci servirà, perciò, una proprietà nel nostro ViewModel in cui memorizzarla. Ecco la definizione:
private string _name;
public string Name
{
get
{
return _name;
}
set
{
Set(ref _name, value);
}
}
Come vedete, il codice è molto simile a quello che avevamo visto nel post precedente quando abbiamo introdotto l’interfaccia INotifyPropertyChanged. L’unica differenza è che il metodo Set() offerto da MVVM Light si fa carico di effettuare due operazioni in un colpo solo: associare alla proprietà Name il valore assegnato e inviare una notifica al canale di binding che il valore è cambiato.
Per lo stesso principio, andiamo a creare all’interno del ViewModel una seconda proprietà: quella che conterrà il messaggio da mostrare all’utente.
private string _message;
public string Message
{
get
{
return _message;
}
set
{
Set(ref _message, value);
}
}
Infine, ci serve gestire l’interazione dell’utente: quando viene premuto un pulsante nell’interfaccia grafica, deve essere mostrato il messaggio di saluto. A livello di codice, ciò si traduce nel:
1. Recuperare il valore della proprietà Name.
2. Sfruttare la concatenazione di stringhe per preparare il messaggio di saluto (ad esempio, “Hello Matteo”).
3. Assegnare il risultato alla proprietà Message, che sarà mostrata all’utente mediante un controllo TextBlock.
Se ricordate la teoria spiegata nel post precedente, il meccanismo che ci permette di gestire le interazioni dell’utente con l’interfaccia grafica da un ViewModel sono i command. Andremo, perciò, a sfruttare la classe RelayCommand, che ci mette a disposizione MVVM Light, per creare un comando che traduca i passaggi elencati in precedenza in codice:
private RelayCommand _sayHello;
public RelayCommand SayHello
{
get
{
if (_sayHello == null)
{
_sayHello = new RelayCommand(() =>
{
Message = $"Hello {Name}";
});
}
return _sayHello;
}
}
La sintassi della classe RelayCommand richiede che, come parametro del costruttore, venga passata una Action che rappresenta il codice da eseguire quando il comando viene invocato. E’ quello che facciamo nell’esempio, sfruttando in questo caso gli anonymous method (ovvero dichiariamo un metodo “inline” direttamente all’interno della proprietà, senza farlo al di fuori e, soprattutto, senza assegnarli un nome).
Il codice definito nel metodo è molto semplice: usando la nuova sintassi per la concatenazione di stringhe introdotta in C# 6 e supportata da Visual Studio 2015, prendiamo il contenuto della proprietà Name, ci aggiungiamo il prefisso “Hello” e lo assegniamo alla proprietà Message.
Creare la View
Ora che abbiamo definito le proprietà e i comandi del nostro ViewModel, possiamo passare a realizzare la View. Questa prima fase dovrebbe già avervi fatto capire il grande punto di forza del pattern MVVM: siamo stati in grado di creare la classe che gestisce la logica e l’interazione della nostra pagina senza aver effettivamente scritto una riga di XAML e senza aver definito i controlli che l’utente andrà ad utilizzare. Con il code-behind, questo non sarebbe stato possibile: ad esempio, per recuperare il nome inserito dall’utente all’interno del controllo TextBox, avremmo dovuto prima inserirlo all’interno della pagina e assegnarli un nome tramite la proprietà x:Name. O per definire il metodo che gestisce la visualizzazione del messaggio, avremmo dovuto necessariamente inserire il controllo Button e sottoscrivere l’evento Click.
La realizzazione dell’interfaccia utente non differisce molto da quella che avremmo creato sfruttando il code-behind: la parte di presentazione, infatti, non ha dipendenze con la logica, quindi l’utilizzo del pattern MVVM non cambia il modo in cui definiamo il layout. L’unica vera differenza è che troveremo un uso molto più massiccio del binding, che ci servirà per collegare le proprietà del nostro ViewModel. Ecco come apparirà la nostra View:
<Page
x:Class="MVVMSample.MVVMLight.Views.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
DataContext="{Binding Source={StaticResource Locator}, Path=Main}"
mc:Ignorable="d">
<Grid>
<StackPanel Margin="12, 30, 0, 0">
<StackPanel Orientation="Horizontal" Margin="0, 0, 0, 30">
<TextBox Text="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="300" Margin="0, 0, 20, 0" />
<Button Command="{Binding Path=SayHello}" Content="Click me!" />
</StackPanel>
<TextBlock Text="{Binding Path=Message}" Style="{StaticResource HeaderTextBlockStyle}" />
</StackPanel>
</Grid>
</Page>
Abbiamo incluso tre controlli:
1. Un TextBox, dove l’utente inserirà il proprio nome: lo abbiamo collegato alla proprietà Name del ViewModel. Possiamo notare due particolarità:
a. Abbiamo impostato l’attributo Mode a TwoWay: questo perché non abbiamo bisogno solamente che al variare della proprietà nel ViewModel il controllo si aggiorni, ma anche il contrario. Quando l’utente inserisce del testo nel controllo, deve essere assegnato alla proprietà nel ViewModel.
b. Abbiamo impostato l’attributo UpdateSourceTrigger a PropertyChanged. Questo ci garantisce che, ad ogni variazione del testo (ovvero, ogni volta che l’utente inserisce una lettera), la proprietà Name si aggiorni con il nuovo contenuto. Senza questa configurazione, la proprietà sarebbe aggiornata solo nel momento in cui il controllo TextBox perderebbe il focus.
2. Un Button, che l’utente dovrà premere per vedere il messaggio di saluto. Lo abbiamo collegato al comando SayHello del ViewModel.
3. Un TextBlock, dove l’utente vedrà il messaggio di saluto. Lo abbiamo collegato alla proprietà Message del ViewModel.
Se abbiamo fatto tutto correttamente e lanciamo l’applicazione, vedremo l’applicazione comportarsi nella maniera attesa: dopo aver inserito il nostro nome e premuto il pulsante, apparirà il messaggio di saluto.
Miglioriamo la gestione dei comandi
L’applicazione scritta finora è migliorabile: se l’utente dovesse premere il pulsante senza aver scritto nulla nella casella di testo, apparirebbe solamente il messaggio “Hello”, senza essere seguito da alcun nome. Vogliamo evitare questo comportamento e disabilitare il pulsante se nel controllo TextBox non è stato inserito alcun testo.
Ci viene in aiuto una funzionalità esposta dai comandi che abbiamo visto nel post precedente: la possibilità di definire quando il comando deve essere abilitato o meno. Lo facciamo aggiungendo un secondo parametro in fase di inizializzazione della classe RelayCommand, specificando una funzione che dovrà restituire un valore di tipo bool.
private RelayCommand _sayHello;
public RelayCommand SayHello
{
get
{
if (_sayHello == null)
{
_sayHello = new RelayCommand(() =>
{
Message = $"Hello {Name}";
},
() => !string.IsNullOrEmpty(Name));
}
return _sayHello;
}
}
Come potete notare, la definizione del RelayCommand ora non contiene soltanto le operazioni da eseguire quando il comando viene invocato, ma anche una funzione che restituisce un valore booleano: in questo caso, viene controllato se la proprietà Name contenga un valore vuoto. In caso affermativo, viene restituito false, altrimenti true.
In questo modo, se la proprietà Name dovesse essere vuota (ovvero l’utente non ha inserito alcun valore nella casella di testo), il pulsante sarebbe automaticamente disabilitato. Se provassimo l’applicazione ora potremmo notare però un problema; all’avvio, il pulsante sarebbe correttamente disabilitato, perché di default il controllo TextBox è vuoto. Se, però, scrivessimo qualcosa al suo interno, il pulsante non si abiliterebbe. Questo perché il ViewModel non è in grado di capire, in automatico, quando l’abilitazione del comando SayHello debba essere rivalutata: dobbiamo, perciò, definirlo manualmente, ogni volta che varia la proprietà che stiamo usando come discriminante (nel nostro caso, Name). Nel metodo di set della proprietà Name, perciò, dobbiamo aggiungere una riga di codice, come nell’esempio seguente:
private string _name;
public string Name
{
get
{
return _name;
}
set
{
Set(ref _name, value);
SayHello.RaiseCanExecuteChanged();
}
}
Oltre ad assegnare il valore alla proprietà e a inviare una notifica all’interfaccia grafica con il metodo Set() , chiamiamo il metodo RaiseCanExecuteChanged() del comando SayHello. Questo farà sì che la condizione venga rivalutata nuovamente: se la proprietà Name conterrà del testo, allora il comando sarà riabilitato; se invece dovesse tornare vuota, il comando sarà nuovamente disabilitato.
Se ora lanciaste l’applicazione, potreste notare che il comportamento è quello atteso: se inserite del testo nel controllo TextBox, il pulsante si abiliterà automaticamente. Se, invece, doveste cancellarlo e riportare vuota la casella di testo, il pulsante si disabiliterà.
Nel prossimo post
Nel corso di questo post ci siamo finalmente “sporcati le mani” e abbiamo iniziato a scrivere la nostra prima applicazione basata sul pattern MVVM. Nel corso dei prossimi post vedremo un po’ di scenari avanzati, come lo scambio di messaggi, la dependency injection o l’uso di comandi legati ad eventi secondari. Nel frattempo, potete trovare l’applicazione di esempio realizzata in questo post su GitHub all’indirizzo https://github.com/qmatteoq/UWP-MVVMSamples. Per aiutarvi nel confronto, troverete lo stesso esempio realizzato anche con Caliburn Micro. Happy coding!
Comments
Anonymous
December 11, 2015
Se si ha l'esigenza di inserire elementi della view a Runtime, come è possibile farlo con un implementazione di tipo mvvm???Anonymous
December 17, 2015
Ciao Andrea, tipicamente se si tratta di elementi della View è buona prassi demandare la gestione al code behind, visto che stiamo comunque parlando della parte di presentazione. Eventualmente, si può usare un'architettura a messaggi per far parlare il ViewModel con la View: questo approccio sarà approfondito in uno dei prossimi post, perciò stay tuned :-)