问题的解释
您要求解释发生了什么问题以及如何解决问题的说明。到目前为止,没有人解释这个问题。我会照办的。
在带有 VirtualizingWrapPanel 的 ListBox 中有五个单独的数据结构来跟踪项目,每个数据结构都以不同的方式:
- ItemsSource:原始集合(在本例中为 ObservableCollection)
- CollectionView:保留一个单独的排序/过滤/分组项目列表(仅在使用这些功能中的任何一个时)
- ItemContainerGenerator:跟踪项目和容器之间的映射
- InternalChildren:跟踪当前可见的容器
- WrapPanelAbstraction:跟踪哪些容器出现在哪一行
从 ItemsSource 中删除项目时,必须通过所有数据结构传播此删除。下面是它的工作原理:
- 您在 ItemsSource 上调用 Remove()
- ItemsSource 删除该项目并触发由 CollectionView 处理的 CollectionChanged
- CollectionView 删除项目(如果正在使用排序/过滤/分组)并触发由 ItemContainerGenerator 处理的 CollectionChanged
- ItemContainerGenerator 更新其映射,触发由 VirtualizingPanel 处理的 ItemsChanged
- VirtualizingPanel 调用由 VirtualizingWrapPanel 实现的虚拟 OnItemsChanged 方法
- VirtualizingWrapPanel 丢弃了它的 WrapPanelAbstraction,因此它将被构建,但它从不更新 InternalChildren
因此,InternalChildren 集合与其他四个集合不同步,从而导致出现错误。
问题的解决方案
要解决此问题,请在 VirtualizingWrapPanel 的 OnItemsChanged 方法中的任意位置添加以下代码:
switch(args.Action)
{
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Replace:
RemoveInternalChildRange(args.Position.Index, args.ItemUICount);
break;
case NotifyCollectionChangedAction.Move:
RemoveInternalChildRange(args.OldPosition.Index, args.ItemUICount);
break;
}
这使 InternalChildren 集合与其他数据结构保持同步。
为什么这里不调用 AddInternalChild/InsertInternalChild
您可能想知道为什么上面的代码中没有调用 InsertInternalChild 或 AddInternalChild,尤其是为什么处理 Replace 和 Move 不需要我们在 OnItemsChanged 期间添加新项目。
理解这一点的关键在于 ItemContainerGenerator 的工作方式。
当 ItemContainerGenerator 收到一个 remove 事件时,它会立即处理所有事情:
- ItemContainerGenerator 立即从其自己的数据结构中删除该项目
- ItemContainerGenerator 触发 ItemChanged 事件。预计专家组将立即移除容器。
- ItemContainerGenerator 通过删除其 DataContext 来“取消准备”容器
另一方面,ItemContainerGenerator 获知添加了一个项目,所有内容通常都是延迟的:
- ItemContainerGenerator 立即在其数据结构中为该项目添加一个“槽”,但不创建容器
- ItemContainerGenerator 触发 ItemChanged 事件。面板调用 InvalidateMeasure() [这是由基类完成的 - 你不必这样做]
- 稍后调用 MeasureOverride 时,使用 Generator.StartAt/MoveNext 生成项目容器。那时,任何新生成的容器都会添加到 InternalChildren。
因此,InternalChildren 集合中的所有删除(包括作为 Move 或 Replace 的一部分的删除)必须在 OnItemsChanged 内完成,但添加可以(并且应该)推迟到下一个 MeasureOverride。