文章

【RxMvvmLight】0x04 实现一个简易的 RxCommand

简单,但不可或缺

【RxMvvmLight】0x04 实现一个简易的 RxCommand

回忆

很久很久以前,在我刚开始使用 MVVM 模式的时候,定义一个 FooCommand 需要经历以下仪式:

  • 声明一个 FooCommand 属性
  • 声明一个 Foo 方法
  • 声明一个 CanFoo 方法
  • 在构造函数中写 FooCommand = new Command(Foo, CanFoo);

如果只是这样其实还好,有的是方法做到瞬发魔法,但如果你需要在 VM 中修改 FooCommand 的 CanExecute 就略微有一点恶心了。

举个例子:如果你希望用户输入 Name 和 Password 之后登录按钮自动亮起来,你通常需要这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private string name;
public string Name{
    get{return name;}
    set
    {
        this.SetProperty(ref name, value);
        FooCommand.NotifyExecuteChanged();
    }
}

private string password;
public string Password{
    get{return password;}
    set
    {
        this.SetProperty(ref password, value);
        FooCommand.NotifyExecuteChanged();
    }
}

private bool CanFoo(){
    return !string.IsNullOrWhitespace(Name) && !string.IsNullOrWhitespace(Password)
}

这都是上古代码了,但即便是到了今天,这些仪式依旧存在,即使使用源生成器,你也要在属性上标记[NotifyCanExecuteChangedFor(nameof(FooCommand))],老实说我觉得这个特性只是把那一行通知挪了个位置,用一行更长的代码生成一个更短的代码,我也不知道为什么要这么做。

而且还有一点很气,如果有多个 Command 依赖某个属性,那个属性上就会贴多个NotifyCanExecuteChangedFor标签,巨长无比还不能折叠起来。

我甚至觉得 NotifyCanExecuteChangedFor 是一种滥用。

另一个回忆

与这种实现相比,RxUI 的实现就比较另类,它的 CanExecute 不是一个方法,而是一个 IObservable<bool>,在有变化的时候自动触发 CanExecuteChanged,这就允许我们把 Command 的依赖写到同一处:

1
2
3
4
IObservable<bool> CanFoo(){
    return this.WhenAnyValue(x=>x.Name, x=>x.Password)
        .Select(x=>!string.IsNullOrWhitespace(x.Item1) && !string.IsNullOrWhitespace(x.Item2))
}

实质上来说,这种写法同样是把通知挪了一个位置,但他挪的比较巧妙,把所有会引发 CanExecute 变化的属性集中在一起,这样就比较好维护。

RxCommand

所以对我来说,RxUI 的 ReactiveCommand 的最大价值其实就是这个 IObservable<bool>,它虽然还允许把 Command 作为 Observable<T>使用,但我觉得有些过度设计了(也可能是我不懂 Rx)。

所以在 RxMvvmLight 中,我决定实现一个简易的 RxCommand:基本就是一个普通的 Command + IObservable<bool> 自动通知,除此之外,也就是默认运行期间自动设置 CanExecute = false 吧,我认为 80% 的场景下这是正确的行为,至于剩下的行为,遇到再说。

这个实现真的很简单,直接贴代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public abstract class RxCommand : ICommand, IDisposable
{
    public abstract event EventHandler? CanExecuteChanged;

    public abstract Observable<bool> IsRunning { get; }

    public abstract bool CanExecute(object? parameter);

    public abstract void Execute(object? parameter);

    public abstract Task ExecuteAsync(object? parameter);

    public abstract void Dispose();

    public static RxCommand Create(Action action, Observable<bool>? canExecuteObservable = null) =>
        new RxCommand<object>(_ => { action(); return Task.CompletedTask; }, canExecuteObservable);

    public static RxCommand Create(Func<Task> action, Observable<bool>? canExecuteObservable = null) =>
        new RxCommand<object>(_ => action(), canExecuteObservable);

    public static RxCommand<T> Create<T>(Action<T> action, Observable<bool>? canExecuteObservable = null) =>
        new RxCommand<T>(obj => { action(obj); return Task.CompletedTask; }, canExecuteObservable);

    public static RxCommand<T> Create<T>(Func<T, Task> action, Observable<bool>? canExecuteObservable = null) =>
        new RxCommand<T>(action, canExecuteObservable);
}

public class RxCommand<T> : RxCommand
{
    private readonly Func<T, Task> execute;
    private readonly IDisposable dis;
    private bool canExecute;
    private readonly BehaviorSubject<bool> runningSubject = new(false);

    internal RxCommand(Func<T, Task> execute, Observable<bool>? canExecuteObservable)
    {
        canExecuteObservable ??= Observable.Return(true);
        canExecuteObservable = canExecuteObservable.Prepend(true); // 保证有一个初始值
        this.execute = execute;
        this.dis = IsRunning.CombineLatest(canExecuteObservable, (isRunning, canExecute) => !isRunning && canExecute)
            .Subscribe(can =>
            {
                if (can != this.canExecute)
                {
                    this.canExecute = can;
                    CanExecuteChanged?.Invoke(this, EventArgs.Empty);
                }
            });
    }

    public override Observable<bool> IsRunning => runningSubject;

    public override event EventHandler? CanExecuteChanged;

    public override bool CanExecute(object? parameter) => canExecute;

    public override async void Execute(object? parameter) => await ExecuteAsync(parameter);

    public override async Task ExecuteAsync(object? parameter)
    {
        if (!canExecute)
            return;

        T param;
        if (parameter is T typed)
        {
            param = typed;
        }
        else if (parameter is null && default(T) is null)
        {
            param = default(T)!;
        }
        else
        {
            return;
        }

        try
        {
            runningSubject.OnNext(true);
            await execute(param);
        }
        finally
        {
            runningSubject.OnNext(false);
        }
    }

    public override void Dispose()
    {
        dis.Dispose();
        runningSubject.OnCompleted();
    }
}
本文由作者按照 CC BY 4.0 进行授权