ReactiveUI - Using a Scheduler in a Interaction handler

1.7k views Asked by At

I want to display an alert dialog with two buttons when an error occurs. To my best knowledge, this is how to do it, using an Interaction property:

this.ViewModel.ConnectionError.RegisterHandler(interaction =>
{
    var retry = await this.DisplayAlert("Connection failed", "Do you want to retry?", "RETRY", "ABORT");
    if (retry)
        interaction.SetOutput(DevicesViewModel.ErrorRecoveryOption.Retry);
    else
        interaction.SetOutput(DevicesViewModel.ErrorRecoveryOption.Abort);
});

The issue is that the exception is thrown inside a thread in a third-party library. DisplayAlert has to be called in the main thread. I tried the following:

this.ViewModel.ConnectionError.RegisterHandler(interaction =>
{
    RxApp.MainThreadScheduler.ScheduleAsync(interaction, async (scheduler, i, cancelationToken) =>
    {
        this.Log().Debug("ScheduleAsync");
        var retry = await this.DisplayAlert("Connection failed", "Do you want to retry?", "RETRY", "ABORT");
        if (retry)
            i.SetOutput(DevicesViewModel.ErrorRecoveryOption.Retry);
        else
            i.SetOutput(DevicesViewModel.ErrorRecoveryOption.Abort);

        return Disposable.Empty;
    });
});

I can see the log message in the console but the dialog doesn't display and the app crashes inside the ReactiveUI.dll. What am I doing wrong?

1

There are 1 answers

1
Kent Boogaart On

If you inspect the details of the crash, I suspect you'll find it complaining that nothing handled the interaction. The reason for this is that your registered handler is synchronous. Sure, it kicks off asynchronous work, but you're not given ReactiveUI any means of waiting for that work to complete.

You can verify this by looking at the overload of RegisterHandler that is resolved by your call. You'll find it's RegisterHandler(Action<InteractionContext<TInput, TOutput>>). In other words, "register a handler that takes the context and synchronously handles the interaction.

What you want to do is call one of the asynchronous RegisterHandler methods:

  • RegisterHandler(Func<InteractionContext<TInput, TOutput>, Task>)
  • RegisterHandler(Func<InteractionContext<TInput, TOutput>, IObservable<Unit>>)

There are various ways to write the logic for this. I tend to prefer Rx as a means of expressing asynchrony, so I'd write it like this:

this
    .ViewModel
    .ConnectionError
    .RegisterHandler(
        context =>
            Observable
                .Start(
                    () => Unit.Default,
                    RxApp.MainThreadScheduler)
                .SelectMany(_ => this.DisplayAlert("Connection failed", "Do you want to retry?", "RETRY", "ABORT"))
                .Do(result => context.SetOutput(result ? ErrorRecoveryOption.Retry : ErrorRecoveryOption.Abort)));

The Observable.Start call is a bit of a hack to get us on the correct thread, and I'd look at ways of cleaning that up if I were you. Specifically, I'd look at instigating the interaction on the correct thread. That is, whatever calls Handle (probably your VM) should do so on the UI thread. Much as it's your responsibility to execute commands on the correct thread, Handle is the same.