Pull to refresh

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

Reading time5 min
Views15K
Класс 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);
        }
    }
}
Tags:
Hubs:
Total votes 11: ↑10 and ↓1+9
Comments17

Articles