WPF MVVM Toolkit
Upcoming SlideShare
Loading in...5
×
 

WPF MVVM Toolkit

on

  • 6,787 views

Tesi di laurea:

Tesi di laurea:
Separazione dei ruoli tra Designer e Developer nello sviluppo di applicazioni Desktop: uso di WPF e del pattern Model-View-ViewModel

Statistics

Views

Total Views
6,787
Views on SlideShare
6,783
Embed Views
4

Actions

Likes
4
Downloads
437
Comments
0

2 Embeds 4

http://www.linkedin.com 3
http://www.google.co.uk 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

WPF MVVM Toolkit WPF MVVM Toolkit Document Transcript

  • UNIVERSITA' DEGLI STUDI DI TRIESTE FACOLTA' DI INGEGNERIA CORSO DI LAUREA IN INGEGNERIA INFORMATICA Separazione dei ruoli tra Designer e Developer nello sviluppo di applicazioni Desktop: uso di WPF e del pattern Model-View-ViewModel Laureando: Relatore: Alessandro Andreose' Chiar.mo Prof. Maurizio Fermeglia Anno Accademico 2008 – 2009
  • Sommario Introduzione............................................................................................................ 6 Un po’ di storia .................................................................................................... 7 Capitolo uno ............................................................................................................ 8 Windows Presentation Foundation .................................................................... 8 Perché WPF ..................................................................................................... 9 XAML ............................................................................................................. 10 Layout e controlli .......................................................................................... 11 Logical Tree e Visual Tree ............................................................................. 11 Command ...................................................................................................... 13 Routed Event................................................................................................. 14 Dependency Property ................................................................................... 14 Attached Property (o Attached Behaviour) .................................................. 15 Object Resources e Resource Dictionary ...................................................... 15 Data Binding .................................................................................................. 16 Data Template............................................................................................... 18 Separazione dei ruoli .................................................................................... 19 Model – View – ViewModel .............................................................................. 20 Model ............................................................................................................ 21 ViewModel .................................................................................................... 21 View .............................................................................................................. 22 Separazione dei ruoli .................................................................................... 23 Capitolo due: Il toolkit .......................................................................................... 24 INotifyPropertyChanged ................................................................................... 24 2
  • Delegate Command .......................................................................................... 27 Scrivere il codice del comando nel ViewModel ............................................ 29 Sapere quando un comando può essere eseguito ....................................... 30 Aggiungere degli shortcut ............................................................................. 31 Associare il comando al controllo nel codice XAML e minimizzare la scrittura di codice ripetitivo ........................................................................................ 32 Come si usa ................................................................................................... 36 IMessageBroker ................................................................................................ 36 UI Composition ................................................................................................. 48 IRegion .......................................................................................................... 49 IModule ......................................................................................................... 54 ViewModel .................................................................................................... 57 Associare una region al ViewModel ............................................................. 58 Application Controller ................................................................................... 60 Capitolo tre: L’applicazione .................................................................................. 63 Obiettivo dell'applicazione ............................................................................... 63 Architettura hardware e software dell'ambiente di produzione ..................... 63 Requisiti tecnici non funzionali ......................................................................... 64 Architettura dell'applicazione........................................................................... 64 Application Server / Web Server .................................................................. 66 Client ............................................................................................................. 68 Attori ................................................................................................................. 69 Struttura dell'applicazione ................................................................................ 71 Alcune user story .............................................................................................. 72 3
  • Conclusioni ............................................................................................................ 80 Appendice uno: come scrivere un'applicazione WPF ........................................... 82 XAML Conventions ............................................................................................ 82 Utilizzare x:Name al posto di Name ............................................................ 82 Posizionare il primo attributo di un elemento nella riga sotto il nome dell'elemento ................................................................................................ 82 Preferire StaticResource a DynamicResource .............................................. 83 Naming Convention per gli elementi ............................................................ 84 Unire le risorse a livello di Applicazione ....................................................... 84 Organizzazione delle risorse ............................................................................. 84 Strutturare le risorse ..................................................................................... 84 Naming Convention ...................................................................................... 86 Appendice due: come disegnare una Window in WPF ........................................ 87 Grid.................................................................................................................... 88 Bibliografia ............................................................................................................ 92 Web ................................................................................................................... 93 4
  • 5
  • Introduzione L’obiettivo primario di questa tesi è lo sviluppo di un toolkit per scrivere applicazioni desktop in WPF applicando il pattern Model-View-ViewModel (MVVM). Questo toolkit permette sia di non dover ogni volta riscrivere quelle parti di codice che naturalmente si ripetono in ogni progetto, sia di avere del codice testato, aggiornato e facile da manutenere. Lo sviluppo di applicazioni senza l’utilizzo di pattern come Model View Controller (MVC)1, Presentation Model, Model-View-ViewModel e derivati soffre di tre gravi problemi:  Non c’è separazione dei ruoli tra Designer e Developer.  Non c’è una gestione dello stato dei controlli.  È molto difficile testare le funzionalità legate alla user interface. Tutti questi pattern cercano di risolvere questi tre problemi, però hanno un costo: si deve scrivere molto più codice che poi deve essere manutenuto. Inoltre, l’utente di oggi si aspetta di utilizzare applicazioni che abbiano un’interfaccia utente accattivante e user-friendly. Ora più che mai c’è bisogno che figure diverse partecipino alla realizzazione dell’intero progetto software. Per quanto riguarda lo sviluppo della interfaccia utente (user interface, UI) sono due i ruoli fondamentali: il developer e il designer. Il primo è in grado di creare tutta la logica, il secondo invece deve curare l'aspetto grafico. Il problema principale di questo approccio è che developer e designer devono lavorare assieme, ma non devono, per quanto possibile, intralciarsi a vicenda. Negli anni sono state sviluppate varie tecniche, sia architetturali, che tecnologiche. Nel mondo web ad esempio la tecnologia dei Cascading Style Sheet 1 http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller 6
  • (CSS) aiuta a suddividere i compiti delle due figure professionali. Dal punto di vista architetturale esistono modelli di riferimento per la scrittura del codice (pattern) come il Model View Controller, che separa il codice di visualizzazione (View) dalle entità di business (Model): per comunicare hanno bisogno di un Controller. Ed è proprio in quest’ultimo che viene inserita la maggior parte della logica. La view ha poco codice (sperabilmente nessuno) e la maggior parte del lavoro lo deve fare il designer. Quando si sviluppano applicazioni con WPF, non è obbligatorio implementare il pattern MVVM e gli altri principi che saranno spiegati in questa tesi. Tutti questi principi, però, definiscono una metodologia di sviluppo che aiuta a separare il ruolo del developer da quello del designer. Il toolkit, insomma, è la formalizzazione di una metodologia che il developer applicherà ripetutamente nelle sue applicazioni desktop con WPF. Un po’ di storia La separazione tra l’interfaccia utente (view) e il modello di business (model) non è una nuova idea dello sviluppo software: ha circa trenta anni2. Recentemente è rinato l’interesse per le architetture view-model, soprattutto a causa della crescita della complessità dei sistemi software moderni e della necessità di visualizzare un prodotto software su diverse interfacce utente (desktop, web, …) senza per questo motivo dover riscrivere l’intera applicazione. Il pattern Model-View-ViewModel è una variazione del pattern Model View Controller, nato verso la fine degli anni settanta come framework sviluppato in Smalltalk, e presentato come pattern da Martin Fowler nel suo libro Patterns of Enterprise Application Architecture3. 2 http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller#History 3 http://martinfowler.com/books.html#eaa 7
  • Il pattern MVVM è stato presentato per la prima volta al pubblico nell’ottobre del 2005 in un post4 sul blog di John Gossman, ed è stato utilizzato per la prima volta in Microsoft per sviluppare Expression Blend, primo software Microsoft a essere sviluppato interamente in Windows Presentation Foundation (WPF) ed orientato alla figura del designer. Capitolo uno Windows Presentation Foundation Niente è più importante della user experience di un'applicazione. Mentre molti professionisti sono più interessati a come lavora un'applicazione, gli utenti che utilizzano il software si preoccupano dell'interfaccia utente. L'interfaccia di un'applicazione compone la stragrande maggioranza della user experience di un software. Per molti utenti l'interfaccia e l'esperienza sono l'applicazione. Se si produce una buona esperienza utente attraverso l'interfaccia, si aumenta la produttività del software stesso. Windows Presentation Foundation (WPF) è una parte del Framework .NET 3.0 uscito nel novembre 2006, aggiornato in seguito con il Framework 3.5 nel novembre 2007 e con il Framework 3.5 SP1 nell’agosto 2008. WPF è la nuova tecnologia Microsoft .NET per sviluppare l’interfaccia utente di un’applicazione desktop ed è l’unica che viene aggiornata con l’aggiunta di nuove feature. Si possono creare interfacce utente (UI) sia scrivendo codice managed, sia utilizzando un linguaggio di markup XAML (eXtensible Application Markup Language), una sintassi Xml per un approccio "dichiarativo", più da designer. È possibile anche utilizzare una tecnica mista che è quella più comunemente utilizzata. Per ogni window o user control di solito si hanno 2 file: un file .xaml 4 http://blogs.msdn.com/johngossman/archive/2005/10/08/478683.aspx 8
  • che contiene il codice XAML e un file .xaml.cs (o .xaml.vb) che contiene il codice associato (nel linguaggio scelto). La prima parte di questo capitolo spiega le caratteristiche principali di WPF e l’ausilio che WPF può dare per separare il lavoro del designer da quello dello sviluppatore. La seconda parte del capitolo, invece, introduce il pattern Model – View – ViewModel e come si collega a WPF. Perché WPF Lo scopo di Windows Presentation Foundation è di facilitare lo sviluppo dell'interfac-cia utente, per migliorare la user experience. Utilizzando WPF sviluppatori e designer possono creare interfacce che incorporino documenti, video, immagini, grafica 2D e 3D, animazioni e molto altro. Piattaforma unificata Prima di WPF la creazione di interfacce utente seguendo i requisiti appena citati, richiedeva l'utilizzo di molte tecnologie differenti. Questo di per sé non è un problema. 9
  • Il problema è che molto spesso uno sviluppatore non conosce tutte queste tecnologie e quindi non le usa. Questo comportamento ha come conseguenza diretta una minore user experience che va tutta a danno dell'utente finale. WPF unifica tutte queste aree in un'unica tecnologia. Gli sviluppatori possono creare applicazioni accattivanti conoscendo a fondo una singola tecnologia. XAML XAML è un linguaggio di markup utilizzato per istanziare oggetti .NET. Nonostante XAML sia una tecnologia che può essere applicata a differenti domini di problemi, la sua principale applicazione è di costruire interfacce utente in WPF: i documenti XAML definiscono la creazione e il posizionamento di pannelli, bottoni e controlli che vivono nelle window di un’applicazione WPF. Lo standard XAML si fonda sull'applicazione di alcune semplici regole di base:  Ogni elemento in un documento XAML mappa un’istanza di una classe .NET. Il nome del tag di ogni elemento è identico al nome della classe.  Come per tutti i documenti XML, è possibile inserire un elemento all’interno di un altro.  È possibile settare le proprietà di ogni classe attraverso gli attributi. Comunque, in certe situazioni un attributo non è sufficientemente potente. In questi casi è possibile inserire dei tag all’interno dell’elemento con una sintassi speciale. L'utilizzo di codice XAML per creare interfacce utente permette a developer e designer di lavorare sullo stesso file utilizzando strumenti diversi. Questo porta ad una maggiore interazione tra le due figure professionali. 10
  • Layout e controlli Per organizzare le varie parti di un'interfaccia, WPF utilizza dei contenitori per il layout. Ogni panel può contenere dei figli, inclusi dei controlli come bottoni e blocchi di testo o altri panel. Tutto ciò, crea una struttura ad albero che ha come padre la window che contiene un panel che a sua volta è il padre di tutti gli altri controlli. Logical Tree e Visual Tree La struttura ad albero principale in WPF è la struttura ad albero dell'elemento. In Windows Presentation Foundation, sono disponibili due modi di elaborazione e concettualizzazione della struttura ad albero dell'elemento: come albero logico e come struttura ad albero visuale. Le distinzioni tra albero logico e struttura ad albero visuale non sono sempre necessariamente importanti. Tuttavia possono talvolta causare problemi ad alcuni sottosistemi WPF e influire sulle scelte fatte nel markup o nel codice. Logical Tree In WPF è possibile aggiungere contenuti agli elementi utilizzando le proprietà. Ad esempio, è possibile aggiungere elementi a un controllo ListBox utilizzando la proprietà Items. In questo modo, gli elementi vengono collocati nell'oggetto ItemCollection del controllo ListBox. Per aggiungere elementi a un 11
  • oggetto DockPanel è necessario utilizzare la relativa proprietà Children. In questo caso, gli elementi vengono aggiunti all'oggetto UIElementCol- lection dell'oggetto DockPanel. Grazie all'albero logico, i modelli di contenuto possono scorrere prontamente i possibili elementi figlio e quindi essere estendibili. Inoltre, l'albero logico fornisce un framework per alcune notifiche, ad esempio quando tutti gli elementi dell'albero logico sono caricati. Ancora, i riferimenti di risorsa5 vengono risolti cercando verso l'alto nell'albero logico gli insiemi di risorse relativi all'elemento di richiesta iniziale e succes- sivamente gli elementi padre. Visual Tree Nella struttura ad albero visuale viene descritta la struttura degli elementi visivi rappresentati dalla classe base Visual. Un'esposizione della struttura ad albero visuale come parte della programmazione di applicazioni WPF convenzionale è necessario, poiché su di esso è implementato il meccanismo degli eventi, 5 Per avere maggiori informazioni sulle risorse, vedere il paragrafo Object Resources e Resource Dictionary, presente in questo capitolo. 12
  • chiamato "routed events"6. Gli "eventi instradati" percorrono la struttura dell'albero visuale e non dell'albero logico. Command In un'applicazione reale, le funzionalità sono divise in attività (task) di alto livello. Questi task possono essere attivati da varie azioni differenti e da molti elementi dell’interfaccia utente, inclusi i menù, i bottoni, le scorciatoie da tastiera e le toolbar. WPF permette di definire questi task, conosciuti come command, ed associare ad essi i controlli, così non c’è bisogno di scrivere codice ripetitivo (code bloat) quando si sottoscrive un evento. Ancora più importante, i comandi controllano lo stato dell’interfaccia utente e automaticamente disabilitano i controlli a essi collegati quando il task non è permesso. L'esecuzione di comandi è un meccanismo di input di WPF che fornisce la gestione di input a un livello semantico maggiore rispetto all'input del dispositivo. Le operazioni Copia, Taglia e Incolla presenti in molte applicazioni sono esempi di comandi. La differenza tra i comandi e un semplice gestore eventi associato a un pulsante o a un timer consiste nel fatto che i comandi separano la semantica e la creazio- 6 Per avere maggiori dettagli sui ruoted event, vedere il paragrafo Routed Event, presente in questo capitolo. 13
  • ne di un'azione dalla relativa logica. In questo modo, più codici sorgente diversi possono richiamare la stessa logica di comando che pertanto può essere personalizzata per obiettivi differenti. Routed Event Ci sono tre tipi di routed event: direct, bubbling e tunneling. I Direct Event sono eventi che possono essere intercettati solo dall'elemento che ha creato l'evento. I Bubbling Event sono eventi che viaggiano verso l'alto attraverso il visual tree e possono essere intercettati da ogni parent dell'elemento sorgente. Infine, i Tunneling Event sono eventi che viaggiano verso il basso attraverso il visual tree e possono essere intercettati da ogni child dell'elemento sorgente. Dependency Property Le Dependency Properties evolvono il concetto standard di proprietà in .NET e sono caratteristiche di WPF. Utilizzano lo storage in maniera più efficiente e supportano alcune feature di alto livello come per esempio la notifica quando cambia il valore e la capacità di propagare i valori di default verso il basso per tutto l’albero di elementi. Le dependency property sono alla base di molte caratteristiche di WPF, come il data binding e gli stili. public string MyProperty { get { return (string)GetValue(MyPropProperty); } set { SetValue(MyPropProperty, value); } } public static readonly DependencyProperty MyPropProperty = DependencyProperty.Register("MyProp", typeof(string), typeof(MyClass), new PropertyMetadata("")); Dal codice si può osservare che una property, anziché memorizzare il valore in un 14
  • campo privato, definisce una variabile di tipo DependencyProperty. Per conven- zione, la DependencyProperty termina con la parola "Property". Il metodo Register() accetta una classe di tipo PropertyMetadata che, fra le altre cose, setta il valore di default della property. Attached Property (o Attached Behaviour) Le attached properties sono delle dependency properties particolari che possono essere agganciate ad un oggetto. La cosa importante è che non devono essere definite nella classe alla quale devono essere aggiunte. Le attached properties sono particolarmente utili per agganciare i command anche agli oggetti WPF e agli eventi che non supportano nativamente i Command. Per esempio la TextBox non accetta nativamente un comando per l’evento KeyPress: tramite un’attached property si può aggiungere. Può essere utile, anche se si utilizza una TextBox come campo di ricerca: è più comodo per l’utente scrivere il testo e premere invio senza dover prima spostare il focus su un bottone “Cerca”. Un altro esempio potrebbe essere il doppio clic su una ListView: è utile per esempio per visualizzare il dettaglio di un oggetto visualizzato in una lista. WPF non lo supporta nativamente, ma tramite le attached property si può aggiungere questa caratteristica. Object Resources e Resource Dictionary WPF introduce un nuovo sistema di risorse che si integrano strettamente con XAML. Questo sistema permette di definire risorse in molti posti all’interno del markup (in un controllo specifico, in una window o per tutta l’applicazione) e riutilizzarle facilmente. Le Object Resource hanno un numero importante di benefici: 15
  •  Efficienza. Le risorse permettono di definire un oggetto una volta sola e utilizzarlo in molti posti nel markup.  Manutenibilità. Le risorse permettono di avere pochi dettagli di formattazione legati direttamente al controllo (come per esempio la dimensione del font) e di spostarli in un luogo centrale dove è più facile cambiarli. È l’equivalente XAML di creare costanti nel codice.  Adattabilità. Se certe informazioni sono separate dal resto dell’applicazione e sono salvate in una resource section, è possibile modificarle dinamicamente. Ogni elemento include una proprietà Resources di tipo Dictionary, in cui è possibile mettere le risorse. Questa collection può contenere qualsiasi tipo di oggetto .NET. Ogni oggetto è indicizzato da una stringa. Nonostante ogni oggetto ha una proprietà Resources, è più comune inserire le risorse a livello di Window. Questo perché ogni oggetto figlio condivide le risorse esposte dal padre. Se si vogliono condividere le risorse, è possibile creare un Resource Dictionary. Un resource dictionary è un documento XAML che fa da contenitore per le risorse che si vogliono utilizzare. La cosa importante delle risorse è che possono essere create e recuperate direttamente tramite markup. Le risorse possono essere create direttamente nel file xaml di una view, oppure in un file esterno. Per fare un paragone si possono immaginare i file di risorse come un file CSS, nel quale sono salvati vari stili. Data Binding Il Data Binding è la possibilità di collegare un dato direttamente all’interfaccia utente, senza doversi preoccupare di aggiornare la UI quando cambia il dato e viceversa. In WPF non c’è limite a cosa sia un “dato”: può essere una stringa, un 16
  • numero, un oggetto di business più o meno complesso, un altro controllo utente, eccetera. L’unico accorgimento è che si può fare il collegamento solo delle proprietà e non dei campi pubblici. <TextBlock Text="{Binding Person, Path=FirstName}" /> Il data binding in WPF può essere di vari tipi:  "OneWay" indica che l'interfaccia utente è aggiornata quando l'oggetto cambia.  "OneWayToSource" è aggiornato l'oggetto quando l'interfaccia utente cambia.  "TwoWay" l'aggiornamento è bidirezionale.  "OneTime" è visualizzato il valore quando l’interfaccia utente viene visualizzata per la prima volta e poi non viene più aggiornato. Un oggetto (source) collegato tramite data binding a una proprietà di un control- lo (target) che risiede nell’interfaccia utente per funzionare correttamente deve avere una serie di caratteristiche:  La proprietà dell’oggetto target deve essere una Dependency Property;  Se si tratta di TwoWay o OneWayToSource allora la proprietà da mettere in binding deve avere definito anche il setter;  Se si tratta di OneWay o TwoWay, source deve implementare INotifyPropertyChanged o INotifyCollectionChanged, 17
  • rispettivamente se la proprietà restituisce un singolo valore o una lista di oggetti;  Se si vuole supportare la gestione degli errori, una soluzione possibile è far implementare a source IDataErrorInfo, oppure si crea una classe che eredita da ValidationRule e si associa al binding che interessa. È possibile che si debba fare una conversione tra il valore della proprietà del source e la proprietà del target. Questo può accadere per esempio perché da una parte si ha una stringa, ma dall’altra parte si vuole avere un bool, oppure perché si vuole formattare in un determinato modo una stringa. Per fare questo esistono i ValueConverter, cioè degli oggetti che implementano l’interfaccia IValueConverter. Data Template I Data Template sono dei descrittori: descrivono come deve essere visualizzato un determinato oggetto. Per esempio si può decidere che un oggetto Person sia visualizzato come una Label contenente la stringa Cognome, Nome. A questo punto se in una listbox si inseriscono degli oggetti Person, verranno visualizzate una lista di Label contente la stringa precedentemente creata. Unendo i concetti di Data Template e risorse, è possibile creare una serie di resource dictionary in file separati con i vari Data Template. In questo modo si definisce una volta sola il template per un determinato oggetto. 18
  • Separazione dei ruoli Con il solo utilizzo di tutte le caratteristiche di WPF, designer e developer possono lavorare assieme senza intralciarsi troppo. Il file con il code behind può essere ridotto al minimo grazie all’utilizzo del Data Binding e dei command. Sia il designer che il developer lavorano sul codice XAML: l’unica cosa che deve fare il designer per agevolare il lavoro del developer è quello di associare (o non modificare) gli handler d’evento ai controlli ed eventi corretti. Sarà poi il developer a scrivere il codice all’interno degli handler nel code behind. Inoltre dovrà associare i controlli tramite data binding ai command e agli oggetti corretti. Grazie agli stili, i resource dictionary e ai template il designer può modificare l‘interfaccia utente modificando poco il file xaml. Il developer dal canto suo deve evitare di modificare gli stili associati ai vari controlli nel file xaml. 19
  • Model – View – ViewModel Il pattern Model-View-ViewModel (MVVM) ha come scopo principale quello di separare l’interfaccia utente dall’implementazione. È stato creato pensando principalmente al Data Binding, in modo che sia possibile minimizzare (fino ad annullare) il codice nel code behind della View. Tutto il codice applicativo si trova nel ViewModel, che fa da ponte con il Model. Il designer lavora solo sulla View, il developer su ViewModel. Per collegare View e ViewModel si utilizzano Data Binding e i Command. Lo scopo del pattern MVVM non è quello di non scrivere codice nel code behind della View e nemmeno quello di scrivere migliaia di righe di codice. Ci sono alcune cose che è corretto scrivere nella View, come per esempio la modifica del focus di un controllo. Quest'operazione è possibile farla sia nella View che nel ViewModel: il posto corretto è nella View (ed è molto più semplice). Inoltre se si scrive il codice per gestire il focus nel ViewModel si crea una dipendenza del ViewModel alla View: questo, però è un esempio di cosa NON fare, poiché questo non è un compito del ViewModel, ma della View. 20
  • La cosa importante quando ci si chiede dove inserire una funzionalità è di prendere in considerazione anche il code behind delle View e non escluderlo a priori perché si sta utilizzando il pattern Model-View-ViewModel. Model Il model rappresenta il dominio applicativo, gli oggetti di business dell’applica- zione, i dati. È completamente indipendente dall’interfaccia utente. Per esempio la classe Person di seguito riportata è un esempio di classe appartenente al modello ed ha due proprietà, FirstName e LastName. ViewModel Il ViewModel (VM) è un “Model of a View”. Può essere immaginato come un’astrazione della view, ma nello stesso tempo è anche una specializzazione del model che la view utilizza per il data binding. Il VM è particolarmente utile quando il Model è complesso, o è già esistente e non si può modificare, oppure quando i tipi di dato del model non sono facilmente collegabili alla view. Come regola, ogni User Story ha il suo ViewModel. Mentre non è detto che ci sia una corrispondenza uno a uno tra View e ViewModel. Può capitare di avere molte View con un solo ViewModel o viceversa. Il ViewModel si pone in mezzo tra View e Model e fa da ponte tra i due mondi: quello del dominio applicativo e l’interfaccia utente. Non ha bisogno di conoscere la view: tramite i Data Template si collega il ViewModel alla view. Il ViewModel deve avere delle proprietà che rappresentano gli elementi che 21
  • interessano alla View, oppure esporre direttamente parte del Model. Per esempio deve avere le proprietà FirstName e LastName oppure una proprietà che restituisce un oggetto Person. La prima soluzione è più elegante perché si disaccoppia completamente la View dal Model, ma richiede la scrittura di più codice. La seconda invece obbliga a scrivere gli oggetti di business implementando determinate interfacce, se si vuole che il Data Binding, la visualizzazione degli errori e altre cose funzionino correttamente. Se non si può modificare il Model, la prima soluzione diventa una scelta obbligata. Il ViewModel deve anche avere delle proprietà che restituiscono degli oggetti che implementano l’interfaccia ICommand. Per esempio SaveCommand e CloseCommand. View La view è formata prevalentemente da codice xaml. È formata da elementi visuali, bottoni, finestre e controlli più complessi. Codifica le scorciatoie da tastiera e controlla gli input dei vari device che è responsabilità del controller nel MVC. Spesso è definita dichiarativamente, spesso tramite tool. Ogni View deve conoscere il suo ViewModel associato perché deve agganciare, tramite Data Binding, tutte le proprietà che servono: per esempio FirstName e LastName se la View rappresenta l’anagrafica di un cliente. Inoltre deva 22
  • agganciare, sempre tramite Data Binding, i command ai vari eventi, come per esempio il clic sul bottone “Ok” per salvare le modifiche. Separazione dei ruoli Unendo le potenzialità di WPF al pattern Model – View – ViewModel si ottiene un code behind della Window, o View, che non contiene codice. Questo permette di separare maggiormente il lavoro del designer da quello del developer. Il developer deve creare la logica a partire dal ViewModel. Il designer invece deve fare l’interfaccia utente. L’unico punto di contatto tra i due ruoli si ha nel file XAML: devono coesistere la parte di data binding e command da una parte e la parte di stili e template dall’altra. Se la View è in mano completamente al designer, l’unica cosa che deve sapere per far funzionare correttamente l’interfaccia utente è il contratto esposto dal ViewModel. A questo punto sta al designer agganciare i command che servono, dove servono e recuperare i dati tramite binding. Il developer dal canto suo, può dimenticarsi dell’esistenza della View, il suo lavoro termina con la definizione del contratto e l’implementazione del ViewModel. 23
  • Capitolo due: Il toolkit In questo capitolo si parlerà in dettaglio del toolkit che è stato costruito per agevolare lo sviluppo di applicazioni basate sul pattern Model-View-ViewModel per non dover riscrivere quelle parti di codice che naturalmente si ripetono in ogni progetto. Sarà spiegato quali sono i problemi che si possono riscontrare e come sono stati risolti. Creare un'applicazione WPF senza utilizzare il pattern MVVM ovviamente è possibile, e molto probabilmente l'interfaccia utente è identica a un'applicazione scritta utilizzando il pattern. La vera differenza si nota soprattutto quando si deve manutenere l'applicazione. Se si applica il pattern, ci sarà una divisione netta tra l'interfaccia utente (e le operazioni proprie della UI) e il modello a oggetti. In caso contrario, una modifica fatta a una tabella del database, potrebbe essere molto più difficile da gestire. INotifyPropertyChanged Come già detto in precedenza, se si vuole aggiornare l'interfaccia utente quando cambia il valore di una proprietà messa in binding, si deve implementare l'interfaccia INotifyPropertyChanged. Questo è indipendente dall'uso del pattern MVVM. La differenza risiede in quale classe deve implementare questa interfaccia. 24
  • Se non si utilizza il pattern, e si collegano alla finestra, tramite data binding, direttamente gli oggetti di dominio, allora saranno gli oggetti di dominio stessi a dover implementare l'interfaccia. Se invece si utilizza il pattern MVVM, View e Model non si conoscono, quindi deve essere il ViewModel a implementare INotifyPropertyChanged. Una soluzione per fare implementare questa interfaccia a tutti i ViewModel è fare in modo che tutti loro ereditino da una classe base ViewModelBase. Questa classe avrà tutte le funzionalità comuni, fra cui l'implementazione delle varie interfacce. using System.Componentmodel; using System.Diagnostics; public class ViewModelBase : INotifyPropertyChanged { public ViewModelBase() { } public event PropertyChangedEventHandler PropertyChanged = delegate { }; protected virtual void OnPropertyChanged( string propertyName) { this.VerifyPropertyName(propertyName); var handler = this.PropertyChanged; if (handler != null) 25
  • { handler( this, new PropertyChangedEventArgs(propertyName)); } } [Conditional("DEBUG")] [DebuggerStepThrough] private void VerifyPropertyName (string propName) { if (TypeDescriptor .GetProperties(this)[propName] == null) { var msg = "Invalid property name: " + propName; Debug.Fail(msg); } } } Il metodo VerifyPropertyName serve per bloccare il debug con un errore se il nome della proprietà passato non esiste. Questo può accadere quando si modifica il nome di una proprietà, ma ci si dimentica di cambiare la stringa che si passa a OnPropertyChanged. Di seguito un breve esempio di come si usa questa classe. public class MyViewModel : ViewModelBase { private string myProperty; public string MyProperty { get { return this.myProperty; } set { if (value != this.myProperty) { this.OnPropertyChanged("MyProperty"); 26
  • this.myProperty = value; } } } } Delegate Command Si supponga di dover eseguire quest'operazione: la finestra View1 ha un bottone, al clic bisogna scrivere in una TextBox "Bottone premuto". Se non si applica il pattern MVVM la soluzione più semplice è quella di intercettare l'evento Click del bottone e scrivere il testo nella TextBox. Questo è il flusso di esecuzione: 1. L'utente preme il bottone. 2. Si esegue l'handler associato all'evento Click del bottone. 3. Il codice setta la proprietà Text della TextBox. 27
  • Se invece si implementa il pattern, il primo problema che si incontra è quello di associare il command all'evento Click del bottone. L'altro problema è come fare a scrivere il codice nel ViewModel e non nella View. Ecco il flusso che si vuole ottenere per aggiornare la TextBox al clic del bottone: 1. L'utente preme il bottone. 2. Viene scatenato il comando associato al bottone. 28
  • 3. Si esegue il metodo Execute del comando. 4. Il codice setta la proprietà Text di tipo string. 5. Tramite data binding viene aggiornata l'interfaccia utente. 6. La TextBox contiene il testo "Bottone premuto". Ci sono poi altre due cose che uno sviluppatore vorrebbe avere quando associa un command ad un evento tramite codice xaml: la possibilità di sapere quando si può eseguire quel comando e la possibilità di aggiungere degli shortcut da tastiera. Ci sono, quindi, quattro problemi da risolvere: 1. Scrivere il codice del comando nel ViewModel. 2. Sapere quando un comando può essere eseguito. 3. Aggiungere degli shortcut. 4. Associare il comando al controllo nel codice XAML e minimizzare la scrittura di codice ripetitivo. Scrivere il codice del comando nel ViewModel La soluzione a questo problema è di creare un proprio comando che implementa l'interfaccia System.Windows.Input.ICommand. public interface ICommand { void Execute(object parameter); bool CanExecute(object paramenter); event CanExecuteChanged; } L'unica accortezza è di accettare un delegato nel quale è scritto il codice che si deve eseguire quando si richiama il metodo Execute di ICommand. public class DelegateCommand : ICommand { private readonly Action<object> executeMethod; 29
  • public DelegateCommand(Action<object> executeMethod) { this.executeMethod = executeMethod; } public void ExecuteMethod(object parameter) { if (this.executeMethod == null) return; this.executeMethod(parameter); } // ... } In questo modo, per creare un comando basta passare al costruttore un delegato. Sarà poi compito dell'infrastruttura chiamare il metodo Execute dell'interfac- cia ICommand. Sapere quando un comando può essere eseguito Quest'operazione è già prevista dall'interfaccia ICommand, che ha il metodo CanExecute e l'evento CanExecuteChanged. Bisogna quindi lavorare in maniera analoga al punto precedente, facendo in modo che il costruttore accetti un secondo delegato che verrà poi eseguito dal metodo CanExecute. Inoltre, bisogna far sapere all'infrastruttura di WPF quando deve controllare nuovamente se può eseguire o no il comando. La soluzione più semplice è quella di dire a WPF di controllare ogni volta che succede qualche modifica. Di seguito c'è il nuovo codice: public class DelegateCommand : ICommand { // ... 30
  • private readonly Func<object, bool> canExecuteMethod; public void CanExecuteMethod(object parameter) { if (this.canExecuteMethod == null) return; this.canExecuteMethod(parameter); } public event EventHandler CanExecuteChanged { add { CommandManager.requerySuggested += value; } remove { CommandManager.requerySuggested -= value; } } } Aggiungere degli shortcut Per risolvere questo problema si deve creare una nuova interfaccia IBindableCommand che eredita da ICommand. public interface IBindableCommand : ICommand { InputBindingCollection InputBindings { get; } string DisplayText { get; } } Fatto ciò, si fa implementare a DelegateCommand questa interfaccia e non ICommand. Seguono le aggiunte alla classe DelegateCommand. using System; using System.Windows.Input; public class DelegateCommand : IBindableCommand { // ... 31
  • public void AddGesture(InputGesture gesture) { if (this.inputBindings == null) { this.inputBindings = new InputBindingCollection(); } this.inputBindings.Add( new InputBinding(this, gesture); } public InputBindingCollection Inputbindings { get { return this.inputBindings; } } public string DisplayText { get; private set; } } Associare il comando al controllo nel codice XAML e minimizzare la scrittura di codice ripetitivo Qui le cose da fare sono due: creare una Markup Extension per il codice XAML e associare agli shortcut ai controlli. Una markup extension è un'estensione alla sintassi del codice XAML. È possibile implementare una markup extension per fornire valori per le proprie- tà nell'utilizzo di un attributo, per le proprietà nell'utilizzo di un elemento pro- prietà o in entrambi i casi. Quando è utilizzata per fornire un valore di attributo, la sintassi che distingue un'estensione di markup in un processore XAML è la presenza delle parentesi graffe di apertura e chiusura { e }. Il tipo di estensione di markup è quindi identificato tramite il token di stringa immediatamente successivo alla parentesi graffa di apertura. 32
  • Quando è utilizzata nella sintassi degli elementi proprietà, un'estensione di markup corrisponde visivamente a qualsiasi altro elemento utilizzato per fornire un valore di elemento proprietà, ovvero una dichiarazione di elementi XAML che fa riferimento alla classe dell'estensione di markup come elemento, racchiuso tra parentesi angolari (<>). Spesso si utilizzano le markup extension per trasformare molte righe di codice XAML in un'espressione molto più concisa. Per fare in modo di poter associare direttamente nello XAML un IBindableCommand, si crea una classe BindCommand che eredita da System.Windows.Markup.MarkupExtension. Nel metodo HandleLoaded si associa lo shortcut. using System; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Markup; using ComponentModel; [MarkupExtensionReturnType(typeof(IBindableCommand))] public class BindCommand : MarkupExtension { public BindCommand() { } public BindCommand(string commandName) { this.Name = commandName; } public string Name { get; set; } public override object ProvideValue( IServiceProvider serviceProvider) { var pvt = serviceProvider.GetService( typeof(IProvideValueTarget)) as IProvideValueTarget; 33
  • if (pvt != null) { var fe = pvt.TargetObject as FrameworkElement; if (fe != null) { fe.Loaded += this.HandleLoaded; } } return null; } private void HandleLoaded(object sender, RoutedEventArgs e) { var fe = sender as FrameworkElement; if (fe == null || DesignerProperties.GetIsInDesignMode(fe)) { return; } if (fe.DataContext == null) { return; } // Use reflection to retrieve provided Command. var property = fe.DataContext.GetType().GetProperty(this.Name); // Get root elements. var rootElement = this.GetRootElement(fe); var command = (IBindableCommand) property.GetValue(fe.DataContext, null); var source = sender as ICommandSource; if (source != null) { var button = fe as Button; var menuItem = fe as MenuItem; // Is it a button? if (button != null) { button.Command = command; if (!String.IsNullOrEmpty(command.DisplayText)) { button.Content = new AccessText { Text = command.DisplayText }; } } 34
  • else if (menuItem != null) { // MenuItem. menuItem.Command = command; if (!String.IsNullOrEmpty(command.DisplayText)) { menuItem.Header = new AccessText { Text = command.DisplayText }; } if (command.InputBindings != null) { var keyGesture = command.InputBindings[0].Gesture as KeyGesture; if (keyGesture != null) { menuItem.InputGestureText = keyGesture.DisplayString; } } } // Add commandbindings to root element. if (command.InputBindings != null) { foreach (InputBinding ib in command.InputBindings) { rootElement.InputBindings.Add(ib); } } } // De-register loaded event. fe.Loaded -= this.HandleLoaded; } private FrameworkElement GetRootElement( FrameworkElement fe) { if (fe.Parent == null) { return fe; } return this.GetRootElement( fe.Parent as FrameworkElement); } } 35
  • Come si usa Per utilizzare queste classi, nel ViewModel si crea un BindableCommand. public class MyViewModel { public ICommand MyCommand { get; private set; } public MyViewModel() { this.MyCommand = new DelegateCommand( o => { // TODO: Add Code here... }); } } Nel codice XAML si associa a un controllo il comando. <UserControl x:Class="MyView" xmlns="..." xmlns:x="..." xmlns:cmd= "clr- namespace:Toolkit.Commands;assembly=Toolkit" > <Button Command="{cmd:BindCommand Name=MyCommand}" /> </UserControl> IMessageBroker Si supponga di dover eseguire quest'operazione: una finestra, View1 ha un bottone. Al clic del bottone si deve visualizzare un'altra finestra, View2. Se non si è applica il pattern MVVM, la soluzione più semplice è creare e visualiz- zare View2 direttamente dall'handler associato all'evento Click del bottone. 36
  • Se però si implementa il pattern MVVM, per eseguire un'operazione bisogna passare dal ViewModel. Una prima soluzione che può venire in mente è: 37
  • 1. Associare un comando al clic del bottone e nel metodo Execute, che si trova nel ViewModel e non nella View. 2. Creare ViewModel2 (VM associato a View1). 3. Nel metodo Show di ViewModel2, creare View2 e visualizzare la finestra. Questo scenario però ha un problema:  I due moduli non sono indipendenti: ViewModel1 conosce View- Model2. Per risolvere questo problema si utilizza il broker. Esso è utilizzato per far comu- nicare due moduli che non si conoscono. La soluzione è creare un terzo attore, il broker appunto, che si mette in mezzo ed è conosciuto da entrambi. In questo modo il modulo A se deve inviare qualcosa al modulo B, invia un messaggio al broker che lo consegna al modulo B. Più in dettaglio un modulo sottoscrive un messaggio e quando qualcuno invia quel particolare messaggio, il broker lo recapita a tutti quelli che lo hanno sottoscritto. Il sistema di comunicazione del broker, sottoscrizione tramite delegato e invio del messaggio, è molto simile agli eventi: sottoscrizione tramite handler e lancio dell'evento. L'utilizzo del broker, quindi, è molto simile ad utilizzare gli eventi, però è stato ottenuto il disaccoppiamento tra i moduli. Il compito del "passacarte" è svolto da una classe che implementa l'interfaccia IMessageBroker. 38
  • L'interfaccia ha due metodi per sottoscrivere un messaggio: il sottoscrittore chiede al message broker di essere notificato, invocando il delegato passato come parametro, quando un messaggio di tipo T è inviato; ha cinque metodi per cancellare la sottoscrizione e un metodo per inviare un messaggio. 39
  • Gli unici metodi che hanno bisogno di una spiegazione sono i cinque metodi Unsubscribe. void Unsubscribe(object subscriber); Cancella tutte le sottoscrizioni di un sottoscrittore. void Unsubscribe<T>(object subscriber) where T : IMessage; Cancella tutte le sottoscrizioni di un sottoscrittore per un determinato messaggio T. void Unsubscribe(object subscriber, object sender); Cancella tutte le sottoscrizioni di un sottoscrittore per un determinato sender. void Unsubscribe<T>(object subscriber, object sender) where T : IMessage; Cancella tutte le sottoscrizioni di un sottoscrittore per un determinato sender e solo per un certo tipo di messaggio T. void Unsubscribe<T>( object subscriber, System.Action<T> callback) where T : IMessage; Cancella tutte le sottoscrizioni di un sottoscrittore per una determinata callback. L'interfaccia IMessage è così definita. 40
  • L'utilizzo del message broker per comunicare tra più viewmodel ha anche il vantaggio che il broker stesso non deve sapere a priori, quali saranno i messaggi da inviare. Questo permette di creare messaggi ad hoc per ogni esigenza, conosciuti solo da sender e sottoscrittore. Di seguito è mostrato un esempio di utilizzo del broker per sottoscrivere un messaggio. class LoadGuardianCostListViewModel : IModule { public void Initialize( object sender, IUnityContainer container) { var broker = container.Resolve<IMessageBroker>(); broker.Subscribe<GetAllItemsMessage<GuardianCost>>( sender, message => { var vm = new GuardianCostListViewModel( container, message.Context); vm.Show(); }); } } La classe LoadGuardianListViewModel ha un unico metodo che crea una sottoscrizione a un messaggio che quando viene inviato causa la visualizzazione della view GuardianCostListViewModel. Per inviare invece un messaggio ecco un esempio. this.Broker.Dispatch( new GetAllItemsMessage<GuardianCost>( this, ServerContextRegistry.GetNew(container))) 41
  • ServerContextRegistry.GetNew restituisce una nuova unit of work da utilizzare per lavorare con il database. Container è un container di inversion of control (IoC), nello specifico Unity di Microsoft. Dependency Inversion Principle (DIP) e Inversion of Control (IoC) Secondo il Dependency Inversion Principle7 ogni modulo di alto livello non dovrebbe dipendere da un modo di livello più basso. Entrambi dovrebbero dipendere da un'astrazione. L'astrazione deve essere indipendente dai dettagli. I dettagli dovrebbero dipendere dall'astrazione. In seguito sono mostrati due esempi: il primo non rispetta questo principio, il secondo sì. La grossa differenza sta nel fatto che nel secondo esempio gli oggetti di basso livello sono iniettati nel costruttore e non creati dall'oggetto di alto livello. public class FinanceInfoService { public string GenerateAsHtml(string symbol) { SomeStockFinder finder = new SomeStockFinder(); StockInfo[] stocks = finder.FindQuotesInfo(symbol); HtmlTableRenderer renderer = new HtmlTableRenderer (); return renderer.RenderQuoteInfo(stocks); } //... } L'obiettivo è ottenere una struttura molto simile all'immagine seguente. 7 Il DIP è stato formalizzato da Robert Martin. È possibile leggere altri dettagli sul sito web http://www.objectmentor.com/resources/articles/dip.pdf 42
  • In questo modo, GenerateAsHtml diventa indipendente da SomeStock- Finder e HtmlTableRenderer. public class FinanceInfoService { IFinder finder; IRenderer renderer; public FinanceInfoService( IFinder finder, IRenderer renderer) { this.finder = finder; this.renderer = renderer; } public string GenerateAsHtml(string symbol) { StockInfo[] stocks = finder.FindQuotesInfo(symbol); return renderer.RenderQuoteInfo(stocks); } //... } L'Inversion of Control è l'applicazione del DIP. Spesso IoC è anche detta Dependency Injection (DI). A volte in letteratura si trova che la DI è il principio, 43
  • mentre IoC è l'applicazione del principio. In ogni caso IoC è basato sul pattern DIP. Oggi IoC/DI è spesso associato con dei framework particolari che hanno una serie di caratteristiche. Esistono vari framework di IoC, ma tutti hanno delle caratteristiche comuni: sono costruiti attorno ad un container che, in base ad alcune informazioni di configurazione, risolve le dipendenze. Il chiamante istanzia il container e gli passa l'interfaccia desiderata come parametro. Come risposta, il framework di IoC/DI restituisce un oggetto concreto che implementa quell'interfaccia. In questa tesi, come framework di IoC è stato utilizzato Microsoft Unity Application Block. Struttura del broker Il broker ha un campo privato: un dictionary chiamato subscriptions. private IDictionary<Type, IList<Action<T>>> subscriptions; Questo dictionary usa come chiave il tipo del messaggio che ha una sottoscrizio- ne e come valore una lista di delegati, che devono essere invocati quando è in- viato un messaggio del tipo presente nella chiave. In WPF non è possibile modificare un oggetto dell'interfaccia utente da un thread diverso da quello che ha creato l'oggetto stesso, pena una CrossThreadException. In WPF esiste quindi un oggetto Dispatcher, che evita questo problema. Il suo metodo Invoke, accetta un delegato, che viene eseguito sempre nel thread visuale dell'applicazione. 44
  • Per questo motivo, il broker ha anche un altro campo privato, un dispatcher, che serve al metodo Dispatch per evitare le CrossThreadException. private readonly Dispatcher dispatcher; Sottoscrivere un messaggio Per sottoscrivere un messaggio, si deve chiamare un overload del metodo Subscribe. 45
  • Questo metodo ha almeno un parametro di tipo Action<T> che è l'azione da eseguire quando viene inviato il messaggio e un parametro generico di tipo T che indica a quale messaggio ci si deve sottoscrivere. Quando qualcuno chiama Subscribe, il broker, scopre se il dictionary subscriptions contiene già il messaggio T, e in caso contrario lo aggiunge. Poi aggiunge il delegato alla lista di delegati di quel messaggio. public void Subscribe<T>(Action<T> callback) where T : IMessage { if (this.subscriptions.ContainsKey(typeof(T))) { var subscribers = this.subscriptions[typeof(T)]; subscribers.Add(callback); } else { this.subscriptions.Add( typeof(T), new List<Action<T>>() { callback }); } } Inviare un messaggio Per inviare un messaggio si chiama il metodo Dispatch<T>(). Quando viene chiamato questo metodo, il broker cerca nel dictionary subscriptions se qualcuno ha sottoscritto il messaggio T. In caso affermativo chiama tutti i delegati associati. 46
  • public void Dispatch<T>() where T : IMessage { if (this.subscriptions.ContainsKey(typeof(T))) { var subscribers = this.subscriptions[typeof(T)]; subscribers .ToList() .ForEach(callback => { try { this.dispatcher.Invoke(callback); } catch (Exception e) { if (e.InnerException != null) throw e.InnerException; else throw; } }); } } 47
  • UI Composition Un'applicazione desktop spesso è formata da molte finestre distinte. Una cosa importante nello sviluppo di quel genere di applicazioni è cercare di tenere le varie finestre fra loro indipendenti. Di seguito ci sono alcune definizioni. Modulo Un modulo è una parte dell'applicazione, generalmente visuale. I moduli fra loro devono essere indipendenti e nel caso di moduli visuali sono visualizzati nella shell all'interno di una region. Region La region contiene i moduli visuali. Shell Finestra principale dell'applicazione. Esiste solo una shell e contiene, in varie region, gli altri moduli. La shell non deve conoscere i moduli, conosce solo le region che li conterranno. Se i moduli sono indipendenti fra loro, ci sono una serie di vantaggi:  Possono essere sviluppati indipendentemente l'uno dall'altro.  È possibile mostrare certi moduli a certi utenti ed altri ad altri senza difficoltà: basta non caricare il modulo che non si vuole visualizzare.  È possibile caricare i moduli che interessano solo a runtime, magari utilizzando un file di configurazione. Per implementare il pattern MVVM non c'è nessuna necessità di scrivere applicazioni composite. Creando applicazioni di questo tipo, però, si riesce a modificare o aggiungere una parte d'interfaccia utente senza interferire con tutto il resto dell'applicazione. E questo è un grosso vantaggio. 48
  • Il pattern MVVM è un buon punto di partenza per creare applicazioni composite per due motivi:  Il broker: poiché i moduli non si possono conoscere, sarà lui l'unico in grado di farli comunicare.  L'indipendenza intrinseca dei ViewModel (Moduli se si parla in termini di UI Composition) che è collegata alla regola "Una User Story, un ViewModel": se si aggiungono, o si modificano alcune User Story (cioè alcuni requisiti del software), basta modificare solo il VM direttamente interessato (e la View naturalmente) lasciando inalterato tutto il resto. Per avere un'applicazione composita mancano ancora alcuni dettagli, che saranno spiegati nei prossimi paragrafi. IRegion Il primo problema che si riscontra nello sviluppo di applicazioni composite è quello di fare in modo che la Shell non abbia alcuna conoscenza dei moduli che andrà a visualizzare. 49
  • Per fare ciò, la Shell avrà una o più region. All'interno di ogni region è possibile visualizzare uno o più moduli. Naturalmente anche un modulo a sua volta può contenere una region, nella quale ci sono altri moduli, e così via. Si supponga di dover eseguire quest'operazione: nella finestra principale (Shell), quando si preme un bottone, si deve inserire nella shell stessa una view (View1). Se non applichiamo la UI Composition, la soluzione più semplice è creare View1 e associarlo alla region nell'handler associato evento Click del bottone. Questo è il flusso di esecuzione: 1. L'utente preme il bottone. 2. Si esegue l'handler associato all'evento Click. 50
  • 3. Il codice, che si trova nella Shell, crea un'istanza di View1 e la inserisce nella region. Quest'approccio ha due problemi:  La Shell crea il modulo View1.  Il codice per inserire un modulo nella region si trova sempre nella Shell. Quello che si vuole ottenere è qualcosa di simile all'immagine sottostante. 51
  • Quello che cambia rispetto alla situazione precedente è tutta racchiusa nel punto due:  Non si crea più il modulo nella Shell, ma si recupera un'istanza dal container di IoC.  Il codice per inserire un modulo non si trova più nella Shell, ma nel metodo ShowViewModel della classe region. Se si implementa anche il pattern MVVM, l'unica differenza è che il codice in 2 si troverebbe nel ViewModel e non nella View. Implementazione Una region implementa l'interfaccia IRegion. public interface IRegion { IEnumerable<IWorkspaceViewModel> ViewModels { get; } void ShowViewModel(IWorkspaceViewModel item); bool? ShowDialogViewModel(IWorkspaceViewModel item); } L'interfaccia ha una proprietà che restituisce la lista di moduli che contiene, e due metodi: ShowViewModel e ShowDialogViewModel. Il secondo apre una finestra modale. Come si può notare entrambi i metodi accettano come unico parametro un oggetto che implementa l'interfaccia IWorkspaceViewModel. Questa interfaccia è la base di tutti i moduli visuali che devono essere visualizzati in una region. public interface IWorkspaceViewModel : IViewModel { event EventHandler Closed; ICommand CloseCommand { get; } 52
  • object Content { get; } void Show(); bool? ShowDialog(); } Utilizzando questa struttura, la shell ha almeno una region con all'interno almeno un modulo da visualizzare. Manca ancora qualcosa però: la region ha una lista di ViewModel, mentre bisogna visualizzare le rispettive View. Questo si risolve utilizzando una caratteristica di WPF. In Windows Presentation Foundation può essere visualizzato qualunque oggetto. Se è un oggetto visuale, è visualizzato, altrimenti è chiamato ToString() dell'oggetto per visualizzare una stringa. C'è una terza possibilità: per un oggetto si può definire quale sarà la sua visualizzazione utilizzando i Data Template. <DataTemplate DataType="{x:Type vm:PhoneViewModel}"> <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" > <vw:PhoneView /> </ScrollViewer> </DataTemplate> Questo template nello specifico, visualizza uno ScrollViewer e all'interno la view associata a PhoneViewModel. Il toolkit contiene tre classi diverse che implementano l'interfaccia IRegion:  ContentContainer: può contenere un solo viewmodel.  ListContainer: può contenere più viewmodel.  DialogContainer: apre una finestra modale e può contenere un solo viewmodel. 53
  • IModule Quando si scrivono applicazioni composite formate da tanti moduli, bisogna trovare un modo semplice per inizializzare i moduli stessi. Portando all'estremo deve essere possibile indicare in un file di configurazione quali moduli inizializzare. Quest'ultima cosa non è presente nel toolkit. Quello che è presente, invece, è la possibilità di scoprire a runtime quali sono i moduli e come inizializzarli indicando a compile time solo gli assembly che contengono i moduli stessi. Inizializzazione di un modulo Tornando all'esempio della figura precedente, manca qualcosa: anziché creare un oggetto View, si utilizza il container di IoC per risolvere la dipendenza. Bisogna però ancora capire come si fa a inizializzare View1. In altre parole, manca chi deve eseguire RegisterInstance(new View1()); public interface IModule { void Initialize(object sender, IUnityContainer container); } L'interfaccia ha solo un metodo che inizializza il modulo. Una piccola nota. Il metodo Initialize non restituisce nulla, quindi se si deve recuperare il 54
  • modulo in un secondo momento, nel metodo suddetto si deve inserire il modulo nel container di IoC. In questo modo poi nell'applicazione sarà possibile recuperare il modulo inizializzato. Di seguito c'è un esempio di inizializzazione di un modulo visuale. internal sealed class LoadPersonViewModel : IModule { public void Initialize( object sender, IUnityContainer container) { var broker = container.Resolve<IMessageBroker>(); broker.Subscribe<EditingMessage<Person>>( sender, message => { var myContainer = container; var pvm = new PersonViewModel( message.Context, myContainer, message.Item, true); pvm.Show(); }); } } Questa classe è un modulo non visuale che recupera l'istanza del message broker, e sottoscrive un messaggio. Il delegato che deve essere eseguito quando il messaggio è inviato altro non fa che creare un ViewModel e visualizzarlo. Questo ViewModel è un modulo visuale che sarà visualizzato all'interno di una region. Discovery dei moduli Naturalmente, dopo aver scritto il codice di inizializzazione dei moduli si deve eseguire il codice stesso. Bisogna prima però scoprire quali moduli sono stati creati per l'applicazione. 55
  • Per fare questo, bisogna dire al toolkit quali assembly contengono le classi che implementano l'interfaccia IModule. public static void RegisterModules( IUnityContainer container, Assembly assembly) { if (container == null) throw new ArgumentNullException("container"); if (assembly == null) throw new ArgumentNullException("assembly"); assembly .GetTypes() .Where(t => t.IsInterfaceImplemented<IModule>()) .ForEach(t => container.RegisterType( typeof(IModule), t, t.FullName, new ContainerControlledLifetimeManager())); } Questo metodo controlla tutti i tipi di un assembly alla ricerca di classi che implementano l'interfaccia IModule. Quando ne trova una, la inserisce nel container di IoC come singleton. ForEach e IsInterfaceImplemented sono due extension method. Gli Extension Method consentono di "aggiungere" metodi ai tipi esistenti senza creare un nuovo tipo derivato, ricompilare o modificare in altro modo il tipo originale. Gli extension method sono uno speciale tipo di metodo statico, ma sono chiamati come se fossero metodi d'istanza sul tipo esteso. L'ultima cosa da fare è quella di recuperare i moduli dal container di IoC e inizializzarli. foreach (var item in this.Container.ResolveAll<IModule>()) 56
  • { item.Initialize(this, this.Container); } ViewModel Tutti i ViewModel implementano l'interfaccia IViewModel. Questa interfaccia ha quattro proprietà. IsInDesignMode serve per indicare se il ViewModel è stato creato a runtime oppure se è stato creato all'interno di un designer come Visual Studio o Expression Blend. Quest'operazione è utile soprattutto in Expression Blend 3 per poter associare il ViewModel direttamente alla View: così facendo il designer elenca tutte le property del ViewModel che possono essere messe in Data Binding con la View. Inoltre nel codice è possibile inizializzare il ViewModel con alcuni valori di test solo quando il ViewModel è utilizzato all'interno di un designer. public interface IViewModel : IDisposable, INotifyPropertyChanged { string DisplayName { get; } IMessageBroker Broker { get; } IUnityContainer Container { get; } bool IsInDesignMode { get; } } Ci sono tre interfacce che ereditano da IViewModel:  ICommandViewModel: si utilizza quando un singolo comando deve avere una view per essere inserito in una view container.  IWorkspaceViewModel: tutti i ViewModel che devono essere inseriti in una region devono implementare questa interfaccia che altro non fa che aggiungere alcune funzionalità di visualizzazione e chiusura della view. 57
  •  IShellViewModel: il ViewModel associato alla Shell deve implementare questa interfaccia. Associare una region al ViewModel Quando si crea una classe che implementa IWorkspaceViewModel è indispensabile anche indicare in quale region deve essere inserito. Questa informazione però interessa solamente a runtime, quindi si può utilizzare un attributo per salvare questa informazione e un po' di reflection per recuperarla a runtime. L'attributo ha due proprietà: una che indica il tipo di region, l'altra, opzionale, che rappresenta il nome di quella region all'interno del container di IoC. A runtime si devono recuperare le informazioni presenti nell'attributo per inserire il ViewModel nella region corretta. 58
  • [Region(typeof(ListContainer), Name = "MyRegion")] class MyViewModel : IWorkspaceViewModel { private IRegion region; public MyViewModel() { RegionAttribute regionAttribute; if (this.GetType() .TryGetCustomAttribute(out regionAttribute)) { if (string.IsNullOrEmpty(regionAttribute.Name)) { this.region = (IRegion)container.Resolve( regionAttribute.Region); } else { this.region = (IRegion)container.Resolve( regionAttribute.Region, regionAttribute.Name); } } } public void Show() { this.region.ShowViewModel(this); } // ... } Il codice recupera le informazioni sulla region a runtime e risolve la dipendenza utilizzando il framework di IoC. E questa region sarà poi utilizzata dal metodo Show. TryGetCustomAttribute è un extension method che altro non fa che valorizzare regionAttribute se il tipo è decorato con l'attributo RegionAttribute. L'ultima cosa da fare è quella di creare un'istanza della region nella quale inserire il ViewModel e registrare la stessa nel framework di IoC. 59
  • this.Container.RegisterType<ListContainer>( "MyRegion", new ContainerControlledLifetimeManager()); Questo codice va inserito nella fase di inizializzazione dell'applicazione, cioè nell'Application Controller del quale parleremo a breve. Application Controller L'ultima cosa che rimane da fare è l'inizializzazione dell'applicazione. Le operazioni da fare sono le seguenti:  Creare un container di IoC e registrare tutto quello che serve.  Creare un'unica istanza del MessageBroker.  Creare un'unica istanza della Shell.  Cercare e inizializzare tutti i moduli. Per eseguire queste operazioni per prima cosa si crea una classe base astratta ApplicationControllerBase: questa classe ha tutte le funzionalità indipendenti dalla particolare applicazione. public abstract class ApplicationControllerBase { private readonly IUnityContainer container; private IShell shell; private IMessageBroker broker; protected ApplicationControllerBase( IUnityContainer container) { this.container = container; } public IShell Shell { get { return this.shell; } } 60
  • protected IUnityContainer Container { get { return this.container; } } public void Run() { // Registra il MessageBroker. this.container .RegisterType<IMessageBroker, MessageBroker>( new ContainerControlledLifetimeManager(), new InjectionConstructor( Dispatcher.CurrentDispatcher)); // Trova tutti i moduli presenti negli assembly // e li registra nel container di IoC. foreach (var assembly in this.GetViewModelAssemblies()) Unity.Discovery.RegisterModules( this.Container, assembly); this.broker = this.Container.Resolve<IMessageBroker>(); this.shell = this.CreateShell(); // Recupera tutti i moduli e li inizializza. foreach (var item in this.Container.ResolveAll<IModule>()) item.Initialize(this, this.Container); } protected abstract IShell CreateShell(); protected abstract IEnumerable<Assembly> GetViewModelAssemblies(); } Questa classe ha due metodi astratti che devono essere implementati dalla classe che eredita da essa, ApplicationController, che personalizza il comportamento di default creando la shell e indicando quali sono gli assembly nei quali si devono cercare i moduli. class ApplicationController : ApplicationControllerBase { 61
  • public ApplicationController( IUnityContainer container) : base(container) { this.Container.RegisterType<ListContainer>( new ContainerControlledLifetimeManager()); } protected override IShell CreateShell() { this.Container.RegisterType<IShell, Shell>( new ContainerControlledLifetimeManager()); var shell = this.Container.Resolve<IShell>(); shell.DataContext = new ShellViewModel(this.Container); return shell; } protected override IEnumerable<Assembly> GetViewModelAssemblies() { return new[] { Assembly.GetExecutingAssembly() }; } } Il costruttore registra nel container di IoC l'unica region dell'applicazione. In questa region saranno visualizzati tutti i moduli visuali. Il metodo CreateShell crea un'istanza della shell e setta anche il ViewModel. Il metodo GetViewmodelAssemblies in questo caso restituisce semplice- mente l'assembly corrente. Infine, all'avvio dell'applicazione, si crea un'istanza della classe Application- Controller al quale si passa un'istanza del container di IoC e si chiama il metodo Run(). var controller = new ApplicationController(new UnityContainer()); controller.Run(); 62
  • Capitolo tre: L’applicazione Questo capitolo parlerà di un'applicazione sviluppata utilizzando il toolkit esposto nel capitolo due per realizzare un'interfaccia utente modulare in Windows Presentation Foundation, basata sul pattern Model-View-ViewModel. Obiettivo dell'applicazione Lo scopo primario dell'applicazione è di gestire le palestre di Trieste. Il committente deve gestire le palestre, assegnare i turni alle varie società che vogliono utilizzarle, inviare dei custodi ad aprire e chiudere gli edifici, pagare i custodi stessi per il loro lavoro e farsi pagare dalle società per l'utilizzo delle palestre. Architettura hardware e software dell'ambiente di produzione L'ambiente di produzione è un dominio Active Directory formato da tre macchine server con Windows Server 2008: la prima è un database server con installato SQL Server 2008, la seconda è un web server con IIS 7.0. Il terzo server è il domain controller della rete. 63
  • Inoltre ci sono una serie di macchine client con Windows Vista. Tutti i computer, client e server, hanno installato il .NET Framework 3.5 Service pack 1. Requisiti tecnici non funzionali Il committente vuole un'applicazione desktop progettata in ambiente Microsoft. In questo momento i client sono pochi, ma in un prossimo futuro è molto probabile che aumentino. Quest'applicazione deve essere visibile e utilizzabile solo all'interno della rete interna. L'interfaccia utente dell'applicazione deve essere personalizzabile in funzione dell'utente che utilizzerà l'applicazione, per esempio user o admin. Inoltre, deve essere estendibile con nuove interfacce e moduli in futuro. Architettura dell'applicazione Visti l'architettura hardware e i requisiti tecnici non funzionali, l'applicazione deve essere sviluppata utilizzando una serie di tecnologie. 1. Deve avere un database SQL Server 2008. 2. Deve essere sviluppata in .NET. 3. Deve avere, se serve, un Web Server IIS 7.0 Il non sapere a priori quanti siano i client e il fatto che possano aumentare molto, porta come conseguenza diretta che il server non deve avere alcuna conoscenza dei client e soprattutto non deve fare nessuna assunzione su di essi. Infine, l'interfaccia utente deve essere modulare: questo per migliorare l'estendibilità e la personalizzazione. Detto questo, l'applicazione è sviluppata secondo un'architettura three-tier, ovvero con un database su una macchina, una parte di application server su un secondo server e la parte client da installare su tutti i computer client della rete. 64
  • La comunicazione tra l'application server e i client avviene tramite servizi REST su http. Representational State Transfer – REST REST è l’acronimo di Representational Transfer State, ed è un paradigma per la realizzazione di applicazioni Web che permette la manipolazione delle risorse per mezzo dei metodi GET, POST, PUT e DELETE del protocollo HTTP. Basando le proprie fondamenta sul protocollo HTTP, il paradigma REST restringe il proprio campo d’interesse alle applicazioni che utilizzano questo protocollo per la comunicazione con altri sistemi. 65
  • Il termite REST è stato coniato nel 2000 da Roy Fielding, uno degli autori del protocollo HTTP, per descrivere un sistema che permette di descrivere ed identificare le risorse web8. REST prevede che la scalabilità del Web e la crescita siano diretti risultati di pochi principi chiave di progettazione:  Lo stato dell'applicazione e le funzionalità sono divisi in Risorse WEB.  Ogni risorsa è unica e indirizzabile usando sintassi universale per uso dei link ipertestuali.  Tutte le risorse sono condivise come interfaccia uniforme per il trasferimento di stato tra client e risorse, questo consiste in: o un insieme vincolato di operazioni ben definite; o un insieme vincolato di contenuti, opzionalmente supportato da codice on demand.  Un protocollo che è: o Client-Server; o Stateless; o Cachable; o A livelli. Application Server / Web Server La parte di application server è formata dall'accesso ai dati utilizzando un O/RM, nello specifico Entity Framework di Microsoft. La parte invece di creazione dei servizi REST è creata utilizzando ADO.NET Data Services v1.0, sempre di Microsoft. 8 http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm 66
  • Object / Relational Mapping – O/RM L'Object-Relational Mapping (ORM) è una tecnica di programmazione per convertire dati fra RDBMS e linguaggi di programmazione orientati agli oggetti. In buona sostanza, associa a ogni operazione ed elemento usato nella gestione del database degli oggetti con adeguate proprietà e metodi, astraendo l'utilizzo del database dal DBMS specifico. I principali vantaggi nell'uso di questo sistema sono i seguenti.  Il superamento (più o meno completo) dell'incompatibilità di fondo tra il progetto orientato agli oggetti ed il modello relazionale sul quale è basata la maggior parte degli attuali DBMS utilizzati; con una metafora legata al mondo dell'elettrotecnica, si parla in questo caso di disadattamento dell'impedenza (Impedence Mismatch) tra paradigma relazionale e ad- oggetti (object/relational impedance mismatch).  Un'elevata portabilità rispetto alla tecnologia DMBS utilizzata: cambiando DBMS non devono essere riscritte le routine che implementano lo strato 67
  • di persistenza; generalmente basta cambiare poche righe nella configurazione del prodotto per l'ORM utilizzato.  Facilità d'uso, poiché non si è tenuti a utilizzare direttamente un linguaggio di query come l'SQL (che comunque rimane disponibile per eseguire compiti complessi o che richiedono un'elevata efficienza). I prodotti per l'ORM in questo momento più diffusi, offrono spesso nativamente funzionalità che altrimenti andrebbero realizzate manualmente dal programmatore:  caricamento automatico del grafo degli oggetti secondo i legami di asso- ciazione definiti a livello di linguaggio;  gestione della concorrenza nell'accesso ai dati durante conversazioni;  meccanismi di caching dei dati (con conseguente aumento delle presta- zioni dell'applicazione e riduzione del carico sul sistema RDBMS). Client Il client è sviluppato in Windows Presentation Foundation, utilizzando il toolkit presentato nel capitolo due. Questo tier è formato da tre livelli:  Accesso ai dati e model.  ViewModel.  View. La parte di accesso ai dati e Model è creata utilizzando la parte client di ADO.NET Data Services; i ViewModel e le View si basano sul toolkit. Cosa fondamentale, poiché si parla di MVVM, la View non conosce il Model. 68
  • Attori In questa parte sono riassunti tutti gli attori, siano essi persone fisiche, società o luoghi o oggetti. Committente Il committente è la società che gestisce le palestre situate sul territorio della provincia di Trieste, siano esse comunali o provinciali. Società La società è chi utilizza la palestra. Una società può utilizzare più palestre diverse in orari differenti. Ogni società deve pagare il committente per l’utilizzo della palestra. Il costo dipende dalla categoria della palestra e 69
  • se la società è affiliata al CONI e/o socia del committente. A ogni società sono associati dei turni. Custode Un custode apre e chiude una palestra. Alla fine di ogni mese deve consegnare al committente un documento, nel quale si attesta quante ore ha fatto. Federazione Una federazione sportiva è paragonabile a una società. L'unica differenza è che una federazione fa parte del CONI. Istituto Comprensivo Rappresenta l’ente che ha in gestione le varie scuole del Comune (e le relative palestre). Ogni istituto può avere uno o più palestre. Solo le palestre comunali hanno un istituto comprensivo. Palestra / Scuola Una palestra può essere collegata a un istituto comprensivo e può avere uno o più custodi. Una scuola (o palestra) è o del comune o della provincia. Ogni palestra ha degli orari di disponibilità. 70
  • Sala CONI Una Sala CONI è paragonabile a una palestra. Ci sono in totale tre sale, tutte di proprietà del CONI. È previsto un custode per la pulizia e la custodia. Turno Un turno in una palestra è un orario della settimana nel quale una determinata società può svolgere attività in palestra. Un turno si deve riferire a un anno sportivo. Struttura dell'applicazione Poiché questa tesi è incentrata sull'interfaccia utente, in quest'ultima parte del capitolo si parlerà solamente del client. Il client è un'applicazione modulare. La shell dell'applicazione è formata da due region:  Una ribbon region, nella quale sono contenuti tutti i comandi.  Una tab region che contiene, all'interno di un TabControl, tutte le view che devono essere visualizzate. L'immagine sottostante, per esempio, visualizza cinque view. Ognuna di esse è uno UserControl che viene iniettato nella shell e naturalmente ogni view non ha nessuna conoscenza né della shell, né delle altre view. 71
  • Alcune user story In quest'ultima parte del capitolo saranno elencate tre user story che producono dei ViewModel molto diversi fra loro. Una User Story9 è un requisito di un sistema software formato da una o due frasi scritte il linguaggio corrente o nel linguaggio di business dell'utente. Le user story sono utilizzate nelle metodologie di sviluppo agili10. Ogni user story è scritta generalmente in un piccolo pezzo di carta, per assicurarsi che non cresca troppo. Le user story dovrebbero essere scritte dai clienti di un progetto software, e sono lo strumento principale per influenzare lo sviluppo del software. 9 http://www.agilemodeling.com/artifacts/userStory.htm 10 http://agilemanifesto.org/ e http://www.agilemodeling.com/ 72
  • Le user story sotto elencate rappresentano la maggior parte dei ViewModel possibili. La prima user story produrrà un ViewModel che è collegato a un solo oggetto del modello per eseguire operazioni d'inserimento e modifica dei dati. Anche il secondo è collegato a un solo oggetto del modello, ma esegue delle operazioni di ricerca, e non visualizza tutti i dati presenti nell'oggetto di dominio. Infine, la terza user story è collegata a molti oggetti del modello e visualizza solamente i dati che interessano. US1 – Inserimento e modifica di un custode Un utente deve poter inserire un nuovo custode. Deve poter anche modificare i dati inseriti. 73
  • L'immagine precedente mostra la View quando si deve inserire un nuovo custode, quella sottostante, invece, visualizza la stessa View quando si deve modificare un custode in precedenza inserito. La prima cosa da notare è che la View è identica: indipendentemente dall'operazione da eseguire, non cambia. L'unica differenza degna di nota è il titolo del Tab che passa da "Nuovo" a "Carla Riboni". Capire in quale stato ci si trova è compito del ViewModel, che fra le altre cose modifica anche il titolo della View. Questo è il primo tipo di ViewModel che capita spesso di trovare. Ecco le sue caratteristiche: 74
  •  Comanda più operazioni (inserimento e aggiornamento) su una sola View.  È associato a un solo oggetto del modello.  Espone tutti i campi dell'oggetto. L'utilizzo principale di questo ViewModel è di eseguire operazioni d'inserimento e aggiornamento di un singolo oggetto del dominio. 75
  • Infatti, il toolkit contiene una classe base, ItemViewModelBase, che incapsula tutte le caratteristiche fondamentali per eseguire queste operazioni. US2 – Lista dei custodi Un utente deve poter cercare un custode per cognome. Deve inoltre poter modificare o eliminare il custode appena trovato. Come si può vedere dall'immagine, la View è molto semplice: ha un'area di ricerca e una zona nella quale sono visualizzati i risultati. Per eseguire la ricerca è possibile cliccare sul bottone cerca o premere il tasto Invio dopo aver scritto qualcosa nel box di ricerca. Facendo doppio clic su un custode, si apre una nuova View per modificare i dati. Infine c'è un bottone per eliminare un custode. 76
  • Questo è il secondo tipo di ViewModel molto frequente. Ecco le sue caratteristiche:  Permette la ricerca su un unico oggetto del dominio.  Visualizza i risultati in una lista.  Permette operazioni di aggiornamento e cancellazione.  Espone solo alcuni campi dell'oggetto. 77
  • Poiché anche questa serie di operazioni sono molto frequenti, esiste una classe nel toolkit che raggruppa tutte queste funzioni di base: ListViewModelBase. US3 – Creazione Turno Si deve poter assegnare un turno a una società presso una palestra e il costo che la società deve accollarsi per usufruire dello spazio. Contestualmente si deve anche associare il custode e quanto deve essere dovuto allo stesso per il lavoro svolto. Questa View permette di scegliere ora, giorno e tipo di turno. A essi associa una palestra, una società e un custode. È possibile fare una ricerca per trovare quello che serve. In oltre, in automatico il sistema propone un costo per la palestra e per il custode; costi che possono essere modificati se necessario. 78
  • Questo è l'ultimo tipo di ViewModel. Sono ViewModel generici, che hanno solo una cosa in comune fra loro: recuperano i dati da molti oggetti diversi del modello e poi visualizzano solo quello che è necessario. Per questi ViewModel non esiste una classe base e spesso sono molto complessi da realizzare perché fanno molte cose diverse. In questo caso specifico, queste sono le caratteristiche del ViewModel:  Legge i dati da sei oggetti diversi.  Esegue tre tipi di ricerche manuali.  Esegue due ricerche automatiche.  Permette la modifica delle ricerche automatiche.  Inserisce il turno nel database. 79
  • Conclusioni In questa tesi si è parlato in dettaglio del pattern Model-View-ViewModel, delle sue caratteristiche, vantaggi e svantaggi ed è stata anche proposta un'imple- mentazione. Il pattern è molto giovane, non esistono ancora delle linee guida: per adesso ci sono solamente una serie di problemi e la comunità degli svilup- patori sta proponendo soluzioni diverse. Anche Windows Presentation Foundation è giovane come tecnologia, e, nonostante Microsoft sia spingendo molto, non è ancora entrata a far parte del bagaglio di tutti gli sviluppatori di applicazioni desktop in ambiente Windows. Tutto ciò ha una serie di cause, prima fra tutte, la curva di apprendimento di WPF molto ripida. Inoltre, mancano ancora una serie di controlli che sono presenti in Windows Forms e rendono ancora questa tecnologia appetibile. Infine solo a metà luglio 2009 è uscito Expression Blend 3, il primo designer che finalmente riesce ad esprimere tutte le potenzialità di WPF. In rete si trovano tutta una serie di articoli e toolkit che implementano il pattern MVVM. Tra i più famosi ci sono il MVVM Light Toolkit11 di Laurent Bugnion, Cinch12 di Sasha Barber e Goldlight13 di Peter O'Hanlon. Questi tre toolkit hanno implementazioni anche molto diverse fra loro, ma tutte risolvono i problemi che sono stati affrontati in questa tesi. Se invece, oltre ad affrontare i problemi associati al pattern MVVM, si vuole anche creare un'applicazione modulare sfruttando la UI composition, il toolkit di riferimento e Prism14 sviluppato dal team di Pattern and Practices di Microsoft. 11 Per maggiori informazioni è possibile consultare il sito http://www.galasoft.ch/mvvm/getstarted/ 12 Per maggiori informazioni è possibile consultare il sito http://cinch.codeplex.com/ 13 Per maggiori informazioni è possibile consultare il sito http://goldlight.codeplex.com/ 14 Per maggiori informazioni è possibile consultare il sito http://compositewpf.codeplex.com/ 80
  • La giovane età di WPF e del pattern MVVM possono far pensare che non sia ancora il caso di sviluppare un'applicazione enterprise utilizzando queste tecniche e tecnologie. Io ritengo invece, che nonostante tutti i problemi che la loro giovinezza può portare, ha assolutamente senso sviluppare una nuova applicazione in WPF in sinergia con il pattern MVVM. Questo perché la versatilità e l'espandibilità, sia nel senso delle caratteristiche, che del layout dell'applicazione portano un vantaggio intrinseco che supera notevolmente qualsiasi problema. 81
  • Appendice uno: come scrivere un'applicazione WPF Quest'appendice contiene alcuni suggerimenti per scrivere applicazioni WPF. XAML Conventions XAML è XML e come tutti i formati XML ha una formattazione standard che è bene utilizzare. Ci sono, però, un paio di cose da prendere in considerazione. Utilizzare x:Name al posto di Name Nonostante entrambi gli attributi facciano la stessa cosa, Name può essere utilizzato solo per alcuni elementi, mentre x:Name funziona per tutti gli elementi (eccetto gli elementi in un resource dictionary). Quando si guarda una lunga lista di attributi di un elemento, gli attributi che cominciano con il prefisso "x:" tendono ad essere più facili da vedere. Per rendere più facile trovare il nome di un elemento è sempre meglio utilizzare x:Name. Posizionare il primo attributo di un elemento nella riga sotto il nome dell'elemento Questa regola esiste perché la maggior parte degli editor XML indenta ogni riga allo stesso livello del primo attributo. Per esempio: <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="3" Background="{TemplateBinding Background}"> <Button Content="Hello" Background="{StaticResource Brush_BtnBackground}" Width="300" /> <ImageButton Source="{StaticResource Image_Copy}" Width="16" Height="16" /> 82
  • Il nome degli elementi XML denota il livello d'indentazione, ma per gli attributi XML non è corretto. Se invece si mette il primo attributo in una nuova riga, si ottiene un layout migliore, come questo: <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="3" Background="{TemplateBinding Background}" > <Button Content="Hello" Background="{StaticResource Brush_BtnBackground}" Width="300" /> <ImageButton Source="{StaticResource Image_Copy}" Width="16" Height="16" /> Utilizzando questo sistema, tutti gli attributi dello stesso livello hanno la medesima indentazione. Preferire StaticResource a DynamicResource La differenza tra {StaticResource } e {DynamicResource } consiste soprattutto nella possibilità di cambiare la risorsa a runtime o meno. Una risorsa statica è collegata allo startup dell'applicazione e poi non può essere più modificata. Una risorsa dinamica invece può essere modificata a runtime. È importante sapere quando utilizzare un tipo di risorsa e quando l'altra. Generalmente StaticResource è il default. Purtroppo, tool come Blend, hanno la naturale tendenza a inserire DynamicResource. Combattere contro un tool non è mai una buona idea, per cui questa più che una regola diventa una preferenza. 83
  • Naming Convention per gli elementi Quando si utilizza x:Name su un elemento, cercare di essere consistenti. Per gli elementi in un ControlTemplate, PART_Qualcosa è lo standard per gli elementi che hanno un significato speciale per il controllo al quale si applica il template. Per tutti gli altri elementi, iniziare il nome con il prefisso "_", oppure nomi camel case, o pascal case vanno bene. In ogni caso, non chiamare mai gli elementi in questo modo: "n", o "foo", o "border1". Unire le risorse a livello di Applicazione Quando si utilizzano dei merged resource dictionary, è possibile creare un riferimento al file XAML della risorsa da ogni finestra ed anche nella root, cioè nello XAML del file Application. Il problema nasce quando il resource dictionary è collegato sia al file della window sia alla root. Questo accade spesso quando i controlli sono dichiarati in progetti separati, perché i tool come Blend tendono ad inserire un riferimento in ogni file che utilizza la risorsa. Quello che accade è che ogni volta che il resource dictionary è istanziato, le risorse sono completamente ricaricate, anche se esistono già più in alto nell'albero dei controlli. Organizzazione delle risorse Non appena i progetti WPF crescono, bisogna spostare le risorse dal file App.xaml ai file di risorse. Ma come organizzare tutti questi file? Strutturare le risorse I file XAML possono essere divisi in due categorie:  File XAML funzionali – per esempio Window, UserControl o Page.  File XAML di risorse – Resource Dictionary e il file App.xaml. I file di risorse possono a loro volta essere suddivisi in categorie: 84
  •  File XAML di risorse, generici – questi file contengono cose che possono essere utilizzate trasversalmente da tutta l'applicazione. Brush, ImageSource, alcuni stili e i converter sono ottimi candidati.  File XAML di risorse, specifici – questi file contengono solamente le risorse di una specifica Window o UserControl o Page. Naturalmente è possibile inserire le risorse specifiche di una window direttamente nel file XAML stesso, per esempio nell'elemento Window.Resources. Questo funziona bene all'inizio, poi, però, con il crescere di data template, stili e quant'altro, le risorse diventano tante. A questo punto la soluzione migliore è quella di migrare tutte le risorse in un file collegato che può essere utilizzato solo da questa determinata window. Una struttura d'esempio potrebbe essere questa:  MyWpfApplication/ o /Shared/ – una cartella che contiene tutte le risorse condivise dall'applicazione.  /Converters/ – contiene tutti i converter.  /Images/ – contiene tutte le immagini.  /BrushResources.xaml – file che contiene solid brush, grandient brush eccetera.  /ConverterResources.xaml – crea una risorsa statica per tutti i converter. In questo modo per utilizzare un converter basta collegare questo resource dictionary senza dover prima creare la risorsa. Questo ha come vantaggio che il converter è creato una volta sola.  /ImageResources.xaml – analogamente al precedente, solo che crea una risorsa per tutte le immagini. o /View/ – una cartella che contiene tutte le view di un'applicazione. 85
  •  MyView.xaml – file xaml funzionale che contiene la struttura di MyView.  MyView.resources.xaml – file XAML che contiene tutte le risorse utilizzate solo da MyView. o /ViewModel/ – una cartella che contiene tutti i view model.  MyViewModel.cs – file che contiene il viewmodel associato a MyView. Questa struttura ha il vantaggio di mantenere i file XAML piccoli e permette allo sviluppatore di trovare rapidamente la risorsa che sta cercando. Se un file XAML ha più di 400 righe, probabilmente bisogna decidere se è il caso di suddividere il file. Naming Convention Il problema maggiore di avere molti file di risorse è che non si sa in quale file si trova una risorsa. In C#, è possibile spostare il mouse su una variabile, e Visual Studio mostra una serie d'informazioni, non ultima in quale file quella variabile è definita. Sfortunatamente non esiste nulla di analogo in XAML. Per limitare questo problema si deve utilizzare una naming convention ben precisa. Le regole sono semplici:  Ogni file di risorse deve terminare con Resources.xaml.  Ogni risorsa, o stile, definito nel file XYZResource.xaml deve iniziare con il prefisso "XYZ_". In questo modo, semplicemente leggendo il nome di una risorsa si capisce in quale file è stata definita.  Ogni risorsa definita in un file XAML funzionale deve iniziare con il prefisso "Local_". 86
  • Appendice due: come disegnare una Window in WPF Metà della battaglia di ogni interfaccia utente è organizzare il contenuto in modo che sia attraente, pratico e flessibile. Ma la vera sfida è fare in modo che il layout si adatti da solo alle differenti dimensioni della finestra. In WPF, si controlla il layout utilizzando diversi contenitori. Ogni contenitore ha la sua specifica logica di layout. Se si proviene da Windows Forms, si rimarrà stupiti che utilizzare un layout basato su coordinate, è fortemente scoraggiato in WPF. Al suo posto, sono consigliati layout più flessibili che si possano adattare al cambiamento di contenuto, lingue differenti e alla varietà di dimensioni della finestra. Per la maggior parte degli sviluppatori che si muovono verso WPF, il nuovo sistema di layout è una grande sorpresa, e la prima vera sfida. Una finestra WPF può contenere solo un singolo elemento. Per inserirne più di uno e creare un'interfaccia utente più pratica, c'è bisogno di inserire un contenitore nella finestra e aggiungere gli altri elementi al contenitore. Questa limitazione deriva dal fatto che la classe Windows eredita da ContentControl. In WPF, il layout è determinato dal contenitore che si utilizza. Nonostante ci siano vari controlli che si possono scegliere, una finestra WPF "ideale" segue questi principi:  Gli elementi (come i controlli) non devono avere una dimensione fissa. In questo modo possono crescere in funzione del loro contenuto. Per esempio, un bottone si espande se si aggiunge più testo. Si possono limitare i controlli settando una dimensione minima e massima.  Gli elementi non devono indicare la loro posizione tramite coordinate. In questo modo sono posizionati dal contenitore in funzione della loro dimensione, ordine e altre informazioni che specifica il contenitore di 87
  • layout. Se c'è bisogno di aggiungere spazio tra i controlli si può utilizzare la proprietà Margin.  I contenitori di layout "condividono" lo spazio disponibile con i loro figli. Essi cercano di dare a ogni elemento la dimensione richiesta se c'è spazio libero. Inoltre possono distribuire lo spazio extra tra i figli.  È possibile inserire i controlli di layout uno dentro l'altro. Una tipica interfaccia utente inizia con una Grid, e contiene altri contenitori di layout per posizionare piccoli gruppi di elementi. Tutti i controlli che ereditano da Panel sono controlli di layout. WPF ne mette a disposizione diversi. Ecco i principali:  Canvas: contenitore base. Permette di posizionare i controlli indicando esplicitamente le coordinate.  StackPanel: contenitore molto popolare a causa della sua semplicità di utilizzo. Come il nome suggerisce, posiziona i controlli figli in pila.  WrapPanel: simile allo StackPanel. Oltre a mettere i figli in pila, se lo spazio sulla riga (o colonna) termina, posiziona gli oggetti rimanenti nella riga sottostante (o colonna a destra).  DockPanel: permette di attaccare un elemento a un bordo del pannello.  Grid: contenitore più versatile. Permette di posizionare i figli in una griglia. Lavorare con una Grid per certi versi è come utilizzare una Table in HTML. Grid La Grid è il più potente contenitore di layout presente in WPF. Tutto quello che si può fare con gli altri contenitori si può fare anche con la Grid. La Grid è ottima anche per suddividere la finestra in piccole regioni che si possono controllare tramite altri contenitori. 88
  • <Window x:Class="WpfApplication2.Window1" xmlns="http://.../winfx/2006/xaml/presentation" xmlns:x="http://.../winfx/2006/xaml" Title="Window1" Height="300" Width="300"> <Grid> </Grid> </Window> La Grid separa gli elementi in una griglia invisibile di righe e colonne. Anche se più di un oggetto può stare nella stessa cella, generalmente ha più senso mettere un solo elemento per cella. Naturalmente, questo elemento può essere un altro contenitore che organizza il suo gruppo di elementi. Creare un layout basato su griglia è un processo in due fasi. 1. Scegliere il numero di righe e colonne. <Grid> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> ... </Grid> 2. Assegnare a ogni controllo il suo numero di riga e colonna, per inserirlo nella cella corretta. <Grid> ... <Button Grid.Row="0" Grid.Column="0"> Top Left </Button> 89
  • <Button Grid.Row="0" Grid.Column="1"> Middle Left </Button> <Button Grid.Row="1" Grid.Column="2"> Bottom Right </Button> <Button Grid.Row="1" Grid.Column="1"> Bottom Middle </Button> </Grid> Se la Grid fosse semplicemente una lista di righe e colonne della stessa dimensione non ci sarebbero problemi. Fortunatamente non è così. Per sbloccare tutto il potenziale della Grid si può cambiare il modo che ogni riga e colonna usa per calcolare la dimensione. La Grid supporta tre strategie di dimensionamento:  Dimensione assoluta. È possibile scegliere la dimensione esatta di una riga (o colonna). È la strategia meno utile, perché è la meno flessibile. <ColumnDefinition Width="100" />  Dimensione automatica. Ogni riga (o colonna) ha esattamente la dimensione che serve e non di più. È la strategia più utilizzata. <ColumnDefinition Width="Auto" /> 90
  •  Dimensione proporzionale. Lo spazio è diviso tra tutte le righe (o colonne). Questo è il valore standard per tutte le righe e colonne. <ColumnDefinition Width="*" /> Infine ecco un esempio di utilizzo. <Grid ShowGridLines="True"> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <TextBox Margin="10" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"> This is a test.</TextBox> <Button Margin="10,10,2,10" Padding="3" Grid.Row="1" Grid.Column="1">OK</Button> <Button Margin="2,10,10,10" Padding="3" Grid.Row="1" Grid.Column="2">Cancel</Button> </Grid> 91
  • Bibliografia nd Pro WPF in C# 2008: Windows Presentation Foundation with .NET 3.5, 2 Edition di Matthew MacDonald. Windows Presentation Foundation Unleashed di Adam Nathan. Patterns of Enterprise Application Architecture di Martin Fowler. Design Patterns: Elements of Reusable Object – Oriented Software, di Erich Gamma, Richard Helm, Ralf Johnson, John M. Vlissides. Test – Driven Development: By Example di Kent Beck. Microsoft .NET: Architecting Applications for the Enterprise di Dino Esposito e Andrea Saltarello. Refactoring – Improving the Design of Existing Code di Martin Fowler. Code Complete 2nd Edition di Steve McConnell. Framework Design Guidelines – Conventions, Idioms, and Patterns for Reusable .NET Libraries di Krzysztof Cwalina e Brad Adam. CLR via C# 2nd Edition di Jeffrey Richter. 92
  • Web WPF Disciples: http://groups.google.com/group/wpf-disciplines Blog di Mauro Servienti: http://blogs.ugidotnet.org/topics/Default.aspx Blog di Corrado Cavalli: http://blogs.ugidotnet.org/corrado/Default.aspx Blog di John Gossman: http://blogs.msdn.com/johngossman/default.aspx Blog di Lauren Bugnion: http://geekswithblogs.net/lbugnion/Default.aspx Blog di Josh Smith: http://joshsmithonwpf.wordpress.com/ MSDN Magazine: http://msdn.microsoft.com/en-us/magazine/default.aspx Wikipedia: http://www.wikipedia.org/ Prism: http://www.codeplex.com/CompositeWPF Microsoft Be. IT: http://www.microsoft.com/italy/beit/ 93