企业应用中的验证

注意

本电子书于 2017 年春季出版,之后再未更新。 书中有许多内容仍然很有价值,但有些材料已经过时。

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

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

Validation classes in the eShopOnContainers mobile app

图 6-1:eShopOnContainers 移动应用中的验证类

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

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

指定验证规则

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

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

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

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

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

    public bool Check(T value)  
    {  
        if (value == null)  
        {  
            return false;  
        }  

        var str = value as string;  
        return !string.IsNullOrWhiteSpace(str);  
    }  
}

Check 方法返回 boolean,指示 value 参数是 null、空还是仅包含空白字符。

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

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

    public bool Check(T value)  
    {  
        if (value == null)  
        {  
            return false;  
        }  

        var str = value as string;  
        Regex regex = new Regex(@"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$");  
        Match match = regex.Match(str);  

        return match.Success;  
    }  
}

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

注意

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

将验证规则添加到属性

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

public ValidatableObject<string> UserName  
{  
    get  
    {  
        return _userName;  
    }  
    set  
    {  
        _userName = value;  
        RaisePropertyChanged(() => UserName);  
    }  
}  

public ValidatableObject<string> Password  
{  
    get  
    {  
        return _password;  
    }  
    set  
    {  
        _password = value;  
        RaisePropertyChanged(() => Password);  
    }  
}

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

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 上的“登录”按钮时,就会发生这种情况。 命令委托调用 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.Clear();  

    IEnumerable<string> errors = _validations  
        .Where(v => !v.Check(Value))  
        .Select(v => v.ValidationMessage);  

    Errors = errors.ToList();  
    IsValid = !Errors.Any();  

    return this.IsValid;  
}

此方法清除 Errors 集合,然后检索添加到对象的 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 移动应用通过以红色线条突出显示包含无效数据的控件来通知用户存在任何验证错误,并通过在包含无效数据的控件下方显示一条错误消息来通知用户数据无效的原因。 无效数据得到更正后,线条会变为黑色,错误消息也会移除。 图 6-2 显示出现验证错误时 eShopOnContainers 移动应用中的 LoginView。

Displaying validation errors during login

图 6-2:在登录期间显示验证错误

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

LineColorBehavior 附加行为用于突出显示发生验证错误的 Entry 控件。 以下代码示例演示如何将 LineColorBehavior 附加行为附加到 Entry 控件:

<Entry Text="{Binding UserName.Value, Mode=TwoWay}">
    <Entry.Style>
        <OnPlatform x:TypeArguments="Style">
            <On Platform="iOS, Android" Value="{StaticResource EntryStyle}" />
            <On Platform="UWP" Value="{StaticResource UwpEntryStyle}" />
        </OnPlatform>
    </Entry.Style>
    ...
</Entry>

Entry 控件使用显式样式,如以下代码示例所示:

<Style x:Key="EntryStyle"  
       TargetType="{x:Type Entry}">  
    ...  
    <Setter Property="behaviors:LineColorBehavior.ApplyLineColor"  
            Value="True" />  
    <Setter Property="behaviors:LineColorBehavior.LineColor"  
            Value="{StaticResource BlackColor}" />  
    ...  
</Style>

此样式设置 Entry 控件上 LineColorBehavior 附加行为的 ApplyLineColorLineColor 附加属性。 有关样式的详细信息,请参阅 样式

设置或更改 ApplyLineColor 附加属性的值时,LineColorBehavior 附加行为将执行 OnApplyLineColorChanged 方法,如以下代码示例所示:

public static class LineColorBehavior  
{  
    ...  
    private static void OnApplyLineColorChanged(  
                BindableObject bindable, object oldValue, object newValue)  
    {  
        var view = bindable as View;  
        if (view == null)  
        {  
            return;  
        }  

        bool hasLine = (bool)newValue;  
        if (hasLine)  
        {  
            view.Effects.Add(new EntryLineColorEffect());  
        }  
        else  
        {  
            var entryLineColorEffectToRemove =   
                    view.Effects.FirstOrDefault(e => e is EntryLineColorEffect);  
            if (entryLineColorEffectToRemove != null)  
            {  
                view.Effects.Remove(entryLineColorEffectToRemove);  
            }  
        }  
    }  
}

此方法的参数提供该行为所附加到的控件的实例,以及 ApplyLineColor 附加属性的旧值和新值。 如果 ApplyLineColor 附加属性为 true,则 EntryLineColorEffect 类将被添加到控件的 Effects 集合中,否则将从控件的 Effects 集合中删除该类。 有关这些行为的详细信息,请参阅实现行为

EntryLineColorEffectRoutingEffect 类编入子类,如以下代码示例所示:

public class EntryLineColorEffect : RoutingEffect  
{  
    public EntryLineColorEffect() : base("eShopOnContainers.EntryLineColorEffect")  
    {  
    }  
}

RoutingEffect 类表示独立于平台的效果,该效果包装特定于平台的内部效果。 这简化了效果删除过程,因为对于特定于平台的效果,没有对类型信息的编译时访问。 EntryLineColorEffect 调用基类构造函数,传入由解析组名称以及每个特定于平台的效果类上指定的唯一 ID 的串联组成的参数。

下面的代码示例演示了如何在 iOS 上实现 eShopOnContainers.EntryLineColorEffect

[assembly: ResolutionGroupName("eShopOnContainers")]  
[assembly: ExportEffect(typeof(EntryLineColorEffect), "EntryLineColorEffect")]  
namespace eShopOnContainers.iOS.Effects  
{  
    public class EntryLineColorEffect : PlatformEffect  
    {  
        UITextField control;  

        protected override void OnAttached()  
        {  
            try  
            {  
                control = Control as UITextField;  
                UpdateLineColor();  
            }  
            catch (Exception ex)  
            {  
                Console.WriteLine("Can't set property on attached control. Error: ", ex.Message);  
            }  
        }  

        protected override void OnDetached()  
        {  
            control = null;  
        }  

        protected override void OnElementPropertyChanged(PropertyChangedEventArgs args)  
        {  
            base.OnElementPropertyChanged(args);  

            if (args.PropertyName == LineColorBehavior.LineColorProperty.PropertyName ||  
                args.PropertyName == "Height")  
            {  
                Initialize();  
                UpdateLineColor();  
            }  
        }  

        private void Initialize()  
        {  
            var entry = Element as Entry;  
            if (entry != null)  
            {  
                Control.Bounds = new CGRect(0, 0, entry.Width, entry.Height);  
            }  
        }  

        private void UpdateLineColor()  
        {  
            BorderLineLayer lineLayer = control.Layer.Sublayers.OfType<BorderLineLayer>()  
                                                             .FirstOrDefault();  

            if (lineLayer == null)  
            {  
                lineLayer = new BorderLineLayer();  
                lineLayer.MasksToBounds = true;  
                lineLayer.BorderWidth = 1.0f;  
                control.Layer.AddSublayer(lineLayer);  
                control.BorderStyle = UITextBorderStyle.None;  
            }  

            lineLayer.Frame = new CGRect(0f, Control.Frame.Height-1f, Control.Bounds.Width, 1f);  
            lineLayer.BorderColor = LineColorBehavior.GetLineColor(Element).ToCGColor();  
            control.TintColor = control.TextColor;  
        }  

        private class BorderLineLayer : CALayer  
        {  
        }  
    }  
}

OnAttached 方法检索 Xamarin.FormsEntry 控件的本机控件,并通过调用 UpdateLineColor 方法更新线条颜色。 OnElementPropertyChanged 重写通过在附加 LineColor 属性更改或 EntryHeight 属性更改时更新线条颜色,来响应 Entry 控件上的可绑定属性更改。 有关效果的更多信息,请参阅效果

Entry 控件中输入有效数据时,它将黑色线条应用于控件底部,以指示没有验证错误。 图 6-3 显示了一个相关示例。

Black line indicating no validation error

图 6-3:指示无验证错误的黑色线条

Entry 控件还向其 Triggers 集合添加了一个 DataTrigger。 以下代码示例显示了 DataTrigger

<Entry Text="{Binding UserName.Value, Mode=TwoWay}">  
    ...  
    <Entry.Triggers>  
        <DataTrigger   
            TargetType="Entry"  
            Binding="{Binding UserName.IsValid}"  
            Value="False">  
            <Setter Property="behaviors:LineColorBehavior.LineColor"   
                    Value="{StaticResource ErrorColor}" />  
        </DataTrigger>  
    </Entry.Triggers>  
</Entry>

DataTrigger 会监视 UserName.IsValid 属性,如果属性的值变为 false,它将执行 Setter,将 LineColorBehavior 附加行为的 LineColor 附加属性更改为红色。 图 6-4 显示了一个相关示例。

Red line indicating validation error

图 6-4:指示验证错误的红色线条

当输入的数据无效时,Entry 控件中的线条将保持红色,否则它将更改为黑色,以指示输入的数据有效。

有关触发器的详细信息,请参阅触发器

显示错误消息

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

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

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

总结

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

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