高效地将一系列数值添加到ObservableCollection中

36

我有一个ObservableCollection类型的项目集合,该集合绑定到视图中的列表控件。

我遇到了这样一种情况:我需要将一系列值添加到该集合的开头。 Collection<T>.Insert说明文档指定每个插入操作都是O(n)操作,并且每次插入还会生成一个CollectionChanged通知。

因此,理想情况下,我希望通过一次移动来整体插入所有项目范围,这意味着只需要对底层列表进行一次洗牌,而且希望只有一个CollectionChanged通知(可能是“重置”)。

Collection<T>没有公开任何方法来实现这一点。 List<T>具有InsertRange()方法,但是IList<T>通过其Items属性被Collection<T>公开时却没有提供该方法。

是否有任何方法可以做到这一点?


如果您有一个集合属性的后备字段 - 您可以将一个新实例分配给它,然后手动为集合属性引发 OnPropertyChanged - sll
1
相关/可能重复:https://dev59.com/tHRB5IYBdhLWcg3wSVcI - Adam
4
如果“ObservableCollection”让你想到量子力学和双缝实验,请点赞(+1)。 - rfmodulator
1
http://blogs.msdn.com/b/nathannesbit/archive/2009/04/20/addrange-and-observablecollection.aspx - Arsen Mkrtchyan
5个回答

60

ObservableCollection公开了一个受保护的Items属性,它是底层集合而没有通知语义。这意味着您可以通过继承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));
    }
}

用法:

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);  
}

8
一位 StackOverflow 上的用户在类似问题的另一个回答中提到,为了通知计数和索引器属性的更改,建议添加以下代码:this.OnPropertyChanged(new PropertyChangedEventArgs("Count")); this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); - bouvierr

11
为了不使用反射来派生新的基类并使上述答案更有用,下面是一个例子:

如何做到这一点,请看以下示例:

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)});
}

1
继承有什么问题吗?为什么要使用反射而不是派生一个新类? - ventiseis
3
继承没有错,当然。这只是一种替代方式。如果你有很多遗留代码,并且你的序列化(二进制)依赖于ObservableCollection类型,那么扩展ObservableCollection会更容易和实际一些。 - outbred
嗨@outbred。抱歉,你的例子该如何“使用”呢?谢谢。 - NevilleDastur
@NevilleDastur - 使用方式如下:var coll = new ObservableCollection(); coll.InsertRange(<一些其他巨大的列表>);这是一个扩展方法,所以请确保您使用了声明扩展方法的命名空间,然后就可以开始使用了。 - outbred
我投票支持这个扩展方法,因为它允许我使用它而无需将我的集合更改为新类。 - user755404

4

这个答案没有显示DataGrid中的新条目。对我有用的是这个OnCollectionChanged:

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[]"));
    }
}

这些似乎是更合适的变更通知。 - OttPrime
对我来说,这导致了一个异常,显示“不支持范围操作”,而另一个答案中的Reset则有效。这是用于填充网格的。 - Hammerite

0
10年后,现在我在我的项目中必须使用C#,而且我不希望触发多个OnCollectionChanged。我已经将@outbred的答案修改为一个扩展类。
使用方法:
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

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)
                });
        }
    }
}


-5

示例: 期望步骤为0、10、20、30、40、50、60、70、80、90、100 --> 最小值=0,最大值=100,步骤数=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())
    );

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接