При очистке ObservableCollection в 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 };
    }
}

Ответ 1

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

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

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

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();
    }
}

Ответ 2

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

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

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

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

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

Ответ 3

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

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

В итоге мы не поддерживали функцию Clear() и бросали исключение NotSupportedException в событие CollectionChanged для действий Reset. RemoveAll выведет действие "Удалить" в событии CollectionChanged с соответствующими старыми элементами.

Ответ 4

Другой вариант - заменить событие 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
    ...
}

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

  • Не нужно подписываться на дополнительное событие (в соответствии с принятым ответом)

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

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

Недостатки:

  • Нет Reset event

  • Небольшие (?) служебные данные, создающие копию списка.

  • ???

EDIT 2012-02-23

К сожалению, при привязке к элементам управления на основе списка WPF очистка коллекции ObservableCollectionNoReset с несколькими элементами приведет к исключению "Недействительные действия диапазона". Для использования с элементами управления с этим ограничением я изменил класс 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 является ложным (по умолчанию), потому что одно уведомление на удаление создается для каждого объекта в коллекции

Ответ 5

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

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

Ответ 6

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

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

Вот код:

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

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

Ответ 7

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

Наконец я создал новое событие с именем 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);
        }
    }

}

Ответ 8

Так работает ObservableCollection, вы можете обойти это, сохранив свой собственный список за пределами ObservableCollection (добавив в список, когда действие добавлено, удалите, когда действие "Удалить" и т.д.), вы можете получить все удаленные элементы ( или добавленные элементы), когда действие Reset, сравнивая ваш список с ObservableCollection.

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

Ответ 9

Для сценария прикрепления и отсоединения обработчиков событий к элементам 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;
            }   
        }
    }

Ответ 10

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

Ответ 11

Хорошо, я решил сам по себе замарать себя.

Microsoft положила много работы на то, чтобы убедиться, что NotifyCollectionChangedEventArgs не имеет никаких данных при вызове reset. Я предполагаю, что это было решение производительности/памяти. Если вы сбросите набор из 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);
    }

Ответ 12

Наблюдаемый набор, а также интерфейс 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;
    }
}

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

  • ClearItems повышает удаление
  • InsertItem повышает Добавлен
  • RemoveItem повышает удаление
  • SetItem повышает удаление и добавление

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

Ответ 13

Я просто просматривал код диаграммы в инструментариях 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);
        }
    }
}

Ответ 14

Это горячая тема... потому что, на мой взгляд, 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 });
                }
            }
        }
    }
}

Ответ 15

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

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

  rest of the code omitted
}

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

Ответ 16

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

// 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);
}

Ответ 17

Я нашел другое "простое" решение, основанное на 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 и перезаписываю частное поле e._oldItems (через Reflections) перед запуском base.OnCollectionChanged

Ответ 18

Вы можете переопределить метод 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[]";
}

Ответ 19

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

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

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

Ответ 20

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

private TestEntities context; // This is your context

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