[RxUI] 17 – 视图模型

[RxUI] 17 – 视图模型

每个MVVM框架的核心都是ViewModel — 虽然此类是MVVM模式中最有趣的方面,但也是最容易被误解的部分。正确推断ViewModel是和不是,对于正确应用MVVM模式至关重要。

ViewModel之禅

大多数UI框架在设计框架时就没有考虑单元测试,或者这些问题被认为超出了范围。结果,UI对象往往很难进行单元测试,因为它们不仅仅是普通对象。它们可能依赖于现有的运行循环,或经常在初始化中以某种方式调用静态类/全局变量。

因此,由于UI类是不可测试的,那么新目标就是将尽可能多的敏感的代码放入表现View的类中,但这只是可以创建的常规类。然后,希望View中的实际代码尽可能简单、机械且简短,因为它本质上是不可测试的。

这个常规的、可测试的类称为ViewModel,它是视图的模型。视图是UI的可组合部分,用于处理页面上特定且定义明确的UI功能。这意味着,每个视图通常将具有一个视图模型来处理该功能。并非必须严格如此,但通常是这样。

理解ViewModel的另一个重要方面是它们是将策略与机制分开的抽象。ViewModel不处理Button、Menu和TextBox的细节,它们仅描述这些元素中的数据如何关联。例如,“复制”ICommand不直接了解与其连接的MenuItem或Button,它仅对复制操作进行建模。视图负责将“复制”命令映射到调用它的控件。

ViewModel可重用

因为ViewModel没有显式引用UI框架或控件,所以这意味着ViewModel通常可以跨平台使用。这是一种非常强大的模式,可以大大减少移植到新平台所需的时间,尤其是与旨在帮助完成此任务的便携式库(例如SplatAkavache)结合使用时。程序的大部分敏感代码(模型/网络处理/缓存/图像加载/ViewModel)都可以在所有平台上使用,并且只需要重写与View相关的类。

常见错误和误解

许多人认为MVVM模式意味着在View代码后面应该没有代码,或者一切都应该在XAML中。尽管某些模式(例如Blend Triggers)通常会促进代码重用,但这通常是一种反模式。与XAML相比,C#是一种更具表现力,更简洁的语言,尽管可能无需编写任何代码即可创建整个复杂的View,但结果将是难以维护的,难以理解的,混乱的。

那么,如何确定要放入View中的内容呢?滚动位置和控件焦点等概念是特定于View的代码的绝佳示例。处理动画和窗口位置/最小化也是通常包含在View中的代码的出色示例。

另一个常见误解是分离 — 尽管ViewModel不引用View或View创建的任何控件十分重要,但事实并非如此。View可以非常自由紧密地绑定到ViewModel,实际上,对于View通过WhenAny、WhenAnyValue和WhenAnyObservable“进入”ViewModel来说十分有用。

丢开理论,来看看如何在ReactiveUI中创建ViewModel。

创建一个ViewModel类

ReactiveObject是ViewModel类的基础对象,它实现了INotifyPropertyChanged接口。此外,ReactiveObject提供了ChangingChanged可观察对象来监视对象的更改。使用ReactiveUI框架创建的典型ViewModel应该继承自ReactiveObject或实现IReactiveObject接口:

public class ExampleViewModel : ReactiveObject { }

读写属性

参与更改通知的属性(即更改时推送消息)以下列方式编写:

private string name;
public string Name 
{
    get => name;
    set => this.RaiseAndSetIfChanged(ref name, value);
}

注意,与其它框架不同,编写方式不可变,总是使用与上面完全相同的样板代码方式来编写。如果尝试将任何东西放入setter中,则几乎可以肯定“做错了”,应该使用WhenAnyValueToProperty

只读属性

那些仅在构造函数中初始化且不会更改的属性,不需要通过RaiseAndSetIfChanged编写,可以将它们证明为普通属性:

// Since Commands should almost always be initialized in the constructor and
// never change, they are good candidates for this pattern.
public ReactiveCommand<Object> PostTweet { get; }

public PostViewModel()
{
    PostTweet = ReactiveCommand.Create(/*...*/);
}

输出属性

到目前为止,还没有什么特别令人惊讶的,只是MVVM的样板功能。但是,ReactiveUI中还有另一种在其它框架中不存在的属性,这对于有效使用ReactiveUI非常重要,即“输出属性”。

输出属性是获取可观察对象并将其转换为ViewModel属性的一种方法。经常将它们与WhenAnyValue(其将ViewModel属性转换为可观察对象)一起使用。顾名思义,“输出属性”通常是只读的(即,源可观察对象控制属性的更改)。

首先,需要使用ObservableAsPropertyHelper<T>类声明一个输出属性:

private readonly ObservableAsPropertyHelper<string> firstName;
public string FirstName => firstName.Value;

就像读写属性,该代码也是样板代码。接下来,将使用帮助方法ToProperty在构造函数中初始化firstName

this.WhenAnyValue(x => x.Name)
    .Where(x => !string.IsNullOrEmpty(x))
    .Select(x => x.Split(' ')[0])
    .ToProperty(this, x => x.FirstName, out firstName);

这里,ToProperty创建了一个ObservableAsPropertyHelper实例,该实例将指示FirstName属性已更改。ToPropertyIObservable<T>的扩展方法,在语义上类似于“Subscribe”。

最佳实践

函数式响应编程的核心概念之一是,要编写函数式、声明性代码,而不是编写命令式代码(即“立即执行A,然后执行B,再然后执行C”)— 不使用事件处理和方法来更改属性,而是使用WhenAnyValueToProperty描述属性之间的关系

总之,编写良好的ReactiveUI ViewModel中几乎所有敏感的代码都将在构造函数中;该代码将描述ViewModel中的属性如何相互关联。编写ViewModel代码时,目标是获取描述视图根据命令和属性应该如何工作的语句,并将它们转换为可放入构造函数的内容。例如:

  • “当用户名和密码不为空时,可以按下登录按钮”
  • “错误消息应该在显示10秒后清除”
  • “DirectMessageToSend对象由目标用户和要发送的消息组成”

所有这些语句都是UI工作方式的简要说明,这些语句都可以直接转换为ViewModel构造函数中的Rx表达式。

注释 使用ReactiveUI.Fody包,可以通过使用[Reactive][ObservableAsProperty]注释属性来实现所描述的模式。负责推送属性更改的代码将在编译时自动注入高getter或setter中。

原文 https://www.reactiveui.net/docs/handbook/view-models

发表评论

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