NotifyCollectionChangedEventArgs:添加的项目未出现在给定索引处在删除过程中

问题描述

我根据这篇文章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,base.Count);
        if (this.CollectionChanged != null)
            this.CollectionChanged(this,e);
        return item;
    }

    //...
}

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

来使其工作
var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);

...但是,当然,这实际上是不正确的,我希望“删除”报告正确。所插入的项目均未覆盖Equals()或operator =。

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

任何想法,为什么它会在这种看似随机的罕见情况下抛出这种异常。

解决方法

该错误不是由您的代码引发的,而是由另一个预订您的事件的类引发的。可能是WPF中的某些内容。此类正在验证您保存在某个位置的堆栈集合的副本仍然有效,并发现该副本无效。

99%的可能原因是堆栈中的更改没有生成CollectionChanged事件。

第一个问题是,在调用Clear时,您应该发出Reset事件。

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

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

执行此操作(并正确实现所有事件!)后,异常就会消失。

,

您发布的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,e);
    return item;
}

备注

要点是,当集合与ItemsControl一起使用时,控件将仅在源集合的ICollectionView上运行。该CollectionView在内部订阅基础集合的CollectionChanged事件以跟踪更改。这样做是因为集合视图维护着一个指针,该指针包括CurrentItemCurrentPosition
在这种情况下,CollectionView通过跟踪从添加到删除的信息来验证更改的项目及其索引。添加项目时,您当前报告的是最后一个索引,例如1,但删除此项时,真实索引为0。这就是内部验证失败并且抛出异常“添加的项目未出现在给定索引'1'处的原因。”

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

要获得预期的行为,您应该扩展基于索引的集合,例如ObservableCollection。然后使用Insert0处的RemoveAt(0)提供LIFO行为。

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

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