【问题标题】:ObservableCollection : calling OnCollectionChanged with multiple new itemsObservableCollection : 使用多个新项目调用 OnCollectionChanged
【发布时间】:2010-07-21 15:15:23
【问题描述】:

请注意,我正在尝试使用 NotifyCollectionChangedAction.Add 操作而不是 .Reset。后者确实有效,但对于大型集合来说效率不高。

所以我继承了 ObservableCollection:

public class SuspendableObservableCollection<T> : ObservableCollection<T>

由于某种原因,这段代码:

private List<T> _cachedItems;
...

    public void FlushCache() {
        if (_cachedItems.Count > 0) {

        foreach (var item in _cachedItems)
            Items.Add(item);

        OnCollectionChanged(new NotifyCollectionChangedEventArgs(
            NotifyCollectionChangedAction.Add, (IList<T>)_cachedItems));
        }
    }

正在投掷 集合添加事件指的是不属于集合的项目

这似乎是 BCL 中的错误?

我可以在调用 OnCollectionChanged 之前逐步查看新项目已添加到 this.Items

刚刚有了一个惊人的发现。这些方法都不适合我(flush、addrange),因为只有当这个集合绑定到我的 Listview 时才会触发错误!

TestObservableCollection<Trade> testCollection = new TestObservableCollection<Trade>();
List<Trade> testTrades = new List<Trade>();

for (int i = 0; i < 200000; i++) 
    testTrades.Add(t);

testCollection.AddRange(testTrades); // no problems here.. 
_trades.AddRange(testTrades); // this one is bound to ListView .. BOOOM!!!

总之,ObservableCollection 确实支持添加增量列表,但 ListView 不支持。 Andyp 想出了一个解决方法,让它与下面的 CollectionView 一起工作,但是由于调用了 .Refresh(),这与调用 OnCollectionChanged( .Reset ) 没有什么不同..

【问题讨论】:

  • 为什么 RemoveRange、AddRange 会触发重置?也许有人不明白删除、添加和重置含义之间的区别?

标签: c# generics c#-4.0 subclass observablecollection


【解决方案1】:

您可以像这样为 ObservableCollection 实现 AddRange(),如 here 所示:

public class RangeObservableCollection<T> : ObservableCollection<T>
{
    private bool _SuppressNotification;

    public override event NotifyCollectionChangedEventHandler CollectionChanged;

    protected virtual void OnCollectionChangedMultiItem(
        NotifyCollectionChangedEventArgs e)
    {
        NotifyCollectionChangedEventHandler handlers = this.CollectionChanged;
        if (handlers != null)
        {
            foreach (NotifyCollectionChangedEventHandler handler in 
                handlers.GetInvocationList())
            {
                if (handler.Target is CollectionView)
                    ((CollectionView)handler.Target).Refresh();
                else
                    handler(this, e);
            }
        }
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (!_SuppressNotification)
        {
            base.OnCollectionChanged(e);
            if (CollectionChanged != null)
                CollectionChanged.Invoke(this, e);
        }
    }

    public void AddRange(IEnumerable<T> list)
    {
        if (list == null)
            throw new ArgumentNullException("list");

        _SuppressNotification = true;

        foreach (T item in list)
        {
            Add(item);
        }
        _SuppressNotification = false;

        OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list));
    }
}

更新:绑定到 ListBox 后,我也看到了 InvalidOperationException(与您看到的消息相同)。根据article,这是因为 CollectionView 不支持范围操作。幸运的是,这篇文章也提供了一个解决方案(虽然感觉有点“hack-ish”)。

更新 2:添加了一个修复程序,该修复程序在 OnCollectionChanged() 的重写实现中引发了重写的 CollectionChanged 事件。

【讨论】:

  • 谢谢,但我正在尝试改变 .Reset 操作。这里的重点是我只想添加新项目。如果我的集合达到大尺寸,.reset 非常慢,因为我也在过滤它
  • 啊,我错过了 - 更新了我的代码以使用 NotifyCollectionChangedAction.Add 而不是 Reset。
  • 添加了解决(避免)CollectionView 的范围操作问题的链接和代码。
  • 嗯...我按原样插入了您的收藏,并且 AddRange 可以正常工作而不会抛出! :) .. 但是由于某些神秘的原因,没有从常规 .Add(foo) 中添加项目.. 很可能问题出在我这边.. 正在调查..
  • 不,抱歉,不是你。我的实现没有在重写的 OnCollectionChange 方法中引发重写的 CollectionChange 事件。修复了这个问题,现在 Add() 也可以工作了。
【解决方案2】:

感谢 AndyP 的灵感。我在您的实现中遇到了一些问题,例如在测试中使用 CollectionView 而不是 ICollectionView,以及在元素上手动调用“重置”。从 CollectionView 继承的元素实际上可能以比调用“this.Reset()”更多的方式处理这些参数,因此最好仍然触发它们的处理程序,只使用它们需要的 Action=Reset 参数,而不是改进的事件参数包括更改的项目列表。下面是我的(非常相似的)实现。

public class BaseObservableCollection<T> : ObservableCollection<T>
{
    //Flag used to prevent OnCollectionChanged from firing during a bulk operation like Add(IEnumerable<T>) and Clear()
    private bool _SuppressCollectionChanged = false;

    /// Overridden so that we may manually call registered handlers and differentiate between those that do and don't require Action.Reset args.
    public override event NotifyCollectionChangedEventHandler CollectionChanged;

    public BaseObservableCollection() : base(){}
    public BaseObservableCollection(IEnumerable<T> data) : base(data){}

    #region Event Handlers
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if( !_SuppressCollectionChanged )
        {
            base.OnCollectionChanged(e);
            if( CollectionChanged != null )
                CollectionChanged.Invoke(this, e);
        }
    }

    //CollectionViews raise an error when they are passed a NotifyCollectionChangedEventArgs that indicates more than
    //one element has been added or removed. They prefer to receive a "Action=Reset" notification, but this is not suitable
    //for applications in code, so we actually check the type we're notifying on and pass a customized event args.
    protected virtual void OnCollectionChangedMultiItem(NotifyCollectionChangedEventArgs e)
    {
        NotifyCollectionChangedEventHandler handlers = this.CollectionChanged;
        if( handlers != null )
            foreach( NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList() )
                handler(this, !(handler.Target is ICollectionView) ? e : new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
    #endregion

    #region Extended Collection Methods
    protected override void ClearItems()
    {
        if( this.Count == 0 ) return;

        List<T> removed = new List<T>(this);
        _SuppressCollectionChanged = true;
        base.ClearItems();
        _SuppressCollectionChanged = false;
        OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
    }

    public void Add(IEnumerable<T> toAdd)
    {
        if( this == toAdd )
            throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");

        _SuppressCollectionChanged = true;
        foreach( T item in toAdd )
            Add(item);
        _SuppressCollectionChanged = false;
        OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(toAdd)));
    }

    public void Remove(IEnumerable<T> toRemove)
    {
        if( this == toRemove )
            throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");

        _SuppressCollectionChanged = true;
        foreach( T item in toRemove )
            Remove(item);
        _SuppressCollectionChanged = false;
        OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(toRemove)));
    }
    #endregion
}

【讨论】:

  • 如果在可移植项目中定义了 BaseObservableCollection,您将如何实现这一点?我相信 ICollectionView 是特定于 Windows 的,因此无法使用。
  • @user2481095 ,将 ICollectionView 的检查(以及可能的操作(如果使用 ICollectionView 进行任何操作,例如上面调用 Refresh()))放入单独的虚拟方法中。在您的可移植基础项目中,虚拟方法不会引用 ICollectionView 而只是恢复为默认行为。然后在 Windows 库中,重写类和检查方法以对 ICollectionView 进行实际检查。
【解决方案3】:

经过多次迭代,我们最终得到了这个版本的 ObservableRangeCollectionReadOnlyObservableRangeCollection,它基于已接受答案中的代码,并且我们在过去 6 个月内无需修改:

public class ObservableRangeCollection<T> : ObservableCollection<T>
{
    private bool suppressNotification;

    public ObservableRangeCollection() { }

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

    public override event NotifyCollectionChangedEventHandler CollectionChanged;

    protected virtual void OnCollectionChangedMultiItem(
        NotifyCollectionChangedEventArgs e)
    {
        var handlers = CollectionChanged;
        if (handlers == null) return;

        foreach (NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList())
        {
            if (handler.Target is ReadOnlyObservableCollection<T>
                && !(handler.Target is ReadOnlyObservableRangeCollection<T>))
            {
                throw new NotSupportedException(
                    "ObservableRangeCollection is wrapped in ReadOnlyObservableCollection which might be bound to ItemsControl " +
                    "which is internally using ListCollectionView which does not support range actions.\n" +
                    "Instead of ReadOnlyObservableCollection, use ReadOnlyObservableRangeCollection");
            }
            var collectionView = handler.Target as ICollectionView;
            if (collectionView != null)
            {
                collectionView.Refresh();
            }
            else
            {
                handler(this, e);
            }
        }
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (suppressNotification) return;

        base.OnCollectionChanged(e);
        if (CollectionChanged != null)
        {
            CollectionChanged.Invoke(this, e);
        }
    }

    public void AddRange(IEnumerable<T> items)
    {
        if (items == null) return;

        suppressNotification = true;

        var itemList = items.ToList();

        foreach (var item in itemList)
        {
            Add(item);
        }
        suppressNotification = false;

        if (itemList.Any())
        {
            OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, itemList));
        }
    }

    public void AddRange(params T[] items)
    {
        AddRange((IEnumerable<T>)items);
    }

    public void ReplaceWithRange(IEnumerable<T> items)
    {
        Items.Clear();
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        AddRange(items);
    }

    public void RemoveRange(IEnumerable<T> items)
    {
        suppressNotification = true;

        var removableItems = items.Where(x => Items.Contains(x)).ToList();

        foreach (var item in removableItems)
        {
            Remove(item);
        }

        suppressNotification = false;

        if (removableItems.Any())
        {
            OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removableItems));
        }
    }
}

public class ReadOnlyObservableRangeCollection<T> : ReadOnlyObservableCollection<T>
{
    public ReadOnlyObservableRangeCollection(ObservableCollection<T> list)
        : base(list)
    {            
    }

    protected override event NotifyCollectionChangedEventHandler CollectionChanged;

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        var handlers = CollectionChanged;
        if (handlers == null) return;

        foreach (NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList())
        {
            var collectionView = handler.Target as ICollectionView;
            if (collectionView != null)
            {
                collectionView.Refresh();
            }
            else
            {
                handler(this, e);
            }
        }
    }
}

我们基本上用ObservableRangeCollection替换了我们应用程序中所有ObservableCollection的用法,它就像一个魅力。

【讨论】:

  • 为什么ReplaceWithRange 中的通知在Add 之后?
  • 您只想在添加完所有项目后通知一次。
【解决方案4】:

我相信您需要将其转换为IList

base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)_cachedItems));

【讨论】:

  • 谢谢,现在我回到“集合添加事件引用不属于集合的项目”
  • hmmm,用Items.Add(item)base.Add(item)代替怎么样?
  • George,尝试了 Add(item) 和 base.Add(item),仍然抱怨它不是集合的一部分...
  • 我实际上的意思是this.Add(item),对此感到抱歉...我在实现ObservableCollection&lt;T&gt; 之前确实遇到了这个错误,但我现在无法访问代码我会明天继续。
  • 这不是。添加与添加相同吗?无论如何,我只是为了确定而尝试过,但仍然是同样的错误..我认为这可能是一个错误:(
猜你喜欢
  • 2011-04-04
  • 2011-10-29
  • 1970-01-01
  • 2014-01-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-25
  • 1970-01-01
相关资源
最近更新 更多