Lors de la suppression d'un ObservableCollection, il n'y a aucun élément dans e.OldItems

91

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

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

Lorsque vous effacez 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 e.OldItems ni e.NewItems ne contiennent quoi que ce soit. Je m'attendrais à ce que e.OldItems soit rempli de 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 l'ont-ils contourné?

Un peu de contexte: 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 carrément qu'elle doit se comporter de cette façon. Mais pour toutes les autres actions, il m'informe de ce qu'il a fait. Donc, je suppose 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 };
    }
}
cplotts
la source
Pourquoi avez-vous besoin de vous désinscrire de l'événement? Dans quelle direction vous abonnez-vous? Les événements créent une référence à l'abonné détenu par le relanceur, et non l'inverse. Si les relanceurs sont des éléments dans une collection qui est effacée, ils seront collectés en toute sécurité et les références disparaîtront - pas de fuite. Si les éléments sont les abonnés et référencés par un relanceur, définissez simplement l'événement sur null dans le relanceur lorsque vous obtenez une réinitialisation - pas besoin de désabonner individuellement les éléments.
Aleksandr Dubinsky
Croyez-moi, je sais comment cela fonctionne. L'événement en question était sur un singleton qui est resté longtemps ... donc les éléments de la collection étaient les abonnés. Votre solution consistant simplement à définir l'événement sur null ne fonctionne pas ... puisque l'événement doit encore se déclencher ... éventuellement en notifiant d'autres abonnés (pas nécessairement ceux de la collection).
cplotts

Réponses:

46

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 s'est produit et que le coût de l'élaboration des ajouts / suppressions dépasserait probablement le coût de la simple réanalyse de la liste à partir de zéro ... c'est donc ce que vous devez faire.

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

Recommencer. Réinitialiser ne veut pas dire clair , cela signifie que vos hypothèses sur la liste sont maintenant invalides. Traitez-le comme s'il s'agissait d'une liste entièrement nouvelle . Clear se trouve être un exemple de cela, mais il pourrait bien y en avoir d'autres.

Quelques exemples:
j'ai eu une liste comme celle-ci avec beaucoup d'éléments, et elle a été liée à un WPF ListViewpour s'afficher à l'écran.
Si vous effacez la liste et déclenchez l' .Resetévénement, les performances sont à peu près instantanées, mais si vous augmentez à la place de nombreux .Removeévénements individuels , les performances sont terribles, car WPF supprime les éléments un par un. J'ai également utilisé .Resetdans mon propre code pour indiquer que la liste a été triée, plutôt que d'émettre des milliers d' Moveopérations individuelles . Comme avec Clear, il y a un grand impact sur les performances lors du déclenchement de nombreux événements individuels.

Orion Edwards
la source
1
Je vais respectueusement en désaccord sur cette base. Si vous consultez la documentation, elle indique: Représente une collection de données dynamiques qui fournit des notifications lorsque des éléments sont ajoutés, supprimés ou lorsque la liste entière est actualisée (voir msdn.microsoft.com/en-us/library/ms668613(v=VS .100) .aspx )
cplotts
6
La documentation indique qu'elle doit vous avertir lorsque des éléments sont ajoutés / supprimés / actualisés, mais elle ne promet pas de vous dire tous les détails des éléments ... juste que l'événement s'est produit. De ce point de vue, le comportement est correct. Personnellement, je pense qu'ils auraient dû simplement mettre tous les éléments OldItemslors de la compensation (il s'agit simplement de copier une liste), mais peut-être qu'il y avait un scénario où cela coûtait trop cher. En tout cas, si vous voulez une collection qui ne vous informe de tous les éléments supprimés, il ne serait pas difficile à faire.
Orion Edwards
2
Eh bien, Resetpour indiquer une opération coûteuse, il est très probable que le même raisonnement s'applique à la copie de toute la liste vers OldItems.
pbalaga
7
Fait amusant: depuis .NET 4.5 , Resetsignifie en fait "Le contenu de la collection a été effacé ." Voir msdn.microsoft.com/en-us/library/…
Athari
9
Cette réponse n'aide pas beaucoup, désolé. Oui, vous pouvez réanalyser la liste entière si vous obtenez une réinitialisation, mais vous n'avez pas accès pour supprimer des éléments, dont vous pourriez avoir besoin pour supprimer les gestionnaires d'événements. C'est un gros problème.
Virus721
22

Nous avons eu le même problème ici. L'action Reset 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 un événement 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.

Decasteljau
la source
Bonne idée. Je n'aime pas ne pas prendre en charge Clear car c'est la méthode (d'après mon expérience) que la plupart des gens utilisent ... mais au moins vous prévenez l'utilisateur avec une exception.
cplotts
Je suis d'accord, ce n'est pas la solution idéale, mais nous avons trouvé que c'était la meilleure solution de contournement acceptable.
decasteljau
Vous n'êtes pas censé utiliser les anciens objets! Ce que vous êtes censé faire est de vider toutes les données que vous avez sur la liste et de les analyser à nouveau comme s'il s'agissait d'une nouvelle liste!
Orion Edwards
16
Le problème, Orion, avec votre suggestion ... est le cas d'utilisation qui a suscité cette question. Que se passe-t-il lorsque la liste contient des éléments dont je souhaite dissocier un événement? Je ne peux pas simplement vider les données de la liste ... cela entraînerait des fuites / pressions de mémoire.
cplotts
5
Le principal inconvénient de cette solution est que si vous supprimez 1000 éléments, vous déclenchez CollectionChanged 1000 fois et l'interface utilisateur doit mettre à jour CollectionView 1000 fois (la mise à jour des éléments de l'interface utilisateur est coûteuse). Si vous n'avez pas peur de remplacer la classe ObservableCollection, vous pouvez faire en sorte qu'elle déclenche l'événement Clear () mais fournisse l'événement Args correct permettant au code de surveillance d'annuler l'enregistrement de tous les éléments supprimés.
Alain
13

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 souscrire à 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é doit uniquement vérifier NewItems & OldItems sur n'importe quel événement pour ajouter / supprimer des gestionnaires d'événements si nécessaire.

Désavantages:

  1. Aucun événement de réinitialisation

  2. Petit (?) Frais généraux créant une copie de la liste.

  3. ???

MODIFIER 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 false (la valeur par défaut) car une notification Remove est générée par objet dans la collection

grantnz
la source
J'aime ça, mais malheureusement, Silverlight 4 NotifyCollectionChangedEventArgs n'a pas de constructeur qui prend une liste d'éléments.
Simon Brangwin
2
J'ai adoré cette solution, mais cela ne fonctionne pas ... Vous n'êtes pas autorisé à déclencher un NotifyCollectionChangedEventArgs qui a plus d'un élément modifié à moins que l'action soit "Réinitialiser". Vous obtenez une exception, Range actions are not supported.je ne sais pas pourquoi il fait cela, mais maintenant cela ne laisse aucune autre option que de supprimer chaque élément un à la fois ...
Alain
2
@Alain L'ObservableCollection n'impose pas cette restriction. Je soupçonne que c'est le contrôle WPF auquel vous avez lié la collection. J'ai eu le même problème et je n'ai jamais eu le temps de publier une mise à jour avec ma solution. Je modifierai ma réponse avec la classe modifiée qui fonctionne lorsqu'elle est liée à un contrôle WPF.
grantnz
Je vois ça maintenant. En fait, j'ai trouvé une solution très élégante qui remplace l'événement CollectionChanged et effectue une boucle sur foreach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged )If handler.Target is CollectionView, alors vous pouvez déclencher le gestionnaire avec Action.Resetargs, sinon, vous pouvez fournir les arguments complets. Le meilleur des deux mondes sur une base gestionnaire par gestionnaire :). Un peu comme ce qui est ici: stackoverflow.com/a/3302917/529618
Alain
J'ai posté ma propre solution ci-dessous. stackoverflow.com/a/9416535/529618 Un grand merci à vous pour votre solution inspirante. Cela m'a amené à mi-chemin.
Alain
10

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

  • Pas besoin de créer une nouvelle classe et de remplacer les méthodes de ObservableCollection
  • Ne modifie pas le fonctionnement de NotifyCollectionChanged (donc pas de jouer avec Reset)
  • Ne fait pas usage de 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 Actionqui sera appelé avant que la collection ne soit effacée.

DeadlyEmbrace
la source
Très belle idée. Simple, élégant.
cplotts
9

J'ai trouvé une solution qui permet à l'utilisateur à la fois 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 satisfaire les besoins des UIElements pour obtenir les arguments Action.Reset alors 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 en fait regarder la cible de chaque gestionnaire enregistré et déterminer leur type. Étant donné que seules les classes ICollectionView nécessitent des NotifyCollectionChangedAction.Resetarguments 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. Voici 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
}
Alain
la source
7

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 effectuer le détachement sur l'événement dont je souhaitais me détacher.

J'espère que cette approche aide 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();
    }
}
cplotts
la source
1
Vous devez renommer votre classe en BrokenObservableCollection, non TrulyObservableCollection- vous ne comprenez pas ce que signifie l'action de réinitialisation.
Orion Edwards
1
@Orion Edwards: Je ne suis pas d'accord. Voir mon commentaire à votre réponse.
cplotts
1
@Orion Edwards: Oh, attendez, je vois, vous êtes drôle. Mais alors je devrais vraiment l'appeler:ActuallyUsefulObservableCollection . :)
cplotts
6
Lol grand nom. Je conviens que c'est un grave oubli dans la conception.
devios1
1
Si vous envisagez d'implémenter une nouvelle classe ObservableCollection de toute façon, il n'est pas nécessaire de créer un nouvel événement qui doit être surveillé séparément. Vous pouvez simplement empêcher ClearItems de déclencher un argument d'événement Action = Reset et le remplacer par un argument d'événement Action = Remove qui contient une liste e.OldItems de tous les éléments qui étaient dans la liste. Voir d'autres solutions dans cette question.
Alain
4

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 des 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 obtenais "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 dans 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);
        }
    }

}

la source
Approche intéressante. Merci de l'avoir publié. Si jamais je rencontre des problèmes avec ma propre approche, je pense que je reviendrai sur la vôtre.
cplotts
3

C'est ainsi que fonctionne ObservableCollection, vous pouvez contourner ce problème en conservant votre propre liste en dehors de ObservableCollection (l'ajout à la liste lorsque l'action est Ajouter, supprimer lorsque l'action est Supprimer, etc.), vous pouvez alors obtenir tous les éléments supprimés (ou éléments ajoutés ) lorsque l'action est Reset en comparant votre liste à 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 c'est beaucoup de frappe.

Nir
la source
J'ai envisagé de garder une trace d'une autre liste aussi bien que vous suggérez d'abord, mais cela semble être beaucoup de travail inutile. Votre deuxième suggestion est très proche de ce que j'ai fini par faire ... que je posterai comme réponse.
cplotts
3

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

Une autre solution «côté client» consiste à utiliser un gestionnaire d'événements au milieu. Enregistrez simplement tous les événements dans 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. J'ai utilisé ceci 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;
            }   
        }
    }
Chris
la source
Je crois qu'avec votre première approche, j'aurais besoin d'une autre liste pour suivre les éléments ... car une fois que vous obtenez l'événement CollectionChanged avec l'action Reset ... la collection est déjà vide. Je ne suis pas tout à fait votre deuxième suggestion. J'adorerais un simple test de harnais qui l'illustre, mais pour ajouter, supprimer et effacer ObservableCollection. Si vous créez un exemple, vous pouvez m'envoyer un e-mail à mon prénom suivi de mon nom de famille sur gmail.com.
cplotts
2

En regardant le NotifyCollectionChangedEventArgs , il semble que OldItems contient uniquement les éléments modifiés à la suite de 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'enregistre pas les éléments supprimés et n'appelle pas du tout le code Remove.

Tvanfosson
la source
6
J'ai vu ça aussi, mais je n'aime pas ça. Cela me semble être un trou béant.
cplotts
Il n'appelle pas le code de suppression car il n'en a pas besoin. Reset signifie "quelque chose de dramatique s'est produit, vous devez recommencer". Une opération claire en est un exemple, mais il y en a d'autres
Orion Edwards
2

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

Microsoft a mis BEAUCOUP de travail pour toujours s'assurer que NotifyCollectionChangedEventArgs ne dispose d'aucune donnée 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 pas de 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);
    }
HaxElit
la source
C'est cool, mais ne fonctionnerait probablement pas dans autre chose qu'un environnement de confiance totale. Réfléchir sur les champs privés nécessite une confiance totale, non?
Paul
1
Pourquoi voudriez-vous faire cela? Il y a d'autres choses qui peuvent provoquer le déclenchement de l'action de réinitialisation - ce n'est pas parce que vous avez désactivé la méthode d'effacement qu'elle a disparu (ou qu'elle devrait)
Orion Edwards
Approche intéressante, mais la réflexion peut être lente.
cplotts
2

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

Lorsque vous voulez 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 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é.

Rick Beerendonk
la source
+1 pour avoir souligné que Microsoft a conçu ObservableCollection avec un cas d'utilisation spécifique à l'esprit ... et avec un œil sur les performances. Je suis d'accord. A laissé un trou pour d'autres situations, mais je suis d'accord.
cplotts
-1 Je peux être intéressé par toutes sortes de choses. J'ai souvent besoin de l'index des éléments ajoutés / supprimés. Je souhaite peut-être optimiser le remplacement. Etc. La conception d'INotifyCollectionChanged est bonne. Le problème qui devrait être résolu est que personne ne l'a implémenté chez MS.
Aleksandr Dubinsky
1

Je passais juste en revue une partie du code de cartographie dans les boîtes à outils Silverlight et WPF et j'ai remarqué qu'ils résolvaient également 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 remplacé ClearItems, appelant Remove sur chaque élément en cours d'effacement.

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);
        }
    }
}
cplotts
la source
Je veux juste souligner que je n'aime pas autant cette approche que celle que j'ai marquée comme réponse ... puisque vous obtenez un événement NotifyCollectionChanged (avec une action Remove) ... pour CHAQUE élément supprimé.
cplotts
1

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 programmé Clear () correctement.

À mon avis, au moins, il faut un argument pour permettre de détacher des objets d'un événement ... mais je comprends aussi l'impact de celui-ci. Ensuite, j'ai imaginé 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 });
                }
            }
        }
    }
}
Eric Ouellet
la source
Je pense toujours que Microsoft devrait fournir un moyen de pouvoir effacer avec notification. Je pense toujours qu'ils ratent le coup en ne fournissant pas de cette façon. Désolé ! Je ne dis pas que clear devrait être supprimé, car il manque quelque chose !!! Pour obtenir un couplage bas, nous devons parfois être informés de ce qui a été supprimé.
Eric Ouellet
1

Pour faire 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 la collection.

Stéphane
la source
C'est très proche de ce que j'ai fait réellement ... voir la réponse acceptée.
cplotts
0

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 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);
}
hypehumain
la source
0

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 j'écrase le champ privé e._oldItems (via Reflections) avant de lancer base.OnCollectionChanged

Formentz
la source
0

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[]";
}
Artem Illarionov
la source
-4

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

Veuillez lire cette documentation les yeux ouverts et le cerveau allumé. Microsoft a tout fait correctement. Vous devez réexaminer votre collection lorsqu'elle envoie une notification de réinitialisation à votre place. Vous recevez une notification de réinitialisation, car lancer Ajouter / Supprimer pour chaque élément (être supprimé et ajouté à la collection) est trop coûteux.

Orion Edwards a tout à fait raison (respect, mec). Veuillez réfléchir plus largement lorsque vous lisez la documentation.

Dima
la source
5
Je pense en fait que vous et Orion avez raison dans votre compréhension de la façon dont Microsoft l'a conçu pour fonctionner. :) Cette conception m'a cependant causé des problèmes que je devais contourner pour ma situation. Cette situation est également courante ... et pourquoi j'ai posté cette question.
cplotts
Je pense que vous devriez examiner un peu plus ma question (et ma réponse marquée). Je ne suggérais pas de supprimer pour chaque élément.
cplotts
Et pour mémoire, je respecte la réponse d'Orion ... Je pense que nous nous amusions juste un peu les uns avec les autres ... du moins c'est ainsi que j'ai compris.
cplotts
Une chose importante: vous n'avez pas à détacher les procédures de gestion des événements des objets que vous supprimez. Le détachement se fait automatiquement.
Dima
1
Donc, en résumé, les événements ne sont pas détachés automatiquement lors de la suppression d'un objet d'une collection.
cplotts
-4

Si votre ObservableCollectionn'est pas clair, vous pouvez essayer 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
Manas
la source