问题
验证属性有两个可以验证的环境:
- 服务器
- 客户
服务器验证 - 简单的多属性
如果你有任何属性:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class RequiredIfAttribute : ValidationAttribute
并把它放在你的类属性上,如下所示:
public class Client
{
public short ResidesWithCd { get; set; };
[RequiredIf(nameof(ResidesWithCd), new[] { 99 }, "Resides with other is required.")]
public string ResidesWithOther { get; set; }
}
然后,只要 服务器 转到validate an object(例如ModelState.IsValid),它就会检查每个属性上的每个ValidationAttribute 并调用.IsValid() 以确定有效性。即使 @987654324@.@987654325@ 设置为 true,这也可以正常工作。
客户端验证 - HTML 属性瓶颈
如果您通过像这样实现IClientValidatable 来启用客户端:
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var modelClientValidationRule = new ModelClientValidationRule
{
ValidationType = "requiredif",
ErrorMessage = ErrorMessageString
};
modelClientValidationRule.ValidationParameters.Add("target", prop.PropName);
modelClientValidationRule.ValidationParameters.Add("values", prop.CompValues);
return new List<ModelClientValidationRule> { modelClientValidationRule };
}
然后 ASP.NET 将在生成时发出以下 HTML:
(只要@987654327@&
@987654328@ 已启用)
<input class="form-control" type="text" value=""
id="Client_CommunicationModificationDescription"
name="Client.CommunicationModificationDescription"
data-val="true"
data-val-requiredif="Communication Modification Description is required."
data-val-requiredif-target="CommunicationModificationCd"
data-val-requiredif-values="99" >
数据属性是我们将规则转储到客户端验证引擎的唯一工具,该引擎将通过内置或自定义的适配器搜索页面上的任何属性。一旦成为客户端规则集的一部分,它将能够使用内置或自定义的方法来确定每个已解析规则的有效性。
所以我们可以调用 jQuery Validate Unobtrusive 通过添加自定义适配器来查找和解析这些属性,这将向引擎添加验证规则:
// hook up to client side validation
$.validator.unobtrusive.adapters.add('requiredif', ['target', 'values'], function (options) {
options.rules["requiredif"] = {
id: '#' + options.params.target,
values: JSON.parse(options.params.values)
};
options.messages['requiredif'] = options.message;
});
然后我们可以通过添加这样的自定义方法来告诉该规则如何运行并确定有效性,这将添加一种自定义方式来评估 requiredif 规则(与日期规则或正则表达式规则相反),这将依赖于我们的参数之前通过适配器加载:
// test validity
$.validator.addMethod('requiredif', function (value, element, params) {
var targetHasCondValue = targetElHasValue(params.id, params.value);
var requiredAndNoValue = targetHasCondValue && !value; // true -> :(
var passesValidation = !requiredAndNoValue; // true -> :)
return passesValidation;
}, '');
所有操作都是这样的:
解决方案
那么,我们学到了什么?好吧,如果我们希望相同的规则在同一个元素上多次出现,适配器将不得不多次查看每个元素的确切规则集,而无法区分多个集中的每个实例。此外,ASP.NET 不会多次呈现相同的属性名称,因为它不是有效的 html。
所以,我们要么需要:
- 将所有客户端规则合并到一个包含所有信息的 mega 属性中
- 用每个实例编号重命名属性,然后找到一种方法来解析它们。
我将探索选项一(发出单个客户端属性),您可以通过以下几种方式实现:
- 创建一个单一的属性,该属性接受多个元素以在服务器客户端上进行验证
- 保留多个不同的服务器端属性,然后通过反射合并所有属性,然后再发送到客户端
在任何一种情况下,您都必须重新编写客户端逻辑(适配器/方法)以获取一组值,而不是一次获取一个值。
我们将构建/传输一个 JSON 序列化对象,如下所示:
var props = [
{
PropName: "RoleCd",
CompValues: ["2","3","4","5"]
},
{
PropName: "IsPatient",
CompValues: ["true"]
}
]
Scripts/ValidateRequiredIfAny.js
以下是我们将如何在客户端适配器/方法中处理它:
// hook up to client side validation
$.validator.unobtrusive.adapters.add("requiredifany", ["props"], function (options) {
options.rules["requiredifany"] = { props: options.params.props };
options.messages["requiredifany"] = options.message;
});
// test validity
$.validator.addMethod("requiredifany", function (value, element, params) {
var reqIfProps = JSON.parse(params.props);
var anytargetHasValue = false;
$.each(reqIfProps, function (index, item) {
var targetSel = "#" + buildTargetId(element, item.PropName);
var $targetEl = $(targetSel);
var targetHasValue = elHasValue($targetEl, item.CompValues);
if (targetHasValue) {
anytargetHasValue = true;
return ;
}
});
var valueRequired = anytargetHasValue;
var requiredAndNoValue = valueRequired && !value; // true -> :(
var passesValidation = !requiredAndNoValue; // true -> :)
return passesValidation;
}, "");
// UTILITY METHODS
function buildTargetId(currentElement, targetPropName) {
// https://stackoverflow.com/a/39725539/1366033
// we are only provided the name of the target property
// we need to build it's ID in the DOM based on a couple assumptions
// derive the stacking context and depth based on the current element's ID/name
// append the target property's name to that context
// currentElement.name i.e. Details[0].DosesRequested
var curId = currentElement.id; // get full id i.e. Details_0__DosesRequested
var context = curId.replace(/[^_]+$/, ""); // remove last prop i.e. Details_0__
var targetId = context + targetPropName; // build target ID i.e. Details_0__OrderIncrement
// fail noisily
if ($("#" + targetId).length === 0)
console.error(
"Could not find id '" + targetId +
"' when looking for '" + targetPropName +
"' on originating element '" + curId + "'");
return targetId;
}
function elHasValue($el, values) {
var isCheckBox = $el.is(":checkbox,:radio");
var isChecked = $el.is(":checked");
var inputValue = $el.val();
var valueInArray = $.inArray(String(inputValue), values) > -1;
var hasValue = (!isCheckBox || isChecked) && valueInArray;
return hasValue;
};
Models/RequiredIfAttribute.cs
在服务器端,我们将像平常一样验证属性,但是当我们要构建客户端属性时,我们将查找所有属性并构建一个超级属性
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Web.Helpers;
using System.Web.Mvc;
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class RequiredIfAttribute : ValidationAttribute, IClientValidatable
{
public PropertyNameValues TargetProp { get; set; }
public RequiredIfAttribute(string compPropName, string[] compPropValues, string msg) : base(msg)
{
this.TargetProp = new PropertyNameValues()
{
PropName = compPropName,
CompValues = compPropValues
};
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
PropertyInfo compareProp = validationContext.ObjectType.GetProperty(TargetProp.PropName);
var compPropVal = compareProp.GetValue(validationContext.ObjectInstance, null);
string compPropValAsString = compPropVal?.ToString().ToLower() ?? "";
var matches = TargetProp.CompValues.Where(v => v == compPropValAsString);
bool needsValue = matches.Any();
if (needsValue)
{
if (value == null || value.ToString() == "" || value.ToString() == "0")
{
return new ValidationResult(FormatErrorMessage(null));
}
}
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
// at this point, who cares that we're on this particular instance - find all instances
PropertyInfo curProp = metadata.ContainerType.GetProperty(metadata.PropertyName);
RequiredIfAttribute[] allReqIfAttr = curProp.GetCustomAttributes<RequiredIfAttribute>().ToArray();
// emit validation attributes from all simultaneously, otherwise each will overwrite the last
PropertyNameValues[] allReqIfInfo = allReqIfAttr.Select(x => x.TargetProp).ToArray();
string allReqJson = Json.Encode(allReqIfInfo);
var modelClientValidationRule = new ModelClientValidationRule
{
ValidationType = "requiredifany",
ErrorMessage = ErrorMessageString
};
// add name for jQuery parameters for the adapter, must be LOWERCASE!
modelClientValidationRule.ValidationParameters.Add("props", allReqJson);
return new List<ModelClientValidationRule> { modelClientValidationRule };
}
}
public class PropertyNameValues
{
public string PropName { get; set; }
public string[] CompValues { get; set; }
}
然后我们可以通过同时应用多个属性将其绑定到我们的模型:
[RequiredIf(nameof(RelationshipCd), new[] { 1,2,3,4,5 }, "Mailing Address is required.")]
[RequiredIf(nameof(IsPatient), new[] { "true" },"Mailing Address is required.")]
public string MailingAddressLine1 { get; set; }
进一步阅读