Quando si cancella una ObservableCollection, non ci sono elementi in e.OldItems


91

Ho qualcosa qui che mi sta davvero prendendo alla sprovvista.

Ho una ObservableCollection di T che è piena di elementi. Ho anche un gestore di eventi collegato all'evento CollectionChanged.

Quando si cancella la raccolta, viene generato un evento CollectionChanged con e.Action impostato su NotifyCollectionChangedAction.Reset. Ok, è normale. Ma la cosa strana è che né e.OldItems né e.NewItems contengono qualcosa. Mi aspetto che e.OldItems venga riempito con tutti gli elementi che sono stati rimossi dalla raccolta.

Qualcun altro l'ha visto? E se è così, come ci sono riusciti?

Qualche background: sto usando l'evento CollectionChanged per collegarmi e scollegarmi da un altro evento e quindi se non ricevo alcun elemento in e.OldItems ... non sarò in grado di scollegarmi da quell'evento.


CHIARIMENTO: So che la documentazione non afferma apertamente che deve comportarsi in questo modo. Ma per ogni altra azione, mi sta avvisando di ciò che ha fatto. Quindi, la mia ipotesi è che mi direbbe ... anche nel caso di Clear / Reset.


Di seguito è riportato il codice di esempio se desideri riprodurlo da solo. Prima di tutto 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>

Successivamente, il codice dietro:

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

Perché devi annullare l'iscrizione all'evento? In quale direzione ti stai iscrivendo? Gli eventi creano un riferimento al sottoscrittore tenuto dal raiser, non viceversa. Se i raisers sono elementi in una raccolta che viene cancellata, verranno raccolti in modo sicuro in modo sicuro e i riferimenti scompariranno, nessuna perdita. Se gli elementi sono i sottoscrittori e fanno riferimento a un raiser, imposta l'evento su null nel raiser quando ottieni un Reset - non c'è bisogno di annullare l'iscrizione individualmente agli elementi.
Aleksandr Dubinsky

Credimi, so come funziona. L'evento in questione era su un singleton che è rimasto in circolazione per molto tempo ... quindi gli elementi della collezione erano gli abbonati. La tua soluzione di impostare semplicemente l'evento su null non funziona ... poiché l'evento deve ancora attivarsi ... possibilmente avvisando altri abbonati (non necessariamente quelli nella raccolta).
cplotts

Risposte:


46

Non pretende di includere i vecchi elementi, perché Reset non significa che l'elenco sia stato cancellato

Significa che è avvenuta una cosa drammatica e il costo per elaborare l'aggiunta / la rimozione molto probabilmente supererebbe il costo della nuova scansione dell'elenco da zero ... quindi è quello che dovresti fare.

MSDN suggerisce un esempio dell'intera raccolta che viene riordinata come candidato per il ripristino.

Reiterare. Reimpostare non significa chiaro , significa che le tue supposizioni sull'elenco non sono valide. Trattalo come se fosse un elenco completamente nuovo . Clear sembra essere un esempio di questo, ma potrebbero essercene anche altri.

Alcuni esempi:
ho avuto un elenco come questo con molti elementi in esso, ed è stato associato a un WPF ListViewper essere visualizzato sullo schermo.
Se si cancella l'elenco e si genera l' .Resetevento, la performance è praticamente istantanea, ma se invece si sollevano molti .Removeeventi individuali , la performance è terribile, poiché WPF rimuove gli elementi uno per uno. Ho anche usato il .Resetmio codice per indicare che l'elenco è stato riordinato, piuttosto che emettere migliaia di singole Moveoperazioni. Come con Clear, si riscontra un grande calo delle prestazioni quando si generano molti eventi individuali.


1
Su questa base sarò rispettosamente in disaccordo. Se guardi la documentazione afferma: rappresenta una raccolta di dati dinamici che fornisce notifiche quando gli elementi vengono aggiunti, rimossi o quando l'intero elenco viene aggiornato (vedi msdn.microsoft.com/en-us/library/ms668613(v=VS .100) .aspx )
cplotts

6
La documentazione afferma che dovrebbe avvisarti quando gli elementi vengono aggiunti / rimossi / aggiornati, ma non promette di dirti tutti i dettagli degli elementi ... solo che l'evento si è verificato. Da questo punto di vista il comportamento va bene. Personalmente penso che avrebbero dovuto semplicemente inserire tutti gli elementi OldItemsdurante la cancellazione, (si tratta solo di copiare un elenco), ma forse c'era qualche scenario in cui questo era troppo costoso. In ogni caso, se si desidera una collezione che non si notifica di tutti gli elementi eliminati, non sarebbe difficile da fare.
Orion Edwards,

2
Ebbene, se Resetsi vuole indicare un'operazione costosa, è molto probabile che lo stesso ragionamento valga per la copia dell'intero elenco a OldItems.
pbalaga

7
Fatto divertente: a partire da .NET 4.5 , in Resetrealtà significa "Il contenuto della raccolta è stato cancellato ". Vedi msdn.microsoft.com/en-us/library/…
Athari

9
Questa risposta non aiuta molto, mi dispiace. Sì, puoi ripetere la scansione dell'intero elenco se ottieni un ripristino, ma non hai accesso per rimuovere gli elementi, che potrebbero essere necessari per rimuovere i gestori di eventi da essi. Questo è un grosso problema.
Virus721

22

Abbiamo avuto lo stesso problema qui. L'azione Reimposta in CollectionChanged non include OldItems. Avevamo una soluzione alternativa: abbiamo utilizzato invece il seguente metodo di estensione:

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

Abbiamo finito per non supportare la funzione Clear () e per lanciare un'eccezione NotSupportedException nell'evento CollectionChanged per le azioni di ripristino. RemoveAll attiverà un'azione Remove nell'evento CollectionChanged, con gli OldItems appropriati.


Buona idea. Non mi piace non supportare Clear perché questo è il metodo (nella mia esperienza) che la maggior parte delle persone usa ... ma almeno stai avvertendo l'utente con un'eccezione.
cplotts

Sono d'accordo, questa non è la soluzione ideale, ma l'abbiamo trovata la migliore soluzione accettabile.
decasteljau

Non dovresti usare i vecchi oggetti! Quello che dovresti fare è scaricare tutti i dati che hai nell'elenco e scansionarli nuovamente come se fosse un nuovo elenco!
Orion Edwards,

16
Il problema, Orion, con il tuo suggerimento ... è il caso d'uso che ha portato a questa domanda. Cosa succede se nell'elenco sono presenti elementi da cui desidero scollegare un evento? Non posso semplicemente scaricare i dati sulla lista ... provocherebbe perdite di memoria / pressione.
cplotts

5
Lo svantaggio principale di questa soluzione è che se rimuovi 1000 elementi, attivi CollectionChanged 1000 volte e l'interfaccia utente deve aggiornare CollectionView 1000 volte (l'aggiornamento degli elementi dell'interfaccia utente è costoso). Se non hai paura di sovrascrivere la classe ObservableCollection, puoi fare in modo che attivi l'evento Clear () ma fornisca gli argomenti dell'evento corretti consentendo al codice di monitoraggio di annullare la registrazione di tutti gli elementi rimossi.
Alain

13

Un'altra opzione è sostituire l'evento Reset con un singolo evento Remove che ha tutti gli elementi cancellati nella sua proprietà OldItems come segue:

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

Vantaggi:

  1. Non è necessario iscriversi a un evento aggiuntivo (come richiesto dalla risposta accettata)

  2. Non genera un evento per ogni oggetto rimosso (alcune altre soluzioni proposte danno luogo a più eventi Removed).

  3. L'abbonato deve solo controllare NewItems e OldItems su qualsiasi evento per aggiungere / rimuovere i gestori di eventi come richiesto.

Svantaggi:

  1. Nessun evento di ripristino

  2. Piccolo (?) Sovraccarico creando una copia dell'elenco.

  3. ???

EDIT 2012-02-23

Sfortunatamente, se associato a controlli basati su elenchi WPF, la cancellazione di una raccolta ObservableCollectionNoReset con più elementi comporterà un'eccezione "Azioni di intervallo non supportate". Per essere utilizzato con controlli con questa limitazione, ho modificato la classe ObservableCollectionNoReset in:

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

Non è efficiente quando RangeActionsSupported è false (impostazione predefinita) perché viene generata una notifica di rimozione per oggetto nella raccolta


Mi piace ma sfortunatamente Silverlight 4 NotifyCollectionChangedEventArgs non ha un costruttore che accetta un elenco di elementi.
Simon Brangwin

2
Mi è piaciuta molto questa soluzione, ma non funziona ... Non sei autorizzato a generare un NotifyCollectionChangedEventArgs che ha più di un elemento modificato a meno che l'azione non sia "Reimposta". Ottieni un'eccezione Range actions are not supported., non so perché lo fa, ma ora questo non lascia altra scelta che rimuovere ogni elemento uno alla volta ...
Alain

2
@Alain The ObservableCollection non impone questa restrizione. Sospetto che sia il controllo WPF a cui hai associato la raccolta. Ho avuto lo stesso problema e non sono mai riuscito a pubblicare un aggiornamento con la mia soluzione. Modificherò la mia risposta con la classe modificata che funziona quando è associata a un controllo WPF.
grantnz

Lo vedo adesso. In realtà ho trovato una soluzione molto elegante che sovrascrive l'evento CollectionChanged e scorre su foreach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged )If handler.Target is CollectionView, quindi puoi disattivare il gestore con Action.Resetargs, altrimenti puoi fornire gli argomenti completi. Il meglio di entrambi i mondi su base gestore per gestore :). Un po 'come quello che c'è qui: stackoverflow.com/a/3302917/529618
Alain

Ho pubblicato la mia soluzione di seguito. stackoverflow.com/a/9416535/529618 Un enorme grazie a te per la tua soluzione stimolante. Mi ha portato a metà strada.
Alain

10

Ok, so che questa è una domanda molto vecchia, ma ho trovato una buona soluzione al problema e ho pensato di condividerla. Questa soluzione prende ispirazione da molte delle ottime risposte qui, ma presenta i seguenti vantaggi:

  • Non è necessario creare una nuova classe e sovrascrivere i metodi da ObservableCollection
  • Non manomette il funzionamento di NotifyCollectionChanged (quindi niente scherzi con Reset)
  • Non fa uso della riflessione

Ecco il codice:

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

Questo metodo di estensione accetta semplicemente un elemento Actionche verrà richiamato prima che la raccolta venga cancellata.


Idea molto carina. Semplice, elegante.
cplotts

9

Ho trovato una soluzione che consente all'utente di trarre vantaggio dall'efficienza dell'aggiunta o della rimozione di molti elementi contemporaneamente, attivando un solo evento e soddisfare le esigenze di UIElements per ottenere gli argomenti dell'evento Action.Reset mentre tutti gli altri utenti lo farebbero come un elenco di elementi aggiunti e rimossi.

Questa soluzione implica l'override dell'evento CollectionChanged. Quando attiviamo questo evento, possiamo effettivamente guardare l'obiettivo di ogni gestore registrato e determinarne il tipo. Poiché solo le classi ICollectionView richiedono NotifyCollectionChangedAction.Resetargomenti quando viene modificato più di un elemento, è possibile individuarli e fornire a tutti gli altri argomenti evento appropriati che contengono l'elenco completo degli elementi rimossi o aggiunti. Di seguito è riportata l'implementazione.

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

Ok, anche se vorrei ancora che ObservableCollection si comportasse come volevo ... il codice qui sotto è quello che ho finito per fare. Fondamentalmente, ho creato una nuova raccolta di T chiamata TrulyObservableCollection e ho sovrascritto il metodo ClearItems che ho quindi utilizzato per generare un evento di compensazione.

Nel codice che utilizza questo TrulyObservableCollection, utilizzo questo evento Clearing per scorrere gli elementi che sono ancora nella raccolta in quel punto per eseguire il distacco dall'evento da cui desideravo scollegarmi.

Spero che questo approccio aiuti anche qualcun altro.

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
Devi rinominare la tua classe in BrokenObservableCollection, non TrulyObservableCollection- stai fraintendendo il significato dell'azione di ripristino.
Orion Edwards,

1
@ Orion Edwards: non sono d'accordo. Vedi il mio commento alla tua risposta.
cplotts

1
@ Orion Edwards: Oh, aspetta, vedo, ti stai comportando in modo divertente. Ma allora dovrei davvero chiamare: ActuallyUsefulObservableCollection. :)
cplotts

6
Lol grande nome. Sono d'accordo che questa sia una grave svista nel design.
devios1

1
Se intendi implementare comunque una nuova classe ObservableCollection, non è necessario creare un nuovo evento che deve essere monitorato separatamente. Puoi semplicemente impedire a ClearItems di attivare un Action = Reset arg eventi evento e sostituirlo con un Action = Rimuovi evento args che contiene un elenco e.OldItems di tutti gli elementi che erano nell'elenco. Vedi altre soluzioni in questa domanda.
Alain

4

Ho affrontato questo in un modo leggermente diverso poiché volevo registrarmi a un evento e gestire tutte le aggiunte e le rimozioni nel gestore eventi. Ho iniziato sovrascrivendo l'evento modificato della raccolta e reindirizzando le azioni di ripristino alle azioni di rimozione con un elenco di elementi. Tutto è andato storto poiché stavo utilizzando la raccolta osservabile come fonte di elementi per una visualizzazione della raccolta e ho ottenuto "Azioni di intervallo non supportate".

Alla fine ho creato un nuovo evento chiamato CollectionChangedRange che agisce nel modo in cui mi aspettavo che funzionasse la versione incorporata.

Non riesco a immaginare perché questa limitazione sarebbe consentita e spero che questo post almeno impedisca ad altri di andare nel vicolo cieco che ho fatto.

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

}

Approccio interessante. Grazie per averlo postato. Se dovessi mai incontrare problemi con il mio approccio, penso che rivisiterò il tuo.
cplotts

3

Questo è il modo in cui funziona ObservableCollection, puoi aggirare questo problema mantenendo il tuo elenco al di fuori di ObservableCollection (aggiungendo all'elenco quando l'azione è Aggiungi, rimuovi quando l'azione è Rimuovi ecc.) Quindi puoi ottenere tutti gli elementi rimossi (o aggiunti elementi ) quando l'azione viene reimpostata confrontando l'elenco con ObservableCollection.

Un'altra opzione è creare la tua classe che implementa IList e INotifyCollectionChanged, quindi puoi allegare e scollegare eventi dall'interno di quella classe (o impostare OldItems su Clear se lo desideri) - non è davvero difficile, ma richiede molta digitazione.


Ho considerato di tenere traccia di un altro elenco così come mi hai suggerito prima, ma sembra un sacco di lavoro non necessario. Il tuo secondo suggerimento è molto vicino a quello che ho deciso di fare ... che posterò come risposta.
cplotts

3

Per lo scenario di collegamento e scollegamento di gestori di eventi agli elementi di ObservableCollection esiste anche una soluzione "lato client". Nel codice di gestione degli eventi è possibile controllare se il mittente è nell'ObservableCollection utilizzando il metodo Contains. Pro: puoi lavorare con qualsiasi ObservableCollection esistente. Contro: il metodo Contains viene eseguito con O (n) dove n è il numero di elementi in ObservableCollection. Quindi questa è una soluzione per piccoli ObservableCollection.

Un'altra soluzione "lato client" consiste nell'utilizzare un gestore di eventi nel mezzo. Basta registrare tutti gli eventi nel gestore eventi nel mezzo. Questo gestore di eventi a sua volta notifica al gestore di eventi reali tramite una richiamata o un evento. Se si verifica un'azione di ripristino, rimuovere il callback o l'evento creare un nuovo gestore di eventi nel mezzo e dimenticare quello vecchio. Questo approccio funziona anche per le grandi raccolte Observable. L'ho usato per l'evento PropertyChanged (vedi codice sotto).

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

Credo che con il tuo primo approccio, avrei bisogno di un altro elenco per tenere traccia degli elementi ... perché una volta ottenuto l'evento CollectionChanged con l'azione Reimposta ... la raccolta è già vuota. Non seguo del tutto il tuo secondo suggerimento. Mi piacerebbe un semplice test harness che lo illustri, ma per aggiungere, rimuovere e cancellare ObservableCollection. Se crei un esempio, puoi inviarmi un'email al mio nome seguito dal mio cognome su gmail.com.
cplotts

2

Guardando NotifyCollectionChangedEventArgs , sembra che OldItems contenga solo elementi modificati a seguito dell'azione Sostituisci, Rimuovi o Sposta. Non indica che conterrà qualcosa su Clear. Sospetto che Clear attivi l'evento, ma non ha registrato gli elementi rimossi e non richiama affatto il codice Remove.


6
L'ho visto anche io, ma non mi piace. Mi sembra un buco spalancato.
cplotts

Non richiama il codice di rimozione perché non è necessario. Reset significa "è successo qualcosa di drammatico, devi ricominciare". Un'operazione chiara ne è un esempio, ma ce ne sono altre
Orion Edwards il

2

Bene, ho deciso di sporcarmi io stesso.

Microsoft ha lavorato MOLTO per assicurarsi sempre che NotifyCollectionChangedEventArgs non abbia dati quando si chiama un ripristino. Presumo che questa sia stata una decisione sulle prestazioni / memoria. Se stai ripristinando una raccolta con 100.000 elementi, presumo che non volessero duplicare tutti quegli elementi.

Ma visto che le mie collezioni non hanno mai più di 100 elementi, non vedo alcun problema con esso.

Ad ogni modo ho creato una classe ereditata con il seguente metodo:

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

Questo è bello, ma probabilmente non funzionerebbe in nient'altro che un ambiente di piena fiducia. Riflettere su campi privati ​​richiede piena fiducia, giusto?
Paul,

1
Perché lo faresti? Ci sono altre cose che possono causare l'attivazione dell'azione Reset - solo perché hai disabilitato il metodo clear non significa che sia andato via (o che dovrebbe)
Orion Edwards

Approccio interessante, ma la riflessione può essere lenta.
cplotts

2

Le interfacce ObservableCollection e INotifyCollectionChanged sono scritte chiaramente con un uso specifico in mente: la creazione dell'interfaccia utente e le sue caratteristiche prestazionali specifiche.

Quando desideri ricevere notifiche sulle modifiche alla raccolta, in genere sei interessato solo ad aggiungere e rimuovere eventi.

Uso la seguente interfaccia:

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

Ho anche scritto il mio sovraccarico di raccolta in cui:

  • ClearItems solleva Removing
  • InsertItem solleva Aggiunto
  • RemoveItem solleva Removing
  • SetItem solleva la rimozione e l'aggiunta

Ovviamente è possibile aggiungere anche AddRange.


+1 per aver sottolineato che Microsoft ha progettato ObservableCollection con un caso d'uso specifico in mente ... e con un occhio alle prestazioni. Sono d'accordo. Ha lasciato un buco per altre situazioni, ma sono d'accordo.
cplotts

-1 Potrei essere interessato a ogni genere di cose. Spesso ho bisogno dell'indice degli elementi aggiunti / rimossi. Potrei voler ottimizzare la sostituzione. Etc. Il design di INotifyCollectionChanged è buono. Il problema che dovrebbe essere risolto è che nessuno in MS lo ha implementato.
Aleksandr Dubinsky

1

Stavo solo esaminando parte del codice dei grafici nei toolkit di Silverlight e WPF e ho notato che hanno anche risolto questo problema (in un modo simile) ... e ho pensato di andare avanti e pubblicare la loro soluzione.

Fondamentalmente, hanno anche creato un ObservableCollection derivato e hanno sovrascritto ClearItems, chiamando Remove su ogni elemento che viene cancellato.

Ecco il codice:

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

Voglio solo sottolineare che questo approccio non mi piace tanto quanto quello che ho contrassegnato come risposta ... poiché ottieni un evento NotifyCollectionChanged (con un'azione Rimuovi) ... per OGNI elemento rimosso.
cplotts

1

Questo è un argomento caldo ... perché a mio parere, Microsoft non ha svolto correttamente il suo lavoro ... ancora una volta. Non fraintendermi, mi piace Microsoft, ma non sono perfette!

Ho letto la maggior parte dei commenti precedenti. Sono d'accordo con tutti coloro che pensano che Microsoft non abbia programmato correttamente Clear ().

Secondo me, almeno, ha bisogno di un argomento per rendere possibile il distacco di oggetti da un evento ... ma ne comprendo anche l'impatto. Quindi, ho pensato a questa soluzione proposta.

Spero che renda tutti felici, o almeno quasi tutti ...

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

Penso ancora che Microsoft dovrebbe fornire un modo per essere in grado di cancellare con notifica. Continuo a pensare che sbagliano il colpo non fornendo in quel modo. Scusa ! Non sto dicendo che il clear dovrebbe essere rimosso, ma manca qualcosa !!! Per ottenere un accoppiamento basso, a volte dobbiamo essere avvisati di ciò che è stato rimosso.
Eric Ouellet

1

Per mantenerlo semplice, perché non sovrascrivi il metodo ClearItem e fai quello che vuoi lì, cioè scollega gli elementi dall'evento.

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

  rest of the code omitted
}

Semplice, pulito e contiene all'interno del codice di raccolta.


Questo è molto vicino a quello che ho fatto effettivamente ... vedi la risposta accettata.
cplotts

0

Ho avuto lo stesso problema e questa è stata la mia soluzione. Sembra funzionare. Qualcuno vede potenziali problemi con questo approccio?

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

Ecco alcuni altri metodi utili nella mia classe:

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

Ho trovato un'altra soluzione "semplice" derivante da ObservableCollection, ma non è molto elegante perché utilizza Reflection ... Se ti piace ecco la mia soluzione:

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

Qui salvo gli elementi correnti in un campo array nel metodo ClearItems, quindi intercetto la chiamata di OnCollectionChanged e sovrascrivo il campo privato e._oldItems (tramite Reflections) prima di avviare base.OnCollectionChanged


0

È possibile sovrascrivere il metodo ClearItems e generare un evento con l'azione Rimuovi e 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));
    }
}

Parte della System.Collections.ObjectModel.ObservableCollection<T>realizzazione:

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

Si prega di leggere questa documentazione con gli occhi aperti e il cervello acceso. Microsoft ha fatto tutto bene. È necessario eseguire nuovamente la scansione della raccolta quando viene generata una notifica di ripristino per te. Ricevi una notifica di ripristino perché lanciare Aggiungi / Rimuovi per ogni elemento (che viene rimosso e aggiunto di nuovo alla raccolta) è troppo costoso.

Orion Edwards ha completamente ragione (rispetto, amico). Si prega di pensare in modo più ampio durante la lettura della documentazione.


5
In realtà penso che tu e Orion abbiate ragione nella vostra comprensione di come Microsoft lo ha progettato per funzionare. :) Questo design tuttavia mi ha causato problemi che dovevo aggirare per la mia situazione. Anche questa situazione è comune ... e perché ho postato questa domanda.
cplotts

Penso che dovresti guardare un po 'di più la mia domanda (e la risposta contrassegnata). Non stavo suggerendo di rimuovere per ogni articolo.
cplotts

E per la cronaca, rispetto la risposta di Orion ... Penso che ci stavamo solo divertendo un po 'l'uno con l'altro ... almeno è così che l'ho presa.
cplotts

Una cosa importante: non è necessario scollegare le procedure di gestione degli eventi dagli oggetti che si stanno rimuovendo. Il distacco avviene automaticamente.
Dima

1
Quindi, in sintesi, gli eventi non vengono scollegati automaticamente quando si rimuove un oggetto da una raccolta.
cplott

-4

Se ObservableCollectionnon stai diventando chiaro, puoi provare questo codice di seguito. può aiutarti:

private TestEntities context; // This is your context

context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.