'How to support ListBox SelectedItems binding with MVVM in a navigable application
I am making a WPF application that is navigable via custom "Next" and "Back" buttons and commands (i.e. not using a NavigationWindow
). On one screen, I have a ListBox
that has to support multiple selections (using the Extended
mode). I have a view model for this screen and store the selected items as a property, since they need to be maintained.
However, I am aware that the SelectedItems
property of a ListBox
is read-only. I have been trying to work around the issue using this solution here, but I have not been able to adopt it into my implementation. I found that I can't differentiate between when one or more elements are deselected and when I navigate between screens (NotifyCollectionChangedAction.Remove
is raised in both cases, since technically all the selected items are deselected when navigating away from the screen). My navigation commands are located in a separate view model which manages the view models for each screen, so I can't put any implementation related to the view model with the ListBox
in there.
I have found several other less elegant solutions, but none of these seem to enforce a two-way binding between the view model and the view.
Any help would be greatly appreciated. I can provide some of my source code if it would help to understand my problem.
Solution 1:[1]
Try creating an IsSelected
property on each of your data items and binding ListBoxItem.IsSelected
to that property
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
Solution 2:[2]
Rachel's solutions works great! But there is one problem I've encountered - if you override the style of ListBoxItem
, you loose the original styling applied to it (in my case responsible for highlighting the selected item etc.). You can avoid this by inheriting from the original style:
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
Note setting BasedOn
(see this answer)
.
Solution 3:[3]
I couldn't get Rachel's solution to work how I wanted it, but I found Sandesh's answer of creating a custom dependency property to work perfectly for me. I just had to write similar code for a 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(nameof(SelectedItemsList), typeof(IList), typeof(ListBoxCustom), new PropertyMetadata(null));
}
In my View Model I just referenced that property to get my selected list.
Solution 4:[4]
I kept looking into an easy solution for this but with no luck.
The solution Rachel has is good if you already have the Selected property on the object within your ItemsSource. If you do not, you have to create a Model for that business model.
I went a different route. A quick one, but not perfect.
On your ListBox create an event for SelectionChanged.
<ListBox ItemsSource="{Binding SomeItemsSource}"
SelectionMode="Multiple"
SelectionChanged="lstBox_OnSelectionChanged" />
Now implement the event on the code behind of your XAML page.
private void lstBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listSelectedItems = ((ListBox) sender).SelectedItems;
ViewModel.YourListThatNeedsBinding = listSelectedItems.Cast<ObjectType>().ToList();
}
Tada. Done.
This was done with the help of converting SelectedItemCollection to a List.
Solution 5:[5]
Here's yet another solution. It's similar to Ben's answer, but the binding works two ways. The trick is to update the ListBox
's selected items when the bound data items change.
public class MultipleSelectionListBox : ListBox
{
public static readonly DependencyProperty BindableSelectedItemsProperty =
DependencyProperty.Register("BindableSelectedItems",
typeof(IEnumerable<string>), typeof(MultipleSelectionListBox),
new FrameworkPropertyMetadata(default(IEnumerable<string>),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));
public IEnumerable<string> BindableSelectedItems
{
get => (IEnumerable<string>)GetValue(BindableSelectedItemsProperty);
set => SetValue(BindableSelectedItemsProperty, value);
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
BindableSelectedItems = SelectedItems.Cast<string>();
}
private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MultipleSelectionListBox listBox)
listBox.SetSelectedItems(listBox.BindableSelectedItems);
}
}
Unfortunately, I wasn't able to use IList
as the BindableSelectedItems type. Doing so sent null
to my view model's property, whose type is IEnumerable<string>
.
Here's the XAML:
<v:MultipleSelectionListBox
ItemsSource="{Binding AllMyItems}"
BindableSelectedItems="{Binding MySelectedItems}"
SelectionMode="Multiple"
/>
There's one thing to watch out for. In my case, a ListBox
may be removed from the view. For some reason, this causes the SelectedItems
property to change to an empty list. This, in turn, causes the view model's property to be changed to an empty list. Depending on your use case, this may not be desirable.
Solution 6:[6]
This was pretty easy to do with a Command and the Interactivities EventTrigger. ItemsCount is just a bound property to use on your XAML, should you want to display the updated count.
XAML:
<ListBox ItemsSource="{Binding SomeItemsSource}"
SelectionMode="Multiple">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"
CommandParameter="{Binding ElementName=MyView, Path=SelectedItems.Count}" />
</i:EventTrigger>
</Interaction.Triggers>
</ListView>
<Label Content="{Binding ItemsCount}" />
ViewModel:
private int _itemsCount;
private RelayCommand<int> _selectionChangedCommand;
public ICommand SelectionChangedCommand
{
get {
return _selectionChangedCommand ?? (_selectionChangedCommand =
new RelayCommand<int>((itemsCount) => { ItemsCount = itemsCount; }));
}
}
public int ItemsCount
{
get { return _itemsCount; }
set {
_itemsCount = value;
OnPropertyChanged("ItemsCount");
}
}
Solution 7:[7]
Turns out binding a check box to the IsSelected property and putting the textblock and checkbox within a stack panel does the trick!
Solution 8:[8]
Not satisfied with the given answers I was trying to find one by myself... Well it turns out to be more like a hack then a solution but for me that works fine. This Solution uses MultiBindings in a special way. First it may look like a ton of Code but you can reuse it with very little effort.
First I implemented a '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 };
}
}
And a SelectedItems Container/Wrapper:
public class SelectedItemsContainer
{
/// Nothing special here...
public object SelectedItems { get; set; }
}
Now we create the Binding for our ListBox.SelectedItem (Singular). Note: You have to create a static Resource for the 'Converter'. This may be done once per application and be reused for all ListBoxes that need the converter.
<ListBox.SelectedItem>
<MultiBinding Converter="{StaticResource SelectedItemsMerger}">
<Binding Mode="OneWay" RelativeSource="{RelativeSource Self}" Path="SelectedItems"/>
<Binding Path="SelectionContainer"/>
</MultiBinding>
</ListBox.SelectedItem>
In the ViewModel I created the Container where I can bind to. It is important to initialize it with new() in order to fill it with the values.
SelectedItemsContainer selectionContainer = new SelectedItemsContainer();
public SelectedItemsContainer SelectionContainer
{
get { return this.selectionContainer; }
set
{
if (this.selectionContainer != value)
{
this.selectionContainer = value;
this.OnPropertyChanged("SelectionContainer");
}
}
}
And that's it. Maybe someone sees some improvements? What do You think about it?
Solution 9:[9]
This was a major issue for me, some of the answers I have seen were either too hackish, or required resetting the SelectedItems
property value breaking any code attached to the properties OnCollectionChanged event. But I managed to get a workable solution by modifying the collection directly and as a bonus it even supports SelectedValuePath
for object collections.
public class MultipleSelectionListBox : ListBox
{
internal bool processSelectionChanges = false;
public static readonly DependencyProperty BindableSelectedItemsProperty =
DependencyProperty.Register("BindableSelectedItems",
typeof(object), typeof(MultipleSelectionListBox),
new FrameworkPropertyMetadata(default(ICollection<object>),
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<dynamic> newSelection = new List<dynamic>();
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<dynamic>;
listBox.SetSelectedItems(newSelection);
}
}
}
Binding works just as you would have expected MS to have done themselves:
<uc:MultipleSelectionListBox
ItemsSource="{Binding Items}"
SelectionMode="Extended"
SelectedValuePath="id"
BindableSelectedItems="{Binding mySelection}"
/>
It has not been thoroughly tested but has passed first glance inspections. I tried to keep it reuseable by employing dynamic types on the collections.
Solution 10:[10]
It took me a while to implement binding/using SelectedItems as I am not an expert at this so I wanted to share my solution if someone might find it useful. Do not forget to download Microsoft.Xaml.Behaviors.Wpf from Nuget for this solution.
I have benefited from Accessing WPF ListBox SelectedItems
View:
Window x:Class="WpfAppSelectedItems.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:local="clr-namespace:WpfAppSelectedItems"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<ListBox Height="250" Width="300"
ItemsSource="{Binding Items}" SelectionMode="Extended"
>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.InputBindings>
<KeyBinding Gesture="Ctrl+A" Command="{Binding SelectAllCommand}" />
</ListBox.InputBindings>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged" >
<i:CallMethodAction TargetObject="{Binding}" MethodName="ListBox_SelectionChanged"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
</Grid>
</Window>
`
Code behind:
namespace WpfAppSelectedItems
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel(); //connecting window to VM
}
}
}
ViewModel:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Microsoft.Xaml.Behaviors;
using System.Windows;
namespace WpfAppSelectedItems
{
internal class ViewModel: Presenter
{
//Creating ItemPresenter class. IsSelected binded to Style in the view
public class ItemPresenter : Presenter
{
private readonly string _value;
public ItemPresenter(string value)
{
_value = value;
}
public override string ToString()
{
return _value;
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
OnPropertyChanged();
}
}
}
//Placing items to the Items which is binded to the ListBox
public ObservableCollection<ItemPresenter> Items { get; } = new ObservableCollection<ItemPresenter>
{
new ItemPresenter("A"),
new ItemPresenter("B"),
new ItemPresenter("C"),
new ItemPresenter("D")
};
//Do something when selection changed including detecting SelectedItems
public void ListBox_SelectionChanged()
{
foreach (var item in Items)
{
if (item.IsSelected)
MessageBox.Show(fufuitem.ToString());
}
}
};
//Notify View if a property changes
public abstract class Presenter : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow