【RxMvvmLight】0x06 数据验证与 ViewModel
这明明是一件非常简单的事情,为什么会变成这样!
上一篇实现了验证引擎核心,但一个合理的框架不是一堆零件,还是需要一些胶水把他们串联起来。
IValidationObservableObject
这其实就是一个超级中转站,让 RuleFor 方法可以构造验证器所需的 Observable<T>,让验证器可以通知 View。
代码很短我就直接贴在这里
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
public interface IValidationObservableObject : INotifyDataErrorInfo
{
ReactiveValidator Validator { get; }
}
public class ValidationObservableObject : ObservableObject, IValidationObservableObject
{
public ReactiveValidator Validator {get;} = new();
public bool HasErrors => Validator.HasErrors;
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
public IEnumerable GetErrors(string? propertyName) => Validator.GetErrors(propertyName) : Enumerable.Empty<string>();
protected ValidationObservableObject()
{
Validator!.ErrorsChanged.Subscribe(errors =>
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(errors.PropertyName));
OnPropertyChanged(nameof(HasErrors));
});
}
}
public static class ValidationObservableObjectExtensions
{
public static PropertyValidatorBuilder<TValue> RuleFor<TVM, TValue>(
this TVM viewModel,
Func<TVM, TValue> expression,
[CallerArgumentExpression(nameof(expression))] string propertyName = "")
where TVM : IValidationObservableObject, INotifyDataErrorInfo, INotifyPropertyChanged
{
var name = RxMvvmLight.Helpers.PropertyNameHelper.ExtractNames(propertyName)[0];
return new(viewModel.Validator, name, viewModel.ObserveChanged(expression, propertyName));
}
public static PropertyValidatorBuilder<TValue> RuleFor<TVM, TValue>(
this TVM viewModel,
Observable<TValue> observable,
string propertyName)
where TVM : IValidationObservableObject, INotifyDataErrorInfo
{
return new(viewModel.Validator, propertyName, observable);
}
}
这么简单的代码能出问题吗?
能的兄弟,能的,考虑以下VM
1
2
3
4
5
6
7
8
9
10
public class LoginViewModel : ValidationObservableObject{
public string UserName{get; set=>this.SetProperty(ref field, value);} = "";
public LoginViewModel(){
this.RuleFor(x => x.UserName)
.NotBlank("用户名不可为空")
.Subscribe();
}
}
用户启动APP,会看到什么?明晃晃几个大字:用户名不可为空!
用户体验这块,谁会希望我一进去就一片红?
让我来解决这个问题,不过,先绕个圈子吧
我肯定不是第一个遇到这个问题的,让我看看前辈怎么解决的
学习 R3 的 BindableReactiveProperty
R3 其实自带了一个 BindableReactiveProperty 并且附带了验证功能,只是这种包装在 MVVM 里实在是难用,所以我一开始就没考虑过它。好看看它怎么解决的:
默认跳过第一个值,如果要强行验证第一个值,需要调用 ForceValidate 方法。
1
2
3
Weight = new BindableReactiveProperty<double>()
.EnableValidation(() => Weight)
.ForceValidate();
好思路,我也这么做,在 Subscribe 方法中判断一下有没有调用 ForceValidate 方法,如果没有,就跳过第一个。
在多次尝试中,我发现,如果是不可为空的属性,我总是需要跳过第一条,这是符合预期的,但对于可空的属性,比如说可选的邮箱地址之类的东西,我就必须让它走一遍验证管道。
为什么?因为我的 ReactiveValidator 还提供了一个 IsValid 属性,如果它一直不走验证管道,那状态就一直是“未验证”,但问题是,默认的空值总是有效的,所以应该让它默认走一下验证管道,刷新一下状态。
那问题来了,这不是要求用户分别处理可空不可空的属性吗?不可空的,必须写一个 Required 可空的必须写一个 ForceValidate,呵,无用的仪式增加了。
分离是一切的解药
我其实绕了很多圈子,绕了有好几天,想过的思路比如说要用户显示设置验证器初始化完成之类的,反正都需要仪式,我不喜欢,用户可以搞他们的仪式,我就别给他们加仪式了。
突然灵光一黑:我为什么非要把状态 和 消息展示 绑定在一起呢?
你看啊,状态要求过一下验证管道,但过一下验证管道不等于我要展示错误啊对不对。
思路打开,我只需要阻断View展示消息就可以了,让验证管线始终运行,但只有属性真正变化之后才展示。
思路有了,经过一些尝试,我实现了新的 ValidationObservableObject 如下
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
public class ValidationObservableObject : ObservableObject, IValidationObservableObject
{
private bool errorDisplayActivated = false;
public ReactiveValidator Validator => field ??= new();
public bool HasErrors => errorDisplayActivated && Validator.HasErrors;
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
public IEnumerable GetErrors(string? propertyName) => errorDisplayActivated ? Validator.GetErrors(propertyName) : Enumerable.Empty<string>();
protected ValidationObservableObject()
{
Validator!.ErrorsChanged.Subscribe(errors =>
{
if (this.errorDisplayActivated)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(errors.PropertyName));
OnPropertyChanged(nameof(HasErrors));
}
});
// 忽略注册验证器之前的变化
this.Changed
.Where(p => p.PropertyName != null)
.Where(p => Validator.ContainsProperty(p.PropertyName!))
.Take(1).Subscribe(x =>
{
this.errorDisplayActivated = true;
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(x.PropertyName));
});
}
}
说白了就是,默认验证器和 View 是断开的,如果有哪个注册过验证器的属性变化了,就放开限制。
这足够灵活了,而且用户啥都不用干,一切总是恰到好处,如果用户想一进去就一片红怎么办呢?好说,你注册早一点,在初始化属性之前注册验证器,或者注册完之后手动通知一下修改,就行了。
当然了,如果你并不认同我的所谓恰到好处,我推荐抄一份代码做一个自己的基类,这样不受我的影响。