2008-11-06 8 views
32

Y at-il une collection (BCL ou autre) qui présente les caractéristiques suivantes:ObservableCollection qui surveille également des changements sur les éléments de collection

envois Aux événement si la collecte est modifiée et envoie l'événement si l'un des éléments de la collection envoie un événement PropertyChanged. Sort d'un ObservableCollection<T>T: INotifyPropertyChanged et la collection surveille également les éléments pour les changements.

Je pourrais envelopper moi-même une collection observable et faire l'événement s'abonner/se désabonner quand des éléments de la collection sont ajoutés/supprimés mais je me demandais si des collections existantes l'avaient déjà fait?

Répondre

43

Fait une mise en œuvre rapide 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); 
    } 
} 

admises, il serait assez confus et trompeur d'avoir le feu PropertyChanged sur la collection lorsque la propriété qui en fait changé est un élément contenu, mais il correspond à mon objectif spécifique. Il pourrait être étendu avec un nouvel événement qui est déclenché à l'intérieur de ContainerElementChanged

Réflexions?

EDIT: Il est à noter que la BCL ObservableCollection expose que l'interface INotifyPropertyChanged par une mise en œuvre explicite de sorte que vous devrez fournir une distribution afin d'attacher à l'événement comme ceci:

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

EDIT2: manutention Ajouté de ClearItems, merci Josh

EDIT3: Ajout d'un désabonnement correct pour PropertyChanged, merci Mark

EDIT4: Wow, c'est apprendre-as-you-go vraiment :). KP a noté que l'événement a été déclenché avec la collection en tant qu'expéditeur et non avec l'élément lorsque l'élément a a été modifié. Il a suggéré de déclarer un événement PropertyChanged sur la classe marquée new. Cela aurait quelques questions que je vais tenter 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 

Vous pouvez voir de l'échantillon que « majeur » l'événement a pour effet secondaire que vous devez être extrêmement prudent dont type de variable que vous utilisez lors de l'abonnement à l'événement, car cela détermine les événements que vous recevez.

+0

Marquer cela comme la réponse car rien de mieux (à mon avis) a fait surface –

+4

Il y a cependant un problème avec cette classe. Si vous appelez Clear(), l'événement OnCollectionChanged recevra une notification de réinitialisation et vous n'aurez pas accès aux éléments qui ont été supprimés de la collection. Cela peut être atténué en remplaçant ClearItems et en désinscrivant les gestionnaires avant d'appeler base.ClearItems(). – Josh

+0

Bien attrapé, Josh. J'ai mis à jour le code pour refléter cela. –

0

Consultez le C5 Generic Collection Library. 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 change.

Je travaille pour quelques extensions à cette librairie here qui dans un proche avenir devrait permettre des "prévisualisation" des événements qui pourraient vous permettre d'annuler un ajout ou un changement.

3

Si vous souhaitez utiliser quelque chose intégré dans le cadre, vous pouvez utiliser FreezableCollection. Ensuite, vous voulez écouter le Changed event.

Se produit lorsque le 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) 
    { 
    } 
} 
+7

Vous devez également noter que FreezableCollections est contraint de contenir uniquement les éléments qui héritent de DependencyObject. –

7

@ soren.enemaerke: j'aurais fait ce commentaire sur votre message de 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, j'ai juste pensé que je mentionnerais que dans votre code que vous avez posté, je ne pense pas que le désabonnement fonctionnerait correctement parce qu'il crée un nouveau lambda en ligne et essaie ensuite de supprimer le gestionnaire d'événements.

Je changerais Ajout/Suppression de lignes de gestionnaire d'événement à quelque chose comme:

element.PropertyChanged += ContainedElementChanged; 

et

element.PropertyChanged -= ContainedElementChanged; 

Et puis changer la signature de la méthode ContainedElementChanged à:

private void ContainedElementChanged(object sender, PropertyChangedEventArgs e) 

Cette reconnaîtrait que la suppression est pour le même gestionnaire que l'ajouter, puis retirez-le corr ectly. Espérons que cela aide quelqu'un :)

+1

Merci Mark, mis à jour ma réponse pour refléter vos idées ... –

1

j'utiliser ReactiveUI'sReactiveCollection:

reactiveCollection.Changed.Subscribe(_ => ...); 
0

@ soren.enemaerke en a fait une réponse afin d'afficher le code approprié que la section commentaires de votre réponse 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 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, je l'ai créé une nouvelle classe PropertyChangedEventArgsEx et a changé 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 à 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 réelle col.PropertyChanged += (s, e) par coulée e-PropertyChangedEventArgsEx

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

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

0

Rxx 2.0 contient operators qui avec ce conversion operator pour ObservableCollection<T>, il est facile d'atteindre votre objectif.

ObservableCollection<MyClass> collection = ...; 

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

La propriété suivante a changé les modèles de notification sont pris en charge pour MyClass et MyChildClass:

  • INotifyPropertyChanged
  • [Propriété] pattern Changed événement (héritage, pour une utilisation par Component Model)
  • dépendance WPF propriétés
0

La façon il est plus simple à faire il suffit de 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 classe BindingList comme été en .net 2.0 sence. Il déclenchera son événement ListChanged à chaque fois qu'un élément de la collection déclenchera INotifyPropertyChanged.

Questions connexes