Класс ObservableCollection не позволяет добавлять, удалять и т.д. коллекции элементов.
Чтобы добавить такую функциональность можно создать потомок этого класса, в котором реализовать необходимый функционал.
Цель:
Избежать множественных событий PropertyChanged и OnCollectionChanged при массовых изменениях коллекции, а выигрыш по синтаксису практически незначительный и роли не играет.
В ObservableCollection есть унаследованное от Collection свойство:
с которым и необходимо работать.
Шаблон доработки такой:
1) Проверить на возможность изменения:
2) Обработать элементы согласно вашей логике:
3) Вызвать событие PropertyChanged для свойств «Count» и «Item[]»:
4) Вызвать событие CollectionChanged с параметрами события: тип изменения Reset, параметры OldItems и NewItems не передавать:
Недостатки:
Из-за п.4 в обработчике события CollectionChanged невозможно будет работать с OldItems и NewItems так как они пустые. Это необходимо из-за того, что некоторые контролы WPF не работают с изменениями коллекции не по одному элементу, а по несколько. При этом, если тип изменения Reset, то это означает что произошло существенно изменение коллекции, и для контролов WPF это нормально. Если же вы используете новый класс не в качестве источника данных для контрола WPF, то можно в п.4 передавать и другие типы изменений, а также заполненные значения OldItems и NewItems и затем спокойно их обрабатывать.
Пример:
Тесты:
Чтобы добавить такую функциональность можно создать потомок этого класса, в котором реализовать необходимый функционал.
Цель:
Избежать множественных событий 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);
}
}
}