我可以帮您翻译成中文:我是否可以暂时禁用WPF数据绑定的更改?

19
我有一个使用MVVM数据绑定的WPF应用程序。我正在向ObservableCollection<...>添加项目,而且确实有很多项目。
现在我想知道,每次我向集合中添加一个项目时,它是否会立即触发事件并导致不必要的开销?如果是这样,我是否可以暂时禁用事件通知,并在我的代码结尾手动触发它,以便如果我添加了10k个项目,它只会被触发一次,而不是10k次?
更新:我尝试使用以下类:
using System;
using System.Linq;
using System.Collections.Specialized;
using System.Collections.Generic;

namespace MyProject
{

    /// <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 ObservableCollection<T> : System.Collections.ObjectModel.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)
        {
            foreach (var i in collection) Items.Add(i);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, collection.ToList()));
        }

        /// <summary> 
        /// Removes the first occurence of each item in the specified collection from ObservableCollection(Of T). 
        /// </summary> 
        public void RemoveRange(IEnumerable<T> collection)
        {
            foreach (var i in collection) Items.Remove(i);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, collection.ToList()));
        }

        /// <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)
        {
            List<T> old = new List<T>(Items);
            Items.Clear();
            foreach (var i in collection) Items.Add(i);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, collection.ToList()));
        }

        /// <summary> 
        /// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class. 
        /// </summary> 
        public ObservableCollection() : 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 ObservableCollection(IEnumerable<T> collection) : base(collection) { }
    }
}

我现在遇到这个错误:
附加信息:不支持范围操作。
错误出现在这里:
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, collection.ToList()));
5个回答

25

这个ObservableCollection的扩展很容易解决这个问题。

它公开了一个SupressNotification属性,允许用户控制何时抑制CollectionChanged通知。

虽然它不提供范围插入/删除,但如果抑制CollectionChanged通知,在大多数情况下减少了对集合进行范围操作的需求。

此实现将所有被抑制的通知替换为Reset通知。这在逻辑上是合理的。当用户抑制通知、进行大量更改、然后重新启用通知时,应该发送一个Reset通知。

public class ObservableCollectionEx<T> : ObservableCollection<T>
{
    private bool _notificationSupressed = false;
    private bool _supressNotification = false;
    public bool SupressNotification
    {
        get
        {
            return _supressNotification;
        }
        set
        {
            _supressNotification = value;
            if (_supressNotification == false && _notificationSupressed)
            {
                this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                _notificationSupressed = false;
            }
        }
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (SupressNotification)
        {
            _notificationSupressed = true;
            return;
        }
        base.OnCollectionChanged(e);
    }
}

1
不错的类,我喜欢你重新设置为true后触发事件的解决方案。 - Tobias
1
以防有人想知道,NotifyCollectionChangedAction.Reset 值的作用是什么,并提供了处理它的方法。https://dev59.com/hG855IYBdhLWcg3wKxDc - Tim Rutter
不错,实现干净利落!在 Visual Studio 2019 中进行了样本测试,使用 WPF 控件绑定到一个包含几十行的 ObservableCollection<T> 对象。原始的 ObservableCollection 每行更新 UI 需要约 90 微秒。而这里的可抑制版本每行只需要约 15 微秒。 - AlainD

19

一种非常快速且简便的方法是子类化ObservableCollection并在调用AddRange时暂停通知。请参阅以下博客文章以获得更多澄清。


8
我认为有一种“棘手”的方式可以相当准确地实现这一点,即编写自己的ObservableCollection并实现AddRange处理。
这样,您可以将所有的10k元素添加到某个“容器集合”中,在完成后使用您的ObservableCollectionAddRange一次性完成操作。
更多相关信息请查看以下链接: ObservableCollection Doesn't support AddRange method.... 或者这个链接: AddRange and ObservableCollection

1
有趣。我想知道为什么这不是ObservableCollection的一部分。 - Tower
@rFactor:老实说,我不知道。如果能像内置一样就太好了,但是......也许像Eric Lippert有时候说的那样:因为没有人去实现它...... - Tigran
我无法让它们工作,当代码调用 OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add)); 时,我得到了 Additional information: Constructor supports only the 'Reset' action. - Tower
@rFactor:你试过使用下面的代码吗:this.OnPropertyChanged("Count"); this.OnPropertyChanged("Item[]");?这样可以同时通知 CountItems 集合的变化。 - Tigran
@Tigran,它期望一个事件对象,而不是一个字符串——这些字符串的等效事件是什么? - Tower
显示剩余3条评论

1

我发现需要对Xiaoguo Ge的答案进行扩展。我的代码与该答案中的代码相同,除了以下几点:

  1. 我添加了一个OnPropertyChanged方法的重写,以便抑制发布PropertyChanged事件。
  2. 在属性设置器中,进行了两次OnPropertyChanged调用
  3. 我为了更清晰地表达,重命名了字段和属性

我的ObservableCollection是DataGrid的ItemsSource,在其中有替换数千个项的情况。如果不实现#1,我发现我没有得到所需的性能提升(它是巨大的!)。我不确定#2有多重要,但在另一个StackOverflow页面中展示了稍微不同的解决方案。我猜测,抑制PropertyChanged事件改善了我的性能,这表明DataGrid已经订阅了该事件,因此在关闭通知抑制时发布事件可能很重要。

一个小提示是,我认为从OnPropertyChanged方法中设置_havePendingNotifications = true是不必要的,但如果您发现不同,请考虑添加它。

    /// <summary>
    /// If this property is set to true, then CollectionChanged and PropertyChanged
    /// events are not published. Furthermore, if collection changes occur while this property is set
    /// to true, then subsequently setting the property to false will cause a CollectionChanged event
    /// to be published with Action=Reset.  This is designed for faster performance in cases where a
    /// large number of items are to be added or removed from the collection, especially including cases
    /// where the entire collection is to be replaced.  The caller should follow this pattern:
    ///   1) Set NotificationSuppressed to true
    ///   2) Do a number of Add, Insert, and/or Remove calls
    ///   3) Set NotificationSuppressed to false
    /// </summary>
    public Boolean NotificationSuppressed
    {
        get { return _notificationSuppressed; }
        set
        {
            _notificationSuppressed = value;
            if (_notificationSuppressed == false && _havePendingNotifications)
            {
                OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
                OnPropertyChanged(new PropertyChangedEventArgs("Count"));
                OnCollectionChanged(
                           new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                _havePendingNotifications = false;
            }
        }
    }
    /// <summary> This field is backing store for public property NotificationSuppressed </summary>
    protected Boolean _notificationSuppressed = false;
    /// <summary>
    /// This field indicates whether there have been notifications that have been suppressed due to the
    /// NotificationSuppressed property having value of true.  If this field is true, then when
    /// NotificationSuppressed is next set to false, a CollectionChanged event is published with
    /// Action=Reset, and the field is reset to false.
    /// </summary>
    protected Boolean _havePendingNotifications = false;
    /// <summary>
    /// This method publishes the CollectionChanged event with the provided arguments.
    /// </summary>
    /// <param name="e">container for arguments of the event that is published</param>
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (NotificationSuppressed)
        {
            _havePendingNotifications = true;
            return;
        }
        base.OnCollectionChanged(e);
    }
    /// <summary>
    /// This method publishes the PropertyChanged event with the provided arguments.
    /// </summary>
    /// <param name="e">container for arguments of the event that is published</param>
    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (NotificationSuppressed) return;
        base.OnPropertyChanged(e);
    }

0

抱歉,我本来想将这个作为评论发布的,因为我不会提供完整的实现细节,但是有点太长了。

关于"不支持范围操作",这是由WPF用于绑定的ListCollectionView引起的,它确实不支持范围操作。然而,普通的CollectionView是支持的。

当绑定的集合实现非泛型的IList接口时,WPF选择使用ListCollectionView。所以,要使AddRange解决方案生效,你需要完全重新实现ObservableCollection(而不是继承它),但是不包含非泛型接口:

public class MyObservableCollection<T> :
    IList<T>,
    IReadOnlyList<T>,
    INotifyCollectionChanged,
    INotifyPropertyChanged
{
   // ...
}

借助dotPeek或类似工具的帮助,实现这个应该不需要太长时间。请注意,您可能会因为使用CollectionView而失去一些优化,而不是ListCollectionView,但从我自己的经验来看,全局使用这样的类可以完全提高性能。


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