在没有视图的情况下处理 ViewModel 中的异常(在 ReactiveUI 中)

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

我目前正在使用包含许多 views/viewmodels 的 ReactiveUI 在 C# 中构建应用程序。其中一些视图模型以预设间隔执行网络请求。这些网络请求随时可能失败。我是这样实现的:

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);

在相应的视图中,我对异常的处理如下:

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

这工作正常,除非视图模型未与视图相关联。我的应用程序使用选项卡,当用户切换到不同的选项卡时,之前的视图将被销毁。这使得视图模型 运行 仍在发出请求,但没有视图。然后,当 RefreshJobList 中发生错误时,没有处理程序与 RefreshError 关联,ReactiveUI 抛出 UnhandledInteractionError 并且我的应用程序崩溃。

我不知道如何干净利落地处理这个问题。我的第一个想法是暂停 ViewModel,直到附加 View,这也可以节省网络流量。但是,我似乎无法通过任何方式检查 View 是否附加到 ViewModel。有什么想法吗?

为什么不能在您的视图模型中使用 WhenActivated 扩展来 'stop'/ 处理在处理视图时不应再使用的可观察对象?

我相信您总能从订阅中处理的交互中捕获异常,只需添加一个 OnError 处理程序。

如果您的视图实现 IViewFor 并且您查看模型实现 ISupportsActivation 您可以在离开视图时处理对 ThrownExceptions 的订阅:

// 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
});

如果视图模型未处于活动状态时发生异常,这将使 ReactiveCommand 抛出异常。要解决这个问题,您可以在停用视图模型时停止 运行 操作。或者,您可以按照杰米的建议捕获异常:RefreshError.Handle(ex).Catch(ex => Observable.Empty<Exception>())

解决方案的关键是使用 WhenActivated,由 ISupportsActivation 在 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)
        ));
    }
}

这非常有效。