Search code examples
c#wpfinotifycollectionchanged

System.InvalidOperationException 'n' index in collection change event is not valid for collection of size '0'


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!


Solution

  • 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