【问题标题】:NotifyCollectionChangedEventArgs: Added item does not appear at given index (during removal) [closed]NotifyCollectionChangedEventArgs:添加的项目未出现在给定索引处(在删除期间)[关闭]
【发布时间】:2020-10-17 07:44:17
【问题描述】:

根据这篇文章:Observable Stack and Queue,我实现了一个 ObservableStack。它在 99% 的时间都在工作,但在某些情况下 - 很少而且似乎没有理由 - 当我尝试弹出堆栈时,我得到一个异常:

System.InvalidOperationException: Added item does not appear at given index '1'.
   at MS.Internal.Data.EnumerableCollectionView.ProcessCollectionChanged(NotifyCollectionChangedEventArgs args)
   at System.Windows.Data.CollectionView.OnCollectionChanged(Object sender, NotifyCollectionChangedEventArgs args)
   at System.Collections.Specialized.NotifyCollectionChangedEventHandler.Invoke(Object sender, NotifyCollectionChangedEventArgs e)
   ...

以下是相关代码:

public class ObservableStack<T> : Stack<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
    public virtual event NotifyCollectionChangedEventHandler CollectionChanged;

    public new virtual void Push(T item)
    {
        base.Push(item);
        var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, base.Count-1);
        if (this.CollectionChanged != null)
            this.CollectionChanged(this, e);
    }

    public new virtual T Pop()
    {
        var item = base.Pop();
        var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, base.Count);
        if (this.CollectionChanged != null)
            this.CollectionChanged(this, e);
        return item;
    }

    //...
}

在极少数情况下,在 Pop() 中调用 CollectionChanged() 时会引发异常。这类似于问题 INotifyCollectionChanged: Added item does not appear at given index '0' ,但在这种情况下似乎没有一个答案适用。请注意,在引发异常的 Remove 行上将 base.count 替换为 -1 或 0 会导致它总是失败。同样,如果我只是从事件参数中排除索引,它总是会抱怨“集合删除事件必须指定项目位置”。从技术上讲,我可以通过将通知参数更改为

var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);

...但当然,这实际上并不正确,我想让“删除”正确报告。插入的所有项目都没有被覆盖的 Equals() 或 operator=。

更多背景知识:堆栈在 UndoHistory 中使用,当值更改时,先前的值被推送到堆栈,并在我撤消时弹出。这适用于绝大多数情况,这就是为什么我很难弄清楚为什么错误仅在少数情况下发生。不幸的是,到目前为止,我一直未能成功地将其缩减为一个始终显示问题的小型独立示例。例如,在实际应用程序中,我可能会执行一个操作并撤消它没有问题,但随后连续执行两次相同的操作,之后撤消将引发异常。在一个较小/简化的示例中,我根本无法让它发生。请注意,堆栈还绑定到 ItemsControl 以显示撤消历史记录 - 尽管它只是单向的(即控件不用于修改堆栈,仅显示其项目)。

任何关于为什么它会在这种看似随机、罕见的情况下抛出这种异常的想法将不胜感激。

【问题讨论】:

  • 是否涉及多个线程?请注意,您可以编写一条语句,而不是在调用 CollectionChanged 之前检查它是否为 null:CollectionChanged?.Invoke(this, e);
  • 我不相信,但只是为了检查,我添加了 Console.WriteLine("Thread ID: " + System.Threading.Thread.CurrentThread.ManagedThreadId); - Push() 和 Pop(),& 他们总是报告相同的 ID 1。所以如果这是一种有效的检查方式,那么不,只涉及一个线程。
  • 但是,堆栈有多个实例。不应该在不同的线程上。
  • 您应该提供引发异常的代码行和重现错误的最小示例。
  • 坦率地说,我对您实现这一点的方法持怀疑态度。使堆栈或队列“可观察”是不寻常的,因为这些数据结构通常旨在一次仅“观察”一个元素。就您可能提供面向集合的视图而言,ObservableCollection&lt;T&gt; 类本身直接提供了这一点。 IE。而不是自己实现INotifyCollectionChanged,您的堆栈和队列类应该使用ObservableCollection&lt;T&gt; 作为支持数据结构,并将事件处理委托给它。

标签: c# wpf observablecollection


【解决方案1】:

此错误不是由您的代码引发的,而是由另一个订阅您的事件的类引发的。可能是WPF中的东西。此类正在验证它保存在某处的堆栈集合的副本是否仍然有效,并发现它不是。

99% 的可能原因是您的堆栈发生了未生成 CollectionChanged 事件的更改。

第一个问题是当Clear被调用时你应该发出Reset事件。

第二个是如果有人将你的对象投射到Stack&lt;T&gt; 并调用PushPull,你的代码将不会被命中。发生这种情况是因为这些方法不是虚拟的,因此您需要使用new 声明您自己的版本。将 您的 方法声明为 virtual 不会改变这一点。

你无法解决这个问题。解决方案不是从Stack 派生您的类,而是在其中创建一个私有Stack 对象,然后创建您自己的方法,以您完全控制的方式公开其功能。

一旦您这样做(并正确实施所有事件!)异常就会消失。

【讨论】:

  • 嗨;感谢您的回复。我编辑了我的问题以添加有关异常的更深入的信息(即它来自哪个 WPF)。回复:很可能有没有生成 CollectionChanged 事件的更改,根据上面的代码怎么会发生这种情况?或者这正是您所指的有人将其投射到 Stack 的场景?
  • 无论如何,我继续并将其更改为组合:不要继承 Stack,而是包装 Stack 对象的私有实例(&我只是暴露了我当前使用的东西) .不幸的是,例外情况保持不变:/
【解决方案2】:

您发布的两个NotifyCollectionChangedEventArgs 都是使用错误的参数创建的。
Stack 就像一个反向集合:最后添加的项目是第一个被删除的项目,并且始终具有索引 0。所以在Stack 中,更改总是发生在索引0
目前你总是报告最后一个索引,即 count - 1,即最后一个要弹出的项,也就是第一个推送的项,它仅在 count == 1 时指向当前弹出的项。

NotifyCollectionChangedEventArgs 具有固定更改索引:

public new virtual void Push(T item)
{
    base.Push(item);
    var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, 0);
    if (this.CollectionChanged != null)
        this.CollectionChanged(this, e);
}

public new virtual T Pop()
{
    var item = base.Pop();
    var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, 0);
    if (this.CollectionChanged != null)
        this.CollectionChanged(this, e);
    return item;
}

备注

关键是,当集合与ItemsControl 一起使用时,控件将仅对源集合的ICollectionView 进行操作。这个CollectionView 在内部订阅了底层集合的CollectionChanged 事件以跟踪更改。这样做是因为集合视图维护一个指针,其中包括 CurrentItemCurrentPosition
在这种情况下,CollectionView 通过跟踪从添加到删除的信息来验证更改的项目及其索引。添加项目时,您当前报告最后一个索引,例如1,但是当删除这个项目时,真正的索引是0。这就是为什么内部验证失败并抛出异常“添加的项目没有出现在给定的索引'1'。”

请注意,像堆栈(或像队列)这样的数据结构不被视为基于索引。但是集合更改通知是。

要获得预期的行为,您应该扩展一个基于索引的集合,例如ObservableCollection。然后在0RemoveAt(0) 处使用Insert 来提供后进先出行为。

如果您希望 O(1) 进行访问操作,则必须实现例如 ICollection&lt;T&gt; + INotifyCollectionChanged 并使用数组作为后备存储。
始终追加到数组并记住最后一个索引。但请注意,在实现索引器或GetEnumerator 时,您始终返回“反转”数组:第一个添加的项目具有索引0。通过这种方式,UI 获得了正确排序的集合视图,并且您拥有用于推送和弹出操作的 O(1)。
诀窍是找到一个好的初始数组大小以避免调整大小。

堆栈是一种只关心返回最后添加的项目的数据结构。您不能随机移动项目或访问项目(按索引)。这就是为什么堆栈或队列不适合 UI 场景的原因。 ItemsControl(和CollectionChanged)始终基于索引进行操作。

【讨论】:

猜你喜欢
  • 2011-08-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-04-05
相关资源
最近更新 更多