Настраиваемый атрибут проверки несколько раз в одном поле - PullRequest
5 голосов
/ 02 августа 2011

Как я могу использовать один и тот же пользовательский атрибут проверки несколько раз в одном поле или просто включить AllowMultiple = true для проверки как на стороне сервера, так и на стороне клиента ??

У меня есть следующий пользовательский атрибут проверки:

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, 
        AllowMultiple = true, Inherited = true)]
public class RequiredIfAttribute : ValidationAttribute,IClientValidatable
{
    public RequiredIfAttribute(string dependentProperties, 
     string dependentValues = "",
     string requiredValue = "val")
     {
     }
}

Где в зависимых свойствах я могу указать несколько зависимых свойств, разделенных запятой, в зависимых значениях я могу указать, для каких значений зависимых свойств должна обрабатываться проверка, и, наконец, в требуемом значении я могу указать ожидаемое значение для проверяемого поля.

В моей модели есть два свойства LandMark, PinCode и я хочу использовать проверку следующим образом:

public string LandMark { get; set; }
[RequiredIf("LandMark","XYZ","500500")]
[RequiredIf("LandMark", "ABC", "500505")]
public string PinCode { get; set; }

Значения здесь приведены только для примера, так как кажется, что я могу добавить атрибут несколько раз и не получаю никакой ошибки компиляции, я реализовал TypeID в атрибуте, и он хорошо работает со стороны сервера, если я удалю проверку клиента из него , Но когда я реализую IClientValidatable для атрибута, он выдает мне ошибку:

«Имена типов проверки в ненавязчивых правилах проверки клиента должны быть уникальными.»

Любая помощь, как я могу решить это?

Ответы [ 3 ]

5 голосов
/ 19 августа 2011

Наконец-то здесь я нашел ответ сам.Посмотрите на следующую статью для решения http://www.codeproject.com/KB/validation/MultipleDataAnnotations.aspx

3 голосов
/ 04 июня 2013

Ссылка в принятом ответе (http://www.codeproject.com/KB/validation/MultipleDataAnnotations.aspx) глючит, и кто-то другой написал ошибку здесь , которую я бы рекомендовал прочитать первым. Ответ выше не обрабатывает наследование. Я считаю,у этого альтернативного решения есть некоторые преимущества (включая поддержку наследования), но он еще далек от идеального кода - улучшения приветствуются.

этот C # использует Json.NET и Stuart Leeks, поставщик атрибутов HTML

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
using Newtonsoft.Json;

namespace DabTrial.Infrastructure.Validation
{
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
    public abstract class MultipleValidationAttribute : ValidationAttribute, IMetadataAware
    {
        private class Validation
        {
            public ICollection<string> ErrorMessage { get; set; }
            public IDictionary<string, ICollection<object>> Attributes { get; set; }
        }
        private object _typeId = new object();
        public const string attributeName = "multipleValidations";
        public MultipleValidationAttribute()
        {
        }
        public override object TypeId
        {
            get
            {
                return this._typeId;
            }
        }
        public void OnMetadataCreated(ModelMetadata metadata)
        {
            Dictionary<string, Validation> allMultis;
            if (metadata.AdditionalValues.ContainsKey(attributeName))
            {
                allMultis = (Dictionary<string, Validation>)metadata.AdditionalValues[attributeName];
            }
            else
            {
                allMultis = new Dictionary<string, Validation>();
                metadata.AdditionalValues.Add(attributeName, allMultis);
            }
            foreach (var result in GetClientValidationRules(metadata))
            {
                if (allMultis.ContainsKey(result.ValidationType))
                {
                    var thisMulti = allMultis[result.ValidationType];
                    thisMulti.ErrorMessage.Add(result.ErrorMessage);
                    foreach (var attr in result.ValidationParameters)
                    {
                        thisMulti.Attributes[attr.Key].Add(attr.Value);
                    }
                }
                else
                {
                    var thisMulti = new Validation
                    {
                        ErrorMessage = new List<string>(),
                        Attributes = new Dictionary<string, ICollection<object>>()
                    };
                    allMultis.Add(result.ValidationType, thisMulti);
                    thisMulti.ErrorMessage.Add(result.ErrorMessage);
                    foreach (var attr in result.ValidationParameters)
                    {
                        var newList = new List<object>();
                        newList.Add(attr.Value);
                        thisMulti.Attributes.Add(attr.Key, newList);
                    }
                }
            }
        }

        public static IEnumerable<KeyValuePair<string, object>> GetAttributes(ModelMetadata metadata)
        {
            if (!metadata.AdditionalValues.ContainsKey(attributeName))
            {
                return null;
            }
            var returnVar = new List<KeyValuePair<string, object>>();
            returnVar.Add(new KeyValuePair<string,object>("data-val", true));
            var allMultis = (Dictionary<string, Validation>)metadata.AdditionalValues[attributeName];
            foreach (var multi in allMultis)
            {
                string valName = "data-val-" + multi.Key;
                returnVar.Add(new KeyValuePair<string,object>(valName, JsonConvert.SerializeObject(multi.Value.ErrorMessage)));
                returnVar.AddRange(multi.Value.Attributes.Select(a=>new KeyValuePair<string,object>(valName + '-' + a.Key, JsonConvert.SerializeObject(a.Value))));
            }
            return returnVar;
        }
        public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata,
                                                                               ControllerContext context)
        {
            throw new NotImplementedException("This function must be overriden");
        }
        public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata)
        {
            return GetClientValidationRules(metadata, null);
        }
    }
}

Global.asax содержит код

HtmlAttributeProvider.Register((metadata) =>
{
    return MultipleValidationAttribute.GetAttributes(metadata);
});

и JavaScript (в рамках пользовательской функции проверки)

function setMultiValidationValues(options, ruleName, values) {
    var i = 0, thisRule;
    for (; i < values.length; i++) {
        thisRule = (i == 0) ? ruleName : ruleName + i;
        options.messages[thisRule] = values[i].message;
        delete values[i].message;
        options.rules[thisRule] = values[i];
        if (ruleName !== thisRule) {
            (function addValidatorMethod() {
                var counter = 0;
                if (!$.validator.methods[ruleName]) {
                    if (++counter > 10) { throw new ReferenceError(ruleName + " is not defined"); }
                    setTimeout(addValidatorMethod, 100);
                    return;
                }
                if (!$.validator.methods[thisRule]) { $.validator.addMethod(thisRule, $.validator.methods[ruleName]); }
            })();
        }
    }
}
function transformValidationValues(options) {
    var rules = $.parseJSON(options.message),
        propNames = [], p, utilObj,i = 0,j, returnVar=[];
    for (p in options.params) {
        if (options.params.hasOwnProperty(p)) {
            utilObj = {};
            utilObj.key = p;
            utilObj.vals = $.parseJSON(options.params[p]);
            propNames.push(utilObj);
        }
    }
    for (; i < rules.length; i++) {
        utilObj = {};
        utilObj.message = rules[i];
        for (j=0; j < propNames.length; j++) {
            utilObj[propNames[j].key] = propNames[j].vals[i];
        }
        returnVar.push(utilObj);
    }
    return returnVar;
}

Примерего использование ниже: C #

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using System.Web.Mvc;

namespace DabTrial.Infrastructure.Validation
{
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
    public class RegexCountAttribute : MultipleValidationAttribute
    {
        # region members
        private string _defaultErrorMessageFormatString;
        protected readonly string _regexStr;
        protected readonly RegexOptions _regexOpt;
        private int _minimumCount=0;
        private int _maximumCount=int.MaxValue;
        #endregion
        #region properties
        public int MinimumCount 
        {
            get { return _minimumCount; } 
            set 
            {
                if (value < 0) { throw new ArgumentOutOfRangeException(); }
                _minimumCount = value; 
            } 
        }
        public int MaximumCount
        {
            get { return _maximumCount; }
            set 
            {
                if (value < 0) { throw new ArgumentOutOfRangeException(); }
                _maximumCount = value; 
            }
        }
        private string DefaultErrorMessageFormatString
        {
            get
            {
                if (_defaultErrorMessageFormatString == null)
                {
                    _defaultErrorMessageFormatString = string.Format(
                        "{{0}} requires a {0}{1}{2} match(es) to regex {3}", 
                        MinimumCount>0?"minimum of "+ MinimumCount:"",
                        MinimumCount > 0 && MaximumCount< int.MaxValue? " and " : "",
                        MaximumCount<int.MaxValue?"maximum of "+ MaximumCount:"",
                        _regexStr);
                }
                return _defaultErrorMessageFormatString;
            }
            set
            {
                _defaultErrorMessageFormatString = value;
            }

        }
        #endregion
        #region instantiation
        public RegexCountAttribute(string regEx, string defaultErrorMessageFormatString = null, RegexOptions regexOpt = RegexOptions.None)
        {
#if debug
            if (minimumCount < 0) { throw new ArgumentException("the minimum value must be non-negative"); }
#endif
            _regexStr = regEx;
            DefaultErrorMessageFormatString = defaultErrorMessageFormatString;
            _regexOpt = regexOpt;
        }
        #endregion
        #region methods

        protected override ValidationResult IsValid(object value,
                                                    ValidationContext validationContext)
        {
            var instr = (string)value;
            int matchCount = 0;
            if (MinimumCount > 0 && instr != null)
            {
                Match match = new Regex(_regexStr,_regexOpt).Match(instr);
                while (match.Success && ++matchCount < MinimumCount)
                {
                   match = match.NextMatch();
                }
                if (MaximumCount != int.MaxValue)
                {
                    while (match.Success && ++matchCount <= MaximumCount)
                    {
                        match = match.NextMatch();
                    }
                }
            }
            if (matchCount >= MinimumCount && matchCount <=MaximumCount)
            {
                return ValidationResult.Success;
            }
            string errorMessage = GetErrorMessage(validationContext.DisplayName);
            return new ValidationResult(errorMessage);
        }
        protected string GetErrorMessage(string displayName)
        {
            return ErrorMessage ?? string.Format(DefaultErrorMessageFormatString,
                displayName,
                MinimumCount);
        }
        private bool HasFlag(RegexOptions options, RegexOptions flag)
        {
            return ((options & flag) == flag);
        }
        private string RegexpModifier
        {
            get 
            {
                string options = string.Empty;
                if (HasFlag(_regexOpt, RegexOptions.IgnoreCase)) { options += 'i'; }
                if (HasFlag(_regexOpt, RegexOptions.Multiline)) { options += 'm'; }
                return options;
            }
        }
        public override IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata)
        {
            var returnVal = new ModelClientValidationRule {
                ErrorMessage = GetErrorMessage(metadata.DisplayName),
                ValidationType = "regexcount",
            };
            returnVal.ValidationParameters.Add("min",MinimumCount);
            returnVal.ValidationParameters.Add("max",MaximumCount);
            returnVal.ValidationParameters.Add("regex",_regexStr);
            returnVal.ValidationParameters.Add("regexopt", RegexpModifier);
            yield return returnVal;
        }
        #endregion
    }
    public class MinNonAlphanum : RegexCountAttribute
    {
        public MinNonAlphanum(int minimum) : base("[^0-9a-zA-Z]", GetDefaultErrorMessageFormatString(minimum)) 
        {
            this.MinimumCount = minimum;
        }
        private static string GetDefaultErrorMessageFormatString(int min)
        {
            if (min == 1)
            {
                return "{0} requires a minimum of {1} character NOT be a letter OR number";
            }
            return "{0} requires a minimum of {1} characters NOT be a letter OR number";
        }
    }
    public class MinDigits : RegexCountAttribute
    {
        public MinDigits(int minimum) : base(@"\d", GetDefaultErrorMessageFormatString(minimum)) 
        {
            this.MinimumCount = minimum;
        }
        private static string GetDefaultErrorMessageFormatString(int min)
        {
            if (min == 1)
            {
                return "{0} requires a minimum of {1} character is a number";
            }
            return "{0} requires a minimum of {1} characters are numbers";
        }
    }
}

JavaScript:

$.validator.addMethod("regexcount", function (value, element, params) {
    var matches = (value.match(params.regex)||[]).length
    return  matches >= params.min && matches <= params.max;
});
$.validator.unobtrusive.adapters.add("regexcount", ["min", "max", "regex", "regexopt"], function (options) {
    var args = transformValidationValues(options), i=0;
    for (; i < args.length; i++) {
        args[i].regex = new RegExp(args[i].regex, args[i].regexopt);
        delete args[i].regexopt;
    }
    setMultiValidationValues(options, "regexcount", args);
});
2 голосов
/ 07 февраля 2018

Проблема

Атрибуты проверки имеют две среды, с которыми они могут проверять:

  1. Сервер
  2. Клиент

Проверка сервера - несколько атрибутов легко

Если у вас есть какой-либо атрибут с:

[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;
}, '');

Что все работает так:

IClientValidatable

Решение

Итак, что мы узнали? Что ж, если мы хотим, чтобы одно и то же правило появлялось несколько раз на одном и том же элементе, адаптер должен был бы видеть точный набор правил несколько раз для каждого элемента, без возможности различать каждый экземпляр в нескольких наборах. Кроме того, ASP.NET не будет отображать одно и то же имя атрибута несколько раз, так как это недопустимый HTML.

Итак, нам либо нужно:

  1. Сверните все правила на стороне клиента в один атрибут мега со всей информацией
  2. Переименуйте атрибуты для каждого номера экземпляра, а затем найдите способ их разбора в наборах.

Я исследую Вариант Один (испускающий один атрибут на стороне клиента), который вы можете сделать несколькими способами:

  1. Создание одного атрибута, который принимает несколько элементов для проверки на клиентском сервере
  2. Сохраните несколько отдельных атрибутов на стороне сервера, а затем объедините все атрибуты с помощью отражения перед отправкой клиенту

В любом случае вам придется переписать клиентскую логику (адаптер / метод), чтобы получить массив значений вместо одного значения за раз.

Мы построим / передаем сериализованный объект 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; }

Дополнительная литература

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...