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][3] 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][4]

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][5]

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

![int3][6]

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

![int4][7]

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][8]

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

public class InterviewViewModel
{
    [DisplayName("Primary use of vehicle")]
    [Required(ErrorMessage =  "You must select vehicle use.")]
    public int VehicleUse { get; set; }
 
    public IEnumerable<SelectListItem> VehicleUseList { get; set; }
 
    [DisplayName("Type of business")]
    [Required(ErrorMessage = "Type of business is required.")]
    public string BusinessType { get; set; }
 
    [DisplayName("Days driven to work (1-5)")]
    [Required(ErrorMessage = "Number of days driven to work is required.")]
    public int? DaysCommute { get; set; }
 
    [DisplayName("Miles driven to work")]
    [Required(ErrorMessage = "Miles driven to work is required.")]
    public int? CommuteMiles { get; set; }
}

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.

public class InputValidationModelBinder : DefaultModelBinder
{
    protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var modelState = controllerContext.Controller.ViewData.ModelState;
        var valueProvider = controllerContext.Controller.ValueProvider;
 
        var keysWithNoIncomingValue = modelState.Keys.Where(x => !valueProvider.ContainsPrefix(x));
        foreach (var key in keysWithNoIncomingValue)
            modelState[key].Errors.Clear();
    }
}

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

<h2>Interview</h2>
<% using (Html.BeginForm()) { %>
    <fieldset>
        <div>
            <%:Html.LabelFor(m => m.VehicleUse) %>
            <%:Html.DropDownListFor(m => m.VehicleUse, Model.VehicleUseList) %>
            <%:Html.ValidationMessageFor(m => m.VehicleUse)%>
        </div>
        <div id="businessTypeDiv">
            <%:Html.LabelFor(m => m.BusinessType) %>
            <%:Html.EditorFor(m => m.BusinessType) %>
            <%:Html.ValidationMessageFor(m => m.BusinessType) %>
        </div>
        <div id="commuteDiv">
            <div>
                <%:Html.LabelFor(m => m.DaysCommute) %>
                <%:Html.EditorFor(m => m.DaysCommute)%>
                <%:Html.ValidationMessageFor(m => m.DaysCommute)%>
            </div>
            <div>
                <%:Html.LabelFor(m => m.CommuteMiles) %>
                <%:Html.EditorFor(m => m.CommuteMiles)%>
                <%:Html.ValidationMessageFor(m => m.CommuteMiles)%>
            </div>
        </div>
    </fieldset>
    <input type="submit" value="Save" />
<% } %>

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:

<script type="text/javascript">
    $(function () {
        $.fn.enable = function () {
            return this.show().removeAttr("disabled");
        }
 
        $.fn.disable = function () {
            return this.hide().attr("disabled", "disabled");
        }
 
        var vehicleUse = $("#VehicleUse");
        var businessTypeSection = $("#businessTypeDiv,#businessTypeDiv input");
        var commuteSection = $("#commuteDiv,#commuteDiv input");
        setControls();
 
        vehicleUse.change(function () {
            setControls();
        });
 
        function setControls() {
            switch (vehicleUse.val()) {
                case "1": //commuteSection
                    commuteSection.enable();
                    businessTypeSection.disable();
                    break;
                case "2": //Pleasure
                case "":
                    commuteSection.disable();
                    businessTypeSection.disable();
                    break;
                case "3": //Business
                    businessTypeSection.enable();
                    commuteSection.disable();
                    break;
            }
        }
    });
</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:

<% 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#][9]. If you look at the solution for MVC you will see this:

![mvc-solution][10]

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][11]

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:

public string[] Validate(string eventName) {
    FieldContext[] fields = Fields;
    ArrayList errors = new ArrayList();
 
    for (int i = 0; i < fields.Length; i++) {
        FieldContext field = fields[i];

        // validate only enabled fields
        if (!field.Elements[0].Disabled)
        {
            string[] thisErrors = field.Validate(eventName);
            if (thisErrors != null)
            {
                ArrayList.AddRange(errors, thisErrors);
            }
        }
    }
 
    if (ReplaceValidationSummary) {
        ClearErrors();
        AddErrors((string[])errors);
    }
 
    return (string[])errors;
}

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][12].

[3]: http://www.codeguru.com/csharp/.net/net_asp/tutorials/article.php/c17481/ASPNET-Framework-MVC-2-Dynamic-Validation.htm /
[4]: http://cdn.stevemichelotti.com/images/o_Interview1.png
[5]: http://cdn.stevemichelotti.com/images/o_Interview-commute.png
[6]: http://cdn.stevemichelotti.com/images/o_Interview-business.png
[7]: http://cdn.stevemichelotti.com/images/o_Interview-pleasure.png
[8]: http://cdn.stevemichelotti.com/images/o_Interview-commute-validation.png
[9]: http://projects.nikhilk.net/ScriptSharp
[10]: http://cdn.stevemichelotti.com/images/o_mvc-solution.png
[11]: http://cdn.stevemichelotti.com/images/o_scripts-folder.png
[12]: https://code.msdn.microsoft.com/Release/ProjectReleases.aspx?ProjectName=michelotti&ReleaseId=4607

Tweet Post Share Update Email RSS