UWP, MVVM e ListView con multiselezione
Il problema di oggi è quello di avere un’app UWP (Universal Windows Platform), sviluppata seguendo la tecnica del Modem-View-ViewModel, che mostra una ListView con attivata la multiselezione. Ci sono alcuni passaggi che dobbiamo risolvere.
Innanzitutto, l’oggetto ListView espone la proprietà SelectedItem (singolare) ma non una proprietà SelectedItems. Quindi dobbiamo trovare un modo per bypassare il problema. Io ho risolto scrivendo una classe statica SelectionManager, che espone una proprietà List di tipo IList. Da viewmodel posso fare binding su questa dependency property con una proprietà ObservableCollection. All’interno, questa classe sottoscrivere l’evento SelectionChanged della ListView; ogni volta che l’utente seleziona o deseleziona un ListViewItem la lista viene sincronizzata.
public static class SelectionManager
{
public static IList GetList(DependencyObject obj)
{
return (IList)obj.GetValue(ListProperty);
}
public static void SetList(DependencyObject obj, IList value)
{
obj.SetValue(ListProperty, value);
}
public static readonly DependencyProperty ListProperty =
DependencyProperty.RegisterAttached("List",
typeof(IList),
typeof(SelectionManager),
new PropertyMetadata(null,
new PropertyChangedCallback(SetupList)));
private static void SetupList(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
ListViewBase lst = d as ListViewBase;
lst.SelectionChanged += Lst_SelectionChanged;
}
private static void Lst_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
ListViewBase element = sender as ListViewBase;
var arrayList = GetList(element);
foreach (object o in e.AddedItems)
{
arrayList.Add(o);
}
foreach (object o in e.RemovedItems)
{
if (arrayList.Contains(o))
{
arrayList.Remove(o);
}
}
}
}
A livello di ViewModel espongo due ObservableCollection pubblici: una funge come ItemsSource per la ListView stessa (grazie alla quale la ListView mostra l’elenco degli Items), l’altra invece è una ObservableCollection che mantiene costantemente aggiornato l’elenco degli Items selezionati. Ecco uno stralcio di XAML per rendere evidente questa cosa.
<ListView
ItemsSource="{Binding Cars}"
SelectionMode="Extended"
hlp:SelectionManager.List="{Binding SelectedCars}" />
La proprietà Cars è una ObservableCollection<Car> che fa da datasource alla ListView, come si fa di solito. La proprietà SelectedCars è un’altra ObservableCollection<Car> dedicata solamente al mantenimento degli items correntemente selezionati.
Un altro approccio che NON ho preferito
Un altro approccio che avrei potuto seguire è quello di passare al mio SelectionManager la stessa ObservableCollection impostata su ItemsSource (nel mio caso qui sopra, quindi, Cars). Con questa logica la mia SelectionManager avrebbe potuto impostarmi una proprietà booleana IsSelected su ciascuna delle istanze di Car che risultano selezionate.
Non ho preferito questa strada per due ragioni:
- Così facendo sarai stato costretto ad aggiungere una proprietà bool IsSelected sul mio viewmodel, cosa che non sempre vorrei/potrei fare. D’altro canto, invece, SelectionManager è in grado di lavorare con qualsiasi tipo di classe. Quando un oggetto risulta selezionato sulla UI esso viene aggiunto alla lista in binding; quando viene deselezionato l’oggetto viene rimosso
- Avendo una proprietà dedicata alla multiselezione, posso fare immediatamente binding sulla mia UI. Nel mio esempio ho una proprietà SelectedCars pronta all’uso ed aggiornata, quindi è semplicissimo aggiungere al visual tree una TextBlock la cui proprietà Text fa binding con un {Binding Path=SelectedCars.Count} (per visualizzare il numero di elementi selezionati). Con questo secondo approccio, invece, avrei un’unica proprietà Cars, e per ottenere l’elenco degli Items selezionati devo continuamente giocare con LINQ e fare una query per ottenere le Cars selezionate
Se serve, posso aggiungere un Command ed un CommandParameter
Una volta sviluppata la classe SelectionManager con il codice qui sopra, è un gioco da ragazzi aggiungere due nuove dependency property: una di tipo ICommand ed una di tipo object. Lo scopo in questo caso è quello potenzialmente di andare ad eseguire un Command sul mio viewmodel dopo che la selezione è stata gestita (se serve, mica è obbligatorio).
Ecco il codice completo della classe SelectionManager:
public static class SelectionManager
{
public static IList GetList(DependencyObject obj)
{
return (IList)obj.GetValue(ListProperty);
}
public static void SetList(DependencyObject obj, IList value)
{
obj.SetValue(ListProperty, value);
}
public static readonly DependencyProperty ListProperty =
DependencyProperty.RegisterAttached("List",
typeof(IList),
typeof(SelectionManager),
new PropertyMetadata(null,
new PropertyChangedCallback(SetupList)));
public static ICommand GetCommand(DependencyObject obj)
{
return (ICommand)obj.GetValue(CommandProperty);
}
public static void SetCommand(DependencyObject obj, ICommand value)
{
obj.SetValue(CommandProperty, value);
}
public static readonly DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command",
typeof(ICommand),
typeof(SelectionManager),
new PropertyMetadata(null,
new PropertyChangedCallback(SetupList)));
public static object GetCommandParameter(DependencyObject obj)
{
return obj.GetValue(CommandParameterProperty);
}
public static void SetCommandParameter(DependencyObject obj,
object value)
{
obj.SetValue(CommandParameterProperty, value);
}
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.RegisterAttached("CommandParameter",
typeof(object),
typeof(SelectionManager),
new PropertyMetadata(null));
private static void SetupList(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
ListViewBase lst = d as ListViewBase;
lst.SelectionChanged += Lst_SelectionChanged;
}
private static void Lst_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
ListViewBase element = sender as ListViewBase;
var arrayList = GetList(element);
foreach (object o in e.AddedItems)
{
arrayList.Add(o);
}
foreach (object o in e.RemovedItems)
{
if (arrayList.Contains(o))
{
arrayList.Remove(o);
}
}
var command = GetCommand(element);
if (command != null)
{
var parameter = element.GetValue(CommandParameterProperty);
if (command.CanExecute(parameter))
{
command.Execute(parameter);
}
}
}
}
Dopo che l’evento SelectionChanged è stato scatenato, la proprietà del viewmodel viene sincronizzata con la selezione corrente. Dopodichè, se c’è un Command in binding viene eseguito, passando anche il suo eventuale parametro object.
