I'm having a hard time trying to figure the proper way to schedule long-running reactive property "getters" in my ViewModel.
This excerpt from Intro to RX describes exactely what I want to do:
- respond to some sort of user action
- do work on a background thread
- pass the result back to the UI thread
- update the UI
Only in this case besides user interaction I want to react to change from other properties.
Below is the generic template I am using to get a derived property from an original property (in the actual code, there are chains of cascading derived properties).
In a Reactive ViewModel (inheriting from ReactiveObject) I already have some properties that derive from others. For exemple, when Original
changes, Derived
is recalculated.
public TOriginal Original
{
get { return _original; }
set { this.RaiseAndSetIfChanged(ref _original, value); }
}
TOriginal _original;
public TDerived Derived { get { return _derived.Value; } }
readonly ObservableAsPropertyHelper<double[,]> _derived;
this.WhenAnyValue(x => x.Original)
.Where(originalValue => originalValue != null)
// ObserveOn? SubscribeOn? Which scheduler?
.Select(derivedValue => LongRunningCalculation(originalValue))
// Same thing here: ObserveOn? SubscribeOn? Which scheduler?
.ToProperty(this, x => x.Derived, out _derived); // should I use the `scheduler:` in this method?
My problems are: I have no idea how these different "design choices" should be combined to get my desired responsive UI:
RxApp.TaskpoolScheduler
, RxApp.MainThreadScheduler
, NewThreadScheduler.Default
, and possibly others.SubscribeOn
vs ObserveOn
of even ObserveOnDispatcher
or the scheduler:
parameter of ToProperty
?Select
operator, but I'm not so sure. I'm not sure Select
is even needed, to be frank.Binding.IsAsync
to true
, but I tried it and haven't seem much difference, but again, maybe it is because of the other factors.SynchronizationContext
and ThreadPriority
relavant here? Is there a way to configure them in the code shown?ReactiveCommand
or some other ReactiveUI class for this?The most nerve-wrecking fact is that, with some combinations, the calculations work properly, but block the UI, while with some other combinations, the values are asynchronously calculated and the UI is a bit less blocked, but sometimes part of the derived values (in a collection of items, for example) are not available!
Sorry if I'm asking too much, but I didn't find any authoritative expected way to do what I need in the docs.
In Rx.NET there are a few schedulers, including a special one that's exclusive to WPF.
TaskPoolScheduler
runs code on the task pool. This is a bit like running code inside a Task
.NewThreadScheduler
spawns a new thread to run the code on. Generally don't use this operator, unless you know that you "need" it (you almost never don't)DispatcherScheduler
runs code on the UI thread. Use this when you're going to set your properties in the VMRxUI brings two platform agnostic scheduler abstractions. No matter what platform you're on (WPF, UWP, Xamarin.iOS, Xamarin.Android) RxApp.MainThreadScheduler
will always refer to the UI thread scheduler, while RxApp.TaskPoolScheduler
will refer to something akin to a background thread.
If you want to keep it simple, just use the RxApp
schedulers; RxApp.MainThreadScheduler
for UI stuff and RxApp.TaskPoolScheduler
for background/heavy duty stuff.
The name SubscribeOn()
is a bit confusing as it doesn't directly affect Subscribe()
method. SubscribeOn()
decides which scheduler the observable will start on; on which scheduler the original/first subscription will be done (not which scheduler the Subscribe()
method will execute on). I like to think that SubsribeOn()
moves up the observable chain to the top and make sure the observable produces values on the given scheduler.
Some operators let's you specify which scheduler they should run on. When they do, you should always prefer to pass a scheduler, that way you know where they're going to do work and prevent them from potentially blocking the UI thead (although they shouldn't). SubsribeOn()
is kind of a "hack" for observables that doesn't let you specify a scheduler. If you use SubscribeOn()
, but the operator specifies a scheduler, the signals from the operator will be emitted on the operators scheduler, not the one you specified in SubscribeOn()
.
ObserveOn()
does much the same as SubscribeOn()
, but it does it "from this point onwards". The operators and code following ObserveOn()
will execute on the scheduler given to ObserveOn()
. I like to think that ObserveOn()
means "change thread to this one".
If you are going to do heavy work, put that in a function and call that function, like what you've done with LongRunningCalculation()
. You could use a put an ObserveOn(RxApp.TaskPoolScheduler)
before the Select()
and an ObserveOn(RxApp.MainThreadScheduler
after it, but I prefer to use Observable.Start()
combined with SelectMany()
.
Observable.Start()
is basically Observable.Return()
for functions: "Give me the result of this function as an observable." You can also specify the scheduler it should call the function on.
SelectMany()
ensures that we get the result of the observable, instead of the observable itself. (It's kind of like the await
for observables: "don't execute this next operator before we have the result of this observable")
You are doing a derived property correct.
Use WhenAnyValue()
to get the changes of the property and pipe that to a ToProperty()
. The operators you put in between may do work on background threads delay the setting of the derived property, but that's why we have INotifyPropertyChanged
.
Here's how I would implement your specific example:
public TOriginal Original
{
get { return _original; }
set { this.RaiseAndSetIfChanged(ref _original, value); }
}
TOriginal _original;
public TDerived Derived { get { return _derived.Value; } }
readonly ObservableAsPropertyHelper<double[,]> _derived;
_derived = this.WhenAnyValue(x => x.Original)
.Where(originalValue => originalValue != null)
// Sepcify the scheduler to the operator directly
.SelectMany(originalValue =>
Observable.Start(
() => LongRunningCalculation(originalValue),
RxApp.TaskPoolScheduler))
.ObserveOn(RxApp.MainThreadScheduler)
// I prefer this overload of ToProperty, which returns an ObservableAsPropertyHelper
.ToProperty(this, x => x.Derived);
We have a Slack team for ReactiveUI which you are welcome to join. You can ask for an invite by clicking here
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With