如何避免在替换所有元素或添加一组元素时,ObservableCollection.CollectionChanged被多次触发?

55

我有一个 ObservableCollection<T> 集合,我想用一组新元素替换其中的所有元素,可以这样做:

collection.Clear(); 

或者:

collection.ClearItems();

顺便问一下,这两种方法有什么区别吗?

我还可以使用foreach逐个添加到集合中collection.Add,但这将会多次触发。

添加一个元素集合时也是如此。

编辑:

我在这里找到了一个不错的库:增强的ObservableCollection支持延迟或禁用通知,但它似乎不支持Silverlight。


如果您想使用其他集合替换第一个集合中的所有项,不能直接将新集合分配给旧集合吗? - m4ngl3r
3
如果这样做,集合本身将被更改,这样你将失去对集合的引用,也就是说,你的CollectionChanged事件将消失。 - Peter Lee
1
重新分配该事件处理程序? - m4ngl3r
1
重新分配事件处理程序?那么CollectionChanged的目的是什么呢? :-) - Peter Lee
5个回答

84

ColinE所提供的所有信息都是正确的。我只想补充一下,这是我在这个特定情况下使用的ObservableCollection子类。

public class SmartCollection<T> : ObservableCollection<T> {
    public SmartCollection()
        : base() {
    }

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

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

    public void AddRange(IEnumerable<T> range) {
        foreach (var item in range) {
            Items.Add(item);
        }

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

    public void Reset(IEnumerable<T> range) {
        this.Items.Clear();

        AddRange(range);
    }
}

18
记得为 Count 和 Items[] 提高 PropertyChanged,这个要点加一分 - 我将不得不更新我在自己的项目中使用的代码 ;-) - ColinE
1
@AdrianRatnapala 它是免费使用的。尽情享用吧。另请参见这个这个 - Jehof
1
为什么您需要分别对Count和Items[]进行提高?观察者在重置时不会检查吗? - Aaron
2
@Aaron,你需要同时提高CountItem[]的属性更改。一些观察者可能会检查这些属性。看看ObservableCollection的实现,你会发现所有操作集合的方法都会为这些属性引发事件。 - Jehof
@Mark13426,“this.Items.Xxxx()”没有触发通知,该属性仅可从“SmartCollection”(或其“ObservableCollection”的子类)访问。如果您使用手动调用的“Clear()”和“Add”,则会收到2次通知。 - hidekuro
显示剩余8条评论

11
您可以通过继承ObservableCollection并实现自己的ReplaceAll方法来实现此目标。该方法的实现将替换内部Items属性中的所有项目,然后触发一个CollectionChanged事件。同样,您还可以添加一个AddRange方法。有关此实现,请参见以下问题的答案:ObservableCollection不支持AddRange方法,所以我会收到每个已添加的项的通知,另外INotifyCollectionChanging怎么处理? Collection.ClearCollection.ClearItems之间的区别在于Clear是公共API方法,而ClearItems是受保护的,它是一个扩展点,允许您扩展/修改Clear的行为。

7

以下是我为其他人实现的参考代码:

// https://dev59.com/s2Yr5IYBdhLWcg3w6eI3
// https://dev59.com/tHRB5IYBdhLWcg3wSVcI
public class ObservableCollectionFast<T> : ObservableCollection<T>
{
    public ObservableCollectionFast()
        : base()
    {

    }

    public ObservableCollectionFast(IEnumerable<T> collection)
        : base(collection)
    {

    }

    public ObservableCollectionFast(List<T> list)
        : base(list)
    {

    }

    public virtual void AddRange(IEnumerable<T> collection)
    {
        if (collection.IsNullOrEmpty())
            return;

        foreach (T item in collection)
        {
            this.Items.Add(item);
        }

        this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
        this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        // Cannot use NotifyCollectionChangedAction.Add, because Constructor supports only the 'Reset' action.
    }

    public virtual void RemoveRange(IEnumerable<T> collection)
    {
        if (collection.IsNullOrEmpty())
            return;

        bool removed = false;
        foreach (T item in collection)
        {
            if (this.Items.Remove(item))
                removed = true;
        }

        if (removed)
        {
            this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
            this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
            this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
            // Cannot use NotifyCollectionChangedAction.Remove, because Constructor supports only the 'Reset' action.
        }
    }

    public virtual void Reset(T item)
    {
        this.Reset(new List<T>() { item });
    }

    public virtual void Reset(IEnumerable<T> collection)
    {
        if (collection.IsNullOrEmpty() && this.Items.IsNullOrEmpty())
            return;

        // Step 0: Check if collection is exactly same as this.Items
        if (IEnumerableUtils.Equals<T>(collection, this.Items))
            return;

        int count = this.Count;

        // Step 1: Clear the old items
        this.Items.Clear();

        // Step 2: Add new items
        if (!collection.IsNullOrEmpty())
        {
            foreach (T item in collection)
            {
                this.Items.Add(item);
            }
        }

        // Step 3: Don't forget the event
        if (this.Count != count)
            this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
        this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
}

我知道这很老了,但我刚找到它并且无法编译。它缺少对IEnumerableUtils的任何引用。 - Tom Padilla

2

我暂时无法评论之前的答案,所以在此添加一个RemoveRange适配器,它是上面SmartCollection实现的改编版本,不会抛出C# InvalidOperationException:集合已修改。它使用谓词来检查是否应该删除该项,在我的情况下,这比创建符合删除条件的子集更加优化。

public void RemoveRange(Predicate<T> remove)
{
    // iterates backwards so can remove multiple items without invalidating indexes
    for (var i = Items.Count-1; i > -1; i--) {
        if (remove(Items[i]))
            Items.RemoveAt(i);
    }

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

例子:

LogEntries.RemoveRange(i => closeFileIndexes.Contains(i.fileIndex));

1

在过去的几年中,我使用了一种更通用的解决方案来消除过多的ObservableCollection通知,即创建批处理更改操作并使用Reset动作通知观察者:

public class ExtendedObservableCollection<T>: ObservableCollection<T>
{
    public ExtendedObservableCollection()
    {
    }

    public ExtendedObservableCollection(IEnumerable<T> items)
        : base(items)
    {
    }

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

使用它很简单:

直接使用即可:

var collection = new ExtendedObservableCollection<string>(new[]
{
    "Test",
    "Items",
    "Here"
});
collection.Execute(items => {
    items.RemoveAt(1);
    items.Insert(1, "Elements");
    items.Add("and there");
});

调用Execute将生成一个通知,但有一个缺点 - 列表将作为整体在UI中更新,而不仅仅是修改的元素。这使其非常适合于items.Clear()后跟着items.AddRange(newItems)。

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