2009-05-23 5 views
36

J'ai vu beaucoup de discussions à propos de cette question, mais peut-être que je suis juste trop d'un débutant pour l'obtenir. Si j'ai une collection observable qui est une collection de « PersonNames » comme dans l'exemple msdn (http: //msdn.microsoft.com/en-us/library/ms748365.aspx), je reçois des mises à jour à mon avis, si un PersonName est ajouté ou supprimé, etc. Je veux une mise à jour mon avis quand je changez également une propriété dans le PersonName. Comme si je change le prénom. Je peux implémenter OnPropertyChanged pour chaque propriété et avoir cette classe dérive de INotifyPropertyChanged et cela semble être appelé comme prévu. Ma question est, comment la vue obtient-elle les données mises à jour à partir du ObservableCollection comme la propriété a changé ne provoque aucun événement pour le ObservableCollection. C'est probablement quelque chose de vraiment simple mais pourquoi je n'arrive pas à trouver un exemple me surprend. Quelqu'un peut-il faire la lumière sur ce point pour moi ou avoir des indications sur des exemples que je serais très reconnaissant. Nous avons ce scénario à plusieurs endroits dans notre application WPF actuelle et nous avons du mal à le comprendre.ObservableCollection et Item PropertyChanged


« En général, le code pour l'affichage des données ajoute un gestionnaire d'événements PropertyChanged à chaque objet actuellement affiché à l'écran. »

Quelqu'un pourrait-il me donner un exemple de ce que cela signifie? Ma vue se lie à mon ViewModel qui a un ObservableCollection. Cette collection est constituée d'un RowViewModel dont les propriétés prennent en charge l'événement PropertiesChanged. Mais je n'arrive pas à comprendre comment mettre la collection à jour pour que ma vue soit mise à jour.

Répondre

4

Comme vous l'avez découvert, il n'y a aucun événement niveau de collecte qui indique qu'une propriété d'un élément de la collection a changé. Généralement, le code responsable de l'affichage des données ajoute un gestionnaire d'événements PropertyChanged à chaque objet actuellement affiché à l'écran.

+0

Merci. J'utilise WPF et ai un DataGrid dont ItemsSource lie en XAML à ObservableCollection. Donc, j'ai besoin d'ajouter du code quelque part dans mon ViewModel pour gérer l'événement PropertyChanged afin que la vue à savoir pour mettre à jour le DataGrid? Et puis dois-je supprimer et ajouter l'élément à la collection pour l'obtenir la vue pour le mettre à jour? Cela semble contre-intuitif (mais cela ne signifie pas que ce n'est pas correct :) –

+0

Le DataGrid le fait automatiquement si les éléments de ObservableCollection implémentent INotifyPropertyChanged (ou sont DependencyObjects). – Goblin

52

Voici comment vous attacher/détacher à l'événement PropertyChanged de chaque élément.

ObservableCollection<INotifyPropertyChanged> items = new ObservableCollection<INotifyPropertyChanged>(); 
items.CollectionChanged += items_CollectionChanged; 

static void items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 
{ 
    if (e.OldItems != null) 
    { 
     foreach (INotifyPropertyChanged item in e.OldItems) 
      item.PropertyChanged -= item_PropertyChanged; 
    } 
    if (e.NewItems != null) 
    { 
     foreach (INotifyPropertyChanged item in e.NewItems) 
      item.PropertyChanged += item_PropertyChanged; 
    } 
} 

static void item_PropertyChanged(object sender, PropertyChangedEventArgs e) 
{ 
    throw new NotImplementedException(); 
} 
+1

C'est magnifique. Je l'ai cherché pendant un moment et cela m'a vraiment aidé. Merci beaucoup. – pqsk

+1

Pourquoi les deux fonctions modifiées sont-elles statiques? –

+1

Ça fait longtemps, et je ne trouve pas la source que j'ai extraite pour cet exemple. Je pense que j'utilisais les propriétés de dépendance WPF qui étaient statiques. Je ne vois pas pourquoi ces fonctions ne peuvent pas être par instance. – chilltemp

13

Bill,

Je suis sûr que vous avez trouvé une solution ou d'une solution à votre problème maintenant, mais je a publié ce billet pour quiconque cette question commune. Vous pouvez substituer cette classe à ObservableCollections qui sont des collections d'objets qui implémentent INotifyPropertyChanged. C'est un peu draconien, car il indique que la liste doit être réinitialisée plutôt que de trouver la propriété/l'élément qui a changé, mais pour les petites listes, le résultat de la performance doit être invisible.

Marc

using System.Collections.Generic; 
using System.Collections.ObjectModel; 
using System.Collections.Specialized; 
using System.ComponentModel; 

namespace WCIOPublishing.Helpers 
{ 
    public class ObservableCollectionWithItemNotify<T> : ObservableCollection<T> where T: INotifyPropertyChanged 
    { 

     public ObservableCollectionWithItemNotify() 
     { 
      this.CollectionChanged += items_CollectionChanged; 
     } 


     public ObservableCollectionWithItemNotify(IEnumerable<T> collection) :base(collection) 
     { 
      this.CollectionChanged += items_CollectionChanged; 
      foreach (INotifyPropertyChanged item in collection) 
       item.PropertyChanged += item_PropertyChanged; 

     } 

     private void items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 
     { 
      if(e != null) 
      { 
       if(e.OldItems!=null) 
        foreach (INotifyPropertyChanged item in e.OldItems) 
         item.PropertyChanged -= item_PropertyChanged; 

       if(e.NewItems!=null) 
        foreach (INotifyPropertyChanged item in e.NewItems) 
         item.PropertyChanged += item_PropertyChanged; 
      } 
     } 

     private void item_PropertyChanged(object sender, PropertyChangedEventArgs e) 
     { 
      var reset = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); 
      this.OnCollectionChanged(reset); 

     } 

    } 
} 
+0

C'est bon. Mais heureusement, vous n'avez pas besoin de réinitialiser la collection, vous pouvez également remplacer par: var replace = new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Replace, expéditeur, expéditeur, this.Items.IndexOf (expéditeur (T))); this.OnCollectionChanged (remplacer); – infografnet

+0

@ Marc-Ziss C'est une bonne solution. – likebobby

+0

@infografnet Je ne peux pas le faire fonctionner avec Remplacer à la place. Cela peut-il être dû au fait que oldItem et newItem sont tous les deux expéditeurs? J'ai vérifié que la modification de la propriété ne vérifie pas si la valeur est la même qu'avant. – likebobby

19

Nous avons écrit cela dans le WPF-chat:

public class OcPropertyChangedListener<T> : INotifyPropertyChanged where T : INotifyPropertyChanged 
{ 
    private readonly ObservableCollection<T> _collection; 
    private readonly string _propertyName; 
    private readonly Dictionary<T, int> _items = new Dictionary<T, int>(new ObjectIdentityComparer()); 
    public OcPropertyChangedListener(ObservableCollection<T> collection, string propertyName = "") 
    { 
     _collection = collection; 
     _propertyName = propertyName ?? ""; 
     AddRange(collection); 
     CollectionChangedEventManager.AddHandler(collection, CollectionChanged); 
    } 

    private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 
    { 
     switch (e.Action) 
     { 
      case NotifyCollectionChangedAction.Add: 
       AddRange(e.NewItems.Cast<T>()); 
       break; 
      case NotifyCollectionChangedAction.Remove: 
       RemoveRange(e.OldItems.Cast<T>()); 
       break; 
      case NotifyCollectionChangedAction.Replace: 
       AddRange(e.NewItems.Cast<T>()); 
       RemoveRange(e.OldItems.Cast<T>()); 
       break; 
      case NotifyCollectionChangedAction.Move: 
       break; 
      case NotifyCollectionChangedAction.Reset: 
       Reset(); 
       break; 
      default: 
       throw new ArgumentOutOfRangeException(); 
     } 

    } 

    private void AddRange(IEnumerable<T> newItems) 
    { 
     foreach (T item in newItems) 
     { 
      if (_items.ContainsKey(item)) 
      { 
       _items[item]++; 
      } 
      else 
      { 
       _items.Add(item, 1); 
       PropertyChangedEventManager.AddHandler(item, ChildPropertyChanged, _propertyName); 
      } 
     } 
    } 

    private void RemoveRange(IEnumerable<T> oldItems) 
    { 
     foreach (T item in oldItems) 
     { 
      _items[item]--; 
      if (_items[item] == 0) 
      { 
       _items.Remove(item); 
       PropertyChangedEventManager.RemoveHandler(item, ChildPropertyChanged, _propertyName); 
      } 
     } 
    } 

    private void Reset() 
    { 
     foreach (T item in _items.Keys.ToList()) 
     { 
      PropertyChangedEventManager.RemoveHandler(item, ChildPropertyChanged, _propertyName); 
      _items.Remove(item); 
     } 
     AddRange(_collection); 
    } 

    public event PropertyChangedEventHandler PropertyChanged; 

    protected virtual void ChildPropertyChanged(object sender, PropertyChangedEventArgs e) 
    { 
     PropertyChangedEventHandler handler = PropertyChanged; 
     if (handler != null) 
      handler(sender, e); 
    } 

    private class ObjectIdentityComparer : IEqualityComparer<T> 
    { 
     public bool Equals(T x, T y) 
     { 
      return object.ReferenceEquals(x, y); 
     } 
     public int GetHashCode(T obj) 
     { 
      return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); 
     } 
    } 
} 

public static class OcPropertyChangedListener 
{ 
    public static OcPropertyChangedListener<T> Create<T>(ObservableCollection<T> collection, string propertyName = "") where T : INotifyPropertyChanged 
    { 
     return new OcPropertyChangedListener<T>(collection, propertyName); 
    } 
} 
  • événements faibles
  • garde la trace du même élément ajouté à plusieurs reprises à la collection
  • Il ~ bulles ~ up la propriété a changé les événements des enfants.
  • La classe statique est juste pour la commodité.

utiliser comme ceci:

var listener = OcPropertyChangedListener.Create(yourCollection); 
listener.PropertyChanged += (sender, args) => { //do you stuff} 
+0

Très sympa - J'aime particulièrement utiliser des événements faibles pour suivre les éléments, car cela élimine une grande partie de la complexité du désabonnement et le rend plus utile. –

+0

Compte tenu de toutes les différentes solutions, je pense que celui-ci est la meilleure mise en œuvre jusqu'à présent. Bien joué Johan. – Maverik

+0

Solution vraiment bien rangé, et fonctionne magnifiquement. – Ted

1

Au lieu de ObservableCollection utiliser simplement le BindingList<T>.
Le code suivant montre une liaison DataGrid à une liste et aux propriétés de l'élément.

<Window x:Class="WpfApplication1.MainWindow" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        Title="MainWindow" Height="350" Width="525"> 
    <DataGrid ItemsSource="{Binding}" AutoGenerateColumns="False" > 
     <DataGrid.Columns> 
      <DataGridTextColumn Header="Values" Binding="{Binding Value}" /> 
     </DataGrid.Columns> 
    </DataGrid> 
</Window> 

using System; 
using System.ComponentModel; 
using System.Windows; 
using System.Windows.Threading; 

namespace WpfApplication1 { 
    public partial class MainWindow : Window { 
     public MainWindow() { 
      var c = new BindingList<Data>(); 
      this.DataContext = c; 
      // add new item to list on each timer tick 
      var t = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) }; 
      t.Tick += (s, e) => { 
       if (c.Count >= 10) t.Stop(); 
       c.Add(new Data()); 
      }; 
      t.Start(); 
     } 
    } 

    public class Data : INotifyPropertyChanged { 
     public event PropertyChangedEventHandler PropertyChanged = delegate { }; 
     System.Timers.Timer t; 
     static Random r = new Random(); 
     public Data() { 
      // update value on each timer tick 
      t = new System.Timers.Timer() { Interval = r.Next(500, 1000) }; 
      t.Elapsed += (s, e) => { 
       Value = DateTime.Now.Ticks; 
       this.PropertyChanged(this, new PropertyChangedEventArgs("Value")); 
      }; 
      t.Start(); 
     } 
     public long Value { get; private set; } 
    } 
} 
1

Voici le code donnant une explication simple de réponse par @Stack et en montrant comment BindingList est l'observation si elle a un élément modifié et montre ObservableCollection ne sera pas observer le changement à l'intérieur d'un élément.

using System; 
using System.Collections.ObjectModel; 
using System.ComponentModel; 

namespace BindingListExample 
{ 
    class Program 
    { 
     public ObservableCollection<MyStruct> oc = new ObservableCollection<MyStruct>(); 
     public System.ComponentModel.BindingList<MyStruct> bl = new BindingList<MyStruct>(); 

     public Program() 
     { 
      oc.Add(new MyStruct()); 
      oc.CollectionChanged += CollectionChanged; 

      bl.Add(new MyStruct()); 
      bl.ListChanged += ListChanged; 
     } 

     void ListChanged(object sender, ListChangedEventArgs e) 
     { 
      //Observe when the IsActive value is changed this event is triggered. 
      Console.WriteLine(e.ListChangedType.ToString()); 
     } 

     void CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) 
     { 
      //Observe when the IsActive value is changed this event is not triggered. 
      Console.WriteLine(e.Action.ToString()); 
     } 

     static void Main(string[] args) 
     { 
      Program pm = new Program(); 
      pm.bl[0].IsActive = false; 
     } 
    } 

    public class MyStruct : INotifyPropertyChanged 
    { 
     public event PropertyChangedEventHandler PropertyChanged; 
     private bool isactive; 
     public bool IsActive 
     { 
      get { return isactive; } 
      set 
      { 
       isactive = value; 
       NotifyPropertyChanged("IsActive"); 
      } 
     } 

     private void NotifyPropertyChanged(String PropertyName) 
     { 
      if (PropertyChanged != null) 
      { 
       PropertyChanged(this, new PropertyChangedEventArgs(PropertyName)); 
      } 
     } 
    } 
} 
Questions connexes