虚拟化和 SelectionChanged 事件

Virtualization and SelectionChanged event

我正在使用 ListBoxSelectionChanged 事件,但它 "doesn't work"。

这里是重现:

public partial class MainWindow : Window
{
    readonly List<Item> _items = new List<Item>
    {
        new Item(),
        ... // add 20 more, to have selected item outside of visible region
        new Item(),
        new Item { IsSelected = true },
        new Item(),
    };

    void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e) =>
        Debug.WriteLine($"Changed {e.AddedItems.Count}/{e.RemovedItems.Count}");

    void button_Click(object sender, RoutedEventArgs e) =>
        listBox.ItemsSource = listBox.ItemsSource == null ? _items : null;
}

public class Item
{
    public bool IsSelected { get; set; }
}

和xaml:

<Grid>
    <ListBox x:Name="listBox" SelectionChanged="listBox_SelectionChanged">
        <ListBox.ItemContainerStyle>
            <Style TargetType="ListBoxItem">
                <Setter Property="IsSelected" Value="{Binding IsSelected}" />
            </Style>
        </ListBox.ItemContainerStyle>
    </ListBox>
    <Button Content="Test" HorizontalAlignment="Right" VerticalAlignment="Bottom"
            Margin="10" Click="button_Click" />
</Grid>

1。禁用虚拟化

添加到列表:

VirtualizingPanel.IsVirtualizing="False"

单击按钮将产生输出。酷


2。虚拟化模式="Standard"

去掉那一行,默认ListBox会使用"Standard"虚拟化:

事件未触发。我需要滚动到所选项目才能触发事件。


3。虚拟化模式="Recycling"

将虚拟化更改为:

VirtualizingPanel.VirtualizationMode="Recycling"

卧槽?即使滚动也不会触发事件。


问题:如何使 SelectionChanged 事件在 最高性能 模式下 正常工作 而无需滚动"Standard" 模式?

通过虚拟化,如果一个项目没有与之关联的容器 (ListBoxItem),那么就没有应用 ItemContainerStyle 的容器。这意味着您的 IsSelected 绑定将不会应用,直到该项目滚动到视图中。在设置 属性 之前,不会发生选择更改,并且不会引发 SelectionChanged

How to get SelectionChanged event to work properly in the most performant mode without need to scroll as in "Standard" mode?

它可以说是 ** 正常工作。如果你从 MVVM 的角度来处理这个问题,那么你不需要依赖来自 UI 元素的事件。在您的模型中自己跟踪项目选择。您可以像这样使用实用程序 class:

public interface ISelectable
{
    bool IsSelected { get; set; }
}

public class ItemEventArgs<T> : EventArgs
{
    public T Item { get; }
    public ItemEventArgs(T item) => this.Item = item;
}

public class SelectionTracker<T> where T : ISelectable
{
    private readonly ObservableCollection<T> _items;
    private readonly ObservableCollection<T> _selectedItems;
    private readonly ReadOnlyObservableCollection<T> _selectedItemsView;
    private readonly HashSet<T> _trackedItems;
    private readonly HashSet<T> _fastSelectedItems;

    public SelectionTracker(ObservableCollection<T> items)
    {
        _items = items;
        _selectedItems = new ObservableCollection<T>();
        _selectedItemsView = new ReadOnlyObservableCollection<T>(_selectedItems);
        _trackedItems = new HashSet<T>();
        _fastSelectedItems = new HashSet<T>();
        _items.CollectionChanged += OnCollectionChanged;
    }

    public event EventHandler<ItemEventArgs<T>> ItemSelected; 
    public event EventHandler<ItemEventArgs<T>> ItemUnselected; 

    public ReadOnlyObservableCollection<T> SelectedItems => _selectedItemsView;

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                if (e.NewItems == null)
                    goto default;
                AddItems(e.NewItems.OfType<T>());
                break;

            case NotifyCollectionChangedAction.Remove:
                if (e.OldItems == null)
                    goto default;
                RemoveItems(e.OldItems.OfType<T>());
                break;

            case NotifyCollectionChangedAction.Replace:
                if (e.OldItems == null || e.NewItems == null)
                    goto default;
                RemoveItems(e.OldItems.OfType<T>());
                AddItems(e.NewItems.OfType<T>());
                break;

            case NotifyCollectionChangedAction.Move:
                break;

            default:
                Refresh();
                break;
        }
    }

    public void Refresh()
    {
        RemoveItems(_trackedItems);
        AddItems(_items);
    }

    private void AddItems(IEnumerable<T> items)
    {
        foreach (var item in items)
        {
            var observableItem = item as INotifyPropertyChanged;
            if (observableItem != null)
                observableItem.PropertyChanged += OnItemPropertyChanged;

            _trackedItems.Add(item);

            UpdateItem(item);
        }
    }

    private void RemoveItems(IEnumerable<T> items)
    {
        foreach (var item in items)
        {
            var observableItem = item as INotifyPropertyChanged;
            if (observableItem != null)
                observableItem.PropertyChanged -= OnItemPropertyChanged;

            _trackedItems.Remove(item);

            UpdateItem(item);
        }
    }

    private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (sender is T item)
            UpdateItem(item);
    }

    private void UpdateItem(T item)
    {
        if (item?.IsSelected == true && _trackedItems.Contains(item))
        {
            if (_fastSelectedItems.Add(item))
            {
                _selectedItems.Add(item);
                this.ItemSelected?.Invoke(this, new ItemEventArgs<T>(item));
            }
        }
        else
        {
            if (_fastSelectedItems.Remove(item))
            {
                _selectedItems.Remove(item);
                this.ItemUnselected?.Invoke(this, new ItemEventArgs<T>(item));
            }
        }
    }
}

当您创建 ObservableCollection 项时,为该集合实例化一个 SelectionTracker。然后订阅 ItemSelectedItemUnselected 来处理个别选择更改,或者订阅 SelectedItems.CollectionChanged。如果您不关心能够将 SelectedItems 作为集合访问,那么您可以摆脱 _selectedItems_selectedItemsView 并避免一些列表删除开销。

[With VirtualizationMode="Recycling"] WTF? Even scrolling doesn't trigger event.

嗯,这很奇怪。我看不出为什么这在这种情况下不起作用,但我也许可以理解为什么它可能 always 不起作用。理论上,一旦容器 'recycled' 并且其 DataContext 被分配了一个新项目,IsSelected 绑定就应该更新。如果容器之前分配的项目 被选中,则可能不会触发 属性 更改,因此事件可能不会触发。但在您的示例中似乎并非如此。可能是回收实施方式的错误或意外后果。

不要使用 IsSelected 来管理选择。

我认为这里最大的收获是使用 ListBoxItem.IsSelected 到 *set* 选择是不可靠的;它应该只被信任 reflect 给定的 container 是否被选中。它实际上是为样式和模板触发器设计的,这样它们就可以知道是否将容器呈现为选中状态。它从来就不是要管理选择,这样使用它是错误的,因为它代表容器的选择状态,而不是它的选择状态关联的数据项。因此,它仅适用于最简单且性能最低的场景,其中每个项目始终与其自己的容器相关联(无虚拟化)。