2010-07-21 2 views
10

обратите внимание, что я пытаюсь использовать действие NotifyCollectionChangedAction.Add вместо .Reset. последний работает, но он не очень эффективен с большими коллекциями.ObservableCollection: вызов OnCollectionChanged с несколькими новыми элементами

так я подклассы 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)); 
     } 
    } 

бросает Коллекции Добавить событие относится к пункту, который не принадлежит к коллекции

это, кажется, бу g в BCL?

я могу пошагово увидеть до вызова OnCollectionChanged, что новые элементы добавляются в this.Items

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).

+0

Почему RemoveRange, AddRange fires Сброс? Может быть, кто-то не мог понять разницу между значениями remove, add и reset? –

ответ

6

можно реализовать AddRange() для ObservableCollection, как это, как показано here:

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 я s исключение InvalidOperationException тоже (то же сообщение, которое вы видели). В соответствии с этим article это потому, что CollectionView не поддерживает действия диапазона. К счастью, статья также поставляет решение (хотя оно чувствует себя немного «хак-иш»).

ОБНОВЛЕНИЕ 2: добавлено исправление, которое вызывает переопределенное событие CollectionChanged в переопределенной реализации OnCollectionChanged().

+0

спасибо, но я пытаюсь перейти от .Reset action. Все дело в том, что я хочу добавить только новые предметы, если моя коллекция достигает большого размера, .reset очень медленно, так как я также фильтрую ее –

+0

А, я пропустил это - обновил мой код, чтобы использовать NotifyCollectionChangedAction.Add вместо Reset. – andyp

+0

Добавлена ​​ссылка и код, который разрешает (избегает) проблемы CollectionView с операциями дальности. – andyp

-1

Я считаю, что вам нужно отдать его IList :

base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)_cachedItems));

+0

спасибо, теперь я вернулся в «Коллекция Добавить событие относится к элементу, который не принадлежит коллекции» –

+0

Я скорректировал свой код, спасибо –

+0

hmmm, а как насчет вместо 'Items.Add (item)', 'base.Add (пункт) '? –

1

Спасибо за вдохновение AndyP. У меня было несколько проблем с вашей реализацией, таких как использование CollectionView вместо ICollectionView в тесте, а также ручное вызов «Сброс» для элементов. Элементы, которые унаследованы от CollectionView, могут иметь дело с этими аргументами больше, чем с вызовом «this.Reset()», поэтому желательно по-прежнему запускать их обработчики, просто с аргументами Action = Reset, которые они требуют вместо улучшенных аргументов событий, которые включите список измененных элементов. Ниже приведена моя (очень похожая) реализация.

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 
} 
+0

Как бы вы это сделали, если BaseObservableCollection определен в переносном проекте? Я считаю, что ICollectionView специфичен для окон и поэтому недоступен для использования. – user2481095

0

После многих итераций, мы закончили с этой версией 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, и это работает как шарм.