ASP.NET MVC 使用 DataAnnotations 和 ModelValidators 进行模型验证示例






4.29/5 (6投票s)
本文向读者介绍了 ASP.NET MVC 验证框架在复杂表单验证方面的灵活性,并简要说明了如何使用提供程序模型在内部创建 ModelMetadata。
引言
ASP.NET MVC 有一个非常好的验证框架。最近我一直在使用数据注解(data annotations)来玩弄 ASP.NET MVC 的验证功能,并获得了一些发现。我认为分享这些信息将对正在使用 MVC 验证框架的人有所帮助。
背景和概述
我最近一直在探索 ASP.NET MVC 验证框架,验证可以有多种类型,从简单到复杂,例如,必填字段验证、电子邮件验证、电话号码验证,或者一个字段取决于另一个字段,例如,电子邮件和确认电子邮件,密码和确认密码等,以及许多其他复杂的验证。
首先,我们如何强制执行模型上的验证规则,以及框架如何确保模型是否符合这些规则。在 ASP.NET 中,验证框架的基础是模型 **元数据(metadata)**。模型的元数据描述了在模型绑定时模型应如何进行验证。
模型属性可以用 `System.ComponentModel.DataAnnotations` 命名空间中的 **数据注解(data Annotations)** 进行修饰。注解定义了模型元数据,它们定义了当模型绑定到表单数据时模型上应该工作的验证,当然我们也可以使用相同的元数据来创建客户端验证规则,Jquery 已经有了验证插件。
ASP.NET 模型元数据系统使用 `ModelMetadata` 的多个实例来存储元数据,下面是 ModelMetadata 的结构。这里要注意的两个重要属性是 `containerType` 和 `Properties`。`ModelMetadata` 代表整个模型,以及模型中的每个属性。模型元数据由多个 `ModelMetadata` 对象实例表示。对于根模型对象,`Metadata containerType` 将为 `null`;对于模型中的属性,`containerType` 将为根模型对象。
public class ModelMetadata
{
public virtual Dictionary<string,> AdditionalValues { get; }
public Type ContainerType { get; }
public virtual bool IsRequired { get; set; }
public object Model { get; set; }
public Type ModelType { get; }
public virtual IEnumerable<modelmetadata> Properties { get; }
.
.
.
.
}
ASP.NET MVC 有很多扩展点,它为我们提供了扩展和插入自定义验证的功能。它在大多数地方使用 **提供程序(provider)** 机制,便于自定义功能的即插即用。我们可以轻松地用自定义提供程序替换现有的提供程序。
一旦模型用 **注解(annotations)** 修饰,`ModelMetadataProvider` 就会帮助获取与模型关联的 `Metadata`。框架使用 `ModelMetadataProviders.Current` 来获取模型的 `ModelMetadataProvier`(它提供从应用了 `DataAnnotations` 构建的 `ModalMetaData` 类),如果我们想自定义 ASP.NET MVC 提供元数据的方式,我们随时可以拥有自己的提供程序。
如果我们想自定义默认的模型元数据提供程序,则从 `DataAnnotationsModelMetadataProvider` 扩展自定义的模型元数据提供程序,并将 `ModelMetadataProviders.Current` 属性设置为 `CustomModelMetadataProvider`。默认情况下,`ModelMetadataProvider` 是 `DataAnnotationsModelMetadataProvider`。
`Provider` 返回的 `ModelMetadata` 由 `ModelValidator` 进行验证。对于每个注解,如 `RequiredAttribute`、`RangeAttribute`,都有一个关联的验证器,如 `RequiredAdapter`、`RangeAdapter` 等。
Using the Code
我们能创建自己的自定义验证器吗?考虑验证 `ConfirmPassword` 应该与 `Password` 匹配,或者 `ConfirmEmail` 应该与 `Email` 匹配,这些验证取决于 `Model` 的两个属性值。例如,如果我们尝试通过注解的 `IsValid(object)` 方法而不是 `ModelValidator` 来验证这一点,那将行不通,因为我们没有模型上下文可用,而我们需要模型上下文来比较模型的两个属性。
这里我创建了一个名为 `SameAsAttribute` 的注解,并创建了关联的验证器 `SameAsValidator`。`SameAsAttribute` 为模型提供元数据,而 `SameAsValidator` 根据元数据验证模型。
上面的代码包含一个 `Employee` 模型以及关联的视图页面和控制器。
以下是验证失败时 `Employee` 视图页面的外观。

以下是 `Employee` 模型的代码。
public class EmployeeModel
{
[Required]
[DisplayName("Employee Id")]
public int Id { get; set; }
[Required]
[DataType(DataType.Text)]
[DisplayName("Name")]
public string Name { get; set; }
[Required]
[DataType(DataType.Text)]
[DisplayName("Address")]
public string Address { get; set; }
[Required]
[DataType(DataType.PhoneNumber)]
[DisplayName("Phone Number")]
public int phoneNumber { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
[DisplayName("Email")]
public string Email { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
[SameAs("Email",ErrorMessage="It should be similar to Email")]
[DisplayName("Confirm Email")]
public string ConfirmEmail { get; set; }
[Required]
[DataType(DataType.Password)]
[DisplayName("Password")]
public string Password { get; set; }
[Required]
[DataType(DataType.Password)]
[DisplayName("Confirm Password")]
[SameAs("Password", ErrorMessage = "It should be similar to Password")]
public string ConfirmPassword { get; set; }
}
`EmployeeModel` 的元数据由多个 `ModelMetadata` 类实例表示。`ModelMetadata` 是一个递归数据结构。`EmployeeModel` 由 `Root ModelMetadata` 对象表示,其 `containerType` 属性为 `null`,`Properties` 属性将包含一个 `ModelMetadata` 对象集合,每个对象指向属性 `ModelMetadata`(属性元数据),例如 `Id`、`Name` 等。对于属性元数据,`containerType` 将是 `EmployeeModel` 对象。
在上面的模型中,`Confirm Email`、`Confirm Password` 属性应用了 `SameAs` 注解,并且我们还设置了依赖属性的名称,当前属性应该与该属性匹配。以下是 `SameAs` 注解的代码。
public class SameAsAttribute: ValidationAttribute
{
public string Property { get; set; }
public SameAsAttribute(string Property)
{
this.Property = Property;
}
public override bool IsValid(object value)
{
//Any additional validation logic specific to the property can go here.
return true;
}
}
上面的代码以 `Property` 作为参数,这是依赖的 `Property`(`Email`),我们需要将当前 `property`(**Confirm Email**)与之匹配。以下是验证器的代码。
public class SameAsValidator :DataAnnotationsModelValidator { public SameAsValidator(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute):base (metadata,context,attribute) { } public override IEnumerable<modelvalidationresult> Validate(object container) { var dependentField= Metadata.ContainerType.GetProperty (((SameAsAttribute)Attribute).Property); var field= Metadata.ContainerType.GetProperty(this.Metadata.PropertyName); if (dependentField != null && field !=null) { object dependentValue = dependentField.GetValue(container, null); object value = field.GetValue(container, null); if ( (dependentValue != null && dependentValue.Equals(value))) { if (!Attribute.IsValid(this.Metadata.Model)) { yield return new ModelValidationResult { Message = ErrorMessage }; } } else yield return new ModelValidationResult { Message = ErrorMessage }; } } }
在上面的代码中,`SameAsValidator` 继承自 `DataAnnotationsModelValidator`,并重写了 `Validate` 方法,该方法返回 `ModelValidationResult` 的枚举,该枚举将由框架附加到 `ModelState` 错误集合中。
如果我们不使用模型验证器进行验证,而是重写 `SameAsAttribute` 类的 `IsValid()` 方法,并在该函数中尝试应用验证逻辑,我们将无法这样做,因为我们将无法获得整个模型的上下文,我们只能获得应用了该注解的属性的值。但在这里,我们需要整个模型上下文来比较模型的两个属性。
在 `SameAsValidator` 类中,`Validate` 方法在应用于属性时,将模型对象(容器)作为参数传递给方法,因为属性对象的元数据容器是根模型对象。我们获取依赖属性和目标属性的值,比较它们,并准备一个 `ModelValidationResult` 对象的枚举列表,框架使用此结果显示验证摘要。
在给定的示例中,我们在 `Global.asax.cs` 文件中将 `SameAsAttribute` 数据注解注册到 `SameAsValidator`,并使用验证提供程序类。
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(SameAsAttribute),
typeof(SameAsValidator));
关注点
为每个注解拥有一个单独的验证器,并能够将整个模型作为参数传递给验证器,这使我们能够执行任何类型的验证。
免责声明
本文向读者介绍了如何着手进行 ASP.NET MVC 模型验证。本文提供的示例不能用于直接的生产部署。