Wouter
Wouter

Reputation: 558

Handling exceptions in a ViewModel without a View (in ReactiveUI)

I am currently building an application in C# using ReactiveUI that contains many views/viewmodels. Some of these viewmodels perform a network request at a preset interval. These network requests may fail at any time. I implemented this as follows:

public ReactiveCommand<Unit, IReactiveList<IJob>> RefreshJobList { get; }
public Interaction<Exception, Unit> RefreshError { get; } = new Interaction<Exception, Unit>();

...

RefreshJobList = ReactiveCommand.CreateFromTask(() => DoNetworkRequest());
RefreshJobList.ThrownExceptions.Subscribe(ex =>
{
    log.Error("Failed to retrieve job list from server.", ex);
    RefreshError.Handle(ex).Subscribe();
});
Observable.Interval(TimeSpan.FromMilliseconds(300)).Select(x => Unit.Default).InvokeCommand(RefreshJobList);

In the corresponding views, I handle the exceptions as follows:

this.WhenActivated(d => d(
    this.ViewModel.RefreshError.RegisterHandler(interaction =>
    {
        MessageBox.Show("Failed to load joblist.", "Error", MessageBoxButton.OK);
        interaction.SetOutput(new Unit());
    })
));

This works fine, except when the viewmodel isn't associated with a view. My application uses tabs, and when the user switches to a different tab, the previous view is destroyed. This leaves the viewmodel running, still making requests, without a view. Then, when an error occurs in RefreshJobList, no handlers are associated with RefreshError, ReactiveUI throws an UnhandledInteractionError and my application crashes.

I am not sure how to deal with this cleanly. My first thought would be to pause the ViewModel until a View is attached, which would also save network traffic. However, I can't seem any way to check whether or not a View is attached to the ViewModel. Any ideas?

Upvotes: 2

Views: 623

Answers (3)

Wouter
Wouter

Reputation: 558

The key to the solution was using WhenActivated, supplied by ISupportsActivation on the ViewModel.

I am now using the following code in de viewmodel:

public class ViewModel : ReactiveObject, ISupportsActivation
    public ViewModelActivator Activator { get; } = new ViewModelActivator();

    public ReactiveCommand<Unit, IReactiveList<IJob>> RefreshJobList { get; }
    public Interaction<Exception, Unit> RefreshError { get; } = new Interaction<Exception, Unit>();

    ...

    public ViewModel(){
        RefreshJobList = ReactiveCommand.CreateFromTask(() => DoNetworkRequest());
        RefreshJobList.ThrownExceptions.Subscribe(ex =>
        {
            log.Error("Failed to retrieve job list from server.", ex);
            RefreshError.Handle(ex).Subscribe();
        });
        this.WhenActivated(d => d(
            Observable.Interval(TimeSpan.FromMilliseconds(300)).Select(x => Unit.Default).InvokeCommand(RefreshJobList)
        ));
    }
}

This works perfectly.

Upvotes: 1

Jon G St&#248;dle
Jon G St&#248;dle

Reputation: 3904

If your view implements IViewFor and you view model implements ISupportsActivation you can dispose the subscription to ThrownExceptions when navigating away from the view:

// In your VM

this.WhenActivated(d=>
{
    RefreshJobList
        .ThrownExceptions
        .Do(ex => log.Error("Failed to retrieve job list from server.", ex))
        .SelectMany(ex => RefreshError.Handle(ex))
        .Subscribe()
        .DisposeWith(d); // This will dispose of the subscription when the view model is deactivated
});

This will make the ReactiveCommand throw an exception if an exception occurs when the view model is not active. To get around that you could either stop the running operation when deactivating the view model. Alternatively you could catch the exception as jamie suggested: RefreshError.Handle(ex).Catch(ex => Observable.Empty<Exception>())

Upvotes: 3

jamie
jamie

Reputation: 1234

Why can't you use the WhenActivated extension in your view model to 'stop' / dispose of observables that should no longer be in play when the view is disposed?

I believe you can always catch the exception from the Interaction being handled in the Subscribe, just add an OnError handler.

Upvotes: 1

Related Questions