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

StackOverflow https://stackoverflow.com/questions/224155

  •  03-07-2019
  •  | 
  •  

Domanda

Ho qualcosa qui che mi sta davvero prendendo alla sprovvista.

Ho un ObservableCollection di T che è pieno di oggetti. Ho anche un gestore eventi collegato all'evento CollectionChanged.

Quando Cancella la raccolta provoca un evento CollectionChanged con e.Action impostato su NotifyCollectionChangedAction.Reset. Ok, è normale. Ma ciò che è strano è che né e.OldItems o e.NewItems hanno qualcosa in esso. Mi aspetto che e.OldItems sia riempito con tutti gli elementi rimossi dalla raccolta.

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

Alcuni retroscena: sto usando l'evento CollectionChanged per allegare e staccare da un altro evento e quindi se non ricevo alcun elemento in e.OldItems ... Non potrò staccarmi da quell'evento.

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

Di seguito è riportato il codice di esempio se si desidera riprodurlo da soli. 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 };
    }
}
È stato utile?

Soluzione 6

Ok, anche se vorrei ancora che ObservableCollection si comportasse come desideravo ... il codice qui sotto è quello che ho finito per fare. Fondamentalmente, ho creato una nuova raccolta di T chiamata TrulyObservableCollection e ho ignorato il metodo ClearItems che ho poi usato per generare un evento Clearing.

Nel codice che utilizza questo TrulyObservableCollection, utilizzo questo evento Clearing per scorrere gli elementi che sono ancora nella raccolta in quel punto per fare il distacco sull'evento che desideravo staccare da.

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

Altri suggerimenti

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

Significa che si è verificata una cosa drammatica e che il costo di elaborazione dell'aggiunta / rimozione supererebbe molto probabilmente il costo della nuova scansione dell'elenco da zero ... quindi è quello che dovresti fare.

MSDN suggerisce un esempio dell'intero insieme che viene riordinato come candidato per il ripristino.

Per ripetere. Ripristina non significa chiaro , significa I tuoi presupposti sull'elenco ora non sono validi. Trattalo come se fosse un elenco completamente nuovo . Chiaro sembra essere un esempio di questo, ma potrebbero essercene altri.

Alcuni esempi:
Ho avuto un elenco come questo con molti elementi al suo interno ed è stato suddiviso in database in un WPF ListView da visualizzare sullo schermo.
Se si cancella l'elenco e si genera l'evento .Reset, le prestazioni sono praticamente istantanee, ma se invece si generano molti singoli eventi .Remove, le prestazioni sono terribili, poiché WPF rimuove gli elementi uno per uno. Ho anche usato Move nel mio codice per indicare che l'elenco è stato riordinato, piuttosto che emettere migliaia di singole <=> operazioni. Come per Clear, c'è un grande successo prestazionale quando si sollevano molti eventi individuali.

Abbiamo avuto lo stesso problema qui. L'azione Reimposta in CollectionChanged non include OldItems. Abbiamo avuto 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);
   }
}

Alla fine non abbiamo supportato la funzione Clear () e abbiamo lanciato un evento NotSupportedException in CollectionChanged per le azioni Reset. RemoveAll attiverà un'azione Rimuovi nell'evento CollectionChanged, con gli OldItems corretti.

Un'altra opzione è quella di 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
    ...
}

I vantaggi:

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

  2. Non genera un evento per ciascun oggetto rimosso (alcune altre soluzioni proposte comportano più eventi rimossi).

  3. L'iscritto deve solo controllare l'amplificatore NewItems &; OldItems su qualsiasi evento per aggiungere / rimuovere i gestori di eventi come richiesto.

Svantaggi:

  1. Nessun evento di reset

  2. Overhead piccolo (?) che crea copia dell'elenco.

  3. ???

MODIFICA 23/02/2012

Sfortunatamente, quando associato ai controlli basati sull'elenco WPF, la cancellazione di una raccolta ObservableCollectionNoReset con più elementi comporterà un'eccezione " Azioni di intervallo non supportate " ;. Per essere utilizzato con i 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.
 }

Questo non è efficiente quando RangeActionsSupported è falso (impostazione predefinita) perché viene generata una notifica Rimuovi per oggetto nella raccolta

Ho trovato una soluzione che consente all'utente di sfruttare sia l'efficienza dell'aggiunta o la rimozione di molti elementi alla volta mentre si attiva solo un evento, sia di soddisfare le esigenze di UIElements per ottenere l'evento Action.Reset mentre tutti altri utenti vorrebbero un elenco di elementi aggiunti e rimossi.

Questa soluzione prevede la sostituzione dell'evento CollectionChanged. Quando andiamo a lanciare questo evento, possiamo effettivamente guardare l'obiettivo di ciascun gestore registrato e determinarne il tipo. Dal momento che solo le classi ICollectionView richiedono NotifyCollectionChangedAction.Reset argomenti quando più di un elemento cambia, possiamo individuarli e fornire a tutti gli altri argomenti relativi agli eventi che contengono l'elenco completo degli elementi rimossi o aggiunti. Di seguito è 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
}

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

  • Non è necessario creare una nuova classe e sovrascrivere i metodi da ObservableCollection
  • Non altera il funzionamento di NotifyCollectionChanged (quindi non si scherza con Reset)
  • Non utilizza la 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 richiede semplicemente un Action che verrà invocato prima che la raccolta venga cancellata.

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 ignorando l'evento della raccolta modificata e reindirizzando le azioni di ripristino alle azioni di rimozione con un elenco di elementi. Tutto è andato storto mentre stavo usando la raccolta osservabile come fonte di oggetti per una vista raccolta e ho ottenuto & Quot; Azioni di intervallo non supportate & Quot ;.

Alla fine ho creato un nuovo evento chiamato CollectionChangedRange che agisce come mi aspettavo che funzionasse la versione integrata.

Non riesco a immaginare perché questa limitazione sia consentita e spero che questo post almeno impedisca ad altri di andare oltre il 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);
        }
    }

}

Ecco come funziona ObservableCollection, puoi aggirare questo mantenendo il tuo elenco al di fuori di ObservableCollection (aggiungendolo all'elenco quando l'azione è Aggiungi, rimuovi quando l'azione è Rimuovi, ecc.) quindi puoi ottenere tutti gli elementi rimossi ( o elementi aggiunti) quando si ripristina l'azione confrontando l'elenco con ObservableCollection.

Un'altra opzione è quella di creare la tua classe che implementa IList e INotifyCollectionChanged, quindi puoi collegare e staccare gli eventi all'interno di quella classe (o impostare OldItems su Clear se vuoi) - non è davvero difficile, ma è molto digitazione.

Per lo scenario di collegamento e scollegamento dei gestori di eventi agli elementi di ObservableCollection c'è anche un " lato client " soluzione. Nel codice di gestione degli eventi è possibile verificare se il mittente si trova in ObservableCollection utilizzando il metodo Contains. Pro: puoi lavorare con qualsiasi ObservableCollection esistente. Contro: il metodo Contains funziona con O (n) dove n è il numero di elementi in ObservableCollection. Quindi questa è una soluzione per piccoli ObservableCollections.

Un altro " lato client " la soluzione è 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 eventi nel mezzo e dimenticare quello precedente. Questo approccio funziona anche per grandi ObservableCollections. 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;
            }   
        }
    }

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

Beh, ho deciso di sporcarmi da solo.

Microsoft ha lavorato molto per assicurarsi sempre che NotifyCollectionChangedEventArgs non avesse alcun dato quando si chiama un reset. Suppongo che questa sia stata una decisione di prestazioni / memoria. Se stai ripristinando una raccolta con 100.000 elementi, suppongo che non volessero duplicare tutti quegli elementi.

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

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

L'interfaccia ObservableCollection e INotifyCollectionChanged sono chiaramente scritti con un uso specifico in mente: la costruzione dell'interfaccia utente e le sue caratteristiche prestazionali specifiche.

Quando si desidera ricevere notifiche sulle modifiche alla raccolta, in genere si è interessati solo agli eventi Aggiungi e Rimuovi.

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 Collection dove:

  • ClearItems aumenta la rimozione
  • Inserimenti InsertItem aggiunti
  • RemoveItem genera la rimozione
  • SetItem genera Rimozione e aggiunta

Naturalmente, è possibile aggiungere anche AddRange.

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

Fondamentalmente, hanno anche creato un ObservableCollection derivato e sovrascrivono ClearItems, chiamando Rimuovi su ogni elemento da cancellare.

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

Questo è un argomento caldo ... perché secondo me, Microsoft non ha fatto bene il suo lavoro ... ancora una volta. Non fraintendermi, mi piace Microsoft, ma non sono perfetti!

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

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

Spero che renderà 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 });
                }
            }
        }
    }
}

Per semplificare il motivo, perché non sovrascrivere il metodo ClearItem e fare ciò che si desidera lì, ad esempio staccare 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 contenuto nel codice di raccolta.

Ho avuto lo stesso problema e questa era 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);
}

Ho trovato un altro " semplice " soluzione derivata 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

Puoi sovrascrivere il metodo ClearItems e generare un evento con Rimuovi azione 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[]";
}

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

Leggi questa documentazione con gli occhi aperti e il cervello acceso. Microsoft ha fatto tutto bene. Devi ripetere la scansione della tua raccolta quando genera una notifica di ripristino per te. Ricevi una notifica di ripristino perché lanciare Aggiungi / Rimuovi per ogni elemento (rimosso e aggiunto alla raccolta) è troppo costoso.

Orion Edwards ha perfettamente ragione (rispetto, amico). Ti preghiamo di pensare in modo più ampio quando leggi la documentazione.

Se il tuo ObservableCollection non viene chiarito, puoi provare questo codice qui sotto. può aiutarti:

private TestEntities context; // This is your context

context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top