Uno degli aspetti di WPF che più mi piace sono i template. Qui si trova una panoramica su ciò che si può fare.
I DataTemplate sono molto comodi quando usiamo MVVM; a parer mio, sono la risposta alla domanda: "Bene ora ho il ViewModel e la View. Come li relaziono tra loro?".
Come funzionano? Tramite i DataTemplate puoi creare una associazione tra uno specifico tipo di dato ed un template che lo rappresenta. Facciamo qualche esempio: supponiamo di avere un ViewModel di tipo CustomerViewModel e di volerlo associare alla sua View, uno UserControl chiamato CustomerDetailView; potremmo scrivere qualcosa del tipo:
<DataTemplate DataType="{x:Type ViewModel:CustomerViewModel}">
<View:CustomerView />
</DataTemplate>
Supponiamo invece di voler definire direttamente all'interno del DataTemplate la struttura del Template; possiamo farlo, per esempio, in questo modo:
<DataTemplate DataType="{x:Type ViewModel:CustomerViewModel}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
...
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Company Name" />
<TextBox Grid.Column="1" Text="{Binding Path=CompanyName}" />
...
</Grid>
</DataTemplate>
In questo modo siamo andati a definire una regola di questo tipo: "quando ti do un CustomerViewModel tu rappresentalo come questo Template".
Questa regola vale all'interno dello scope di azione del template. Se, per esempio, volessimo che i nostri template siano condivisi da più parti della nostra applicazione, sarà sufficiente inserire la loro definizione all'interno di una ResourceDictionary, da includere nelle nostre Window o nei nostri UserControl. Se invece volessimo usare un determinato template solo per una Window o uno UserControl, basterà definirlo all'interno delle loro risorse locali.
Come facciamo quindi, in definitiva, ad usare questi template? Facciamo l'esempio più semplice ovvero visualizziamo dentro una Window il CustomerViewModel di cui sopra:
<Window x:Class="xxx.Presentation.WPF.UI.View.CustomerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.Resources>
<ResourceDictionary Source="../MainResources.xaml" />
</Window.Resources>
<DockPanel>
<HeaderedContentControl
DockPanel.Dock="Top"
Content="{Binding Path=Customer}" />
</DockPanel>
</Window>
Impostando come proprietà Content dell'HeaderedContentControl il Binding corretto (in questo esempio supponiamo di avere come DataContext della Window una classe che contiene una proprietà Customer di tipo CustomerViewModel), visualizzeremo all'interno della Window il template definito sopra. Ovviamente non è obbligatorio usare un HeaderedContentControl.
L'esempio non è un caso eclatante; passiamo quindi ad un esempio un po' più articolato. Supponiamo di voler realizzare una OrderListView che contiene quindi una lista di ordini, con una sezione dedicata ai dettagli dell'ordine selezionato e che presenti un form di ricerca. Per realizzarla potremmo decidere di creare un ViewModel per ogni entità coinvolta: OrderViewModel, OrderSearchFormViewModel e un OrderListViewModel organizzato più o meno così:
public class OrderListViewModel
{
public OrderSearchFormViewModel SearchForm { get; set; }
public List<OrderViewModel> Orders { get; set; }
}
Il processo è poi analogo a quello precedente: ogni ViewModel ha una relativa View; questi sono collegati tra loro con dei DataTemplate. Possiamo quindi costruire uno UserControl in questo modo:
<UserControl x:Class="xxx.Presentation.WPF.UI.View.OrderListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:View="clr-namespace:xxx.Presentation.WPF.UI.View">
<UserControl.Resources>
<ResourceDictionary Source="../MainResources.xaml" />
</UserControl.Resources>
<DockPanel>
<Grid DockPanel.Dock="Top">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter Grid.Row="0" Content="{Binding SearchForm}" />
<ListView Grid.Row="1" Name="OrderList" DataContext="{Binding Path=Orders}">
<ListView.View>
<GridView>
...
</GridView>
</ListView.View>
</ListView>
<ContentPresenter Grid.Row="2" Content="{Binding Path=SelectedItem, ElementName=OrderList}" />
</Grid>
</DockPanel>
</UserControl>
In questo modo abbiamo creato un controllo che dispone i dati sulle tre righe della grid:
- visualizzerà sulla prima riga il SearchForm e lo rappresenterà con il DataTemplate relativo
- la stessa cosa farà per l'ultima riga che verrà visualizzata solo quando selezioniamo un ordine dalla lista
- la ListView centrale invece provvederà a mostrare tutti gli ordini in una GridView: in questo caso il template viene definito in-line
Infine questo UserControl, che altro non è che la OrderListView, verrà associato a OrderListViewModel sempre tramite un DataTemplate e il giro ricomincia.
Quali sono, a mio parere, i vantaggi di questa soluzione? In sostanza tre:
- Centralizzare l'associazione tra View e ViewModel, in modo da poterla modificare in un solo punto se necessario
- La possibilità di legare strettamente il metodo di rappresentazione al tipo rappresentato: se nell'esempio di prima la lista di OrderViewModel contenesse istanze diverse, per esempio StockOrderViewModel e SalesOrderViewModel, rappresentate da View diverse, il ContentPresenter verrebbe rappresentanto sempre con la View corretta senza dover fare nessuna modifica al codice XAML
- La possibilità di rendere condizionale la scelta del DataTemplate (questo punto lo discuterò in un prossimo post)
Matteo