验证

提示

此内容摘自电子书《使用 .NET MAUI 的企业应用程序模式》,可在 .NET 文档上获取,也可作为免费可下载的 PDF 脱机阅读。

Enterprise Application Patterns Using .NET MAUI eBook cover thumbnail.

接受用户输入的任何应用都应确保输入有效。 例如,应用可以检查输入是否仅包含特定范围内的字符,是否为特定长度或匹配特定格式。 如果没有验证,用户可能会提供导致应用失败的数据。 适当的验证会强制实施业务规则,有助于防止攻击者注入恶意数据。

在模型-视图-视图模型 (MVVM) 模式的上下文中,通常需要视图模型或模型来执行数据验证并向视图发出信号,指示存在验证错误,以便用户可以更正它们。 eShopOnContainers 多平台应用执行视图模型属性的同步客户端验证,并通过突出显示包含无效数据的控件来通知用户任何验证错误,并通过显示错误消息通知用户数据无效的原因。 下图显示了在 eShopOnContainers 多平台应用中执行验证所涉及的类。

Validation classes in the eShopOnContainers multi-platform app.

需要验证的视图模型属性属于 ValidatableObject<T> 类型,每个 ValidatableObject<T> 实例都将验证规则添加到其 Validations 属性中。 通过调用 ValidatableObject<T> 实例的 Validate 方法从视图模型调用验证,该方法检索验证规则并针对 ValidatableObject<T>.Value 属性执行这些规则。 任何验证错误都会放入 ValidatableObject<T> 实例的 Errors 属性中,并且 ValidatableObject<T> 实例的 IsValid 属性会更新以指示验证是成功还是失败。 下面的代码演示 ValidatableObject<T> 的实现:

using CommunityToolkit.Mvvm.ComponentModel;
namespace eShopOnContainers.Validations;
public class ValidatableObject<T> : ObservableObject, IValidity
{
    private IEnumerable<string> _errors;
    private bool _isValid;
    private T _value;
    public List<IValidationRule<T>> Validations { get; } = new();
    public IEnumerable<string> Errors
    {
        get => _errors;
        private set => SetProperty(ref _errors, value);
    }
    public bool IsValid
    {
        get => _isValid;
        private set => SetProperty(ref _isValid, value);
    }
    public T Value
    {
        get => _value;
        set => SetProperty(ref _value, value);
    }
    public ValidatableObject()
    {
        _isValid = true;
        _errors = Enumerable.Empty<string>();
    }
    public bool Validate()
    {
        Errors = Validations
            ?.Where(v => !v.Check(Value))
            ?.Select(v => v.ValidationMessage)
            ?.ToArray()
            ?? Enumerable.Empty<string>();
        IsValid = !Errors.Any();
        return IsValid;
    }
}

属性更改通知由 ObservableObject 类提供,因此 Entry 控件可以绑定到视图模型类中 ValidatableObject<T> 实例的 IsValid 属性,以便收到指示输入数据是否有效的通知。

指定验证规则

通过创建派生自 IValidationRule<T> 接口的类来指定验证规则,如以下代码示例所示:

public interface IValidationRule<T>
{
    string ValidationMessage { get; set; }
    bool Check(T value);
}

此接口指定验证规则类必须提供一个用于执行所需验证的布尔 Check 方法,以及一个 ValidationMessage 属性,其值是验证失败时将显示的验证错误消息。

以下代码示例显示了 IsNotNullOrEmptyRule<T> 验证规则,用于在 eShopOnContainers 多平台应用中使用模拟服务时,对用户在 LoginView 上输入的用户名和密码执行验证:

public class IsNotNullOrEmptyRule<T> : IValidationRule<T>
{
    public string ValidationMessage { get; set; }

    public bool Check(T value) =>
        value is string str && !string.IsNullOrWhiteSpace(str);
}

Check 方法返回一个布尔值,指示 value 参数是 null、空还是仅包含空白字符。

尽管 eShopOnContainers 多平台应用未使用,但以下代码示例显示了用于验证电子邮件地址的验证规则:

public class EmailRule<T> : IValidationRule<T>
{
    private readonly Regex _regex = new(@"^([w.-]+)@([w-]+)((.(w){2,3})+)$");

    public string ValidationMessage { get; set; }

    public bool Check(T value) =>
        value is string str && _regex.IsMatch(str);
}

Check 方法返回一个布尔值,指示 value 参数是否是有效的电子邮件地址。 这是通过搜索 value 参数以查找在 Regex 构造函数中指定的正则表达式模式的第一个匹配项来实现的。 可以通过依据 Regex.IsMatch 检查 value 来确定是否在输入字符串中找到了正则表达式模式。

注意

属性验证有时可能涉及依赖属性。 依赖属性的一个示例是属性 A 的有效值集取决于已在属性 B 中设置的特定值。若要检查属性 A 的值是否为允许值之一,需要检索属性 B 的值。此外,当属性 B 的值发生更改时,需要重新验证属性 A。

将验证规则添加到属性

在 eShopOnContainers 多平台应用中,需要验证的视图模型属性声明为 ValidatableObject<T> 类型,其中 T 是要验证的数据的类型。 以下代码示例演示了两个这样的属性:

public ValidatableObject<string> UserName { get; private set; }
public ValidatableObject<string> Password { get; private set; }

若要进行验证,必须将验证规则添加到每个 ValidatableObject<T> 实例的“验证”集合中,如以下代码示例所示:

private void AddValidations()
{
    UserName.Validations.Add(new IsNotNullOrEmptyRule<string> 
    { 
        ValidationMessage = "A username is required." 
    });

    Password.Validations.Add(new IsNotNullOrEmptyRule<string> 
    { 
        ValidationMessage = "A password is required." 
    });
}

此方法将 IsNotNullOrEmptyRule<T> 验证规则添加到每个 ValidatableObject<T> 实例的 Validations 集合中,为验证规则的 ValidationMessage 属性指定值,该属性指定验证失败时将显示的验证错误消息。

触发验证

eShopOnContainers 多平台应用中使用的验证方法可以手动触发属性验证,并在属性更改时自动触发验证。

手动触发验证

可以为视图模型属性手动触发验证。 例如,在 eShopOnContainers 多平台应用中,当用户在使用模拟服务时点击 LoginView 上的 Login 按钮时,就会发生这种情况。 命令委托调用 LoginViewModel 中的 MockSignInAsync 方法,该方法通过执行 Validate 方法来调用验证,如以下代码示例所示:

private bool Validate()
{
    bool isValidUser = ValidateUserName();
    bool isValidPassword = ValidatePassword();
    return isValidUser && isValidPassword;
}

private bool ValidateUserName()
{
    return _userName.Validate();
}

private bool ValidatePassword()
{
    return _password.Validate();
}

Validate 方法通过在每个 ValidatableObject<T> 实例上调用 Validate 方法来验证用户在 LoginView 上输入的用户名和密码。 以下代码示例显示了 ValidatableObject<T> 类中的 Validate 方法:

public bool Validate()
{
    Errors = _validations
        ?.Where(v => !v.Check(Value))
        ?.Select(v => v.ValidationMessage)
        ?.ToArray()
        ?? Enumerable.Empty<string>();

    IsValid = !Errors.Any();

    return IsValid;
}

此方法检索添加到对象的 Validations 集合中的任何验证规则。 执行检索到的每条验证规则的 Check 方法,并将未能验证数据的任何验证规则的 ValidationMessage 属性值添加到 ValidatableObject<T> 实例的 Errors 集合中。 最后,设置 IsValid 属性,并将其值返回给调用方法,指示验证是成功还是失败。

属性更改时触发验证

每当绑定属性发生更改时,也会自动触发验证。 例如,当 LoginView 中的双向绑定设置 UserNamePassword 属性时,将触发验证。 以下代码示例演示了这种情况:

<Entry Text="{Binding UserName.Value, Mode=TwoWay}">
    <Entry.Behaviors>
        <behaviors:EventToCommandBehavior
            EventName="TextChanged"
            Command="{Binding ValidateUserNameCommand}" />
    </Entry.Behaviors>
</Entry>

Entry 控件绑定到 ValidatableObject<T> 实例的 UserName.Value 属性,并且该控件的 Behaviors 集合添加了一个 EventToCommandBehavior 实例。 此行为执行 ValidateUserNameCommand 以响应在 Entry 上触发的 TextChanged 事件,当 Entry 中的文本发生更改时便会引发该事件。 反过来,ValidateUserNameCommand 委托执行 ValidateUserName 方法,该方法在 ValidatableObject<T> 实例上执行 Validate 方法。 因此,每当用户在用户名的 Entry 控件中输入一个字符时,都会对输入的数据进行验证。

显示验证错误

eShopOnContainers 多平台应用通过以红色背景突出显示包含无效数据的控件来通知用户存在任何验证错误,并通过在包含无效数据的控件下方显示一条错误消息来通知用户数据无效的原因。 无效数据得到更正后,后台会变回默认状态,错误消息也会移除。 下图显示了存在验证错误时 eShopOnContainers 多平台应用中的 LoginView

Displaying validation errors during login.

突出显示包含无效数据的控件

.NET MAUI 提供了多种向最终用户呈现验证信息的方法,但最直接的方法之一是使用 TriggersTriggers 为我们提供了一种根据控件发生的事件或数据更改来更改控件状态(通常是外观)的方法。 为了进行验证,我们将使用 DataTrigger 来侦听从绑定属性引发的更改并响应这些更改。 LoginView 上的 Entry 控件是使用以下代码设置的:

<Entry Text="{Binding UserName.Value, Mode=TwoWay}">
    <Entry.Style>
        <OnPlatform x:TypeArguments="Style">
            <On Platform="iOS, Android" Value="{StaticResource EntryStyle}" />
            <On Platform="WinUI" Value="{StaticResource WinUIEntryStyle}" />
        </OnPlatform>
    </Entry.Style>
    <Entry.Behaviors>
        <mct:EventToCommandBehavior
            EventName="TextChanged"
            Command="{Binding ValidateCommand}" />
    </Entry.Behaviors>
    <Entry.Triggers>
        <DataTrigger 
            TargetType="Entry"
            Binding="{Binding UserName.IsValid}"
            Value="False">
            <Setter Property="BackgroundColor" Value="{StaticResource ErrorColor}" />
        </DataTrigger>
    </Entry.Triggers>
</Entry>

DataTrigger 指定以下属性:

属性 说明
TargetType 触发器所属的控件类型。
Binding 数据 Binding 标记,它将为触发器条件提供更改通知和值。
Value 指定何时满足触发器条件的数据值。

对于此 Entry,我们将侦听对 LoginViewModel.UserName.IsValid 属性的更改。 每次此属性引发更改时,都会将该值与 DataTrigger 中设置的 Value 属性进行比较。 如果值相等,则将满足触发条件并且将执行提供给 DataTrigger 的任何 Setter 对象。 此控件有一个 Setter 对象,它将 BackgroundColor 属性更新为使用 StaticResource 标记定义的自定义颜色。 如果不再满足 Trigger 条件,此控件会将 Setter 对象设置的属性恢复到它们之前的状态。 有关 Triggers 的详细信息,请参阅 .NET MAUI 文档:触发器

显示错误消息

UI 在数据验证失败的每个控件下方的标签控件中显示验证错误消息。 以下代码示例显示了在用户未输入有效用户名时显示验证错误消息的 Label

<Label
    Text="{Binding UserName.Errors, Converter={StaticResource FirstValidationErrorConverter}"
    Style="{StaticResource ValidationErrorLabelStyle}" />

每个标签都绑定到正在验证的视图模型对象的 Errors 属性。 Errors 属性由 ValidatableObject<T> 类提供,属于 IEnumerable<string> 类型。 因为 Errors 属性可以包含多个验证错误,所以 FirstValidationErrorConverter 实例用于从集合中检索第一个错误以供显示。

总结

eShopOnContainers 多平台应用执行视图模型属性的同步客户端验证,并通过突出显示包含无效数据的控件来通知用户任何验证错误,并通过显示错误消息通知用户数据无效的原因。

需要验证的视图模型属性属于 ValidatableObject<T> 类型,每个 ValidatableObject<T> 实例都将验证规则添加到其 Validations 属性中。 通过调用 ValidatableObject<T> 实例的 Validate 方法从视图模型调用验证,该方法检索验证规则并针对 ValidatableObject<T> Value 属性执行这些规则。 任何验证错误都会放入 ValidatableObject<T> 实例的 Errors 属性中,并且 ValidatableObject<T> 实例的 IsValid 属性会更新以指示验证是成功还是失败。