在同一字段上使用多个自定义 DataAnnotations 与 jQuery 验证






4.50/5 (4投票s)
服务器端和客户端都使用 AllowMultiple=true 的自定义数据注解的解决方法。
引言
在本文中,我将介绍如何在 ASP.NET MVC3 应用程序中实现自定义 DataAnnotation 验证,同时支持服务器端和客户端的 AllowMultiple = true
。本文解释了在自定义验证特性上启用 AllowMultiple = true
时遇到的问题和解决方案。服务器端验证的解决方案非常简单,但要使非侵入式客户端验证正常工作非常困难,因此需要一种解决方法。
问题
可以定义自定义验证特性以满足特定的验证需求。例如,Dependent Property 验证或 RequiredIf 验证。在许多情况下,需要多次使用同一个验证特性。通过启用特性的 AllowMultiple
来实现,如下所示:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property,
AllowMultiple = true, Inherited = true)]
public class RequiredIfAttribute : ValidationAttribute,IClientValidatable
{
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
//.....
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelMetadata metadata, ControllerContext context)
{
//.....
}
}
现在,肯定有一个需求,我想在以下模型上多次使用它:
public class UserInfo
{
[Required(ErrorMessage="Name is required")]
public string Name { get; set; }
[Required(ErrorMessage="Address is required")]
public string Address { get; set; }
public string Area { get; set; }
public string AreaDetails { get; set; }
[RequiredIf("Area,AreaDetails")]
public string LandMark { get; set; }
[RequiredIf("Area,AreaDetails", "val,Yes")]
[RequiredIf("LandMark","Rocks","413402")]
[RequiredIf("LandMark", "Silicon", "500500")]
public string PinCode { get; set; }
}
首先,由于两个原因,它工作得不太好。让我以特定字段或属性 PinCode
为例进行说明:
TypeID
- 客户端验证
它实际上并没有添加三次特性,只添加了最后一个 [RequiredIf("LandMark", "Silicon", "500500")]
到该字段,并且只对该字段执行验证。
这可以通过重写 TypeID
来解决;请参阅有关 TypeID
的文章 此处。
一旦重写了 TypeID
,验证就会为同一字段上 RequiredIfAttribute
的每个实例添加,因此最终它会尝试获取每个实例的 ClientValidationRule
。如果我们像这样实现 GetClientValidation
规则:
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelMetadata metadata, ControllerContext context)
{
yield return new RequiredIfValidationRule(ErrorMessageString,
requiredFieldValue, Props, Vals);
}
public class RequiredIfValidationRule : ModelClientValidationRule
{
public RequiredIfValidationRule(string errorMessage,string reqVal,
string otherProperties,string otherValues)
{
ErrorMessage = errorMessage;
ValidationType = "requiredif";
ValidationParameters.Add("reqval", reqVal);
ValidationParameters.Add("others", otherProperties);
ValidationParameters.Add("values", otherValues);
}
}
由于我们将自定义验证特性多次添加到同一个字段或属性(例如 PinCode
),这会导致错误消息。
"Validation type names in unobtrusive client validation rules must be unique."
这是因为我们将 requiredif
客户端 ValidationType
多次分配给同一个字段。
解决方案
强制服务器端自定义验证特性的解决方案很简单;只需重写 TypeID
即可创建一个不同的特性实例。但是,要解决客户端验证问题,需要一种解决方法。我们将要做的就是:
- 使用一个静态字段来跟踪每个字段或属性有多少个特性,并根据计数,在为该字段或属性生成的每个后续规则的
ValidationType
中附加字母 a、b、c 等。 - 提供一个自定义 HTML Helper 来渲染该字段的编辑器;HTML Helper 将解析该字段上的所有“HTML-5 data-val”属性,并将它们转换为该字段的
requiredifmultiple
规则(客户端规则,对服务器端代码没有影响)。 - 为该自定义验证特性的客户端验证提供两个适配器和验证函数,一个用于字段上只有一个特性实例(即
RequiredIf
),另一个用于字段上有多个特性实例(即RequiredIfMultiple
)。
代码
以下是特性、规则、Helper、jQuery 和视图的代码:
- Attribute
- 验证规则
- HTML Helper
- jQuery
- 视图
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property,
AllowMultiple = true, Inherited = true)]
public class RequiredIfAttribute : ValidationAttribute,IClientValidatable
{
private string DefaultErrorMessageFormatString = "The {0} is required";
public List<string> DependentProperties { get; private set; }
public List<string> DependentValues { get; private set; }
public string Props { get; private set; }
public string Vals { get; private set; }
public string requiredFieldValue { get; private set; }
//To avoid multiple rules with same name
public static Dictionary<string, int> countPerField = null;
//Required if you want to use this attribute multiple times
private object _typeId = new object();
public override object TypeId
{
get { return _typeId; }
}
public RequiredIfAttribute(string dependentProperties,
string dependentValues = "", string requiredValue = "val")
{
if (string.IsNullOrWhiteSpace(dependentProperties))
{
throw new ArgumentNullException("dependentProperties");
}
string[] props = dependentProperties.Trim().Split(new char[] { ',' });
if (props != null && props.Length == 0)
{
throw new ArgumentException("Prameter Invalid:DependentProperties");
}
if (props.Contains("") || props.Contains(null))
{
throw new ArgumentException("Prameter Invalid:DependentProperties," +
"One of the Property Name is Empty");
}
string[] vals = null;
if (!string.IsNullOrWhiteSpace(dependentValues))
vals = dependentValues.Trim().Split(new char[] { ',' });
if (vals != null && vals.Length != props.Length)
{
throw new ArgumentException("Different Number " +
"Of DependentProperties And DependentValues");
}
DependentProperties = new List<string>();
DependentProperties.AddRange(props);
Props = dependentProperties.Trim();
if (vals != null)
{
DependentValues = new List<string>();
DependentValues.AddRange(vals);
Vals = dependentValues.Trim();
}
if (requiredValue == "val")
requiredFieldValue = "val";
else if (string.IsNullOrWhiteSpace(requiredValue))
{
requiredFieldValue = string.Empty;
DefaultErrorMessageFormatString = "The {0} should not be given";
}
else
{
requiredFieldValue = requiredValue;
DefaultErrorMessageFormatString =
"The {0} should be:" + requiredFieldValue;
}
if (props.Length == 1)
{
if (vals != null)
{
ErrorMessage = DefaultErrorMessageFormatString +
", When " + props[0] + " is ";
if (vals[0] == "val")
ErrorMessage += " given";
else if (vals[0] == "")
ErrorMessage += " not given";
else
ErrorMessage += vals[0];
}
else
ErrorMessage = DefaultErrorMessageFormatString +
", When " + props[0] + " is given";
}
else
{
if (vals != null)
{
ErrorMessage = DefaultErrorMessageFormatString +
", When " + dependentProperties + " are: ";
foreach (string val in vals)
{
if (val == "val")
ErrorMessage += "AnyValue,";
else if (val == "")
ErrorMessage += "Empty,";
else
ErrorMessage += val + ",";
}
ErrorMessage = ErrorMessage.Remove(ErrorMessage.Length - 1);
}
else
ErrorMessage = DefaultErrorMessageFormatString + ", When " +
dependentProperties + " are given";
}
}
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
//Validate Dependent Property Values First
for (int i = 0; i < DependentProperties.Count; i++)
{
var contextProp =
validationContext.ObjectInstance.GetType().
GetProperty(DependentProperties[i]);
var contextPropVal = Convert.ToString(contextProp.GetValue(
validationContext.ObjectInstance, null));
var requiredPropVal = "val";
if (DependentValues != null)
requiredPropVal = DependentValues[i];
if (requiredPropVal ==
"val" && string.IsNullOrWhiteSpace(contextPropVal))
return ValidationResult.Success;
else if (requiredPropVal == string.Empty &&
!string.IsNullOrWhiteSpace(contextPropVal))
return ValidationResult.Success;
else if (requiredPropVal != string.Empty && requiredPropVal !=
"val" && requiredPropVal != contextPropVal)
return ValidationResult.Success;
}
string fieldVal = (value != null ? value.ToString() : string.Empty);
if (requiredFieldValue == "val" && fieldVal.Length == 0)
return new ValidationResult(string.Format(
ErrorMessageString, validationContext.DisplayName));
else if (requiredFieldValue == string.Empty && fieldVal.Length != 0)
return new ValidationResult(string.Format(
ErrorMessageString, validationContext.DisplayName));
else if (requiredFieldValue != string.Empty && requiredFieldValue
!= "val" && requiredFieldValue != fieldVal)
return new ValidationResult(string.Format(ErrorMessageString,
validationContext.DisplayName));
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelMetadata metadata, ControllerContext context)
{
int count = 0;
string Key = metadata.ContainerType.FullName + "." + metadata.GetDisplayName();
if(countPerField==null)
countPerField = new Dictionary<string, int>();
if (countPerField.ContainsKey(Key))
{
count = ++countPerField[Key];
}
else
countPerField.Add(Key, count);
yield return new RequiredIfValidationRule(string.Format(ErrorMessageString,
metadata.GetDisplayName()), requiredFieldValue, Props, Vals, count);
}
}
查看粗体行。第一行粗体是用于静态 Dictionary(FieldName,Count)
,它将跟踪我们在同一字段上添加特性的次数。实际上,它是在 GetClientValidationRule
方法中这样做的,因此对于每个递增的计数,a、b 或 c 会附加到客户端验证规则(ValidationType
)中。不用担心静态字段和存储,在自定义 Helper 中,我们会每次清空字典,因此不会浪费任何有用的内存。下一行蓝色是关于 TypeID
的。这里我们重写了 TypeID
,以便服务器端验证对同一字段上的每个特性实例都起作用。
第三行粗体在 GetClientValidationRules
中,我们正在生成用于添加到字典的唯一键。该键代表“程序集全名”和字段本身。下一行是关于存储、递增和检索该“键”的规则计数。现在让我们看看规则的实现。
public class RequiredIfValidationRule : ModelClientValidationRule
{
public RequiredIfValidationRule(string errorMessage,string reqVal,
string otherProperties,string otherValues,int count)
{
string tmp = count == 0 ? "" : Char.ConvertFromUtf32(96 + count);
ErrorMessage = errorMessage;
ValidationType = "requiredif"+tmp;
ValidationParameters.Add("reqval", reqVal);
ValidationParameters.Add("others", otherProperties);
ValidationParameters.Add("values", otherValues);
}
}
这里的第一行粗体获取要附加到以生成唯一规则名称的字符(a、b、c..),第二行粗体将该字符附加到规则。现在让我们看看 HTML Helper 的代码。
public static class RequiredIfHelpers
{
public static MvcHtmlString EditorForRequiredIf<TModel, TValue>(
this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression,
string templateName=null, string htmlFieldName=null,
object additionalViewData=null)
{
string mvcHtml=html.EditorFor(expression, templateName,
htmlFieldName, additionalViewData).ToString();
string element = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(
ExpressionHelper.GetExpressionText(expression));
string Key = html.ViewData.Model.ToString() + "." + element;
RequiredIfAttribute.countPerField.Remove(Key);
if (RequiredIfAttribute.countPerField.Count == 0)
RequiredIfAttribute.countPerField = null;
string pattern = @"data\-val\-requiredif[a-z]+";
if (Regex.IsMatch(mvcHtml, pattern))
{
return MergeClientValidationRules(mvcHtml);
}
return MvcHtmlString.Create(mvcHtml);
}
public static MvcHtmlString MergeClientValidationRules(string str)
{
const string searchStr="data-val-requiredif";
const string val1Str="others";
const string val2Str="reqval";
const string val3Str="values";
List<XmlAttribute> mainAttribs = new List<XmlAttribute>();
List<XmlAttribute> val1Attribs = new List<XmlAttribute>();
List<XmlAttribute> val2Attribs = new List<XmlAttribute>();
List<XmlAttribute> val3Attribs = new List<XmlAttribute>();
XmlDocument doc = new XmlDocument();
doc.LoadXml(str);
XmlNode node = doc.DocumentElement;
foreach (XmlAttribute attrib in node.Attributes)
{
if (attrib.Name.StartsWith(searchStr))
{
if (attrib.Name.EndsWith("-" + val1Str))
val1Attribs.Add(attrib);
else if (attrib.Name.EndsWith("-" + val2Str))
val2Attribs.Add(attrib);
else if (attrib.Name.EndsWith("-" + val3Str))
val3Attribs.Add(attrib);
else
mainAttribs.Add(attrib);
}
}
var mainAttrib=doc.CreateAttribute(searchStr+"multiple");
var val1Attrib = doc.CreateAttribute(searchStr + "multiple-"+val1Str);
var val2Attrib = doc.CreateAttribute(searchStr + "multiple-"+val2Str);
var val3Attrib = doc.CreateAttribute(searchStr + "multiple-"+val3Str);
mainAttribs.ForEach(new Action<XmlAttribute>(delegate(XmlAttribute attrib)
{
mainAttrib.Value += attrib.Value + "!";
node.Attributes.Remove(attrib);
}
));
val1Attribs.ForEach(new Action<XmlAttribute>(delegate(XmlAttribute attrib)
{
val1Attrib.Value += attrib.Value + "!";
node.Attributes.Remove(attrib);
}
));
val2Attribs.ForEach(new Action<XmlAttribute>(delegate(XmlAttribute attrib)
{
val2Attrib.Value += attrib.Value + "!";
node.Attributes.Remove(attrib);
}
));
val3Attribs.ForEach(new Action<XmlAttribute>(delegate(XmlAttribute attrib)
{
val3Attrib.Value += attrib.Value + "!";
node.Attributes.Remove(attrib);
}
));
mainAttrib.Value=mainAttrib.Value.TrimEnd('!');
val1Attrib.Value=val1Attrib.Value.TrimEnd('!');
val2Attrib.Value=val2Attrib.Value.TrimEnd('!');
val3Attrib.Value = val3Attrib.Value.TrimEnd('!');
node.Attributes.Append(mainAttrib);
node.Attributes.Append(val1Attrib);
node.Attributes.Append(val2Attrib);
node.Attributes.Append(val3Attrib);
return MvcHtmlString.Create(node.OuterXml);
}
}
这里重要的是,使用内置的 EditorFor
获取 HTML5;但是,您可以为文本框或复选框等实现自己的逻辑。接下来是“KEY”。由于我们在特性类中使用了静态字段,因此我们可以直接访问它,因此请清除它占用的内存,并且如果字典中没有其他数据,则将字典本身设置为 null。
下一个重要的事情是 RegEx 模式:它用于查找给定字段是否具有任何 requiredif'a' 或 'b' 或 'c' ... 规则。请注意,如果计数未增长,则字段上只有一个规则,即 requiredif
,因此如果我们有任何以 a 或 b 或 c 结尾的规则,那么我们将将其转换为 requiredifmultiple
规则,这就是下一个逻辑。
这是我使用 XML 逻辑来解析和组合规则的方法。接下来是这些规则(requiredif
或 requiredifmultiple
)的客户端 jQuery。
(function ($) {
var reqIfValidator = function (value, element, params) {
var values = null;
var others = params.others.split(',');
var reqVal = params.reqval + "";
var currentVal = value + "";
if (params.values + "" != "")
values = params.values.split(',')
var retVal = false;
//Validate Dependent Prop Values First
$.each(others, function (index, value) {
var $other = $('#' + value);
var currentOtherVal = ($other.attr('type').toUpperCase() == "CHECKBOX") ?
($other.attr("checked") ? "true" : "false") :
$other.val();
var requiredOtherVal = "val";
if (values != null)
requiredOtherVal = values[index];
if (requiredOtherVal == "val" && currentOtherVal == "")
retVal = true;
else if (requiredOtherVal == "" && currentOtherVal != "")
retVal = true;
else if (requiredOtherVal != "" && requiredOtherVal != "val" &&
requiredOtherVal != currentOtherVal) {
retVal = true;
}
if (retVal == true) {
return false;
}
});
if (retVal == true)
return true;
if (reqVal == "val" && currentVal == "")
return false;
else if (reqVal == "" && currentVal != "")
return false;
else if (reqVal != "" && reqVal != "val" && reqVal != currentVal)
return false;
return true;
}
var reqIfMultipleValidator = function (value, element, params) {
var others = params.others.split('!');
var reqVals = params.reqval.split('!');
var msgs = params.errorMsgs.split('!');
var errMsg = "";
var values = null;
if (params.values + "" != "")
values = params.values.split('!')
var retVal = true;
$.each(others, function (index, val) {
var myParams = { "others": val, "reqval": reqVals[index],
"values": values[index] };
retVal = reqIfValidator(value, element, myParams);
if (retVal == false) {
errMsg = msgs[index];
return false;
}
});
if (retVal == false) {
var evalStr = "this.settings.messages." + $(element).attr("name") +
".requiredifmultiple='" + errMsg + "'";
eval(evalStr);
}
return retVal;
}
$.validator.addMethod("requiredif", reqIfValidator);
$.validator.addMethod("requiredifmultiple", reqIfMultipleValidator);
$.validator.unobtrusive.adapters.add("requiredif", ["reqval", "others", "values"],
function (options) {
options.rules['requiredif'] = {
reqval: options.params.reqval,
others: options.params.others,
values: options.params.values
};
options.messages['requiredif'] = options.message;
});
$.validator.unobtrusive.adapters.add(
"requiredifmultiple", ["reqval", "others", "values"],
function (options) {
options.rules['requiredifmultiple'] = {
reqval: options.params.reqval,
others: options.params.others,
values: options.params.values,
errorMsgs: options.message
};
options.messages['requiredifmultiple'] = "";
});
} (jQuery));
jQuery 有两个函数 RequiredIf
和 RequiredIfMultiple
,并且为这两个规则注册了两个适配器。如果字段具有 requiredif
规则,它将使用第一个函数进行客户端验证;如果它具有 requiredifmultiple
规则,则调用第二个函数,该函数会拆分值并调用第一个函数进行验证。在整个会话中,让这些函数工作是一项非常艰巨的任务。这里非常重要的一点是如何更改错误消息,因为在从服务器发送时,我们将所有错误消息组合在一起发送,并且它会在错误中显示组合的消息。我查阅了 Google 上的数千页文章来查找如何动态更改错误消息,但一无所获 /p>
因此,我只突出显示了整个 jQuery 代码中的一行,那是文章的最后一行。我在 jQuery 中放置了一个断点,并使用 Firebug 观察了这些值,最终找到了解决方案。最后是如何在视图中使用它。请注意,我在第二个代码片段中提供了 Model 代码。UserInfo
是我的 Model。
@model MVC_FirstApp.Models.UserInfo
@using MVC_FirstApp.CustomValidations.RequiredIf;
@{
ViewBag.Title = "Create";
}
<h2>Create</h2>
@section JavaScript
{
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")"
type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")"
type="text/javascript"></script>
<script src="../../CustomValidations/RequiredIf/RequiredIf.js"
type="text/javascript"></script>
}
@using (Html.BeginForm("Create", "UserInfo", FormMethod.Post,
new { id = "frmCreateUserInfo" }))
{
@Html.ValidationSummary(true)
<fieldset>
<legend>UserInfo</legend>
<div class="editor-label">
@Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Address)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Address)
@Html.ValidationMessageFor(model => model.Address)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Area)
</div>
<div class="editor-field">
@Html.DropDownList("Area",new SelectList(
new List<string>{"One","Two","Three"},null))
@Html.ValidationMessageFor(model => model.Area)
@Html.DropDownList("AreaDetails",
new SelectList(new List<string>{"Yes","No"},null))
@Html.ValidationMessageFor(model => model.AreaDetails)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.LandMark)
</div>
<div class="editor-field">
@Html.EditorForRequiredIf(model => model.LandMark)
@Html.ValidationMessageFor(model => model.LandMark)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.PinCode)
</div>
<div class="editor-field">
@Html.EditorForRequiredIf(model => model.PinCode)
@Html.ValidationMessageFor(model => model.PinCode)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
历史
First version.