При очистке ObservableCollection в e.OldItems нет элементов - PullRequest
87 голосов
/ 22 октября 2008

У меня есть кое-что, что действительно застает меня врасплох.

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

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

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

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


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


Ниже приведен пример кода, если вы хотите воспроизвести его самостоятельно. Сначала от 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 };
    }
}

Ответы [ 20 ]

45 голосов
/ 03 июня 2010

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

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

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

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

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

21 голосов
/ 22 октября 2008

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

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

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

12 голосов
/ 21 января 2012

Другой вариант - заменить событие 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. Не генерирует событие для каждого удаленного объекта (некоторые другие предлагаемые решения приводят к нескольким удаленным событиям).

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

Недостатки:

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

  2. Небольшие (?) Накладные расходы на создание копии списка.

  3. ???

РЕДАКТИРОВАТЬ 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 имеет значение false (по умолчанию), поскольку для каждого объекта в коллекции создается одно уведомление об удалении

9 голосов
/ 23 февраля 2012

Я нашел решение, которое позволяет пользователю извлекать выгоду из эффективности добавления или удаления множества элементов одновременно, одновременно вызывая только одно событие, - и удовлетворять потребности 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
}
8 голосов
/ 10 марта 2017

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

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

Вот код:

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

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

7 голосов
/ 22 октября 2008

Хорошо, хотя я все еще хочу, чтобы 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();
    }
}
4 голосов
/ 05 июля 2009

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

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

}
3 голосов
/ 22 октября 2008

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

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

3 голосов
/ 28 мая 2010

Для сценария присоединения и отсоединения обработчиков событий к элементам ObservableCollection также существует решение «на стороне клиента». В коде обработки событий вы можете проверить, находится ли отправитель в ObservableCollection с помощью метода Contains. Pro: вы можете работать с любой существующей коллекцией ObservableCollection. Минусы: метод Contains запускается с 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;
            }   
        }
    }
2 голосов
/ 13 июля 2010

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

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

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

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.

...