本篇定位
ObservableValidator 在 ObservableObject 之上增加了 INotifyDataErrorInfo 数据注解验证。
1. 示例一:表单验证
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
| public partial class RegisterViewModel : ObservableValidator { [ObservableProperty] [Required(ErrorMessage = "用户名不能为空")] [MinLength(3, ErrorMessage = "用户名至少3个字符")] [NotifyDataErrorInfo] private string _userName;
[ObservableProperty] [Required(ErrorMessage = "邮箱不能为空")] [EmailAddress(ErrorMessage = "邮箱格式不正确")] [NotifyDataErrorInfo] private string _email;
[ObservableProperty] [Required(ErrorMessage = "密码不能为空")] [MinLength(6, ErrorMessage = "密码至少6个字符")] [NotifyDataErrorInfo] private string _password;
public bool CanSubmit => !HasErrors;
[RelayCommand] private void Submit() { ValidateAllProperties();
if (HasErrors) return;
MessageBox.Show("注册成功!"); } }
|
XAML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <StackPanel> <TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}" /> <TextBlock Text="{Binding UserName, ValidatesOnNotifyDataErrors=True, NotifyOnValidationError=True}" Foreground="Red" />
<TextBox Text="{Binding Email}" /> <TextBlock Text="{Binding Email, ValidatesOnNotifyDataErrors=True}" Foreground="Red" />
<PasswordBox Password="{Binding Password}" />
<Button Content="注册" Command="{Binding SubmitCommand}" /> </StackPanel>
|
行为演示:
| 操作 |
错误状态 |
| 加载页面 |
HasErrors = true(所有字段未填) |
| 输入用户名 “ab” |
UserName 错误:”至少3个字符” |
| 输入用户名 “admin” |
UserName 错误消失 |
| 点击注册 |
ValidateAllProperties() |
| 全部有效 |
HasErrors = false,注册成功 |
2. 示例二:TrySetProperty 先验证再赋值
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
| public partial class AgeViewModel : ObservableValidator { [Required] [Range(0, 150, ErrorMessage = "年龄必须在0-150之间")] private int _age;
public string SetAge(string input) { if (int.TryParse(input, out int age)) { if (TrySetProperty(ref _age, age, out var errors)) return "设置成功"; else return $"验证失败: {string.Join(", ", errors.Select(e => e.ErrorMessage))}"; } return "请输入数字"; }
[RelayCommand] private void Save() { ValidateAllProperties(); if (HasErrors) return; } }
|
3. 示例三:自定义验证注解
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
| public class PasswordMatchAttribute : ValidationAttribute { private readonly string _otherProperty;
public PasswordMatchAttribute(string otherProperty) { _otherProperty = otherProperty; ErrorMessage = "两次密码不一致"; }
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { var instance = validationContext.ObjectInstance; var otherValue = instance.GetType() .GetProperty(_otherProperty)?.GetValue(instance);
return Equals(value, otherValue) ? ValidationResult.Success : new ValidationResult(ErrorMessage); } }
public partial class RegisterViewModel : ObservableValidator { [ObservableProperty] [Required] [NotifyDataErrorInfo] private string _password;
[ObservableProperty] [Required] [PasswordMatch(nameof(Password))] [NotifyDataErrorInfo] private string _confirmPassword; }
|
4. HasErrors O(1) 实现
1 2 3 4 5
| private int totalErrors;
public bool HasErrors => this.totalErrors > 0;
|
5. ValidateProperty 源码
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
| protected internal void ValidateProperty(object? value, [CallerMemberName] string propertyName = null!) { if (!this.errors.TryGetValue(propertyName, out var propertyErrors)) { propertyErrors = new List<ValidationResult>(); this.errors.Add(propertyName, propertyErrors); }
bool errorsChanged = propertyErrors.Count > 0; propertyErrors.Clear();
this.validationContext.MemberName = propertyName; this.validationContext.DisplayName = GetDisplayNameForProperty(propertyName); bool isValid = Validator.TryValidateProperty(value, this.validationContext, propertyErrors);
if (isValid && errorsChanged) this.totalErrors--; else if (!isValid && !errorsChanged) this.totalErrors++;
if (errorsChanged || !isValid) ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); }
|
6. ValidateAllProperties 的双路径策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| protected void ValidateAllProperties() { Action<object> validationAction = EntityValidatorMap.GetValue( GetType(), type => { if (type.Assembly.GetType( "CommunityToolkit.Mvvm.ComponentModel.__Internals.__ObservableValidatorExtensions") is Type extType && extType.GetMethod("CreateAllPropertiesValidator", new[] { type }) is MethodInfo method) { return (Action<object>)method.Invoke(null, new object[] { null })!; } return Fallback(type); });
validationAction(this); }
|
下一篇预告:StrongReferenceMessenger 源码分析 — Recipient 结构体与 Mapping 设计