In questo post spiegherò come modificare il DataTemplateSelector descritto da WindowsPhoneGeek per ottenere i seguenti benefici e miglioramenti:
- Spostare le definizioni dei DataTemplate dalla ListBox alle risorse della pagina o dell'intera applicazione
- Vedere il risultato della modifica dei data template a design time.
Prima di modificare il codice, vediamo come funziona un DataTemplateSelector. Con Silverlight per Windows Phone non abbiamo a disposizione una classe nativa di tipo DataTemplateSelector, come in WPF, quindi abbiamo bisogno di costruircela da zero.
Ipotizzando di avere:
- Una ListBox
- Una ObservableCollection di Item come source della ListBox
- Degli item che possono essere di diverso tipo, ad esempio A, B or C, con una proprietà dell'Item che contiene un valore che ne identifica il tipo.
- Abbiamo un data template per ogni tipo di Item
L'obiettivo è quello di mostrare nella lista ciascun item utilizzando il data template corrispondente al tipo di item, come mostrato in figura:
Per ottenere tale risultato, dobbiamo impostare la proprietà ItemTemplate della ListBox a qualcosa che cambia dinamicamente a runtime, sulla base del valore contenuto nella proprietà che descrive il tipo di item:
Quindi "SomeMagicElement" dev'essere qualcosa che abbia:
- Un evento che venga chiamato ogni qual volta un item dev'essere renderizzato;
- Una proprietà Content che contenga il riferimento all'item che dev'essere renderizzato;
- Una proprietà DataTemplate che si possa dinamicamente impostare attraverso una nostra logica ogni volta che viene scatenato l'evento.
E qui calza a pennello la classe ContentControl, osservando la sua definizione:
Grazie alla natura di XAML, bastata sulla composizione recursiva dell'interfaccia utente, possiamo tranquillamente utilizzare questo component come DataTemplate dell'ItemTemplate della ListBox.
L'unica cosa che manca è il punto 3, ma possiamo implementarlo con una classe derivata da ContentControl, che chiamiamo "DataTemplateSelector", nella quale facciamo l'override del metodo OnContentChanged aggiungendo una chiamata al metodo "SelectTemplate", nel quale viene implementata la logica di selezione del data template da utilizzare:
Questa però non si rivela una buona scelta perché nella nostra applicazione possiamo avere svariati usi del DataTemplateSelector, con tipi diversi e logiche diverse. Un approccio migliore è quello di creare una classe astratta con un metodo virtuale, in modo da poter derivare da essa una varietà di selettori personalizzati, per ogni necessità.
Una prima implementazione di tale classe astratta è la seguente:
Qui di seguito un esempio di un selettore personalizzato:
Si può facilmente notare che è una classe derivata dalla classe astratta DataTemplateSelector e che è stato fatto l'override del metodo SelectTemplate.
Quello che però a prima vista è poco chiaro è il motivo per il quale sono state aggiunte le tre proprietà di tipo DataTemplate. Il motivo è presto detto e risiede nel modo in cui vengono passate le definizioni dei data template tramite XAML:
La proprietà DataTemplate dell'ItemTemplate viene impostata con una istanza del CustomTemplateSelector, e al suo interno vengono impostate le proprietà DataTemplateA, DataTemplateB e DataTemplateC. In questo modo il CustomTemplateSelector può usare i valori contenuti in quelle proprietà per restituire il data template associato ad ogni tipo di item.
E con questo termina la spiegazione di quanto proposto da WindowsPhoneGeek nel suo articolo.
Ciò non di meno, permangono due problemi di non poco conto:
- Non è possibile visualizzare la ListBox a design time ed è quindi necessario avviare l'applicazione per testare le modifiche ai data template, con grande dispendio di tempo.
- Le definizioni dei data template sono inserite all'interno della ListBox. Se abbiamo più di una ListBox nella nostra applicazione che utilizza gli stessi data template avremo necessariamente una duplicazione di codice, e questa non è mai una buona cosa.
Qui di seguito l'elenco e la spiegazione delle modifiche da me apportate al fine di risolvere entrambi I problemi.
Prima di tutto, utilizziamo un contenitore per il DataContext della pagina (una classe ViewModel, in gergo MVVM…):
Poiché non vogliamo che il DataContext venga impostato solo a runtime, non è possibile utilizzare la classica modalità da code behind:
Andiamo quindi a definire il DataContext della pagina direttamente in XAML. Per fare ciò, dobbiamo definire innanzitutto un nuovo namespace e tramite questo andiamo a impostare la proprietà DataContext della pagina stessa:
In questo modo la classe MainViewModel viene istanziata a design time. Ciò nonostante non saremo ancora in grado di vedere nulla perché l' observable collection non sarà stata ancora inizializzata. Per aggiungere alcuni dati di prova possiamo utilizzare un trucchetto: basterà testare la proprietà statica "IsInDesignModeProperty" della classe statica "DesignerProperties" all'interno del costruttore di MainViewModel e se vero chiamare il metodo "LoadFakeData":
Questo risolve completamente il primo problema perché è ora possibile visualizzare la ListBox e le eventuali modifiche ai data template direttamente a design time:
Per quanto invece riguarda il secondo problema, abbiamo bisogno di modifiche più radicali perché dobbiamo cambiare il modo in cui il nostro "CustomTemplateSelector" ottiene le definizioni dei data template. Infatti, spostando tali definizioni fuori dalla ListBox perdiamo la possibilità di impostarne le proprietà.
La soluzione sta nel fare a meno di tali proprietà, andando a recuperare le definizioni cercandole direttamente nei dizionari delle risorse della pagina e/o dell'applicazione. Ciò però pone subito due nuovi problemi da risolvere:
- Dobbiamo in qualche modo mantenere traccia delle definizioni trovate, senza doverle ricercare ogni volta visto che il "CustomTemplateSelector" verrà richiamato molte volte, una per ogni volta che la ListBox ha bisogno di renderizzare un item. In caso contrario si avrebbe una sicura penalizzazione delle prestazioni.
- Ottenere il riferimento al dizionario delle risorse dell'applicazione, all'interno del metodo "SelectTemplate" è relativamente facile, ma ottenere quello relativo al dizionario delle risorse della pagina lo è molto meno.
Per risolvere il primo basta aggiungere un dizionario nella classe astratta:
Tale dizionario conterrà I risultati delle ricerche effettuate, in modo da non doverle ripetere.
Il secondo problema si risolve cercando prima nel dizionario delle risorse dell'applicazione, infine utilizzando il metodo "GetParent" della classe statica "VisualTreeHelper", a partire dal contenitore dell'item da renderizzare, fino alla pagina che contiene la ListBox e quindi al dizionario delle risorse ti tale pagina:
Avendo quindi spostato le definizioni al di fuori della ListBox, in questo caso dentro le risorse della pagina, si ha:
Con il nuovo DataTemplateSelector dotato di dizionario e metodo di ricerca:
Otteniamo un "CustomTemplateSelector" molto più pulito perché contenente solo la logica di selezione del data template:
Qui potete scaricare la soluzione completa: http://sdrv.ms/1lGje5J
That's all folks!