虚拟化和 SelectionChanged 事件
Virtualization and SelectionChanged event
我正在使用 ListBox
的 SelectionChanged
事件,但它 "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
。然后订阅 ItemSelected
和 ItemUnselected
来处理个别选择更改,或者订阅 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 是否被选中。它实际上是为样式和模板触发器设计的,这样它们就可以知道是否将容器呈现为选中状态。它从来就不是要管理选择,这样使用它是错误的,因为它代表容器的选择状态,而不是它的选择状态关联的数据项。因此,它仅适用于最简单且性能最低的场景,其中每个项目始终与其自己的容器相关联(无虚拟化)。
我正在使用 ListBox
的 SelectionChanged
事件,但它 "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
。然后订阅 ItemSelected
和 ItemUnselected
来处理个别选择更改,或者订阅 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 是否被选中。它实际上是为样式和模板触发器设计的,这样它们就可以知道是否将容器呈现为选中状态。它从来就不是要管理选择,这样使用它是错误的,因为它代表容器的选择状态,而不是它的选择状态关联的数据项。因此,它仅适用于最简单且性能最低的场景,其中每个项目始终与其自己的容器相关联(无虚拟化)。