2

I have a stream with several notification types. One notification type contains information about the current file and is sent continuously. Another type is emitted when the user clicks a button. Both notifications are inside a single stream.

When the user clicks the button I want to do something with current file. I split the source stream in two streams and try combine the "button click" with the latest "current file" notification.

static IObservable<DoSomething> DoSomethingWithFile(IObservable<object> stream)
{
  var buttonClick = stream.OfType<ButtonClick>();
  var file = stream.OfType<CurrentFile>();

  var match = buttonClick
    .And(file)
    .Then((command, f) => new DoSomething());

  return Observable.When(match);
}

This is the desired marble diagram:

File    1-1-1-1-2-2-2-3-3-3-3
Button  ----x-----------x----

Desired ----1-----------3----
            x           x

The code generally works, but only a few times if the current file information changes. Instead of the marble diagram above, I get this one:

File    1-1-1-1-2-2-2-3-3-3-3
Button  ----x-----------x----

Actual  ----1-----------2----
            x           x

CombineLatest will not work in this scenario because on every new file it'll emit a notification to the destination sequence, although the user did not click the button.

I know the question is very general, but of course we're talking about a real project :-)

Alexander Groß
  • 10,200
  • 1
  • 30
  • 33

2 Answers2

2

So And, Plan and Pattern were all new to me. Very cool stuff. Based on my experimentation with And it seems to buffer the fast observable until the slow one emits a value, at which point it returns the first value from the fast observable and the latest value from the slow observable. That would explain the behavior you are seeing. I cannot find any documentation on MSDN that explains in detail how And works, unfortunately, that could confirm what I think I am seeing.

I think this will give you the results you are looking for:

static IObservable<DoSomething> DoSomethingWithFile(IObservable<object> stream)
{
    var buttonClick = stream.OfType<ButtonClick>();
    var file = stream.OfType<CurrentFile>();

    return
        file
        .Select(f =>
            buttonClick
            .Select(b => new { File = f, ButtonClick = b })
            //.Take(1)
            // Include this if you only want to register 
            // one button click per file change
            )
        .Switch()
        .Select(x => new DoSomething());
}
Jason Boyd
  • 6,839
  • 4
  • 29
  • 47
  • You pointed me in the right direction, I think. I got it working by slowing down the fast observable with `var file = stream.OfType().DistinctUntilChanged();` as well (all else unchanged). – Alexander Groß Apr 19 '15 at 20:50
  • 1
    With this approach, for each time the file changes, for every time the button is clicked, you'll get an output. So if the file changes 3 times, then the user clicks the button 3 times, you will get 9 outputs. – Niall Connaughton Apr 20 '15 at 01:38
  • @NiallConnaughton - You are correct. The code has been updated to use `Select` and `Switch` rather than `SelectMany`. – Jason Boyd Apr 20 '15 at 14:46
2

Zip, And or CombineLatest are the right operators to be looking at here. Your change to use DistinctUntilLatest on the file stream is a good start. However, in your example above, you will still get a notification for (2, x) with any of these, even using DistinctUntilChanged.

Zip will push you a notification for each combined distinct value of the two streams. So you can't skip the notification for File 2 using Zip alone. And will have the same behaviour. CombineLatest will also give you the notification with 2 in it.

If you want the marble diagram you drew above, where you only get the latest value from the file stream when the button is clicked, you could use an approach where you do:

file.CombineLatest(buttonClick, (file, button) => new { file, button })
    .DistinctUntilChanged(data => data.button)

Consider this example, with a slow stream and a fast stream. We're taking the latest value from the fast stream every time the slow stream ticks:

var fast = Observable.Interval(TimeSpan.FromSeconds(1));
var slow = Observable.Interval(TimeSpan.FromSeconds(1.5));

fast.CombineLatest(slow, (fastValue, slowValue) => new { fastValue, slowValue })
    .DistinctUntilChanged(data => data.slowValue)
    .Dump();

If you comment out the DistinctUntilChanged, you'll see it produces a value each time the fast source ticks.

RxJS has an operator to do this for us called withLatestFrom, but it's unfortunately not in the .Net version. You'd be able to do button.withLatestFrom(file) and go from there.

Niall Connaughton
  • 15,518
  • 10
  • 52
  • 48