Posts
203
Comments
1119
Trackbacks
51
Validating a Dynamic UI with MVC 2

When MVC 2 was released, there was a last minute change to use Model Validation instead of Input Validation. Essentially, Model validation means that your entire view model will be validated regardless of which values actually got posted to the server. On the other hand, with Input validation, only the values that get posted to the server will get validated. While this was the right decision by the MVC team for the most mainstream cases, there are still some cases where the previous behavior of Input validation would be more convenient. A workaround to enable Input validation-like behavior is presented in this post by Steve Sanderson. Keep in mind that this is just validation on view models and not on domain models. For domain models you still want model validation so that there is no security risk by a user bypassing your validations by tampering with what gets posted back to the server – but validation for view models is facilitating a good end-user experience.

My team is currently developing a UI that has many dynamic controls depending on the user’s previous answers and could benefit from Input validation. We found that, while Sanderson’s solution worked great for the server-side, we were still left with no client-side validation. My team’s initial solution is presented here by Sajad Deyargaroo. In my post here I will walkthrough an end to end scenario. First, let’s consider this scenario – a user is filling out an interview to purchase insurance for their car and one of the questions they get asked is how they will be using the vehicle:

 int1

Based on the answer to that question, they would get presented with other controls that are relevant to the answer they just gave. For example, if the user says they are using it to “Commute”, then we must dynamically show a couple of other textboxes to collect information about their commute:

 int2

If they select “Business”, then we must collect the type of business:

 int3

and if they select “Pleasure”, then no other contextual information is needed:

 int4

In this case, we just want to use simple client-side jQuery to show/hide controls when the user selects a value from the dropdown without an additional round trip to the server. Additionally, we obviously want to have validation (with the normal Data Annotations attributes) but *only* if the fields are actually displayed. For example, if the user selects “Commute” then the fields related to the commute must be validated since they are visible – but we should *not* validate the other textboxes (e.g,. type of business) because they are not required if they are not visible.

int5

The view model is still leveraging the normal Data Annotation attributes:

   1:  public class InterviewViewModel
   2:  {
   3:      [DisplayName("Primary use of vehicle")]
   4:      [Required(ErrorMessage =  "You must select vehicle use.")]
   5:      public int VehicleUse { get; set; }
   6:   
   7:      public IEnumerable<SelectListItem> VehicleUseList { get; set; }
   8:   
   9:      [DisplayName("Type of business")]
  10:      [Required(ErrorMessage = "Type of business is required.")]
  11:      public string BusinessType { get; set; }
  12:   
  13:      [DisplayName("Days driven to work (1-5)")]
  14:      [Required(ErrorMessage = "Number of days driven to work is required.")]
  15:      public int? DaysCommute { get; set; }
  16:   
  17:      [DisplayName("Miles driven to work")]
  18:      [Required(ErrorMessage = "Miles driven to work is required.")]
  19:      public int? CommuteMiles { get; set; }
  20:  }

By creating a custom model binder that performs input validation (based on Sanderson’s post where he uses an Action filter), we had our solution to the problem for server-side validation.

   1:  public class InputValidationModelBinder : DefaultModelBinder
   2:  {
   3:      protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
   4:      {
   5:          var modelState = controllerContext.Controller.ViewData.ModelState;
   6:          var valueProvider = controllerContext.Controller.ValueProvider;
   7:   
   8:          var keysWithNoIncomingValue = modelState.Keys.Where(x => !valueProvider.ContainsPrefix(x));
   9:          foreach (var key in keysWithNoIncomingValue)
  10:              modelState[key].Errors.Clear();
  11:      }
  12:  }

Let’s look at the mark up for the page:

   1:  <h2>Interview</h2>
   2:  <% using (Html.BeginForm()) { %>
   3:      <fieldset>
   4:          <div>
   5:              <%:Html.LabelFor(m => m.VehicleUse) %>
   6:              <%:Html.DropDownListFor(m => m.VehicleUse, Model.VehicleUseList) %>
   7:              <%:Html.ValidationMessageFor(m => m.VehicleUse)%>
   8:          </div>
   9:          <div id="businessTypeDiv">
  10:              <%:Html.LabelFor(m => m.BusinessType) %>
  11:              <%:Html.EditorFor(m => m.BusinessType) %>
  12:              <%:Html.ValidationMessageFor(m => m.BusinessType) %>
  13:          </div>
  14:          <div id="commuteDiv">
  15:              <div>
  16:                  <%:Html.LabelFor(m => m.DaysCommute) %>
  17:                  <%:Html.EditorFor(m => m.DaysCommute)%>
  18:                  <%:Html.ValidationMessageFor(m => m.DaysCommute)%>
  19:              </div>
  20:              <div>
  21:                  <%:Html.LabelFor(m => m.CommuteMiles) %>
  22:                  <%:Html.EditorFor(m => m.CommuteMiles)%>
  23:                  <%:Html.ValidationMessageFor(m => m.CommuteMiles)%>
  24:              </div>
  25:          </div>
  26:      </fieldset>
  27:      <input type="submit" value="Save" />
  28:  <% } %>

This displays all of the required HTML that we need.  We also have a section of jQuery will handles the showing/hiding of elements based on the section of the VehicleUse dropdown:

   1:  <script type="text/javascript">
   2:      $(function () {
   3:          $.fn.enable = function () {
   4:              return this.show().removeAttr("disabled");
   5:          }
   6:   
   7:          $.fn.disable = function () {
   8:              return this.hide().attr("disabled", "disabled");
   9:          }
  10:   
  11:          var vehicleUse = $("#VehicleUse");
  12:          var businessTypeSection = $("#businessTypeDiv,#businessTypeDiv input");
  13:          var commuteSection = $("#commuteDiv,#commuteDiv input");
  14:          setControls();
  15:   
  16:          vehicleUse.change(function () {
  17:              setControls();
  18:          });
  19:   
  20:          function setControls() {
  21:              switch (vehicleUse.val()) {
  22:                  case "1": //commuteSection
  23:                      commuteSection.enable();
  24:                      businessTypeSection.disable();
  25:                      break;
  26:                  case "2": //Pleasure
  27:                  case "":
  28:                      commuteSection.disable();
  29:                      businessTypeSection.disable();
  30:                      break;
  31:                  case "3": //Business
  32:                      businessTypeSection.enable();
  33:                      commuteSection.disable();
  34:                      break;
  35:              }
  36:          }
  37:      });
  38:  </script>

Notice that in addition to showing/hiding the controls, we also enable/disable the controls by setting the “disabled” attribute. Setting the disabled attribute will prevent the element from being posted to the server on the form submission. When the user selects the “Commute” option i the dropdown, for example, we will fall into case “1” on line 22 – this will enable/show all the elements inside the <div id=”commuteDiv”> and it will disable/hide all the elements inside the <div id=”businessTypeDiv”>.

This all works great when *only* server-side validation is enabled with our custom model binder that does input validation. However, when we add:

   1:  <% Html.EnableClientValidation(); %>

this prevents the form from being submitted! The OOTB Microsoft JavaScript library is performing validation on *all* controls regardless of whether the controls are enabled or not.

Microsoft’s JavaScript files are actually produced from C# by using Script#. If you look at the solution for MVC you will see this:

mvc-solution

The 2 projects highlighted above produce these JavaScript files which are ultimately embedded in your project when you do “File – New – MVC Web Application” inside Visual Studio:

scriptsfolder

Notice there is a *.debug.js version produced for each one. The debug version is human readable and the non-debug version is minified (e.g., whitespace is removed, variable names are shortened, etc.). Inside the MicrosoftMvcValidationScript project there is a class called FormContext which has a Validate() method. We can modify this method by adding a single IF statement (on line #9 below) to only perform validation IF the field is not disabled:

   1:  public string[] Validate(string eventName) {
   2:      FieldContext[] fields = Fields;
   3:      ArrayList errors = new ArrayList();
   4:   
   5:      for (int i = 0; i < fields.Length; i++) {
   6:          FieldContext field = fields[i];
   7:          
   8:          // validate only enabled fields
   9:          if (!field.Elements[0].Disabled)
  10:          {
  11:              string[] thisErrors = field.Validate(eventName);
  12:              if (thisErrors != null)
  13:              {
  14:                  ArrayList.AddRange(errors, thisErrors);
  15:              }
  16:          }
  17:      }
  18:   
  19:      if (ReplaceValidationSummary) {
  20:          ClearErrors();
  21:          AddErrors((string[])errors);
  22:      }
  23:   
  24:      return (string[])errors;
  25:  }

Once we build the project, it will produce new versions of MicrosoftMvcValidation.js and MicrosoftMvcValidation.debug.js which we can then copy into our solution to replace the original versions.  Now our scenario works end-to-end and now *includes* client-side validation behavior in the way we expect. Our form is no longer prevented from being posted to the server due to the hidden/disabled fields not having a value.

The complete solution for this can be downloaded here.

posted on Wednesday, July 7, 2010 10:42 AM Print
Comments
Gravatar
# re: Validating a Dynamic UI with MVC 2
Gregor Suttie
7/7/2010 11:05 AM
Very interesting article and very helpful too - thanks for taking the time to share this, I'm sure many people will find this of use.

Gregor
Gravatar
# re: Validating a Dynamic UI with MVC 2
Raghuraman
7/7/2010 5:35 PM
Steve,

Thanks for another Informative and very helpful write-up.

KRK
Gravatar
# re: Validating a Dynamic UI with MVC 2
Justin Kong
7/16/2010 10:11 AM
Thank you for posting this! More posts like this are needed b/c they are REAL WORLD examples. Kills me when so many sample projects / posts only detail the most basic CRUD type of situations.
Gravatar
# re: Validating a Dynamic UI with MVC 2
vamsikumar
1/14/2011 7:24 AM
Thanks For Your useful information..

i used the same in the same dropdown scenario,but for me it is validating the errors in the invisible section too.

i think the problem is the controls are not disabling properly.

can any one suggest me what the problem might be with my coding..

Thanks in Advance,
Vamsi .K
Gravatar
# re: Validating a Dynamic UI with MVC 2
Steve
1/27/2011 8:36 PM
@Vamsi - The above code specifically does not validate fields that are disabled. Check the disabled property to ensure that first.
Gravatar
# re: Validating a Dynamic UI with MVC 2
James
2/7/2011 11:31 PM
I tried to use the code but found that the framework will not validate any fields. After doing some research, it appears that adding base.OnModelUpdated(...) to the beginning of InputValidationModelBinder.OnModelUpdated() will fix the issue. It now validates and only validates the set values.
Gravatar
# re: Validating a Dynamic UI with MVC 2
Resad
9/7/2011 3:29 PM
Thank you very much, you helped me resolve the issue. I cannot believe that it is not still fixed in MVC3 projects.

Post Comment

Title *
Name *
Email
Comment *  
Verification

View Steve Michelotti's profile on LinkedIn

profile for Steve Michelotti at Stack Overflow, Q&A for professional and enthusiast programmers




Google My Blog

Tag Cloud