[RxUI] 9 – 交互
有时,视图模型代码需要向用户请求确认。例如,在删除文件之前或发生错误之后。
从视图模型显示交互对话框是一个简单的解决方案,但是它将视图模型与特定的UI框架联系在一起,使程序难以测试或无法测试。
交互是ReactiveUI的解决方案,用于解决在用户提供某些输入之前挂起视图模型的执行路径问题。
API概述
Interaction<TInput, TOutput>
类是交互基础结构的基础。它将交互的协作组件粘合在一起,协调交互,并将其分发给处理程序。
交互接收输入并产生输出。视图使用输入来处理交互。视图模型从交互中接收输出。例如,视图模型可能需要在删除文件之前要求用户进行确认。使用交互,可以将文件路径作为输入,并给返回布尔值作为输出,用来指示是否可以删除文件。
Interaction<TInput, TOutput>
的输入和输出类型是通用类型,因此可用作输入或输出的内容没有任何限制。
注释 有时,输入类型并不重要。这时可以使用
Unit
。Unit
也可以用作输出类型,但是这意味着视图模型没有使用交互来做出决定,只是单纯的通知即将要发生的事情。
交互处理程序接收一个Interaction<TInput, TOutput>
。交互的输入通过交互上下文的Input
属性公开。处理程序可以使用交互上下文的SetOutput
方法提供交互的输出。
下面是交互组件的典型布置:
- View Model : 需要知道问题的答案,例如“可以删除此文件吗?”
- View : 询问用户问题,并在交互过程中提供答案。
虽然这种情况是最常见的,但不是强制性的。例如,视图可以自行回答问题,而无需用户干预。或者这两个组件都可以是视图模型。ReactiveUI的交互不会以任何方式限制协作组件。
但是,假定最常见的情况是,视图模型将创建公开的Interaction<TInput, TOutput>
示例。它的关联视图通过调用交互的RegisterHandler
方法来为此交互注册一个处理程序。为开始交互,视图模型将TInput
的实例传递给交互的Handle
方法。当异步方法最终返回时,视图模型将收到类型为TOutput
的结果。
一个示例
public class ViewModel : ReactiveObject
{
private readonly Interaction<string, bool> confirm;
public ViewModel()
{
this.confirm = new Interaction<string, bool>();
}
public Interaction<string, bool> Confirm => this.confirm;
public async Task DeleteFileAsync()
{
var fileName = ...;
// this will throw an exception if nothing handles the interaction
var delete = await this.confirm.Handle(fileName);
if (delete)
{
// delete the file
}
}
}
public class View
{
public View()
{
this.WhenActivated(
d =>
{
d(this
.ViewModel
.Confirm
.RegisterHandler(
async interaction =>
{
var deleteIt = await this.DisplayAlert(
"Confirm Delete",
$"Are you sure you want to delete '{interaction.Input}'?",
"YES",
"NO");
interaction.SetOutput(deleteIt);
}));
});
}
}
还可以创建在程序中的多个组价之间共享的Interaction<TInput, TOutput>
。一个常见的例子是错误恢复。许多组件都有可能引发组件,但只需要一个公共的处理程序。下面是一个如何实现此目的的示例:
public enum ErrorRecoveryOption
{
Retry,
Abort
}
public static class Interactions
{
public static readonly Interaction<Exception, ErrorRecoveryOption> Errors = new Interaction<Exception, ErrorRecoveryOption>();
}
public class SomeViewModel : ReactiveObject
{
public async Task SomeMethodAsync()
{
while (true)
{
Exception failure = null;
try
{
DoSomethingThatMightFail();
}
catch (Exception ex)
{
failure = ex;
}
if (failure == null)
{
break;
}
// this will throw if nothing handles the interaction
var recovery = await Interactions.Errors.Handle(failure);
if (recovery == ErrorRecoveryOption.Abort)
{
break;
}
}
}
}
public class RootView
{
public RootView()
{
Interactions.Errors.RegisterHandler(
async interaction =>
{
var action = await this.DisplayAlert(
"Error",
"Something bad has happened. What do you want to do?",
"RETRY",
"ABORT");
interaction.SetOutput(action ? ErrorRecoveryOption.Retry : ErrorRecoveryOption.Abort);
});
}
}
注释 为清除起见,这里的示例代码将TPL和Rx代码混合在一起。生产代码通常会与其中之一保持一致。
警告
Handle
返回的可观察对象结果是“冷”的。必须订阅它才能调用处理程序。
处理程序优先级
Interaction<TInput, TOutput>
实现处理程序链。可以注册任意数量的处理程序,后面的注册比之前的注册具有更高的优先级。当使用Handle
方法激发交互时,每个处理程序都将有机会处理该交互(即设置输出)。处理程序没有义务实际处理交互。如果处理程序选择不设置输出,则调用链中的下一个处理程序。
注释
Interaction<TInput, TOutput>
类被设计为可扩展的。子类可以更改Handle
的行为,使其不表现为上述行为。例如,可以编写一个仅尝试列表中第一个处理程序的实现。
此优先级链使得可以定义默认处理程序,然后临时覆盖该处理程序。例如,根级别处理器可以提供默认的错误恢复行为。但是程序中的特定视图可能知道如何在不提示用户的情况下从特定错误中恢复。它可以在激活时注册处理程序,然后失活时取消注册。显然,这种方法需要共享的交互实例。
未处理的互动
如果没有处理程序或没有任何处理程序设置结果,则认为该交互未处理。在这种情况下,调用Handle
将引发UnhandledInteractionException<TInput, TOutput>
异常。该异常具有Interaction
和Input
属性,它们提供有关该错误的更多详细信息。
测试
可以通过注册交互处理程序来轻松测试视图模型中的交互逻辑:
[Fact]
public async Task interaction_test()
{
var fixture = new ViewModel();
fixture
.Confirm
.RegisterHandler(interaction => interaction.SetOutput(true));
await fixture.DeleteFileAsync();
Assert.True(/* file was deleted */);
}
如果测试与共享交互有关,则可能要在测试返回之取消注册:
[Fact]
public async Task interaction_test()
{
var fixture = new SomeViewModel();
using (Interactions.Error.RegisterHandler(interaction => interaction.SetOutput(ErrorRecoveryOption.Abort)))
{
fixture.SomeMethodAsync();
// assert abort here
}
}