【问题标题】:Can I somehow temporarily disable WPF data binding changes?我可以以某种方式暂时禁用 WPF 数据绑定更改吗?
【发布时间】:2012-05-21 07:11:10
【问题描述】:

我有一个使用 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()));

【问题讨论】:

    标签: c# wpf data-binding mvvm


    【解决方案1】:

    我发现有必要扩展Xiaoguo Ge's answer。我的代码与该答案相同,除了:

    1. 我添加了对 OnPropertyChanged 方法的覆盖,以禁止发布 PropertyChanged 事件。
    2. 在属性设置器中,对 OnPropertyChanged 进行了两次调用
    3. 为了清晰起见,我重命名了字段和属性

    我的 ObservableCollection 是 DataGrid 的 ItemsSource,在那里我有替换数千个项目的案例。如果不实施#1,我发现我没有获得我需要的性能提升(这是巨大的!)。我不确定#2 可能有多重要,但它显示在another StackOverflow page 中,它对同一问题采取了稍微不同的方法。我猜测抑制 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);
        }
    

    【讨论】:

      【解决方案2】:

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

      关于“不支持范围动作”,这来自 WPF 用于绑定的ListCollectionView,它确实不支持范围动作。但是,普通的CollectionView 可以。

      当绑定集合实现非泛型IList 接口时,WPF 选择使用ListCollectionView。所以基本上要让AddRange 解决方案正常工作,您需要完全重新实现 ObservableCollection(而不是对其进行交互),但没有非泛型接口:

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

      在 dotPeek 或等效工具的帮助下,应该不会花很长时间来实现这一点。请注意,您可能会因为您将使用 CollectionView 而不是 ListCollectionView 而失去一些优化,但根据我自己的经验,在全球范围内使用这样的类可以完全提高性能。

      【讨论】:

        【解决方案3】:

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

        它公开了一个公共的 SupressNotification 属性,以允许用户控制何时禁止 CollectionChanged 通知。

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

        此实现将所有被抑制的通知替换为重置通知。这是合乎逻辑的。当用户禁止通知,进行批量更改然后重新启用它时,应该适当地发送重新发送通知。

        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);
            }
        }
        

        【讨论】:

        • 不错的课程,我喜欢您在将事件重新设置为 true 后触发事件的解决方案。
        • 以防万一有人想知道,这就是 NotifyCollectionChangedAction.Reset 值的作用,并提供了处理它的方法 stackoverflow.com/questions/4495904/…
        • 不错,干净的实现! Visual Studio 2019 中的示例测试,其中 WPF 控件绑定到具有几十行的 ObservableCollection&lt;T&gt; 对象。原始ObservableCollection 每行需要大约 90 µs 来更新 UI。此处的可抑制版本每行需要约 15 µs。
        【解决方案4】:

        有一种“棘手”的方式,但在我看来,实现这一点非常准确。 就是写你自己的ObservableCollection 并实现AddRange 处理。

        通过这种方式,您可以将所有 10k 元素添加到某个“持有人集合”中,然后在您完成后,使用 您的ObservableCollecitonAddRange 来执行此操作。

        您可以在此链接上找到更多信息:

        ObservableCollection Doesn't support AddRange method....

        或者这个也是

        AddRange and ObservableCollection

        【讨论】:

        • 有趣。我想知道为什么这不是ObservableCollection 的一部分。
        • @rFactor:老实说,不知道。像内置一样拥有它会非常好,但是......可能是,就像 Eric Lippert 有时说的那样:因为它没有实现......
        • 我无法让它们工作,当代码调用 OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add)); 时,我得到:Additional information: Constructor supports only the 'Reset' action.
        • @rFactor:阅读 API 文档。您需要使用构造函数来获取已添加项目的列表。
        • @KentBoogaart 我明白了,现在添加collection.ToList() 作为第二个参数后,我得到Additional information: Range actions are not supported.
        【解决方案5】:

        一个非常快速和简单的方法是继承 ObservableCollection 并在调用 AddRange 时暂停通知。有关说明,请参阅following blog post

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2015-07-05
          • 1970-01-01
          • 2021-09-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-09-08
          相关资源
          最近更新 更多