65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (4投票s)

2011年7月31日

CPOL

5分钟阅读

viewsIcon

65935

服务器端和客户端都使用 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 为例进行说明:

  1. TypeID
  2. 它实际上并没有添加三次特性,只添加了最后一个 [RequiredIf("LandMark", "Silicon", "500500")] 到该字段,并且只对该字段执行验证。

    这可以通过重写 TypeID 来解决;请参阅有关 TypeID 的文章 此处

  3. 客户端验证
  4. 一旦重写了 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 即可创建一个不同的特性实例。但是,要解决客户端验证问题,需要一种解决方法。我们将要做的就是:

  1. 使用一个静态字段来跟踪每个字段或属性有多少个特性,并根据计数,在为该字段或属性生成的每个后续规则的 ValidationType 中附加字母 a、b、c 等。
  2. 提供一个自定义 HTML Helper 来渲染该字段的编辑器;HTML Helper 将解析该字段上的所有“HTML-5 data-val”属性,并将它们转换为该字段的 requiredifmultiple 规则(客户端规则,对服务器端代码没有影响)。
  3. 为该自定义验证特性的客户端验证提供两个适配器和验证函数,一个用于字段上只有一个特性实例(即 RequiredIf),另一个用于字段上有多个特性实例(即 RequiredIfMultiple)。

代码

以下是特性、规则、Helper、jQuery 和视图的代码:

  1. Attribute
  2. [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 中,我们正在生成用于添加到字典的唯一键。该键代表“程序集全名”和字段本身。下一行是关于存储、递增和检索该“键”的规则计数。现在让我们看看规则的实现。

  3. 验证规则
  4. 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 的代码。

  5. HTML Helper
  6. 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 逻辑来解析和组合规则的方法。接下来是这些规则(requiredifrequiredifmultiple)的客户端 jQuery。

  7. jQuery
  8. (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 有两个函数 RequiredIfRequiredIfMultiple,并且为这两个规则注册了两个适配器。如果字段具有 requiredif 规则,它将使用第一个函数进行客户端验证;如果它具有 requiredifmultiple 规则,则调用第二个函数,该函数会拆分值并调用第一个函数进行验证。在整个会话中,让这些函数工作是一项非常艰巨的任务。这里非常重要的一点是如何更改错误消息,因为在从服务器发送时,我们将所有错误消息组合在一起发送,并且它会在错误中显示组合的消息。我查阅了 Google 上的数千页文章来查找如何动态更改错误消息,但一无所获 Frown | :( /p>

    因此,我只突出显示了整个 jQuery 代码中的一行,那是文章的最后一行。我在 jQuery 中放置了一个断点,并使用 Firebug 观察了这些值,最终找到了解决方案。最后是如何在视图中使用它。请注意,我在第二个代码片段中提供了 Model 代码。UserInfo 是我的 Model。

  9. 视图
  10. @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.

© . All rights reserved.