Проблема
Атрибуты проверки имеют две среды, с которыми они могут проверять:
- Сервер
- Клиент
Проверка сервера - несколько атрибутов легко
Если у вас есть какой-либо атрибут с:
[AttributeUsage(AttributeTargets.Property, <b>AllowMultiple = true</b>)]
public class RequiredIfAttribute : <b>ValidationAttribute</b>
И поместите его в свойство класса следующим образом:
public class Client
{
public short ResidesWithCd { get; set; };
<b>[RequiredIf(nameof(ResidesWithCd), new[] { 99 }, "Resides with other is required.")]</b>
public string ResidesWithOther { get; set; }
}
Затем каждый раз, когда Сервер переходит к , проверяет объект (например, ModelState.IsValid
, он проверяет каждые ValidationAttribute
на каждом свойстве и вызывает .IsValid()
для определения достоверности. Это будет работать нормально, даже если для <a href="https://msdn.microsoft.com/en-us/library/System.AttributeUsageAttribute" rel="nofollow noreferrer" title="System.AttributeUsageAttribute">AttributeUsage</a>.<a href="https://msdn.microsoft.com/en-us/library/System.AttributeUsageAttribute.AllowMultiple" rel="nofollow noreferrer" title="System.AttributeUsageAttribute.AllowMultiple">AllowMultiple</a>
установлено значение true.
Проверка клиента - узкое место атрибута HTML
Если вы включите клиентскую сторону, реализовав IClientValidatable
следующим образом:
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var modelClientValidationRule = new ModelClientValidationRule
{
ValidationType = <b>"requiredif"</b>,
ErrorMessage = ErrorMessageString
};
modelClientValidationRule.ValidationParameters.Add(<b>"target"</b>, prop.PropName);
modelClientValidationRule.ValidationParameters.Add(<b>"values"</b>, prop.CompValues);
return new List<ModelClientValidationRule> { modelClientValidationRule };
}
Тогда ASP.NET будет генерировать следующий HTML-код при генерации:
(Пока <a href="https://msdn.microsoft.com/en-us/library/System.Web.Mvc.ViewContext.ClientValidationEnabled" rel="nofollow noreferrer" title="System.Web.Mvc.ViewContext.ClientValidationEnabled">ClientValidationEnabled</a>
&
<a href="https://msdn.microsoft.com/en-us/library/System.Web.Mvc.ViewContext.UnobtrusiveJavaScriptEnabled" rel="nofollow noreferrer" title="System.Web.Mvc.ViewContext.UnobtrusiveJavaScriptEnabled">UnobtrusiveJavaScriptEnabled</a>
включены)
<input class="form-control" type="text" value=""
id="Client_CommunicationModificationDescription"
name="Client.CommunicationModificationDescription"
<i><b>data-val="true"</b>
<b>data-val-requiredif</b>="Communication Modification Description is required."
<b>data-val-requiredif-target</b>="CommunicationModificationCd"
<b>data-val-requiredif-values</b>="99" ></i>
Атрибуты данных - это единственный способ, которым мы располагаем для выгрузки правил в механизм проверки на стороне клиента, который будет искать любые атрибуты на странице через встроенный или настраиваемый адаптер . И как только он станет частью набора правил на стороне клиента, он сможет определять достоверность каждого проанализированного правила с помощью встроенного или пользовательского метода .
.
Таким образом, мы можем сделать jQuery Validate Unobtrusive для поиска и анализа этих атрибутов, добавив добавив пользовательский адаптер , который добавит правило проверки в движок:
// hook up to client side validation
$.validator.unobtrusive.adapters.add(<b>'requiredif'</b>, [<b>'target'</b>, <b>'values'</b>], function (options) {
options.rules[<b>"requiredif"</b>] = {
id: '#' + options.params.<b>target</b>,
values: JSON.parse(options.params.<b>values</b>)
};
options.messages['requiredif'] = options.<b>message</b>;
});
Затем мы можем сказать этому правилу, как функционировать, и определить валидность, добавив собственный метод, подобный этому, который добавит собственный способ оценки правил requiredif
(в отличие от правил даты или правил регулярных выражений), которые будут полагаться на параметры, которые мы загружен ранее через адаптер:
// test validity
$.validator.addMethod(<b>'requiredif'</b>, 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.
Итак, нам либо нужно:
- Сверните все правила на стороне клиента в один атрибут мега со всей информацией
- Переименуйте атрибуты для каждого номера экземпляра, а затем найдите способ их разбора в наборах.
Я исследую Вариант Один (испускающий один атрибут на стороне клиента), который вы можете сделать несколькими способами:
- Создание одного атрибута, который принимает несколько элементов для проверки на клиентском сервере
- Сохраните несколько отдельных атрибутов на стороне сервера, а затем объедините все атрибуты с помощью отражения перед отправкой клиенту
В любом случае вам придется переписать клиентскую логику (адаптер / метод), чтобы получить массив значений вместо одного значения за раз.
Мы построим / передаем сериализованный объект 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; }
Дополнительная литература