文章

【RxMvvmLight】0x02 让 ViewModel 与 Reactive 和谐相处

包括 ReactiveUI 在内的大多数 MVVM 框架,他们的 VM 定义其实都是一样的,核心就是实现 INotifyPropertyChanged 接口。如果能让 VM 直接提供 IObservable,想来写 Rx 代码的时候能更舒服一些。

【RxMvvmLight】0x02 让 ViewModel 与 Reactive 和谐相处

背景

包括 ReactiveUI 在内的大多数 MVVM 框架,他们的 VM 定义其实都是一样的,核心就是实现 INotifyPropertyChanged 接口。如果能让 VM 直接提供 IObservable<T>,想来写 Rx 代码的时候能更舒服一些。

既然我已经打算做一个新的 RxMvvmLight, 那就大胆一些,给这个二十年没有变化的类型加一点新东西,哪怕失败了,大不了回到传统设计呗。

说干就干

起名字

首先得给这个类型起个名字,否则后面我都不知道怎么叫它,总不好一直叫它 VM 基类吧,这个没什么好纠结的,就先叫他 ObservableObject

定义接口

目标已经定下,名字也定好了,现在需要想想怎么定义这个 ObservableObject 才能让他原生支持 Rx,而不是通过 INotifyPropertyChanged 中转。

跟着直觉走其实很快的,我都不知道合不合适,先定义了一个接口如下,反正还不知道能不能走通,就别想合不合适的事情了。

1
2
3
4
5
6
7
8
public record PropertyValue(string PropertyName, object? Value);

public interface IObservableObject
{
    Observable<PropertyValue> Changing { get; }
    Observable<PropertyValue> Changed { get; }
}

Changing 和 Changed 其实就是 INotifyPropertyChanging, INotifyPropertyChanged 的等价物,唯一的区别就是这个 PropertyValue 比事件参数多带了一个当前属性值。

实现接口

人家 ReactiveUI 那么多年没做这个,肯定是有原因的,人家只是老,又不是傻。让我想想怎么才能实现这个接口。

尝试通过 INotifyPropertyChanged 转接

这是个很自然的想法,既然总是会触发 INotifyPropertyChanged 接口,那我订阅这个接口,然后把值推出去,那不就实现了吗?不过这里有个小问题,那就是我没办法获取当前属性值。本着大力出奇迹的想法,我觉得,只要我稍微修改一下接口,加一个根据名字获取属性值的方法,比如这样:

1
2
3
4
5
6
public interface IObservableObject
{
    Observable<PropertyValue> Changing { get; }
    Observable<PropertyValue> Changed { get; }
    object? GetPropertyValue(string name);
}

然后通过源生成器给每一个 VM 生成对应的 GetPropertyValue 实现,想来就可以通过订阅事件来实现推送 Observable<PropertyValue>。至于现在,我先用反射实现一个,看看效果。

这种简单的代码 AI 写的是又快又好,比我还好。

经过试用,逻辑是通的,这个逻辑太简单了,没道理不通,代码就不贴了,临时测试用的代码,用完就删了,我也不知道那时候怎么写的hhh

成功了一半

于是,我现在面对以下问题:

  • 怎么获取当前值
  • 怎么把 object 转换回对应的类型

虽然我没细究过 ReactiveUI 的原理,但是我依稀记得,只要我订阅 this.WhenAnyValue ,马上就可以收到一个属性当前值, 我这个半成品能吗?不太行,我这个必须等待 INotifyPropertyChanged 事件,如果事件不触发,那我收不到当前值。另外,Changed 里全都是 object,这就不清真了,用object那不是又回到了c# 1.0 时代吗?

冥思苦想。

此时,我离走通整个链路就只差三个东西:

  • 属性名:用于从 Changed 里过滤属性
  • 属性值:用于提供初始值
  • 属性类型:用于强制转换

有没有什么东西能同时提供这三个东西呢?

其实有的,比如表达式树,这都是传统成熟方案了,我都不用试就知道一定能成功,别的不说,人家 ReactiveUI 就是用的这个,那问题来了,如果要用表达式树,那我还搞什么 Changed 流啊,还是得想想别的办法。

冥思苦想。

还真让我找到一个方法,我依稀记得,好像在哪个版本的 C# 里,加了一个获取调用方表达式的编译器特性。问了一下AI,说是 CallerArgumentExpression, 那么假设哈,我定义这么一个方法

1
2
3
4
5
6
    public static Observable<TValue> GetObservable<TVM, TValue>(this TVM vm, TValue value,
        [CallerArgumentExpression(nameof(value))] string name = "")
        where TVM : IObservableObject
    {

    }

我能获取什么?

  • value 就是当前值,我没道理不能直接用啊,
  • name 就是属性名,可以拿来过滤了,
  • TValue 就是属性类型

有戏,写完

1
2
3
4
5
6
7
8
9
10
11
12
    public static Observable<TValue> GetObservable<TVM, TValue>(this TVM vm, TValue value,
        [CallerArgumentExpression(nameof(value))] string name = "")
        where TVM : IObservableObject
    {
        var index = name.LastIndexOf('.');
        if (index > 0)
        {
            name = name[(index + 1)..].Trim();
        }

        return vm.Changed.Where(p => p.PropertyName == name).Select(p => (TValue)p.Value!).Prepend(value);
    }

试用一下,完美。

现在我有了新想法

既然我获取可以通过 CallerArgumentExpression 获取属性名,那如果我改造一下传统的 OnPropertyChanged, 也用这东西获取属性名,那是不是就可以直接在这里 OnPropertyChanged 里面推送新值了?如果可以,我就可以丢掉那个一看就很暴力的 GetPropertyValue 方法了?

说干就干,改造后 ObservableObject 定义如下

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
public class ObservableObject : IObservableObject, INotifyPropertyChanging, INotifyPropertyChanged
{
    private readonly Subject<PropertyValue> changingSubject = new();
    private readonly Subject<PropertyValue> changedSubject = new();

    PropertyChangingEventHandler? propertyChangingEventHandler;

    PropertyChangedEventHandler? propertyChangedEventHandler;

    public Observable<PropertyValue> Changing => changingSubject;

    public Observable<PropertyValue> Changed => changedSubject;

    // 用显式接口实现,避免子类绕过 OnPropertyChanged 方法
    event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged
    {
        add
        {
            propertyChangedEventHandler += value;
        }

        remove
        {
            propertyChangedEventHandler -= value;
        }
    }

    event PropertyChangingEventHandler? INotifyPropertyChanging.PropertyChanging
    {
        add
        {
            propertyChangingEventHandler += value;
        }

        remove
        {
            propertyChangingEventHandler -= value;
        }
    }


    protected void OnPropertyChanging<T>(T value, [CallerArgumentExpression(nameof(value))] string propertyName = "")
    {
        propertyName = propertyName[(propertyName.IndexOf('.') + 1)..].Trim();

        changingSubject?.OnNext(new(propertyName, value));
        propertyChangingEventHandler?.Invoke(this, new(propertyName));
    }

    protected void OnPropertyChanged<T>(T value, [CallerArgumentExpression(nameof(value))] string propertyName = "")
    {
        propertyName = propertyName[(propertyName.IndexOf('.') + 1)..].Trim();

        changedSubject?.OnNext(new(propertyName, value));
        propertyChangedEventHandler?.Invoke(this, new(propertyName));
    }

    protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
    {
        if (!EqualityComparer<T>.Default.Equals(field, value))
        {
            OnPropertyChanging(field, propertyName);
            field = value;
            OnPropertyChanged(value, propertyName);
            return true;
        }
        return false;
    }
}

不知道我是不是第一个对 OnPropertyChanged 下手的人,反正吧我感觉我现在大逆不道。不过,管他呢,反正别人也不用。

这样一来,子类在写 OnPropertyChanged 的时候需要带上对应的属性,比如写 this.OnPropertyChanged(Property)this.OnPropertyChanged(value) 或者 this.OnPropertyChanged(nameof(Property)) 都是不行的。 AI 在单纯读代码的情况下也不一定能正确理解这个API怎么用,反正我记得它写半天没写对,然后在思维链里面嘀咕“这个API怎么如此难用”

我承认,这个是容易写错的,但我不打算改,因为写他的概率不大,大概率是修改属性顺便通知另一个属性变化的时候才会用,而在这种场景下, 原本的写法是 OnPropertyChanged(nameof(Property)), 新写法是 OnPropertyChanged(Property) 其实是更短了,只是写法不常规,但作为一个娱乐大于实用的项目来说,这个新API足够好用,留着挺好的。

就这么定下了

这个实现挺好的,我已经懒得想别的实现会不会更好,给这个 GetObservable 补上几个重载,理论上应该比 ReactiveUI 的 this.WhenAnyValue 好用一些,至少不会有76个重载,AOT 也没问题。

而有了这个 GetObservable 不管是简单订阅变化也好,还是做数据验证,总之 ViewModel 可以提供原生的响应式流,后续在做响应式数据验证的时候大概不用从监听属性变化开始了。

我感觉,这个设计应该挺有独创性的,它不一定是最好的,但理论上讲,它是目前为止把 Rx 和 VM 结合的最紧密的,我突然有点狂了,我宣布,从现在开始,这个 RxMvvmLight 定位是下一代 RxUI。

(小声逼逼:定位是定位,实现是实现,随时烂尾hhh)

本文由作者按照 CC BY 4.0 进行授权