System.InvalidOperationException 'n' index

2019-06-12 16:16发布

I'm getting this exception when triggering a CollectionChanged event on a custom implementation of INotifyCollectionChanged:

An exception of type 'System.InvalidOperationException' occurred in PresentationFramework.dll but was not handled in user code

Additional information: '25' index in collection change event is not valid for collection of size '0'.

A XAML Datagrid is bound to the collection as ItemsSource.

How can this exception occurrence be avoided?

The code follows:

public class MultiThreadObservableCollection<T> : ObservableCollection<T>
{
    private readonly object lockObject;

    public MultiThreadObservableCollection()
    {
        lockObject = new object();
    }

    private NotifyCollectionChangedEventHandler myPropertyChangedDelegate;


    public override event NotifyCollectionChangedEventHandler CollectionChanged
    {
        add
        {
            lock (this.lockObject)
            {
                myPropertyChangedDelegate += value;
            }
        }
        remove
        {
            lock (this.lockObject)
            {
                myPropertyChangedDelegate -= value;
            }
        }
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
            var eh = this.myPropertyChangedDelegate;
            if (eh != null)
            {
                Dispatcher dispatcher;
                lock (this.lockObject)
                {
                    dispatcher = (from NotifyCollectionChangedEventHandler nh in eh.GetInvocationList()
                                  let dpo = nh.Target as DispatcherObject
                                  where dpo != null
                                  select dpo.Dispatcher).FirstOrDefault();
                }

                if (dispatcher != null && dispatcher.CheckAccess() == false)
                {
                    dispatcher.Invoke(DispatcherPriority.DataBind, (Action)(() => this.OnCollectionChanged(e)));
                }
                else
                {
                    lock (this.lockObject)
                    {
                            foreach (NotifyCollectionChangedEventHandler nh in eh.GetInvocationList())
                            {
                                nh.Invoke(this, e);
                            }
                    }
                }
            }           
    }

The error occurs in the following line:

nh.Invoke(this, e);

Thanks!

1条回答
爷、活的狠高调
2楼-- · 2019-06-12 16:31

The point is that (by design) nh.Invoke(this, e); is called asynchronously. When the collection is bound, in a XAML, and the collection changes, System.Windows.Data.ListCollectionView's private method AdjustBefore is called. Here, ListCollectionView checks that the indexes provided in the eventArgs belong to the collection; if not, the exception in the subject is thrown.

In the implementation reported in the question, the NotifyCollectionChangedEventHandler is invoked at a delayed time, when the collection may have been changed, already, and the indexes provided in the eventArgs may not belong to it any more.

A way to avoid that the ListCollectionView performs this check is to replace the eventargs with a new eventargs that, instead of reporting the added or removed items, just has a Reset action (of course, efficiency is lost!).

Here's a working implementation:

public class MultiThreadObservableCollection<T> : ObservableCollectionEnh<T>
{
    public override event NotifyCollectionChangedEventHandler CollectionChanged;

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        var eh = CollectionChanged;
        if (eh != null)
        {
            Dispatcher dispatcher = (from NotifyCollectionChangedEventHandler nh in eh.GetInvocationList()
                                     let dpo = nh.Target as DispatcherObject
                                     where dpo != null
                                     select dpo.Dispatcher).FirstOrDefault();

            if (dispatcher != null && dispatcher.CheckAccess() == false)
            {
                dispatcher.Invoke(DispatcherPriority.DataBind, (Action)(() => this.OnCollectionChanged(e)));
            }
            else
            {
                // IMPORTANT NOTE:
                // We send a Reset eventargs (this is inefficient).
                // If we send the event with the original eventargs, it could contain indexes that do not belong to the collection any more,
                // causing an InvalidOperationException in the with message like:
                // 'n2' index in collection change event is not valid for collection of size 'n2'.
                NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);

                foreach (NotifyCollectionChangedEventHandler nh in eh.GetInvocationList())
                {
                    nh.Invoke(this, notifyCollectionChangedEventArgs);
                }
            }
        }
    }
}

References: https://msdn.microsoft.com/library/system.windows.data.listcollectionview(v=vs.110).aspx

https://msdn.microsoft.com/library/ms752284(v=vs.110).aspx

查看更多
登录 后发表回答