ObservableCollection : calling OnCollectionChanged with multiple new items

15,796

Solution 1

you can implement AddRange() for the ObservableCollection like this as shown here:

public class RangeObservableCollection<T> : ObservableCollection<T>
{
    private bool _SuppressNotification;

    public override event NotifyCollectionChangedEventHandler CollectionChanged;

    protected virtual void OnCollectionChangedMultiItem(
        NotifyCollectionChangedEventArgs e)
    {
        NotifyCollectionChangedEventHandler handlers = this.CollectionChanged;
        if (handlers != null)
        {
            foreach (NotifyCollectionChangedEventHandler handler in 
                handlers.GetInvocationList())
            {
                if (handler.Target is CollectionView)
                    ((CollectionView)handler.Target).Refresh();
                else
                    handler(this, e);
            }
        }
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (!_SuppressNotification)
        {
            base.OnCollectionChanged(e);
            if (CollectionChanged != null)
                CollectionChanged.Invoke(this, e);
        }
    }

    public void AddRange(IEnumerable<T> list)
    {
        if (list == null)
            throw new ArgumentNullException("list");

        _SuppressNotification = true;

        foreach (T item in list)
        {
            Add(item);
        }
        _SuppressNotification = false;

        OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list));
    }
}

UPDATE: After binding to ListBox I was seeing an InvalidOperationException too (same message you were seeing). According to this article that's because CollectionView doesn't support range actions. Luckily the article also supplies a solution (although it feels a little "hack-ish").

UPDATE 2: Added a fix that raises the overridden CollectionChanged event in the overridden implementation of OnCollectionChanged().

Solution 2

Thanks for the inspiration AndyP. I had a few problems with your implementation, such as the use of CollectionView instead of ICollectionView in the test, as well as manually calling "Reset" on the elements. Elements that inherit from CollectionView might actually deal with these args in more ways than calling "this.Reset()", so it's preferable to still fire their handlers, just with the Action=Reset args that they require instead of the improved event args that include the list of items changed. Below is my (very similar) implementation.

public class BaseObservableCollection<T> : ObservableCollection<T>
{
    //Flag used to prevent OnCollectionChanged from firing during a bulk operation like Add(IEnumerable<T>) and Clear()
    private bool _SuppressCollectionChanged = false;

    /// Overridden so that we may manually call registered handlers and differentiate between those that do and don't require Action.Reset args.
    public override event NotifyCollectionChangedEventHandler CollectionChanged;

    public BaseObservableCollection() : base(){}
    public BaseObservableCollection(IEnumerable<T> data) : base(data){}

    #region Event Handlers
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if( !_SuppressCollectionChanged )
        {
            base.OnCollectionChanged(e);
            if( CollectionChanged != null )
                CollectionChanged.Invoke(this, e);
        }
    }

    //CollectionViews raise an error when they are passed a NotifyCollectionChangedEventArgs that indicates more than
    //one element has been added or removed. They prefer to receive a "Action=Reset" notification, but this is not suitable
    //for applications in code, so we actually check the type we're notifying on and pass a customized event args.
    protected virtual void OnCollectionChangedMultiItem(NotifyCollectionChangedEventArgs e)
    {
        NotifyCollectionChangedEventHandler handlers = this.CollectionChanged;
        if( handlers != null )
            foreach( NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList() )
                handler(this, !(handler.Target is ICollectionView) ? e : new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
    #endregion

    #region Extended Collection Methods
    protected override void ClearItems()
    {
        if( this.Count == 0 ) return;

        List<T> removed = new List<T>(this);
        _SuppressCollectionChanged = true;
        base.ClearItems();
        _SuppressCollectionChanged = false;
        OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
    }

    public void Add(IEnumerable<T> toAdd)
    {
        if( this == toAdd )
            throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");

        _SuppressCollectionChanged = true;
        foreach( T item in toAdd )
            Add(item);
        _SuppressCollectionChanged = false;
        OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(toAdd)));
    }

    public void Remove(IEnumerable<T> toRemove)
    {
        if( this == toRemove )
            throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");

        _SuppressCollectionChanged = true;
        foreach( T item in toRemove )
            Remove(item);
        _SuppressCollectionChanged = false;
        OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(toRemove)));
    }
    #endregion
}

Solution 3

After many iterations, we ended up with this version of ObservableRangeCollection and ReadOnlyObservableRangeCollection which is based on the code from the accepted answer, and which we didn't have to modify in the last 6 months:

public class ObservableRangeCollection<T> : ObservableCollection<T>
{
    private bool suppressNotification;

    public ObservableRangeCollection() { }

    public ObservableRangeCollection(IEnumerable<T> items)
        : base(items)
    {
    }

    public override event NotifyCollectionChangedEventHandler CollectionChanged;

    protected virtual void OnCollectionChangedMultiItem(
        NotifyCollectionChangedEventArgs e)
    {
        var handlers = CollectionChanged;
        if (handlers == null) return;

        foreach (NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList())
        {
            if (handler.Target is ReadOnlyObservableCollection<T>
                && !(handler.Target is ReadOnlyObservableRangeCollection<T>))
            {
                throw new NotSupportedException(
                    "ObservableRangeCollection is wrapped in ReadOnlyObservableCollection which might be bound to ItemsControl " +
                    "which is internally using ListCollectionView which does not support range actions.\n" +
                    "Instead of ReadOnlyObservableCollection, use ReadOnlyObservableRangeCollection");
            }
            var collectionView = handler.Target as ICollectionView;
            if (collectionView != null)
            {
                collectionView.Refresh();
            }
            else
            {
                handler(this, e);
            }
        }
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (suppressNotification) return;

        base.OnCollectionChanged(e);
        if (CollectionChanged != null)
        {
            CollectionChanged.Invoke(this, e);
        }
    }

    public void AddRange(IEnumerable<T> items)
    {
        if (items == null) return;

        suppressNotification = true;

        var itemList = items.ToList();

        foreach (var item in itemList)
        {
            Add(item);
        }
        suppressNotification = false;

        if (itemList.Any())
        {
            OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, itemList));
        }
    }

    public void AddRange(params T[] items)
    {
        AddRange((IEnumerable<T>)items);
    }

    public void ReplaceWithRange(IEnumerable<T> items)
    {
        Items.Clear();
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        AddRange(items);
    }

    public void RemoveRange(IEnumerable<T> items)
    {
        suppressNotification = true;

        var removableItems = items.Where(x => Items.Contains(x)).ToList();

        foreach (var item in removableItems)
        {
            Remove(item);
        }

        suppressNotification = false;

        if (removableItems.Any())
        {
            OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removableItems));
        }
    }
}

public class ReadOnlyObservableRangeCollection<T> : ReadOnlyObservableCollection<T>
{
    public ReadOnlyObservableRangeCollection(ObservableCollection<T> list)
        : base(list)
    {            
    }

    protected override event NotifyCollectionChangedEventHandler CollectionChanged;

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        var handlers = CollectionChanged;
        if (handlers == null) return;

        foreach (NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList())
        {
            var collectionView = handler.Target as ICollectionView;
            if (collectionView != null)
            {
                collectionView.Refresh();
            }
            else
            {
                handler(this, e);
            }
        }
    }
}

We basically replaced all usages of ObservableCollection in our app by ObservableRangeCollection, and it works like a charm.

Share:
15,796

Related videos on Youtube

Sonic Soul
Author by

Sonic Soul

Updated on April 28, 2022

Comments

  • Sonic Soul
    Sonic Soul about 2 years

    please note that I am trying to use NotifyCollectionChangedAction.Add action instead of .Reset. the latter does work, but it is not very efficient with large collections.

    so i subclassed ObservableCollection:

    public class SuspendableObservableCollection<T> : ObservableCollection<T>
    

    for some reason, this code:

    private List<T> _cachedItems;
    ...
    
        public void FlushCache() {
            if (_cachedItems.Count > 0) {
    
            foreach (var item in _cachedItems)
                Items.Add(item);
    
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(
                NotifyCollectionChangedAction.Add, (IList<T>)_cachedItems));
            }
        }
    

    is throwing A collection Add event refers to item that does not belong to collection

    this appears to be a bug in BCL ?

    I can step through and see prior to calling OnCollectionChanged that new items are added to this.Items

    WOW

    just made a staggering discovery. None of these approaches worked for me (flush, addrange), because the error appears to be triggered ONLY if this collection is bound to my Listview!!

    TestObservableCollection<Trade> testCollection = new TestObservableCollection<Trade>();
    List<Trade> testTrades = new List<Trade>();
    
    for (int i = 0; i < 200000; i++) 
        testTrades.Add(t);
    
    testCollection.AddRange(testTrades); // no problems here.. 
    _trades.AddRange(testTrades); // this one is bound to ListView .. BOOOM!!!
    

    In conclusion, ObservableCollection does support adding incremental lists, but a ListView doesn't. Andyp figured out a workaround to make it work with CollectionView below, but since .Refresh() is called, that is no different than just calling OnCollectionChanged( .Reset )..

  • Sonic Soul
    Sonic Soul almost 14 years
    thanks, now i am back to "A collection Add event refers to item that does not belong to collection"
  • George Howarth
    George Howarth almost 14 years
    hmmm, how about instead of Items.Add(item), base.Add(item)?
  • Sonic Soul
    Sonic Soul almost 14 years
    George, tried Add(item) and base.Add(item), still complains it is not part of the collection...
  • George Howarth
    George Howarth almost 14 years
    I actually meant this.Add(item), sorry about that... I did get this error before when implementing an ObservableCollection<T>, but I don't have access to the code at the moment I'll into it tomorrow.
  • Sonic Soul
    Sonic Soul almost 14 years
    isnt this.Add same as Add ? anyways, i tried it just to be sure, and still same error.. i think this may be a bug :(
  • Кое Кто
    Кое Кто almost 6 years
    What is the purpose of base.OnCollectionChanged(e)? Is anyone could subscribe on base.CollectionChanged?
  • Кое Кто
    Кое Кто about 4 years
    Why notificating in ReplaceWithRange is after Add?
  • xhafan
    xhafan about 4 years
    you want to notify only once after all items have been added.