9
votes

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?

1
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).MickyD
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
@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

1 Answers

10
votes

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