4

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?

Antao Almada
  • 445
  • 5
  • 12

1 Answers1

5

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.

Kent Boogaart
  • 175,602
  • 35
  • 392
  • 393
  • Just like you, I also prefer Rx syntax to express asynchrony but I was missing the Unit observable trick. Never though of that. You need to add a .Select(_ => Unit.Default) right after the Do statement or the Action overload will still be used. It still crashes but now I can see the exception: "Invalid window handle. This API must be called from a thread with a CoreWindow or a window must have been set explicitly." in – Antao Almada Dec 13 '16 at 22:13