【问题标题】:Virtualizing WPF Wrap Panel Issue虚拟化 WPF Wrap 面板问题
【发布时间】:2010-07-28 18:30:38
【问题描述】:

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

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

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

谢谢!


问题摘要:

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

重现步骤:

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

MeasureOverride 中的 Debug.Assert 失败 (Debug.Assert(child == _children[childIndex], "Wrong child was generated");),继续执行会导致 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

【问题讨论】:

    标签: wpf .net-3.5 codeplex


    【解决方案1】:

    问题说明

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

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

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

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

    1. 您在 ItemsSource 上调用 Remove()
    2. ItemsSource 删除该项目并触发其 CollectionChanged 由 CollectionView 处理
    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 收到一个删除事件时,它会立即处理所有事情:

    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。

    【讨论】:

    • 看起来 Tom Goff 在我输入答案时提供了必要的代码。他的回答也是正确的,和我的基本一样,没有详细解释。
    • 嗨,Ray - 很好的总结,你得到了我的投票。您的回答的一个问题是,问题并不是“InternalChildren 集合与其他四个集合不同步”,但我确信它没有帮助。根本的问题是,意识到的孩子没有被“清理干净”。如果删除索引 10 处的项目,则索引 11 处的项目将移动到索引 10。当你去实现索引 10 处的项目时(之前是 11),你最终会得到断言“错误的孩子已生成”,因为另一个孩子从未实现过。
    【解决方案2】:

    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;
        }
    }
    

    【讨论】:

      【解决方案3】:

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

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

      三、检查null之后:

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

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

      当您看到 null 时,还要检查 InternalChildren,以查看该访问路径是否提供与您的 _children 相同的结果(如大小、内部数据、同一位置的 null)。

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

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-06-03
        • 2011-10-20
        • 2023-03-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-10-02
        • 1970-01-01
        相关资源
        最近更新 更多