虚拟化和SelectionChanged事件 [英] Virtualization and SelectionChanged event

查看:75
本文介绍了虚拟化和SelectionChanged事件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用ListBoxSelectionChanged事件,但它不起作用".

I am using SelectionChanged event of ListBox, but it "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.禁用虚拟化

添加到列表:


1. Disable virtualization

Add to list:

VirtualizingPanel.IsVirtualizing="False"

单击按钮将产生输出.很好.

Clicking button will produce the output. Cool.

删除该行,默认情况下,ListBox将使用标准"虚拟化:

Remove that line, by default ListBox will use "Standard" virtualization:

事件未触发.我需要滚动到选定的项目才能触发事件.

Event is not triggered. I need to scroll to selected item to have event triggered.

将虚拟化更改为:

VirtualizingPanel.VirtualizationMode="Recycling"

WTF?即使滚动也不会触发事件.

WTF? Even scrolling doesn't trigger event.

问题:如何使SelectionChanged事件在最高效的模式下正常运行 ,而无需像在标准"模式下那样滚动?

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

推荐答案

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

With virtualization, if an item doesn't have a container (ListBoxItem) associated with it, then there's no container to which that ItemContainerStyle is applied. That means your IsSelected binding won't be applied until the item is scrolled into view. Until that property is set, no selection change occurs, and SelectionChanged is not raised.

如何使SelectionChanged事件在性能最高的模式下正常工作而无需像在标准"模式下那样滚动?

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

可以说 *工作正常.如果从MVVM角度进行处理,则不必依赖UI元素中的事件.在模型中自己跟踪项目选择.您可以使用以下实用程序类:

It arguably *is* working properly. If you approach this from an MVVM angle, then you need not rely on events from UI elements. Track the item selection yourself, in your model. You could use a utility class like this:

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并避免一些列表删除开销.

When you create your ObservableCollection of items, instantiate a SelectionTracker for that collection. Then subscribe to ItemSelected and ItemUnselected to handle individual selection changes, or alternatively subscribe to SelectedItems.CollectionChanged. If you don't care about being able to access SelectedItems as a collection, then you can get rid of _selectedItems and _selectedItemsView and avoid some list removal overhead.

[使用VirtualizationMode="Recycling"] WTF?即使滚动也不会触发事件.

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

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

Well, that's a strange one. I see no reason why that should not work in this case, but I can perhaps see why it might not always work. In theory, as soon as the container is 'recycled' and its DataContext is assigned a new item, the IsSelected binding should update. If the container's previously assigned item had also been selected, that might not trigger a property change, and thus the event might not fire. But that doesn't seem to be the case in your example. Possibly a bug or unintended consequence of how recycling is implemented.

我认为这里的主要收获是使用ListBoxItem.IsSelected进行* 设置 *时选择不可靠;是否应该信任 reflect 是否选择了给定的容器.它确实是用于样式和模板触发器的,因此他们可以知道是否将容器呈现为选定状态.从来没有打算 management 进行选择,用这种方式是一个错误,因为它代表了容器的选择状态,而不是其关联的数据项.因此,它仅在最幼稚和性能最差的情况下起作用,在这种情况下,每个项目始终与自己的容器关联(没有虚拟化).

I think the big takeaway here is that using ListBoxItem.IsSelected to *set* the selection is unreliable; it should only be trusted to reflect whether a given container is selected. It’s really intended for style and template triggers, so that they may know whether to render a container as selected or not. It was never meant to manage selection, and it’s a mistake to use it that way, because it represents the selection state of the container and not its associated data item. Thus, it only works in the most naïve and least performant scenario where every item is always associated with its own container (no virtualization).

这篇关于虚拟化和SelectionChanged事件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆