Click here to Skip to main content
Click here to Skip to main content

Multiple Custom DataAnnotations on Same Field With jQuery Validation

, 1 Jun 2012
Rate this:
Please Sign up or sign in to vote.
Workaround on using AllowMultiple=true for custom data annotations on both server side and client side.

Introduction

In this article, I will describe implementing custom DataAnnotation validation for ASP.NET MVC3 applications with support for AllowMultiple = true on both server side and client side. The article explains problems and solutions when you enable AllowMultiple = true on a custom defined validation attribute. The server side solution for validation is very simple but getting unobtrusive client side validation to work is difficult and a workaround is used.

The Problem

Custom validation attributes can be defined to meet specific validation requirements. E.g., Dependent Property validation or RequiredIf validation. There are many places where one would require using the same validation attribute multiple times. Enable AllowMultiple on the attribute like:

[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)
    {
        //.....
    }
}

Now there is certainly a requirement and I want to use it multiple times on the following model:

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

Firstly, it doesn't work very well for two reasons. Let me explain this for a specific field or property, PinCode:

  1. TypeID
  2. It actually does not add the attribute three times, only the last one [RequiredIf("LandMark", "Silicon", "500500")] gets added for the field and validation executes for that only.

    This can be solved by overriding TypeID; take a look at the article on TypeID here.

  3. Client side calidation
  4. Once you override the TypeID, validation gets added for each instance of RequiredIfAttribute on the same filed, so eventually it tries to get the ClientValidationRules for each instance. If we have the implementation of GetClientValidation rule like this:

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

    As we are adding the custom validation attribute multiple times on the same field or property (e.g., PinCode), this ends up with an error message.

    "Validation type names in unobtrusive client validation rules must be unique."

    This is because we are assigning the requiredif client side ValidationType to the same field multiple times.

The Solution

The solution to force a custom validation attribute on server side is simple; just override TypeID which creates a distinct instance for Attribute. But to solve the issue with client side validation, there is a workaround required. What we are going to do is:

  1. Use a static field which will keep track of how many attributes per field or property there are, and as per the count, appends letters a, b, c... in the ValidationType of each next rule produced for the field or property.
  2. Provide a custom HTML Helper to render the editor for the field; the HTML Helper will then parse all "HTML-5 data-val" attributes on the field and will convert them to a requiredifmultiple rule (client side rule which doesn't change anything on the server side code) for the field.
  3. Provide two adaptors and validation functions for the client side validation of this custom validation attribute, one if there is only one instance of the Attribute on the field (i.e., RequiredIf), another when there are multiple instances of the Attribute on the field (i.e., RequiredIfMultiple).

The Code

Following is the code for Attribute, Rule, Helper, jQuery and View:

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

    Look at the lines in bold. The first bold line is for the static Dictionary(FieldName,Count) which will keep track of the number of times we are adding the attribute on the same field. Actually it does so in the GetClientValidationRule method, so for each incremented count, a or b or c is appended to the client side validation rule (ValidationType). Don't worry about the static field and the storage, in the custom helper, we will empty the dictionary each time so none of the useful memory is wasted. The next blue line is about TypeID. Here we are overriding TypeID so that server side validation works for each instance of the attribute on the same field.

    The third bold line is inside GetClientValidationRules where we are generating the unique key for adding to the Dictionary. The key represents the "Full Assembly Name" and field name itself. The next line is all about storing, incrementing, and retrieving the count of rules for that "KEY". Now let's look at the implementation of Rule.

  3. Validation Rule
  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);
        }
    }

    Here the first bold line gets the character (a, b, c..) to be appended to produce the unique rule name and the second bold line appends the character to the rule. Now let's look at the code for the 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);
        }
    }

    Here the important things are, get HTML5 using the in-built EditorFor; however, you can implement your own logic here for text box or check box or whatever. The next is "KEY". Since in the attribute class we used a static field, it is directly accessible to us, so clear the memory used by it, and if there is no other data in the Dictionary, make the Dictionary itself null.

    The next important thing is the RegEx pattern: it is to find where the given field has any requiredif'a' or 'b' or 'c' ... rules. Note that if the count doesn't grow, we have only one rule, i.e., requiredif on the field, so if we have any rule ending with a or b or c, then we will convert it to a requiredifmultiple rule and that is the next logic.

    This method is where I used the XML logic to parse and combine the rules. The next is the client side jQuery for these rules (requiredif or requiredifmultiple).

  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 has two functions RequiredIf and RequiredIfMultiple, and two adaptors are registered for both rules. If the field has a requiredif rule, it uses first function for client side validation; if it has a requiredifmultiple rule, the second function is called which splits the values and calls the first function for validation. It was a very difficult task in the entire session to make these functions work. The very important point here is how to change the error message, because while sending from server, we are sending all the error messages combined together, and it was displaying the combined message on the error. I looked thousands of pages on Google to find how to change the error message dynamically, and got nothing Frown | :( /p>

    So I have highlighted only one line in the entire jQuery code which was the bottom line of the article. I placed a break point in jQuery and observed the values using Firebug, and finally got the solution. The final thing is how to use it in View. Please note that I have given the Model code in the second snippet. UserInfo is my Model here.

  9. View
  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>

History

First version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Satish Tate
Software Developer (Senior) Symphony Services
India India
No Biography provided

Comments and Discussions

 
QuestionQuestion about the js PinmemberMember 233208919-Jan-12 9:56 
AnswerRe: Question about the js PinmemberSatish Tate3-Feb-12 3:56 
Since you are using dotted names, it is creating array of controls which is accessed with [] in js. You can use the firbug and placing break point on evalStr will tell you if I am correct. If you add watch for $(element) you should see a collection of elements with that name!!!!
GeneralRe: Question about the js Pinmembernidhi417-Oct-12 23:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 1 Jun 2012
Article Copyright 2011 by Satish Tate
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid