[RxUI] 19 – When Any
在交互式UI应用程序中,状态不断变化以响应用户操作和应用程序事件。ReactiveUI可以将程序状态的更改表示为值流,并可以使用功能强大的Reactive Extensions库进行组合和操作。
当考虑它时,动机足够直观。不难想象,对属性的更改可以视为事件 — 这就是INotifyPropertyChanged
的工作方式。从这里,在Rx上使用的参数与事件上的相同。特别是在MVVM应用程序设计的上下文中,将属性更改为可观察对象具有以下优点:
- 可以根据属性更改来定义程序的逻辑
- 可以使用Rx操作符以声明的方式构建和表达逻辑
- 由于像时间和异步等概念在可观察对象上下文中得到了一流的处理,因此更易于推理。
ReactiveUI提供了WhenAny
的多种变体,来帮助将属性作为可观察流使用。
WhenAny是什么
WhenAny
是一组扩展方法,它们以WhenAny
前缀开头,可以在对象的属性发生更改时推送通知。
当需要在一个或多个属性更改时获取通知,可以考虑WhenAny
扩展方法。
WhenAny
支持多种属性类型,包括INotifyPropertyChanged
、DependencyProperty
和BindableProperty
。
它会检查该属性对每种属性类型的支持,当调用Subscribe
时,它将订阅适当的属性通知机制提供的事件。
默认情况下,WhenAny
只是这些属性通知事件的包装,并且不会在订阅之前存储任何值。可以使用诸如Publish
、Replay
之类的技术来获取并存储这些值。
还可以将WhenAny
包装在Observable.Defer
中,以避免在订阅发生之前计算值。使用延迟功能时,对于ObservableAsPropertyHelper
很有用。
基础语法
下面的示例展示了WhenAnyValue
的简单用法,这是WhenAny
最常用的变体。
WhenAnyValue
观察单个属性
返回一个可观察对象,该对象会在Foo更改时推送它的当前值。
this.WhenAnyValue(x => x.Foo)
观察多个属性
返回一个可观察对象,该对象会在任何属性更改时推送用最新的RGB值创建的新Color。最后一个参数是一个选择器,用来描述如何组合三个观察到的属性:
this.WhenAnyValue(x => x.Red, x => x.Green, x => x.Blue,
(r, g, b) => new Color(r, g, b));
观察嵌套属性
WhenAny
变体也可以观察嵌套属性的更改:
this.WhenAnyValue(x => x.Foo.Bar.Baz);
用法
以下是WhenAny
变体返回的可观察对象的一些典型用法:
订阅
可以Subscribe``WhenAny
变体返回的可观察对象,并在值更改时获取通知。
this.WhenAnyValue(x => x.SearchText)
.Subscribe(x => Console.WriteLine(x));
该方法订阅的是,每当更改当前对象的SearchText
时,将其值打印到Console.WriteLine
方法。
公开“计算后的”属性
通常,仅使用WhenAny
可观察对象(或任何可观察对象)来设置属性可能是臭代码。习惯上,ToProperty
操作符用于创建一个“只读”计算属性,该属性可以公开给程序的其余部分,只能由其前面的WhenAny
链设置:
this.WhenAnyValue(x => x.SearchText, x => x.Length, (text, length) => text + " (" + length + ")")
.ToProperty(this, x => x.SearchTextLength, out _searchTextLength);
将设置一个名为_searchTextLength
的(ObservableAsPropertyHelper属性)字段,然后通过SearchTextLength
属性公开该字段。ObservableAsPropertyHelper
属性不能直接更改,而是通过WhenAnyValue
选择器的lambda计算得出。
有关此模式的更多信息,请参见ObservableAsPropertyHelper部分。
ReactiveCommand.CanExecute
可观察对象
WhenAny
可以为ReactiveCommand
的可不可以执行提供逻辑。
var canCreateUser = this.WhenAnyValue(
x => x.Username, x => x.Password,
(user, pass) =>
!string.IsNullOrWhiteSpace(user) &&
!string.IsNullOrWhiteSpace(pass) &&
user.Length >= 3 &&
pass.Length >= 8)
.DistinctUntilChanged();
CreateUserCommand = ReactiveCommand.CreateFromTask(CreateUser, canCreateUser);
这里,WhenAnyValue
用于观察Username
和Password
字段的更改,选择器将确定是否可以执行CreateUserCommand
,从而阻止用户执行命令直到满足验证条件。
调用命令
命令通常绑定到可由用户触发的视图中的按钮或控件。当属性值更改时,也可以触发命令。
// In the ViewModel.
this.WhenAnyValue(x => x.SearchText)
.Where(x => !String.IsNullOrWhiteSpace(x))
.Throttle(TimeSpan.FromSeconds(.25))
.InvokeCommand(SearchCommand);
// In the View.
this.Bind(ViewModel, vm => vm.SearchText, v => v.SearchTextField.Text);
在上面,只要SearchText
属性已更改,并且自上次更改过去四分之一秒,InvokeCommand
将导致SearchCommand
被调用。
视图将绑定到SearchText
属性,该属性将自动触发命令。
作为BindTo
的输入执行特定于视图的转换
理想情况下,视图上的控件直接绑定到视图模型上的属性。如果需要将视图模型值转换为特定于视图的值(例如,将bool
转换为Visibility
),则应该注册BindingConverter
。不过,可能会遇到想要直接在视图中执行转换的情况。在这里,观察视图模型的ShowToolTip
属性,将true
/false
值分别转换为1
和0
,然后将结果绑定到ToolTipLabel
的Alpha
属性。
// In the View.
ViewModel.WhenAnyValue(x => x.ShowToolTip)
.Select(show => show ? 1f : 0f)
.BindTo(this, x => x.ToolTipLabel.Alpha);
更好的选择是使用OneWayBind
属性执行绑定。
this.OneWayBind(this.ViewModel, vm => vm.ShowToolTip, view => view.ToolTipLabel.Alpha, show => show ? 1f : 0f);
WhenAny
WhenAny
允许将发送方和表达式传递到WhenAny
。这对于Sender
很重要的场景很有用,例如在View中需要了解调用属性更改的控件。
WhenAny
将传递一个Observable.Changed
对象,该对象具有以下属性:
Value
— 更改的值Sender
— 发生属性更改的对象Expression
— 更改表达式。通常外部用户不需要。
this.WhenAny(x => x.ComboBox.SelectedItem).Subscribe(x => Console.WriteLine($"The {x.Sender} changed value to {x.Value}"));
在上面,输出了Sender
和SelectedItem
属性的最新值。
WhenAnyObservable
WhenAnyObservable
观察一个或多个可观察对象并提供最新的可观察对象值,自动处理新可观察对象的订阅以及前可观察对象的取消订阅。默认情况下,WhenAnyObservable
是惰性订阅,这意味着只有订阅后才能获得值。
public class MyViewModel
{
[Reactive]
public Document Document { get; set; }
public MyViewModel()
{
this.WhenAnyObservable(x => x.Document.IsSaved).Subscribe(x => Console.WriteLine($"Document Saved: {x}"));
}
}
public class Document
{
public IObservable<bool> IsSaved { get; }
}
在上面,每当文档被保存,就会打印IsSaved
可观察对象的值。当Document
属性更改后,将会自动取消订阅并重新订阅。
其它注意事项
需要属性更改通知
使用WhenAny
变体非常简单。但是,有几个方面需要强调。
WhenAny
变体支持各种属性更改通知。例如,可以为视图模型支持INotifyPropertyChanged
,在基于Windows的XAML平台上支持DependencyProperty
,在Apple上支持NSObject
属性更改的通知。要获取值更改通知,对象必须实现这些已知属性更改通知机制之一。
如果这些都不支持,那么只会获得该属性的初始值,并且不会收到任何更新通知。此外,将在运行时发出警告(确保已经注册ILogger
服务来查看此信息)。
通常在视图模型上使用ReactiveObject
支持的INotifyPropertyChanged
接口,通常会使用ReactiveUI的RaiseAndSetIfChanged
或引发标准INotifyPropertyChanged
事件。
WhenAny
具有“冷”可观察性和行为语义
WhenAny
是纯“冷”的可观察对象,最终直接连接到UI组件事件。对于诸如DependencyProperty
之类的事件,可能不太适合通过Publish
来进行优化。当链接到ToProperty
(另一个“冷”操作符)时,必须读取(.Value
)或观察目标ObservableAsPropertyHelper
(例如,在绑定中使用或用作有订阅的另一个WhenAny
的一部分),才能执行链的所有部分。
另外,WhenAny
始终会在订阅后立即提供当前值 — 在这种情况下,它实际上是BehaviorSubject
。
WhenAny
不会在受监视的表达式中传播NullReferenceException
仅当读取给定的表达式不会引发NullReferenceException
时,WhenAny
才会推送通知。看以下代码:
this.WhenAny(x => x.Foo.Bar.Baz, _ => "Hello!")
.Subscribe(x => Console.WriteLine(x));
// Example 1
this.Foo.Bar.Baz = null;
>>> Hello!
// Example 2: Nothing printed!
this.Foo.Bar = null;
// Example 3
this.Foo.Bar = new Bar() { Baz = "Something" };
>>> Hello!
在Example 1中,尽管Baz
为null,但由于可以对表达式进行求值,因此会收到通知。
然而,在Example 2中,this.Foo.Bar.Baz
不会提供null,将会崩溃。因此,WhenAny
禁止生成任何通知。将Bar
设置为新值会生成新通知。
WhenAny
仅在输出值改变时推送通知
WhenAny
仅在输入表达式的最终值改变时推送通知。即使结果更改是由表达式链中的中间值造成的,也是如此。下面是一个说明示例:
this.WhenAny(x => x.Foo.Bar.Baz, _ => "Hello!")
.Subscribe(x => Console.WriteLine(x));
// Example 1
this.Foo.Bar.Baz = "Something";
>>> Hello!
// Example 2: Nothing printed!
this.Foo.Bar.Baz = "Something";
// Example 3: Still nothing
this.Foo.Bar = new Bar() { Baz = "Something" };
// Example 4: The result changes, so we print
this.Foo.Bar = new Bar() { Baz = "Else" };
>>> Hello!
值得注意的是,在Example 3中,即使中间的Bar
对象被新实例替换,也不会触发任何更改 — 因为完整的Foo.Bar.Baz
表达式的结果没有更改。
WhenAnyValue
内部的null传播
由于表达式还不支持此功能,因此WhenAnyValue
无法直接执行null传播。
可以通过将WhenAnyValue()
调用链接到每个属性的方式来模拟对null传播的支持。下面是一个示例:
this.WhenAnyValue(x => x.Foo, x => x.Foo.Bar, x => x.Foo.Bar.Baz, (foo, bar, baz) => foo?.Bar?.Baz)
.Subscribe(x => Console.WriteLine(x));
WhenAny如何了解不同类型的属性
WhenAny
操作符将使用ICreatesObservableForProperty
接口在Splat中注册的服务中查找。
该接口有两个方法,GetAffinityForObject
和GetNotificationForProperty
。
第一个方法GetAffinityForObject
基于属性类型、属性名称,并且在属性更改前或后进行通知,并要求对可更改的属性在转换值时的置信度进行“投票”。GetAffinityForObject
的值为0表示根本无法创建属性更改可观察对象。系统将从所有已注册的ICreatesObservableForProperty
接口中获取投票,并且投票数字最高的获胜。在最坏的情况下,POCOObservableForProperty
的值将始终大于0。这种类型的属性更改可观察对象将仅获取属性的初始值,而且从不更新。
在投票结束后,将使用属性类型、名称和是否为属性更改前或后调用GetNotificationForProperty
。然后将创建属性更改可观察对象。
当作为WhenAny
链的一部分进行调用时,将会获取链上每个属性的属性更改可观察对象。
因此,例如,this.WhenAnyValue(x => x.Property1.Property2)
将获取Property1
的属性更改可观察对象,然后是Property2
的。因此每个都可以是不同类型的可观察属性,例如,INotifyPropertyChanged
和DependencyObject
。