Già sento le voci… “noooo, sappiamo già come personalizzare una listbox!”. E’ vero, sul web si possono trovare centinaia di post che spiegano come personalizzare una Listbox, ma visto che personalmente ho faticato un pò per “aggregare” tutte le informazioni, ho pensato di fare un post che dimostri come personalizzare graficamente una Listbox con un tema con gradienti e selezione personalizzata. Il controllo ListBox permette di fatto di ottenere una altissima personalizzazione, ma non è tra i controlli più semplici di WPF da customizzare.
Tutto sarà pensato come “rivestimento” di un’applicazione MVVM, visto che ormai sono “schiavo” di questo pattern per lo sviluppo di client applications. Nell’immagine a destra potete vedere il risultato che vogliamo ottenere.
Il motore
Per prima cosa descriviamo brevemente la parte Model e ViewModel, ovvero l’implementazione in C# delle funzionalità della nostra applicazione. In pratica si tratta di una semplice lista di film che possono essere selezionati.
Necessiteremo di:
- Un Model per il Film
- Un Service che “estrae” i film dalla nostra sorgente dati
- Un ViewModel per il film
- Un ViewModel per la MainWindow
Quindi per quanto riguarda il Model, avremo una classe Film:
1: public class Film
2: {
3:
4: public Film(string title, string originalTitle, string cover, string genre)
5: {
6: this.Title = title;
7: this.OriginalTitle = originalTitle;
8: this.Cover = cover;
9: this.Genre = genre;
10: }
11:
12: public string Title { get; set; }
13: public string OriginalTitle { get; set; }
14: public string Cover { get; set; }
15: public string Genre { get; set; }
16: }
La nostra ListBox si troverà in una window ed utilizzando il pattern MVVM dovremo assegnare al DataContext di questa Window un ViewModel così composto:
1: public class MainViewModel : ViewModelBase
2: {
3: //snip...
4:
5: ObservableCollection<FilmViewModel> _films;
6: public ObservableCollection<FilmViewModel> Films
7: {
8: get
9: {
10: if (_films == null)
11: {
12: _films = new ObservableCollection<FilmViewModel>();
13: Services.IFilmService _filmService = new Services.FilmService();
14: List<Models.Film> _result = _filmService.GetFilms();
15: foreach (Models.Film _film in _result)
16: {
17: _films.Add(new FilmViewModel(_film));
18: }
19: }
20: return _films;
21: }
22: }
23:
24: //Snip...
25: }
Il ViewModel del film è piuttosto banale:
1: public class FilmViewModel
2: {
3: private Models.Film _film;
4:
5: public FilmViewModel(Models.Film film)
6: {
7: _film = film;
8: }
9:
10: public string Title
11: {
12: get
13: {
14: return _film.Title;
15: }
16: }
17:
18: public string OriginalTitle
19: {
20: get
21: {
22: return _film.OriginalTitle;
23: }
24: }
25:
26: public string Cover
27: {
28: get
29: {
30: return _film.Cover;
31: }
32: }
33:
34: public string Genre
35: {
36: get
37: {
38: return _film.Genre;
39: }
40: }
41: }
Queste sono le parti che sono necessarie per implementare le parti di “plumbing” della nostra applicazione. Ora procederemo con la personalizzazione della listbox, operazione decisamente divertente, ma non priva di qualche difficoltà e qualche accorgimento interessante.
La carrozzeria
Per personalizzare la nostra listbox, procederemo per prima cosa impiegando un DataTemplate. Il DataTemplate in questione verrà utilizzato all’interno della list box per visualizzare i film. In pratica si tratta di una View associata al FilmViewModel contenuto nella ObservableCollectionche verrà “databindata” alla ListBox. Come è possibile vedere nello snippet seguente, la ListBox contenuta nella View NonStyledMainView è piuttosto semplice:
1: <Window x:Class="FilmList.Views.NonStyledMainView"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:Views="clr-namespace:FilmList.Views"
5: Title="NonStyledMainView" Height="600" Width="600">
6: <Grid>
7: <ListBox ItemsSource="{Binding Path=Films}" Grid.Column="1" Grid.Row="1"
8: Grid.ColumnSpan="2" Grid.RowSpan="3" Margin="8,8,8,8" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
9: HorizontalContentAlignment="Stretch">
10: <ListBox.ItemTemplate>
11: <DataTemplate>
12: <Views:NonStyledFilmView/>
13: </DataTemplate>
14: </ListBox.ItemTemplate>
15: </ListBox>
16: </Grid>
17: </Window>
Nello specifico, l’item data template è anch’esso una View, NonStyledFilmView, così realizzata:
1: <UserControl x:Class="FilmList.Views.NonStyledFilmView"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:Converters="clr-namespace:FilmList.Converters">
5: <UserControl.Resources>
6: <Converters:ImagePathConverter x:Key="ImagePathConverter"/>
7: </UserControl.Resources>
8: <Grid>
9: <Grid.RowDefinitions>
10: <RowDefinition/>
11: <RowDefinition/>
12: </Grid.RowDefinitions>
13: <Grid.ColumnDefinitions>
14: <ColumnDefinition Width="70"></ColumnDefinition>
15: <ColumnDefinition Width="300"></ColumnDefinition>
16: <ColumnDefinition Width="*"></ColumnDefinition>
17: </Grid.ColumnDefinitions>
18: <Border Grid.Column="0" Grid.RowSpan="2" VerticalAlignment="Stretch" Background="White" Margin="0,0,10,0" CornerRadius="4">
19: <Image Source="{Binding Path=Cover, Converter={StaticResource ImagePathConverter}, ConverterParameter=../Images}" Margin="2,2,2,2" VerticalAlignment="Center" Height="64"/>
20: </Border>
21: <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Path=Title}" MinWidth="50" Margin="0,0,5,0" />
22: <TextBlock Grid.Row="1" Grid.Column="2" Text="{Binding Path=Genre}" FontSize="20" MinWidth="50" Margin="0,0,5,0" />
23: <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Path=OriginalTitle}" FontSize="20" MinWidth="50" Margin="0,0,5,0" />
24: </Grid>
25: </UserControl>
26:
Questo approccio consente di isolare le view e di renderle indipendenti e potenzialmente riusabili. Il risultato non è dei migliori, in quanto:
- mancano bordi, sfondi e gradienti
- la selezione è fissata sul colore azzurro di default di windows (SystemColors.HighlightBrushKey)
- tutto è molto “boring”… dobbiamo mettere un pò di pepe al controllo, altrimenti non va bene (si sa che l’abito fa il monaco ;-))
Nella prossima parte di questo lungo articolo ci focalizzeremo su stili e triggers per ottenere un aspetto gradevole per la nostra ListBox. Per il codice, pazientate fino alla seconda parte dell’articolo.