Search code examples
c#wpfsystem.reactivereactiveui

Observable.Do never fires, even when the underlying IObservable changes


I'm getting a bit of a crash course in ReactiveUI and System.Reactive.Linq after finding that a UI library I need uses it for everything. It seems to be pretty understandable for the most part, but there's one operation that isn't doing anything.

I have a control whose value I need to use in two places. I have an IObservable<T> representing its value, which I'm using as follows:

case 1: I need to feed a value to another observable, by combining this with another observable value. So I produce it using Observable.CombineLatest(myObservable, otherObservable, (m, o) => ProduceValue(m, o)) This updates exactly as expected. From this, I know that myObservable is firing updates correctly.

case 2: I need to use this value elsewhere, in a non-observable context. So: myObservable.Do(v => UpdateViewModelWith(v)). This never fires. I've verified that by putting a breakpoint in the lambda and running it under the debugger.

From case 1 I know that the observable is firing correctly. As I understand it, observables are conceptually a lot like Events, (with a bunch of machinery to make them feel more like IEnumerables,) and like Events are perfectly capable of accepting multiple listeners, so the fact that there's two of them shouldn't be a problem. (Verified by changing the order in which the two listeners are set up, which produces no change in observed behavior.) So what can cause case 2 to never run?


Solution

  • @Erwin's answer is close. Just to elaborate:

    Do, like most Rx-related functions, is an operator. Operators do nothing without subscriptions. For example:

    var source = Observable.Range(0, 5);
    var squares = source.Select(i => i * i);
    var logged = squares.Do(i => Console.WriteLine($"Logged Do: {i}));
    
    var sameThingChained = Observable.Range(0, 5)
        .Select(i => i * i)
        .Do(i => Console.WriteLine($"Chained Do: {i}));
    
    //until here, we're in no-op land. 
    

    Range, Select and Do are all operators, which do nothing without a subscription. If you want any of this to do anything, you need a subscription.

    var subscription = logged.Subscribe(i => Console.Writeline($"Subscribe: {i}");
    

    Output:

    Logged Do: 0
    Subscribe: 0
    Logged Do: 1
    Subscribe: 1
    Logged Do: 4
    Subscribe: 4
    Logged Do: 9
    Subscribe: 9
    Logged Do: 16
    Subscribe: 16
    

    Generally, side-effect code (non-functional code) should reside in a Subscribe function. Do is best used for logging/debugging. So if I wanted to log the original, non-square integer, I could do as follows.

    var chainedSub = Observable.Range(0, 5)
        .Do(i => Console.WriteLine($"Original int: {i}"));
        .Select(i => i * i)
        .Subscribe(i => Console.Writeline($"Subscribe: {i}");
    

    In pure Rx.NET (without ReactiveUI), there's only one way to get a subscription: the various Subscribe overloads. However, ReactiveUI does have a bunch of functions that create subscriptions themselves so you don't have to deal with them (like ToProperty). If you're using ReactiveUI, those are probably a better choice than Subscribe.