Tuesday, April 19, 2011

Client-Side Validation with AJAX/Dynamic Form

One of the thing in my to-do list for MVC is "figure out how to do client-side validation". Isn't that supplied out of the box since like ASP.NET MVC 2? Yes, it is - but it only work out of the box if the form is not dynamically fetched.

In my projects, most of the form is a dynamic form - so user click an icon and then the form (empty or pre-filled) is fetched via AJAX and then displayed (some times in a jQuery dialog). This is a quite common scenario, for example: you are looking at your account profile screen and displayed there is a list of your phone numbers that you have registered. Let's say you then want to edit one of those since you changed your phone number. So you click the edit button/icon next to your phone number on the screen and a dialog box pops with the old phone number, you edit it, click "Save" and the list refreshes with your phone number.

To simply enforcing a "required" field validation in this scenario was quite complicated. Especially when your AJAX submit handler is a custom handler (instead of using the stock ASP.NET MVC AJAX Helper). But now, by adding less than 10 lines of code, add 2 lines of configuration settings in the web.config and including less than 3 javascripts - it is all possible!


So let's say here is our original code - we have a form with 2 fields.

The model:

   public class Profile{
      [Required(ErrorMessage = "Name is required")]
      public string Name{ get; set; }

      [Required(ErrorMessage = "Email is required")]
      public string Email{ get; set; }
   }
Our view:
<div class="message"><%:Html.ValidationSummary(true)%></div>
<% using (Html.BeginForm("/MyController/MyAction", FormMethod.Post, new { id = "MyForm", name = "MyForm" })) { %>
<table class="StripeTable">
    <tr>
        <td><%:Html.LabelFor(m => m.Name)%></td>
        <td><%:Html.EditorFor(m => m.Name)%> <%:Html.ValidationMessageFor(m => m.Name)%></td>
    </tr>
    <tr>
        <td><%:Html.SmartLabelFor(m => m.Email)%></td>
        <td><%:Html.EditorFor(m => m.Email)%> <%:Html.ValidationMessageFor(m => m.Name)%></td>
    </tr>
    <tr>
        <td colspan="2"><input type="submit" value="Save" />  <input type="button" value="Cancel" onclick="javascript:closeDialog();" /> </td>
    </tr>
</table>
Our jQuery submit handler:
$(document).ready(function () {
        $("#MyForm").submit(function () {
            var f = $("#MyForm");
            var action = f.attr("action");
            var serializedForm = f.serialize();
            $.ajax({
                type: 'POST',
                url: action,
                data: serializedForm,
                error: function (request, textStatus, error) {
                    // do error handling
                },
                success: function (data, textStatus, request) {
                    // do whatever to continue
                }
            });
            return false;
        });
    });
The controller action:
[HttpPost]
        public ActionResult MyAction(Profile profile) {
            if (ModelState.IsValid) {
                // save the record
                return null;
            } else {
                return View(profile);
            }
        }
The client-validation does not work by default because the parsing is only done during the initial page load. So our form, which is dynamically loaded via AJAX is not parsed - therefore the client validators don't know about the new fields that need to be validated. Eventually this then result in the form being posted to the controller - and even though my business object is handling the validation as well, it would be nice if the client side is also preventing that - thus saving the round trip.

So to enable the unobtrusive client-side validation, we need to enable it in our web.config (by default it is disabled to preserve compatibility with MVC2 and MVC1).
<configuration>
    <appSettings>
        <add key="ClientValidationEnabled" value="true"/>
        <add key="UnobtrusiveJavaScriptEnabled" value="true"/>

    </appSettings>
</configuration>
You can also do this through code.
HtmlHelper.ClientValidationEnabled = true;
HtmlHelper.UnobtrusiveJavaScriptEnabled = true;
Now, the only other change is in our jQuery submit handler code. In it we need to force the parsing again - but since we don't want to parse the whole document again, so we want to limit it to the just the dynamic form. The new unobtrusive validation has a method we can call to do this: $.validator.unobtrusive.parse();. We also need to handle the validity of the form on the client side - so after making those modifications, our code looks like this:
$(document).ready(function () {
        $.validator.unobtrusive.parse($("#MyForm"));
        $("#MyForm").submit(function () {
            var f = $("#MyForm");
            var action = f.attr("action");
            var serializedForm = f.serialize();
            if (f.valid()) {
                $.ajax({
                    type: 'POST',
                    url: action,
                    data: serializedForm,
                    error: function (request, textStatus, error) {
                        // do error handling
                    },
                    success: function (data, textStatus, request) {
                        // do whatever to continue
                    }
                });
                return false;
            } else return false;
        });
    });
That's it!

Brad Wilson wrote an excellent blog post about this feature (with a hint of the dynamic form).

No comments: