4

I'm using RX and I want to bind/map a source stream to a destination stream so that the source stream can be dynamically changed without affecting any subscription to the destination stream.

I'll layout my (naive) solution here in the hope that someone can show me a better solution.

I'm hoping there will be existing extension methods that can be composed to achieve this result. And if not I'm hoping to make a custom extension method that simplifies my solution.

/// <summary>
/// Used to bind a source stream to destination stream
/// Clients can subscribe to the destination stream before the source stream has been bound.
/// The source stream can be changed as desired without affecting the subscription to the destination stream.
/// </summary>
public class BindableStream<T>
{
    /// <summary>
    /// The source stream that is only set when we bind it.
    /// </summary>
    private IObservable<T> sourceStream;

    /// <summary>
    /// Used to unsubscribe from the source stream.
    /// </summary>
    private IDisposable sourceStreamDisposer;

    /// <summary>
    /// Subject used as the destination stream.
    /// For passing data from source to dest stream.
    /// </summary>
    private Subject<T> destStream = new Subject<T>();

    /// <summary>
    /// Get the destination stream. Clients can subscribe to this to receive data that is passed on from the source stream.
    /// Later on we can set or change the underlying source stream without affecting the destination stream.
    /// </summary>
    public IObservable<T> GetDestStream()
    {
        return destStream;
    }

    /// <summary>
    /// Bind to a source stream that is to be propagated to the destination stream.
    /// </summary>
    public void Bind(IObservable<T> sourceStream)
    {
        Unbind();

        this.sourceStream = sourceStream;
        this.sourceStreamDisposer = sourceStream.Subscribe(dataItem =>
        {
            //
            // Pass the source item on to the client via the subject.
            //
            destStream.OnNext(dataItem);
        });
    }

    /// <summary>
    /// Unsubscribe from the source stream.
    /// </summary>
    public void Unbind()
    {
        if (sourceStreamDisposer != null)
        {
            sourceStreamDisposer.Dispose();
        }

        sourceStreamDisposer = null;
        sourceStream = null;
    }

}

Here is a very simple example of how this is used:

static void Main(string[] args)
{
    var bindableStream = new BindableStream<long>();

    // Subscribe before binding the source stream.
    bindableStream.GetDestStream().Subscribe(i => Console.WriteLine(i));

    Thread.Sleep(1000);

    // Bind a source stream.
    bindableStream.Bind(Observable.Interval(TimeSpan.FromSeconds(1)));

    Thread.Sleep(5000);

    // Bind a new source stream.
    bindableStream.Bind(Observable.Interval(TimeSpan.FromSeconds(1)));

    Console.ReadKey();
}
Ashley Davis
  • 9,896
  • 7
  • 69
  • 87
  • Note: I can improve my naive solution slightly by making BindableStream an implementation of IObservable, then instead of calling GetDestStream.Subscribe()... we can instead just call Subscribe() because with this modification BindableStream itself is an observable stream. – Ashley Davis Aug 03 '14 at 02:54
  • 1
    I was thinking of this problem just yesterday. I implemented it similarly, though with a more intuitive interface: as an `ObserveLatest(this IObservable> o)`. I'd love to see more opinions on this. – Cory Nelson Aug 03 '14 at 03:04
  • @ChristopherHarris Thanks. Where can that be found? – Cory Nelson Aug 03 '14 at 03:13
  • 2
    Oh, you mean `Switch()`. – Cory Nelson Aug 03 '14 at 03:14
  • that might be it. I'm used to `switchLatest` which is in RxJS. – cwharris Aug 03 '14 at 03:15
  • http://msdn.microsoft.com/en-us/library/hh229197(v=vs.103).aspx – cwharris Aug 03 '14 at 03:15
  • 1
    We'll I had to find a solution quickly to keep development moving. The 'BindableStream' presented above was a generalization of the original solution and it only took a 1/2 hour to put together. So not really a huge amount of time. And I had to put that sample together so that I could formalize my question. So all in the name of product development and learning. I see that as a good use of my time. And I did a lot of googling before putting together this question and I wasn't able to find the information I need, so I had to invest the time to make this question. – Ashley Davis Aug 03 '14 at 04:36

2 Answers2

4

You can use the Observable.Switch(...) operator to get what you want.

Switch creates a "rolling" subscription. As a new observable is yielded, it disposes of its subscription to the previous observable, and subscribes to the new one.

static void Main(string[] args)
{
    var streams = new Subject<IObservable<long>>();

    // Subscribe before binding the source stream.
    streams.Switch().Subscribe(Console.WriteLine);

    Thread.Sleep(1000);

    // Bind a source stream.
    streams.OnNext(Observable.Interval(TimeSpan.FromSeconds(1)));

    Thread.Sleep(5000);

    // Bind a new source stream.
    streams.OnNext(Observable.Interval(TimeSpan.FromSeconds(1)));

    Console.ReadKey();
}

Or, if you know where your "streams" are coming from...

static void Main(string[] args)
{
    var interval = Observable.IntervalTimeSpan.FromSeconds(1));

    var sourcesOvertime = new [] {
        // Yield the first source after one second
        Observable.Return(interval).Delay(TimeSpan.FromSeconds(1)),
        // Yield the second source after five seconds
        Observable.Return(interval).Delay(TimeSpan.FromSeconds(5))
    };

    sourcesOvertime
        // merge these together so we end up with a "stream" of our source observables
        .Merge()
        // Now only listen to the latest one.
        .SwitchLatest()
        // Feed the values from the latest source to the console.
        .Subscribe(Console.WriteLine);

    Console.ReadKey();
}

EDIT:

As a simplification to the BindableStream class...

static void Main(string[] args)
{
    // var bindableStream = new BindableStream<long>();
    var bindableStream = new Subject<IObservable<long>>();
    var dest = bindableStream.Switch();

    // Subscribe before binding the source stream.
    // bindableStream.Subscribe(i => Console.WriteLine(i));
    dest.Subscribe(i => Console.WriteLine(i));

    Thread.Sleep(1000);

    // Bind a source stream.
    // bindableStream.Bind(Observable.Interval(TimeSpan.FromSeconds(1)));
    bindableStream.OnNext(Observable.Interval(TimeSpan.FromSeconds(1)));

    Thread.Sleep(5000);

    // Bind a new source stream.
    // bindableStream.Bind(Observable.Interval(TimeSpan.FromSeconds(1)));
    bindableStream.OnNext(Observable.Interval(TimeSpan.FromSeconds(1)));

    Thread.Sleep(4000);

    Console.WriteLine("Unbound!");

    // Unbind the source and dest streams.
    // bindableStream.Unbind();
    bindableStream.OnNext(Observable.Empty<long>());

    Console.ReadKey();
}

Or if that's too verbose...

public static class SubjectEx
{
    public static class OnNextEmpty<T>(this ISubject<IObservable<T>> subject)
   {
       subject.OnNext(Observable.Empty<T>());
   }
}
cwharris
  • 17,835
  • 4
  • 44
  • 64
  • I'm not convinced this does exactly what I need. What happens to the 1st stream once the 2nd stream is added? – Ashley Davis Aug 03 '14 at 03:18
  • Also... is there a way to achieve this without using Subject? – Ashley Davis Aug 03 '14 at 03:19
  • There's no reason to avoid Subject when you actually need one. In fact, your original implementation is simulating what a Subject is doing anyways, so the implementation isn't much different. The reason you should *avoid* Subjects is that there are *usually* better ways to do things. That being said, I've added an example of how to do it without subjects. – cwharris Aug 03 '14 at 03:20
  • So there isn't a way to remove Subject from the solution? Fair enough. I was hoping there was an extension method that wrapped this whole thing up in one go. Thanks for sharing anyway, Switch is very handy to know about. – Ashley Davis Aug 03 '14 at 03:22
  • One question about Switch. Relating to my first comment (what does switch do with the first sequence?) What if the first sequence starts producing values again? Does switch revert back to the first sequence? – Ashley Davis Aug 03 '14 at 03:24
  • 1
    You'll need to use a proxy (subject) of some kind if you don't have a way of injecting your sources at the instantiation of your outer observable. However, if you know of the sources ahead of time, you can use any number of operators to merge sources together to form your stream of "sources". – cwharris Aug 03 '14 at 03:24
  • 1
    Switch is a rolling subscription. It doesn't move back and forth. If you want something that merges elements from each Observable, use 'Merge' – cwharris Aug 03 '14 at 03:25
  • Also my second example is different from your original requirements... fix it now. – cwharris Aug 03 '14 at 03:25
1

After input from @ChristopherHarris I have revised my original solution. I think this is much better than my original example, although I'd still love to be able to boil this down to a custom extension method.

If you can figure out how to simplify this please post an answer.

NOTE: The use of Switch simplifies my original solution and removes the need for a manual Subscribe on the source sequence.

/// <summary>
/// Used to bind a source stream to destination stream
/// Clients can subscribe to the destination stream before the source stream has been bound.
/// The source stream can be changed as desired without affecting the subscription to the destination stream.
/// </summary>
public class BindableStream<T> : IObservable<T>
{
    /// <summary>
    /// Subject used as the destination stream.
    /// For passing data from source to dest stream.
    /// This is a stream of streams.
    /// When a new stream is added it replaces whichever stream was previously added.
    /// </summary>
    private Subject<IObservable<T>> destStream = new Subject<IObservable<T>>();

    /// <summary>
    /// Subscribe to the destination stream.
    /// Clients can subscribe to this to receive data that is passed on from the source stream.
    /// Later on we can set or change the underlying source stream without affecting the destination stream.
    /// </summary>
    public IDisposable Subscribe(IObserver<T> observer)
    {
        return destStream.Switch().Subscribe(observer);
    }

    /// <summary>
    /// Bind to a new source stream that is to be propagated to the destination stream.
    /// </summary>
    public void Bind(IObservable<T> sourceStream)
    {
        destStream.OnNext(sourceStream);
    }

    /// <summary>
    /// Unbind the source stream.
    /// </summary>
    public void Unbind()
    {
        destStream.OnNext(Observable.Empty<T>());
    }
}

An example of using 'BindableStream':

static void Main(string[] args)
{
    var bindableStream = new BindableStream<long>();

    // Subscribe before binding the source stream.
    bindableStream.Subscribe(i => Console.WriteLine(i));

    Thread.Sleep(1000);

    // Bind a source stream.
    bindableStream.Bind(Observable.Interval(TimeSpan.FromSeconds(1)));

    Thread.Sleep(5000);

    // Bind a new source stream.
    bindableStream.Bind(Observable.Interval(TimeSpan.FromSeconds(1)));

    Thread.Sleep(4000);

    Console.WriteLine("Unbound!");

    // Unbind the source and dest streams.
    bindableStream.Unbind();

    Console.ReadKey();
}
Ashley Davis
  • 9,896
  • 7
  • 69
  • 87
  • As a consumer, I would prefer the term "Set" and "UnSet" rather than "Bind" and "Unbind", as "Bind" has a very specific meaning in functional programming. http://en.wikipedia.org/wiki/Monad_(functional_programming) – cwharris Aug 04 '14 at 20:43
  • What advantage is there to using a custom class over Bind? It seems as though the only added functionality here is the ability to "Unbind" the latest source stream... – cwharris Aug 04 '14 at 20:44
  • Do you mean 'over Switch'? – Ashley Davis Aug 05 '14 at 21:14
  • Switch is still used of course under the hood. I think using a custom class makes the code more expressive. That is to say that when I use this class I'm attempting to express my intent more clearly, rather than having to have other programmers figure out my intent by studying my use of Subject+Switch+OnNext. – Ashley Davis Aug 05 '14 at 21:16
  • Here my intent is: bind 1 stream to another, be able to change the stream dynamically, be able to unbind the stream at any time. – Ashley Davis Aug 05 '14 at 21:17
  • If I simply used the low-level code you'd have to read the code more carefully to understand what I was trying to achieve. Instead using higher-level code you see my intentions more easily (and thus be able to work with my code more quickly and have a greater chance of changing it without causing defects). – Ashley Davis Aug 05 '14 at 21:19