ObservableCollection不支持AddRange方法,因此我会对添加的每个项目收到通知,除此之外,INotifyCollectionChanging怎么样?

185
13个回答

133
请参考更新和优化的C#7版本,我不想删除VB.NET版本,所以我将其放在一个单独的答案中发布。

转到更新版本

看起来它不被支持,我自己实现了这个功能,供您参考,希望对您有所帮助:
我更新了VB版本,现在在更改集合之前会引发一个事件,因此您可以后悔(使用、等时非常有用,并且您可以向用户显示“是否确定”确认),更新的VB版本在此消息底部
请原谅我的屏幕太窄,无法容纳我的代码,我也不喜欢这样做。 VB.NET:
Imports System.Collections.Specialized

Namespace System.Collections.ObjectModel
    ''' <summary>
    ''' Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
    ''' </summary>
    ''' <typeparam name="T"></typeparam>
    Public Class ObservableRangeCollection(Of T) : Inherits System.Collections.ObjectModel.ObservableCollection(Of T)

        ''' <summary>
        ''' Adds the elements of the specified collection to the end of the ObservableCollection(Of T).
        ''' </summary>
        Public Sub AddRange(ByVal collection As IEnumerable(Of T))
            For Each i In collection
                Items.Add(i)
            Next
            OnCollectionChanged(New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))
        End Sub

        ''' <summary>
        ''' Removes the first occurence of each item in the specified collection from ObservableCollection(Of T).
        ''' </summary>
        Public Sub RemoveRange(ByVal collection As IEnumerable(Of T))
            For Each i In collection
                Items.Remove(i)
            Next

            OnCollectionChanged(New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))
        End Sub

        ''' <summary>
        ''' Clears the current collection and replaces it with the specified item.
        ''' </summary>
        Public Sub Replace(ByVal item As T)
            ReplaceRange(New T() {item})
        End Sub
        ''' <summary>
        ''' Clears the current collection and replaces it with the specified collection.
        ''' </summary>
        Public Sub ReplaceRange(ByVal collection As IEnumerable(Of T))
            Dim old = Items.ToList
            Items.Clear()
            For Each i In collection
                Items.Add(i)
            Next
            OnCollectionChanged(New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))
        End Sub

        ''' <summary>
        ''' Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class.
        ''' </summary>
        ''' <remarks></remarks>
        Public Sub New()
            MyBase.New()
        End Sub
        ''' <summary>
        ''' Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class that contains elements copied from the specified collection.
        ''' </summary>
        ''' <param name="collection">collection: The collection from which the elements are copied.</param>
        ''' <exception cref="System.ArgumentNullException">The collection parameter cannot be null.</exception>
        Public Sub New(ByVal collection As IEnumerable(Of T))
            MyBase.New(collection)
        End Sub
    End Class   

End Namespace

C#:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;

/// <summary> 
/// Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed. 
/// </summary> 
/// <typeparam name="T"></typeparam> 
public class ObservableRangeCollection<T> : ObservableCollection<T>
{
    /// <summary> 
    /// Adds the elements of the specified collection to the end of the ObservableCollection(Of T). 
    /// </summary> 
    public void AddRange(IEnumerable<T> collection)
    {
        if (collection == null) throw new ArgumentNullException("collection");

        foreach (var i in collection) Items.Add(i);
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    /// <summary> 
    /// Removes the first occurence of each item in the specified collection from ObservableCollection(Of T). 
    /// </summary> 
    public void RemoveRange(IEnumerable<T> collection)
    {
        if (collection == null) throw new ArgumentNullException("collection");

        foreach (var i in collection) Items.Remove(i);
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    /// <summary> 
    /// Clears the current collection and replaces it with the specified item. 
    /// </summary> 
    public void Replace(T item)
    {
        ReplaceRange(new T[] { item });
    }

    /// <summary> 
    /// Clears the current collection and replaces it with the specified collection. 
    /// </summary> 
    public void ReplaceRange(IEnumerable<T> collection)
    {
        if (collection == null) throw new ArgumentNullException("collection");

        Items.Clear();
        foreach (var i in collection) Items.Add(i);
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    /// <summary> 
    /// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class. 
    /// </summary> 
    public ObservableRangeCollection()
        : base() { }

    /// <summary> 
    /// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class that contains elements copied from the specified collection. 
    /// </summary> 
    /// <param name="collection">collection: The collection from which the elements are copied.</param> 
    /// <exception cref="System.ArgumentNullException">The collection parameter cannot be null.</exception> 
    public ObservableRangeCollection(IEnumerable<T> collection)
        : base(collection) { }
}

更新 - 可观察的范围集合,带有集合更改通知

Imports System.Collections.Specialized
Imports System.ComponentModel
Imports System.Collections.ObjectModel

Public Class ObservableRangeCollection(Of T) : Inherits ObservableCollection(Of T) : Implements INotifyCollectionChanging(Of T)
    ''' <summary>
    ''' Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub New()
        MyBase.New()
    End Sub

    ''' <summary>
    ''' Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class that contains elements copied from the specified collection.
    ''' </summary>
    ''' <param name="collection">collection: The collection from which the elements are copied.</param>
    ''' <exception cref="System.ArgumentNullException">The collection parameter cannot be null.</exception>
    Public Sub New(ByVal collection As IEnumerable(Of T))
        MyBase.New(collection)
    End Sub

    ''' <summary>
    ''' Adds the elements of the specified collection to the end of the ObservableCollection(Of T).
    ''' </summary>
    Public Sub AddRange(ByVal collection As IEnumerable(Of T))
        Dim ce As New NotifyCollectionChangingEventArgs(Of T)(NotifyCollectionChangedAction.Add, collection)
        OnCollectionChanging(ce)
        If ce.Cancel Then Exit Sub

        Dim index = Items.Count - 1
        For Each i In collection
            Items.Add(i)
        Next

        OnCollectionChanged(New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, collection, index))
    End Sub


    ''' <summary>
    ''' Inserts the collection at specified index.
    ''' </summary>
    Public Sub InsertRange(ByVal index As Integer, ByVal Collection As IEnumerable(Of T))
        Dim ce As New NotifyCollectionChangingEventArgs(Of T)(NotifyCollectionChangedAction.Add, Collection)
        OnCollectionChanging(ce)
        If ce.Cancel Then Exit Sub

        For Each i In Collection
            Items.Insert(index, i)
        Next

        OnCollectionChanged(New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))
    End Sub


    ''' <summary>
    ''' Removes the first occurence of each item in the specified collection from ObservableCollection(Of T).
    ''' </summary>
    Public Sub RemoveRange(ByVal collection As IEnumerable(Of T))
        Dim ce As New NotifyCollectionChangingEventArgs(Of T)(NotifyCollectionChangedAction.Remove, collection)
        OnCollectionChanging(ce)
        If ce.Cancel Then Exit Sub

        For Each i In collection
            Items.Remove(i)
        Next

        OnCollectionChanged(New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))
    End Sub



    ''' <summary>
    ''' Clears the current collection and replaces it with the specified item.
    ''' </summary>
    Public Sub Replace(ByVal item As T)
        ReplaceRange(New T() {item})
    End Sub

    ''' <summary>
    ''' Clears the current collection and replaces it with the specified collection.
    ''' </summary>
    Public Sub ReplaceRange(ByVal collection As IEnumerable(Of T))
        Dim ce As New NotifyCollectionChangingEventArgs(Of T)(NotifyCollectionChangedAction.Replace, Items)
        OnCollectionChanging(ce)
        If ce.Cancel Then Exit Sub

        Items.Clear()
        For Each i In collection
            Items.Add(i)
        Next
        OnCollectionChanged(New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))
    End Sub

    Protected Overrides Sub ClearItems()
        Dim e As New NotifyCollectionChangingEventArgs(Of T)(NotifyCollectionChangedAction.Reset, Items)
        OnCollectionChanging(e)

        If e.Cancel Then Exit Sub

        MyBase.ClearItems()
    End Sub

    Protected Overrides Sub InsertItem(ByVal index As Integer, ByVal item As T)
        Dim ce As New NotifyCollectionChangingEventArgs(Of T)(NotifyCollectionChangedAction.Add, item)
        OnCollectionChanging(ce)
        If ce.Cancel Then Exit Sub

        MyBase.InsertItem(index, item)
    End Sub

    Protected Overrides Sub MoveItem(ByVal oldIndex As Integer, ByVal newIndex As Integer)
        Dim ce As New NotifyCollectionChangingEventArgs(Of T)()
        OnCollectionChanging(ce)
        If ce.Cancel Then Exit Sub

        MyBase.MoveItem(oldIndex, newIndex)
    End Sub

    Protected Overrides Sub RemoveItem(ByVal index As Integer)
        Dim ce As New NotifyCollectionChangingEventArgs(Of T)(NotifyCollectionChangedAction.Remove, Items(index))
        OnCollectionChanging(ce)
        If ce.Cancel Then Exit Sub

        MyBase.RemoveItem(index)
    End Sub

    Protected Overrides Sub SetItem(ByVal index As Integer, ByVal item As T)
        Dim ce As New NotifyCollectionChangingEventArgs(Of T)(NotifyCollectionChangedAction.Replace, Items(index))
        OnCollectionChanging(ce)
        If ce.Cancel Then Exit Sub

        MyBase.SetItem(index, item)
    End Sub

    Protected Overrides Sub OnCollectionChanged(ByVal e As Specialized.NotifyCollectionChangedEventArgs)
        If e.NewItems IsNot Nothing Then
            For Each i As T In e.NewItems
                If TypeOf i Is INotifyPropertyChanged Then AddHandler DirectCast(i, INotifyPropertyChanged).PropertyChanged, AddressOf Item_PropertyChanged
            Next
        End If
        MyBase.OnCollectionChanged(e)
    End Sub

    Private Sub Item_PropertyChanged(ByVal sender As T, ByVal e As ComponentModel.PropertyChangedEventArgs)
        OnCollectionChanged(New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset, sender, IndexOf(sender)))
    End Sub

    Public Event CollectionChanging(ByVal sender As Object, ByVal e As NotifyCollectionChangingEventArgs(Of T)) Implements INotifyCollectionChanging(Of T).CollectionChanging
    Protected Overridable Sub OnCollectionChanging(ByVal e As NotifyCollectionChangingEventArgs(Of T))
        RaiseEvent CollectionChanging(Me, e)
    End Sub
End Class


Public Interface INotifyCollectionChanging(Of T)
    Event CollectionChanging(ByVal sender As Object, ByVal e As NotifyCollectionChangingEventArgs(Of T))
End Interface

Public Class NotifyCollectionChangingEventArgs(Of T) : Inherits CancelEventArgs

    Public Sub New()
        m_Action = NotifyCollectionChangedAction.Move
        m_Items = New T() {}
    End Sub

    Public Sub New(ByVal action As NotifyCollectionChangedAction, ByVal item As T)
        m_Action = action
        m_Items = New T() {item}
    End Sub

    Public Sub New(ByVal action As NotifyCollectionChangedAction, ByVal items As IEnumerable(Of T))
        m_Action = action
        m_Items = items
    End Sub

    Private m_Action As NotifyCollectionChangedAction
    Public ReadOnly Property Action() As NotifyCollectionChangedAction
        Get
            Return m_Action
        End Get
    End Property

    Private m_Items As IList
    Public ReadOnly Property Items() As IEnumerable(Of T)
        Get
            Return m_Items
        End Get
    End Property
End Class

7
Scott Dorman是正确的。调用内置的Add方法会触发大约3个事件(一个用于项目计数的NotifyPropertyChange,一个用于项目索引[]的NotifyPropertyChange和一个用于添加项目的NotifyCollectionChanged事件)。为了获得大规模更新的最佳性能,需要完全替换ObservableCollection。 - iCollect.it Ltd
15
@HiTechMagic Scott的说法是不正确的。因为这里调用了Collection<T>受保护的Items IList<T>上的Add方法,所以不会发送任何更改事件。我已经通过单元测试验证了使用AddRange时CollectionChanged事件的调用次数,可以确认只有一次。 - weston
8
需要在添加/删除/替换范围方法中调用OnPropertyChanged("Count");OnPropertyChanged("Item[]");以完全模仿标准ObservableCollection。 - weston
11
C#代码无法运行:附加信息:不支持范围操作。 - Saykor
4
这段 C# 代码有问题,但很容易修复。只需将所有 NotifyCollectionChangedAction.* 实例更改为 NotifyCollectionChangedAction.Reset 并不传递集合。我猜测这会重新评估整个集合,但基本上与多次通知每次添加相比仍然要好得多。顺便说一下:这让我处理需要大约 2 分钟的数据在 1 秒内完成了,非常感谢! - tobriand
显示剩余8条评论

60
首先,请在.NET仓库上的API请求中投票并发表评论。
这是我优化过的ObservableRangeCollection版本(基于James Montemagno的版本)。
它执行得非常快,旨在在可能时重复使用现有元素并避免不必要的事件或将它们合并成一个。 ReplaceRange方法通过适当的索引替换/删除/添加所需的元素,并批处理可能的事件。
在Xamarin.Forms UI上经过测试,对于大量集合的非常频繁的更新(每秒5-7次),结果非常好。
注意: 由于WPF不习惯使用范围操作,因此在WPF UI相关工作中使用下面的ObservableRangeCollection(例如将其绑定到ListBox等)时,它会抛出NotSupportedException异常(如果不绑定到UI,则仍然可以使用ObservableRangeCollection<T>)。 但是,您可以使用WpfObservableRangeCollection<T>解决方法。 真正的解决方案是创建一个CollectionView,它知道如何处理范围操作,但我仍然没有时间实现这一点。 原始代码 - 打开为原始代码,然后按Ctrl+A选择全部,再按Ctrl+C复制。
// 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
{
  /// <summary>
  /// Implementation of a dynamic data collection based on generic Collection&lt;T&gt;,
  /// implementing INotifyCollectionChanged to notify listeners
  /// when items get added, removed or the whole list is refreshed.
  /// </summary>
  public class ObservableRangeCollection<T> : ObservableCollection<T>
  {
    //------------------------------------------------------
    //
    //  Private Fields
    //
    //------------------------------------------------------

    #region Private Fields    
    [NonSerialized]
    private DeferredEventsCollection _deferredEvents;
    #endregion Private Fields


    //------------------------------------------------------
    //
    //  Constructors
    //
    //------------------------------------------------------

    #region Constructors
    /// <summary>
    /// Initializes a new instance of ObservableCollection that is empty and has default initial capacity.
    /// </summary>
    public ObservableRangeCollection() { }

    /// <summary>
    /// 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.
    /// </summary>
    /// <param name="collection">The collection whose elements are copied to the new list.</param>
    /// <remarks>
    /// The elements are copied onto the ObservableCollection in the
    /// same order they are read by the enumerator of the collection.
    /// </remarks>
    /// <exception cref="ArgumentNullException"> collection is a null reference </exception>
    public ObservableRangeCollection(IEnumerable<T> collection) : base(collection) { }

    /// <summary>
    /// Initializes a new instance of the ObservableCollection class
    /// that contains elements copied from the specified list
    /// </summary>
    /// <param name="list">The list whose elements are copied to the new list.</param>
    /// <remarks>
    /// The elements are copied onto the ObservableCollection in the
    /// same order they are read by the enumerator of the list.
    /// </remarks>
    /// <exception cref="ArgumentNullException"> list is a null reference </exception>
    public ObservableRangeCollection(List<T> list) : base(list) { }

    #endregion Constructors

    //------------------------------------------------------
    //
    //  Public Methods
    //
    //------------------------------------------------------

    #region Public Methods

    /// <summary>
    /// Adds the elements of the specified collection to the end of the <see cref="ObservableCollection{T}"/>.
    /// </summary>
    /// <param name="collection">
    /// The collection whose elements should be added to the end of the <see cref="ObservableCollection{T}"/>.
    /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.
    /// </param>
    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
    public void AddRange(IEnumerable<T> collection)
    {
      InsertRange(Count, collection);
    }

    /// <summary>
    /// Inserts the elements of a collection into the <see cref="ObservableCollection{T}"/> at the specified index.
    /// </summary>
    /// <param name="index">The zero-based index at which the new elements should be inserted.</param>
    /// <param name="collection">The collection whose elements should be inserted into the List<T>.
    /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.</param>                
    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
    /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is not in the collection range.</exception>
    public void InsertRange(int index, IEnumerable<T> 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<T> 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<T>)Items;
      target.InsertRange(index, collection);

      OnEssentialPropertiesChanged();

      if (!(collection is IList list))
        list = new List<T>(collection);

      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list, index));
    }


    /// <summary> 
    /// Removes the first occurence of each item in the specified collection from the <see cref="ObservableCollection{T}"/>.
    /// </summary>
    /// <param name="collection">The items to remove.</param>        
    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
    public void RemoveRange(IEnumerable<T> collection)
    {
      if (collection == null)
        throw new ArgumentNullException(nameof(collection));

      if (Count == 0)
      {
        return;
      }
      else if (collection is ICollection<T> countable)
      {
        if (countable.Count == 0)
          return;
        else if (countable.Count == 1)
          using (IEnumerator<T> enumerator = countable.GetEnumerator())
          {
            enumerator.MoveNext();
            Remove(enumerator.Current);
            return;
          }
      }
      else if (!(ContainsAny(collection)))
      {
        return;
      }

      CheckReentrancy();

      var clusters = new Dictionary<int, List<T>>();
      var lastIndex = -1;
      List<T> 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<T> { item };
        }
      }

      OnEssentialPropertiesChanged();

      if (Count == 0)
        OnCollectionReset();
      else
        foreach (KeyValuePair<int, List<T>> cluster in clusters)
          OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster.Value, cluster.Key));

    }

    /// <summary>
    /// Iterates over the collection and removes all items that satisfy the specified match.
    /// </summary>
    /// <remarks>The complexity is O(n).</remarks>
    /// <param name="match"></param>
    /// <returns>Returns the number of elements that where </returns>
    /// <exception cref="ArgumentNullException"><paramref name="match"/> is null.</exception>
    public int RemoveAll(Predicate<T> match)
    {
      return RemoveAll(0, Count, match);
    }

    /// <summary>
    /// Iterates over the specified range within the collection and removes all items that satisfy the specified match.
    /// </summary>
    /// <remarks>The complexity is O(n).</remarks>
    /// <param name="index">The index of where to start performing the search.</param>
    /// <param name="count">The number of items to iterate on.</param>
    /// <param name="match"></param>
    /// <returns>Returns the number of elements that where </returns>
    /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
    /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is out of range.</exception>
    /// <exception cref="ArgumentNullException"><paramref name="match"/> is null.</exception>
    public int RemoveAll(int index, int count, Predicate<T> 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<T> 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<T> { 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;
    }

    /// <summary>
    /// Removes a range of elements from the <see cref="ObservableCollection{T}"/>>.
    /// </summary>
    /// <param name="index">The zero-based starting index of the range of elements to remove.</param>
    /// <param name="count">The number of elements to remove.</param>
    /// <exception cref="ArgumentOutOfRangeException">The specified range is exceeding the collection.</exception>
    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<T>, see constructors
      var items = (List<T>)Items;
      List<T> removedItems = items.GetRange(index, count);

      CheckReentrancy();

      items.RemoveRange(index, count);

      OnEssentialPropertiesChanged();

      if (Count == 0)
        OnCollectionReset();
      else
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, index));
    }

    /// <summary> 
    /// Clears the current collection and replaces it with the specified collection,
    /// using the default <see cref="EqualityComparer{T}"/>.
    /// </summary>             
    /// <param name="collection">The items to fill the collection with, after clearing it.</param>
    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
    public void ReplaceRange(IEnumerable<T> collection)
    {
      ReplaceRange(0, Count, collection, EqualityComparer<T>.Default);
    }

    /// <summary>
    /// Clears the current collection and replaces it with the specified collection,
    /// using the specified comparer to skip equal items.
    /// </summary>
    /// <param name="collection">The items to fill the collection with, after clearing it.</param>
    /// <param name="comparer">An <see cref="IEqualityComparer{T}"/> 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.</param>
    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
    /// <exception cref="ArgumentNullException"><paramref name="comparer"/> is null.</exception>
    public void ReplaceRange(IEnumerable<T> collection, IEqualityComparer<T> comparer)
    {
      ReplaceRange(0, Count, collection, comparer);
    }

    /// <summary>
    /// Removes the specified range and inserts the specified collection,
    /// ignoring equal items (using <see cref="EqualityComparer{T}.Default"/>).
    /// </summary>
    /// <param name="index">The index of where to start the replacement.</param>
    /// <param name="count">The number of items to be replaced.</param>
    /// <param name="collection">The collection to insert in that location.</param>
    /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
    /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is out of range.</exception>
    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
    public void ReplaceRange(int index, int count, IEnumerable<T> collection)
    {
      ReplaceRange(index, count, collection, EqualityComparer<T>.Default);
    }

    /// <summary>
    /// Removes the specified range and inserts the specified collection in its position, leaving equal items in equal positions intact.
    /// </summary>
    /// <param name="index">The index of where to start the replacement.</param>
    /// <param name="count">The number of items to be replaced.</param>
    /// <param name="collection">The collection to insert in that location.</param>
    /// <param name="comparer">The comparer to use when checking for equal items.</param>
    /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
    /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is out of range.</exception>
    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
    /// <exception cref="ArgumentNullException"><paramref name="comparer"/> is null.</exception>
    public void ReplaceRange(int index, int count, IEnumerable<T> collection, IEqualityComparer<T> 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<T> 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<T> list))
        list = new List<T>(collection);

      using (BlockReentrancy())
      using (DeferEvents())
      {
        var rangeCount = index + count;
        var addedCount = list.Count;

        var changesMade = false;
        List<T>
            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<T> { @new };
              oldCluster = new List<T> { old };
            }
            else
            {
              newCluster.Add(@new);
              oldCluster.Add(old);
            }

            changesMade = true;
          }
        }

        OnRangeReplaced(i, newCluster, oldCluster);

        //exceeding position
        if (count != addedCount)
        {
          var items = (List<T>)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

    /// <summary>
    /// Called by base class Collection&lt;T&gt; when the list is being cleared;
    /// raises a CollectionChanged event to any listeners.
    /// </summary>
    protected override void ClearItems()
    {
      if (Count == 0)
        return;

      CheckReentrancy();
      base.ClearItems();
      OnEssentialPropertiesChanged();
      OnCollectionReset();
    }

    /// <summary>
    /// Called by base class Collection&lt;T&gt; when an item is set in list;
    /// raises a CollectionChanged event to any listeners.
    /// </summary>
    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);
    }

    /// <summary>
    /// Raise CollectionChanged event to any listeners.
    /// Properties/methods modifying this ObservableCollection will raise
    /// a collection changed event through this virtual method.
    /// </summary>
    /// <remarks>
    /// When overriding this method, either call its base implementation
    /// or call <see cref="BlockReentrancy"/> to guard against reentrant collection changes.
    /// </remarks>
    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

    /// <summary>
    /// Helper function to determine if a collection contains any elements.
    /// </summary>
    /// <param name="collection">The collection to evaluate.</param>
    /// <returns></returns>
    private static bool ContainsAny(IEnumerable<T> collection)
    {
      using (IEnumerator<T> enumerator = collection.GetEnumerator())
        return enumerator.MoveNext();
    }

    /// <summary>
    /// Helper to raise Count property and the Indexer property.
    /// </summary>
    private void OnEssentialPropertiesChanged()
    {
      OnPropertyChanged(EventArgsCache.CountPropertyChanged);
      OnIndexerPropertyChanged();
    }

    /// <summary>
    /// /// Helper to raise a PropertyChanged event for the Indexer property
    /// /// </summary>
    private void OnIndexerPropertyChanged() =>
      OnPropertyChanged(EventArgsCache.IndexerPropertyChanged);

    /// <summary>
    /// Helper to raise CollectionChanged event to any listeners
    /// </summary>
    private void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index) =>
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));

    /// <summary>
    /// Helper to raise CollectionChanged event with action == Reset to any listeners
    /// </summary>
    private void OnCollectionReset() =>
      OnCollectionChanged(EventArgsCache.ResetCollectionChanged);

    /// <summary>
    /// Helper to raise event for clustered action and clear cluster.
    /// </summary>
    /// <param name="followingItemIndex">The index of the item following the replacement block.</param>
    /// <param name="newCluster"></param>
    /// <param name="oldCluster"></param>
    //TODO should have really been a local method inside ReplaceRange(int index, int count, IEnumerable<T> collection, IEqualityComparer<T> comparer),
    //move when supported language version updated.
    private void OnRangeReplaced(int followingItemIndex, ICollection<T> newCluster, ICollection<T> oldCluster)
    {
      if (oldCluster == null || oldCluster.Count == 0)
      {
        Debug.Assert(newCluster == null || newCluster.Count == 0);
        return;
      }

      OnCollectionChanged(
          new NotifyCollectionChangedEventArgs(
              NotifyCollectionChangedAction.Replace,
              new List<T>(newCluster),
              new List<T>(oldCluster),
              followingItemIndex - oldCluster.Count));

      oldCluster.Clear();
      newCluster.Clear();
    }

    #endregion Private Methods

    //------------------------------------------------------
    //
    //  Private Types
    //
    //------------------------------------------------------

    #region Private Types
    private sealed class DeferredEventsCollection : List<NotifyCollectionChangedEventArgs>, IDisposable
    {
      private readonly ObservableRangeCollection<T> _collection;
      public DeferredEventsCollection(ObservableRangeCollection<T> 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

  }

  /// <remarks>
  /// To be kept outside <see cref="ObservableCollection{T}"/>, since otherwise, a new instance will be created for each generic type used.
  /// </remarks>
  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);
  }
}

2
你能帮我看一下这段代码吗?也许我使用的框架版本不对,但是比如在第48行的AddRange方法中,有一个“if(collection is ICollection<T> list)”的语句 - 但是这个“list”对象是什么? - Bartek Boro
3
C#7 模式匹配。 - Shimmy Weitzhandler
1
能否在WPF应用程序中使用它?我正在尝试这样做,但是出现了“不支持范围操作”异常。 - lucas.mdo
1
System.NotSupportedException: 不支持范围操作。dot net 4.5 vs 2017 - Henka Programmer
2
当使用AddRange方法时,遇到异常“添加的项未出现在给定索引'0'”的人们可能会遇到以下问题:该问题的可能原因是传递了一个带有Linq Where()子句的IEnumerable,导致过滤掉了一些项目。这似乎会影响插入操作。在将项目集合传递给AddRange方法之前,请使用ToList、ToArray等方法。当我清除我的集合并添加一系列新对象时,我也遇到了这个问题。 - Bjorn De Rijcke
显示剩余8条评论

33

我认为AddRange最好实现方式如下:

public void AddRange(IEnumerable<T> collection)
{
    foreach (var i in collection) Items.Add(i);
    OnCollectionChanged(
        new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}

这样可以避免复制列表。另外,如果你想要微调性能,你可以将添加操作限制在前 N 个元素上,如果超过 N 个元素被添加,则重置列表。


11
http://blogs.msdn.com/nathannesbit/archive/2009/04/20/addrange-and-observablecollection.aspx - Sam Saffron
2
你在ObservableCollection中保留了一个列表副本,但失去了对消费者的细粒度区分能力。如果你自己使用ObervableCollection,你可能能够更好地优化,因为你可以区分“添加了许多项”和重置(通常会强制你从头开始)。 - NobodysNightmare
@SamSaffron,链接已失效。 - AgentFire
3
SamSaffron提到的链接现在是https://learn.microsoft.com/en-us/archive/blogs/nathannesbit/addrange-and-observablecollection。 - habakuk

14
你需要小心地将 UI 绑定到你的自定义集合上——默认的 CollectionView 类只支持一次性通知项目。

13

需要使用OnPropertyChanged("Count")OnPropertyChanged("Item[]")来满足ObservableCollection的行为要求。请注意,如果您不关心这些,后果是什么我不知道!

下面是一个测试方法,它显示在普通可观察集合中每个添加操作会触发两个属性更改事件,一个是"Count",另一个是"Item[]"

[TestMethod]
public void TestAddSinglesInOldObsevableCollection()
{
  int colChangedEvents = 0;
  int propChangedEvents = 0;
  var collection = new ObservableCollection<object>();
  collection.CollectionChanged += (sender, e) => { colChangedEvents++; };
  (collection as INotifyPropertyChanged).PropertyChanged += (sender, e) => { propChangedEvents++; };
  collection.Add(new object());
  collection.Add(new object());
  collection.Add(new object());
  Assert.AreEqual(3, colChangedEvents);
  Assert.AreEqual(6, propChangedEvents);
}

@Shimmy,将标准的集合替换为你的集合,并更改为add range,你将得到零个PropertyChanges。请注意,集合更改确实有效,但不完全像ObservableCollection那样。因此,shimmy集合的测试如下:

[TestMethod]
public void TestShimmyAddRange()
{
  int colChangedEvents = 0;
  int propChangedEvents = 0;
  var collection = new ShimmyObservableCollection<object>();
  collection.CollectionChanged += (sender, e) => { colChangedEvents++; };
  (collection as INotifyPropertyChanged).PropertyChanged += (sender, e) => { propChangedEvents++; };
  collection.AddRange(new[]{
    new object(), new object(), new object(), new object()}); //4 objects at once
  Assert.AreEqual(1, colChangedEvents);  //great, just one!
  Assert.AreEqual(2, propChangedEvents); //fails, no events :(
}

以下是ObservableCollection中InsertItem方法的代码(也被Add方法调用):

protected override void InsertItem(int index, T item)
{
  base.CheckReentrancy();
  base.InsertItem(index, item);
  base.OnPropertyChanged("Count");
  base.OnPropertyChanged("Item[]");
  base.OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index);
}

3
C#总结后代。
更多阅读:http://blogs.msdn.com/b/nathannesbit/archive/2009/04/20/addrange-and-observablecollection.aspx
public sealed class ObservableCollectionEx<T> : ObservableCollection<T>
{
    #region Ctor

    public ObservableCollectionEx()
    {
    }

    public ObservableCollectionEx(List<T> list) : base(list)
    {
    }

    public ObservableCollectionEx(IEnumerable<T> collection) : base(collection)
    {
    }

    #endregion

    /// <summary> 
    /// Adds the elements of the specified collection to the end of the ObservableCollection(Of T). 
    /// </summary> 
    public void AddRange(
        IEnumerable<T> itemsToAdd,
        ECollectionChangeNotificationMode notificationMode = ECollectionChangeNotificationMode.Add)
    {
        if (itemsToAdd == null)
        {
            throw new ArgumentNullException("itemsToAdd");
        }
        CheckReentrancy();

        if (notificationMode == ECollectionChangeNotificationMode.Reset)
        {
            foreach (var i in itemsToAdd)
            {
                Items.Add(i);
            }

            OnPropertyChanged(new PropertyChangedEventArgs("Count"));
            OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

            return;
        }

        int startIndex = Count;
        var changedItems = itemsToAdd is List<T> ? (List<T>) itemsToAdd : new List<T>(itemsToAdd);
        foreach (var i in changedItems)
        {
            Items.Add(i);
        }

        OnPropertyChanged(new PropertyChangedEventArgs("Count"));
        OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, changedItems, startIndex));
    }

    public enum ECollectionChangeNotificationMode
    {
        /// <summary>
        /// Notifies that only a portion of data was changed and supplies the changed items (not supported by some elements,
        /// like CollectionView class).
        /// </summary>
        Add,

        /// <summary>
        /// Notifies that the entire collection was changed, does not supply the changed items (may be inneficient with large
        /// collections as requires the full update even if a small portion of items was added).
        /// </summary>
        Reset
    }
}

1
正如您链接的博客所提到的,ListCollectionView(以及可能还有其他类)不支持多项更改事件。在这里,您必须使用Reset操作而不是Add。 - Søren Boisen

3

由于在ObservableCollection上可能需要执行多个操作,例如首先清除Clear,然后AddRange,最后为ComboBox插入"All"项,因此我得出了以下解决方案:

public static class LinqExtensions
{
    public static ICollection<T> AddRange<T>(this ICollection<T> source, IEnumerable<T> addSource)
    {
        foreach(T item in addSource)
        {
            source.Add(item);
        }

        return source;
    }
}

public class ExtendedObservableCollection<T>: ObservableCollection<T>
{
    public void Execute(Action<IList<T>> itemsAction)
    {
        itemsAction(Items);
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
}

一个如何使用它的示例:

MyDogs.Execute(items =>
{
    items.Clear();
    items.AddRange(Context.Dogs);
    items.Insert(0, new Dog { Id = 0, Name = "All Dogs" });
});

重置通知仅在执行完基础列表的处理后被调用一次。

2
这是一个简单的ObservableCollection,它在AddRange方法结束时发出通知,基于这篇文章:https://peteohanlon.wordpress.com/2008/10/22/bulk-loading-in-observablecollection/
它也是异步的,并且可以跨线程修改,基于这篇文章:https://thomaslevesque.com/2009/04/17/wpf-binding-to-an-asynchronous-collection/
public class ConcurrentObservableCollection<T> : ObservableCollection<T>
{
    private SynchronizationContext _synchronizationContext = SynchronizationContext.Current;

    private bool _suppressNotification = false;

    public ConcurrentObservableCollection()
        : base()
    {
    }
    public ConcurrentObservableCollection(IEnumerable<T> list)
        : base(list)
    {
    }

    public void AddRange(IEnumerable<T> collection)
    {
        if (collection != null)
        {
            _suppressNotification = true;
            foreach (var item in collection)
            {
                this.Add(item);
            }
            _suppressNotification = false;

            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
    }
    public void RemoveRange(IEnumerable<T> collection)
    {
        if (collection != null)
        {
            _suppressNotification = true;
            foreach (var item in collection)
            {
                this.Remove(item);
            }
            _suppressNotification = false;

            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (SynchronizationContext.Current == _synchronizationContext)
        {
            // Execute the CollectionChanged event on the current thread
            RaiseCollectionChanged(e);
        }
        else
        {
            // Raises the CollectionChanged event on the creator thread
            _synchronizationContext.Send(RaiseCollectionChanged, e);
        }
    }
    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (SynchronizationContext.Current == _synchronizationContext)
        {
            // Execute the PropertyChanged event on the current thread
            RaisePropertyChanged(e);
        }
        else
        {
            // Raises the PropertyChanged event on the creator thread
            _synchronizationContext.Send(RaisePropertyChanged, e);
        }
    }

    private void RaiseCollectionChanged(object param)
    {
        // We are in the creator thread, call the base implementation directly
        if (!_suppressNotification)
            base.OnCollectionChanged((NotifyCollectionChangedEventArgs)param);
    }
    private void RaisePropertyChanged(object param)
    {
        // We are in the creator thread, call the base implementation directly
        base.OnPropertyChanged((PropertyChangedEventArgs)param);
    }
}

2

是的,添加自定义Observable Collection就足够了。不要忘记引发适当的事件,无论它当前是否被UI使用;) 您必须为“Item[]”属性(WPF侧和绑定控件所需)引发属性更改通知,以及使用一组添加的项(您的范围)引发NotifyCollectionChangedEventArgs。 我也做过这样的事情(以及排序支持和其他一些东西),在Presentation和Code Behind层都没有问题。


1
ObservableRangeCollection应该通过类似以下测试的测试:
[Test]
public void TestAddRangeWhileBoundToListCollectionView()
{
    int collectionChangedEventsCounter = 0;
    int propertyChangedEventsCounter = 0;
    var collection = new ObservableRangeCollection<object>();

    collection.CollectionChanged += (sender, e) => { collectionChangedEventsCounter++; };
    (collection as INotifyPropertyChanged).PropertyChanged += (sender, e) => { propertyChangedEventsCounter++; };

    var list = new ListCollectionView(collection);

    collection.AddRange(new[] { new object(), new object(), new object(), new object() });

    Assert.AreEqual(4, collection.Count);
    Assert.AreEqual(1, collectionChangedEventsCounter);
    Assert.AreEqual(2, propertyChangedEventsCounter);
}

否则我们会得到


System.NotSupportedException : Range actions are not supported.

在使用控件时,我并没有看到一个理想的解决方案,但是使用NotifyCollectionChangedAction.Reset代替Add/Remove可以部分地解决问题。请参见http://blogs.msdn.com/b/nathannesbit/archive/2009/04/20/addrange-and-observablecollection.aspx,正如net_prog所提到的那样。

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