Reputation: 558
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
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
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
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