При очистке ObservableCollection в e.OldItems нет элементов

91

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

У меня есть 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 };
    }
}
графики
источник
Зачем нужно отказываться от подписки на мероприятие? В каком направлении вы подписываетесь? События создают ссылку на подписчика, удерживаемого рейзером, а не наоборот. Если сборщики представляют собой элементы в очищаемой коллекции, они будут безопасно собраны в мусор, а ссылки исчезнут - утечки нет. Если элементы являются подписчиками и на которые ссылается один рейзер, просто установите для события значение null в рейзере, когда вы получите Reset - нет необходимости индивидуально отказываться от подписки на элементы.
Александр Дубинский
Поверьте, я знаю, как это работает. Рассматриваемое событие было на синглтоне, которое оставалось на долгое время ... таким образом, элементы в коллекции были подписчиками. Ваше решение просто установить для события значение null не работает ... поскольку событие все еще должно запускаться ... возможно, уведомляя других подписчиков (не обязательно тех, кто находится в коллекции).
график

Ответы:

46

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

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

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

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

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

Орион Эдвардс
источник
1
Я собираюсь с уважением не согласиться на этом основании. Если вы посмотрите документацию, в ней говорится: представляет динамический сбор данных, который предоставляет уведомления, когда элементы добавляются, удаляются или когда обновляется весь список (см. Msdn.microsoft.com/en-us/library/ms668613(v=VS .100) .aspx )
график
6
В документации указано, что он должен уведомлять вас, когда элементы добавляются / удаляются / обновляются, но он не обещает рассказать вам все детали элементов ... только то, что произошло событие. С этой точки зрения поведение нормальное. Лично я считаю, что они должны были просто поместить все элементы OldItemsпри очистке (это просто копирование списка), но, возможно, был какой-то сценарий, когда это было слишком дорого. Во всяком случае, если вы хотите коллекцию , которая делает уведомление о всех удаленных элементов, это не было бы трудно сделать.
Орион Эдвардс
2
Что ж, если Resetэто указать на дорогостоящую операцию, очень вероятно, что те же рассуждения применимы к копированию всего списка в OldItems.
pbalaga
7
Забавный факт: начиная с .NET 4.5 , на Resetсамом деле означает «Содержимое коллекции было очищено ». См. Msdn.microsoft.com/en-us/library/…
Athari,
9
Этот ответ не очень помогает, извините. Да, вы можете повторно сканировать весь список, если получите сброс, но у вас нет доступа для удаления элементов, которые могут потребоваться для удаления из них обработчиков событий. Это большая проблема.
Virus721
22

У нас была такая же проблема. Действие 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.

Decasteljau
источник
Хорошая идея. Мне не нравится не поддерживать Clear, поскольку это метод (по моему опыту), который использует большинство людей ... но, по крайней мере, вы предупреждаете пользователя об исключении.
график
Я согласен, это не идеальное решение, но мы сочли его наиболее подходящим решением.
decasteljau,
Вы не должны использовать старые предметы! Что вы должны сделать, так это выгрузить все данные, которые есть в списке, и повторно просканировать их, как если бы это был новый список!
Orion Edwards
16
Проблема, Орион, с вашим предложением ... это вариант использования, который вызвал этот вопрос. Что происходит, когда в списке есть элементы, от которых я хочу отделить событие? Я не могу просто выгрузить данные из списка ... это приведет к утечкам / переполнению памяти.
график
5
Основным недостатком этого решения является то, что при удалении 1000 элементов вы запускаете CollectionChanged 1000 раз, и пользовательский интерфейс должен обновлять CollectionView 1000 раз (обновление элементов пользовательского интерфейса стоит дорого). Если вы не боитесь переопределить класс ObservableCollection, вы можете сделать так, чтобы он запускал событие Clear (), но предоставлял правильные аргументы события, позволяя коду мониторинга отменить регистрацию всех удаленных элементов.
Ален
13

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

Грантнц
источник
Мне это нравится, но, к сожалению, Silverlight 4 NotifyCollectionChangedEventArgs не имеет конструктора, который принимает список элементов.
Саймон Брэнгвин,
2
Мне понравилось это решение, но оно не работает ... Вам не разрешено создавать NotifyCollectionChangedEventArgs, в котором было изменено более одного элемента, если только действие не "Сброс". Вы получаете исключение, Range actions are not supported.я не знаю, почему он это делает, но теперь это не оставляет другого выбора, кроме как удалять каждый элемент по одному ...
Ален,
2
@Alain Коллекция ObservableCollection не накладывает этого ограничения. Я подозреваю, что это элемент управления WPF, к которому вы привязали коллекцию. У меня была такая же проблема, и я так и не успел опубликовать обновление с моим решением. Я отредактирую свой ответ измененным классом, который работает при привязке к элементу управления WPF.
grantnz
Теперь я это понимаю. На самом деле я нашел очень элегантное решение, которое переопределяет событие CollectionChanged и перебирает foreach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged )If handler.Target is CollectionView, тогда вы можете запустить обработчик с помощью Action.Resetаргументов, в противном случае вы можете предоставить полные аргументы. Лучшее из обоих миров в зависимости от обработчика :). Вроде как здесь: stackoverflow.com/a/3302917/529618
Ален,
Я разместил свое собственное решение ниже. stackoverflow.com/a/9416535/529618 Огромное спасибо за ваше вдохновляющее решение. Это меня на полпути.
Ален
10

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

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

Вот код:

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

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

Смертельный объятие
источник
Очень хорошая идея. Просто, элегантно.
график
9

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

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

В коде, который использует эту 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();
    }
}
графики
источник
1
Вам нужно переименовать свой класс в BrokenObservableCollection, а не TrulyObservableCollection- вы неправильно понимаете, что означает действие сброса.
Орион Эдвардс,
1
@ Орион Эдвардс: Я не согласен. Смотрите мой комментарий к вашему ответу.
график
1
@ Орион Эдвардс: Ой, подожди, я вижу, ты смешной. Но тогда я должен действительно назвать это: ActuallyUsefulObservableCollection. :)
график
6
Лол отличное имя. Согласен, это серьезная оплошность в дизайне.
devios1
1
Если вы все равно собираетесь реализовать новый класс ObservableCollection, нет необходимости создавать новое событие, которое необходимо отслеживать отдельно. Вы можете просто запретить ClearItems запускать аргументы события Action = Reset и заменить его аргументом события Action = Remove, который содержит список e.OldItems всех элементов, которые были в списке. См. Другие решения в этом вопросе.
Ален
4

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

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

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

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

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

Для сценария присоединения и отсоединения обработчиков событий к элементам 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;
            }   
        }
    }
Крис
источник
Я считаю, что с вашим первым подходом мне понадобится другой список для отслеживания элементов ... потому что, как только вы получите событие CollectionChanged с действием Reset ... коллекция уже пуста. Я не совсем понимаю ваше второе предложение. Мне бы очень понравился простой тестовый набор, иллюстрирующий это, но для добавления, удаления и очистки ObservableCollection. Если вы создадите пример, вы можете написать мне по электронной почте на мое имя и мою фамилию на gmail.com.
график
2

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

tvanfosson
источник
6
Я тоже это видел, но мне это не нравится. Мне это кажется зияющей дырой.
график
Он не вызывает код удаления, потому что в этом нет необходимости. Сброс означает, что «произошло что-то драматическое, вам нужно начать заново». Четкая операция - один из примеров этого, но есть и другие
Орион Эдвардс
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);
    }
HaxElit
источник
Это круто, но, вероятно, не будет работать ни в чем, кроме среды полного доверия. Размышление о частных полях требует полного доверия, верно?
Пол
1
Зачем тебе это делать? Есть и другие вещи, которые могут вызвать срабатывание действия Reset - то, что вы отключили метод clear, не означает, что он исчез (или должен)
Орион Эдвардс,
Интересный подход, но размышления могут быть медленными.
график
2

Интерфейс ObservableCollection, а также интерфейс 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.

Рик Бирендонк
источник
+1 за указание на то, что Microsoft разработала ObservableCollection с учетом конкретного случая использования ... и с учетом производительности. Согласен. Оставил дыру для других ситуаций, но согласен.
график
-1 Меня может интересовать всякое. Часто мне нужен указатель добавленных / удаленных элементов. Возможно, я захочу оптимизировать замену. И т.д. Дизайн INotifyCollectionChanged хорош. Проблема, которая должна быть исправлена, отсутствует, в MS ее реализовали.
Александр Дубинский
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);
        }
    }
}
графики
источник
Я просто хочу указать, что мне не нравится этот подход так же, как тот, который я пометил как ответ ... поскольку вы получаете событие NotifyCollectionChanged (с действием Remove) ... для КАЖДОГО элемента, который удаляется.
график
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 });
                }
            }
        }
    }
}
Эрик Уэлле
источник
Я по-прежнему считаю, что Microsoft должна предоставить возможность очистки с уведомлением. Я до сих пор считаю, что они упускают шанс из-за того, что не предоставляют такой способ. К сожалению ! Я не говорю, что очистку нужно удалить, если что-то не хватает !!! Чтобы получить низкое сцепление, нам иногда нужно сообщить, что было удалено.
Эрик Уэлле
1

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

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

  rest of the code omitted
}

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

Стефан
источник
Это очень близко к тому, что я сделал на самом деле ... см. Принятый ответ.
график
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 и перезаписываю приватное поле e._oldItems (через Reflections) перед запуском base.OnCollectionChanged

Форменц
источник
0

Вы можете переопределить метод ClearItems и вызвать событие с помощью действия Remove и 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[]";
}
Артем Илларионов
источник
-4

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

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

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

Дима
источник
5
Я действительно думаю, что вы и Орион правы в своем понимании того, как Microsoft разработала его для работы. :) Этот дизайн, однако, вызвал у меня проблемы, которые мне нужно было решить в моей ситуации. Эта ситуация тоже обычная ... и почему я разместил этот вопрос.
график
Я думаю, вам стоит еще немного взглянуть на мой вопрос (и отмеченный ответ). Я не предлагал удалять все элементы.
график
И для протокола, я уважаю ответ Ориона ... Я думаю, мы просто немного повеселились друг с другом ... по крайней мере, я так воспринял это.
график
Одна важная вещь: вам не нужно отделять процедуры обработки событий от удаляемых объектов. Отсоединение происходит автоматически.
Дима
1
Таким образом, события не отключаются автоматически при удалении объекта из коллекции.
график
-4

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

private TestEntities context; // This is your context

context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context
Манас
источник