Podczas czyszczenia kolekcji ObservableCollection w e.OldItems nie ma żadnych elementów

91

Mam tutaj coś, co naprawdę mnie zaskakuje.

Mam ObservableCollection z T, który jest wypełniony elementami. Mam również program obsługi zdarzeń dołączony do zdarzenia CollectionChanged.

Kiedy Usunąć kolekcję powoduje zdarzenie CollectionChanged z e.Action zestaw do NotifyCollectionChangedAction.Reset. Ok, to normalne. Ale dziwne jest to, że ani e.OldItems, ani e.NewItems nie mają w tym nic. Spodziewałbym się, że e.OldItems zostanie wypełnione wszystkimi elementami, które zostały usunięte z kolekcji.

Czy ktoś jeszcze to widział? A jeśli tak, jak sobie z tym poradzili?

Trochę tła: używam zdarzenia CollectionChanged do dołączania i odłączania się od innego zdarzenia, a zatem jeśli nie otrzymam żadnych elementów w e.OldItems ... nie będę mógł odłączyć się od tego zdarzenia.


WYJAŚNIENIE: Wiem, że dokumentacja nie stwierdza wprost , że ma się tak zachowywać. Ale w przypadku każdego innego działania informuje mnie o tym, co zrobiło. Więc przypuszczam, że powie mi ... także w przypadku Clear / Reset.


Poniżej znajduje się przykładowy kod, jeśli chcesz go odtworzyć samodzielnie. Po pierwsze 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>

Następnie kod za:

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 };
    }
}
cplotts
źródło
Dlaczego musisz zrezygnować z subskrypcji wydarzenia? W jakim kierunku subskrybujesz? Zdarzenia tworzą odniesienie do abonenta posiadanego przez osobę podnoszącą, a nie na odwrót. Jeśli podbijacze są pozycjami w kolekcji, która zostanie wyczyszczona, zostaną bezpiecznie zebrane jako śmieci, a odniesienia znikną - bez wycieku. Jeśli pozycje są subskrybentami, do których odwołuje się jeden z podwyżek, po uzyskaniu resetu po prostu ustaw zdarzenie na wartość zerową w podbierającym - nie ma potrzeby indywidualnego anulowania subskrypcji.
Aleksandr Dubinsky
Uwierz mi, wiem, jak to działa. Wydarzenie, o którym mowa, miało miejsce na singletonie, który tkwił w pobliżu przez długi czas ... więc pozycje w kolekcji były subskrybentami. Twoje rozwiązanie polegające na ustawieniu zdarzenia na wartość null nie działa ... ponieważ zdarzenie nadal musi zostać uruchomione ... prawdopodobnie powiadamiając innych subskrybentów (niekoniecznie tych w kolekcji).
cplotts

Odpowiedzi:

46

Nie twierdzi, że zawiera stare elementy, ponieważ Reset nie oznacza, że ​​lista została wyczyszczona

Oznacza to, że wydarzyło się coś dramatycznego, a koszt opracowania dodawania / usuwania najprawdopodobniej przewyższyłby koszt ponownego przeskanowania listy od zera ... więc to właśnie powinieneś zrobić.

MSDN sugeruje przykład ponownego sortowania całej kolekcji jako kandydata do zresetowania.

Powtarzać. Reset nie oznacza jasności , oznacza to, że Twoje założenia dotyczące listy są teraz nieważne. Traktuj to tak, jakby to była zupełnie nowa lista . Zdarza się, że jest jeden taki przypadek, ale mogą istnieć inne.

Kilka przykładów:
Miałem taką listę z wieloma elementami i została ona umieszczona w WPF w ListViewcelu wyświetlenia na ekranie.
Jeśli wyczyścisz listę i podniesiesz .Resetzdarzenie, wydajność jest prawie natychmiastowa, ale jeśli zamiast tego zgłosisz wiele pojedynczych .Removezdarzeń, wydajność jest straszna, ponieważ WPF usuwa elementy jeden po drugim. Użyłem również .Resetw moim własnym kodzie, aby wskazać, że lista została ponownie posortowana, zamiast wykonywać tysiące pojedynczych Moveoperacji. Podobnie jak w przypadku Clear, przy podbijaniu wielu indywidualnych wydarzeń występuje duży spadek wydajności.

Orion Edwards
źródło
1
Na tej podstawie z pełnym szacunkiem się nie zgodzę. Jeśli spojrzysz na dokumentację, która zawiera: Reprezentuje dynamiczną kolekcję danych, która zapewnia powiadomienia, gdy elementy zostaną dodane, usunięte lub gdy cała lista zostanie odświeżona (patrz msdn.microsoft.com/en-us/library/ms668613(v=VS .100) .aspx )
cplotts
6
Dokumenty stwierdzają, że powinien powiadomić Cię, gdy elementy zostaną dodane / usunięte / odświeżone, ale nie obiecuje podać wszystkich szczegółów elementów ... tylko, że zdarzenie miało miejsce. Z tego punktu widzenia zachowanie jest w porządku. Osobiście uważam, że powinni po prostu umieścić wszystkie elementy OldItemspodczas czyszczenia (to tylko kopiowanie listy), ale być może był jakiś scenariusz, w którym było to zbyt drogie. W każdym razie, jeśli chcesz mieć kolekcję, która to robi informowała o wszystkich usuniętych elementów, to nie byłoby trudne do zrobienia.
Orion Edwards,
2
Więc jeśli Reset chcesz wskazać kosztowną operację, jest bardzo prawdopodobne, że to samo dotyczy kopiowania całej listy do OldItems.
pbalaga
7
Ciekawostka: od .NET 4.5 , Resetw rzeczywistości oznacza „Zawartość kolekcji został wyczyszczone ”. Zobacz msdn.microsoft.com/en-us/library/…
Athari
9
Ta odpowiedź niewiele pomaga, przepraszam. Tak, możesz ponownie przeskanować całą listę, jeśli otrzymasz reset, ale nie masz dostępu do usuwania elementów, co może być konieczne do usunięcia z nich programów obsługi zdarzeń. To jest duży problem.
Virus721
22

Tutaj mieliśmy ten sam problem. Akcja Reset w CollectionChanged nie obejmuje OldItems. Mieliśmy obejście: zamiast tego użyliśmy następującej metody rozszerzenia:

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

Skończyło się na tym, że nie wspieraliśmy funkcji Clear () i zgłosiliśmy NotSupportedException w zdarzeniu CollectionChanged dla akcji Reset. RemoveAll wyzwoli akcję Remove w zdarzeniu CollectionChanged z odpowiednimi OldItems.

decasteljau
źródło
Dobry pomysł. Nie lubię nie wspierać Clear, ponieważ jest to metoda (z mojego doświadczenia) używana przez większość ludzi ... ale przynajmniej ostrzegasz użytkownika o wyjątku.
cplotts
Zgadzam się, nie jest to idealne rozwiązanie, ale uznaliśmy, że jest to najlepsze dopuszczalne obejście.
decasteljau
Nie powinieneś używać starych przedmiotów! To, co powinieneś zrobić, to zrzucić wszystkie dane, które masz na liście, i ponownie przeskanować je, jakby to była nowa lista!
Orion Edwards
16
Problem, Orion, z twoją sugestią ... to przypadek użycia, który wywołał to pytanie. Co się dzieje, gdy na liście są elementy, od których chcę odłączyć wydarzenie? Nie mogę po prostu zrzucić danych z listy ... spowodowałoby to wycieki / ciśnienie pamięci.
cplotts
5
Główną wadą tego rozwiązania jest to, że jeśli usuniesz 1000 elementów, uruchomisz CollectionChanged 1000 razy, a interfejs użytkownika musi zaktualizować CollectionView 1000 razy (aktualizowanie elementów interfejsu użytkownika jest kosztowne). Jeśli nie boisz się przesłonić klasy ObservableCollection, możesz to zrobić tak, aby wyzwalała zdarzenie Clear (), ale zapewniała poprawne argumenty zdarzenia, umożliwiające monitorowanie kodu w celu wyrejestrowania wszystkich usuniętych elementów.
Alain
13

Inną opcją jest zastąpienie zdarzenia Reset pojedynczym zdarzeniem Remove, które ma wszystkie wyczyszczone elementy we właściwości OldItems w następujący sposób:

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

Zalety:

  1. Nie ma potrzeby zapisywania się na dodatkowe wydarzenie (zgodnie z zaakceptowaną odpowiedzią)

  2. Nie generuje zdarzenia dla każdego usuniętego obiektu (niektóre inne proponowane rozwiązania powodują wiele zdarzeń Usunięto).

  3. Abonent musi tylko sprawdzić NewItems i OldItems w każdym zdarzeniu, aby dodać / usunąć programy obsługi zdarzeń zgodnie z wymaganiami.

Niedogodności:

  1. Brak wydarzenia resetowania

  2. Mały (?) Narzut tworzenia kopii listy.

  3. ???

EDYCJA 2012-02-23

Niestety, po powiązaniu z kontrolkami opartymi na liście WPF wyczyszczenie kolekcji ObservableCollectionNoReset z wieloma elementami spowoduje wyjątek „Akcje zakresu nie są obsługiwane”. Aby używać kontrolek z tym ograniczeniem, zmieniłem klasę ObservableCollectionNoReset na:

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

Nie jest to tak wydajne, gdy RangeActionsSupported ma wartość false (wartość domyślna), ponieważ dla każdego obiektu w kolekcji jest generowane jedno powiadomienie o usunięciu

grantnz
źródło
Podoba mi się to, ale niestety Silverlight 4 NotifyCollectionChangedEventArgs nie ma konstruktora, który pobiera listę elementów.
Simon Brangwin
2
Podobało mi się to rozwiązanie, ale nie działa ... Nie możesz zgłosić NotifyCollectionChangedEventArgs, w którym zmieniono więcej niż jeden element, chyba że akcją jest „Reset”. Dostajesz wyjątek Range actions are not supported., nie wiem, dlaczego to robi, ale teraz nie pozostawia to nic innego, jak tylko usunąć każdą pozycję pojedynczo ...
Alain
2
@Alain The ObservableCollection nie narzuca tego ograniczenia. Podejrzewam, że jest to formant WPF, z którym powiązałeś kolekcję. Miałem ten sam problem i nigdy nie udało mi się opublikować aktualizacji z moim rozwiązaniem. Edytuję moją odpowiedź za pomocą zmodyfikowanej klasy, która działa po powiązaniu z kontrolką WPF.
grantnz
Teraz to rozumiem. Właściwie znalazłem bardzo eleganckie rozwiązanie, które przesłania zdarzenie CollectionChanged i zapętla foreach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged )If handler.Target is CollectionView, a następnie możesz odpalić procedurę obsługi za pomocą Action.Resetargumentów, w przeciwnym razie możesz podać pełne argumenty. Najlepsze z obu światów na zasadzie przewodnika po przewodniku :). Coś jak to, co tutaj: stackoverflow.com/a/3302917/529618
Alain,
Poniżej zamieściłem własne rozwiązanie. stackoverflow.com/a/9416535/529618 Ogromne podziękowania za inspirujące rozwiązanie. Doprowadziło mnie to do połowy.
Alain
10

OK, wiem, że to bardzo stare pytanie, ale znalazłem dobre rozwiązanie tego problemu i pomyślałem, że się nim podzielę. To rozwiązanie czerpie inspirację z wielu świetnych odpowiedzi tutaj, ale ma następujące zalety:

  • Nie ma potrzeby tworzenia nowej klasy i zastępowania metod z ObservableCollection
  • Nie ingeruje w działanie NotifyCollectionChanged (więc nie ma problemów z resetowaniem)
  • Nie wykorzystuje refleksji

Oto kod:

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

Ta metoda rozszerzenia po prostu przyjmuje metodę, Actionktóra zostanie wywołana przed wyczyszczeniem kolekcji.

DeadlyEmbrace
źródło
Bardzo fajny pomysł. Prosty, elegancki.
cplotts
9

Znalazłem rozwiązanie, które pozwala użytkownikowi zarówno wykorzystać efektywność dodawania lub usuwania wielu elementów jednocześnie, przy jednoczesnym uruchamianiu tylko jednego zdarzenia - i zaspokajać potrzeby UIElements, aby uzyskać Action.Reset event args, podczas gdy wszyscy inni użytkownicy będą jak lista elementów dodanych i usuniętych.

To rozwiązanie obejmuje przesłanianie zdarzenia CollectionChanged. Kiedy idziemy uruchomić to zdarzenie, możemy faktycznie spojrzeć na cel każdego zarejestrowanego modułu obsługi i określić jego typ. Ponieważ tylko klasy ICollectionView wymagają NotifyCollectionChangedAction.Resetargumentów, gdy zmienia się więcej niż jeden element, możemy je wyróżnić i przekazać wszystkim innym odpowiednie argumenty zdarzeń, które zawierają pełną listę elementów usuniętych lub dodanych. Poniżej znajduje się realizacja.

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
}
Alain
źródło
7

Ok, chociaż nadal chciałbym, aby ObservableCollection zachowywał się tak, jak chciałem ... poniższy kod jest tym, co zrobiłem. Zasadniczo utworzyłem nową kolekcję T o nazwie TrulyObservableCollection i zastąpiłem metodę ClearItems, której następnie użyłem do zgłoszenia zdarzenia Clearing.

W kodzie, który używa tego TrulyObservableCollection, używam tego zdarzenia Clearing do przechodzenia w pętlę przez elementy , które nadal znajdują się w kolekcji w tym momencie, aby wykonać odłączenie w zdarzeniu, od którego chciałem się odłączyć.

Mam nadzieję, że takie podejście pomoże również komuś innemu.

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();
    }
}
cplotts
źródło
1
Musisz zmienić nazwę klasy na BrokenObservableCollection, nie TrulyObservableCollection- nie rozumiesz, co oznacza resetowanie.
Orion Edwards
1
@Orion Edwards: Nie zgadzam się. Zobacz mój komentarz do twojej odpowiedzi.
cplotts
1
@Orion Edwards: Och, czekaj, widzę, jesteś zabawny. Ale wtedy mam naprawdę nazywają to: ActuallyUsefulObservableCollection. :)
cplotts
6
Lol świetne imię. Zgadzam się, że jest to poważne niedopatrzenie w projekcie.
devios1
1
Jeśli mimo wszystko zamierzasz zaimplementować nową klasę ObservableCollection, nie ma potrzeby tworzenia nowego zdarzenia, które musi być oddzielnie monitorowane. Możesz po prostu uniemożliwić ClearItems wyzwalanie argumentów zdarzenia Action = Reset i zastąpić je argumentami zdarzenia Action = Remove, które zawierają listę e.OldItems wszystkich elementów, które znajdowały się na liście. Zobacz inne rozwiązania w tym pytaniu.
Alain
4

Zajmowałem się tym w nieco inny sposób, ponieważ chciałem zarejestrować się do jednego zdarzenia i obsłużyć wszystkie dodatki i usunięcia w programie obsługi zdarzeń. Zacząłem od nadpisywania zdarzenia zmiany kolekcji i przekierowywania akcji resetowania na akcje usuwania z listą elementów. Wszystko poszło nie tak, ponieważ używałem obserwowalnej kolekcji jako źródła elementów dla widoku kolekcji i otrzymałem komunikat „Zakres działań nie jest obsługiwany”.

W końcu stworzyłem nowe zdarzenie o nazwie CollectionChangedRange, które działa w sposób, którego oczekiwałem od wbudowanej wersji.

Nie mogę sobie wyobrazić, dlaczego to ograniczenie byłoby dozwolone i mam nadzieję, że ten post przynajmniej powstrzyma innych przed pójściem w ślepą uliczkę, którą zrobiłem.

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

}

źródło
Ciekawe podejście. Dzięki za wysłanie. Jeśli kiedykolwiek napotkam problemy z własnym podejściem, myślę, że ponownie odwiedzę twoje.
cplotts
3

W ten sposób działa ObservableCollection, możesz obejść ten problem, utrzymując własną listę poza ObservableCollection (dodając do listy, gdy akcja jest Dodaj, usuń, gdy akcja jest Usuń itp.), A następnie możesz pobrać wszystkie usunięte elementy (lub dodane elementy ) gdy akcja jest resetowana, porównując swoją listę z ObservableCollection.

Inną opcją jest utworzenie własnej klasy, która implementuje IList i INotifyCollectionChanged, a następnie możesz dołączać i odłączać zdarzenia z tej klasy (lub ustawić OldItems na Clear, jeśli chcesz) - to naprawdę nie jest trudne, ale wymaga dużo pisania.

Nir
źródło
Rozważałem śledzenie innej listy, tak dobrze, jak sugerujesz najpierw, ale wydaje mi się, że jest to dużo niepotrzebnej pracy. Twoja druga sugestia jest bardzo zbliżona do tego, z czym się skończyłem ... którą opublikuję jako odpowiedź.
cplotts
3

W przypadku scenariusza dołączania i odłączania programów obsługi zdarzeń do elementów ObservableCollection istnieje również rozwiązanie „po stronie klienta”. W kodzie obsługi zdarzenia możesz sprawdzić, czy nadawca znajduje się w ObservableCollection przy pomocy metody Contains. Pro: możesz pracować z dowolną istniejącą ObservableCollection. Wady: metoda Contains działa z O (n), gdzie n jest liczbą elementów w ObservableCollection. Więc to jest rozwiązanie dla małych ObservableCollections.

Innym rozwiązaniem po stronie klienta jest użycie procedury obsługi zdarzeń w środku. Po prostu zarejestruj wszystkie zdarzenia w module obsługi zdarzeń w środku. Ta procedura obsługi zdarzeń z kolei powiadamia rzeczywistą procedurę obsługi zdarzeń poprzez wywołanie zwrotne lub zdarzenie. Jeśli wystąpi akcja Reset, usuń wywołanie zwrotne lub zdarzenie, utwórz nową procedurę obsługi zdarzeń w środku i zapomnij o starej. To podejście działa również w przypadku dużych ObservableCollections. Użyłem tego dla zdarzenia PropertyChanged (patrz kod poniżej).

    /// <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;
            }   
        }
    }
Chris
źródło
Uważam, że przy twoim pierwszym podejściu potrzebowałbym kolejnej listy do śledzenia elementów ... ponieważ po otrzymaniu zdarzenia CollectionChanged z akcją Reset ... kolekcja jest już pusta. Nie do końca rozumiem twoją drugą sugestię. Bardzo chciałbym, aby ilustrująca to prostą wiązkę testową, ale do dodawania, usuwania i czyszczenia ObservableCollection. Jeśli utworzysz przykład, możesz wysłać mi e-maila na moje imię, a następnie moje nazwisko na gmail.com.
cplotts
2

Patrząc na NotifyCollectionChangedEventArgs , wydaje się, że OldItems zawiera tylko elementy zmienione w wyniku akcji Zamień, Usuń lub Przenieś. Nie oznacza to, że będzie zawierał cokolwiek w Clear. Podejrzewam, że Clear uruchamia zdarzenie, ale nie rejestruje usuniętych elementów i w ogóle nie wywołuje kodu Usuń.

tvanfosson
źródło
6
Też to widziałem, ale mi się to nie podoba. Wydaje mi się, że to ziejąca dziura.
cplotts
Nie wywołuje kodu usuwania, ponieważ nie musi. Reset oznacza „wydarzyło się coś dramatycznego, musisz zacząć od nowa”. Wyraźna operacja jest jednym z przykładów, ale są też inne
Orion Edwards
2

Cóż, sam postanowiłem się tym ubrudzić.

Firma Microsoft włożyła dużo pracy, aby zawsze upewnić się, że NotifyCollectionChangedEventArgs nie ma żadnych danych podczas wywoływania resetowania. Zakładam, że była to decyzja dotycząca wydajności / pamięci. Jeśli resetujesz kolekcję zawierającą 100 000 elementów, zakładam, że nie chcieli powielać wszystkich tych elementów.

Ale ponieważ moje kolekcje nigdy nie mają więcej niż 100 elementów, nie widzę z tym problemu.

W każdym razie stworzyłem dziedziczoną klasę za pomocą następującej metody:

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
źródło
To jest fajne, ale prawdopodobnie nie działałoby w niczym innym, jak tylko w środowisku pełnego zaufania. Refleksja nad polami prywatnymi wymaga pełnego zaufania, prawda?
Paul,
1
Dlaczego miałbyś to zrobić? Są inne rzeczy, które mogą spowodować uruchomienie akcji Reset - tylko dlatego, że wyłączyłeś metodę czyszczenia, nie oznacza, że ​​zniknęła (lub że powinna)
Orion Edwards,
Ciekawe podejście, ale refleksja może być powolna.
cplotts
2

ObservableCollection, a także INotifyCollectionChanged interfejs są wyraźnie napisane z myślą o konkretnym zastosowaniu: budowaniu interfejsu użytkownika i jego specyficznych cechach wydajności.

Jeśli chcesz otrzymywać powiadomienia o zmianach kolekcji, zazwyczaj interesuje Cię tylko dodawanie i usuwanie wydarzeń.

Używam następującego interfejsu:

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

Napisałem również własne przeciążenie kolekcji, w którym:

  • ClearItems podnosi usuwanie
  • InsertItem podnosi Added
  • RemoveItem podnosi Removing
  • SetItem podnosi usuwanie i dodawanie

Oczywiście można również dodać AddRange.

Rick Beerendonk
źródło
+1 za wskazanie, że Microsoft zaprojektował ObservableCollection z myślą o konkretnym przypadku użycia ... i mając na uwadze wydajność. Zgadzam się. Zostawiłem dziurę na inne sytuacje, ale zgadzam się.
cplotts
-1 Mogę być zainteresowany różnymi rzeczami. Często potrzebuję indeksu dodanych / usuniętych elementów. Może zechcę zoptymalizować wymianę. Itd. Projekt INotifyCollectionChanged jest dobry. Problem, który powinien zostać naprawiony, to nikt w MS go nie wdrożył.
Aleksandr Dubinsky
1

Właśnie przeglądałem część kodu wykresów w zestawach narzędzi Silverlight i WPF i zauważyłem, że rozwiązały one również ten problem (w podobny sposób) ... i pomyślałem, że pójdę dalej i opublikuję ich rozwiązanie.

Zasadniczo utworzyli również pochodną ObservableCollection i zastąpili ClearItems, wywołując Remove dla każdego wyczyszczonego elementu.

Oto kod:

/// <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);
        }
    }
}
cplotts
źródło
Chcę tylko zaznaczyć, że nie podoba mi się to podejście tak bardzo, jak to, które oznaczyłem jako odpowiedź ... ponieważ otrzymujesz zdarzenie NotifyCollectionChanged (z akcją Usuń) ... dla KAŻDEGO usuwanego elementu.
cplotts
1

To gorący temat ... ponieważ moim zdaniem Microsoft nie wykonał poprawnie swojej pracy ... po raz kolejny. Nie zrozum mnie źle, lubię Microsoft, ale nie są one doskonałe!

Przeczytałem większość poprzednich komentarzy. Zgadzam się ze wszystkimi, którzy uważają, że Microsoft nie zaprogramował poprawnie Clear ().

Moim zdaniem przynajmniej potrzebny jest argument, aby można było oderwać przedmioty od zdarzenia ... ale rozumiem też jego wpływ. Następnie wymyśliłem to proponowane rozwiązanie.

Mam nadzieję, że uszczęśliwi to wszystkich, a przynajmniej prawie wszystkich ...

Eric

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 });
                }
            }
        }
    }
}
Eric Ouellet
źródło
Nadal uważam, że Microsoft powinien zapewnić sposób na wyczyszczenie z powiadomieniem. Nadal uważam, że nie trafiają w ujęcie, nie zapewniając w ten sposób. Przepraszam! Nie mówię, że należy usunąć jasne, czy czegoś brakuje !!! Aby uzyskać niskie sprzężenie, czasami musimy wiedzieć, co zostało usunięte.
Eric Ouellet
1

Aby było to proste, dlaczego nie nadpisać metody ClearItem i nie zrobić, co chcesz, tj. Odłączyć elementy od zdarzenia.

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

  rest of the code omitted
}

Prosty, czysty i zawarty w kodzie kolekcji.

Stéphane
źródło
To jest bardzo bliskie temu, co faktycznie zrobiłem ... zobacz akceptowaną odpowiedź.
cplotts
0

Miałem ten sam problem i to było moje rozwiązanie. Wydaje się, że działa. Czy ktoś widzi potencjalne problemy z tym podejściem?

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

Oto kilka innych przydatnych metod w mojej klasie:

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);
}
hypehuman
źródło
0

Znalazłem inne „proste” rozwiązanie pochodzące z ObservableCollection, ale nie jest ono zbyt eleganckie, ponieważ wykorzystuje Reflection ... Jeśli Ci się spodoba, oto moje rozwiązanie:

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

Tutaj zapisuję bieżące elementy w polu tablicy w metodzie ClearItems, a następnie przechwytuję wywołanie OnCollectionChanged i nadpisuję pole prywatne e._oldItems (przez Reflections) przed uruchomieniem base.OnCollectionChanged

Formentz
źródło
0

Możesz zastąpić metodę ClearItems i zgłosić zdarzenie za pomocą akcji Remove i 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));
    }
}

Część System.Collections.ObjectModel.ObservableCollection<T>realizacji:

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[]";
}
Artem Illarionov
źródło
-4

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

Przeczytaj tę dokumentację z otwartymi oczami i włączonym mózgiem. Microsoft zrobił wszystko dobrze. Musisz ponownie przeskanować swoją kolekcję, gdy wyśle ​​za Ciebie powiadomienie o zresetowaniu. Otrzymujesz powiadomienie o zresetowaniu, ponieważ wyrzucenie Dodaj / Usuń dla każdego elementu (usuwania i dodawania z powrotem do kolekcji) jest zbyt kosztowne.

Orion Edwards ma całkowitą rację (szacunek, stary). Proszę pomyśleć szerzej podczas czytania dokumentacji.

Dima
źródło
5
Właściwie myślę, że ty i Orion macie rację w rozumieniu tego, jak Microsoft zaprojektował to do działania. :) Ten projekt spowodował jednak problemy, które musiałem obejść w mojej sytuacji. Ta sytuacja też jest powszechna ... i dlaczego zadałem to pytanie.
cplotts
Myślę, że powinieneś przyjrzeć się mojemu pytaniu (i zaznaczonej odpowiedzi) trochę więcej. Nie sugerowałem usuwania dla każdej pozycji.
cplotts
I tak dla porządku, szanuję odpowiedź Oriona ... Myślę, że po prostu trochę się ze sobą bawiliśmy ... przynajmniej tak to odebrałem.
cplotts
Jedna ważna rzecz: nie musisz odłączać procedur obsługi zdarzeń od usuwanych obiektów. Oderwanie odbywa się automatycznie.
Dima,
1
Podsumowując, zdarzenia nie są odłączane automatycznie podczas usuwania obiektu z kolekcji.
cplotts
-4

Jeśli ObservableCollectionnie jest jasne, możesz wypróbować poniższy kod. może ci pomóc:

private TestEntities context; // This is your context

context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context
Manas
źródło