2008-10-22 2 views
81

У меня есть что-то здесь, что на самом деле меня отвлекает.При очистке наблюдаемого набора, нет элементов в e.OldItems

У меня есть ObservableCollection of T, который заполнен элементами. У меня также есть обработчик событий, связанный с событием CollectionChanged.

Когда вы Очистите коллекцию, которая вызывает событие CollectionChanged с параметром e.Action. NotifyCollectionChangedAction.Reset. Хорошо, это нормально. Но странно то, что ни e.OldItems, ни e.NewItems ничего в этом нет. Я бы ожидал, что e.OldItems будет заполнен всеми элементами, которые были удалены из коллекции.

Кто-нибудь еще видел это? И если да, то как они обошли это?

Некоторые предпосылки: Я использую событие CollectionChanged для присоединения и отсоединения от другого события, и, таким образом, если я не получаю никаких элементов в e.OldItems ... Я не смогу отсоединиться от этого события.


ПОЯСНЕНИЯ: Я знаю, что документация не вчистую утверждают, что он должен вести себя таким образом. Но для каждого другого действия он уведомляет меня о том, что он сделал. Итак, я полагаю, что это скажет мне ... в случае Clear/Reset.


Ниже приведен пример кода, если вы хотите, чтобы воспроизвести его самостоятельно. Во-первых в XAML:

<Window 
    x:Class="ObservableCollection.Window1" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Title="Window1" 
    Height="300" 
    Width="300" 
> 
    <StackPanel> 
     <Button x:Name="addButton" Content="Add" Width="100" Height="25" Margin="10" Click="addButton_Click"/> 
     <Button x:Name="moveButton" Content="Move" Width="100" Height="25" Margin="10" Click="moveButton_Click"/> 
     <Button x:Name="removeButton" Content="Remove" Width="100" Height="25" Margin="10" Click="removeButton_Click"/> 
     <Button x:Name="replaceButton" Content="Replace" Width="100" Height="25" Margin="10" Click="replaceButton_Click"/> 
     <Button x:Name="resetButton" Content="Reset" Width="100" Height="25" Margin="10" Click="resetButton_Click"/> 
    </StackPanel> 
</Window> 

Далее, код позади:

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Windows; 
using System.Windows.Controls; 
using System.Windows.Data; 
using System.Windows.Documents; 
using System.Windows.Input; 
using System.Windows.Media; 
using System.Windows.Media.Imaging; 
using System.Windows.Navigation; 
using System.Windows.Shapes; 
using System.Collections.ObjectModel; 

namespace ObservableCollection 
{ 
    /// <summary> 
    /// Interaction logic for Window1.xaml 
    /// </summary> 
    public partial class Window1 : Window 
    { 
     public Window1() 
     { 
      InitializeComponent(); 
      _integerObservableCollection.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(_integerObservableCollection_CollectionChanged); 
     } 

     private void _integerObservableCollection_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) 
     { 
      switch (e.Action) 
      { 
       case System.Collections.Specialized.NotifyCollectionChangedAction.Add: 
        break; 
       case System.Collections.Specialized.NotifyCollectionChangedAction.Move: 
        break; 
       case System.Collections.Specialized.NotifyCollectionChangedAction.Remove: 
        break; 
       case System.Collections.Specialized.NotifyCollectionChangedAction.Replace: 
        break; 
       case System.Collections.Specialized.NotifyCollectionChangedAction.Reset: 
        break; 
       default: 
        break; 
      } 
     } 

     private void addButton_Click(object sender, RoutedEventArgs e) 
     { 
      _integerObservableCollection.Add(25); 
     } 

     private void moveButton_Click(object sender, RoutedEventArgs e) 
     { 
      _integerObservableCollection.Move(0, 19); 
     } 

     private void removeButton_Click(object sender, RoutedEventArgs e) 
     { 
      _integerObservableCollection.RemoveAt(0); 
     } 

     private void replaceButton_Click(object sender, RoutedEventArgs e) 
     { 
      _integerObservableCollection[0] = 50; 
     } 

     private void resetButton_Click(object sender, RoutedEventArgs e) 
     { 
      _integerObservableCollection.Clear(); 
     } 

     private ObservableCollection<int> _integerObservableCollection = new ObservableCollection<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; 
    } 
} 

ответ

6

Хорошо, хотя я все еще хочу, чтобы ObservableCollection вел себя так, как я хотел ... код ниже - это то, что я закончил делать. В принципе, я создал новую коллекцию T с именем TrulyObservableCollection и переопределил метод ClearItems, который затем использовал для создания события Clearing.

В коде, который использует этот TrulyObservableCollection, я использую это Клиринговое событие цикла по элементам , которые все еще находятся в коллекции в этой точке сделать отсоединение от события, что я желающем расставаться.

Надеюсь, что этот подход поможет и кому-то другому.

public class TrulyObservableCollection<T> : ObservableCollection<T> 
{ 
    public event EventHandler<EventArgs> Clearing; 
    protected virtual void OnClearing(EventArgs e) 
    { 
     if (Clearing != null) 
      Clearing(this, e); 
    } 

    protected override void ClearItems() 
    { 
     OnClearing(EventArgs.Empty); 
     base.ClearItems(); 
    } 
} 
+1

Вам нужно переименовать свой класс в `BrokenObservableCollection`, а не` TrulyObservableCollection` - вы не понимаете, что означает действие сброса. – 2010-06-03 04:43:32

2

Глядя на NotifyCollectionChangedEventArgs, оказывается, что OldItems содержит только элементы изменены в результате Заменить, Удалить или Переместить действие. Он не указывает, что он будет содержать что-либо в Clear. Я подозреваю, что Clear запускает событие, но не регистрирует удаленные элементы и вообще не вызывает код Remove.

+6

Я тоже это видел, но мне это не нравится. Мне кажется, что это явная дыра. – cplotts 2008-10-22 01:48:14

+0

Он не вызывает код удаления, потому что ему это не нужно. Сброс означает, что произошло нечто драматическое, вам нужно начать снова ». Яркая операция - один из примеров этого, но есть и другие. – 2010-06-03 04:42:51

3

Как работает ObservableCollection, вы можете обойти это, сохранив свой собственный список за пределами ObservableCollection (добавив в список, когда действие «Добавить», «Удалить», когда действие «Удалить» и т. Д.), Вы можете получить все удаленные элементы (или добавленные элементы), когда действие Сброс, сравнивая ваш список с ObservableCollection.

Другой вариант - создать свой собственный класс, который реализует IList и INotifyCollectionChanged, тогда вы можете присоединять и отключать события из этого класса (или устанавливать OldItems on Clear, если хотите) - это действительно не сложно, но это очень сложно печатания.

+0

Я считал отслеживание другого списка, а также предлагаю сначала, но, похоже, много ненужной работы. Ваше второе предложение очень близко к тому, что я в конечном итоге собираюсь с ..., который я выложу в качестве ответа. – cplotts 2008-10-22 13:35:28

20

У нас была такая же проблема. Действие Reset в CollectionChanged не включает OldItems. Мы имели обходной путь: вместо того, чтобы мы использовали следующий метод расширения:

public static void RemoveAll(this IList list) 
{ 
    while (list.Count > 0) 
    { 
     list.RemoveAt(list.Count - 1); 
    } 
} 

Мы кончались не поддерживает функцию Clear(), и бросание NotSupportedException в CollectionChanged случае для сброса действий. RemoveAll выведет действие «Удалить» в событии CollectionChanged с помощью соответствующих файлов OldItems.

+0

Хорошая идея. Мне не нравится не поддерживать Clear, поскольку это метод (по моему опыту), который использует большинство людей ... но по крайней мере вы предупреждаете пользователя об исключении. – cplotts 2008-10-22 13:33:26

+0

Я согласен, это не идеальное решение, но мы нашли его лучшим приемлемым решением. – decasteljau 2008-10-22 14:35:33

+0

Вы не должны использовать старые предметы! То, что вы должны делать, - это сбросить все данные, которые у вас есть в списке, и повторно просмотреть его, как если бы это был новый список! – 2010-06-03 04:40:10

3

Я решил эту один в несколько иной манере, как я хотел, чтобы зарегистрироваться на одно событие и обрабатывать все дополнения и абсорбцию в обработчик событий. Я начал отменять событие с изменением коллекции и перенаправил действия сброса на действия удаления со списком элементов. Все это пошло не так, поскольку я использовал наблюдаемую коллекцию в качестве источника элементов для представления коллекции и получил «действия диапазона не поддерживаются».

Наконец-то я создал новое событие под названием CollectionChangedRange, которое действует так, как я ожидал, что встроенная версия будет действовать.

Я не могу себе представить, почему это ограничение будет разрешено, и надеемся, что это сообщение, по крайней мере, остановит других на том, что я спустился в тупик.

/// <summary> 
/// An observable collection with support for addrange and clear 
/// </summary> 
/// <typeparam name="T"></typeparam> 
[Serializable] 
[TypeConverter(typeof(ExpandableObjectConverter))] 
public class ObservableCollectionRange<T> : ObservableCollection<T> 
{ 
    private bool _addingRange; 

    [field: NonSerialized] 
    public event NotifyCollectionChangedEventHandler CollectionChangedRange; 

    protected virtual void OnCollectionChangedRange(NotifyCollectionChangedEventArgs e) 
    { 
     if ((CollectionChangedRange == null) || _addingRange) return; 
     using (BlockReentrancy()) 
     { 
      CollectionChangedRange(this, e); 
     } 
    } 

    public void AddRange(IEnumerable<T> collection) 
    { 
     CheckReentrancy(); 
     var newItems = new List<T>(); 
     if ((collection == null) || (Items == null)) return; 
     using (var enumerator = collection.GetEnumerator()) 
     { 
      while (enumerator.MoveNext()) 
      { 
       _addingRange = true; 
       Add(enumerator.Current); 
       _addingRange = false; 
       newItems.Add(enumerator.Current); 
      } 
     } 
     OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems)); 
    } 

    protected override void ClearItems() 
    { 
     CheckReentrancy(); 
     var oldItems = new List<T>(this); 
     base.ClearItems(); 
     OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItems)); 
    } 

    protected override void InsertItem(int index, T item) 
    { 
     CheckReentrancy(); 
     base.InsertItem(index, item); 
     OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); 
    } 

    protected override void MoveItem(int oldIndex, int newIndex) 
    { 
     CheckReentrancy(); 
     var item = base[oldIndex]; 
     base.MoveItem(oldIndex, newIndex); 
     OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, newIndex, oldIndex)); 
    } 

    protected override void RemoveItem(int index) 
    { 
     CheckReentrancy(); 
     var item = base[index]; 
     base.RemoveItem(index); 
     OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); 
    } 

    protected override void SetItem(int index, T item) 
    { 
     CheckReentrancy(); 
     var oldItem = base[index]; 
     base.SetItem(index, item); 
     OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, oldItem, item, index)); 
    } 
} 

/// <summary> 
/// A read only observable collection with support for addrange and clear 
/// </summary> 
/// <typeparam name="T"></typeparam> 
[Serializable] 
[TypeConverter(typeof(ExpandableObjectConverter))] 
public class ReadOnlyObservableCollectionRange<T> : ReadOnlyObservableCollection<T> 
{ 
    [field: NonSerialized] 
    public event NotifyCollectionChangedEventHandler CollectionChangedRange; 

    public ReadOnlyObservableCollectionRange(ObservableCollectionRange<T> list) : base(list) 
    { 
     list.CollectionChangedRange += HandleCollectionChangedRange; 
    } 

    private void HandleCollectionChangedRange(object sender, NotifyCollectionChangedEventArgs e) 
    { 
     OnCollectionChangedRange(e); 
    } 

    protected virtual void OnCollectionChangedRange(NotifyCollectionChangedEventArgs args) 
    { 
     if (CollectionChangedRange != null) 
     { 
      CollectionChangedRange(this, args); 
     } 
    } 

} 
+0

Интересный подход. Спасибо, что опубликовали его. Если я когда-нибудь столкнусь с проблемами с моим собственным подходом, я думаю, что перейду к вам. – cplotts 2009-07-08 01:35:56

1

Я только пройдя через некоторые из кода построения графиков в инструментарии Silverlight и WPF и заметил, что они решили эту проблему (в каком-то подобным образом) ... и я думал, что будет идти вперед и опубликовать их решение.

В основном, они также создали производный ObservableCollection и перечеркнули ClearItems, вызвав Remove на каждом очищаемом элементе.

Вот код:

/// <summary> 
/// An observable collection that cannot be reset. When clear is called 
/// items are removed individually, giving listeners the chance to detect 
/// each remove event and perform operations such as unhooking event 
/// handlers. 
/// </summary> 
/// <typeparam name="T">The type of item in the collection.</typeparam> 
public class NoResetObservableCollection<T> : ObservableCollection<T> 
{ 
    public NoResetObservableCollection() 
    { 
    } 

    /// <summary> 
    /// Clears all items in the collection by removing them individually. 
    /// </summary> 
    protected override void ClearItems() 
    { 
     IList<T> items = new List<T>(this); 
     foreach (T item in items) 
     { 
      Remove(item); 
     } 
    } 
} 
+0

Я просто хочу отметить, что мне не нравится такой подход, как тот, который я обозначил как ответ ..., поскольку вы получаете событие NotifyCollectionChanged (с действием Remove) ... для КАЖДОГО элемента, который удаляется. – cplotts 2010-06-03 14:33:53

2

Ну, я решил пачкаются с ним сам.

Microsoft приложил много усилий, чтобы убедиться, что NotifyCollectionChangedEventArgs не имеет данных при вызове сброса. Я предполагаю, что это было решение производительности/памяти. Если вы сбросите набор из 100 000 элементов, я предполагаю, что они не хотели дублировать все эти элементы.

Но, поскольку мои коллекции никогда не имеют более 100 элементов, я не вижу проблемы с этим.

Во всяком случае я создал унаследованный класс со следующим методом:

protected override void ClearItems() 
{ 
    CheckReentrancy(); 
    List<TItem> oldItems = new List<TItem>(Items); 

    Items.Clear(); 

    OnPropertyChanged(new PropertyChangedEventArgs("Count")); 
    OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); 

    NotifyCollectionChangedEventArgs e = 
     new NotifyCollectionChangedEventArgs 
     (
      NotifyCollectionChangedAction.Reset 
     ); 

     FieldInfo field = 
      e.GetType().GetField 
      (
       "_oldItems", 
       BindingFlags.Instance | BindingFlags.NonPublic 
      ); 
     field.SetValue(e, oldItems); 

     OnCollectionChanged(e); 
    } 
+0

+1 для взлома. – 2010-02-18 08:21:02

+0

Это классно, но, вероятно, не будет работать ни в чем, кроме среды полного доверия. Размышление над частными полями требует полного доверия, верно? – Paul 2010-04-05 17:18:06

+1

Зачем вам это делать? Есть другие вещи, которые могут привести к срабатыванию эффекта Reset - только потому, что вы отключили метод clear, это не значит, что он ушел (или он должен) – 2010-06-03 04:41:43

3

Для сценария присоединения и отсоединения обработчиков событий к элементам ObservableCollection есть также решение «на стороне клиента». В коде обработки событий вы можете проверить, находится ли отправитель в ObservableCollection с помощью метода Contains. Pro: вы можете работать с любым существующим ObservableCollection. Минусы: метод Содержит работает с O (n), где n - количество элементов в ObservableCollection. Так что это решение для небольших ObservableCollections.

Другим решением «клиентской стороны» является использование обработчика событий в середине. Просто зарегистрируйте все события в обработчике событий посередине.Этот обработчик событий в свою очередь уведомляет обработчик реального события через обратный вызов или событие. Если происходит действие Reset, удалите обратный вызов или событие, создайте новый обработчик событий посередине и забудьте о старом. Этот подход также работает для больших ObservableCollections. Я использовал это для события PropertyChanged (см. Код ниже).

/// <summary> 
    /// Helper class that allows to "detach" all current Eventhandlers by setting 
    /// DelegateHandler to null. 
    /// </summary> 
    public class PropertyChangedDelegator 
    { 
     /// <summary> 
     /// Callback to the real event handling code. 
     /// </summary> 
     public PropertyChangedEventHandler DelegateHandler; 
     /// <summary> 
     /// Eventhandler that is registered by the elements. 
     /// </summary> 
     /// <param name="sender">the element that has been changed.</param> 
     /// <param name="e">the event arguments</param> 
     public void PropertyChangedHandler(Object sender, PropertyChangedEventArgs e) 
     { 
      if (DelegateHandler != null) 
      { 
       DelegateHandler(sender, e); 
      } 
      else 
      { 
       INotifyPropertyChanged s = sender as INotifyPropertyChanged; 
       if (s != null) 
        s.PropertyChanged -= PropertyChangedHandler; 
      } 
     } 
    } 
43

Оно не претендует включать старые элементы, потому что сброс не означает, что список был очищен

Это означает, что некоторые драматические вещи произошли, и стоимость разработки добавление/удаление скорее всего превысит стоимость просто повторного сканирования списка с нуля ... так вот что вы должны делать.

MSDN предлагает пример переустановки всей коллекции в качестве кандидата для сброса.

Повторить. Сброс не означает четкое, это значит Ваши предположения о списке в настоящее время не действительны. Относитесь к нему так, как будто это совершенно новый список. Ясно, что это один из примеров этого, но вполне могут быть другие.

Некоторые примеры:
У меня был список, как это с большим количеством элементов в нем, и это было привязки данных к WPF ListView для отображения на экране.
Если вы очистите список и поднимите событие .Reset, производительность будет очень мгновенной, но если вы вместо этого увеличите количество отдельных событий .Remove, производительность будет ужасной, так как WPF удаляет элементы по одному. Я также использовал .Reset в своем собственном коде, чтобы указать, что список был пересортирован, вместо того, чтобы выпускать тысячи отдельных операций Move. Как и в случае с Clear, при повышении количества отдельных событий происходит большая производительность.

+1

Я собираюсь с уважением не согласиться на этой основе. Если вы посмотрите на документацию, она заявляет: представляет динамический набор данных, который предоставляет уведомления, когда элементы становятся добавленными, удалены или когда весь список обновляется (см. Http://msdn.microsoft.com/en-us/library/ms668613 (v = VS.100) .aspx) – cplotts 2010-06-03 14:12:17

+0

Я просто хочу указать еще раз (как я это сделал в вопросе), что я знаю, что в документации нет OUTRIGHT, что вызов Clear должен уведомлять вас о том, какие элементы удаляются. .. но в этом случае мне кажется, что коллекция НЕ действительно наблюдает за изменениями. – cplotts 2010-06-03 14:17:32

2

Наблюдаемый элемент коллекции, а также интерфейс INotifyCollectionChanged четко написаны с особым учетом: создание пользовательского интерфейса и его конкретные характеристики производительности.

Когда вы хотите получать уведомления об изменениях в коллекции, вас обычно интересуют только события добавления и удаления.

Я использую следующий интерфейс:

using System; 
using System.Collections.Generic; 

/// <summary> 
/// Notifies listeners of the following situations: 
/// <list type="bullet"> 
/// <item>Elements have been added.</item> 
/// <item>Elements are about to be removed.</item> 
/// </list> 
/// </summary> 
/// <typeparam name="T">The type of elements in the collection.</typeparam> 
interface INotifyCollection<T> 
{ 
    /// <summary> 
    /// Occurs when elements have been added. 
    /// </summary> 
    event EventHandler<NotifyCollectionEventArgs<T>> Added; 

    /// <summary> 
    /// Occurs when elements are about to be removed. 
    /// </summary> 
    event EventHandler<NotifyCollectionEventArgs<T>> Removing; 
} 

/// <summary> 
/// Provides data for the NotifyCollection event. 
/// </summary> 
/// <typeparam name="T">The type of elements in the collection.</typeparam> 
public class NotifyCollectionEventArgs<T> : EventArgs 
{ 
    /// <summary> 
    /// Gets or sets the elements. 
    /// </summary> 
    /// <value>The elements.</value> 
    public IEnumerable<T> Items 
    { 
     get; 
     set; 
    } 
} 

Я также написал свою собственную перегрузку коллекции, где:

  • ClearItems поднимает Удаление
  • InsertItem повышает Добавлено
  • RemoveItem поднимает Удаление
  • SetItem повышает снятие и добавление

Конечно, AddRange также может быть добавлен.

1

Это горячая тема ... потому что, на мой взгляд, Microsoft не выполняла свою работу должным образом ... еще раз. Не поймите меня неправильно, мне нравится Microsoft, но они не идеальны!

Я прочитал большинство предыдущих комментариев. Я согласен со всеми теми, кто считает, что Microsoft не запрограммировала Clear() должным образом.

По моему мнению, по крайней мере, ему нужен аргумент, позволяющий отделить объекты от события ... но я также понимаю его влияние. Затем я придумал это предлагаемое решение.

Я надеюсь, что это сделает всех счастливыми, или, по крайней мере, большинство все ...

Эрику

using System; 
using System.Collections.Generic; 
using System.Collections.ObjectModel; 
using System.Collections.Specialized; 
using System.Reflection; 

namespace WpfUtil.Collections 
{ 
    public static class ObservableCollectionExtension 
    { 
     public static void RemoveAllOneByOne<T>(this ObservableCollection<T> obsColl) 
     { 
      foreach (T item in obsColl) 
      { 
       while (obsColl.Count > 0) 
       { 
        obsColl.RemoveAt(0); 
       } 
      } 
     } 

     public static void RemoveAll<T>(this ObservableCollection<T> obsColl) 
     { 
      if (obsColl.Count > 0) 
      { 
       List<T> removedItems = new List<T>(obsColl); 
       obsColl.Clear(); 

       NotifyCollectionChangedEventArgs e = 
        new NotifyCollectionChangedEventArgs 
        (
         NotifyCollectionChangedAction.Remove, 
         removedItems 
        ); 
       var eventInfo = 
        obsColl.GetType().GetField 
        (
         "CollectionChanged", 
         BindingFlags.Instance | BindingFlags.NonPublic 
        ); 
       if (eventInfo != null) 
       { 
        var eventMember = eventInfo.GetValue(obsColl); 
        // note: if eventMember is null 
        // nobody registered to the event, you can't call it. 
        if (eventMember != null) 
         eventMember.GetType().GetMethod("Invoke"). 
          Invoke(eventMember, new object[] { obsColl, e }); 
       } 
      } 
     } 
    } 
} 
-3

http://msdn.microsoft.com/en-us/library/system.collections.specialized.notifycollectionchangedaction(VS.95).aspx

Пожалуйста, прочитайте документацию с открытыми глазами, и ваш мозг превратился на. Microsoft все сделала правильно. Вы должны повторно сканировать свою коллекцию, когда она выдает вам уведомление о сбросе. Вы получаете уведомление о Сбросе, потому что бросать Add/Remove для каждого элемента (будучи удаленным и добавленным обратно в коллекцию) слишком дорого.

Орион Эдвардс совершенно прав (уважение, человек). При чтении документации, пожалуйста, подумайте.

1

Чтобы все было проще, почему бы вам не переопределить метод ClearItem и делать то, что вы хотите там, т.е. отсоединить элементы от события.

public class PeopleAttributeList : ObservableCollection<PeopleAttributeDto>, { 
{ 
    protected override void ClearItems() 
    { 
    Do what ever you want 
    base.ClearItems(); 
    } 

    rest of the code omitted 
} 

Простой, чистый и содержащийся в коде коллекции.

12

Другой вариант заключается в замене событие Reset с одним Remove событием, которое имеет все выровненные позиции в своей собственности OldItems следующим образом:

public class ObservableCollectionNoReset<T> : ObservableCollection<T> 
{ 
    protected override void ClearItems() 
    { 
     List<T> removed = new List<T>(this); 
     base.ClearItems(); 
     base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); 
    } 

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) 
    { 
     if (e.Action != NotifyCollectionChangedAction.Reset) 
      base.OnCollectionChanged(e); 
    } 
    // Constructors omitted 
    ... 
} 

Преимущества:

  1. Нет необходимости, чтобы подписаться к дополнительному событию (в соответствии с принятым ответом)

  2. Не генерирует событие для каждого объекта (некоторые другие предлагаемые решения ons приводит к нескольким удаленным событиям).

  3. Абонент должен только проверять NewItems & OldItems в любом случае для добавления/удаления обработчиков событий по мере необходимости.

Недостатки:

  1. Нет Сброс события

  2. Малых накладных создания копии списка (?).

  3. ???

EDIT 2012-02-23

К сожалению, когда они связаны с элементами управления на основе списка WPF, Очистка коллекции ObservableCollectionNoReset с несколькими элементами приведет к исключению «действия Range не поддерживается». Для использования с элементами управления с этим ограничением, я изменил класс ObservableCollectionNoReset на:

public class ObservableCollectionNoReset<T> : ObservableCollection<T> 
{ 
    // Some CollectionChanged listeners don't support range actions. 
    public Boolean RangeActionsSupported { get; set; } 

    protected override void ClearItems() 
    { 
     if (RangeActionsSupported) 
     { 
      List<T> removed = new List<T>(this); 
      base.ClearItems(); 
      base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); 
     } 
     else 
     { 
      while (Count > 0) 
       base.RemoveAt(Count - 1); 
     }     
    } 

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) 
    { 
     if (e.Action != NotifyCollectionChangedAction.Reset) 
      base.OnCollectionChanged(e); 
    } 

    public ObservableCollectionNoReset(Boolean rangeActionsSupported = false) 
    { 
     RangeActionsSupported = rangeActionsSupported; 
    } 

    // Additional constructors omitted. 
} 

Это не так эффективно, когда RangeActionsSupported ложно (по умолчанию), потому что один Удалить уведомление генерируется для каждого объекта в коллекции

8

Я нашел решение, которое позволяет пользователю как извлечь выгоду из эффективности добавления или удаления множества элементов одновременно, а только для запуска одного события - и удовлетворить потребности UIElements, чтобы получить аргументы события Action.Reset, пока все другим пользователям нужен список добавленных и удаленных элементов.

Это решение включает в себя переопределение события CollectionChanged. Когда мы собираемся запустить это событие, мы можем посмотреть на цель каждого зарегистрированного обработчика и определить их тип. Поскольку для классов ICollectionView требуются аргументы NotifyCollectionChangedAction.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 
} 
-4

Если ObservableCollection не становится ясно, то вы можете попробовать это ниже код. это может вам помочь:

private TestEntities context; // This is your context 

context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context 
0

У меня была такая же проблема, и это было мое решение. Кажется, это работает. Кто-нибудь видит какие-либо потенциальные проблемы с этим подходом?

// overriden so that we can call GetInvocationList 
public override event NotifyCollectionChangedEventHandler CollectionChanged; 

protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) 
{ 
    NotifyCollectionChangedEventHandler collectionChanged = CollectionChanged; 
    if (collectionChanged != null) 
    { 
     lock (collectionChanged) 
     { 
      foreach (NotifyCollectionChangedEventHandler handler in collectionChanged.GetInvocationList()) 
      { 
       try 
       { 
        handler(this, e); 
       } 
       catch (NotSupportedException ex) 
       { 
        // this will occur if this collection is used as an ItemsControl.ItemsSource 
        if (ex.Message == "Range actions are not supported.") 
        { 
         handler(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 
        } 
        else 
        { 
         throw ex; 
        } 
       } 
      } 
     } 
    } 
} 

Вот некоторые другие полезные методы в моем классе:

public void SetItems(IEnumerable<T> newItems) 
{ 
    Items.Clear(); 
    foreach (T newItem in newItems) 
    { 
     Items.Add(newItem); 
    } 
    NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 
} 

public void AddRange(IEnumerable<T> newItems) 
{ 
    int index = Count; 
    foreach (T item in newItems) 
    { 
     Items.Add(item); 
    } 
    NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(newItems), index); 
    NotifyCollectionChanged(e); 
} 

public void RemoveRange(int startingIndex, int count) 
{ 
    IList<T> oldItems = new List<T>(); 
    for (int i = 0; i < count; i++) 
    { 
     oldItems.Add(Items[startingIndex]); 
     Items.RemoveAt(startingIndex); 
    } 
    NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(oldItems), startingIndex); 
    NotifyCollectionChanged(e); 
} 

// this needs to be overridden to avoid raising a NotifyCollectionChangedEvent with NotifyCollectionChangedAction.Reset, which our other lists don't support 
new public void Clear() 
{ 
    RemoveRange(0, Count); 
} 

public void RemoveWhere(Func<T, bool> criterion) 
{ 
    List<T> removedItems = null; 
    int startingIndex = default(int); 
    int contiguousCount = default(int); 
    for (int i = 0; i < Count; i++) 
    { 
     T item = Items[i]; 
     if (criterion(item)) 
     { 
      if (removedItems == null) 
      { 
       removedItems = new List<T>(); 
       startingIndex = i; 
       contiguousCount = 0; 
      } 
      Items.RemoveAt(i); 
      removedItems.Add(item); 
      contiguousCount++; 
     } 
     else if (removedItems != null) 
     { 
      NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, startingIndex)); 
      removedItems = null; 
      i = startingIndex; 
     } 
    } 
    if (removedItems != null) 
    { 
     NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, startingIndex)); 
    } 
} 

private void NotifyCollectionChanged(NotifyCollectionChangedEventArgs e) 
{ 
    OnPropertyChanged(new PropertyChangedEventArgs("Count")); 
    OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); 
    OnCollectionChanged(e); 
} 
0

я нашел еще один «простое» решение, производное от ObservableCollection, но это не очень элегантно, потому что он использует Reflection ... Если вы как это здесь мое решение:

public class ObservableCollectionClearable<T> : ObservableCollection<T> 
{ 
    private T[] ClearingItems = null; 

    protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e) 
    { 
     switch (e.Action) 
     { 
      case System.Collections.Specialized.NotifyCollectionChangedAction.Reset: 
       if (this.ClearingItems != null) 
       { 
        ReplaceOldItems(e, this.ClearingItems); 
        this.ClearingItems = null; 
       } 
       break; 
     } 
     base.OnCollectionChanged(e); 
    } 

    protected override void ClearItems() 
    { 
     this.ClearingItems = this.ToArray(); 
     base.ClearItems(); 
    } 

    private static void ReplaceOldItems(System.Collections.Specialized.NotifyCollectionChangedEventArgs e, T[] olditems) 
    { 
     Type t = e.GetType(); 
     System.Reflection.FieldInfo foldItems = t.GetField("_oldItems", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); 
     if (foldItems != null) 
     { 
      foldItems.SetValue(e, olditems); 
     } 
    } 
} 

здесь я сохранить текущие элементы в поле массива в методе ClearItems, то я перехватывать вызов OnCollectionChanged и перезаписать е ._oldItems частное поле (через Reflections) перед запуском base.OnCollectionChanged

0

Вы можете переопределить метод ClearItems и поднять событие с помощью действия «Удалить» и «OldItems».

public class ObservableCollection<T> : System.Collections.ObjectModel.ObservableCollection<T> 
{ 
    protected override void ClearItems() 
    { 
     CheckReentrancy(); 
     var items = Items.ToList(); 
     base.ClearItems(); 
     OnPropertyChanged(new PropertyChangedEventArgs("Count")); 
     OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); 
     OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, items, -1)); 
    } 
} 

Часть System.Collections.ObjectModel.ObservableCollection<T> реализации:

public class ObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged 
{ 
    protected override void ClearItems() 
    { 
     CheckReentrancy(); 
     base.ClearItems(); 
     OnPropertyChanged(CountString); 
     OnPropertyChanged(IndexerName); 
     OnCollectionReset(); 
    } 

    private void OnPropertyChanged(string propertyName) 
    { 
     OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); 
    } 

    private void OnCollectionReset() 
    { 
     OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 
    } 

    private const string CountString = "Count"; 

    private const string IndexerName = "Item[]"; 
} 
6

Хорошо, я знаю, что это очень старый вопрос, но я пришел с хорошим решением вопроса и думал, что я хотел бы поделиться. Это решение берет вдохновение от многих больших ответов здесь, но имеет следующие преимущества:

  • Нет необходимости, чтобы создать новый класс и переопределить методы из ObservableCollection
  • Не фальсифицировать с работой NotifyCollectionChanged (так не баловаться с Reset)
  • не использовать отражения

Здесь пока код:

public static void Clear<T>(this ObservableCollection<T> collection, Action<ObservableCollection<T>> unhookAction) 
{ 
    unhookAction.Invoke(collection); 
    collection.Clear(); 
} 

Этот метод расширения просто принимает Action, который будет вызываться перед очисткой коллекции.

Смежные вопросы