企业应用中的验证
注意
本电子书于 2017 年春季出版,之后再未更新。 书中有许多内容仍然很有价值,但有些材料已经过时。
接受用户输入的任何应用都应确保输入有效。 例如,应用可以检查输入是否仅包含特定范围内的字符,是否为特定长度或匹配特定格式。 如果没有验证,用户可能会提供导致应用失败的数据。 验证会强制实施业务规则,可防止攻击者注入恶意数据。
在模型-视图-视图模型 (MVVM) 模式的上下文中,通常需要视图模型或模型来执行数据验证并向视图发出信号,指示存在验证错误,以便用户可以更正它们。 eShopOnContainers 移动应用执行视图模型属性的同步客户端验证,通过突出显示包含无效数据的控件来通知用户任何验证错误,并通过显示错误消息告知用户数据无效的原因。 图 6-1 显示了在 eShopOnContainers 移动应用中执行验证所涉及的类。
图 6-1:eShopOnContainers 移动应用中的验证类
需要验证的视图模型属性属于 ValidatableObject<T>
类型,每个 ValidatableObject<T>
实例都将验证规则添加到其 Validations
属性中。 通过调用Validate
实例的方法ValidatableObject<T>
从视图模型调用验证,该方法检索验证规则,并针对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);
}
此接口指定验证规则类必须提供用于boolean
Check
执行所需验证的方法,以及一个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
中的双向绑定设置 UserName
或 Password
属性时,将触发验证。 以下代码示例演示了这种情况:
<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。
图 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
附加行为的 ApplyLineColor
和 LineColor
附加属性。 有关样式的详细信息,请参阅 样式。
设置或更改 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
集合中删除该类。 有关这些行为的详细信息,请参阅实现行为。
EntryLineColorEffect
将 RoutingEffect
类编入子类,如以下代码示例所示:
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
属性更改或 Entry
的 Height
属性更改时更新线条颜色,来响应 Entry
控件上的可绑定属性更改。 有关效果的更多信息,请参阅效果。
在 Entry
控件中输入有效数据时,它将黑色线条应用于控件底部,以指示没有验证错误。 图 6-3 显示了一个相关示例。
图 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 显示了一个相关示例。
图 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
属性中。 通过调用Validate
实例的方法ValidatableObject<T>
从视图模型调用验证,该方法检索验证规则,并针对ValidatableObject<T>
Value
属性执行这些规则。 任何验证错误都会放入 ValidatableObject<T>
实例的 Errors
属性中,并且 ValidatableObject<T>
实例的 IsValid
属性会更新以指示验证是成功还是失败。