[RxUI] 19 – When Any

[RxUI] 19 – When Any

在交互式UI应用程序中,状态不断变化以响应用户操作和应用程序事件。ReactiveUI可以将程序状态的更改表示为值流,并可以使用功能强大的Reactive Extensions库进行组合和操作。

当考虑它时,动机足够直观。不难想象,对属性的更改可以视为事件 — 这就是INotifyPropertyChanged的工作方式。从这里,在Rx上使用的参数与事件上的相同。特别是在MVVM应用程序设计的上下文中,将属性更改为可观察对象具有以下优点:

  • 可以根据属性更改来定义程序的逻辑
  • 可以使用Rx操作符以声明的方式构建和表达逻辑
  • 由于像时间和异步等概念在可观察对象上下文中得到了一流的处理,因此更易于推理。

ReactiveUI提供了WhenAny的多种变体,来帮助将属性作为可观察流使用。

WhenAny是什么

WhenAny是一组扩展方法,它们以WhenAny前缀开头,可以在对象的属性发生更改时推送通知。

当需要在一个或多个属性更改时获取通知,可以考虑WhenAny扩展方法。

WhenAny支持多种属性类型,包括INotifyPropertyChangedDependencyPropertyBindableProperty

它会检查该属性对每种属性类型的支持,当调用Subscribe时,它将订阅适当的属性通知机制提供的事件。

默认情况下,WhenAny只是这些属性通知事件的包装,并且不会在订阅之前存储任何值。可以使用诸如PublishReplay之类的技术来获取并存储这些值。

还可以将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用于观察UsernamePassword字段的更改,选择器将确定是否可以执行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值分别转换为10,然后将结果绑定到ToolTipLabelAlpha属性。

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

在上面,输出了SenderSelectedItem属性的最新值。

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中注册的服务中查找。

该接口有两个方法,GetAffinityForObjectGetNotificationForProperty

第一个方法GetAffinityForObject基于属性类型、属性名称,并且在属性更改前或后进行通知,并要求对可更改的属性在转换值时的置信度进行“投票”。GetAffinityForObject的值为0表示根本无法创建属性更改可观察对象。系统将从所有已注册的ICreatesObservableForProperty接口中获取投票,并且投票数字最高的获胜。在最坏的情况下,POCOObservableForProperty的值将始终大于0。这种类型的属性更改可观察对象将仅获取属性的初始值,而且从不更新。

在投票结束后,将使用属性类型、名称和是否为属性更改前或后调用GetNotificationForProperty。然后将创建属性更改可观察对象。

当作为WhenAny链的一部分进行调用时,将会获取链上每个属性的属性更改可观察对象。

因此,例如,this.WhenAnyValue(x => x.Property1.Property2)将获取Property1的属性更改可观察对象,然后是Property2的。因此每个都可以是不同类型的可观察属性,例如,INotifyPropertyChangedDependencyObject

原文 https://www.reactiveui.net/docs/handbook/when-any

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注