0

MVC – Validating Additional Required Inputs

Posted September 26th, 2010 in Uncategorized by cody

In MVC, common validation can be done with a trivial amount of code using attributes on your model views, but once you step outside of the simple required, length, and type validations things can get complicated in a hurry.

Our client needed a view that required the user to provide additonal input if a question was answered as yes.  Providing this functionality required more than a simple [Required] attribute on the display model.

There are multiple ways this can be done; this post will provide a quick tutorial on how we went about implementing a solution.

We had already chosen to use the JQuery validation library provided by the MVC2 template.  The function __MVC_EnableClientValidation(validationContext) in the MicrosoftMvcJQueryValidation.js file must be modified to add hidden elements to the “ignore” list:

   1: var options = {
   2:         errorClass: "input-validation-error",
   3:         //validClass: "",
   4:         errorElement: "span",
   5:         errorPlacement: function (error, element) {
   6:             var messageSpan = fieldToMessageMappings[element.attr("name")];
   7:             $(messageSpan).empty();
   8:             $(messageSpan).removeClass("field-validation-valid");
   9:             $(messageSpan).addClass("field-validation-error");
  10:             //error.removeClass("input-validation-error");
  11:             error.addClass("field-validation-error");
  12:             error.attr("_for_validation_message", messageSpan);
  13:             error.appendTo(messageSpan);
  14:         },
  15:         messages: errorMessagesObj,
  16:         rules: rulesObj,
  17:         invalidHandler: function () { theForm.trigger('invalidHandler'); },
  18:         ignore: ":not(:visible)",
  19:         success: function (label) {
  20:             var messageSpan = $(label.attr("_for_validation_message"));
  21:             $(messageSpan).empty();
  22:             $(messageSpan).addClass("field-validation-valid");
  23:             $(messageSpan).removeClass("field-validation-error");
  24:         }
  25:     };

The next step is to create your question/additional input model:

   1: [Required()]
   2: [Display(Name = "Do you like MVC2 better than WebForms?")]
   3: [YesNoControl(new string[2] { "Date", "Why" }, "DoesLikeMvcBetter")]
   4: public bool DoesLikeMvcBetter{ get; set; }
   5: [Required(ErrorMessage = "Date is required.")]
   6: public DateTime Date { get; set; }
   7: [Required(ErrorMessage = "A reason is required.")]
   8: public string Why { get; set; }

The YesNoControl attribute will be discussed later in this post.  The model needs at least two properties, the question control as a boolean and at least one other control that will be validated, but can display any additional number of controls.  In this example, there are two additional controls, one for the date the questions is answered and a reason.

   1: <div>
   2:     <%= Html.LabelFor(m => m.IsMVCBetterThanWebForms)%>
   3:     <span style="display:block;">
   4:         <%= Html.RadioButton("IsMVCBetterThanWebForms", "true", Model != null ? Model.IsMVCBetterThanWebForms : false, new { @onclick = "$('#IsMVCBetterThanWebForms_span').show()" })%><label for="Yes" style="width:20px;display:inline">Yes</label><br />
   5:         <span id="IsMVCBetterThanWebForms_span" style="display: <%= Model != null && Model.IsMVCBetterThanWebForms ? "normal" : "none" %>; width:800px; position:relative; left:25px">
   6:             Date:<%= Html.EditorFor(m => m.Date) %><%= Html.ValidationMessageFor(m => m.Date)%>
   7:             and Why: <%= Html.EditorFor(m => m.Why) %><%= Html.ValidationMessageFor(m => m.Why)%>
   8:         </span>
   9:     </span>
  10:     <%= Html.RadioButton("IsMVCBetterThanWebForms", "false", Model != null ? !Model.IsMVCBetterThanWebForms : false, new { @onclick = "$('#IsMVCBetterThanWebForms_span').hide()" })%><label for="No" style="width:20px;display:inline-block;">No</label><br />
  11:     <%= Html.ValidationMessageFor(m => m.IsMVCBetterThanWebForms)%>
  12: </div>

Then in your view you add markup that displays a radio button for the yes/no answer.  This markup adds the radio button that the user must select yes or no.  If the user selects yes, then the follow-up questions are display and require input before the form will post successfully.

So now we have a question control.

image

If the user doesn’t enter a value, the error is caught client-side and the form will not post.

image

If the user selects “Yes”, additional inputs are displayed.

image

And must be answered before proceeding as well.

image

This takes care of the client side validation, but since the server validates the model during binding, ModelState.IsValid will return false if the user answered “No” since Date and Why are empty.  This could be handled manually in the controller code, but the route we chose was to let  model binding handle it.

The next step is to create an attribute to use to decorate the model view:

   1: [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
   2:    public sealed class YesNoControlAttribute : Attribute
   3:    {
   4:        private static string defaultTemplateName;
   5:        public static string DefaultTemplateName
   6:        {
   7:            get
   8:            {
   9:                if (string.IsNullOrEmpty(defaultTemplateName))
  10:                {
  11:                    defaultTemplateName = "YesNoControl";
  12:                }
  13:
  14:                return defaultTemplateName;
  15:            }
  16:            set
  17:            {
  18:                defaultTemplateName = value;
  19:            }
  20:        }
  21:        public string TemplateName { get; private set; }
  22:        public string[] HiddenControls { get; private set; }
  23:        public IDictionary<string, object> HtmlAttributes { get; private set; }
  24:        public string ControlName { get; set; }
  25:
  26:        public YesNoControlAttribute(object hiddenControls, string controlName)
  27:            : this(DefaultTemplateName, hiddenControls, controlName, null)
  28:        {
  29:        }
  30:
  31:        public YesNoControlAttribute(object hiddenControls, string controlName, object htmlAttributes)
  32:            : this(DefaultTemplateName, hiddenControls, controlName, htmlAttributes)
  33:        {
  34:        }
  35:
  36:        public YesNoControlAttribute(string templateName, object hiddenControls, string controlName, object htmlAttributes)
  37:        {
  38:            if (string.IsNullOrEmpty(templateName))
  39:            {
  40:                throw new ArgumentException("Template name cannot be empty.");
  41:            }
  42:
  43:            if (string.IsNullOrEmpty(controlName))
  44:            {
  45:                throw new ArgumentException("Control field cannot be empty.");
  46:            }
  47:
  48:            if (((string[])hiddenControls).Count() < 1)
  49:            {
  50:                throw new ArgumentException("Hidden Controls must contain at least one control name.");
  51:            }
  52:
  53:            TemplateName = templateName;
  54:            ControlName = controlName;
  55:            HiddenControls = (string[])hiddenControls;
  56:            HtmlAttributes = new RouteValueDictionary(htmlAttributes);
  57:        }
  58:
  59:    }

This class inherits from the attribute and allows you to decorate a property and add the name of the controls that will be displayed and validated as a required field if the use answers as yes.

Then create a custom model binder to handle the binding of your question model.  If you are not familiar with custom model binding there are many sources that have covered that topic.

   1: public class QuestionDataModelBinder : IModelBinder
   2:     {
   3:         public new BindResult BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   4:         {
   5:             var b = new BindResult(base.BindModel(controllerContext, bindingContext), null);
   6:             var pdm = b.Value as AllMyQuestionsModel;
   7:
   8:             if(pdm == null) return b;
   9:
  10:             var props = pdm.GetType().GetProperties();
  11:
  12:             // this checks if any of the properties have a custom attribute of "YesNoControlAttribute"
  13:             // if it does, it checks to see if it's false, and removes the errors of the controls specified
  14:             foreach (string ctlName in (from propertyInfo in props
  15:                                         let isYes = propertyInfo.GetCustomAttributes(typeof (YesNoControlAttribute), false).Any()
  16:                                         where isYes
  17:                                         let res = bindingContext.ValueProvider.GetValue(propertyInfo.Name)
  18:                                         where res != null && bool.FalseString.Equals(res.AttemptedValue, StringComparison.CurrentCultureIgnoreCase)
  19:                                         select propertyInfo).SelectMany(propertyInfo => Enumerable.SelectMany<string[], string>((from attrib in propertyInfo.GetCustomAttributes(typeof (YesNoControlAttribute), false)
  20:                                                                                                                                  select ((YesNoControlAttribute) attrib).HiddenControls), ctls => ctls)))
  21:             {
  22:                 RemoveErrors(bindingContext, ctlName);
  23:             }
  24:             return b;
  25:         }
  26:     }

This class checks to see if any properties on the model it’s binding have a “YesNoControl” attribute.  If the attempted value is false, it removes the model state errors that are on any of the controls specified in the attribute.

Digg This
Reddit This
Stumble Now!
Buzz This
Vote on DZone
Share on Facebook
Bookmark this on Delicious
Kick It on DotNetKicks.com
Shout it
Share on LinkedIn
Bookmark this on Technorati
Post on Twitter
Google Buzz (aka. Google Reader)

Leave a Reply