8

用于 WPF 的虚拟化包装面板的选项并不多。出于某种原因,MS 决定不在标准库中发布一个。

如果有人能如此大胆地为以下 codeplex 项目的第一个工作项提供众包答案(和解释),我将不胜感激:

http://virtualwrappanel.codeplex.com/workitem/1

谢谢!


问题总结:

我最近尝试使用这个项目中的虚拟化包装面板并遇到了一个错误。

重现步骤:

  1. 创建列表框。
  2. 将虚拟化包装面板设置为列表框面板模板中的项目主机。
  3. 将列表框的 itemsource 绑定到可观察的集合。
  4. 从支持的可观察集合中删除一个项目。

Debug.Assert 在 MeasureOverride 中失败 (Debug.Assert(child == _children[childIndex], "Wrong child was generated");),并且继续执行会导致 Cleanup 方法中出现空异常 [请参见随附的屏幕截图]。

如果您能够纠正此问题,请告诉我。

谢谢,

AO


代码:

http://virtualwrappanel.codeplex.com/SourceControl/list/changesets#

替代文字 http://virtualwrappanel.codeplex.com/Project/Download/AttachmentDownload.ashx?ProjectName=virtualwrappanel&WorkItemId=1&FileAttachmentId=138959

4

3 回答 3

8

问题的解释

您要求解释发生了什么问题以及如何解决问题的说明。到目前为止,没有人解释这个问题。我会照办的。

在带有 VirtualizingWrapPanel 的 ListBox 中有五个单独的数据结构来跟踪项目,每个数据结构都以不同的方式:

  1. ItemsSource:原始集合(在本例中为 ObservableCollection)
  2. CollectionView:保留一个单独的排序/过滤/分组项目列表(仅在使用这些功能中的任何一个时)
  3. ItemContainerGenerator:跟踪项目和容器之间的映射
  4. InternalChildren:跟踪当前可见的容器
  5. WrapPanelAbstraction:跟踪哪些容器出现在哪一行

从 ItemsSource 中删除项目时,必须通过所有数据结构传播此删除。下面是它的工作原理:

  1. 您在 ItemsSource 上调用 Remove()
  2. ItemsSource 删除该项目并触发由 CollectionView 处理的 CollectionChanged
  3. CollectionView 删除项目(如果正在使用排序/过滤/分组)并触发由 ItemContainerGenerator 处理的 CollectionChanged
  4. ItemContainerGenerator 更新其映射,触发由 VirtualizingPanel 处理的 ItemsChanged
  5. VirtualizingPanel 调用由 VirtualizingWrapPanel 实现的虚拟 OnItemsChanged 方法
  6. 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 事件时,它会立即处理所有事情:

  1. ItemContainerGenerator 立即从其自己的数据结构中删除该项目
  2. ItemContainerGenerator 触发 ItemChanged 事件。预计专家组将立即移除容器。
  3. ItemContainerGenerator 通过删除其 DataContext 来“取消准备”容器

另一方面,ItemContainerGenerator 获知添加了一个项目,所有内容通常都是延迟的:

  1. ItemContainerGenerator 立即在其数据结构中为该项目添加一个“槽”,但不创建容器
  2. ItemContainerGenerator 触发 ItemChanged 事件。面板调用 InvalidateMeasure() [这是由基类完成的 - 你不必这样做]
  3. 稍后调用 MeasureOverride 时,使用 Generator.StartAt/MoveNext 生成项目容器。那时,任何新生成的容器都会添加到 InternalChildren。

因此,InternalChildren 集合中的所有删除(包括作为 Move 或 Replace 的一部分的删除)必须在 OnItemsChanged 内完成,但添加可以(并且应该)推迟到下一个 MeasureOverride。

于 2010-08-18T21:09:07.017 回答
4

OnItemsChanged 方法需要正确处理 args 参数。请参阅此问题以获取更多信息。复制该问题的代码,您需要像这样更新 OnItemsChanged:

protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args) {
    base.OnItemsChanged(sender, args);
    _abstractPanel = null;
    ResetScrollInfo();

    // ...ADD THIS...
    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;
    }
}
于 2010-08-18T19:35:26.570 回答
0

首先,请注意,一般来说,如果您从集合中删除一个对象并且您没有它的引用,那么该对象在删除时就已经死了。所以至少 RemoveInternalChildRange 调用在删除后是非法的,但这不是核心问题。

其次,您可能会遇到一些竞争条件,即使它不是严格的多线程。必须检查(使用断点)该事件处理程序是否反应过于急切 - 即使它是单个项目,您也不希望事件处理程序在您仍在删除过程中运行。

第三,检查null之后:

UIElement child = _generator.GenerateNext(out newlyRealized) as UIElement;

并且对于第一次试验,将代码更改为优雅退出,在这种情况下意味着优雅继续 - 必须使用 for 循环和循环中的增量才能完全继续。

当您看到 null 时,还要检查 InternalChildren 以查看该访问路径是否给出与您的 _children 相同的结果(如大小、内部数据、null 在同一个地方)。

如果只是跳过一个空值(无异常渲染),则在调试器中立即停止它并检查这些数组/集合是否已解决(内部没有空值)。

此外,发布完全可编译的示例项目,在某处提供重现(作为 zip 文件) - 减少随机假设并允许 ppl 只是构建/运行和查看。

说到假设——检查你的“可观察集合”在做什么。如果您要从集合中删除项目,则该集合的先前状态中的任何迭代器/枚举器都有权抛出或给出空值,并且在试图过于智能的 UI 中,很容易出现陈旧的迭代器。

于 2010-08-17T22:12:03.777 回答