Функциональность с Range в ObservableCollection

    Класс ObservableCollection не позволяет добавлять, удалять и т.д. коллекции элементов.
    Чтобы добавить такую функциональность можно создать потомок этого класса, в котором реализовать необходимый функционал.


    Цель:
    Избежать множественных событий PropertyChanged и OnCollectionChanged при массовых изменениях коллекции, а выигрыш по синтаксису практически незначительный и роли не играет.

    В ObservableCollection есть унаследованное от Collection свойство:
    protected IList<T> Items { get; }

    с которым и необходимо работать.

    Шаблон доработки такой:
    1) Проверить на возможность изменения:
    protected void CheckReentrancy();

    2) Обработать элементы согласно вашей логике:
    protected IList<T> Items { get; }

    3) Вызвать событие PropertyChanged для свойств «Count» и «Item[]»:
    
                OnPropertyChanged(new PropertyChangedEventArgs("Count"));
                OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
    

    4) Вызвать событие CollectionChanged с параметрами события: тип изменения Reset, параметры OldItems и NewItems не передавать:
    
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    


    Недостатки:
    Из-за п.4 в обработчике события CollectionChanged невозможно будет работать с OldItems и NewItems так как они пустые. Это необходимо из-за того, что некоторые контролы WPF не работают с изменениями коллекции не по одному элементу, а по несколько. При этом, если тип изменения Reset, то это означает что произошло существенно изменение коллекции, и для контролов WPF это нормально. Если же вы используете новый класс не в качестве источника данных для контрола WPF, то можно в п.4 передавать и другие типы изменений, а также заполненные значения OldItems и NewItems и затем спокойно их обрабатывать.

    Пример:
    
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Collections.Specialized;
    using System.ComponentModel;
    using System.Linq;
    
    namespace Common.Utils
    {
        public class ObservableRangeCollection<T> : ObservableCollection<T>
        {
            private const string CountString = "Count";
            private const string IndexerName = "Item[]";
    
            protected enum ProcessRangeAction
            {
                Add,
                Replace,
                Remove
            };
    
            public ObservableRangeCollection() : base()
            {
            }
    
            public ObservableRangeCollection(IEnumerable<T> collection) : base(collection)
            {
            }
    
            public ObservableRangeCollection(List<T> list) : base(list)
            {
            }
    
            protected virtual void ProcessRange(IEnumerable<T> collection, ProcessRangeAction action)
            {
                if (collection == null) throw new ArgumentNullException("collection");
    
                var items = collection as IList<T> ?? collection.ToList();
                if (!items.Any()) return;
    
                this.CheckReentrancy();
    
                if (action == ProcessRangeAction.Replace) this.Items.Clear();
                foreach (var item in items)
                {
                    if (action == ProcessRangeAction.Remove) this.Items.Remove(item);
                    else this.Items.Add(item);
                }
    
                this.OnPropertyChanged(new PropertyChangedEventArgs(CountString));
                this.OnPropertyChanged(new PropertyChangedEventArgs(IndexerName));
                this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
            }
    
            public void AddRange(IEnumerable<T> collection)
            {
                this.ProcessRange(collection, ProcessRangeAction.Add);
            }
    
            public void ReplaceRange(IEnumerable<T> collection)
            {
                this.ProcessRange(collection, ProcessRangeAction.Replace);
            }
    
            public void RemoveRange(IEnumerable<T> collection)
            {
                this.ProcessRange(collection, ProcessRangeAction.Remove);
            }
        }
    }
    


    Тесты:

    
    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.ComponentModel;
    using Common.Utils;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    
    namespace Tests.Common
    {
        [TestClass]
        public class ObservableRangeCollectionTests
        {
            [TestMethod]
            public void AddRangeTest()
            {
                var eventCollectionChangedCount = 0;
                var eventPropertyChangedCount = 0;
    
                var orc = new ObservableRangeCollection<int>(new List<int> {0, 1, 2, 3});
                orc.CollectionChanged += (sender, e) =>
                {
                    Assert.AreEqual(NotifyCollectionChangedAction.Reset, e.Action);
                    eventCollectionChangedCount++;
                };
                ((INotifyPropertyChanged) orc).PropertyChanged += (sender, e) =>
                {
                    CollectionAssert.Contains(new[] { "Count", "Item[]" }, e.PropertyName);
                    eventPropertyChangedCount++;
                };
                
                orc.AddRange(new List<int> { 4, 5, 6, 7 });
    
                Assert.AreEqual(8, orc.Count);
                CollectionAssert.AreEqual(new List<int> { 0, 1, 2, 3, 4, 5, 6, 7 }, orc);
                Assert.AreEqual(1, eventCollectionChangedCount);
                Assert.AreEqual(2, eventPropertyChangedCount);
            }
    
            [TestMethod]
            public void ReplaceRangeTest()
            {
                var eventCollectionChangedCount = 0;
                var eventPropertyChangedCount = 0;
    
                var orc = new ObservableRangeCollection<int>(new List<int> { 0, 1, 2, 3 });
                orc.CollectionChanged += (sender, e) =>
                {
                    Assert.AreEqual(NotifyCollectionChangedAction.Reset, e.Action);
                    eventCollectionChangedCount++;
                };
                ((INotifyPropertyChanged)orc).PropertyChanged += (sender, e) =>
                {
                    CollectionAssert.Contains(new[] { "Count", "Item[]" }, e.PropertyName);
                    eventPropertyChangedCount++;
                };
    
                orc.ReplaceRange(new List<int> { 4, 5, 6 });
    
                Assert.AreEqual(3, orc.Count);
                CollectionAssert.AreEqual(new List<int> { 4, 5, 6 }, orc);
                Assert.AreEqual(1, eventCollectionChangedCount);
                Assert.AreEqual(2, eventPropertyChangedCount);
            }
    
            [TestMethod]
            public void RemoveRangeTest()
            {
                var eventCollectionChangedCount = 0;
                var eventPropertyChangedCount = 0;
    
                var orc = new ObservableRangeCollection<int>(new List<int> { 0, 1, 2, 3 });
                orc.CollectionChanged += (sender, e) =>
                {
                    Assert.AreEqual(NotifyCollectionChangedAction.Reset, e.Action);
                    eventCollectionChangedCount++;
                };
                ((INotifyPropertyChanged)orc).PropertyChanged += (sender, e) =>
                {
                    CollectionAssert.Contains(new[] { "Count", "Item[]" }, e.PropertyName);
                    eventPropertyChangedCount++;
                };
    
                orc.RemoveRange(new List<int> { 1, 3, 6 });
    
                Assert.AreEqual(2, orc.Count);
                CollectionAssert.AreEqual(new List<int> { 0, 2 }, orc);
                Assert.AreEqual(1, eventCollectionChangedCount);
                Assert.AreEqual(2, eventPropertyChangedCount);
            }
    
            private enum RangeAction
            {
                Add,
                Replace,
                Remove
            }
    
            private void EmptyRangeTest(RangeAction action)
            {
                var eventCollectionChangedCount = 0;
                var eventPropertyChangedCount = 0;
    
                var orc = new ObservableRangeCollection<int>(new List<int> { 0, 1, 2, 3 });
                orc.CollectionChanged += (sender, e) =>
                {
                    eventCollectionChangedCount++;
                };
                ((INotifyPropertyChanged)orc).PropertyChanged += (sender, e) =>
                {
                    eventPropertyChangedCount++;
                };
    
                switch (action)
                {
                    case RangeAction.Replace: orc.ReplaceRange(new List<int>());
                        break;
                    case RangeAction.Remove: orc.RemoveRange(new List<int>());
                        break;
                    default: orc.AddRange(new List<int>());
                        break;
                }
    
                Assert.AreEqual(4, orc.Count);
                CollectionAssert.AreEqual(new List<int> { 0, 1, 2, 3 }, orc);
                Assert.AreEqual(0, eventCollectionChangedCount);
                Assert.AreEqual(0, eventPropertyChangedCount);
            }
    
            [TestMethod]
            public void AddEmptyRangeTest()
            {
                this.EmptyRangeTest(RangeAction.Add);
            }
    
            [TestMethod]
            public void ReplaceEmptyRangeTest()
            {
                this.EmptyRangeTest(RangeAction.Replace);
            }
    
            [TestMethod]
            public void RemoveEmptyRangeTest()
            {
                this.EmptyRangeTest(RangeAction.Remove);
            }
    
            [TestMethod]
            [ExpectedException(typeof(ArgumentNullException))]
            public void AddNullRangeTest()
            {
                new ObservableRangeCollection<int>().AddRange(null);
            }
    
            [TestMethod]
            [ExpectedException(typeof(ArgumentNullException))]
            public void ReplaceNullRangeTest()
            {
                new ObservableRangeCollection<int>().ReplaceRange(null);
            }
    
            [TestMethod]
            [ExpectedException(typeof(ArgumentNullException))]
            public void RemoveNullRangeTest()
            {
                new ObservableRangeCollection<int>().RemoveRange(null);
            }
        }
    }
    
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 17

      +1
      Обычно делают дополнительные методы типа SuspendNotification и ResumeNotification. Если нотификация была приостановлена в процессе добавления, то после её включения делаем Reset. Если не приостанавливалась — делаем CollectionChanged на каждый элемент.
      И непонятно, зачем Вы делаете «collection.ToList()». Вам не нужен список внутри реализации. Зачем лишнюю память занимать?
        0
        И непонятно, зачем Вы делаете «collection.ToList()». Вам не нужен список внутри реализации. Зачем лишнюю память занимать?


        Чтобы исключить возможный бесконечный цикл ниже:
        foreach (var item in items)


        Напишите, пожалуйста, в комментах реализацию SuspendNotification и ResumeNotification.
          0
          Чтобы исключить возможный бесконечный цикл ниже:

          Наверно, я чего-то недопонимаю. Причём тут бесконечный цикл? И как ToList позволит его избежать?

          Напишите, пожалуйста, в комментах реализацию SuspendNotification и ResumeNotification.

          Ну блин, это же совсем просто. Сделать флаг, который бы устанавливался в Suspend и сбрасывался в Resume. Ну и всю обвязку. Советую скачать триал Telerik-а и глянуть dotPeek-ом реализацию вот этого: docs.telerik.com/devtools/wpf/api/html/t_telerik_windows_data_radobservablecollection_1.htm
            0
            Про бесконечный цикл. Значение переданного параметра IEnumerable может быть реализовано криво, так что получится бесконечный цикл. При использовании ToList он тоже будет бесконечным, но до начала изменения нашей коллекции, а не в момент изменения. Если создание нового объекта раздражает, то можно убрать данную строку.

            Спасибо за ссылку.
              0
              Жесть. У Вас реально такое случалось в практике? Если кто-то вызвал добавление в коллекцию бесконечного числа элементов, то проблема за пределами коллекции.
              Если у Вас действительно в вашем ПО такое может случиться, то лучше обезопаситься на уровне интерфейсов — убрать конструктор «ObservableRangeCollection(IEnumerable collection)» и везде поменять IEnumerable на ICollection.

              P. S. Не могли бы Вы ещё привести конкретные примеры «некоторые контролы WPF не работают с изменениями коллекции не по одному элементу, а по несколько.»? Я с таким не встречался. Чисто для самообразования.
                0
                P. S. Не могли бы Вы ещё привести конкретные примеры «некоторые контролы WPF не работают с изменениями коллекции не по одному элементу, а по несколько.»? Я с таким не встречался. Чисто для самообразования.

                Код из System.Windows.Controls.ItemContainerGenerator, который используется для создания элементов в ItemsControl и его наследниках.
                private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
                {
                    if (sender != this.ItemsInternal && args.Action != NotifyCollectionChangedAction.Reset)
                        return;
                    switch (args.Action)
                    {
                    case NotifyCollectionChangedAction.Add:
                        if (args.NewItems.Count != 1)
                            throw new NotSupportedException(System.Windows.SR.Get("RangeActionsNotSupported"));
                        this.OnItemAdded(args.NewItems[0], args.NewStartingIndex);
                        break;
                    case NotifyCollectionChangedAction.Remove:
                        if (args.OldItems.Count != 1)
                            throw new NotSupportedException(System.Windows.SR.Get("RangeActionsNotSupported"));
                        this.OnItemRemoved(args.OldItems[0], args.OldStartingIndex);
                        break;
                
                  0
                  А также,
                  CollectionView
                  CompositeCollectionView
                  ListCollectionView
                  CollectionViewSource
            0
            Напишите, пожалуйста, в комментах реализацию SuspendNotification и ResumeNotification.

            вот пример вместе с методом AddRange() peteohanlon.wordpress.com/2008/10/22/bulk-loading-in-observablecollection/
              0
              Указанную статью я знаю. Можно сделать и так, смысла это не меняет. Но там не учтен п.1 из моей статьи
              this.CheckReentrancy();
              , который все-таки должен быть. Иначе в процессе обработки данных в обработчиках события CollectionChanged, актуальность данных не гарантируется.
              То есть возможны (в зависимости от комбинации операций над данными в текущем методе и в обработчиках события CollectionChanged):
              — потерянное обновление (lost update)
              — «грязное» чтение (dirty read)
              — неповторяющееся чтение (non-repeatable read)
              — фантомное чтение (fantom read).
            +1
            А почему бы просто не написать несколько методов для расширения стандартного ObservableCollection?

            public static void ForEach<T>(this IEnumerable<T> collection, Action<T> predicate) { foreach (var item in collection) predicate(item); }

            public static void AddRange<T>(this IList<T> collection, IEnumerable<T> itemsToAdd) { itemsToAdd.ForEach(collection.Add); }

            public static void RemoveRange<T>(this IList<T> collection, IEnumerable<T> itemsToRemove) { itemsToRemove.ForEach(p => collection.Remove(p)); }

            public static void ReplaceRange<T>(this IList<T> collection, IEnumerable<T> itemsToReplace) { collection.Clear(); collection.AddRange(itemsToReplace); }
              0
              Чтобы избежать множественного вызова событий:
               PropertyChanged
               PropertyChanged
               OnCollectionChanged
              

              На добавление/удаление каждого элемента у вас вызовется по три события.

              Например, если коллекция служит источником для отображения на форме, обновление контрола будет производиться на добавление/удаление каждого элемента, а это не хорошо, если элементов много.
              А приведенном мной примере вызовутся три события только в конце обработки всех элементов. То есть обновление контрола произойдет один раз.
                0
                Вообще-то в WPF всё гораздо хитрее. И контрол не будет обновляться каждый раз. Это только если специально так через Dispatcher реализовывать.
                  0
                  Нет, в WPF всё тупо.
                  Особенно печально, если коллекция используется в качестве источника данных для отсортированного представления коллекции. На каждый чих представление будет пересортировываться. При этом контролы не перерисовываются немедленно, это да, но зато серьёзно нагружается ItemContainerGenerator. При операциях добавления большого числа элементов в подключенную к ItemsControl'у коллекцию, даже если в нем включена виртуализация, возникает шторм событий. Производительность добавления при этом на порядки ниже чем при одной нотификации Reset.
              0
              Как я понял, единственная цель работы — избежать множественных событий PropertyChanged, PropertyChanged и OnCollectionChanged при массовых изменениях коллекции, а выигрыш по синтаксису практически незначительный и роли не играет. Хорошо бы это было написано в начале.
                0
                Да — так оно и есть, спасибо за уточнение. Внес правку.
                0
                Дополнение:

                При использовании коллекции в WPF, для изменения коллекции асинхронно, не в потоке UI, можно реализовать как предлагается по этой ссылке.

                Иначе возникнет ошибка {«this type of collectionview does not support changes to its sourcecollection from a thread different from the dispatcher thread.»}
                  0
                  Вообще, это делается так:
                      public class ObservableCollectionEx<T> : ObservableCollection<T>
                      {
                          private int    m_RefreshDeferred;
                          private bool   m_Modified;
                  
                          /// <summary>
                          /// Raises the <see cref="E:CollectionChanged" /> event.
                          /// </summary>
                          /// <param name="e">The instance containing the event data.</param>
                          protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
                          {
                              if (m_RefreshDeferred > 0)
                              {
                                  m_Modified = true;
                                  return;
                              }
                  
                              base.OnCollectionChanged(e);
                          }
                  
                          /// <summary>
                          /// Отложить посылку уведомлений об изменении состава коллекции.
                          /// </summary>
                          /// <returns>Дескриптор.</returns>
                          public DeferRefreshHelper DeferRefresh()
                          {
                              return new DeferRefreshHelper(this);
                          }
                  
                          /// <summary>
                          /// Дескриптор отложенных изменений.
                          /// </summary>
                          public struct DeferRefreshHelper : IDisposable
                          {
                              private ObservableCollectionEx<T> m_Owner;
                  
                              internal DeferRefreshHelper(ObservableCollectionEx<T> owner)
                              {
                                  if (null == owner)
                                      throw new ArgumentNullException("owner");
                                  m_Owner = owner;
                                  m_Owner.m_RefreshDeferred++;
                              }
                  
                              /// <summary>
                              /// Уменьшить счетчик отложенной посылки обновлений.
                              /// </summary>
                              public void Dispose()
                              {
                                  if (null == m_Owner)
                                      return;
                  
                                  var temp = m_Owner;
                                  m_Owner = null;
                  
                                  if (0 == --temp.m_RefreshDeferred && temp.m_Modified)
                                      temp.OnCollectionChanged(new NotifyCollectionChangedEventArgs(
                                          NotifyCollectionChangedAction.Reset));
                              }
                          }
                      }
                  


                  С уровнем малость промахнулся.

                Only users with full accounts can post comments. Log in, please.