Как избежать обстрела ObservableCollection.CollectionChanged несколько раз при замене всех элементов или добавлении коллекции элементов
У меня есть коллекция ObservableCollection<T>
, и я хочу заменить все элементы новой коллекцией элементов, я мог бы сделать:
collection.Clear();
ИЛИ:
collection.ClearItems();
(Кстати, какая разница между этими двумя методами?)
Я мог бы также использовать foreach
to collection.Add
один за другим, но это будет срабатывать несколько раз
То же самое при добавлении коллекции элементов.
EDIT:
Я нашел здесь хорошую библиотеку: Enhanced ObservableCollection с возможностью отсрочки или отключения уведомлений, но похоже, что он НЕ поддерживает silverlight.
Ответы
Ответ 1
ColinE прав со всеми его сведениями. Я хочу добавить только мой подкласс ObservableCollection
, который я использую для этого конкретного случая.
public class SmartCollection<T> : ObservableCollection<T> {
public SmartCollection()
: base() {
}
public SmartCollection(IEnumerable<T> collection)
: base(collection) {
}
public SmartCollection(List<T> list)
: base(list) {
}
public void AddRange(IEnumerable<T> range) {
foreach (var item in range) {
Items.Add(item);
}
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void Reset(IEnumerable<T> range) {
this.Items.Clear();
AddRange(range);
}
}
Ответ 2
Это можно сделать путем подклассификации ObservableCollection
и реализации собственного метода ReplaceAll
. Реализация этих методов заменит все элементы внутри внутреннего свойства Items
, а затем запустит событие CollectionChanged
. Аналогично, вы можете добавить метод AddRange
. Для реализации этого см. Ответ на этот вопрос:
ObservableCollection Не поддерживает метод AddRange, поэтому я получаю уведомление для каждого добавленного элемента, помимо того, что касается INotifyCollectionChanging?
Разница между Collection.Clear
и Collection.ClearItems
заключается в том, что Clear
является общедоступным методом API, тогда как ClearItems
защищен, это точка расширения, которая позволяет расширять/изменять поведение Clear
.
Ответ 3
Вот что я применил для ссылки других людей:
// http://stackoverflow.com/questions/13302933/how-to-avoid-firing-observablecollection-collectionchanged-multiple-times-when-r
// http://stackoverflow.com/questions/670577/observablecollection-doesnt-support-addrange-method-so-i-get-notified-for-each
public class ObservableCollectionFast<T> : ObservableCollection<T>
{
public ObservableCollectionFast()
: base()
{
}
public ObservableCollectionFast(IEnumerable<T> collection)
: base(collection)
{
}
public ObservableCollectionFast(List<T> list)
: base(list)
{
}
public virtual void AddRange(IEnumerable<T> collection)
{
if (collection.IsNullOrEmpty())
return;
foreach (T item in collection)
{
this.Items.Add(item);
}
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
// Cannot use NotifyCollectionChangedAction.Add, because Constructor supports only the 'Reset' action.
}
public virtual void RemoveRange(IEnumerable<T> collection)
{
if (collection.IsNullOrEmpty())
return;
bool removed = false;
foreach (T item in collection)
{
if (this.Items.Remove(item))
removed = true;
}
if (removed)
{
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
// Cannot use NotifyCollectionChangedAction.Remove, because Constructor supports only the 'Reset' action.
}
}
public virtual void Reset(T item)
{
this.Reset(new List<T>() { item });
}
public virtual void Reset(IEnumerable<T> collection)
{
if (collection.IsNullOrEmpty() && this.Items.IsNullOrEmpty())
return;
// Step 0: Check if collection is exactly same as this.Items
if (IEnumerableUtils.Equals<T>(collection, this.Items))
return;
int count = this.Count;
// Step 1: Clear the old items
this.Items.Clear();
// Step 2: Add new items
if (!collection.IsNullOrEmpty())
{
foreach (T item in collection)
{
this.Items.Add(item);
}
}
// Step 3: Don't forget the event
if (this.Count != count)
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
Ответ 4
Я еще не могу комментировать предыдущие ответы, поэтому я добавляю здесь адаптацию RemoveRange реализованных выше реализаций SmartCollection, которая не будет вызывать исключение С# InvalidOperationException: сборник был изменен. Он использует предикат, чтобы проверить, должен ли элемент быть удален, что в моем случае более оптимально, чем создание подмножества элементов, соответствующих критериям удаления.
public void RemoveRange(Predicate<T> remove)
{
// iterates backwards so can remove multiple items without invalidating indexes
for (var i = Items.Count-1; i > -1; i--) {
if (remove(Items[i]))
Items.RemoveAt(i);
}
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
Пример:
LogEntries.RemoveRange(i => closeFileIndexes.Contains(i.fileIndex));
Ответ 5
В течение последних нескольких лет я использую более общее решение для устранения слишком большого количества уведомлений ObservableCollection, создавая операцию изменения партии и уведомляя наблюдателей с действием Reset:
public class ExtendedObservableCollection<T>: ObservableCollection<T>
{
public ExtendedObservableCollection()
{
}
public ExtendedObservableCollection(IEnumerable<T> items)
: base(items)
{
}
public void Execute(Action<IList<T>> itemsAction)
{
itemsAction(Items);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
Использование этого просто:
var collection = new ExtendedObservableCollection<string>(new[]
{
"Test",
"Items",
"Here"
});
collection.Execute(items => {
items.RemoveAt(1);
items.Insert(1, "Elements");
items.Add("and there");
});
Вызов Execute будет генерировать одно уведомление, но с недостатком - список будет обновлен в пользовательском интерфейсе в целом, а не только измененные элементы. Это делает его идеальным для items.Clear(), а затем items.AddRange(newItems).