Let\'s pretend I have something like this:
And something like this:
publ
You may be surprised how hard this is as a pure RX solution. It's subtly different to the similar (and typical Rx 101 example) of submitting a throttled search in response to textbox changes - in that case, it's ok to fire off concurrent searches, cancelling all but the latest one.
In this case, once DoWork() is off and running it can't be replaced or interrupted.
The problem is that Rx streams flow in one direction and can't "talk backwards" - so events queue up against slow consumers. To drop events due to slow consumers is quite hard in Rx.
It's much easier in a world where DoWork()
can be cancelled and replaced when a new (probably throttled) event arrives.
First I present a pure Rx solution. Then at the end, a simpler approach where the slow consumer is dealt with by a dispatching mechanism outside of Rx.
For the pure approach, you'll need this helper extension method to drop events queued against a slow consumer which you can read about here:
public static IObservable ObserveLatestOn(
this IObservable source, IScheduler scheduler)
{
return Observable.Create(observer =>
{
Notification outsideNotification = null;
var gate = new object();
bool active = false;
var cancelable = new MultipleAssignmentDisposable();
var disposable = source.Materialize().Subscribe(thisNotification =>
{
bool wasNotAlreadyActive;
lock (gate)
{
wasNotAlreadyActive = !active;
active = true;
outsideNotification = thisNotification;
}
if (wasNotAlreadyActive)
{
cancelable.Disposable = scheduler.Schedule(self =>
{
Notification localNotification = null;
lock (gate)
{
localNotification = outsideNotification;
outsideNotification = null;
}
localNotification.Accept(observer);
bool hasPendingNotification = false;
lock (gate)
{
hasPendingNotification = active = (outsideNotification != null);
}
if (hasPendingNotification)
{
self();
}
});
}
});
return new CompositeDisposable(disposable, cancelable);
});
}
With this available you can then do something like:
// run DoWork() when this.Text changes
Observable.FromEventPattern(this, "PropertyChanged")
.Where(x => x.EventArgs.PropertyName.Equals("Text"))
.Sample(TimeSpan.FromSeconds(1)) // get the latest event in each second
.ObservableLatestOn(Scheduler.Default) // drop all but the latest event
.Subscribe(x => DoWork().Wait()); // block to avoid overlap
To be honest, you are probably better off avoiding the pure Rx solution here, and instead DON'T call DoWork()
directly from a subscriber. I would wrap it with an intermediate dispatching mechanism called from the Subscribe
method that handles not calling it if it's already running - the code would be way simpler to maintain.
EDIT:
After thinking on this for a few days, I didn't do any better than some of the other answers here - I'll leave the above for interest, but I think I like Filip Skakun approach the best.