Technology Experience
.NET World

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:

  1. 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
  2. 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.

 
Fine!
Send to Kindle

Igor Damiani

La sua passione per l'informatica nasce nella prima metà degli anni '80, quando suo padre acquistò un Texas Instruments TI-99. Da allora ha continuato a seguire l'evoluzione sia hardware che software avvenuta nel corso degli anni. E' un utente, un videogiocatore ed uno sviluppatore software a tempo pieno. Igor ha lavorato e lavora anche oggi con le più moderne tecnologie Microsoft per lo sviluppo di applicazioni: .NET Framework, XAML, Universal Windows Platform, su diverse piattaforme, tra cui spiccano Windows 10 piattaforme mobile. Numerose sono le app che Igor ha creato e pubblicato sul marketplace sotto il nome VivendoByte, suo personale marchio di fabbrica. Adora mantenere i contatti attraverso Twitter e soprattutto attraverso gli eventi delle community .NET.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.