Tyler Hundley
Tyler Hundley

Reputation: 897

How to dynamically delay observable over time

I currently have a feature that users Timer() to fire an observable immediately and then every x millseconds after.

HoldPayloads = Observable.Merge(
    EnumeratedSymbolKeys.Select(
        o => Observable.FromEventPattern<MouseButtonEventHandler, MouseButtonEventArgs>(
            h => o.PreviewMouseLeftButtonDown += h,
            h => o.PreviewMouseLeftButtonDown -= h)
            .Select(_ => Observable.Timer(DateTimeOffset.Now, TimeSpan.FromMilliseconds(o.Frequency))
            .TakeUntil(Observable.FromEventPattern<MouseButtonEventHandler, MouseButtonEventArgs>(
                h => o.PreviewMouseLeftButtonUp += h,
                h => o.PreviewMouseLeftButtonUp -= h)))
                .Switch()
                .Select(_ => o.Payload)));

What I'd like to have is The observable fire immediately when a button is clicked, then after initial longer delay start to repeat at either a faster interval or a decreasing interval down to a limit, something like this:

--x------x--x--x--x--x--x-->

or

--x------x----x---x--x-x-x->

I attempted to use Delay() combined with Scan() to generate exponentially lower values to delay by but couldn't get it to work. Was I on the right track? Any better ways to do something like this?

Revised code using the answer from Shlomo:

HoldPayloads = Observable.Merge(
EnumeratedSymbolKeys.Select(
    o => Observable.FromEventPattern<MouseButtonEventHandler, MouseButtonEventArgs>(
        h => o.PreviewMouseLeftButtonDown += h,
        h => o.PreviewMouseLeftButtonDown -= h)
        .Select(_ => Observable.Generate(
            1,
            q => true,
            i => i+1,
            i => i,
            i => i==1
                ? TimeSpan.FromMilliseconds(0)
                : i > 10
                    ? TimeSpan.FromMilliseconds(50)
                    : TimeSpan.FromMilliseconds(500/i))
        .TakeUntil(Observable.FromEventPattern<MouseButtonEventHandler, MouseButtonEventArgs>(
            h => o.PreviewMouseLeftButtonUp += h,
            h => o.PreviewMouseLeftButtonUp -= h)))
            .Switch()
            .Select(_ => o.Payload)));

Ended up modifying the conditional so that the first item was immediate.

Upvotes: 2

Views: 930

Answers (2)

Enigmativity
Enigmativity

Reputation: 117029

I just thought I'd just add an answer to help to make the final query more readable. I haven't changed the answer in any way and I'm not looking to have this answer accepted or even up-voted. I just wanted to help add some clarity in how to understand the query.

I've just done a bit of substitution refactoring and have changed the query to be:

HoldPayloads =
    EnumeratedSymbolKeys
        .Select(o =>
            MouseDowns(o)
                .Select(_ => Generate().TakeUntil(MouseUps(o)))
                .Switch()
                .Select(_ => o.Payload))
        .Merge();

To me this is much easier to read and the intent of the query is very clear.

It just leaves the definition of MouseDowns, MouseUps, and Generate. These are:

IObservable<Unit> MouseDowns(EnumeratedSymbolKey o) =>
    Observable
        .FromEventPattern<MouseButtonEventHandler, MouseButtonEventArgs>(
            h => o.PreviewMouseLeftButtonDown += h, h => o.PreviewMouseLeftButtonDown -= h)
        .Select(EncodingProvider => Unit.Default);

IObservable<Unit> MouseUps(EnumeratedSymbolKey o) =>
    Observable
        .FromEventPattern<MouseButtonEventHandler, MouseButtonEventArgs>(
            h => o.PreviewMouseLeftButtonUp += h, h => o.PreviewMouseLeftButtonUp -= h)
        .Select(EncodingProvider => Unit.Default);          

IObservable<int> Generate() =>
    Observable
        .Generate(1, i => true, i => i + 1, i => i,
            i => i == 1
                ? TimeSpan.FromMilliseconds(0) 
                : (i > 10 ? TimeSpan.FromMilliseconds(50) : TimeSpan.FromMilliseconds(500 / i)));

Now that these are separated out it's easier to confirm that each are correct and hopefully that the whole code is correct.

Upvotes: 1

Shlomo
Shlomo

Reputation: 14350

Observable.Generate is your friend (discussed well on this page).

For increasingly frequent values, replace .Select(_ => Observable.Timer(DateTimeOffset.Now, TimeSpan.FromMilliseconds(o.Frequency)) with something like this:

Observable.Generate(
    1, 
    _ => true,
    i => i + 1,
    i => i,
    i => i > 10
        ? TimeSpan.FromMilliseconds(50)
        : TimeSpan.FromMilliseconds(500 / i)
)            

Whatever your pattern is, fit it into that last parameter. Think of Generate as like a reactive for loop with all the dials available to you.


EDIT:

With the code above, the first item from Generate will be delayed. If you want the first item immediately, then the second item delayed, you can do as follows:

Observable.Generate(
    1, 
    _ => true,
    i => i + 1,
    i => i,
    i => i > 10
        ? TimeSpan.FromMilliseconds(50)
        : TimeSpan.FromMilliseconds(500 / i)
)            
.StartWith(0)

So the original .Select(_ => Observable.Timer(DateTimeOffset.Now, TimeSpan.FromMilliseconds(o.Frequency) is now .Select(_ => Observable.Generate(...).StartWith(0))

Upvotes: 3

Related Questions