Lorsque vous effacez une collection Observable, il n'y a aucun élément dans e.OldItems

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

  •  03-07-2019
  •  | 
  •  

Question

J'ai quelque chose ici qui me prend vraiment au dépourvu.

J'ai une ObservableCollection of T qui est remplie d'éléments. J'ai également un gestionnaire d'événement attaché à l'événement CollectionChanged.

Lorsque vous Effacez la collection, un événement CollectionChanged avec e.Action défini sur NotifyCollectionChangedAction.Reset est créé. Ok c'est normal. Mais ce qui est étrange, c’est que ni e.OldItems ni e.NewItems n’a rien en elle. Je suppose que e.OldItems sera rempli avec tous les éléments supprimés de la collection.

Quelqu'un d'autre a-t-il vu cela? Et si oui, comment se sont-ils débrouillés?

Un peu d’arrière-plan: j’utilise l’événement CollectionChanged pour attacher et détacher un autre événement. Par conséquent, si je ne récupère aucun élément dans e.OldItems ... je ne pourrai pas me détacher de cet événement.


CLARIFICATION: Je sais que la documentation ne dit pas carrément qu’elle doit se comporter de cette façon. Mais pour chaque autre action, il me notifie ce qu’elle a fait. Donc, mon hypothèse est que cela me dirait… dans le cas de Clear / Reset également.


Vous trouverez ci-dessous un exemple de code si vous souhaitez le reproduire vous-même. Tout d’abord le 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>

Ensuite, le code derrière:

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 };
    }
}
Était-ce utile?

La solution 6

Bien, même si je souhaite encore qu'ObservableCollection se comporte comme je le souhaitais ... le code ci-dessous est ce que j'ai fini par faire. Fondamentalement, j'ai créé une nouvelle collection de T appelée TrulyObservableCollection et remplacé la méthode ClearItems que j'ai ensuite utilisée pour déclencher un événement Clearing.

Dans le code qui utilise cette TrulyObservableCollection, j'utilise cet événement Clearing pour parcourir en boucle les éléments qui sont toujours dans la collection à ce moment-là pour effectuer le détachement de l'événement que je souhaitais détacher. de.

J'espère que cette approche aidera aussi quelqu'un d'autre.

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

Autres conseils

Il ne prétend pas inclure les anciens éléments, car Réinitialiser ne signifie pas que la liste a été effacée

Cela signifie que quelque chose de dramatique a eu lieu et que le coût de la préparation de l'ajout / de la suppression dépasserait très probablement le coût d'une nouvelle analyse de la liste à partir de zéro ... c'est donc ce que vous devriez faire.

MSDN suggère un exemple de l'ensemble de la collection en cours de tri sélectif en tant que candidat à la réinitialisation.

Pour réitérer. Réinitialiser ne veut pas dire clair , cela signifie Vos hypothèses sur la liste sont maintenant invalides. Traitez-la comme s'il s'agissait d'une liste entièrement nouvelle . Il se trouve que Clear en est un exemple, mais il pourrait en exister d’autres.

Quelques exemples:
J'ai eu une liste comme celle-ci avec beaucoup d'éléments, et il a été lié à une donnée par un WPF ListView pour l'afficher à l'écran.
Si vous effacez la liste et augmentez l'événement .Reset, les performances sont assez instantanées, mais si vous créez plutôt de nombreux événements .Remove individuels, les performances sont terribles car WPF supprime les éléments un par un. J'ai également utilisé Move dans mon propre code pour indiquer que la liste a été triée à nouveau, plutôt que d'émettre des milliers d'opérations individuelles <=>. Comme dans le cas de Clear, les performances sont nombreuses lorsque vous soulevez de nombreux événements individuels.

Nous avons eu le même problème ici. L'action Réinitialiser dans CollectionChanged n'inclut pas les OldItems. Nous avons eu une solution de contournement: nous avons utilisé à la place la méthode d'extension suivante:

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

Nous avons fini par ne pas prendre en charge la fonction Clear () et en lançant une exception NotSupportedException dans CollectionChanged pour les actions de réinitialisation. RemoveAll déclenchera une action Remove dans l'événement CollectionChanged, avec les OldItems appropriés.

Une autre option consiste à remplacer l'événement Reset par un seul événement Remove contenant tous les éléments désactivés dans sa propriété OldItems, comme suit:

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

Avantages:

  1. Il n'est pas nécessaire de s'abonner à un événement supplémentaire (comme requis par la réponse acceptée)

  2. Ne génère pas d'événement pour chaque objet supprimé (d'autres solutions proposées entraînent la suppression de plusieurs événements).

  3. L'abonné doit uniquement vérifier NewItems & amp; OldItems sur tout événement pour ajouter / supprimer des gestionnaires d’événements selon les besoins.

Inconvénients:

  1. Aucun événement de réinitialisation

  2. Petite (?) surcharge créant une copie de la liste.

  3. ???

EDIT 2012-02-23

Malheureusement, lorsqu’il est lié à des contrôles basés sur une liste WPF, effacer une collection ObservableCollectionNoReset avec plusieurs éléments entraînera une exception & "Actions de la plage non prises en charge &"; Pour être utilisé avec des contrôles avec cette limitation, j'ai modifié la classe ObservableCollectionNoReset en:

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

Cela n’est pas aussi efficace lorsque RangeActionsSupported est défini sur false (valeur par défaut) car une notification de suppression est générée par objet de la collection

J'ai trouvé une solution qui permet à l'utilisateur de capitaliser sur l'efficacité de l'ajout ou de la suppression de plusieurs éléments à la fois tout en ne lançant qu'un seul événement. d'autres utilisateurs aimeraient avoir une liste d'éléments ajoutés et supprimés.

Cette solution implique le remplacement de l'événement CollectionChanged. Lorsque nous allons déclencher cet événement, nous pouvons réellement examiner la cible de chaque gestionnaire enregistré et déterminer son type. Etant donné que seules les classes ICollectionView nécessitent NotifyCollectionChangedAction.Reset args lorsque plusieurs éléments changent, nous pouvons les isoler et donner à tous les autres des arguments d'argument appropriés contenant la liste complète des éléments supprimés ou ajoutés. Vous trouverez ci-dessous la mise en œuvre.

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
}

Bien, je sais que la question est très ancienne, mais j’ai trouvé une bonne solution au problème et je pensais la partager. Cette solution s’inspire de bon nombre des bonnes solutions proposées ici, mais présente les avantages suivants:

  • Inutile de créer une nouvelle classe et de remplacer des méthodes à partir de ObservableCollection
  • Ne modifie pas le fonctionnement de NotifyCollectionChanged (donc ne modifiez pas la réinitialisation)
  • Ne pas utiliser la réflexion

Voici le code:

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

Cette méthode d'extension utilise simplement un Action qui sera appelé avant l'effacement de la collection.

J'ai abordé celui-ci d'une manière légèrement différente car je voulais m'inscrire à un événement et gérer tous les ajouts et suppressions dans le gestionnaire d'événements. J'ai commencé par annuler l'événement de la collection modifiée et rediriger les actions de réinitialisation en actions de suppression avec une liste d'éléments. Tout cela a mal tourné, car j’utilisais la collection observable comme source d’éléments pour une vue de collection et j’obtenais & "; Les actions de la plage ne sont pas prises en charge &";.

.

J'ai finalement créé un nouvel événement appelé CollectionChangedRange, qui agit comme je m'attendais de la part de la version intégrée.

Je ne peux pas imaginer pourquoi cette limitation serait autorisée et j'espère que ce poste empêchera au moins d'autres personnes de sombrer dans l'impasse que j'ai suivie.

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

}

Voici comment ObservableCollection fonctionne. Vous pouvez contourner ce problème en conservant votre propre liste en dehors de ObservableCollection (ajouter à la liste lorsque l’action est Ajouter, supprimer lorsque l’action est Supprimer, etc.), puis vous pouvez obtenir tous les éléments supprimés ( ou des éléments ajoutés) lorsque l’action est réinitialisée en comparant votre liste avec ObservableCollection.

Une autre option consiste à créer votre propre classe qui implémente IList et INotifyCollectionChanged. Vous pouvez ensuite attacher et détacher des événements de cette classe (ou définir OldItems sur Clear si vous le souhaitez). Ce n'est vraiment pas difficile, mais beaucoup en tapant.

Pour le scénario consistant à attacher et détacher des gestionnaires d'événements aux éléments de ObservableCollection, il existe également un & "côté client &"; Solution. Dans le code de gestion des événements, vous pouvez vérifier si l'expéditeur se trouve dans ObservableCollection à l'aide de la méthode Contains. Pro: vous pouvez travailler avec n'importe quelle ObservableCollection existante. Inconvénients: la méthode Contains s'exécute avec O (n), où n est le nombre d'éléments dans ObservableCollection. C'est donc une solution pour les petites ObservableCollections.

Un autre " côté client " La solution consiste à utiliser un gestionnaire d'événements au milieu. Il suffit d’enregistrer tous les événements sur le gestionnaire d’événements au milieu. Ce gestionnaire d'événements informe à son tour le gestionnaire d'événements réel via un rappel ou un événement. Si une action de réinitialisation se produit, supprimez le rappel ou l'événement, créez un nouveau gestionnaire d'événements au milieu et oubliez l'ancien. Cette approche fonctionne également pour les grandes ObservableCollections. Je l'ai utilisé pour l'événement PropertyChanged (voir le code ci-dessous).

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

Consultation du NotifyCollectionChangedEventArgs , il apparaît que OldItems ne contient que des éléments modifiés à la suite d'une action Remplacer, Supprimer ou Déplacer. Cela n'indique pas qu'il contiendra quoi que ce soit sur Clear. Je suppose que Clear déclenche l'événement, mais n'enregistre pas les éléments supprimés et n'invoque pas du tout le code de suppression.

Eh bien, j'ai décidé de me salir moi-même.

Microsoft a BEAUCOUP de travail en s'assurant toujours que NotifyCollectionChangedEventArgs ne contient aucune donnée lors de l'appel d'une réinitialisation. Je suppose que c'était une décision performance / mémoire. Si vous réinitialisez une collection de 100 000 éléments, je suppose qu'ils ne veulent pas dupliquer tous ces éléments.

Mais comme mes collections ne contiennent jamais plus de 100 éléments, cela ne me pose aucun problème.

Quoi qu'il en soit, j'ai créé une classe héritée avec la méthode suivante:

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’ObservableCollection ainsi que l’interface INotifyCollectionChanged sont clairement écrits avec une utilisation spécifique à l’esprit: le bâtiment d’UI et ses caractéristiques de performances spécifiques.

Lorsque vous souhaitez que les notifications de modifications de la collection soient notifiées, vous n'êtes généralement intéressé que par les événements Ajout / Suppression.

J'utilise l'interface suivante:

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

J'ai également écrit ma propre surcharge de Collection où:

  • ClearItems soulève la suppression
  • InsertItem soulève Ajouté
  • RemoveItem soulève Suppression
  • SetItem soulève la suppression et l'ajout

Bien sûr, AddRange peut également être ajouté.

Je parcourais quelques-uns des codes de cartographie des kits d’outils Silverlight et WPF et j’ai remarqué qu’ils avaient également résolu ce problème (de manière similaire) ... et j’imaginais que je publierais leur solution.

Fondamentalement, ils ont également créé une ObservableCollection dérivée et redéfini ClearItems en appelant Supprimer pour chaque élément en cours de suppression.

Voici le code:

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

C’est un sujet brûlant ... car à mon avis, Microsoft n’a pas fait son travail correctement ... encore une fois. Ne me comprenez pas mal, j'aime Microsoft, mais ils ne sont pas parfaits!

J'ai lu la plupart des commentaires précédents. Je suis d’accord avec tous ceux qui pensent que Microsoft n’a pas programmé Clear () correctement.

À mon avis, du moins, il faut un argument pour pouvoir dissocier des objets d’un événement ... mais j’en comprends également l’impact. Ensuite, j'ai réfléchi à la solution proposée.

J'espère que cela rendra tout le monde heureux, ou du moins presque tout le monde ...

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

Pour rester simple, pourquoi ne pas remplacer la méthode ClearItem et faire ce que vous voulez, par exemple, détacher les éléments de l'événement.

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

  rest of the code omitted
}

Simple, propre et contenu dans le code de la collection.

J'ai eu le même problème, et c'était ma solution. Cela semble fonctionner. Quelqu'un voit-il des problèmes potentiels avec cette approche?

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

Voici d'autres méthodes utiles dans ma 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);
}

J'ai trouvé un autre " simple " La solution dérivant de ObservableCollection, mais elle n’est pas très élégante car elle utilise la réflexion ... Si vous l’aimez, voici ma solution:

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

Ici, je sauvegarde les éléments actuels dans un champ de tableau de la méthode ClearItems, puis j'intercepte l'appel de OnCollectionChanged et écrase le champ privé e._oldItems (via Reflections) avant de lancer base.OnCollectionChanged

Vous pouvez remplacer la méthode ClearItems et déclencher un événement avec l'action Remove et 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));
    }
}

Partie de System.Collections.ObjectModel.ObservableCollection<T> réalisation:

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 / fr-fr / library / system.collections.specialized.notifycollectionchangedaction (VS.95) .aspx

Veuillez lire cette documentation avec les yeux ouverts et le cerveau en marche. Microsoft a tout fait correctement. Vous devez ré-analyser votre collection lorsqu'elle émet une notification de réinitialisation pour vous. Vous recevez une notification de réinitialisation car le lancement de Ajouter / Supprimer pour chaque élément (être supprimé et ajouté à la collection) est trop coûteux.

Orion Edwards a complètement raison (respect, homme). Veuillez penser plus large lors de la lecture de la documentation.

Si votre ObservableCollection n’est pas clair, essayez le code ci-dessous. cela peut vous aider:

private TestEntities context; // This is your context

context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top