pjs
pjs

Reputation: 2731

How do you use UserError handlers asynchronously

Most (if not all) of the examples online register a handler, but then return a discrete Observable value (i.e. Observable.Return(RecoveryOptionResult.CancelOperation)). For a proper implementation, it would be best to present the RecoveryOptions list to the user as a list of buttons (or something similar) and pass on flow control to the user.

What I am struggling with is how to await a button click by the user (or more specifically, how to wait for one of the RecoveryOption commands to have its RecoveryResult set).

I managed to hack something together that does this, but I cannot imagine this way is correct. My lack of experience using reactiveui is preventing me from conceptualizing the proper way of monitoring a ReactiveList<IRecoveryCommand>.

Below is my hacked up code.

// UserError.RegisterHandler(x => HandleErrorAsync(x));

private async Task<RecoveryOptionResult> HandleErrorAsync(UserError error)
{
    // present error UI...

    // use ReactiveCommand's IsExecuting observable to monitor changes (since RecoverResult is not an observable)
    // is there a better way to do this??? this seems sub-optimal
    await error.RecoveryOptions
        .Select(x => x.IsExecuting)
        .Merge()
        .Where(_ => error.RecoveryOptions.Any(x => x.RecoveryResult.HasValue))
        .FirstAsync();

    // recovery option was clicked in the UI

    // get the recovery option that was chosen
    return error.RecoveryOptions
        .Where(x => x.RecoveryResult.HasValue)
        .Select(x => x.RecoveryResult.Value)
        .First();
}

The main issue is that the RecoveryResult is not observable. So I have to monitor IsExecuting that is observable and then check the RecoveryResult value. It seems however, that there must be a better way to do this.

Upvotes: 3

Views: 170

Answers (1)

pjs
pjs

Reputation: 2731

Looking at this again today, I noticed that the reason I cannot observe RecoveryResult is because RecoveryOptions is a ReactiveList<IRecoveryCommand> and IRecoveryCommand is not observable. The easy fix for this problem is to just assume all recovery options are actually RecoveryCommand objects (which are observable), but a more appropriate answer would be to generate an observable stream based on the IRecoveryCommand contract.

We can adapt the code described in the RxUI docs on Recovery Options to support RxUI 6.5 by doing the following:

public static IObservable<RecoveryOptionResult> GetResultAsync(this UserError This, RecoveryOptionResult defaultResult = RecoveryOptionResult.CancelOperation)
{
    return This.RecoveryOptions.Any() ?
        This.RecoveryOptions
            .Select(x => x.IsExecuting
                .Skip(1) // we can skip the first event because it's just the initial state
                .Where(_ => x.RecoveryResult.HasValue) // only stream results that have a value
                .Select(_ => x.RecoveryResult.Value)) // project out the result value
            .Merge() // merge the list of command events into a single event stream
            .FirstAsync() : //only consume the first event
        Observable.Return(defaultResult);
}

This extension method is required if you want to support any type of IRecoveryCommand because it bases its observable stream on one of the only two observables it knows about. However, if you can be certain that you are only ever dealing with RecoveryCommand objects, you can do the following instead:

public static IObservable<RecoveryOptionResult> GetResultAsync(this UserError This, RecoveryOptionResult defaultResult = RecoveryOptionResult.CancelOperation)
{
    return This.RecoveryOptions.Any() ?
        This.RecoveryOptions
            .Cast<RecoveryCommand>()
            .Select(x => x // our command is now observable
                // we don't Skip(1) here because we're not observing a property any more
                .Where(_ => x.RecoveryResult.HasValue)
                .Select(_ => x.RecoveryResult.Value))
            .Merge()
            .FirstAsync() :
        Observable.Return(defaultResult);
}

I'll leave this answer up for the next while in hopes that @paul-betts can confirm or deny this is the appropriate strategy.

Upvotes: 1

Related Questions