Guest post: Il pattern MVVM nelle Universal Windows app - Dependency injection
Questo post è stato scritto da Matteo Pagani , Support Engineer in Microsoft per il programma AppConsult
Nei post precedenti abbiamo appreso la teoria che guida il pattern MVVM per poi metterla in pratica, in una semplice applicazione di esempio. Nel corso di questo post, invece, vedremo qualche scenario più avanzato, che ci aiuterà a implementare il pattern MVVM in progetti reali e, di conseguenza, più complessi. Prima, però, introduciamo un nuovo progetto di esempio leggermente più articolato, che ci permetterà poi di capire meglio i nuovi concetti che andrò ad introdurre.
Costruiamo un lettore di notizie
La dependency injection è un meccanismo che consente di gestire molto più semplicemente scenari di test, design e refactoring della propria applicazione ed è strettamente legata al pattern MVVM. Prima di entrare nei dettagli tecnici, però, partiamo da un esempio reale, che ci guiderà lungo il percorso.
Ipotizziamo di voler sviluppare un’applicazione in grado di mostrare un elenco di notizie recuperate da un feed RSS. Come abbiamo fatto nel post precedente, il primo passo è quello di separare l’applicazione nelle tre componenti:
· Il model è rappresentato dall’entità base che rappresenta la singola notizia disponibile all’interno del feed.
· La View è rappresentata dalla pagina XAML, che conterrà un controllo ListView per mostrare l’elenco di notizie.
· Il ViewModel si farà carico di recuperare le notizie dal feed RSS, di includerle in una collezione di oggetti e di passarle alla View.
Nell’ottica di separare il più possibile, però, gli strati dell’applicazione, è una buona idea non includere la logica che si fa carico di trasformare il feed RSS in classi e oggetti direttamente all’interno del ViewModel, ma affidare questo compito ad una classe dedicata. Andremo, perciò, a creare una classe di servizio (che chiameremo RssService) che si farà carico di scaricare l’XML del feed RSS, trasformarlo in una collezione di oggetti e restituirlo al ViewModel.
Partiamo, perciò, dalla definizione dal model e creiamo una nuova classe chiamata FeedItem, che rappresenterà la singola notizia:
public class FeedItem
{
public string Title { get; set; }
public string Description { get; set; }
public string Link { get; set; }
public string Content { get; set; }
public DateTime PublishDate { get; set; }
}
Ora possiamo creare il nostro servizio, che si farà carico, grazie a LINQ to XML, di prendere l’RSS di un sito (che è, appunto, un XML) e convertirlo in una serie di oggetti di tipo FeedItem:
public class RssService: IRssService
{
public async Task<List<FeedItem>> GetNews(string url)
{
HttpClient client = new HttpClient();
string result = await client.GetStringAsync(url);
var xdoc = XDocument.Parse(result);
return (from item in xdoc.Descendants("item")
select new FeedItem
{
Title = (string)item.Element("title"),
Description = (string)item.Element("description"),
Link = (string)item.Element("link"),
PublishDate = DateTime.Parse((string)item.Element("pubDate"))
}).ToList();
}
}
Il codice è semplice da decifrare: innanzitutto viene usata la classe HttpClient e il relativo metodo GetStringAsync() per scaricare il contenuto del feed RSS in memoria. Dopodiché, sfruttando LINQ to XML, che è un linguaggio di manipolazione dell’XML incluso nel framework, convertiamo il feed in oggetti, trasformando i singoli nodi dell’XML in proprietà. Ogni nodo item viene convertito in nuovo oggetto FeedItem e ognuna delle informazioni che contiene (title, description, link, ecc.) viene salvata come proprietà.
Ora abbiamo a disposizione una classe che ci permette, dato in ingresso l’URL di un feed RSS, di ottenere in cambio una lista di oggetti che possiamo mostrare e manipolare nella nostro ViewModel. Ecco come potrebbe apparire il ViewModel legato alla pagina principale della nostra applicazione:
public class MainViewModel: ViewModelBase
{
private List<FeedItem> _news;
public List<FeedItem> News
{
get { return _news; }
set { Set(ref _news, value); }
}
private RelayCommand _loadCommand;
public RelayCommand LoadCommand
{
get
{
if (_loadCommand == null)
{
_loadCommand = new RelayCommand(async () =>
{
RssService rssService = new RssService();
List<FeedItem> items = await rssService.GetNews("http://wp.qmatteoq.com/rss");
News = items;
});
}
return _loadCommand;
}
}
}
Se abbiamo letto con attenzione il post precedente, dovrebbe essere semplice comprendere il codice di esempio:
1. Abbiamo definito una proprietà di nome News di tipo List<FeedItem> , all’interno della quale memorizzare l’elenco di notizie, che poi collegheremo ad un controllo nell’interfaccia utente.
2. Abbiamo definito un command, che potremo legare ad un evento (ad esempio, la pressione di un pulsante), che va a creare una nuova istanza della classe RssService e a chiamare il metodo GetNews() che abbiamo definito in precedenza.
Anche la View, ovvero la pagina XAML, è piuttosto semplice da interpretare alla luce di quanto abbiamo appreso nei post precedenti:
<Page
x:Class="MVVMLight.Advanced.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
DataContext="{Binding Source={StaticResource ViewModelLocator}, Path=Main}"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ListView ItemsSource="{Binding Path=News}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=PublishDate}" Style="{StaticResource SubtitleTextBlockStyle}" />
<TextBlock Text="{Binding Path=Title}" Style="{StaticResource TitleTextBlockStyle}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
<Page.BottomAppBar>
<CommandBar>
<CommandBar.PrimaryCommands>
<AppBarButton Icon="Refresh" Label="Load" Command="{Binding Path=LoadCommand}" />
</CommandBar.PrimaryCommands>
</CommandBar>
</Page.BottomAppBar>
</Page>
All’interno della pagina abbiamo incluso:
1. Un controllo ListView la cui proprietà ItemsSource è collegata, tramite binding, alla proprietà News del ViewModel. Quando il ViewModel caricherà all’interno di News l’elenco di notizie, il controllo aggiornerà il proprio stato visivo per mostrarle. Nello specifico, definendo la proprietà ItemTemplate, andremo a mostrare la data di pubblicazione della notizia (PublishDate) e il titolo (Title).
2. Un pulsante nell’application bar inferiore legato, sempre tramite binding, al comando LoadCommand: quando il pulsante sarà premuto, il ViewModel andrà a scaricare le notizie dal feed RSS sfruttando la classe RssService che abbiamo creato in precedenza.
Ora che abbiamo messo in piedi la nostra nuova applicazione, siamo pronti per introdurre alcuni nuovi concetti che ci aiuteranno a migliorarla.
La dependency injection
Ipotizziamo che, ad un certo punto, sorga la necessità di sostituire la classe RssService con una controparte “fittizia” che, invece di recuperare i dati da un RSS reale, sfrutti dei dati finti da mostrare all’interno dell’applicazione. Le motivazioni potrebbero essere tante: ad esempio, il designer deve lavorare sull’interfaccia grafica e abbiamo bisogno di simulare degli scenari non convenzionali (come un titolo della notizia molto lungo), che non è detto che i dati reali attuali possano riprodurre. Oppure pensiamo ad uno scenario più complicato di quello attuale dove, al posto di un semplice RSS, ci sia magari un’infrastruttura cloud o un database che non è detto siano sempre disponibili o raggiungibili.
Raggiungere questo obiettivo nella nostra applicazione è molto semplice. Basta andare a creare una nuova classe (ad esempio, di nome FakeRssService) che restituisca un elenco di oggetti FeedItem fittizi e, all’interno del comando LoadCommand, andare a cambiarne l’inizializzazione, come nell’esempio seguente:
private RelayCommand _loadCommand;
public RelayCommand LoadCommand
{
get
{
if (_loadCommand == null)
{
_loadCommand = new RelayCommand(async () =>
{
FakeRssService rssService = new FakeRssService ();
List<FeedItem> items = await rssService.GetNews("http://wp.qmatteoq.com/rss");
News = new ObservableCollection<FeedItem>(items);
});
}
return _loadCommand;
}
}
Fin qui, è tutto semplice. Ipotizziamo, però, che la nostra applicazione sia molto più complessa di quella che abbiamo realizzato e, invece di avere una pagina collegata da un ViewModel, contenga 30 differenti pagine, ognuna con il suo ViewModel; ognuno di essi potrebbe fare uso della classe RssService e, di conseguenza, nel momento in cui volessimo rimpiazzarla con la versione fittizia, dovremmo andare all’interno di tutti i ViewModel e effettuare la sostituzione. Vi sarà facile immagine come questo approccio sia poco efficiente e molto suscettibile agli errori.
Per questo motivo introduciamo il concetto di dependency injection, che è un meccanismo che ci permette di gestire meglio questo scenario. Il problema precedentemente descritto nasce dal fatto che, tipicamente, in un’applicazione le varie dipendenze di un ViewModel (ovvero le classi che sono indispensabili affinché sia in grado di eseguire tutte le operazioni necessarie) vengono create in fase di compilazione. La dependency injection sposta l’ago della bilancia, demandando la gestione di tali dipendenze a runtime, ovvero mentre l’applicazione è in esecuzione.
Ciò è reso possibile da una classe, che viene definita container, che possiamo immaginare come una grande scatola, all’interno della quale vengono inseriti tutti i ViewModel e tutti i servizi da cui dipendono della nostra applicazione. In fase di esecuzione, quando abbiamo bisogno di un ViewModel (perché la pagina ad esso legata è stata caricata), non lo creeremo più manualmente, ma andremo a chiederlo al container. Lui controllerà, al suo interno, se è in grado di soddisfare tutte le dipendenze e, in caso affermativo, ci passerà un’istanza del ViewModel già pronta con, al suo interno, tutti i servizi di cui abbiamo bisogno. In gergo, si dice che le dipendenze vengono “iniettate” all’interno del ViewModel e non più istanziate in fase di compilazione, da cui il termine dependency injection.
Perché questo approccio ci permette di risolvere il problema descritto poco fa, ovvero la necessità di andare a modificare tutti i ViewModel dell’applicazione se vogliamo modificare l’utilizzo di uno dei servizi? Perché la dipendenza tra ViewModel e servizio viene gestita a livello di container, che è unico in tutta l’applicazione: i ViewModel non istanzieranno più manualmente la classe RssService, ma si limiteranno ad avere un parametro di tipo RssService nel costruttore. Ci penserà il container a passare una nuova istanza della classe ogni qualvolta che, durante l’esecuzione, qualcuno avrà bisogno di un ViewModel. Di conseguenza, nel momento in cui non volessimo più usare la classe RssService ma FakeRssService, dovremo semplicemente andare a cambiare il tipo di servizio registrato nel container e, in automatico, tutti i ViewModel inizieranno ad utilizzarla.
Per predisporre questo nuovo approccio è necessario apportare un po’ di cambiamenti al progetto iniziale. Vediamoli in dettaglio.
Creare un’interfaccia
Il nostro progetto, rispetto allo scenario descritto poco fa, ha un difetto: il ViewModel dipende da RssService, che è una classe concreta. Di conseguenza, non abbiamo modo, anche usando la dependency injection, di cambiare velocemente RssService con un’altra classe. Per farlo, ci serve qualcosa che descriva la classe RssService in maniera astratta, da utilizzare all’interno del ViewModel. È proprio quello a cui servono le interfacce di C#: descrivere una serie di proprietà e metodi, che poi saranno implementati nel concreto in una classe. Andremo, perciò, a creare un’interfaccia che descriva il comportamento generico del nostro servizio di elaborazione del feed RSS. Tale interfaccia sarà poi implementata sia dal servizio reale (la classe RssService) che da quella fittizia (la classe FakeRssService). In questo modo:
· All’interno del ViewModel utilizzeremo l’interfaccia e non la classe concreta.
· All’interno del container, registreremo l’interfaccia specificando quale classe concreta vogliamo usare. In fase di esecuzione, il container inietterà nel ViewModel l’implementazione che abbiamo scelto. Nel momento in cui vogliamo cambiare tale implementazione, ci basterà associare una differente classe all’interfaccia nel container.
Iniziamo, perciò, creando un’interfaccia di nome IRssService che descriva il nostro servizio:
public interface IRssService
{
Task<List<FeedItem>> GetNews(string url);
}
L’interfaccia contiene semplicemente la descrizione di un metodo di nome GetNews() , che riprende quanto abbiamo già visto in precedenza: riceve in ingresso l’URL del feed RSS e restituisce una collezione di oggetti di tipo FeedItem.
Ora dobbiamo cambiare l’implementazione delle nostre classi RssService e FakeRssService per implementare questa interfaccia:
public class RssService : IRssService
{
public async Task<List<FeedItem>> GetNews(string url)
{
HttpClient client = new HttpClient();
string result = await client.GetStringAsync(url);
var xdoc = XDocument.Parse(result);
return (from item in xdoc.Descendants("item")
select new FeedItem
{
Title = (string)item.Element("title"),
Description = (string)item.Element("description"),
Link = (string)item.Element("link"),
PublishDate = DateTime.Parse((string)item.Element("pubDate"))
}).ToList();
}
}
public class FakeRssService: IRssService
{
public Task<List<FeedItem>> GetNews(string url)
{
List<FeedItem> items = new List<FeedItem>
{
new FeedItem
{
PublishDate = new DateTime(2015, 9, 3),
Title = "Sample news 1"
},
new FeedItem
{
PublishDate = new DateTime(2015, 9, 4),
Title = "Sample news 2"
},
new FeedItem
{
PublishDate = new DateTime(2015, 9, 5),
Title = "Sample news 3"
},
new FeedItem
{
PublishDate = new DateTime(2015, 9, 6),
Title = "Sample news 4"
}
};
return Task.FromResult(items);
}
}
Come vedete, entrambi i servizi, dato che implementano la stessa interfaccia, hanno la medesima struttura: l’unica differenza è che, nel primo caso, il metodo GetNews() va a scaricare il contenuto di un RSS reale mentre, nel secondo caso, viene restituito un elenco fittizio di notizie.
Il ViewModel
Il passaggio successivo è quello di modificare il nostro ViewModel con due accorgimenti:
1. Affinchè il sistema di dependency injection funzioni correttamente, non dobbiamo più istanziare manualmente la classe RssService, ma dobbiamo aggiungerlo come dipendenza al costruttore del ViewModel.
2. Dobbiamo modificare il command LoadCommand per utilizzare il servizio recuperato nel costruttore.
Ecco come appare il ViewModel dopo queste modifiche:
public class MainViewModel : ViewModelBase
{
private readonly IRssService _rssService;
public MainViewModel(IRssService rssService)
{
_rssService = rssService;
}
private ObservableCollection<FeedItem> _news;
public ObservableCollection<FeedItem> News
{
get { return _news; }
set { Set(ref _news, value); }
}
private RelayCommand _loadCommand;
public RelayCommand LoadCommand
{
get
{
if (_loadCommand == null)
{
_loadCommand = new RelayCommand(async () =>
List<FeedItem> items = await _rssService.GetNews("http://wp.qmatteoq.com/rss");
News = new ObservableCollection<FeedItem>(items);
});
}
return _loadCommand;
}
}
Il ViewModelLocator
L’ultimo passaggio è quello più importante, che ci permette di mettere insieme l’architettura della dependency injection: creare il container e registrare al suo interno i nostri ViewModel e servizi.
Tale configurazione viene fatta, solitamente, in fase di avvio dell’applicazione: per semplicità, lo facciamo all’interno della classe ViewModelLocator, dato che è quella che si fa carico di istanziare e gestire i vari ViewModel.
Ecco come appare il ViewModelLocator con il supporto alla dependency injection:
public class ViewModelLocator
{
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<IRssService, RssService>();
SimpleIoc.Default.Register<MainViewModel>();
}
public MainViewModel Main
{
get { return ServiceLocator.Current.GetInstance<MainViewModel>(); }
}
}
Innanzitutto, è bene precisare come, in rete, esistano tantissime librerie per l’implementazione della dependency injection: tra le più celebri citiamo Ninject e Castle Winsdor. Tutti le principali librerie dedicate al pattern MVVM, però, solitamente includono loro stesse una serie di classi che permettono di gestire questa funzionalità. MVVM Light non fa eccezione, includendo un container rappresentato dalla classe SimpleIoc, che viene impostato tramite il metodo SetLocatorProvider() della classe ServiceLocator come container predefinito per l’applicazione.
Il passaggio successivo è registrare, all’interno del container, tutti i nostri servizi e ViewModel. Lo facciamo sfruttando il metodo Register() della classe SimpleIoc, che accetta due varianti:
1. La variante Register<T>() , che viene utilizzata quando abbiamo semplicemente bisogno di una nuova istanza della classe e, di conseguenza, non ci serve avere un’interfaccia che la descriva. È il caso del nostro ViewModel.
2. La variante Register<T, Y>() , che viene utilizzata quando invece dobbiamo registrare una classe che è descritta da un’interfaccia. In questo caso, come T specifichiamo l’interfaccia e come Y l’implementazione concreta che vogliamo utilizzare.
Infine, dobbiamo cambiare il modo con cui recuperiamo la proprietà Main: non creeremo più manualmente un’istanza della classe MainViewModel, ma andremo a chiedere al container di restituircela.
Il gioco è fatto. A questo punto, nel momento in cui l’applicazione partirà:
1. Sarà creata una nuova istanza della classe ViewModelLocator, la quale si farà carico di registrare all’interno del container SimpleIoc il ViewModel di nome MainViewModel e il servizio RssService, rappresentato dall’interfaccia IRssService.
2. Verrà scatenata la navigazione verso la pagina principale dell’applicazione: di conseguenza, la pagina andrà a chiedere al ViewModelLocator un’istanza della classe MainViewModel, dato che è impostata come DataContext della pagina.
3. Il ViewModelLocator andrà a cercare, all’interno del container, se è stata registrata una classe di nome MainViewModel. La troverà, riscontrando che tale classe ha una dipendenza dall’interfaccia IRssService, dato che è uno dei parametri del costruttore. Di conseguenza, il container effettuerà un’altra ricerca, per verificare se esista una classe registrata al suo interno legata all’interfaccia IRssService. Anche in questo caso, la risposta è affermativa: il container restituirà perciò un’istanza della classe MainViewModel con, al suo interno, un riferimento alla classe RssService.
È importante sottolineare come sia sufficiente che anche solo una delle condizioni elencate al punto 3 non sia soddisfatta per scatenare un’eccezione: il container non sarebbe in grado, infatti, di soddisfare tutte le dipendenze richieste.
Ora che siamo arrivati alla fine del viaggio, vi sarà semplice capire il vantaggio della dependency injection. Nel momento in cui dovessimo fare dei test e volessimo usare la classe FakeRssService al posto di RssService, ci basterebbe andare a cambiare una riga nel costruttore del ViewModelLocator. Da:
SimpleIoc.Default.Register<IRssService, RssService>();
a:
SimpleIoc.Default.Register<IRssService, FakeRssService>();
A questo punto, poco importa se la nostra applicazione contenga uno o cinquanta ViewModel che dipendono dall’interfaccia IRssService: in automatico, il container andrà ad iniettare il nuovo servizio all’interno di tutti i ViewModel.
Il vantaggio di questo approccio non è solamente nel testing, ma anche nel refactoring. Ipotizziamo, ad esempio, che la nostra applicazione non debba più recuperare le notizie da un feed RSS, ma da un servizio REST. Fintanto che la nuova classe continuerà a sfruttare la struttura definita dall’interfaccia IRssService (quindi un metodo GetNews() che restituisce una collezione di FeedItem), non dovremo intervenire in alcun modo nei ViewModel: dovremo semplicemente creare una nuova classe che implementi l’interfaccia IRssService e registrarla all’interno del container, al posto di RssService.
Nel prossimo post
Siamo quasi al termine del percorso: nel prossimo post andremo a vedere altri scenari avanzati, come lo scambio di messaggi e la gestione di eventi secondari. Trovate un progetto di esempio con cui poter mettere in pratica quanto visto in questo post su GitHub all’indirizzo https://github.com/qmatteoq/UWP-MVVMSamples Happy coding!
Comments
Anonymous
January 25, 2016
Ciao Matteo. Il fatto di demandare a runtime la creazione delle classi non inficia sulle prestazioni? Volendo, la parte delle dependency injection, potevamo ignorarla e nel ViewModelLocator passare la classe RssService come costruttore dei ViewModel ... è vero che se volevamo cambiare classe rss non dovevamo intervenire solo su una riga di codice ma su una trentina ad esempio (in un caso molto complesso). Ma sarebbero tutte in un punto. Mi chiedevo, ma ne vale la pena usare questo metodo?Anonymous
January 25, 2016
Ciao Giorgio, secondo me si, vale assolutamente la pena perchè la perdita di prestazioni è minima (l'unico "peso" aggiuntivo è quello del container che deve orchestrare le classi il quale, alla fine comuque si fa carico di creare instanze in memoria degli oggetti, esattamente come faresti a compile time), ma l'applicazione (soprattutto se complessa) ne guadagna parecchio in leggibilità e manutenibilità. Sicuramente si potrebbe fare anche come dici tu, però non si raggiunge comunque lo stesso livello di "pulizia" del codice. Ovvio che la dependency injection si apprezza maggiormente su progetti complessi e, magari, con anche ViewModel condivisi tra piattaforme differenti. Per progetti molto semplici con pochi ViewModel e servizi coinvolti, il gioco potrebbe non valere la candela.