I want to be able to add a range and get updated for the entire bulk.
I also want to be able to cancel the action before it\'s done (i.e. collection changing besides
First of all, please vote and comment on the API request on the .NET repo.
Here's my optimized version of the ObservableRangeCollection (optimized version of James Montemagno's one).
It performs very fast and is meant to reuse existing elements when possible and avoid unnecessary events, or batching them into one, when possible.
The ReplaceRange method replaces/removes/adds the required elements by the appropriate indices and batches the possible events.
Tested on Xamarin.Forms UI with great results for very frequent updates to the large collection (5-7 updates per second).
Note:
Since WPF is not accustomed to work with range operations, it will throw a NotSupportedException, when using the ObservableRangeCollection from below in WPF UI-related work, such as binding it to a ListBox etc. (you can still use the ObservableRangeCollection if not bound to UI).
However you can use the WpfObservableRangeCollection
The real solution would be creating a CollectionView that knows how to deal with range operations, but I still didn't have the time to implement this.
RAW Code - open as Raw, then do Ctrl+A to select all, then Ctrl+C to copy.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
namespace System.Collections.ObjectModel
{
///
/// Implementation of a dynamic data collection based on generic Collection<T>,
/// implementing INotifyCollectionChanged to notify listeners
/// when items get added, removed or the whole list is refreshed.
///
public class ObservableRangeCollection : ObservableCollection
{
//------------------------------------------------------
//
// Private Fields
//
//------------------------------------------------------
#region Private Fields
[NonSerialized]
private DeferredEventsCollection _deferredEvents;
#endregion Private Fields
//------------------------------------------------------
//
// Constructors
//
//------------------------------------------------------
#region Constructors
///
/// Initializes a new instance of ObservableCollection that is empty and has default initial capacity.
///
public ObservableRangeCollection() { }
///
/// Initializes a new instance of the ObservableCollection class that contains
/// elements copied from the specified collection and has sufficient capacity
/// to accommodate the number of elements copied.
///
/// The collection whose elements are copied to the new list.
///
/// The elements are copied onto the ObservableCollection in the
/// same order they are read by the enumerator of the collection.
///
/// collection is a null reference
public ObservableRangeCollection(IEnumerable collection) : base(collection) { }
///
/// Initializes a new instance of the ObservableCollection class
/// that contains elements copied from the specified list
///
/// The list whose elements are copied to the new list.
///
/// The elements are copied onto the ObservableCollection in the
/// same order they are read by the enumerator of the list.
///
/// list is a null reference
public ObservableRangeCollection(List list) : base(list) { }
#endregion Constructors
//------------------------------------------------------
//
// Public Methods
//
//------------------------------------------------------
#region Public Methods
///
/// Adds the elements of the specified collection to the end of the .
///
///
/// The collection whose elements should be added to the end of the .
/// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.
///
/// is null.
public void AddRange(IEnumerable collection)
{
InsertRange(Count, collection);
}
///
/// Inserts the elements of a collection into the at the specified index.
///
/// The zero-based index at which the new elements should be inserted.
/// The collection whose elements should be inserted into the List.
/// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.
/// is null.
/// is not in the collection range.
public void InsertRange(int index, IEnumerable collection)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
if (index > Count)
throw new ArgumentOutOfRangeException(nameof(index));
if (collection is ICollection countable)
{
if (countable.Count == 0)
{
return;
}
}
else if (!ContainsAny(collection))
{
return;
}
CheckReentrancy();
//expand the following couple of lines when adding more constructors.
var target = (List)Items;
target.InsertRange(index, collection);
OnEssentialPropertiesChanged();
if (!(collection is IList list))
list = new List(collection);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list, index));
}
///
/// Removes the first occurence of each item in the specified collection from the .
///
/// The items to remove.
/// is null.
public void RemoveRange(IEnumerable collection)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));
if (Count == 0)
{
return;
}
else if (collection is ICollection countable)
{
if (countable.Count == 0)
return;
else if (countable.Count == 1)
using (IEnumerator enumerator = countable.GetEnumerator())
{
enumerator.MoveNext();
Remove(enumerator.Current);
return;
}
}
else if (!(ContainsAny(collection)))
{
return;
}
CheckReentrancy();
var clusters = new Dictionary>();
var lastIndex = -1;
List lastCluster = null;
foreach (T item in collection)
{
var index = IndexOf(item);
if (index < 0)
{
continue;
}
Items.RemoveAt(index);
if (lastIndex == index && lastCluster != null)
{
lastCluster.Add(item);
}
else
{
clusters[lastIndex = index] = lastCluster = new List { item };
}
}
OnEssentialPropertiesChanged();
if (Count == 0)
OnCollectionReset();
else
foreach (KeyValuePair> cluster in clusters)
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster.Value, cluster.Key));
}
///
/// Iterates over the collection and removes all items that satisfy the specified match.
///
/// The complexity is O(n).
///
/// Returns the number of elements that where
/// is null.
public int RemoveAll(Predicate match)
{
return RemoveAll(0, Count, match);
}
///
/// Iterates over the specified range within the collection and removes all items that satisfy the specified match.
///
/// The complexity is O(n).
/// The index of where to start performing the search.
/// The number of items to iterate on.
///
/// Returns the number of elements that where
/// is out of range.
/// is out of range.
/// is null.
public int RemoveAll(int index, int count, Predicate match)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count));
if (index + count > Count)
throw new ArgumentOutOfRangeException(nameof(index));
if (match == null)
throw new ArgumentNullException(nameof(match));
if (Count == 0)
return 0;
List cluster = null;
var clusterIndex = -1;
var removedCount = 0;
using (BlockReentrancy())
using (DeferEvents())
{
for (var i = 0; i < count; i++, index++)
{
T item = Items[index];
if (match(item))
{
Items.RemoveAt(index);
removedCount++;
if (clusterIndex == index)
{
Debug.Assert(cluster != null);
cluster.Add(item);
}
else
{
cluster = new List { item };
clusterIndex = index;
}
index--;
}
else if (clusterIndex > -1)
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster, clusterIndex));
clusterIndex = -1;
cluster = null;
}
}
if (clusterIndex > -1)
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster, clusterIndex));
}
if (removedCount > 0)
OnEssentialPropertiesChanged();
return removedCount;
}
///
/// Removes a range of elements from the >.
///
/// The zero-based starting index of the range of elements to remove.
/// The number of elements to remove.
/// The specified range is exceeding the collection.
public void RemoveRange(int index, int count)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count));
if (index + count > Count)
throw new ArgumentOutOfRangeException(nameof(index));
if (count == 0)
return;
if (count == 1)
{
RemoveItem(index);
return;
}
//Items will always be List, see constructors
var items = (List)Items;
List removedItems = items.GetRange(index, count);
CheckReentrancy();
items.RemoveRange(index, count);
OnEssentialPropertiesChanged();
if (Count == 0)
OnCollectionReset();
else
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, index));
}
///
/// Clears the current collection and replaces it with the specified collection,
/// using the default .
///
/// The items to fill the collection with, after clearing it.
/// is null.
public void ReplaceRange(IEnumerable collection)
{
ReplaceRange(0, Count, collection, EqualityComparer.Default);
}
///
/// Clears the current collection and replaces it with the specified collection,
/// using the specified comparer to skip equal items.
///
/// The items to fill the collection with, after clearing it.
/// An to be used
/// to check whether an item in the same location already existed before,
/// which in case it would not be added to the collection, and no event will be raised for it.
/// is null.
/// is null.
public void ReplaceRange(IEnumerable collection, IEqualityComparer comparer)
{
ReplaceRange(0, Count, collection, comparer);
}
///
/// Removes the specified range and inserts the specified collection,
/// ignoring equal items (using ).
///
/// The index of where to start the replacement.
/// The number of items to be replaced.
/// The collection to insert in that location.
/// is out of range.
/// is out of range.
/// is null.
public void ReplaceRange(int index, int count, IEnumerable collection)
{
ReplaceRange(index, count, collection, EqualityComparer.Default);
}
///
/// Removes the specified range and inserts the specified collection in its position, leaving equal items in equal positions intact.
///
/// The index of where to start the replacement.
/// The number of items to be replaced.
/// The collection to insert in that location.
/// The comparer to use when checking for equal items.
/// is out of range.
/// is out of range.
/// is null.
/// is null.
public void ReplaceRange(int index, int count, IEnumerable collection, IEqualityComparer comparer)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count));
if (index + count > Count)
throw new ArgumentOutOfRangeException(nameof(index));
if (collection == null)
throw new ArgumentNullException(nameof(collection));
if (comparer == null)
throw new ArgumentNullException(nameof(comparer));
if (collection is ICollection countable)
{
if (countable.Count == 0)
{
RemoveRange(index, count);
return;
}
}
else if (!ContainsAny(collection))
{
RemoveRange(index, count);
return;
}
if (index + count == 0)
{
InsertRange(0, collection);
return;
}
if (!(collection is IList list))
list = new List(collection);
using (BlockReentrancy())
using (DeferEvents())
{
var rangeCount = index + count;
var addedCount = list.Count;
var changesMade = false;
List
newCluster = null,
oldCluster = null;
int i = index;
for (; i < rangeCount && i - index < addedCount; i++)
{
//parallel position
T old = this[i], @new = list[i - index];
if (comparer.Equals(old, @new))
{
OnRangeReplaced(i, newCluster, oldCluster);
continue;
}
else
{
Items[i] = @new;
if (newCluster == null)
{
Debug.Assert(oldCluster == null);
newCluster = new List { @new };
oldCluster = new List { old };
}
else
{
newCluster.Add(@new);
oldCluster.Add(old);
}
changesMade = true;
}
}
OnRangeReplaced(i, newCluster, oldCluster);
//exceeding position
if (count != addedCount)
{
var items = (List)Items;
if (count > addedCount)
{
var removedCount = rangeCount - addedCount;
T[] removed = new T[removedCount];
items.CopyTo(i, removed, 0, removed.Length);
items.RemoveRange(i, removedCount);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed, i));
}
else
{
var k = i - index;
T[] added = new T[addedCount - k];
for (int j = k; j < addedCount; j++)
{
T @new = list[j];
added[j - k] = @new;
}
items.InsertRange(i, added);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, added, i));
}
OnEssentialPropertiesChanged();
}
else if (changesMade)
{
OnIndexerPropertyChanged();
}
}
}
#endregion Public Methods
//------------------------------------------------------
//
// Protected Methods
//
//------------------------------------------------------
#region Protected Methods
///
/// Called by base class Collection<T> when the list is being cleared;
/// raises a CollectionChanged event to any listeners.
///
protected override void ClearItems()
{
if (Count == 0)
return;
CheckReentrancy();
base.ClearItems();
OnEssentialPropertiesChanged();
OnCollectionReset();
}
///
/// Called by base class Collection<T> when an item is set in list;
/// raises a CollectionChanged event to any listeners.
///
protected override void SetItem(int index, T item)
{
if (Equals(this[index], item))
return;
CheckReentrancy();
T originalItem = this[index];
base.SetItem(index, item);
OnIndexerPropertyChanged();
OnCollectionChanged(NotifyCollectionChangedAction.Replace, originalItem, item, index);
}
///
/// Raise CollectionChanged event to any listeners.
/// Properties/methods modifying this ObservableCollection will raise
/// a collection changed event through this virtual method.
///
///
/// When overriding this method, either call its base implementation
/// or call to guard against reentrant collection changes.
///
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (_deferredEvents != null)
{
_deferredEvents.Add(e);
return;
}
base.OnCollectionChanged(e);
}
protected virtual IDisposable DeferEvents() => new DeferredEventsCollection(this);
#endregion Protected Methods
//------------------------------------------------------
//
// Private Methods
//
//------------------------------------------------------
#region Private Methods
///
/// Helper function to determine if a collection contains any elements.
///
/// The collection to evaluate.
///
private static bool ContainsAny(IEnumerable collection)
{
using (IEnumerator enumerator = collection.GetEnumerator())
return enumerator.MoveNext();
}
///
/// Helper to raise Count property and the Indexer property.
///
private void OnEssentialPropertiesChanged()
{
OnPropertyChanged(EventArgsCache.CountPropertyChanged);
OnIndexerPropertyChanged();
}
///
/// /// Helper to raise a PropertyChanged event for the Indexer property
/// ///
private void OnIndexerPropertyChanged() =>
OnPropertyChanged(EventArgsCache.IndexerPropertyChanged);
///
/// Helper to raise CollectionChanged event to any listeners
///
private void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index) =>
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
///
/// Helper to raise CollectionChanged event with action == Reset to any listeners
///
private void OnCollectionReset() =>
OnCollectionChanged(EventArgsCache.ResetCollectionChanged);
///
/// Helper to raise event for clustered action and clear cluster.
///
/// The index of the item following the replacement block.
///
///
//TODO should have really been a local method inside ReplaceRange(int index, int count, IEnumerable collection, IEqualityComparer comparer),
//move when supported language version updated.
private void OnRangeReplaced(int followingItemIndex, ICollection newCluster, ICollection oldCluster)
{
if (oldCluster == null || oldCluster.Count == 0)
{
Debug.Assert(newCluster == null || newCluster.Count == 0);
return;
}
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Replace,
new List(newCluster),
new List(oldCluster),
followingItemIndex - oldCluster.Count));
oldCluster.Clear();
newCluster.Clear();
}
#endregion Private Methods
//------------------------------------------------------
//
// Private Types
//
//------------------------------------------------------
#region Private Types
private sealed class DeferredEventsCollection : List, IDisposable
{
private readonly ObservableRangeCollection _collection;
public DeferredEventsCollection(ObservableRangeCollection collection)
{
Debug.Assert(collection != null);
Debug.Assert(collection._deferredEvents == null);
_collection = collection;
_collection._deferredEvents = this;
}
public void Dispose()
{
_collection._deferredEvents = null;
foreach (var args in this)
_collection.OnCollectionChanged(args);
}
}
#endregion Private Types
}
///
/// To be kept outside , since otherwise, a new instance will be created for each generic type used.
///
internal static class EventArgsCache
{
internal static readonly PropertyChangedEventArgs CountPropertyChanged = new PropertyChangedEventArgs("Count");
internal static readonly PropertyChangedEventArgs IndexerPropertyChanged = new PropertyChangedEventArgs("Item[]");
internal static readonly NotifyCollectionChangedEventArgs ResetCollectionChanged = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
}
}