Posts
203
Comments
1116
Trackbacks
51
July 2010 Entries
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 | Comments (7)

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