Adding a range of values to an ObservableCollection efficiently
Solution 1
The ObservableCollection exposes an protected Items
property which is the underlying collection without the notification semantics. This means you can build a collection that does what you want by inheriting ObservableCollection:
class RangeEnabledObservableCollection<T> : ObservableCollection<T>
{
public void InsertRange(IEnumerable<T> items)
{
this.CheckReentrancy();
foreach(var item in items)
this.Items.Add(item);
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
Usage:
void Main()
{
var collection = new RangeEnabledObservableCollection<int>();
collection.CollectionChanged += (s,e) => Console.WriteLine("Collection changed");
collection.InsertRange(Enumerable.Range(0,100));
Console.WriteLine("Collection contains {0} items.", collection.Count);
}
Solution 2
To make the above answer useful w/o deriving a new base class using reflection, here's an example:
public static void InsertRange<T>(this ObservableCollection<T> collection, IEnumerable<T> items)
{
var enumerable = items as List<T> ?? items.ToList();
if (collection == null || items == null || !enumerable.Any())
{
return;
}
Type type = collection.GetType();
type.InvokeMember("CheckReentrancy", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic, null, collection, null);
var itemsProp = type.BaseType.GetProperty("Items", BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Instance);
var privateItems = itemsProp.GetValue(collection) as IList<T>;
foreach (var item in enumerable)
{
privateItems.Add(item);
}
type.InvokeMember("OnPropertyChanged", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic, null,
collection, new object[] { new PropertyChangedEventArgs("Count") });
type.InvokeMember("OnPropertyChanged", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic, null,
collection, new object[] { new PropertyChangedEventArgs("Item[]") });
type.InvokeMember("OnCollectionChanged", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic, null,
collection, new object[]{ new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)});
}
Solution 3
This answer didn't show me the new entries in a DataGrid. This OnCollectionChanged works for me:
public class SilentObservableCollection<T> : ObservableCollection<T>
{
public void AddRange(IEnumerable<T> enumerable)
{
CheckReentrancy();
int startIndex = Count;
foreach (var item in enumerable)
Items.Add(item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(enumerable), startIndex));
OnPropertyChanged(new PropertyChangedEventArgs("Count"));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
}
}
GazTheDestroyer
Updated on March 27, 2020Comments
-
GazTheDestroyer over 4 years
I have an
ObservableCollection
of items that is bound to a list control in my view.I have a situation where I need to add a chunk of values to the start of the collection.
Collection<T>.Insert
documentation specifies each insert as an O(n) operation, and each insert also generates aCollectionChanged
notification.Therefore I would ideally like to insert the whole range of items in one move, meaning only one shuffle of the underlying list, and hopefully one
CollectionChanged
notification (presumably a "reset").Collection<T>
does not expose any method for doing this.List<T>
hasInsertRange()
, butIList<T>
, thatCollection<T>
exposes via itsItems
property does not.Is there any way at all to do this?