Sfruttare le Action ASP.NET MVC da Silverlight: dalla teoria alla pratica
Come detto, in alcuni scenari legati a Silverlight, è possibile sostituire le funzionalità di WCF Ria Services
con delle più “snelle” action ASP.NET MVC. L’obiettivo dichiarato è rimuovere la complessità di una piattaforma
di servizi che, a volte, può risultare eccessivamente pesante da utilizzarsi in situazioni tutto sommato semplici.
Se non si fa un massiccio uso del “tracking” (cioè del tracciamento dello stato di ogni modello emesso dal server), l'uso dei WCF Ria Services è rimpiazzabile con una soluzione, a mio parere, semplice e performante; una soluzione che rappresenta il giusto compromesso tra produttività e innovazione, orientata a tecnologie più moderne.
Se siamo in una situazione in cui l'applicazione Silverlight (come detto, basata su WCF Ria Services) è già completa e funzionante - certo, se non volete farvi del male - vi esorterei a domandarvi se esiste una reale necessità di cambiare il meccanismo per migliorare le performance. Se invece l'applicazione è in sviluppo oppure la sua dimensione è ridotta, ritengo sia un "enhancement" che vale la pena di provare.
Mi sembra inutile dirlo, ma bisogna innanzitutto partire dal fatto che l'applicazione web che terrà in hosting il file ".xap" di Silverlight dovrà essere basata su ASP.NET MVC (almeno la versione 3.0, anche se consiglio di partire subito con la 4.0). Una volta pronta la "solution" e fatto il setup della pagina che ispiterà il plugin di Silverlight, è necessario inserire un controller apposito per permettere di ospitare le action che emetteranno le informazioni. Nel mio esempio ho creato un semplicissimo controller denominato "ProductsController", che contiene la action "FetchProducts", e prende in input l'identificativo della categoria di cui vogliamo recuperare l'elenco di prodotti (sto usando il solito, noioso esempio Prodotti/Categorie... scusate la mancanza di fantasia).
Il contenuto è auto-esplicativo; talvolta tendo ad essere molto pigro, quindi non ho nemmeno provato ad accennare una persistenza del dato sul un eventuale database applicativo: la cosa richiederebbe tempo e non è lo scopo di questo articolo. Quindi, da sfaticato, genero un migliaio di elementi con valori randomizzati.
A scopo di test ho inserito anche uno "sleep" di 5 secondi per simulare un caricamento da una base dati estremamente lenta. Magari, nelle vostre soluzioni, se usate lo stesso "trick" durante lo sviluppo, ricordatevi di toglierlo quando passate a Produzione. Non come ho fatto io qualche anno fa, quando i clienti hanno utilizzato per tre settimane un sistema che sospendeva l'esecuzione ad ogni accesso alla base dati. :)
La funzione emette dei modelli: una lista dei famosi "Model" di MVC ("Model", "View", "Controller"). Essi rappresentano il dato che, presente sul server, deve essere inviato al client. E a questo punto, va subito detto che non sta scritto da nessuna parte che il modello di persistenza (il nostro Prodotto presente sulla base dati), debba essere trasferito esattamente identico sul client.
Il "modello" che deve arrivare sull'interfaccia utente "può" avere una rappresentazione identica all'entità database; ma è molto più comune lo scenario in cui solo alcune delle informazioni contenute siano necessarie per l'interazione sulla UI; oppure sia necessario integrare il modello con informazioni aggiuntive che lo completano.
Nel nostro esempio, a sostegno di questa tesi, ho introdotto un campo "Quantity", che contiene la giacenza a magazzino delle unità del prodotto. Normalmente, questa informazione sarebbe archiviata in una entità diversa dal "Product", e solo in rari casi si procederebbe ad "inquinare" l'anagrafica dei prodotti con un dato che poco centra con le sue caratteristiche. Una cosa che ho sempre apprezzato dei WCF Ria Services (dei tools di Visual Studio, per la verità) è la capacità di auto-generare il codice client per persistere i modelli provenienti dal server. Rimuovendo dall'equazione questa piattaforma, ci perdiamo questa funzione...ma poco male a confronto dei vantaggi che otterremo. Basta un semplice "copia&incolla" (tecnica molto in voga negli ultimi tempi ;) ) e il nostro "ProductModel" sarà presente anche nella folder "Models" presente nel progetto Silverlight.
Ora Il nostro lavoro si trasferisce sul client: quello che serve è un sistema che permetta di agganciare il risultato JSON (Javascript Object Notation) emesso dalla action MVC "FetchProducts" definita sopra. E cosa ci può essere di meglio di un semplicissimo "WebClient"? Niente, dico io... Il tutto è wrappato in un helper, con un metodo che vuole assomigliare molto alla chiamata asincrona di JQuery per il recupero delle informazioni server. Se vogliamo essere pignoli, il componente XHR (XMLHttpRequest) è lo stesso utilizzato da JQuery, quindi stiamo solo "simulando" un metodo JQuery in ambiente .NET. Figo, no?
Il codice è tutto sommato molto semplice: istanziamo il webclient impostando gli header di base in modo che la richiesta inviata sia in formato JSON, agganciamo il metodo di completamento della richesta, e la avviamo in maniera asincrona specificando l'indirizzo, il verb HTTP da usare ("GET", "POST", etc.) e gli eventuali dati da inviare al server.
Nella funzione di callback va posto l'accento sull'utilizzo della fantastica libreria Json.NET (a mio parere la libreria per parsing di contenuti JSON migliore in assoluto). I dati emessi dal server arrivano sul client in formato testuale; è tuttavia necessario eseguire il parsing di tale testo in un modello strutturato: Json.NET fa questo lavoro per noi in maniera assolutamente trasparente (e velocissima), renderizzando il modello (o la lista di modelli) specificati come parametro generico.
Resta ancora il problema di recuperare il corretto indirizzo della action lato server: il metodo presente nella classe di utilità ci aiuta in questo. Ammetto che non è proprio una soluzione elegante, ma fa il suo sporco lavoro... L'utilizzo del client è altrettanto semplice. Sono solito segregare il più possibile le funzionalità applicative, quindi ho previsto l'inserimento di una classe denominata "ServerDomain" che conterrà l'elenco delle possibili operazioni richiamabili sul server, in un unico punto. L'obiettivo è "nascondere" i dettagli implementativi di ciascun metodo, permettendo di avere nei vari viewmodels solo invocazioni minimali e pulite. Il metodo "FetchProductsAsync" fa il paio con "FetchProducts" presente sul server. Il parametro valore specificato rappresenta l'identificativo della categoria, mentre la "Action" definita come secondo argomento è la funzione di callback gestita sul viewmodel.
Il dettaglio implementativo della funzione contiene la "colla" che unisce le varie parti: composizione del url della action MVC sul server, composizione dei "data" (in formato JSON) da inviare al server con l'identificativo della categoria, ed invocazione della funzione asincrona di richiesta.
Un'altra piccola nota va dedicata ai "data" da "postare" sul server. Come detto la request deve essere in formato JSON, quindi sfruttiamo nuovamente Json.NET per serializzare un oggetto Javascript che contiene una singola proprietà denominata "categoryId". Vi esorto a fare attenzione al funzionamento del "model binding" di ASP.NET MVC: per eseguire correttamente la chiamata ad una specifica action con parametri, è necessario che il nome dei parametri definiti sul server sia una stringa identica al nome della proprietà dell'oggetto JSON serializzato lato client.
Adesso che tutto è (spero) chiaro, vediamo come invocare il tutto partendo dal nostro "MainViewModel". Nel pieno rispetto del pattern MVVM ("Model-View-ViewModel") in Silverlight - non iniziate nemmeno ad usare Silverlight o WPF se non lo conoscete - definisco le proprietà "osservabili" che mi servono per il binding con l'interfaccia XAML; definisco le collezioni osservabili che ospiteranno i dati provenienti dal server, e definisco il "command" che esegue il caricamento dei dati da remoto.
La parte succosa è "_Domain.FetchProductsAsync(..." che permette di invocare il metodo definito sul "ServerDomain", passando una categoria con identificativo numerico "47" (la mia pigrizia continua), e gestendo il callback tramite una elegante lamda expression, ricevente la lista di "ProductModel" deserializzati a partire dalla risposta JSON del server.
Il resto è solo paglia: composizione dei viewmodels a partire dai modelli ricevuti, e aggiunta alla collezione in binding con una listview sulla UI. Per fare un pò di scena ho anche aggiunto una variabile per "misurare" la durata dell'operazione di interazione con il server, visualizzando i risultati a video.
Magari, per esercizio, provare ad implementare il medesimo flusso logico usando i WCF Ria Services, confrontando l'esecuzione delle due versioni e toccando con mano quanto questa soluzione sia più performante.
Detto questo, non è necessario che lo scenario sia applicato a tappeto sull'applicativo; ci possono essere situazioni ibride, con "WCF Ria Services" e action MVC che lavorano contemporaneamente. L'unico accorgimento da considerare è che le entità "gestite" da una piattaforma di servizi, avrà qualche difficoltà ad essere gestita con l'altra (per esempio non potete "scaricare" un oggetto usando Mvc e rimandarlo al server, in "update", usando Ria). Ciononostante tutte queste situazioni possono essere gestite con un po' di fantasia e usando nella maniera opportuna la "materia grigia"...
Stay tuned...
M.
Se non si fa un massiccio uso del “tracking” (cioè del tracciamento dello stato di ogni modello emesso dal server), l'uso dei WCF Ria Services è rimpiazzabile con una soluzione, a mio parere, semplice e performante; una soluzione che rappresenta il giusto compromesso tra produttività e innovazione, orientata a tecnologie più moderne.
Se siamo in una situazione in cui l'applicazione Silverlight (come detto, basata su WCF Ria Services) è già completa e funzionante - certo, se non volete farvi del male - vi esorterei a domandarvi se esiste una reale necessità di cambiare il meccanismo per migliorare le performance. Se invece l'applicazione è in sviluppo oppure la sua dimensione è ridotta, ritengo sia un "enhancement" che vale la pena di provare.
Mi sembra inutile dirlo, ma bisogna innanzitutto partire dal fatto che l'applicazione web che terrà in hosting il file ".xap" di Silverlight dovrà essere basata su ASP.NET MVC (almeno la versione 3.0, anche se consiglio di partire subito con la 4.0). Una volta pronta la "solution" e fatto il setup della pagina che ispiterà il plugin di Silverlight, è necessario inserire un controller apposito per permettere di ospitare le action che emetteranno le informazioni. Nel mio esempio ho creato un semplicissimo controller denominato "ProductsController", che contiene la action "FetchProducts", e prende in input l'identificativo della categoria di cui vogliamo recuperare l'elenco di prodotti (sto usando il solito, noioso esempio Prodotti/Categorie... scusate la mancanza di fantasia).
Il contenuto è auto-esplicativo; talvolta tendo ad essere molto pigro, quindi non ho nemmeno provato ad accennare una persistenza del dato sul un eventuale database applicativo: la cosa richiederebbe tempo e non è lo scopo di questo articolo. Quindi, da sfaticato, genero un migliaio di elementi con valori randomizzati.
A scopo di test ho inserito anche uno "sleep" di 5 secondi per simulare un caricamento da una base dati estremamente lenta. Magari, nelle vostre soluzioni, se usate lo stesso "trick" durante lo sviluppo, ricordatevi di toglierlo quando passate a Produzione. Non come ho fatto io qualche anno fa, quando i clienti hanno utilizzato per tre settimane un sistema che sospendeva l'esecuzione ad ogni accesso alla base dati. :)
La funzione emette dei modelli: una lista dei famosi "Model" di MVC ("Model", "View", "Controller"). Essi rappresentano il dato che, presente sul server, deve essere inviato al client. E a questo punto, va subito detto che non sta scritto da nessuna parte che il modello di persistenza (il nostro Prodotto presente sulla base dati), debba essere trasferito esattamente identico sul client.
Il "modello" che deve arrivare sull'interfaccia utente "può" avere una rappresentazione identica all'entità database; ma è molto più comune lo scenario in cui solo alcune delle informazioni contenute siano necessarie per l'interazione sulla UI; oppure sia necessario integrare il modello con informazioni aggiuntive che lo completano.
Nel nostro esempio, a sostegno di questa tesi, ho introdotto un campo "Quantity", che contiene la giacenza a magazzino delle unità del prodotto. Normalmente, questa informazione sarebbe archiviata in una entità diversa dal "Product", e solo in rari casi si procederebbe ad "inquinare" l'anagrafica dei prodotti con un dato che poco centra con le sue caratteristiche. Una cosa che ho sempre apprezzato dei WCF Ria Services (dei tools di Visual Studio, per la verità) è la capacità di auto-generare il codice client per persistere i modelli provenienti dal server. Rimuovendo dall'equazione questa piattaforma, ci perdiamo questa funzione...ma poco male a confronto dei vantaggi che otterremo. Basta un semplice "copia&incolla" (tecnica molto in voga negli ultimi tempi ;) ) e il nostro "ProductModel" sarà presente anche nella folder "Models" presente nel progetto Silverlight.
Ora Il nostro lavoro si trasferisce sul client: quello che serve è un sistema che permetta di agganciare il risultato JSON (Javascript Object Notation) emesso dalla action MVC "FetchProducts" definita sopra. E cosa ci può essere di meglio di un semplicissimo "WebClient"? Niente, dico io... Il tutto è wrappato in un helper, con un metodo che vuole assomigliare molto alla chiamata asincrona di JQuery per il recupero delle informazioni server. Se vogliamo essere pignoli, il componente XHR (XMLHttpRequest) è lo stesso utilizzato da JQuery, quindi stiamo solo "simulando" un metodo JQuery in ambiente .NET. Figo, no?
Il codice è tutto sommato molto semplice: istanziamo il webclient impostando gli header di base in modo che la richiesta inviata sia in formato JSON, agganciamo il metodo di completamento della richesta, e la avviamo in maniera asincrona specificando l'indirizzo, il verb HTTP da usare ("GET", "POST", etc.) e gli eventuali dati da inviare al server.
Nella funzione di callback va posto l'accento sull'utilizzo della fantastica libreria Json.NET (a mio parere la libreria per parsing di contenuti JSON migliore in assoluto). I dati emessi dal server arrivano sul client in formato testuale; è tuttavia necessario eseguire il parsing di tale testo in un modello strutturato: Json.NET fa questo lavoro per noi in maniera assolutamente trasparente (e velocissima), renderizzando il modello (o la lista di modelli) specificati come parametro generico.
Resta ancora il problema di recuperare il corretto indirizzo della action lato server: il metodo presente nella classe di utilità ci aiuta in questo. Ammetto che non è proprio una soluzione elegante, ma fa il suo sporco lavoro... L'utilizzo del client è altrettanto semplice. Sono solito segregare il più possibile le funzionalità applicative, quindi ho previsto l'inserimento di una classe denominata "ServerDomain" che conterrà l'elenco delle possibili operazioni richiamabili sul server, in un unico punto. L'obiettivo è "nascondere" i dettagli implementativi di ciascun metodo, permettendo di avere nei vari viewmodels solo invocazioni minimali e pulite. Il metodo "FetchProductsAsync" fa il paio con "FetchProducts" presente sul server. Il parametro valore specificato rappresenta l'identificativo della categoria, mentre la "Action" definita come secondo argomento è la funzione di callback gestita sul viewmodel.
Il dettaglio implementativo della funzione contiene la "colla" che unisce le varie parti: composizione del url della action MVC sul server, composizione dei "data" (in formato JSON) da inviare al server con l'identificativo della categoria, ed invocazione della funzione asincrona di richiesta.
Un'altra piccola nota va dedicata ai "data" da "postare" sul server. Come detto la request deve essere in formato JSON, quindi sfruttiamo nuovamente Json.NET per serializzare un oggetto Javascript che contiene una singola proprietà denominata "categoryId". Vi esorto a fare attenzione al funzionamento del "model binding" di ASP.NET MVC: per eseguire correttamente la chiamata ad una specifica action con parametri, è necessario che il nome dei parametri definiti sul server sia una stringa identica al nome della proprietà dell'oggetto JSON serializzato lato client.
Adesso che tutto è (spero) chiaro, vediamo come invocare il tutto partendo dal nostro "MainViewModel". Nel pieno rispetto del pattern MVVM ("Model-View-ViewModel") in Silverlight - non iniziate nemmeno ad usare Silverlight o WPF se non lo conoscete - definisco le proprietà "osservabili" che mi servono per il binding con l'interfaccia XAML; definisco le collezioni osservabili che ospiteranno i dati provenienti dal server, e definisco il "command" che esegue il caricamento dei dati da remoto.
La parte succosa è "_Domain.FetchProductsAsync(..." che permette di invocare il metodo definito sul "ServerDomain", passando una categoria con identificativo numerico "47" (la mia pigrizia continua), e gestendo il callback tramite una elegante lamda expression, ricevente la lista di "ProductModel" deserializzati a partire dalla risposta JSON del server.
Il resto è solo paglia: composizione dei viewmodels a partire dai modelli ricevuti, e aggiunta alla collezione in binding con una listview sulla UI. Per fare un pò di scena ho anche aggiunto una variabile per "misurare" la durata dell'operazione di interazione con il server, visualizzando i risultati a video.
Magari, per esercizio, provare ad implementare il medesimo flusso logico usando i WCF Ria Services, confrontando l'esecuzione delle due versioni e toccando con mano quanto questa soluzione sia più performante.
Detto questo, non è necessario che lo scenario sia applicato a tappeto sull'applicativo; ci possono essere situazioni ibride, con "WCF Ria Services" e action MVC che lavorano contemporaneamente. L'unico accorgimento da considerare è che le entità "gestite" da una piattaforma di servizi, avrà qualche difficoltà ad essere gestita con l'altra (per esempio non potete "scaricare" un oggetto usando Mvc e rimandarlo al server, in "update", usando Ria). Ciononostante tutte queste situazioni possono essere gestite con un po' di fantasia e usando nella maniera opportuna la "materia grigia"...
Stay tuned...
M.
Commenti
Posta un commento