Wednesday, October 13, 2010

MVC - Validating Required Optional Inputs

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] attirbute 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 first 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 provides an attribute that 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.


A sample decoration is as follows:




1: [Required()]
2: [Display(Name = "Have you ever applied to any other certification program?")]
3: [YesNoControl(new string[2] { "AppliedDate", "AppliedTo" }, "HasAppliedToOtherProgram")]
4: public bool HasAppliedToOtherProgram { get; set; }
5: [Required(ErrorMessage = "Applied Date is required.")]
6: public DateTime AppliedDate { get; set; }
7: [Required(ErrorMessage = "The program you applied to is required.")]
8: public string AppliedTo { get; set; }


The model creates three properties, the control must have at least one other control that will be validated, but can display any additonal number of controls. 




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 radiobutton that will be used as the