虚拟化WPF Wrap Panel问题

时间:2010-07-28 18:30:38

标签: wpf .net-3.5 codeplex

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

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

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

谢谢!


问题摘要:

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

重现的步骤:

  1. 创建列表框。
  2. 将virtualizing wrappanel设置为listboxpanel模板中的itemhost。
  3. 将列表框的itemsource绑定到可观察的集合。
  4. 从支持可观察集合中删除项目。
  5. Debug.Assert失败(Debug.Assert(child == _children [childIndex],“生成了错误的子项”);)在MeasureOverride中,继续执行会导致Cleanup方法中出现空异常[请参阅附件截图] 。

    如果你能纠正这个问题,请告诉我。

    谢谢,

    AO


    代码:

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

    alt text http://virtualwrappanel.codeplex.com/Project/Download/AttachmentDownload.ashx?ProjectName=virtualwrappanel&WorkItemId=1&FileAttachmentId=138959

3 个答案:

答案 0 :(得分:8)

问题说明

您要求解释出现问题的原因以及如何解决问题的说明。到目前为止,没有人解释过这个问题我会这样做的。

在带有VirtualizingWrapPanel的ListBox中,有五个独立的数据结构,它们以不同的方式跟踪项目:

  1. ItemsSource:原始集合(在本例中为ObservableCollection)
  2. CollectionView:保留已排序/已过滤/已分组项目的单独列表(仅当正在使用这些功能时)
  3. ItemContainerGenerator:跟踪项目和容器之间的映射
  4. InternalChildren:跟踪当前可见的容器
  5. WrapPanelAbstraction:跟踪哪些容器出现在哪一行
  6. 从ItemsSource中删除项目时,必须通过所有数据结构传播此删除。以下是它的工作原理:

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

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

        另一方面,ItemContainerGenerator了解到添加的项目通常是延期的:

        1. ItemContainerGenerator会立即为其数据结构中的项添加“插槽”,但不会创建容器
        2. ItemContainerGenerator会触发ItemChanged事件。面板调用InvalidateMeasure()[这是由基类完成的 - 你不必这样做]
        3. 稍后调用MeasureOverride时,Generator.StartAt / MoveNext用于生成项容器。此时,任何新生成的容器都会添加到InternalChildren中。
        4. 因此,来自InternalChildren集合的所有删除(包括属于Move或Replace的那些)必须在OnItemsChanged内完成,但添加可以(并且应该)推迟到下一个MeasureOverride。

答案 1 :(得分:4)

OnItemsChanged方法需要正确处理args参数。有关详细信息,请参阅此question。从该问题中复制代码,您需要像这样更新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;
    }
}

答案 2 :(得分:0)

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

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

第三,在以下后检查为空:

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

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

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

如果只是跳过null幸存(没有异常渲染),请在调试器之后立即停止它并检查这些数组/集合是否已解决(内部没有空值)。

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

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