7

I have a Class, MyClass that implements INotifyPropertyChanged and has some properties that implement PropertyChanged. When MyClass.MyProperty changes, PropertyChanged fires as expected. Another class contains a SortedList<MyClass> .I've tried merging the events into a single observable in the class that contains the SortedSet<MyClass> and subscribing to it, but it doesn't seem to ever have any events. Here's what I'm trying:

Observable.Merge(MySortedList.ToObservable())
   .Subscribe(evt => Console.WriteLine("{0} changed", evt.MyProperty));

What I'm trying to get is a single observable that contains all of the events from every item in my SortedList<MyClass>. I've tried using ObservableCollection instead, but that doesn't change anything, nor would it be expected to, really, since it doesn't fire collectionchanged when a property of a contained item changes, anyway. I can listen to individual elements in SortedList<MyClass> and see the PropertyChanged event fire, but what I want is a single Observable that contains a stream of ALL of the PropertyChanged events from all of the elements in SortedList<MyClass>.

It seems like this should be something fairly easy to do using Rx, but I can't seem to figure out how.

Daniel Winks
  • 345
  • 3
  • 9

2 Answers2

10

I have produced an article for the RxCookBook on this subject that you can find here https://github.com/LeeCampbell/RxCookbook/blob/master/Model/CollectionChange.md Further article on PropertyChange notification is here https://github.com/LeeCampbell/RxCookbook/blob/master/Model/PropertyChange.md

It solves what you need by aggregating up the changes from an ObservableCollection<T>. By using the ObservableCollection<T> you also get notifications when items are added or removed from the collection.

If you dont want to use the ObservableCollection<T> (i.e. you only want to track properties at a given snapshot of the collection) then you will need to do something else. First I assume you have an INoftifyPropertyChanged to IObservable<T> extension method or you are just going to use the standard event to IObservable<T> methods.

Next you can project the List of values into a list of change sequences i.e. IEnumerable<T> to IEumerable<IObserable<T>>. This allows you to use Observable.Merge to flatten the list of changes in to a single stream of changes.

Here is a sample if you dont want to use the link above:

void Main()
{
    var myList = new List<MyThing>{
        new MyThing{Name="Lee", Age=31},
        new MyThing{Name="Dave", Age=37},
        new MyThing{Name="Erik", Age=44},
        new MyThing{Name="Bart", Age=24},
        new MyThing{Name="James", Age=32},
    };

    var subscription = Observable.Merge(myList.Select(t=>t.OnAnyPropertyChanges()))
                .Subscribe(x=>Console.WriteLine("{0} is {1}", x.Name, x.Age));

    myList[0].Age = 33;
    myList[3].Name = "Bob";

    subscription.Dispose();
}

// Define other methods and classes here
public class MyThing : INotifyPropertyChanged
{
private string _name;
private int _age;

public string Name
{
    get { return _name; }
    set
    {
        _name = value;
        OnPropertyChanged("Name");
    }
}

public int Age
{
    get { return _age; }
    set
    {
        _age = value;
        OnPropertyChanged("Age");
    }
}

public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(string propertyName)
{
    var handler = PropertyChanged;
    if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}

public static class NotificationExtensions
{
    /// <summary>
    /// Returns an observable sequence of the source any time the <c>PropertyChanged</c> event is raised.
    /// </summary>
    /// <typeparam name="T">The type of the source object. Type must implement <seealso cref="INotifyPropertyChanged"/>.</typeparam>
    /// <param name="source">The object to observe property changes on.</param>
    /// <returns>Returns an observable sequence of the value of the source when ever the <c>PropertyChanged</c> event is raised.</returns>
    public static IObservable<T> OnAnyPropertyChanges<T>(this T source)
        where T : INotifyPropertyChanged
    {
            return Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
                                handler => handler.Invoke,
                                h => source.PropertyChanged += h,
                                h => source.PropertyChanged -= h)
                            .Select(_=>source);
    }
}

Which will output:

Lee is 33
Bob is 24
Lee Campbell
  • 10,631
  • 1
  • 34
  • 29
  • This is EXACTLY what I was looking to do. I'm still a bit new to Rx, and after a few solid hours of searching I couldn't find a reference to how to do this. I'm definitely taking a look at your links above as well and bookmarking them. Thanks again! – Daniel Winks Sep 12 '13 at 19:23
  • Any way to make this work on nested properties? I actually used `Items.CollectionItemsChange(item => item.Price).Subscribe(x => { TotalPrice = Items.Sum(item => item.Price); })` but if I try with a nested property as in `Items.CollectionItemsChange(item => item.Detail.Price).Subscribe(x => { TotalPrice = Items.Sum(item => item.Detail.Price); })` it doesn't get notified. I'm fairly new to rx so some help would be highly appreciated! – Filippo Vigani Aug 09 '16 at 14:22
  • Yes there is, but be careful what you wish for. In your first example you already have a very inefficient implementation (consider what it is doing if I add 100 items). In your second implementation, you would need to track add/remove/updates to `Items`, changes to each of the item's `Details` property and then internally subscribe/unsubscribe each time any of those happened to then listen to changes to the `Price` property. So yes doable. However each time this was asked of me, we found that actually a better design was actually needed, so I have never written it. I believe impls are avail – Lee Campbell Aug 09 '16 at 23:54
0

Assuming from your description that you're able to provide this:

IEnumerable<KeyValuePair<string, IObservable<object>>> observableProperties;

You can use this to merge the property observables into a single observable:

IObservable<KeyValuePair<string, object>> changes = observableProperties
    .Select(p => p.Value
        .Select(o => new KeyValuePair<string, object>(p.Key, o)))
    .Merge();

If you find this insufficient, you'll need to give more details about why you can't provide observableProperties.

For example, if I had a class like this:

class MyClass
{
    public IObservable<int> X { get { ... } }
    public IObservable<string> S { get { ... } }
}

I could create observableProperties like this:

var myObject = new MyClass();
var observableProperties = new []
{
    new KeyValuePair<string, IObservable<object>>("X", myObject.X.Cast<object>()),
    new KeyValuePair<string, IObservable<object>>("S", myObject.S.Cast<object>())
};
Timothy Shields
  • 75,459
  • 18
  • 120
  • 173
  • Edited my question for a bit more clarity and information. I don't think your current answer meets my needs, as I need a single `Observable` that contains all of the events from `PropertyChanged` on my `SortedList`, where each `` element is the source of the event steams that require merging. Your answer seems to address combining multiple events from multiple properties on a single instance of a class. I have a `SortedList` and need a combined stream of the events from all the N elements in said list. The number of elements is unknown at compile time. – Daniel Winks Sep 12 '13 at 12:56