98 votes

Lors de l'effacement d'une ObservableCollection, il n'y a aucun élément dans e.OldItems

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énement attaché à l'événement CollectionChanged.

Quand vous Clair la collection, cela provoque un événement CollectionChanged avec e.Action défini sur NotifyCollectionChangedAction.Reset. C'est normal. Mais ce qui est bizarre, 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é retirés de la collection.

Quelqu'un d'autre a vu ça ? Et si oui, comment l'ont-ils contourné ?

Un peu de contexte : J'utilise l'événement CollectionChanged pour attacher et détacher un autre événement et donc si je n'ai pas d'éléments dans e.OldItems ... je ne pourrai pas me détacher de cet événement.

CLARIFICATION : Je sais que la documentation n'a pas carrément qu'il doit se comporter de cette façon. Mais pour toute autre action, il m'informe de ce qu'il a fait. Donc, je suppose qu'il me le 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 };
    }
}

0 votes

Pourquoi devez-vous désinscrire l'événement ? Dans quel sens vous abonnez-vous ? Les événements créent une référence à l'abonné détenu par le raisonneur, et non l'inverse. Si les raisonneurs sont des éléments d'une collection qui est vidée, ils seront vidés en toute sécurité et les références disparaîtront - pas de fuite. Si les éléments sont les abonnés et sont référencés par un éleveur, alors il suffit de mettre l'événement à null dans l'éleveur lorsque vous obtenez un Reset - pas besoin de désabonner individuellement les éléments.

0 votes

Croyez-moi, je sais comment ça marche. L'événement en question concernait un singleton qui est resté en place pendant un long moment... donc les éléments de la collection étaient les abonnés. Votre solution, qui consiste à attribuer la valeur null à l'événement, ne fonctionne pas... puisque l'événement doit quand même être déclenché... en notifiant éventuellement d'autres abonnés (pas nécessairement ceux de la collection).

47voto

Orion Edwards Points 54939

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

Cela signifie qu'un événement dramatique s'est produit, et que le coût des ajouts/retraits serait probablement supérieur au coût d'une nouvelle analyse de la liste depuis le début... c'est donc ce que vous devez faire.

MSDN propose un exemple de la collection entière triée à nouveau comme candidat à la réinitialisation.

Je le répète. Réinitialisation ne veut pas dire clarté cela signifie Vos hypothèses sur la liste sont maintenant invalides. Traitez-la comme s'il s'agissait d'une liste entièrement nouvelle. . Clear en est un exemple, 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 aux données d'un WPF. ListView pour s'afficher à l'écran.
Si vous effacez la liste et augmentez le .Reset les performances sont pratiquement instantanées, mais si, au lieu de cela, vous déclenchez de nombreux événements individuels .Remove la performance est terrible, car WPF supprime les éléments un par un. J'ai également utilisé .Reset dans mon propre code pour indiquer que la liste a été triée à nouveau, plutôt que d'émettre des milliers de messages individuels Move opérations. Comme pour Clear, il y a une forte baisse de performance lorsque l'on lève de nombreux événements individuels.

1 votes

Je vais respectueusement ne pas être d'accord sur ce point. Si vous regardez la documentation, elle indique : Représente une collection de données dynamique qui fournit des notifications lorsque des éléments sont ajoutés, supprimés ou lorsque la liste entière est rafraîchie (voir msdn.microsoft.com/fr/us/library/ms668613(v=VS.100).aspx )

0 votes

Je veux juste souligner à nouveau (comme je l'ai fait dans la question), que je sais que la documentation ne dit pas DIRECTEMENT que l'appel à Clear doit vous informer des éléments qui sont supprimés ... mais dans ce cas, il me semble que la collection n'observe PAS vraiment les changements.

6 votes

La documentation indique qu'il doit vous notifier lorsque des éléments sont ajoutés/supprimés/rafraîchis, mais il ne promet pas de vous donner 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û mettre tous les éléments dans le fichier OldItems lors de l'effacement, (il s'agit simplement de copier une liste), mais peut-être y a-t-il un scénario où cela est trop coûteux. Quoi qu'il en soit, si vous voulez une collection qui fait vous informer de tous les éléments supprimés, ce ne serait pas difficile à faire.

24voto

decasteljau Points 3305

Nous avons eu le même problème ici. L'action Reset dans CollectionChanged n'inclut pas les OldItems. Nous avons trouvé 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 par lancer une NotSupportedException dans l'événement CollectionChanged pour les actions de réinitialisation. La fonction RemoveAll déclenchera une action Remove dans l'événement CollectionChanged, avec les bons OldItems.

0 votes

Bonne idée. Je n'aime pas ne pas prendre en charge Clear, car c'est la méthode (selon mon expérience) que la plupart des gens utilisent... mais au moins, vous avertissez l'utilisateur avec une exception.

0 votes

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.

0 votes

Vous n'êtes pas censé utiliser les anciens articles ! Ce que vous êtes censé faire, c'est jeter toutes les données que vous avez sur la liste, et la re-scanner comme si c'était une nouvelle liste !

13voto

grantnz Points 3569

Une autre option consiste à remplacer l'événement Reset par un événement Remove unique qui contient 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. Il n'est pas nécessaire de s'abonner à un événement supplémentaire (comme l'exige la réponse acceptée).

  2. Ne génère pas d'événement pour chaque objet retiré (certaines autres solutions proposées entraînent des événements multiples pour les objets retirés).

  3. L'abonné n'a qu'à vérifier NewItems et OldItems sur n'importe quel événement pour ajouter/supprimer les gestionnaires d'événements si nécessaire.

Inconvénients :

  1. Non É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 WPF basés sur des listes, l'effacement d'une collection ObservableCollectionNoReset comportant plusieurs éléments entraîne une exception "Range actions not supported". 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.

0 votes

J'aime bien cette idée mais malheureusement le NotifyCollectionChangedEventArgs de Silverlight 4 n'a pas de constructeur qui prend une liste d'éléments.

3 votes

J'ai adoré cette solution, mais elle ne fonctionne pas... Vous n'êtes pas autorisé à déclencher un NotifyCollectionChangedEventArgs dont plus d'un élément a été modifié, sauf si l'action est "Reset". Vous obtenez une exception Range actions are not supported. Je ne sais pas pourquoi il fait ça, mais maintenant cela ne laisse pas d'autre choix que de supprimer chaque élément un par un...

2 votes

@Alain L'ObservableCollection n'impose pas cette restriction. Je pense 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 poster une mise à jour avec ma solution. Je vais éditer ma réponse avec la classe modifiée qui fonctionne lorsqu'elle est liée à un contrôle WPF.

9voto

Alain Points 10079

J'ai trouvé une solution qui permet à l'utilisateur de profiter de 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 de UIElements pour obtenir les arguments de l'événement Action.Reset alors que tous les autres utilisateurs voudraient une liste des éléments ajoutés et supprimés.

Cette solution consiste à remplacer l'événement CollectionChanged. Lorsque nous allons déclencher cet événement, nous pouvons examiner la cible de chaque gestionnaire enregistré et déterminer son type. Étant donné que seules les classes ICollectionView nécessitent NotifyCollectionChangedAction.Reset lorsque plus d'un élément change, nous pouvons les isoler et donner à tous les autres des arguments d'événement appropriés contenant la liste complète des éléments supprimés ou ajoutés. Voici l'implémentation.

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
}

8voto

cplotts Points 7630

Ok, même si je souhaite toujours que ObservableCollection se comporte comme je le souhaite ... le code ci-dessous est ce que j'ai fini par faire. En gros, j'ai créé une nouvelle collection de T appelée TrulyObservableCollection et j'ai surchargé la méthode ClearItems que j'ai ensuite utilisée pour lever un événement Clearing.

Dans le code qui utilise cette TrulyObservableCollection, j'utilise l'événement Clearing pour parcourir les éléments en boucle. qui sont encore dans la collection à ce moment-là 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();
    }
}

1 votes

Vous devez renommer votre classe en BrokenObservableCollection pas TrulyObservableCollection - vous comprenez mal ce que signifie l'action de réinitialisation.

1 votes

@Orion Edwards : Je ne suis pas d'accord. Voir mon commentaire à votre réponse.

1 votes

@Orion Edwards : Oh, attends, je vois, tu es drôle. Mais alors je devrais vraiment l'appeler : ActuallyUsefulObservableCollection . :)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X