优化 C# 代码片段、ObservableCollection 和 AddRange

Posted

技术标签:

【中文标题】优化 C# 代码片段、ObservableCollection 和 AddRange【英文标题】:Optimizing C# code fragment, ObservableCollection and AddRange 【发布时间】:2016-02-24 08:52:00 【问题描述】:

我正在分析其他人编写的 Silverlight 组件。 我发现了很多热点和瓶颈,现在我遇到了这个:

public static class CollectionExtensions

    public static void AddRange<T>(this ObservableCollection<T> collection, IEnumerable<T> items)
    
        foreach (var item in items)
        
            collection.Add(item);
        
    

这个扩展方法,当然是把 AddRange 方法添加到 ObservableCollection 中,但是计算量很大。 有没有人有更好的实现,或者关于如何提高这块 cos 的性能的任何建议?

谢谢

【问题讨论】:

既然你的代码有效,这个问题应该发到Code Review 【参考方案1】:

多次调用 Add 会导致多次引发 INotifyCollectionChanged,这通常会导致 UI 重绘自身。

虽然 Lee 的回答在技术上是正确的,即在添加所有项目后引发 Reset 事件是正确的方法,但我从经验中发现许多网格控件(例如)并不积极支持 Reset 事件。

最普遍支持的选项是从ObservableCollection 修改集合并重新创建ObservableCollection 属性本身。

换句话说,您的ObservableCollection 在您的 VM 上定义如下...

private ObservableCollection<MyItem> _items;
public ObservableCollection<MyItem> Items 
    get  return _items;
    set 
     
        _items = value;
        OnPropertyChanged(()=> Items);
    

...按如下方式添加您的新项目...

var tempColl = _items.ToList();
tempColl.AddRange(newItems);
Items = new ObservableCollection(tempColl);

关于此技术要记住的另一件事是它是线程安全的,因为如果您重新创建ObservableCollection,您可以从后台线程将项目添加到ObservableCollection。普通的 ObservableCollection 不能通过非 Dispatcher 线程的 Add 方法添加项目。

【讨论】:

好建议,但我担心这种“补偿行为”只会鼓励可用的低质量 WPF 控件。如果您使用的控件不能正确支持 WPF 绑定,请向控件的提供者提出错误,或者使用可以正常工作的东西。 #rantOver。我还要指出,通常要避免在集合上设置 setter - msdn.microsoft.com/en-us/library/dn169389(v=vs.110).aspx。在 ViewModel 中可能会很好,但对于下一个开发人员来说并不好。【参考方案2】:

这是由于 ObservableCollection 会在每次将项目添加到集合时触发 PropertyChanged 事件。在批量添加项目时防止触发此事件是您想要查看的内容。这是一个优雅的解决方案,虽然我自己没有尝试过。

https://peteohanlon.wordpress.com/2008/10/22/bulk-loading-in-observablecollection/

【讨论】:

我喜欢这个解决方案,它真的很优雅。【参考方案3】:

这里的成本通常是由于每次添加都会引发更改通知。最好的做法是创建一个新的集合实现,该实现针对接受数据范围进行了优化。您可以添加所有值,然后引发单个事件,而不是为每个更改引发更改通知,然后绑定引擎将每个更改处理为单个更新。此事件可以具有作为Reset大锤,或者您可以提供更改的项目,以及它们更改的索引。

这是一个在其AddRange 方法上使用单个Reset 通知的示例:

/// <summary>
/// An implementation of <seealso cref="ObservableCollectionT"/> that provides the ability to suppress
/// change notifications. In sub-classes that allows performing batch work and raising notifications 
/// on completion of work. Standard usage takes advantage of this feature by providing AddRange method.
/// </summary>
/// <typeparam name="T">The type of elements in the list.</typeparam>
public class ObservableList<T> : ObservableCollection<T>

    #region Fields
    private readonly Queue<PropertyChangedEventArgs> _notifications = new Queue<PropertyChangedEventArgs>();
    private readonly Queue<NotifyCollectionChangedEventArgs> _collectionNotifications = new Queue<NotifyCollectionChangedEventArgs>();
    private int _notificationSupressionDepth;
    #endregion

    public ObservableList()
    
    
    public ObservableList(IEnumerable<T> collection)
        : base(collection)
    
    

    public void AddRange(IEnumerable<T> list)
    
        using (SupressNotifications())
        
            foreach (var item in list)
            
                Add(item);
            
        
        OnPropertyChanged("Count");
        OnPropertyChanged("Item[]");
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    

    public void RemoveRange(IEnumerable<T> list)
    
        using (SupressNotifications())
        
            foreach (var item in list)
            
                Remove(item);
            
        
        OnPropertyChanged("Count");
        OnPropertyChanged("Item[]");
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    

    public void ReplaceRange(IEnumerable<T> list)
    
        using (SupressNotifications())
        
            Clear();
            foreach (var item in list)
            
                Add(item);
            
        
        OnPropertyChanged("Count");
        OnPropertyChanged("Item[]");
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    


    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    
        if (_notificationSupressionDepth == 0)
        
            base.OnCollectionChanged(e);
        
        else
        
            //We cant filter duplicate Collection change events as this will break how UI controls work. -LC
            _collectionNotifications.Enqueue(e);
        
    

    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
    
        if (_notificationSupressionDepth == 0)
        
            base.OnPropertyChanged(e);
        
        else
        
            if (!_notifications.Contains(e, NotifyEventComparer.Instance))
            
                _notifications.Enqueue(e);
            
        
    

    protected void OnPropertyChanged(string propertyName)
    
        OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    

    protected IDisposable QueueNotifications()
    
        _notificationSupressionDepth++;
        return Disposable.Create(() =>
                                     
                                         _notificationSupressionDepth--;
                                         TryNotify();
                                     );
    

    protected IDisposable SupressNotifications()
    
        _notificationSupressionDepth++;
        return Disposable.Create(() =>
        
            _notificationSupressionDepth--;
        );
    

    private void TryNotify()
    
        if (_notificationSupressionDepth == 0)
        
            while (_collectionNotifications.Count > 0)
            
                var collectionNotification = _collectionNotifications.Dequeue();
                base.OnCollectionChanged(collectionNotification);
            

            while (_notifications.Count > 0)
            
                var notification = _notifications.Dequeue();
                base.OnPropertyChanged(notification);
            
        
    

编辑:添加缺少的 NotifyEventComparer 类和示例 Disposable.Create 方法

public sealed class NotifyEventComparer : IEqualityComparer<PropertyChangedEventArgs>

    public static readonly NotifyEventComparer Instance = new NotifyEventComparer();

    bool IEqualityComparer<PropertyChangedEventArgs>.Equals(PropertyChangedEventArgs x, PropertyChangedEventArgs y)
    
        return x.PropertyName == y.PropertyName;
    

    int IEqualityComparer<PropertyChangedEventArgs>.GetHashCode(PropertyChangedEventArgs obj)
    
        return obj.PropertyName.GetHashCode();
    


//Either use Rx to access Disposable.Create or this simple implementation will do
public static class Disposable

    public static IDisposable Create(Action dispose)
    
        if (dispose == null)
            throw new ArgumentNullException("dispose");

        return new AnonymousDisposable(dispose);
    

    private sealed class AnonymousDisposable : IDisposable
    
        private Action _dispose;

        public AnonymousDisposable(Action dispose)
        
            _dispose = dispose;
        

        public void Dispose()
        
            var dispose = Interlocked.Exchange(ref _dispose, null);
            if (dispose != null)
            
                dispose();
            
        
    

【讨论】:

谢谢李,无论如何我很难弄清楚什么是 NotifyEventComparer 道歉。更新为完整且独立。【参考方案4】:

您可以在此处查看 AddRange 方法的实现(对于 List): http://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs

【讨论】:

嗨,Oleksandr,我已经看到了 list.cs 的实现,但是它需要太多的私有字段和太多的代码:D【参考方案5】:

此线程中已经有一个公认的答案,但对于所有正在寻找支持 AddRangeReplaceRangeObservableRangeCollection 良好实现的人使用单个 CollectionChanged 通知,我真的会推荐 this piece of code written by James Montemagno。

【讨论】:

以上是关于优化 C# 代码片段、ObservableCollection 和 AddRange的主要内容,如果未能解决你的问题,请参考以下文章

c#代码片段快速构建代码

此 Canon SDK C++ 代码片段的等效 C# 代码是啥?

是否可以动态编译和执行 C# 代码片段?

C#常用代码片段备忘

VS2015使用技巧 打开代码片段C#部分

记录C#常用的代码片段