Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the appropriate way to use schedulers in derived properties to have a responsive UI?

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:

  • Which scheduler to use? I have seen examples with RxApp.TaskpoolScheduler, RxApp.MainThreadScheduler, NewThreadScheduler.Default, and possibly others.
  • When to use SubscribeOn vs ObserveOn of even ObserveOnDispatcher or the scheduler: parameter of ToProperty?
  • Does the order make a difference? I have put the re-scheduling methods before and after the Select operator, but I'm not so sure. I'm not sure Select is even needed, to be frank.
  • I have seen some examples that set the Binding.IsAsync to true, but I tried it and haven't seem much difference, but again, maybe it is because of the other factors.
  • Are the concepts of SynchronizationContext and ThreadPriority relavant here? Is there a way to configure them in the code shown?
  • Should I be using 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.

like image 516
heltonbiker Avatar asked Dec 24 '22 16:12

heltonbiker


1 Answers

Schedulers

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 VM

RxUI 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.

ObserveOn/SubscribeOn

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".

Doing heavy work

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")

Derived properties

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.

My take

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

like image 132
Jon G Stødle Avatar answered Dec 26 '22 11:12

Jon G Stødle