How to propagate property change notifications of objects within collections
Asked Answered
A

2

6

Lets say I have classes like this

public class R
{
    protected string name;
    protected List<S> listOfObjectS;
}

public class S
{
    private string name, ID;
    private A objectA;
}

public class A
{
    private string name;
    private int count;
}

If a user has two views open, one displaying instances of R and another allowing users to modify an instance of A, I need the view of R to change when the user changes any instance of A.

If the user changes a property of an instance of A, what is the best way to propagate that change (through instances of S) so that all instances of R display the new state of A?

Adit answered 30/12, 2013 at 19:30 Comment(0)
M
10

EDIT: Overhauling this answer to be more specific to the question since the tags show you already knew about INotifyPropertyChanged.

You need to implement INotifyPropertyChanged in class A and in class S. Make it so objectA can only be set through a property that will raise the PropertyChanged event on S whenever a property is changed in A. Example:

public class A : INotifyPropertyChanged
{
    private string name;
    public string Name
    {
        get { return name; }
        set { name = value; OnPropertyChanged("Name"); }
    }

    private int count;

    public int Count
    {
        get { return count; } 
        set { count = value; OnPropertyChanged("Count"); }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

... and class S...

public class S : INotifyPropertyChanged
{
    private string name, ID;
    private A objectA;

    public A ObjectA
    {
        get { return objectA; }
        set
        {
            var old = objectA;
            objectA = value;

            // Remove the event subscription from the old instance.
            if (old != null) old.PropertyChanged -= objectA_PropertyChanged;

            // Add the event subscription to the new instance.
            if (objectA != null) objectA.PropertyChanged += objectA_PropertyChanged;

            OnPropertyChanged("ObjectA");
        }
    }

    void objectA_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        // Propagate the change to any listeners. Prefix with ObjectA so listeners can tell the difference.
        OnPropertyChanged("ObjectA." + e.PropertyName);
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

For class R, use ObservableCollection<S> instead of List<S>, and subscribe to its CollectionChanged event, and monitor when objects are added or removed to listOfObjectS. When they are added, subscribe to S's PropertyChanged events. Then updated R's view. Example:

public class R
{
    protected string name;
    protected System.Collections.ObjectModel.ObservableCollection<S> ListOfObjectS { get; private set; }

    public R()
    {
        // Use ObservableCollection instead.
        ListOfObjectS = new ObservableCollection<S>();

        // Subscribe to all changes to the collection.
        ListOfObjectS.CollectionChanged += listOfObjectS_CollectionChanged;
    }

    void listOfObjectS_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Remove)
        {
            // When items are removed, unsubscribe from property change notifications.
            var oldItems = (e.OldItems ?? new INotifyPropertyChanged[0]).OfType<INotifyPropertyChanged>();
            foreach (var item in oldItems)
                item.PropertyChanged -= item_PropertyChanged;
        }

        // When item(s) are added, subscribe to property notifications.
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            var newItems = (e.NewItems ?? new INotifyPropertyChanged[0]).OfType<INotifyPropertyChanged>();
            foreach (var item in newItems)
                item.PropertyChanged += item_PropertyChanged;
        }

        // NOTE: I'm not handling NotifyCollectionChangedAction.Reset.
        // You'll want to look into when this event is raised and handle it
        // in a special fashion.
    }

    void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName.StartsWith("ObjectA."))
        {
            // Refresh any dependent views, forms, controls, whatever...
        }
    }
}
Migratory answered 30/12, 2013 at 19:37 Comment(2)
One nit, not all calls to a setter need to trigger a OnPropertyChanged, normally you will want to do a Object.Equals(backingMember, value) check (and because you did not override Equals(object) it will be a reference equality check, which is fine in this situation) to see if the property really was changed before notifying any subscribers of that fact.Solecism
@Scott Agreed, esp. because redrawing the UI is so slow. I am probably not going to update the example code, however, because it's pretty verbose already and every added line makes it more difficult to digest. In some situations, it may be desirable that setting the property (even if the value didn't change) cause a refresh. Not exactly how it was meant to be used, but I've seen it before. As a nit of my own, I would not use Object.Equals because we know the type, so EqualityComparer<T>.Default.Equals(backingValue, value) would be a better choice.Migratory
H
0

Let's say you have a form1 where you use an instance of class R to display a list of instances from class A. You than press edit and you send the instance of that same class A from the class R instance towards the new form.

This will than be a reference to the object contained in the instance of R and therefore be updated within form2. The only thing you than have to do is refresh the instance of class A in the list of form1.

To explain: when you are calling a form or method with the an object instance of a class, this will create a reference, not a clone and therefore can be updated from the second form2.

Hamachi answered 30/12, 2013 at 19:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.