46

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.

kck
  • 609
  • 1
  • 7
  • 13
  • 1
    ah, i see, you already try using a behaviour. use a BindableCollection for the selected items, it should work. If you have more problems, just let me know. Describe them and we will have a look. – Mare Infinitus Jun 21 '12 at 17:27
  • 2
    please show some code, especially the SelectedItems and the XAML. Is SelectedItems a property? Suspect that behaviour when SelectedItems was just a public member of BindableCollection, not a Property. – Mare Infinitus Jun 22 '12 at 05:27
  • 2
    Ah, I didn't realize that the property had to explicitly be called `SelectedItems` (mine was called `SelectedLanguages`). Now I get an `InvalidOperationException` thrown in the `BindableCollection` constructor when I click the "Back" button at the line the dispatcher is invoked with the `RaisePropertyChangedEventHandler`. I tried just putting in a try/catch block with `Dispatcher.BeginInvoke` in the catch block, but then the list items aren't re-selected when the page is navigated back to. – kck Jun 22 '12 at 12:24
  • Does this answer your question? [Bind to SelectedItems from DataGrid or ListBox in MVVM](https://stackoverflow.com/questions/9880589/bind-to-selecteditems-from-datagrid-or-listbox-in-mvvm) – StayOnTarget Jul 20 '20 at 16:59

11 Answers11

62

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>
Rachel
  • 130,264
  • 66
  • 304
  • 490
  • 1
    My `ListBox` contains `CultureInfo` items, so would it be possible to use your solution without creating a custom class that inherits from `CultureInfo` and has the `IsSelected` property? Or am I interpreting your solution incorrectly? – kck Jun 21 '12 at 18:18
  • 1
    @user1463765 No, you'd have to have an `IsSelected` property on your object for this to work – Rachel Jun 21 '12 at 18:22
  • 1
    Ok, thank you. I'm going to try to solve my problem using a behavior first, since I would prefer to avoid defining a custom class. I will definitely try your solution if I can't get the behavior to work though. – kck Jun 21 '12 at 18:33
  • 3
    @Rachel - what is your feeling about adding attributes such as `IsSelected` to model objects? I did this and got into a bit of a muddle especially when the same instance of a model object appears in more than one place (in a Tree). When one is selected then they all appear selected. This is not always desired. Also it seems to me that the model is being 'polluted' by View stuff. What's your experience? – paul Mar 13 '13 at 09:59
  • 1
    @paul Usually there's a reason to be tracking the selected item in the data layer - the most common cause is running a command against the selected item(s). But if there's never any good reason to track the selected item(s) in the business layer, then I try to keep that sort of functionality out of the Models and ViewModels. If I really *need* to track the selected item, but have no practical purpose for it in the data models, then I'll track it in the ViewModel which is a Model that the View is meant to reflect. – Rachel Mar 13 '13 at 15:56
  • 3
    @EduardoBrites That shouldn't matter if your `IsSelected` value is being stored on the `DataContext` – Rachel Jun 27 '13 at 15:54
  • 1
    @Rachel I am storing IsSelected in my DataContext (a ViewModel), but if I e.g. use Ctrl+A to select all, and I have a ton of items not visible in the viewport, not all of the ViewModels' IsSelected gets set to true. – hypehuman Nov 03 '16 at 13:38
  • 2
    @hypehuman That probably has to do with Virtualization, where the UI will only render visible objects (and a few extra for scroll buffer), and just change the DataContext behind the items. So in your SelectAll, you need to make sure all data items get selected, because looping through all UI items is not accurate because only visible items get created. – Rachel Nov 03 '16 at 14:07
  • 2
    @Rachel Thanks. Makes sense, but I think I'll just disable virtualization instead. There's Ctrl+A, Ctrl+Shift+End, and probably other hotkeys I'm not aware of. It's a shame we can't bind the SelectedItems as two-way, then this would all be taken care of automatically. – hypehuman Nov 03 '16 at 14:21
  • 2
    @hypehuman That's always an option, but be mindful it might have perfomance issues if you have a large data set. – Rachel Nov 03 '16 at 14:23
24

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) .

Community
  • 1
  • 1
Mifeet
  • 12,949
  • 5
  • 60
  • 108
  • this works, but how to get change notification in ViewModel ? In my case I get notifications only in Model. – Lucy82 May 03 '21 at 10:13
13

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.

lbonn
  • 2,499
  • 22
  • 32
Ben
  • 1,169
  • 3
  • 17
  • 27
  • I like this answer, but I would probably tweak the code as such: https://pastebin.com/YTccwmxG – maxp Jan 24 '18 at 08:27
  • In view model not upbringing property .. can you please sharethe xaml too – Moumit Mar 31 '18 at 18:28
  • 1
    @Moumit seems to be ok for one way binding, but could it be adapted for use with ObservableCollection and made two way? – Wobbles Aug 10 '18 at 21:37
6

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.

AzzamAziz
  • 2,144
  • 1
  • 24
  • 34
  • And how do you go about getting listSelectedItems to your ViewModel? – Tyler Wilson Jun 03 '22 at 15:43
  • If I'm not mistaken, you are creating separate instance of ViewModel. So binding to the "ListThatNeedsBinding" will be on a separate instance from my existing ViewModel. Is that correct? – Tyler Wilson Jun 03 '22 at 16:34
  • Your ViewModel must be a separate class, so indeed it is a separate "instance". A view will contain a XAML and a class. So on your view's class, you will need an instance of the ViewModel (Which is a separate class) Within that ViewModel there is an object called "YourListThatNeedsBinding" Hint: "ViewModel.YourListThatNeedsBinding =" Hope this helps. – AzzamAziz Jun 03 '22 at 18:35
4

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.

redcurry
  • 2,381
  • 2
  • 24
  • 38
2

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");
             }
        }
str8ball
  • 111
  • 1
  • 12
2

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));
        }
    }

}
Amadeus
  • 157
  • 10
0

Turns out binding a check box to the IsSelected property and putting the textblock and checkbox within a stack panel does the trick!

tsage080667
  • 89
  • 1
  • 10
0

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?

Fresch
  • 77
  • 10
0

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.

Wobbles
  • 3,033
  • 1
  • 25
  • 51
  • I would really really loved this one to work but it doesn't. It does upto a point. When my form opens the values that should be selected are not selected in the listbox. However if I click a value it does get added. If I add and remove a value that should have been shown, it will be removed. – kenny Dec 22 '18 at 14:43
  • @kenny My example does not set the initial value, but that can easily be added, I just didnt need for my use. – Wobbles Feb 27 '19 at 14:34
  • I found Binding to a Dependency-Property of a control can become tricky. You may try to set the Binding Attributes "notifyon...", Updatetrigger, and so on... the Mode=Twoway ist also appropriate here. – dba Mar 27 '19 at 09:39
0

if you just want to get the Name of the selected Element you can do the following:

View:

<ListBox
    x:Name="Folders"
    Grid.Row="1"
    Grid.Column="0"
    ItemsSource="{Binding YourListWithStings}"
    SelectionMode="Single"
    SelectedItem="{Binding ToYourOutputVariable}"
    >
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding}"/>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Viewmodel:

private string _ToYourOutputVariable
public string ToYourOutputVariable
{
    get {return _ToYourOutputVariable; }
    set 
       {
          _ToYourOutputVariable = value;
          NotifyOfPropertyChange(); 
          MessageBox.Show(_ToYourOutputVariable);
       }
}

The messageBox shows the name of the selected listitem. You could call a function where you open the MessageBox

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81