任何接受用户输入的应用程序都应确保输入有效。 例如,应用程序可以检查仅包含特定范围内的字符的输入,具有一定长度,或匹配特定格式。 没有验证,用户可以提供导致应用失败的数据。 验证强制执行业务规则,并防止攻击者注入恶意数据。
在Model-ViewModel-Model(MVVM)模式的上下文中,通常需要视图模型或模型来执行数据验证,并向视图发出任何验证错误,以便用户可以对其进行更正。 eShopOnContainers移动应用程序执行视图模型属性的同步客户端验证,并通过突出显示包含无效数据的控件,并通过显示通知用户为什么数据无效的错误消息来通知用户任何验证错误。 图6-1显示了在eShopOnContainers移动应用程序中执行验证所涉及的类。
图6-1:eShopOnContainers移动应用程序中的验证类
查看需要验证的模型属性的类型为ValidatableObject ,每个ValidatableObject 实例将验证规则添加到其Validations属性中。 通过调用ValidatableObject 实例的Validate方法从视图模型中调用验证,该实例检索验证规则并根据ValidatableObject Value属性执行验证规则。 任何验证错误都被放置在ValidatableObject 实例的Errors属性中,并且ValidatableObject 实例的IsValid属性被更新,以指示验证是成功还是失败。
属性更改通知由ExtendedBindableObject类提供,因此Entry控件可以绑定到视图模型类中的ValidatableObject 实例的IsValid属性,以通知输入的数据是否有效。
指定验证规则
验证规则通过创建从IValidationRule 接口派生的类来指定,如下面的代码示例所示:
点击(此处)折叠或打开
-
public interface IValidationRule<T>
-
{
-
string ValidationMessage { get; set; }
-
bool Check(T value);
- }
此接口指定验证规则类必须提供用于执行所需验证的布尔检查方法,以及ValidationMessage属性,其值为验证失败时将显示的验证错误消息。
以下代码示例显示了IsNotNullOrEmptyRule 验证规则,用于在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方法返回一个布尔值,指示value参数是空值,空值还是仅由空格字符组成。
虽然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方法返回一个布尔值,表示value参数是否是有效的电子邮件地址。 这是通过搜索value参数来获得第一次出现在正则表达式模式中指定的正则表达式构造函数。 是否在输入字符串中找到正则表达式模式可以通过检查Match对象的Success属性的值来确定。
注意:属性验证有时可能涉及依赖属性。 依赖属性的示例是属性A的有效值集合取决于在属性B中设置的特定值。要检查属性A的值是允许的值之一将涉及检索属性B的值 此外,当属性B的值更改时,属性A将需要重新生效。
向属性添加验证规则
在eShopOnContainers手机应用程序中,需要验证的视图模型属性被声明为ValidatableObject 类型,其中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 实例的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 验证规则添加到每个ValidatableObject 实例的Validations集合中,指定验证规则的ValidationMessage属性的值,该属性指定验证失败时将显示的验证错误消息。
触发验证
eShopOnContainers移动应用程序中使用的验证方法可以手动触发属性的验证,并在属性更改时自动触发验证。
手动触发验证
视图模型属性可以手动触发验证。 例如,当使用模拟服务时,当用户点击LoginView上的Login按钮时,会发生在eShopOnContainers移动应用程序中。 命令委托在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 实例上的Validate方法来验证用户在LoginView上输入的用户名和密码。 以下代码示例显示了ValidatableObject 类中的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集合的任何验证规则。 执行每个检索的验证规则的检查方法,并且无法验证数据的任何验证规则的ValidationMessage属性值都将添加到ValidatableObject 实例的错误集合中。 最后,设置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 实例的UserName.Value属性,控件的Behaviors集合中添加了一个EventToCommandBehavior实例。 响应于条目中的[TextChanged]事件触发,此行为执行ValidateUserNameCommand,当条目中的文本更改时引发。 反过来,ValidateUserNameCommand委托执行ValidateUserName方法,该方法在ValidatableObject 实例上执行Validate方法。 因此,每当用户在用户名的Entry控件中输入一个字符时,进行输入数据的验证。
有关行为的更多信息,请参阅。
显示验证错误
ShopOnContainers手机应用程序通过突出显示包含无效数据的控件以红色线条通知用户任何验证错误,并显示一条错误消息,通知用户为什么数据在包含无效数据的控件下无效。 当无效数据更正时,行变为黑色,并删除错误消息。 eShopOnContainers手机应用程序中的LoginView如图6-2所示。
图6-2:登录时显示验证错误
突出显示包含无效数据的控件
LineColorBehavior附加行为用于突出显示已发生验证错误的Entry控件。 以下代码示例显示如何将LineColorBehavior附加行为附加到Entry控件:
点击(此处)折叠或打开
-
<Entry Text="{Binding UserName.Value, Mode=TwoWay}">
-
<Entry.Style>
-
<OnPlatform x:TypeArguments="Style"
-
iOS="{StaticResource EntryStyle}"
-
Android="{StaticResource EntryStyle}"
-
WinPhone="{StaticResource UwpEntryStyle}"/>
-
</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>
此样式将EntryColorBehavior附加行为的ApplyLineColor和LineColor附加属性设置为Entry控件。 有关样式的更多信息,请参阅Xamarin开发人员中心的样式。
当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类将添加到控件的“效果”集合中,否则将从控件的“效果”集合中移除。 有关行为的更多信息,请参阅。
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.Forms Entry控件的本机控件,并通过调用UpdateLineColor方法来更新行颜色。 OnElementPropertyChanged覆盖通过更改线颜色(如果附加的LineColor属性更改)或Entry的Height属性更改来响应Entry控件上的可绑定属性更改。 有关效果的更多信息,请参阅对Xamarin开发人员中心的影响。
当在Entry控件中输入有效数据时,它将在控件的底部应用一条黑线,以表示没有验证错误。 图6-3显示了一个例子。
图6-3:黑线表示无验证错误
Entry控件还将DataTrigger添加到其Triggers集合中。 以下代码示例显示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:红线表示验证错误
输入控件中的行将保持红色,而输入的数据无效,否则将变为黑色,表示输入的数据有效。
有关触发器的更多信息,请参阅Xamarin开发人员中心的触发器。
显示错误信息
UI会在数据失败验证的每个控件下面的Label控件中显示验证错误消息。 以下代码示例显示如果用户未输入有效的用户名,则该标签显示验证错误消息:
点击(此处)折叠或打开
-
<Label Text="{Binding UserName.Errors, Converter={StaticResource FirstValidationErrorConverter}"
- Style="{StaticResource ValidationErrorLabelStyle}" />
每个标签绑定到正在验证的视图模型对象的Errors属性。 错误属性由ValidatableObject 类提供,类型为List 。 因为Errors属性可能包含多个验证错误,FirstValidationErrorConverter实例用于从集合中检索第一个错误以进行显示。
概要
eShopOnContainers移动应用程序执行视图模型属性的同步客户端验证,并通过突出显示包含无效数据的控件,并通过显示通知用户为什么数据无效的错误消息通知用户任何验证错误。
查看需要验证的模型属性的类型为ValidatableObject ,每个ValidatableObject 实例将验证规则添加到其Validations属性中。 通过调用ValidatableObject 实例的Validate方法从视图模型中调用验证,该实例检索验证规则并根据ValidatableObject Value属性执行验证规则。 任何验证错误都被放置在ValidatableObject 实例的Errors属性中,并且ValidatableObject 实例的IsValid属性被更新,以指示验证是成功还是失败。