自定义验证属性在同一字段上多次

时间:2011-08-02 14:18:21

标签: c# asp.net-mvc-3 jquery-validate data-annotations

如何在同一字段上多次使用相同的自定义验证属性,或者只是为服务器端和客户端验证启用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")
     {
     }
}

在dependentProperties中,我可以指定由逗号分隔的多个依赖属性,在dependentValues中,我可以指定依赖属性验证应处理哪些值,最后在requiredValue中,我可以指定要验证的字段的期望值。

在我的模型中有两个属性LandMark,PinCode和我想使用验证如下:

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

这里的值只是例如,因为我似乎可以多次添加属性并且不会出现任何编译错误,我已经在属性中实现了TypeID,如果我从中删除客户端验证,它在服务器端运行良好。但是当我在属性上实现IClientValidatable时,它给了我一个错误:

“不显眼的客户端验证规则中的验证类型名称必须是唯一的。”

任何帮助我该如何解决?

3 个答案:

答案 0 :(得分:5)

最后,我找到了自己的答案。 请看以下文章以获得解决方案 http://www.codeproject.com/KB/validation/MultipleDataAnnotations.aspx

答案 1 :(得分:3)

接受的答案(http://www.codeproject.com/KB/validation/MultipleDataAnnotations.aspx)中的链接是错误的,其他人写了一个勘误here,我建议先阅读。上面的答案不处理继承。 我相信这种替代解决方案具有一些优势(包括继承的支持),但仍然远非完美的代码 - 改进了改进。

此C#使用Json.NETStuart Leeks HTML Attribute provider

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 :(得分:2)

问题

验证属性有两个可以验证的环境:

  1. 服务器
  2. 客户端
  3. 服务器验证 - 轻松实现多个属性

    如果您有以下任何属性:

    [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()确定有效性。即使AttributeUsage.AllowMultiple设置为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:
    (只要ClientValidationEnabled&amp; UnobtrusiveJavaScriptEnabled已启用)

    <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" >
    

    数据属性是我们将规则转储到客户端验证引擎的唯一工具,该引擎将通过内置或自定义适配器在页面上搜索任何属性。并且,一旦成为客户端规则集的一部分,它就能够使用内置或自定义方法来确定每个已解析规则的有效性。

    因此我们可以通过添加自定义适配器来查找和解析这些属性,以便为引擎添加验证规则:

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

    这一切都是这样的:

    IClientValidatable

    解决方案

    那么,我们学到了什么?好吧,如果我们希望相同的规则在同一个元素上多次出现,那么适配器必须多次查看每个元素的确切规则集,无法区分多个集合中的每个实例。此外,ASP.NET不会多次呈现相同的属性名称,因为它不是有效的html。

    所以,我们要么:

    1. 将所有客户端规则折叠为一个包含所有信息的 mega 属性
    2. 使用每个实例编号重命名属性,然后找到一种在集合中解析它们的方法。
    3. 我将探索选项一(发出单个客户端属性),您可以采取以下几种方式:

      1. 创建一个属性,该属性包含多个要在服务器客户端上验证的元素
      2. 保留多个不同的服务器端属性,然后在发送到客户端之前通过反射合并所有属性
      3. 在任何一种情况下,您都必须重新编写客户端逻辑(适配器/方法)以获取值数组,而不是一次获取单个值。

        我们要构建/传输一个如下所示的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

        在服务器端,我们将像普通一样验证属性,但是当我们构建客户端属性时,我们将查找所有属性并构建一个mega属性

        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; }
        

        进一步阅读