ObservableCollection: вызов OnCollectionChanged с несколькими новыми элементами
Обратите внимание, что я пытаюсь использовать действие NotifyCollectionChangedAction.Add вместо .Reset. последний работает, но он не очень эффективен с большими коллекциями.
поэтому я подклассифицирован ObservableCollection:
public class SuspendableObservableCollection<T> : ObservableCollection<T>
по какой-то причине, этот код:
private List<T> _cachedItems;
...
public void FlushCache() {
if (_cachedItems.Count > 0) {
foreach (var item in _cachedItems)
Items.Add(item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add, (IList<T>)_cachedItems));
}
}
бросает
Коллекция Добавить событие относится к элементу, который не принадлежит коллекции
похоже, это ошибка в BCL?
Я могу пройти и увидеть перед вызовом OnCollectionChanged, что к этому добавляются новые элементы. Элементы
WOW
просто сделал ошеломляющее открытие. Ни один из этих подходов не работал у меня (flush, addrange), потому что ошибка, похоже, срабатывает ТОЛЬКО, если эта коллекция привязана к моему списку!
TestObservableCollection<Trade> testCollection = new TestObservableCollection<Trade>();
List<Trade> testTrades = new List<Trade>();
for (int i = 0; i < 200000; i++)
testTrades.Add(t);
testCollection.AddRange(testTrades); // no problems here..
_trades.AddRange(testTrades); // this one is bound to ListView .. BOOOM!!!
В заключение ObservableCollection поддерживает добавление инкрементных списков, но ListView не делает этого. Andyp разработал обходное решение, чтобы заставить его работать с CollectionView ниже, но так как вызывается .Refresh(), это ничем не отличается от вызова OnCollectionChanged (.Reset)..
Ответы
Ответ 1
вы можете реализовать AddRange() для ObservableCollection следующим образом: здесь:
public class RangeObservableCollection<T> : ObservableCollection<T>
{
private bool _SuppressNotification;
public override event NotifyCollectionChangedEventHandler CollectionChanged;
protected virtual void OnCollectionChangedMultiItem(
NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChangedEventHandler handlers = this.CollectionChanged;
if (handlers != null)
{
foreach (NotifyCollectionChangedEventHandler handler in
handlers.GetInvocationList())
{
if (handler.Target is CollectionView)
((CollectionView)handler.Target).Refresh();
else
handler(this, e);
}
}
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_SuppressNotification)
{
base.OnCollectionChanged(e);
if (CollectionChanged != null)
CollectionChanged.Invoke(this, e);
}
}
public void AddRange(IEnumerable<T> list)
{
if (list == null)
throw new ArgumentNullException("list");
_SuppressNotification = true;
foreach (T item in list)
{
Add(item);
}
_SuppressNotification = false;
OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list));
}
}
UPDATE: после привязки к ListBox я тоже видел исключение InvalidOperationException (то же сообщение, которое вы видели). Согласно этой статье, потому что CollectionView не поддерживает действия диапазона. К счастью, статья также поставляет решение (хотя он чувствует себя немного "хак-иш" ).
UPDATE 2: добавлено исправление, которое вызывает переопределенное событие CollectionChanged в переопределенной реализации OnCollectionChanged().
Ответ 2
Спасибо за вдохновение AndyP. У меня было несколько проблем с вашей реализацией, таких как использование CollectionView вместо ICollectionView в тесте, а также ручное вызов "Reset" для элементов. Элементы, которые наследуются от CollectionView, могут иметь дело с этими аргументами больше, чем с вызовом "this.Reset()", поэтому предпочтительнее по-прежнему запускать их обработчики, просто с помощью аргументов Action = Reset, которые они требуют вместо улучшенного события args, которые включают список измененных элементов. Ниже приведена моя (очень похожая) реализация.
public class BaseObservableCollection<T> : ObservableCollection<T>
{
//Flag used to prevent OnCollectionChanged from firing during a bulk operation like Add(IEnumerable<T>) and Clear()
private bool _SuppressCollectionChanged = false;
/// Overridden so that we may manually call registered handlers and differentiate between those that do and don't require Action.Reset args.
public override event NotifyCollectionChangedEventHandler CollectionChanged;
public BaseObservableCollection() : base(){}
public BaseObservableCollection(IEnumerable<T> data) : base(data){}
#region Event Handlers
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if( !_SuppressCollectionChanged )
{
base.OnCollectionChanged(e);
if( CollectionChanged != null )
CollectionChanged.Invoke(this, e);
}
}
//CollectionViews raise an error when they are passed a NotifyCollectionChangedEventArgs that indicates more than
//one element has been added or removed. They prefer to receive a "Action=Reset" notification, but this is not suitable
//for applications in code, so we actually check the type we're notifying on and pass a customized event args.
protected virtual void OnCollectionChangedMultiItem(NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChangedEventHandler handlers = this.CollectionChanged;
if( handlers != null )
foreach( NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList() )
handler(this, !(handler.Target is ICollectionView) ? e : new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
#endregion
#region Extended Collection Methods
protected override void ClearItems()
{
if( this.Count == 0 ) return;
List<T> removed = new List<T>(this);
_SuppressCollectionChanged = true;
base.ClearItems();
_SuppressCollectionChanged = false;
OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
}
public void Add(IEnumerable<T> toAdd)
{
if( this == toAdd )
throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");
_SuppressCollectionChanged = true;
foreach( T item in toAdd )
Add(item);
_SuppressCollectionChanged = false;
OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(toAdd)));
}
public void Remove(IEnumerable<T> toRemove)
{
if( this == toRemove )
throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");
_SuppressCollectionChanged = true;
foreach( T item in toRemove )
Remove(item);
_SuppressCollectionChanged = false;
OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(toRemove)));
}
#endregion
}
Ответ 3
После многих итераций мы закончили с этой версией ObservableRangeCollection
и ReadOnlyObservableRangeCollection
, которая основана на коде из принятого ответа и которую мы не должны изменять за последние 6 месяцев:
public class ObservableRangeCollection<T> : ObservableCollection<T>
{
private bool suppressNotification;
public ObservableRangeCollection() { }
public ObservableRangeCollection(IEnumerable<T> items)
: base(items)
{
}
public override event NotifyCollectionChangedEventHandler CollectionChanged;
protected virtual void OnCollectionChangedMultiItem(
NotifyCollectionChangedEventArgs e)
{
var handlers = CollectionChanged;
if (handlers == null) return;
foreach (NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList())
{
if (handler.Target is ReadOnlyObservableCollection<T>
&& !(handler.Target is ReadOnlyObservableRangeCollection<T>))
{
throw new NotSupportedException(
"ObservableRangeCollection is wrapped in ReadOnlyObservableCollection which might be bound to ItemsControl " +
"which is internally using ListCollectionView which does not support range actions.\n" +
"Instead of ReadOnlyObservableCollection, use ReadOnlyObservableRangeCollection");
}
var collectionView = handler.Target as ICollectionView;
if (collectionView != null)
{
collectionView.Refresh();
}
else
{
handler(this, e);
}
}
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (suppressNotification) return;
base.OnCollectionChanged(e);
if (CollectionChanged != null)
{
CollectionChanged.Invoke(this, e);
}
}
public void AddRange(IEnumerable<T> items)
{
if (items == null) return;
suppressNotification = true;
var itemList = items.ToList();
foreach (var item in itemList)
{
Add(item);
}
suppressNotification = false;
if (itemList.Any())
{
OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, itemList));
}
}
public void AddRange(params T[] items)
{
AddRange((IEnumerable<T>)items);
}
public void ReplaceWithRange(IEnumerable<T> items)
{
Items.Clear();
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
AddRange(items);
}
public void RemoveRange(IEnumerable<T> items)
{
suppressNotification = true;
var removableItems = items.Where(x => Items.Contains(x)).ToList();
foreach (var item in removableItems)
{
Remove(item);
}
suppressNotification = false;
if (removableItems.Any())
{
OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removableItems));
}
}
}
public class ReadOnlyObservableRangeCollection<T> : ReadOnlyObservableCollection<T>
{
public ReadOnlyObservableRangeCollection(ObservableCollection<T> list)
: base(list)
{
}
protected override event NotifyCollectionChangedEventHandler CollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
var handlers = CollectionChanged;
if (handlers == null) return;
foreach (NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList())
{
var collectionView = handler.Target as ICollectionView;
if (collectionView != null)
{
collectionView.Refresh();
}
else
{
handler(this, e);
}
}
}
}
В основном мы заменили все применения ObservableCollection
в нашем приложении на ObservableRangeCollection
, и это работает как шарм.
Ответ 4
Я считаю, что вам нужно отправить его в IList
:
base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)_cachedItems));