Під час очищення спостережуваної колекції в 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 не працює ... оскільки подія все одно повинна запускатися ... можливо, повідомляючи інших абонентів (не обов'язково тих, хто в колекції).
cplotts

Відповіді:


46

Він не претендує на включення старих елементів, оскільки Скинути не означає, що список очищено

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

MSDN пропонує приклад того, як вся колекція пересортується як кандидат для скидання.

Повторити. Скидання не означає чітке , це означає, що ваші припущення щодо списку тепер недійсні. Ставтеся до цього так, ніби це абсолютно новий список . Очевидно, це один приклад цього, але цілком можуть бути й інші.

Кілька прикладів:
У мене був такий список із великою кількістю елементів, і він був прив’язаний до WPF ListViewдля відображення на екрані.
Якщо ви очистите список і піднімете .Resetподію, продуктивність буде майже миттєвою, але якщо ви натомість піднімете багато окремих .Removeподій, продуктивність буде жахливою, оскільки WPF видаляє елементи по одному. Я також використовував .Resetу своєму власному коді вказівку на те, що список був пересортований, а не видавав тисячі окремих Moveоперацій. Як і у випадку з Clear, є великий показник ефективності, коли піднімаються багато окремих подій.


1
Я збираюся з повагою не погодитися на цій основі. Якщо ви подивитесь на документацію, в ній зазначено: Представляє динамічний збір даних, що забезпечує сповіщення про додавання, видалення або коли оновлюється весь список (див. Msdn.microsoft.com/en-us/library/ms668613(v=VS .100) .aspx )
cplotts

6
У документах зазначено, що він повинен повідомляти вас, коли елементи додаються / видаляються / оновлюються, але він не обіцяє розповісти вам усі подробиці елементів ... лише про те, що сталася подія. З цієї точки зору поведінка прекрасна. Особисто я думаю, що їм слід було просто вкласти всі предмети під OldItemsчас очищення (це просто копіювання списку), але, можливо, був якийсь сценарій, коли це було занадто дорого. У всякому разі, якщо ви хочете колекцію , яка робить повідомлення про всіх віддалених елементів, це не було б важко зробити.
Orion Edwards

2
Ну, якщо Resetпотрібно вказати на дорогу операцію, дуже ймовірно, що ті самі міркування стосуються і копіювання всього списку в OldItems.
pbalaga

7
Кумедний факт: з .NET 4.5 , на Resetсамому ділі означає «Зміст збірника був очищений .» Див. Msdn.microsoft.com/en-us/library/…
Атхарі

9
Ця відповідь не дуже допомогла, вибачте. Так, ви можете пересканувати весь список, якщо отримаєте Скидання, але у вас немає доступу до видалення елементів, які вам можуть знадобитися, щоб видалити з них обробники подій. Це велика проблема.
Virus721

22

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

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

Врешті-решт, ми не підтримали функцію Clear () і викинули NotSupportedException у подію CollectionChanged для дій Reset. RemoveAll ініціює дію видалення в події CollectionChanged з відповідними OldItems.


Гарна ідея. Мені не подобається не підтримувати Clear, оскільки це метод (на мій досвід), який використовують більшість людей ... але принаймні ви попереджаєте користувача за винятком.
cplotts

Я згоден, це не ідеальне рішення, але ми виявили, що це найкраще прийнятне рішення.
decasteljau

Ви не повинні використовувати старі предмети! Що ви повинні зробити, це скинути всі дані, які є у вас у списку, і повторно відсканувати їх, ніби це новий список!
Оріон Едвардс

16
Проблема, Оріоне, з вашою пропозицією ... - це варіант використання, який спричинив це запитання. Що трапляється, коли в списку є елементи, від яких я хочу відокремити подію? Я не можу просто скинути дані зі списку ... це призведе до витоків пам'яті / тиску.
cplotts

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

РЕДАГУВАТИ 23.02.2012

На жаль, при зв’язку з елементами керування на основі списку 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 The ObservableCollection не накладає цього обмеження. Я підозрюю, що це елемент керування WPF, до якого ви прив’язали колекцію. У мене була та сама проблема, і я ніколи не міг публікувати оновлення зі своїм рішенням. Я відредагую свою відповідь із модифікованим класом, який працює, коли прив’язаний до елемента керування WPF.
grantnz

Я це бачу зараз. Я насправді знайшов дуже елегантне рішення, яке перекриває подію CollectionChanged і перемикається на foreach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged )If handler.Target is CollectionView, тоді ви можете запустити обробник за допомогою Action.Resetargs, інакше ви можете надати всі аргументи. Найкраще з обох світів на базі обробника :). Начебто як тут: stackoverflow.com/a/3302917/529618
Ален

Я розмістив своє власне рішення нижче. stackoverflow.com/a/9416535/529618 Величезне спасибі вам за ваше надихаюче рішення. Це привело мене до половини шляху.
Ален

10

Гаразд, я знаю, що це дуже давнє запитання, але я придумав хороше рішення проблеми і думав, що поділюсь цим. Це рішення черпає натхнення з багатьох чудових відповідей тут, але має наступні переваги:

  • Не потрібно створювати новий клас і перевизначати методи з ObservableCollection
  • Не втручається в роботу NotifyCollectionChanged (тому не потрібно возитися зі Скиданням)
  • Не використовує рефлексію

Ось код:

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

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


Дуже приємна ідея. Просто, елегантно.
cplotts

9

Я знайшов рішення, яке дозволяє користувачеві одночасно скористатися ефективністю додавання або видалення багатьох елементів одночасно, запускаючи лише одну подію, - і задовольнити потреби UIElements для отримання Action. Скинути аргументи події, тоді як всі інші користувачі як список елементів, доданих та видалених.

Це рішення передбачає перевизначення події CollectionChanged. Коли ми запускаємо цю подію, ми можемо фактично переглянути ціль кожного зареєстрованого обробника та визначити їх тип. Оскільки для цього потрібні лише класи ICollectionViewNotifyCollectionChangedAction.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, я використовую цю подію очищення, щоб прокрутити елементи , які все ще перебувають у колекції на той момент. щоб від'єднати подію, від якої я хотів від'єднатися.

Сподіваюся, такий підхід допомагає і комусь іншому.

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
@ Оріон Едвардс: Я не згоден. Дивіться мій коментар до вашої відповіді.
cplotts

1
@ Оріон Едвардс: О, почекай, я бачу, ти забавна. Але тоді я повинен дійсно назвати це: ActuallyUsefulObservableCollection. :)
cplotts

6
Лол велике ім'я. Я згоден, що це серйозний нагляд за дизайном.
devios1

1
Якщо ви все одно збираєтесь реалізувати новий клас ObservableCollection, немає необхідності створювати нову подію, за якою потрібно стежити окремо. Ви можете просто не допустити, щоб ClearItems запускали аргументи події Action = Reset і замінювали їх на Action = Remove args події, що містить список 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);
        }
    }

}

Цікавий підхід. Дякуємо за розміщення. Якщо я коли-небудь зіткнуся з проблемами з власним підходом, я думаю, що я перегляну ваш.
cplotts

3

Ось як працює ObservableCollection, ви можете обійти це, зберігаючи свій власний список поза ObservableCollection (додавання до списку, коли дія - Додати, видалити, коли дія - Видалити тощо), тоді Ви зможете отримати всі видалені елементи (або додані елементи ), коли дія Скидається, порівнявши ваш список із ObservableCollection.

Інший варіант - створити власний клас, який реалізує IList та INotifyCollectionChanged, тоді ви можете приєднувати та від'єднувати події всередині цього класу (або встановлювати OldItems на Clear, якщо хочете) - це насправді не складно, але багато введення тексту.


Я розглядав можливість відстеження іншого списку так само, як ви пропонуєте спочатку, але це, здається, багато непотрібної роботи. Ваша друга пропозиція дуже близька до того, з чим я в кінцевому підсумку пішов ... і який я розміщу як відповідь.
cplotts

3

Для сценарію приєднання та від'єднання обробників подій до елементів ObservableCollection існує також рішення "на стороні клієнта". У коді обробки подій ви можете перевірити, чи перебуває відправник в ObservableCollection, використовуючи метод Contains. Про: ви можете працювати з будь-яким існуючим ObservableCollection. Мінуси: метод Contains працює з O (n), де n - кількість елементів у ObservableCollection. Отже, це рішення для невеликих ObservableCollections.

Іншим рішенням "на стороні клієнта" є використання обробника подій посередині. Просто зареєструйте всі події в обробнику подій посередині. Цей обробник подій, у свою чергу, повідомляє реальний обробник подій через зворотний виклик або подію. Якщо відбувається дія скидання, видаліть зворотний виклик або подію, створіть новий обробник подій посередині і забудьте про старий. Цей підхід також працює для великих 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 за допомогою дії Скинути ... колекція вже порожня. Я не зовсім слідую вашій другій пропозиції. Я хотів би простий тестовий джгут, який це ілюструє, але для додавання, видалення та очищення ObservableCollection. Якщо ви створюєте приклад, ви можете надіслати мені електронного листа на моє ім’я, а потім моє прізвище на gmail.com.
cplotts

2

Переглядаючи NotifyCollectionChangedEventArgs , виявляється, що OldItems містить лише елементи, змінені в результаті дії Замінити, Вилучити або Перемістити. Це не означає, що він буде містити що-небудь на Clear. Я підозрюю, що Clear запускає подію, але не реєструє вилучені елементи та взагалі не викликає код видалення.


6
Я теж це бачив, але мені це не подобається. Мені це здається зяючою дірою.
cplotts

Він не викликає код видалення, тому що це не потрібно. Скидання означає "щось драматичне сталося, потрібно починати спочатку". Чітка операція - один із прикладів цього, але є й інші
Оріон Едвардс

2

Ну, я вирішив сам із цим забруднитися.

Корпорація Майкрософт багато працювала, завжди переконуючись, що 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);
    }

Це круто, але, мабуть, це не спрацювало б ні в чому, окрім повноцінного середовища. Роздуми над приватними полями вимагають повної довіри, так?
Пол

1
Чому ви це робите? Є й інші речі, які можуть спричинити запуск дії «Скидання» - те, що ви вимкнули метод clear, не означає, що він зник (або що повинен)
Orion Edwards

Цікавий підхід, але роздуми можуть бути повільними.
cplotts

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

Я також написав власну перевантаження колекції, де:

  • ClearItems піднімає Видалення
  • InsertItem піднімає додано
  • RemoveItem піднімається Видалення
  • SetItem піднімає Видалення та Додавання

Звичайно, AddRange також можна додати.


+1 за вказівку на те, що Microsoft розробила ObservableCollection з урахуванням конкретного випадку використання ... та з огляду на продуктивність. Я згоден. Залишив діру для інших ситуацій, але я згоден.
cplotts

-1 Мене можуть цікавити всілякі речі. Часто мені потрібен індекс доданих / видалених елементів. Можливо, я хочу оптимізувати заміну. І т. Д. Дизайн INotifyCollectionChanged хороший. Проблема, яку слід виправити, полягає в тому, що ніхто з MS не реалізував її.
Олександр Дубінський

1

Я просто переглядав деякі графічні коди в наборах інструментів Silverlight та WPF і помітив, що вони також вирішили цю проблему (подібним чином) ... і я подумав, що піду далі і опублікую їх рішення.

По суті, вони також створили похідну ObservableCollection і замінили ClearItems, викликаючи Видалити для кожного очищуваного елемента.

Ось код:

/// <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 (з дією Видалити) ... для КОЖНОГО видаленого елемента.
cplotts

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

Я все ще вважаю, що корпорація Майкрософт повинна забезпечити спосіб очищення за допомогою сповіщення. Я все ще думаю, що вони пропускають постріл, не надаючи такого шляху. Вибачте! Я не кажу, що слід видалити чисте, якщо там щось не вистачає !!! Щоб отримати низьке зчеплення, нам іноді доводиться повідомляти про те, що було знято.
Ерік Уелле

1

Для спрощення, чому б вам не перевизначити метод ClearItem і не робити там все, що завгодно, тобто від'єднати елементи від події.

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

  rest of the code omitted
}

Простий, чистий і вміщений у коді колекції.


Це дуже близько до того, що я зробив насправді ... див. Прийняту відповідь.
cplotts

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 action та 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 спроектувала його для роботи. :) Однак цей дизайн викликав у мене проблеми, якими мені потрібно було обійти свою ситуацію. Ця ситуація теж поширена ... і чому я розмістив це запитання.
cplotts

Думаю, вам слід ще трохи поглянути на моє запитання (та позначену відповідь). Я не пропонував видалити кожен предмет.
cplotts

І для протоколу, я поважаю відповідь Оріона ... Я думаю, ми просто трохи розважились одне з одним ... принаймні я так сприйняв це.
cplotts

Одна важлива річ: вам не доведеться від’єднувати процедури обробки подій від об’єктів, які ви видаляєте. Від'єднання здійснюється автоматично.
Діма

1
Отже, підсумовуючи, події не від’єднуються автоматично при вилученні об’єкта з колекції.
cplotts

-4

Якщо ваш стан ObservableCollectionне стає зрозумілим, ви можете спробувати цей код нижче. це може вам допомогти:

private TestEntities context; // This is your context

context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.