9

It seems in extended selection mode IsSelected binding is buggy. Looks like only last item from selection which become out of scope is handled properly.

Demonstration:

Items 0, 1, 2 and 98, 97, 96 are selected with Control. When selecting 94 (without Control!) selection counter should be 1, but you see 3 instead. Scrolling up reveals what only one (last) item of selection out of scope was unselected.

Below is mcve:

xaml:

<ListBox ItemsSource="{Binding Items}" SelectionMode="Extended" SelectionChanged="ListBox_SelectionChanged">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Text}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
            <Setter Property="IsSelected" Value="{Binding IsSelected}" />
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>

cs:

public class NotifyPropertyChanged : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged([CallerMemberName] string property = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}

public class Item : NotifyPropertyChanged
{
    bool _isSelected;
    public bool IsSelected
    {
        get { return _isSelected; }
        set { _isSelected = value; }
    }

    public string Text { get; set; }
}

public class ViewModel : NotifyPropertyChanged
{
    public ObservableCollection<Item> Items { get; }

    public ViewModel()
    {
        var list = new List<Item>();
        for (int i = 0; i < 100; i++)
            list.Add(new Item() { Text = i.ToString() });
        Items = new ObservableCollection<Item>(list);
    }
}

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new ViewModel();
    }

    void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        Title = ((ViewModel)DataContext).Items.Count(item => item.IsSelected).ToString();
    }
}

A quick fix is to disable list control (ListBox or ListView) virtualization:

VirtualizingStackPanel.IsVirtualizing="False"

Question: any idea how to fix it without disabling virtualization?

Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • I suspect the "selected" aspect of items is something you need to do yourself when virtualisation is enabled. Virtualisation (at least from native Windows is concerned) is merely there to provide hints as to _what items_ should be displayed rather than displaying it for you. It works in conjunction with the scroll bar. This allows your app to show 1000s of items. The number of items visible generally remains constant (except on the last page or when the number of items is less than what would normally fit on screen). –  May 31 '16 at 09:45
  • It looks to me like the visualization mode is set to recycle, so it will reuse selected item, Have you tried changing the Virtualization mode, like : `VirtualizingPanel.VirtualizationMode="Standard"`? – XAMlMAX May 31 '16 at 09:47
  • @XAMlMAX, you can copy mcve and try for yourself, maybe you will find a solution. `VirtualizingMode="Standard"` doesn't improve anything (tried with `VirtualizingPanel` and `VirtualizingStackPanel`). – Sinatr May 31 '16 at 09:55
  • Duplicate of [VirtualizingStackPanel + MVVM + multiple selection](https://stackoverflow.com/questions/1273659/virtualizingstackpanel-mvvm-multiple-selection) – Herohtar Nov 24 '19 at 09:21

1 Answers1

11

Well, this is expected behavior. Virtualization only creates visual containers (ListBoxItem) for visible items. In order for bindings to work, the container must exist in the first place, so only visible items are affected.

There are two obvious solutions:

  1. Disable virtualization.
  2. Use SelectionChanged event instead. You can get added and removed items from SelectionChangedEventArgs. Then all you need to do is perform a cast and set the IsSelected property accordingly (you don't need to iterate over Items). Ctrl+A will work as well, you just have to handle added items too (and remove the binding altogether):

    void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        foreach (var removedItem in e.RemovedItems.Cast<Item>())
        {
            removedItem.IsSelected = false;
        }
        foreach (var addedItem in e.AddedItems.Cast<Item>())
        {
            addedItem.IsSelected = true;
        }
        Title = ((ViewModel) DataContext).Items.Count(item => item.IsSelected).ToString();
    }
    
Nikita B
  • 3,303
  • 1
  • 23
  • 41
  • 1
    Is unselecting last item from selection outside of scope is also *expected behavior*? I couldn't explain it, can you? – Sinatr May 31 '16 at 10:08
  • @Sinatr, it is expected to not work properly. Why it doesn't work in this particular manner I do not know, I can only speculate. :) It probably has something to do with how and when ListViewItems are cleaned up. If I were to guess, I would say that the last selected item container is somehow "held" by `SelectedItem` property, therefore it is not destroyed when you navigate away from it (unlike all the other items in current selection). – Nikita B May 31 '16 at 10:21
  • I see. I overlook your suggestion to use added/removed items at first, this is useful, currently trying to make it working for `CTR`+`A` without getting `StackOverflowException` for added items. – Sinatr May 31 '16 at 10:35
  • @Sinatr the code I posted worked on my machine without exceptions, I have no idea what can cause stack to overflow, there is no recursion. Make sure you remove `IsSelected` binding from your xaml. – Nikita B May 31 '16 at 10:43
  • I don't want to remove binding (it is needed for ViewModel to restore selection). And your solution works better than mine (removing it). – Sinatr May 31 '16 at 11:21