Adding a range of values to an ObservableCollection efficiently
Asked Answered
B

5

37

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 a CollectionChanged 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> has InsertRange(), but IList<T>, that Collection<T> exposes via its Items property does not.

Is there any way at all to do this?

Bustup answered 22/12, 2011 at 16:38 Comment(4)
If you have a backing field for collection property - you can assign a new instance to it and then raise OnPropertyChanged for collection proeprty manuallyLobell
related/possible duplicate: #671077Various
+1 if ObservableCollection makes you think of quantum mechanics and the double-slit experiment.Mobility
blogs.msdn.com/b/nathannesbit/archive/2009/04/20/…Stefanstefanac
W
60

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);  
}
Worcester answered 22/12, 2011 at 16:49 Comment(1)
This other answer to a similar question suggested to add the following code to notify changes to the count and indexer properties: this.OnPropertyChanged(new PropertyChangedEventArgs("Count")); this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));Astarte
O
12

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)});
}
Outmoded answered 2/5, 2014 at 0:24 Comment(5)
What's wrong with inheritenance? Why are use using reflection instead of deriving a new class?Athanor
There's nothing wrong with inheritance, of course. This is just an alternative to that. It may prove useful if you have a ton of legacy code and your serialization (binary) depends on an ObservableCollection type. Easier and a far more pragmatic choice to just extend ObservableCollection in that case.Outmoded
Hi @Outmoded . sorry but how do you "use" your example. ThanksInfrasonic
@Infrasonic - use it like this: var coll = new ObservableCollection(); coll.InsertRange(<some other huge list>); It's an extension method so just make sure you are using the namespace where you declare the extension method and you should be good to go.Outmoded
I voted for this as extension method allows me to use without having to change my collections to a new class.Franchot
T
4

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[]"));
    }
}
Trueblue answered 26/3, 2017 at 20:10 Comment(2)
These seem like the more appropriate change notifications.Finzer
For me, this resulted in an exception saying "Range actions are not supported", whereas the Reset one from the other answer worked. This was for populating a grid.Sollars
A
0

10 years later, now that I've to use C# in my project and I don't wish to trigger multiple OnCollectionChanged. I've revised @outbred's answer into an Extension class.

Usage:

var stuff = new ObservableCollection<Stuff>() {...};
...
// will trigger only 1 OnCollectionChanged event
stuff.ReplaceCollection(newThings);

// equivalent without the extension methods
// stuff.Clear();       // triggers 1 OnCollectionChanged 
// foreach (var thing in newThings)
//    stuff.Add(thing); // triggers multiple OnCollectionChanged

The ObservableCollectionExtensions.cs:

using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reflection;

namespace System.Collections.ObjectModel
{
    public static class ObservableCollectionExtensions
    {
        private static BindingFlags ProtectedMember = BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic;
        private static BindingFlags ProtectedProperty = BindingFlags.Instance | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic;

        /// <summary>
        /// Insert a collection without triggering OnCollectionChanged event 
        /// </summary>
        private static void InsertWithoutNotify<T>(this ObservableCollection<T> collection, IEnumerable<T> items, int index = -1)
        {
            if (collection == null || items == null || !items.Any()) return;
            Type type = collection.GetType();

            type.InvokeMember("CheckReentrancy", ProtectedMember, null, collection, null);

            PropertyInfo itemsProp = type.BaseType.GetProperty("Items", ProtectedProperty);
            IList<T> protectedItems = itemsProp.GetValue(collection) as IList<T>;

            // Behave the same as Add if no index is being passed
            int start = index > -1 ? index : protectedItems.Count();
            int end = items.Count();
            for (int i = 0; i < end; i++)
            {
                protectedItems.Insert(start + i, items.ElementAt(i));
            }

            type.InvokeMember("OnPropertyChanged", ProtectedMember, null,
              collection, new object[] { new PropertyChangedEventArgs("Count") });

            type.InvokeMember("OnPropertyChanged", ProtectedMember, null,
              collection, new object[] { new PropertyChangedEventArgs("Item[]") });
        }

        public static void AddRange<T>(this ObservableCollection<T> collection, IEnumerable<T> items)
        {
            if (collection == null || items == null || !items.Any()) return;

            Type type = collection.GetType();

            InsertWithoutNotify(collection, items);

            type.InvokeMember("OnCollectionChanged", ProtectedMember, null,
              collection, new object[] {
                  // Notify that we've added new items into the collection
                  new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)
              });
        }

        public static void InsertRange<T>(this ObservableCollection<T> collection, int index, IEnumerable<T> items)
        {
            if (collection == null || items == null || !items.Any()) return;

            Type type = collection.GetType();

            InsertWithoutNotify(collection, items, index);

            type.InvokeMember("OnCollectionChanged", ProtectedMember, null,
              collection, new object[] {
                  // Notify that we've added new items into the collection
                  new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)
              });
        }

        public static void ReplaceCollection<T>(this ObservableCollection<T> collection, IEnumerable<T> items)
        {
            if (collection == null || items == null || !items.Any()) return;

            Type type = collection.GetType();

            // Clear the underlaying list items without triggering a change
            PropertyInfo itemsProp = type.BaseType.GetProperty("Items", ProtectedProperty);
            IList<T> protectedItems = itemsProp.GetValue(collection) as IList<T>;
            protectedItems.Clear();

            // Perform the actual update
            InsertWithoutNotify(collection, items);

            type.InvokeMember("OnCollectionChanged", ProtectedMember, null,
                collection, new object[] {
                    // Notify that we have replaced the entire collection
                    new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)
                });
        }
    }
}

Apiculate answered 30/10, 2023 at 17:28 Comment(0)
L
-5

Example: Desired steps 0,10,20,30,40,50,60,70,80,90,100 --> min=0, max=100, steps=11

    static int min = 0;
    static int max = 100;
    static int steps = 11; 

    private ObservableCollection<string> restartDelayTimeList = new ObservableCollection<string> (
        Enumerable.Range(0, steps).Select(l1 => (min + (max - min) * ((double)l1 / (steps - 1))).ToString())
    );
Levinson answered 14/7, 2016 at 13:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.