虚拟化 WPF WPF 包裹面板问题

发布于 2024-09-12 05:09:19 字数 1231 浏览 5 评论 0原文

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

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

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

谢谢!


问题摘要:

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

重现步骤:

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

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

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

谢谢,

AO


代码:

http://virtualwrappanel.codeplex.com/SourceControl/list/changesets#< /a>

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

There are not very many options for a virtualizing wrap panel for use in WPF. For one reason or another MS decided to not ship one in the standard library.

If anyone could be so bold as to provide a crowd source answer (and explaination) to the first work item on the following codeplex project, I would greatly appreciate it:

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

Thanks!


Summary of issue:

I've recently tried using the virtualizing wrappanel from this project and have encountered a bug.

Steps to reproduce:

  1. Create listbox.
  2. Set the virtualizing wrappanel as the itemhost in a listboxpanel template.
  3. Bind the itemsource of the listbox to an observable collection.
  4. Remove an item from the backing observable collection.

The Debug.Assert fails (Debug.Assert(child == _children[childIndex], "Wrong child was generated");) in MeasureOverride, and continued execution results in a null exception in the Cleanup method [see attached screenshot].

Please let me know if you are able to correct this.

Thanks,

AO


Code:

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

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

走走停停 2024-09-19 05:09:19

问题说明

您要求对出现的问题进行说明以及如何解决该问题的说明。到目前为止还没有人解释这个问题。我会这样做。

在带有 VirtualizingWrapPanel 的 ListBox 中,有五个独立的数据结构用于跟踪项目,每个数据结构都以不同的方式进行跟踪:

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

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

  1. 您在 ItemsSource 上调用 Remove()
  2. ItemsSource 会删除该项目并触发由 CollectionView 处理的 CollectionChanged
  3. CollectionView 会删除该项目(如果正在使用排序/过滤/分组)并触发由 CollectionView 处理的 CollectionChanged通过 ItemContainerGenerator
  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 不需要我们在执行过程中添加新项项目已更改。

理解这一点的关键在于 ItemContainerGenerator 的工作方式。

当 ItemContainerGenerator 收到删除事件时,它会立即处理所有内容:

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

。 另一方面,ItemContainerGenerator 了解到添加项目后,所有内容通常都会被推迟:

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

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

Explanation of the problem

You asked for an explanation of what is going wrong as well as instructions how to fix it. So far nobody has explained the problem. I will do so.

In ListBox with a VirtualizingWrapPanel there are five separate data structures that track items, each in different ways:

  1. ItemsSource: The original collection (in this case ObservableCollection)
  2. CollectionView: Keeps a separate list of sorted/filtered/grouped items (only if any of these features are in use)
  3. ItemContainerGenerator: Tracks the mapping between items and containers
  4. InternalChildren: Tracks containers that are currently visible
  5. WrapPanelAbstraction: Tracks which containers appear on which line

When an item is removed from ItemsSource, this removal must be propagated through all data structures. Here is how it works:

  1. You call Remove() on the ItemsSource
  2. ItemsSource removes the item and fires its CollectionChanged which is handled by the CollectionView
  3. CollectionView removes the item (if sorting/filtering/grouping is in use) and fires its CollectionChanged which is handled by the ItemContainerGenerator
  4. ItemContainerGenerator updates its mapping, fires its ItemsChanged which is handled by VirtualizingPanel
  5. VirtualizingPanel calls its virtual OnItemsChanged method which is implemented by VirtualizingWrapPanel
  6. VirtualizingWrapPanel discards its WrapPanelAbstraction so it will be built, but it never updates InternalChildren

Because of this, the InternalChildren collection is out of sync with the other four collections, leading to the errors that were experienced.

Solution to the problem

To fix the problem, add the following code anywhere within VirtualizingWrapPanel's OnItemsChanged method:

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

This keeps the InternalChildren collection in sync with the other data structures.

Why AddInternalChild/InsertInternalChild is not called here

You may wonder why there are no calls to InsertInternalChild or AddInternalChild in the above code, and especially why handling Replace and Move don't require us to add a new item during OnItemsChanged.

The key to understanding this is in the way ItemContainerGenerator works.

When ItemContainerGenerator receives a remove event it handles everything immediately:

  1. ItemContainerGenerator immediately removes the item from its own data structures
  2. ItemContainerGenerator fires the ItemChanged event. The panel is expected to immediately remove the container.
  3. ItemContainerGenerator "unprepares" the container by removing its DataContext

On the other hand, ItemContainerGenerator learns that an item is added everything is typically deferred:

  1. ItemContainerGenerator immediately adds a "slot" for the item in its data structure but does not create a container
  2. ItemContainerGenerator fires the ItemChanged event. The panel calls InvalidateMeasure() [this is done by the base class - you do not have to do it]
  3. Later when MeasureOverride is called, Generator.StartAt/MoveNext is used to generate the item containers. Any newly-generated containers are added to InternalChildren at that time.

Thus, all removals from the InternalChildren collection (including ones that are part of a Move or Replace) must be done inside OnItemsChanged, but additions can (and should) be deferred until the next MeasureOverride.

七度光 2024-09-19 05:09:19

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

The OnItemsChanged method needs to properly handle the args parameters. Please see this question for more information. Copying the code from that question, you would need to update OnItemsChanged like so:

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;
    }
}
十六岁半 2024-09-19 05:09:19

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

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

第三,检查 null after:

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

并且对于第一次尝试,更改代码以正常退出,在本例中这意味着正常继续 - 必须使用 for 循环和循环中的增量才能完全继续。

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

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

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

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

First, beware that in general, if you are removing an object from a collection and you don't have it's reference, that object is dead at the point of removal. So at the very least RemoveInternalChildRange call is illegal after removal but that's not the core issue.

Second, you might be having a little race condition, even if it's not strictly multi-threaded. Have to check (with breakpoint) if that event handler is reacting too eagerly - you don't want event handler running while you are still in the middle of a removal even if it's a single item.

Third, check for null after:

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

and for the first trial change the code to have a graceful exit, which in this case means gracefull continue - have to use for loop and increments in the loop to be able to do continue at all.

Also check InternalChildren whne you see that null to see if that access path gives the same result as your _children (as in size, internal data, null in the same place).

If just skipping a null survives (renders without exceptions) stop it in debugger right after that and check if these arrays/collections got settled (no nulls inside).

Also, post the fully compilable sample project that gives the repro (as a zip file) somewhere - reduces random assumprions and allows ppl to just build/run and see.

Speaking of assumptions - check what's your "observable collection" doing. If you are removing an item from a collection, any and every iterator/enumerator from a prior state of that collection has the right to throw or give nulls and in a UI that tries to be too smart, having a stale iterator can happen easily.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文