Skip to content

Instantly share code, notes, and snippets.

@ReinisV
Created June 2, 2017 08:05
Show Gist options
  • Save ReinisV/3d924e004241719b7a7f18884c8c2245 to your computer and use it in GitHub Desktop.
Save ReinisV/3d924e004241719b7a7f18884c8c2245 to your computer and use it in GitHub Desktop.
An ObservableCollection which allows batch updates by differentiating between the handlers of CollectionChanged event, and giving the Reset type event only to CollectionViews
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
public class ObservableBatchCollection<T> : ObservableCollection<T>
{
// Flag used to prevent OnCollectionChanged from firing during a bulk operation like Add(IEnumerable<T>) and Clear()
private bool _SuppressCollectionChanged;
public ObservableBatchCollection()
{
}
public ObservableBatchCollection(IEnumerable<T> data) : base(data)
{
}
/// 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 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.");
}
this._SuppressCollectionChanged = true;
var newAddIndex = this.Count;
foreach (T item in toAdd)
{
this.Add(item);
}
this._SuppressCollectionChanged = false;
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(toAdd), newAddIndex);
this.OnCollectionChangedMultiItem(args);
}
public void Remove(IEnumerable<T> toRemove)
{
// batch add can be executed in a single event, since when you add, you add all entries after each other, so the start index
// is the same for the entire range of the new elements.
// batch remove cannot be executed in a single event, since when you remove, you can remove elements that are located in different
// positions in the list, so the remove index can be different for each element.
// that is why we we iterate over our collection, creating ranges of removable entries (which would share an event and the remove index)
// and raise them one by one.
if (this == toRemove)
{
throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");
}
var batchRemoveRange = new List<T>();
var removeRangeStartIndex = 0;
this._SuppressCollectionChanged = true;
var currentItemsCount = this.Count;
var indexOfEntriesShiftAdjusted = 0;
for (int index = 0; index < currentItemsCount; index++)
{
var currentItem = this[indexOfEntriesShiftAdjusted];
if (toRemove.Contains(currentItem))
{
// range is still going, keep collecting it
batchRemoveRange.Add(currentItem);
this.Remove(currentItem);
indexOfEntriesShiftAdjusted -= 1;
}
else
{
// range finished, raise events
if (batchRemoveRange.Any())
{
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(batchRemoveRange), removeRangeStartIndex);
this.OnCollectionChangedMultiItem(args);
// start a new range clear the batch list
removeRangeStartIndex = indexOfEntriesShiftAdjusted;
batchRemoveRange.Clear();
}
}
indexOfEntriesShiftAdjusted += 1;
}
if (batchRemoveRange.Any())
{
// raise events for the last range
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(batchRemoveRange), removeRangeStartIndex);
this.OnCollectionChangedMultiItem(args);
// start a new range remove batch
removeRangeStartIndex = indexOfEntriesShiftAdjusted;
batchRemoveRange.Clear();
}
this._SuppressCollectionChanged = false;
}
protected override void ClearItems()
{
if (this.Count == 0)
{
return;
}
var removed = new List<T>(this);
this._SuppressCollectionChanged = true;
base.ClearItems();
this._SuppressCollectionChanged = false;
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed);
this.OnCollectionChangedMultiItem(args);
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!this._SuppressCollectionChanged)
{
base.OnCollectionChanged(e);
this.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)
{
var handlers = this.CollectionChanged;
if (handlers != null)
{
var invocationList = handlers.GetInvocationList();
foreach (NotifyCollectionChangedEventHandler handler in invocationList)
{
var eventArgs = !(handler.Target is ICollectionView)
? e
: new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
handler(this, eventArgs);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment