Correva l'anno 1979, più o meno un paio di ere geologiche fa parlando in termini di evoluzione informatica, quando Trygve Mikkjel Heyerdahl Reenskaug, uno studioso informatico Norvegese dell'epoca, lavorando ad una sperimentazione presso il laboratori dello Xerox Palo Alto Research Center enunciò una proposta che avrebbe dovuto semplificare notevolmente la realizzazione di interfacce utente.
L'idea in realtà era una delle dirette conseguenze di una sua precedente pubblicazione, "Administrative Control in the Shipyard" risalente addirittura al 1973, in cui egli teorizzò, nel corso della prima edizione della International Conference on Computing Applications, quello che oggi tutti noi conosciamo e che va sotto il nome di "Separation of Concerns". Ci vollero tuttavia 6 anni per arrivare all'enunciazione di un nuovo pattern architetturale perchè a quel tempo la programmazione orientata agli oggetti non era nulla di più di una promettente materia di studio e fu necessario attendere che un linguaggio come Smalltalk fornisse degli strumenti adeguati per sentire i primi vagiti del pattern Model-View-Controller.
Nonostante MVC sia tuttora un pattern architetturale di riconosciuta efficacia, tanto che moderne tecnologie quali ASP.NET, Java e Ruby ne abbracciano o ne stanno abbracciando i dettami, esistono casi in cui la sua struttura comincia a risentire della veneranda età e richiede nuove e più efficaci evoluzioni.
È il caso ad esempio di tecnologie quali WPF e Silverlight in cui la presenza di strumenti quali il DataBinding
suggerisce che l'adozione di MVC sia sostanzialmente un passo indietro rispetto le potenzialità della tecnologia, dato che la rinuncia al loro utilizzo è addirittura più penalizzante in termini di produttività che la rinuncia al pattern stesso. Ecco spiegato perchè recentemente si è cominciato a parlare e ad usare un nuovo pattern che va sotto il nome di Model-View-ViewModel.
Da MVC a MVVM
Il pattern Model-View-Controller porta per la prima volta il paradigma della separazione di compiti, tuttora estremamente importante nel campo della progettazione del software, al livello più vicino all'utente finale ovvero l'interfaccia utente. Fondamentalmente il pattern determina che esistono tre componenti che danno vita all'interfaccia utente:
Componente | Descrizione |
---|---|
Model | la rappresentazione dei dati che dovranno essere presentati all'utente. Questa parte del pattern la possiamo immaginare come una serie di metodi cui la View attinge per visualizzare l'interfaccia (query) e altri metodi usati dal controller per modificare i dati (mutators). Il model è anche in grado di notificare la view che qualcosa è cambiato e provocare l'aggiornamento dell'interfaccia |
View | la parte a diretto contatto con l'utente. Pensiamola in termini di finestre, pagine, componenti, etc. Il compito della view è semplicemente di visualizzare le informazioni che provengono dal Model e raccogliere l'interazione dell'utente per inoltrarle al Controller |
Controller | si tratta della parte che coordina la View e il Model applicando la logica necessaria. In buona sostanza il controller riceve le azioni effettuate sulla View e compie le dovute operazioni sul Model facendo uso dei metodi di tipo mutator |
Una volta digeriti questi concetti, allo scopo di comprendere l'evoluzione verso MVVM, è necessario rendersi conto che in MVC la comunicazione tra View, Controller e Model è fatta semplicemente di chiamate a metodi e/o proprietà, da una parte e/o dall'altra. Ad esempio, se l'utente preme il pulsante "Aggiungi prodotto
", la View chiamerà il relativo metodo AddProductClick()
sul controller. Quest'ultimo opererà sul Model per completare l'operazione. Al termine il model notifica la View che recupera le modifiche chiamando i metodi o le proprietà necessarie così da visualizzare il prodotto appena aggiunto.
Chi programma in WPF o Silverlight sa bene invece che quando l'applicazione è ben strutturata non vi è alcuna necessità di arrivare a modificare direttamente le componenti della user-interface, quali TextBox
, CheckBox
, etc. La modifica invece si applicherà direttamente sul dato collegato all'interfaccia utente mediante DataBinding e il runtime si occuperà per noi di aggiornare i controlli che visualizzano questa informazione e viceversa di riportare nelle proprietà i valori modificati dall'utente nel caso contrario.
Potremo quindi dire che, nel caso di Silverlight e WPF, una parte del controller è la fonte dati - il DataContext
per intenderci - per la View ed è connesso ad essa esclusivamente per mezzo di DataBinding. La comunicazione sarà perciò sempre tra View e Controller e tra Controller e Model ma in realtà una parte del Model (quello soggetto al databinding) sarà esposto dal Controller.
Quello che abbiamo appena descritto, altro non è che il pattern Model-View-ViewModel che per molti versi è un dialetto del MVC ma che differisce sostanzialmente da esso per il fatto di essere conscio dell'esistenza dei uno strumento come il DataBinding e trarne vantaggio. La differenza quindi è soprattuto nella modalità di accoppiamento tra Controller (qui chiamato ViewModel proprio perchè incapsula una parte del Model) e la View che se in una direzione prende la forma del DataBinding nell'altra si estrinseca nel passaggio di comandi, cioè nell'uso della caratteristica peculiare di WPF che consente il routing di comandi all'interno dell'interfaccia. Ed è proprio questo il punto in cui Silverlight e WPF divergono, perchè il concetto di comando non esiste in Silverlight. Almeno non nativamente.
Un esempio pratico
Ora che sono stati delineati i concetti che governano il funzionamento del pattern MVVM vale la pena di provare a realizzare una semplicissima applicazione il cui scopo sia di dimostrare come occorre strutturare classi, proprietà e quant'altro. L'applicazione si occuperà di visualizzare un elenco di prodotti e quindi innanzitutto dimostrerà come raccogliere i dati dal model e visualizzarli all'utente.
Prima di tutto quindi, partendo dal basso, il Model espone un metodo per estrarre i prodotti secondo un criterio ben definito. Nel caso dell'esempio in questione il metodo accetta una stringa in ingresso con lo scopo di simulare una ricerca; la stringa verrà usata in seguito per selezionare i prodotti che la contengono.
public class ProductRepository : IProductRepository
{
public void GetProductsByKeyword(
string searchKey,
Action<IEnumerable<Product>> success,
Action<Exception> fail)
{
// qui chiamo un WebService oppure WCF Ria Services
ProductsClient client = new ProductsClient();
client.GetProductsByKeyword +=
(s, e) =>
{
// qui ho il risultato...
if (e.Error != null) fail(e.Error);
// ...e lo ritorno
success(e.Result);
};
client.GetProductsByKeywordAsync(searchKey);
}
}
A prima vista può sembrare strano il modo con cui viene ritornato il risultato del metodo. Bisogna però tenere in considerazione che in Silverlight l'accesso ai dati sarà sempre mediato da un WebService, piuttosto che da un Ria Service e questa operazione sarà sempre asincrona. Strutturare il metodo in questo modo consente di trarre vantaggio dalle lambda expressions e mantenere il codice più compatto senza dover ricorrere a delegate ed eventi.
Il ViewModel
Il ViewModel è la classe che fa da connettore tra View e Model. Nel caso dell'esempio dovrà esporre delle proprietà contenenti i dati da visualizzare nell'interfaccia. Essa inoltre si farà carico di chiamare il Model per ottenere da esso i prodotti che rispettano la query. Il Model verrà passato al ViewModel come argomento, usando l'interfaccia da esso implementata. Questo ci consentirà poi di usare la Dependency Injection per rendere testabile il ViewModel.
public class ProductViewModel
{
private IProductRepository Repository { get; set; }
public ObservableCollection<Product> Products { get; set; }
public ProductViewModel(IProductRepository repository)
{
this.Repository = repository;
this.Products = new ObservableCollection<Product>();
// carico i prodotti dal repository
this.Load(string.Empty);
}
private void Load(string keyword)
{
this.Repository.GetProductsByKeyword(
keyword,
BindProducts,
Utilities.PublishException);
}
private void BindProducts(IEnumerable<Product> result)
{
this.Products.Clear();
foreach (Product prd in result) this.Products.Add(prd);
}
}
La classe è piuttosto semplice e comprensibile. Nel costruttore si inizializza la proprietà Products
, dichiarata come ObservableCollection<Product>
e poi si chiama il metodo che caricherà i prodotti dal database. All'arrivo dei prodotti, quando il caricamento è completo, essi sono aggiunti alla collection.
La parte importante da tenere presente è l'uso di ObservableCollection. Tale classe infatti implementando INotifyCollectionChanged
è in grado di notificare quando viene modificato il suo contenuto. In questo modo il ViewModel può essere iniettato nel DataContext
delle View e la sua proprietà "Bindata" ad uno specifico controllo. Vediamo il markup XAML:
<UserControl x:Class="MyApplication.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"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
<Grid x:Name="LayoutRoot">
<ItemsControl ItemsSource="{Binding Products}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl>
La View
La view, come già detto, altro non è che una parte dell'interfaccia. In questo caso, e nella stragrande maggioranza dei casi, si tratta di uno UserControl, ma può essere anche una ChildWindow
oppure spesso anche solamente una riga di una DataGrid
.
Questo esempio di markup contiene un ItemsControl, cioè un controllo che è in grado di visualizzare il contenuto di una collection. Esso infatti è collegato alla proprietà "Products" del ViewModel. L'ultima cosa che manca alla prima parte dell'esempio è come fare a collegare tra loro le tre componenti di questo piccolo ecosistema.
Fondamentalmente effettueremo questa operazione ovunque sia necessario istanziare una nuova View. Talvolta questa operazione sarà supportata dall'uso di speciali servizi "container" che sono in grado di istanziare le varie parti grazie a opportune configurazioni. È il caso ad esempio degli Inversion of Control containers. Tuttavia in questo esempio ci limiteremo a scrivere qualche semplice riga di codice.
private void Application_Startup(object sender, StartupEventArgs e)
{
IProductRepository repository = RepositoryFactory.CreateProduct();
ProductViewModel vm = new ProductViewModel(repository);
this.RootVisual = new MainPage { DataContext = vm };
}
In questo snippet dapprima si istanzia il Model. In seguito esso è associato al ViewModel mediante il costruttore della classe. Per come è fatto il ViewModel, questa operazione provocherà il caricamento dei prodotti. Quindi si associa il ViewModel impostando la proprietà DataContext della View. Il risultato sarà che grazie al DataBinding la View verrà popolata con i prodotti provenienti dal Model.
Dalla View al ViewModel: Prism
Con la parte precedente dell'esempio abbiamo preso in considerazione solamente metà del problema. Infatti se è chiaro come attingere alla sorgente dati per popolare la View, non è altrettanto chiaro come intercettare l'interazione dell'utente per provocare modifiche ai dati.
Poniamo ad esempio di voler aggiungere una TextBox e un Pulsante per fare in modo di consentire all'utente di impostare la chiave di ricerca e avviare una nuova ricerca. Il problema, come già anticipato, è che in Silverlight non esiste il concetto di "comando" che invece è presente in WPF.
Per poter chiudere il cerchio dobbiamo fare uso di una libreria esterna rilasciata da Microsoft chiamata Prism. All'interno della libreria si trova una classe DelegateCommand
che ci servirà allo scopo.
Una volta referenziato l'assembly Microsoft.Practices.Composite.Presentation.dll
è possibile modificare opportunamente il ViewModel. Si aggiungono innanzitutto due proprietà:
public string SearchKey { get; set; }
public DelegateCommand<object> SearchCommand { get; set; }
La prima riceverà il testo digitato nella TextBox. La seconda invece è l'istanza del DelegateCommand
che corrisponde all'azione della ricerca. Questo consente di usare il DataBinding anche per collegare il comando all'apposito controllo garantendo un disaccoppiamento fortissimo. Ma una cosa alla volta. Ora è necessario esaminare la logica necessaria:
public ProductViewModel(IProductRepository repository)
{
this.Repository = repository;
this.Products = new ObservableCollection<Product>();
this.SearchCommand = new DelegateCommand<object>(this.Search);
this.Load(string.Empty);
}
private void Search(object payload)
{
this.Load(this.SearchKey);
}
Il DelegateCommand
viene inizializzato con un riferimento ad un metodo che effettua la ricerca. In questo modo, ogni volta che il comando è ricevuto dall'interfaccia, la collection Products
viene ricaricata con una diversa selezione di prodotti e questa verrà mostrata nell'interfaccia. Ecco infine il codice di markup modificato:
<UserControl x:Class="SilverlightApplication1.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"
xmlns:cmd="clr-namespace:Microsoft.Practices.Composite.Presentation.Commands;assembly=Microsoft.Practices.Composite.Presentation"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
<Grid x:Name="LayoutRoot">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
</Grid>
<ItemsControl ItemsSource="{Binding Products}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Grid.Row="1">
<TextBlock Text="{Binding SearchKey, Mode=TwoWay}" Width="100" />
<Button cmd:Click.Command="{Binding SearchCommand}" Content="Search" />
</StackPanel>
</Grid>
</UserControl>
La parte interessante da notare è l'uso di una Attached Property per collegare il comando al pulsante. Prism purtroppo definisce un solo tipo di attached property con cui si può collegare un comando all'evento click di un pulsante. È tuttavia possibile creare comandi personalizzati per qualsiasi tipo di evento. Il tipo dichiarato in T è quello del parametro (qui inutilizzato) che si può specificare usando la proprietà CommandParameter alla stregua del comando stesso.
Quando usare MVVM?
Al termine di questo breve excursus sul pattern MVVM, che è solo l'incipit ad altre necessarie puntata successive, molti potrebbero chiedersi quando usare il pattern e quali sono i motivi scatenanti che ne consigliano l'uso.
Fondamentalmente io individuo tre motivi per prendere in considerazione l'uso del pattern:
- Testabilità: Ad oggi quello che viene considerato il miglior motivo per l'adozione del pattern MVVM è la possibilità di applicare test automatizzati al ViewModel. In questo proposito l'adozione di MVVM è solamente la punta dell'iceberg in quanto si dovrà in effetti pensare tutta l'applicazione per essere testabile e adottare strumenti di IoC.
- Separazione dei ruoli: In una moderna azienda lo sviluppo di interfacce efficaci non può essere demandato unicamente alla buona volontà dello sviluppatore. Sarà necessaria la collaborazione di un Designer, esperto di usabilità e l'uso del pattern MVVM consente una netta divisione dei compiti e quindi una più proficua collaborazione.
- Migliore struttura: Ovviamente nello sviluppo di applicazioni medio grandi l'adozione di MVVM consente una migliore organizzazione del codice e di conseguenza migliore riutilizzo, manutenibilità etc.
Qualora nessuna di queste condizioni sia verificata il mio consiglio è di non usare il pattern MVVM altrimenti vi troverete ad aver a che fare con un codice eccessivamente logorroico e molto frammentato. Non avrete alcun beneficio dall'uso del pattern ma sicuramente numerosi mal di testa nel cercare quello di cui avete bisogno.