Come supportare ListBox SelectedItems vincolante con MVVM in un’applicazione navigabile

Sto facendo un’applicazione WPF che è navigabile tramite i pulsanti e i comandi personalizzati “Avanti” e “Indietro” (cioè non usando una NavigationWindow ). In una schermata, ho un ListBox che deve supportare selezioni multiple (usando la modalità Extended ). Dispongo di un modello di visualizzazione per questa schermata e memorizzo gli elementi selezionati come proprietà, poiché devono essere mantenuti.

Tuttavia, sono a conoscenza che la proprietà SelectedItems di un object ListBox è di sola lettura. Ho cercato di risolvere il problema utilizzando questa soluzione qui , ma non sono stato in grado di adottarlo nella mia implementazione. Ho scoperto che non posso distinguere tra quando uno o più elementi sono deselezionati e quando NotifyCollectionChangedAction.Remove tra le schermate ( NotifyCollectionChangedAction.Remove viene generato in entrambi i casi, poiché tecnicamente tutti gli elementi selezionati vengono deselezionati quando si naviga lontano dallo schermo). I miei comandi di navigazione si trovano in un modello di visualizzazione separato che gestisce i modelli di visualizzazione per ogni schermata, quindi non posso inserire alcuna implementazione relativa al modello di visualizzazione con ListBox .

Ho trovato diverse altre soluzioni meno eleganti, ma nessuna di queste sembra imporre un legame bidirezionale tra il modello di vista e la vista.

Qualsiasi aiuto sarebbe molto apprezzato. Posso fornire un po ‘del mio codice sorgente se possa aiutare a capire il mio problema.

Prova a creare una proprietà IsSelected su ciascun elemento di dati e ListBoxItem.IsSelected a quella proprietà

  

Le soluzioni di Rachel funzionano alla grande! Ma c’è un problema che ho riscontrato: se si sostituisce lo stile di ListBoxItem , si perde lo stile originale applicato (nel mio caso è responsabile per evidenziare l’elemento selezionato ecc.). Puoi evitarlo ereditando dallo stile originale:

  

Nota impostazione BasedOn (vedere questa risposta ).

Non riuscivo a ottenere la soluzione di Rachel per funzionare come volevo, ma ho trovato la risposta di Sandesh di creare una proprietà di dipendenza personalizzata per funzionare perfettamente per me. Ho appena dovuto scrivere un codice simile per un ListBox:

 public class ListBoxCustom : ListBox { public ListBoxCustom() { SelectionChanged += ListBoxCustom_SelectionChanged; } void ListBoxCustom_SelectionChanged(object sender, SelectionChangedEventArgs e) { SelectedItemsList = SelectedItems; } public IList SelectedItemsList { get { return (IList)GetValue(SelectedItemsListProperty); } set { SetValue(SelectedItemsListProperty, value); } } public static readonly DependencyProperty SelectedItemsListProperty = DependencyProperty.Register("SelectedItemsList", typeof(IList), typeof(ListBoxCustom), new PropertyMetadata(null)); } 

Nel mio View Model ho appena fatto riferimento a quella proprietà per ottenere la mia lista selezionata.

Ho continuato a cercare una soluzione facile ma senza fortuna.

La soluzione che Rachel ha è buona se hai già la proprietà Selected sull’object all’interno di ItemsSource. In caso contrario, è necessario creare un modello per quel modello aziendale.

Sono andato su una strada diversa. Uno veloce, ma non perfetto.

Sulla tua ListBox crea un evento per SelectionChanged.

  

Ora implementa l’evento sul codice sottostante della tua pagina XAML.

 private void lstBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) { var listSelectedItems = ((ListBox) sender).SelectedItems; ViewModel.YourListThatNeedsBinding = listSelectedItems.Cast().ToList(); } 

Tada. Fatto.

Questo è stato fatto con l’aiuto di convertire SelectedItemCollection in una lista .

Non soddisfatto delle risposte date stavo cercando di trovarne uno da solo … Beh, risulta essere più come un hack che una soluzione, ma per me funziona bene. Questa soluzione utilizza MultiBindings in un modo speciale. In primo luogo può sembrare una tonnellata di codice, ma puoi riutilizzarlo con pochissimo sforzo.

Per prima cosa ho implementato un ‘IMultiValueConverter’

 public class SelectedItemsMerger : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { SelectedItemsContainer sic = values[1] as SelectedItemsContainer; if (sic != null) sic.SelectedItems = values[0]; return values[0]; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { return new[] { value }; } } 

E un contenitore / involucro SelectedItems:

 public class SelectedItemsContainer { /// Nothing special here... public object SelectedItems { get; set; } } 

Ora creiamo il binding per il nostro ListBox.SelectedItem (Singular). Nota: è necessario creare una risorsa statica per il “convertitore”. Questo può essere fatto una volta per applicazione ed essere riutilizzato per tutti i ListBox che necessitano del convertitore.

       

Nel ViewModel ho creato il contenitore dove posso bind. È importante inizializzarlo con new () per riempirlo con i valori.

  SelectedItemsContainer selectionContainer = new SelectedItemsContainer(); public SelectedItemsContainer SelectionContainer { get { return this.selectionContainer; } set { if (this.selectionContainer != value) { this.selectionContainer = value; this.OnPropertyChanged("SelectionContainer"); } } } 

E questo è tutto. Forse qualcuno vede dei miglioramenti? Cosa ne pensi?

Ecco un’altra soluzione. È simile alla risposta di Ben, ma l’associazione funziona in due modi. Il trucco consiste nell’aggiornare gli elementi selezionati del ListBox quando cambiano gli elementi di dati associati.

 public class MultipleSelectionListBox : ListBox { public static readonly DependencyProperty BindableSelectedItemsProperty = DependencyProperty.Register("BindableSelectedItems", typeof(IEnumerable), typeof(MultipleSelectionListBox), new FrameworkPropertyMetadata(default(IEnumerable), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged)); public IEnumerable BindableSelectedItems { get => (IEnumerable)GetValue(BindableSelectedItemsProperty); set => SetValue(BindableSelectedItemsProperty, value); } protected override void OnSelectionChanged(SelectionChangedEventArgs e) { base.OnSelectionChanged(e); BindableSelectedItems = SelectedItems.Cast(); } private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is MultipleSelectionListBox listBox) listBox.SetSelectedItems(listBox.BindableSelectedItems); } } 

Sfortunatamente, non ero in grado di usare IList come tipo BindableSelectedItems. In tal modo viene inviato null alla proprietà del mio modello di visualizzazione, il cui tipo è IEnumerable .

Ecco la XAML:

  

C’è una cosa a cui prestare attenzione. Nel mio caso, un ListBox può essere rimosso dalla vista. Per qualche motivo, questo fa sì che la proprietà SelectedItems trasformi in una lista vuota. Questo, a sua volta, fa sì che la proprietà del modello di vista venga modificata in una lista vuota. A seconda del tuo caso d’uso, questo potrebbe non essere desiderabile.

Questo è stato un grosso problema per me, alcune delle risposte che ho visto erano o troppo logiche o necessarie per reimpostare il valore della proprietà SelectedItems infrangendo qualsiasi codice associato all’evento OnCollectionChanged delle proprietà. Ma sono riuscito a ottenere una soluzione praticabile modificando direttamente la collezione e come bonus supporta anche SelectedValuePath per le raccolte di oggetti.

 public class MultipleSelectionListBox : ListBox { internal bool processSelectionChanges = false; public static readonly DependencyProperty BindableSelectedItemsProperty = DependencyProperty.Register("BindableSelectedItems", typeof(object), typeof(MultipleSelectionListBox), new FrameworkPropertyMetadata(default(ICollection), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged)); public dynamic BindableSelectedItems { get => GetValue(BindableSelectedItemsProperty); set => SetValue(BindableSelectedItemsProperty, value); } protected override void OnSelectionChanged(SelectionChangedEventArgs e) { base.OnSelectionChanged(e); if (BindableSelectedItems == null || !this.IsInitialized) return; //Handle pre initilized calls if (e.AddedItems.Count > 0) if (!string.IsNullOrWhiteSpace(SelectedValuePath)) { foreach (var item in e.AddedItems) if (!BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null))) BindableSelectedItems.Add((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)); } else { foreach (var item in e.AddedItems) if (!BindableSelectedItems.Contains((dynamic)item)) BindableSelectedItems.Add((dynamic)item); } if (e.RemovedItems.Count > 0) if (!string.IsNullOrWhiteSpace(SelectedValuePath)) { foreach (var item in e.RemovedItems) if (BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null))) BindableSelectedItems.Remove((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)); } else { foreach (var item in e.RemovedItems) if (BindableSelectedItems.Contains((dynamic)item)) BindableSelectedItems.Remove((dynamic)item); } } private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is MultipleSelectionListBox listBox) { List newSelection = new List(); if (!string.IsNullOrWhiteSpace(listBox.SelectedValuePath)) foreach (var item in listBox.BindableSelectedItems) { foreach (var lbItem in listBox.Items) { var lbItemValue = lbItem.GetType().GetProperty(listBox.SelectedValuePath).GetValue(lbItem, null); if ((dynamic)lbItemValue == (dynamic)item) newSelection.Add(lbItem); } } else newSelection = listBox.BindableSelectedItems as List; listBox.SetSelectedItems(newSelection); } } } 

L’associazione funziona come ci si sarebbe aspettati che la SM si fosse comportata da sola:

  

Non è stato testato a fondo ma ha superato le prime ispezioni. Ho cercato di mantenerlo riutilizzabile utilizzando tipi dinamici nelle raccolte.

Risulta vincolante una casella di controllo per la proprietà IsSelected e mettendo il blocco di testo e la casella di controllo all’interno di un pannello di stack fa il trucco!