web-dev-qa-db-fra.com

ObservableCollection qui surveille également les modifications apportées aux éléments de la collection

Existe-t-il une collection (BCL ou autre) présentant les caractéristiques suivantes:

Envoie un événement si la collection est modifiée ET envoie un événement si l'un des éléments de la collection envoie un événement PropertyChanged. Triez un ObservableCollection<T>T: INotifyPropertyChanged et la collection surveillent également les éléments pour y apporter des modifications.

Je pourrais emballer moi-même une collection observable et faire en sorte que l'événement s'abonne/se désabonne lorsque des éléments de la collection sont ajoutés/supprimés, mais je me demandais si des collections existantes l'avaient déjà fait?

32
soren.enemaerke

J'ai fait une rapide implémentation moi-même:

public class ObservableCollectionEx<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        Unsubscribe(e.OldItems);
        Subscribe(e.NewItems);
        base.OnCollectionChanged(e);
    }

    protected override void ClearItems()
    {
        foreach(T element in this)
            element.PropertyChanged -= ContainedElementChanged;

        base.ClearItems();
    }

    private void Subscribe(IList iList)
    {
        if (iList != null)
        {
            foreach (T element in iList)
                element.PropertyChanged += ContainedElementChanged;
        }
    }

    private void Unsubscribe(IList iList)
    {
        if (iList != null)
        {
            foreach (T element in iList)
                element.PropertyChanged -= ContainedElementChanged;
        }
    }

    private void ContainedElementChanged(object sender, PropertyChangedEventArgs e)
    {
        OnPropertyChanged(e);
    }
}

Admis, il serait un peu déroutant et trompeur de faire feu PropertyChanged sur la collection lorsque la propriété qui a réellement été modifiée se trouve sur un élément contenu, mais cela conviendrait à mon objectif spécifique. Il pourrait être étendu avec un nouvel événement déclenché à la place de ContainerElementChanged

Pensées?

EDIT: Notez que BCL ObservableCollection n’expose que l’interface INotifyPropertyChanged via une implémentation explicite. Vous devez donc fournir un transt pour pouvoir attacher à l’événement de la manière suivante:

ObservableCollectionEx<Element> collection = new ObservableCollectionEx<Element>();
((INotifyPropertyChanged)collection).PropertyChanged += (x,y) => ReactToChange();

EDIT2: Ajout du traitement des ClearItems, merci Josh

EDIT3: Ajout d'une désinscription correcte pour PropertyChanged, merci Mark

EDIT4: Wow, c'est vraiment apprendre-comme-vous-allez :). KP a noté que l'événement avait été déclenché avec la collection en tant qu'expéditeur et non avec l'élément lorsque l'élément contenu était modifié. Il a suggéré de déclarer un événement PropertyChanged sur la classe marquée avec new . Cela aurait quelques problèmes que je vais essayer d'illustrer avec l'exemple ci-dessous:

  // work on original instance
  ObservableCollection<TestObject> col = new ObservableCollectionEx<TestObject>();
  ((INotifyPropertyChanged)col).PropertyChanged += (s, e) => { Trace.WriteLine("Changed " + e.PropertyName); };

  var test = new TestObject();
  col.Add(test); // no event raised
  test.Info = "NewValue"; //Info property changed raised

  // working on explicit instance
  ObservableCollectionEx<TestObject> col = new ObservableCollectionEx<TestObject>();
  col.PropertyChanged += (s, e) => { Trace.WriteLine("Changed " + e.PropertyName); };

  var test = new TestObject();
  col.Add(test); // Count and Item [] property changed raised
  test.Info = "NewValue"; //no event raised

L'exemple ci-dessous montre que le fait de "remplacer" l'événement a pour effet secondaire de faire très attention au type de variable que vous utilisez lorsque vous vous abonnez à l'événement, car cela détermine les événements que vous recevez.

43
soren.enemaerke

@ soren.enemaerke: J'aurais fait ce commentaire sur votre réponse, mais je ne peux pas (je ne sais pas pourquoi, peut-être parce que je n'ai pas beaucoup de points de rep). Quoi qu'il en soit, je pensais simplement que je mentionnerais que dans votre code, vous avez posté, je ne pense pas que le désabonnement fonctionnerait correctement car il crée un nouveau lambda en ligne et tente ensuite de supprimer le gestionnaire d'événements correspondant.

Je voudrais changer les lignes du gestionnaire d’événements d’ajout/suppression en quelque chose comme:

element.PropertyChanged += ContainedElementChanged;

et

element.PropertyChanged -= ContainedElementChanged;

Et puis modifiez la signature de la méthode ContainedElementChanged en:

private void ContainedElementChanged(object sender, PropertyChangedEventArgs e)

Cela reconnaîtrait que la suppression concerne le même gestionnaire que l’ajouté, puis la supprime correctement. J'espère que ça aide quelqu'un :)

7
Mark Whitfeld

Si vous voulez utiliser quelque chose de construit dans le framework, vous pouvez utiliser FreezableCollection . Ensuite, vous voudrez écouter l’événement Changed .

Se produit lorsque l'objet Freezable ou un objet qu'il contient est modifié.

Voici un petit échantillon. La méthode collection_Changed sera appelée deux fois.

public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();

        FreezableCollection<SolidColorBrush> collection = new FreezableCollection<SolidColorBrush>();
        collection.Changed += collection_Changed;
        SolidColorBrush brush = new SolidColorBrush(Colors.Red);
        collection.Add(brush);
        brush.Color = Colors.Blue;
    }

    private void collection_Changed(object sender, EventArgs e)
    {
    }
}
3
Todd White

Je voudrais utiliser ReactiveUI'sReactiveCollection:

reactiveCollection.Changed.Subscribe(_ => ...);
1
Lukas Cenovsky

Rxx 2.0 contient opérateurs qui, associé à ce opérateur de conversion pour ObservableCollection<T>, facilite la réalisation de votre objectif.

ObservableCollection<MyClass> collection = ...;

var changes = collection.AsCollectionNotifications<MyClass>();
var itemChanges = changes.PropertyChanges();
var deepItemChanges = changes.PropertyChanges(
  item => item.ChildItems.AsCollectionNotifications<MyChildClass>());

Les modèles de notification de propriété modifiés suivants sont pris en charge pour MyClass et MyChildClass:

  • INotifyPropertyChanged
  • [Propriété] Modèle d'événement modifié (hérité, à utiliser par le modèle de composant)
  • Propriétés de dépendance WPF
0
Dave Sexton

Le moyen le plus simple de le faire est de le faire

using System.ComponentModel;
public class Example
{
    BindingList<Foo> _collection;

    public Example()
    {
        _collection = new BindingList<Foo>();
        _collection.ListChanged += Collection_ListChanged;
    }

    void Collection_ListChanged(object sender, ListChangedEventArgs e)
    {
        MessageBox.Show(e.ListChangedType.ToString());
    }

}

La BindingList class a été dans .net sence 2.0. Il déclenchera son événement ListChanged chaque fois qu'un élément de la collection sera déclenché INotifyPropertyChanged.

0
Scott Chamberlain

Découvrez la Bibliothèque de collections génériques C5 . Toutes ses collections contiennent des événements que vous pouvez utiliser pour attacher des rappels lorsque des éléments sont ajoutés, supprimés, insérés, effacés ou lorsque la collection est modifiée.

Je travaille pour certaines extensions de cette librairie ici qui, dans un avenir proche, devraient permettre des événements de "prévisualisation" qui pourraient vous permettre d’annuler un ajout ou une modification.

0
Marcus Griep

@ soren.enemaerke A fait une réponse pour pouvoir poster le code correct, car la section commentaires de votre réponse le rendrait illisible. Le seul problème que j'ai eu avec la solution est que l'élément particulier qui déclenche l'événement PropertyChanged est perdu et que vous n'avez aucun moyen de le savoir dans l'appel PropertyChanged.

col.PropertyChanged += (s, e) => { Trace.WriteLine("Changed " + e.PropertyName)

Pour résoudre ce problème, j'ai créé une nouvelle classe PropertyChangedEventArgsEx et modifié la méthode ContainedElementChanged au sein de votre classe.

nouvelle classe

public class PropertyChangedEventArgsEx : PropertyChangedEventArgs
{
    public object Sender { get; private set; }

    public PropertyChangedEventArgsEx(string propertyName, object sender) 
        : base(propertyName)
    {
        this.Sender = sender;
    }
}

changements dans votre classe

 private void ContainedElementChanged(object sender, PropertyChangedEventArgs e)
    {
        var ex = new PropertyChangedEventArgsEx(e.PropertyName, sender);
        OnPropertyChanged(ex);
    }

Après cela, vous pouvez obtenir l’élément Sender dans col.PropertyChanged += (s, e) en convertissant e en PropertyChangedEventArgsEx

((INotifyPropertyChanged)col).PropertyChanged += (s, e) =>
        {
            var argsEx = (PropertyChangedEventArgsEx)e;
            Trace.WriteLine(argsEx.Sender.ToString());
        };

Encore une fois, vous devriez noter que la s est la collection d’éléments, et non l’élément réel qui a déclenché l’événement. D'où la nouvelle propriété Sender dans la classe PropertyChangedEventArgsEx.

0
Ovi