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.
If the user doesn’t enter a value, the error is caught client-side and the form will not post.
If the user selects “Yes”, additional inputs are displayed.
And must be answered before proceeding as well.
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.













