web-dev-qa-db-fra.com

Lors de la suppression d'une collection observable, il n'y a aucun élément dans e.OldItems

J'ai quelque chose ici qui me surprend vraiment.

J'ai une collection observable de T qui est remplie d'articles. J'ai également un gestionnaire d'événements attaché à l'événement CollectionChanged.

Lorsque vous Clear la collection, cela provoque un événement CollectionChanged avec e.Action défini sur NotifyCollectionChangedAction.Reset. Ok, c'est normal. Mais ce qui est étrange, c'est que ni les e.OldItems ni les e.NewItems ne contiennent quoi que ce soit. Je m'attendrais à ce que e.OldItems soit rempli avec tous les éléments qui ont été supprimés de la collection.

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

Quelques informations: j'utilise l'événement CollectionChanged pour attacher et détacher d'un autre événement et donc si je n'obtiens aucun élément dans e.OldItems ... je ne pourrai pas me détacher de cet événement.


CLARIFICATION: Je sais que la documentation n'indique pas qu'elle doit se comporter de cette façon. Mais pour chaque autre action, il m'informe de ce qu'il a fait. Donc, mon hypothèse est que cela me dirait ... dans le cas de Clear/Reset également.


Vous trouverez ci-dessous l'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 };
    }
}
88
cplotts

Ok, même si je souhaite toujours que 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 les éléments qui sont toujours dans la collection à ce stade pour faire le détachement sur l'événement dont je souhaitais me détacher.

J'espère que cette approche aidera également 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();
    }
}
7
cplotts

Nous avons eu le même problème ici. L'action de réinitialisation dans CollectionChanged n'inclut pas les OldItems. Nous avons eu une solution: 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 lever 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.

21
decasteljau

Une autre option consiste à remplacer l'événement Reset par un seul événement Remove qui a tous les éléments effacé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. Pas besoin 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é (certaines autres solutions proposées entraînent plusieurs événements supprimés).

  3. L'abonné a seulement besoin de vérifier NewItems et OldItems sur n'importe quel événement pour ajouter/supprimer des gestionnaires d'événements selon les besoins.

Désavantages:

  1. Aucun événement de réinitialisation

  2. Petit (?) Frais généraux 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, la suppression d'une collection ObservableCollectionNoReset avec plusieurs éléments entraînera une exception "Actions de plage non prises en charge". Pour être utilisé avec des contrôles avec cette limitation, j'ai changé 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.
 }

Ce n'est pas aussi efficace lorsque RangeActionsSupported est faux (par défaut) car une notification de suppression est générée par objet dans la collection

12
grantnz

J'ai trouvé une solution qui permet à l'utilisateur de capitaliser sur l'efficacité de l'ajout ou de la suppression de nombreux éléments à la fois tout en ne déclenchant qu'un seul événement - et de répondre aux besoins des UIElements pour obtenir les événements d'événement Action.Reset tandis que tous les autres utilisateurs le feraient comme une liste d'éléments ajoutés et supprimés.

Cette solution implique de remplacer l'événement CollectionChanged. Lorsque nous allons déclencher cet événement, nous pouvons réellement regarder la cible de chaque gestionnaire enregistré et déterminer leur type. Étant donné que seules les classes ICollectionView nécessitent NotifyCollectionChangedAction.Reset args lorsque plus d'un élément change, nous pouvons les distinguer et donner à tous les autres des arguments d'événement appropriés qui contiennent la liste complète des éléments supprimés ou ajoutés. Ci-dessous est 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
}
9
Alain

D'accord, je sais que c'est une très vieille question, mais j'ai trouvé une bonne solution au problème et j'ai pensé partager. Cette solution s'inspire de nombreuses bonnes réponses ici, mais présente les avantages suivants:

  • Pas besoin de créer une nouvelle classe et de remplacer les méthodes de ObservableCollection
  • Ne falsifie pas le fonctionnement de NotifyCollectionChanged (donc pas de problème avec Reset)
  • N'utilise pas 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 prend simplement un Action qui sera invoqué avant l'effacement de la collection.

8
DeadlyEmbrace

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é à remplacer l'événement de modification de la collection et à rediriger les actions de réinitialisation vers les actions de suppression avec une liste d'éléments. Tout s'est mal passé car j'utilisais la collection observable comme source d'éléments pour une vue de collection et j'ai obtenu "Actions de plage non prises en charge".

J'ai finalement créé un nouvel événement appelé CollectionChangedRange qui agit de la manière dont je m'attendais à ce que la version intégrée agisse.

Je ne peux pas imaginer pourquoi cette limitation serait autorisée et j'espère que ce message empêchera au moins les autres de descendre l'impasse que j'ai faite.

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

}
4
Jim Hough

Voici comment fonctionne ObservableCollection, vous pouvez contourner cela en gardant votre propre liste en dehors de ObservableCollection (en ajoutant à la liste lorsque l'action est Ajouter, en supprimant lorsque l'action est Supprimer, etc.), vous pouvez alors obtenir tous les éléments supprimés (ou les é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, puis vous pouvez 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 c'est beaucoup de saisie.

3
Nir

Pour le scénario d'attachement et de détachement de gestionnaires d'événements aux éléments de ObservableCollection, il existe également une solution "côté client". 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 la ObservableCollection. C'est donc une solution pour les petites ObservableCollections.

Une autre solution "côté client" consiste à utiliser un gestionnaire d'événements au milieu. Enregistrez simplement tous les événements au 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. J'ai utilisé cela 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;
            }   
        }
    }
3
Chris

Eh bien, j'ai décidé de me salir avec ça.

Microsoft a mis beaucoup de travail à toujours s'assurer que NotifyCollectionChangedEventArgs n'a pas de données lors de l'appel d'une réinitialisation. Je suppose que c'était une décision de performance/mémoire. Si vous réinitialisez une collection avec 100 000 éléments, je suppose qu'ils ne voulaient pas dupliquer tous ces éléments.

Mais comme mes collections n'ont jamais plus de 100 éléments, je n'y vois 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);
    }
2
HaxElit

En regardant NotifyCollectionChangedEventArgs , il apparaît que OldItems ne contient que des éléments modifiés suite à l'action Remplacer, Supprimer ou Déplacer. Cela n'indique pas qu'il contiendra quoi que ce soit sur Clear. Je soupçonne que Clear déclenche l'événement, mais n'a pas enregistré les éléments supprimés et n'invoque pas du tout le code Remove.

2
tvanfosson

L'observableCollection ainsi que l'interface INotifyCollectionChanged sont clairement écrits avec une utilisation spécifique à l'esprit: la construction de l'interface utilisateur et ses caractéristiques de performance spécifiques.

Lorsque vous souhaitez recevoir des notifications de modifications de collection, vous n'êtes généralement intéressé que par les événements Ajouter et Supprimer.

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 augmente la suppression
  • InsertItem soulève Ajouté
  • RemoveItem soulève Retrait
  • SetItem soulève la suppression et l'ajout

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

2
Rick Beerendonk

Pour rester simple, pourquoi ne pas remplacer la méthode ClearItem et faire ce que vous voulez, c'est-à-dire 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 collection.

1
Stéphane

Je venais de passer en revue certains des codes dans les boîtes à outils Silverlight et WPF et j'ai remarqué qu'ils avaient également résolu ce problème (d'une manière similaire) ... et j'ai pensé que j'irais de l'avant et publierais leur solution.

Fondamentalement, ils ont également créé une ObservableCollection dérivée et ont remplacé ClearItems, appelant Remove sur 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);
        }
    }
}
1
cplotts

C'est un sujet brûlant ... car à mon avis, Microsoft n'a pas fait son travail correctement ... encore une fois. Ne vous méprenez pas, 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 correctement programmé Clear ().

À mon avis, au moins, il a besoin d'un argument pour permettre de détacher des objets d'un événement ... mais j'en comprends aussi l'impact. Ensuite, j'ai pensé à cette 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 });
                }
            }
        }
    }
}
1
Eric Ouellet

J'ai trouvé une autre solution "simple" dérivant d'ObservableCollection, mais elle n'est pas très élégante car elle utilise Reflection ... 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 dans 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

0
Formentz

Vous pouvez remplacer la méthode ClearItems et déclencher l'é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> la concrétisation:

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[]";
}
0
Artem Illarionov

J'avais 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 quelques 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);
}
0
hypehuman