Thursday, October 20, 2011

Emulate ASP.NET validation groups with jQuery validation


In my most recent post, I demonstrated a workaround to allow using the jQuery validation plugin with WebForms pages. The basic idea was to trigger validation only on submissions that occurred within a single logical form, instead of catching submissions anywhere on WebForms’ all-encompassing physical form.
This approach worked fine for a single logical form, but wasn’t robust enough when handling validation for multiple logical forms on a single page. Additionally, it did not properly handle the enter key, allowing users to (perhaps accidentally) slip past validation if they simply hit the enter key within a TextBox.
In this post, we will continue by refining the solution from last time. So, if you haven’t read the previous post, familiarize yourself with it first. Specifically, this post will cover how to implement an analogue of WebForms’ ValidationGroup, use that to independently validate multiple form regionshandle the enter key, and refactor the final solution to minimize duplicated code.

ValidationGroups

In WebForms, we have the concept of a ValidationGroup to mitigate the issues that come with wrapping the entire page in a single form element. Whether right or wrong in principle, this scheme does a pretty good job of keeping the ASP.NET Validation controls from getting their wires crossed on complex forms.
However, using ASP.NET’s ValidationGroups requires that you use the WebForms validation controls, which generates quite a bit of cruft in your markup and injects two additional script references on your page.
An example of some of the client-side code the ASP.NET Validators generates
If you’re like me, trying to trend away from client-heavy WebForms pages these side-effects are prohibitive.

Emulating Validation Groups

Though its implementation renders a bit messy on the client-side, the WebForms paradigm of a ValidationGroup is exactly what we need for segregating our physical form element into logical forms. In fact, I’m going to use the same nomenclature in this example (ValidationGroup and CausesValidation).
Using CSS classes as flags is a great way to emulate that concept in plain (X)HTML markup. Especially when using jQuery, CSS “flags” are a great way to tag elements with arbitrary attributes, that are easy to find with simple DOM selectors later. Taking the form shown in my previous post and tagging its fieldsets with a validationGroup class, we end up with this markup:
<fieldset class="validationGroup">
<legend>Returning customer? Login here</legend>
 
<!-- Username and Password labels and inputs here -->
</fieldset>
Not a very large change, but it allows us to keep these logical forms separate, both when performing validation and when initially setting up their validation triggers.

Creating a CausesValidation counterpart

ValidationGroups may control the organization of logical forms, but it’s the controls marked with the CausesValidation property that drive validation of those forms. In similar fashion, we need a way to indicate which elements should trigger our own emulation of WebForms’ grouped validation.
Sticking with the same naming scheme and CSS flagging technique, it makes sense to tag our Button controls with a .causesValidation class:
<fieldset class="validationGroup">
<legend>Returning customer? Login here</legend>
 
<!-- Username and Password labels and inputs here -->
 
<asp:Button runat="server" ID="Login" Text="Login"
CssClass="causesValidation" />
</fieldset>
Now we just need to wire up functionality to make those causesValidation flags actually do something.

Acting on the validationGroup flag

With the markup modified to allow selective targeting of the validation groups, the next step is implementing validation functionality that leverages that targeting. Using jQuery’s powerful CSS-based selectors, that isn’t difficult:
$(document).ready(function () {
$("#form1").validate({ onsubmit: false });
 
// Search for controls marked with the causesValidation flag
// that are contained anywhere within elements marked as
// validationGroups, and wire their click event up.
$('.validationGroup .causesValidation').click(function (evt) {
// Ascend from the button that triggered this click event
// until we find a container element flagged with
// .validationGroup and store a reference to that element.
var $group = $(this).parents('.validationGroup');
 
var isValid = true;
 
// Descending from that .validationGroup element, find any input
// elements within it, iterate over them, and run validation on
// each of them.
$group.find(':input').each(function (i, item) {
if (!$(item).valid())
isValid = false;
});
 
if (!isValid)
evt.preventDefault();
});
});
Note: For more explanation of any uncommented code above, be sure to see the previous post in this series. Those parts are explained in detail there.
This sets up a click event handler on any element flagged with the causesValidation class; the two Button controls in our case. When those raise click events, we start at the triggering element and use the parents() traversal method to search upward for the nearest parent flagged as a validationGroup.
In this example, that will find a reference to the fieldset which contains the Button control that triggers the click event (e.g. if the user clicks the Login Button, then $group will store a reference to the first fieldset element).
With that reference to the the logical form requiring validation, jQuery’s find() traversal method allows us to select a set of all the input elements within just that region of the page. Note that this will also include the Button control that triggered the event, but since the valid() method returns true for elements that don’t have validation rules configured, this doesn’t cause a problem.
From there, it’s straightforward to iterate over the appropriate input elements and validate each independently, using the valid() trick covered in the last post.

Handling the enter key

At this point, everything works pretty well, so long as the user clicks on the Button controls to submit the logical forms. Unfortunately, things fall apart if the user triggers form submission by pressing enter in one of the form fields.
One way to fix that would be to handle the form’s onsubmit event, determine if an element flagged with the causesValidation class triggered the submission, and then run through our validation first. That’s perfectly valid, but I avoid that because it tends to clash with other functionality that handles the eventthe jQuery form plugin for example.
The alternative that I prefer is to handle a validated field’s onkeydown event. That way, if the element does need to trigger validation, it can do so early, and get out of the way quickly otherwise.
Using jQuery’s cross-browser normalized event object, testing for the enter key is not difficult at all. When handling keyboard related events, one property of that object is keyCode. This property will contain the ASCII character code of the key which triggered the event. In the case of the enter key, that keyCode is 13.
That in mind, this is a first iteration of adding enter key handling to our existing validation code:
// Select any input[type=text] elements within a validation group
// and attach keydown handlers to all of them.
$('.validationGroup :text').keydown(function (evt) {
// Only execute validation if the key pressed was enter.
if (evt.keyCode == 13) {
// Validation code goes here.
}
});
Whether the form is submitted by clicking a button or hitting the enter key within one of our validation groups’ text fields, the appropriate inputs will be validated, error messages displayed if necessary, and submission will only continue if the form is valid.

Refactoring to eliminate duplication

After adding the keydown handler, everything works great, but it’s no good to have that validation code duplicated for both the click and keydown handlers. By passing around a reference to the jQuery event object, we can reuse the same validation code for both event types and make the code much more concise:
$(document).ready(function () {
// Initialize validation on the entire ASP.NET form.
$("#form1").validate({
// This prevents validation from running on every
// form submission by default.
onsubmit: false
});
 
// Search for controls marked with the causesValidation flag
// that are contained anywhere within elements marked as
// validationGroups, and wire their click event up.
$('.validationGroup .causesValidation').click(ValidateAndSubmit);
 
// Select any input[type=text] elements within a validation group
// and attach keydown handlers to all of them.
$('.validationGroup :text').keydown(function (evt) {
// Only execute validation if the key pressed was enter.
if (evt.keyCode == 13) {
ValidateAndSubmit(evt);
}
});
});
 
function ValidateAndSubmit(evt) {
// Ascend from the button that triggered this click event
// until we find a container element flagged with
// .validationGroup and store a reference to that element.
var $group = $(evt.currentTarget).parents('.validationGroup');
 
var isValid = true;
 
// Descending from that .validationGroup element, find any input
// elements within it, iterate over them, and run validation on
// each of them.
$group.find(':input').each(function (i, item) {
if (!$(item).valid())
isValid = false;
});
 
// If any fields failed validation, prevent the button's click
// event from triggering form submission.
if (!isValid)
evt.preventDefault();
}
First, the validation code is refactored into a separate function: Validate.
Since it needs the ability to conditionally call preventDefault in order to stop form submission, the function accepts the event handlers’ jQuery event object as a parameter.
In fact, because $(this) is a reasonable place to begin the parents() traversal for either event that may call the method, very little refactoring is necessary.
The one thing that may seem strange is that the Validate function is being passed as a click handler without any parameters. The reason that this works is because the Validate function is defined with the same signature that jQuery expects. Because of that alignment, Validate will automatically be provided with the same event object that we’ve been using in anonymous callback functions thus far.

Calculated readability

Note that I attached keydown handlers to all of the text inputs within a validation group, regardless of whether or not they are actually validated fields. Similarly, you may have noticed that the click handler finds every input element within its validation group, even if those inputs aren’t marked for validation. This may seem like an oversight, but it’s an intended readability compromise.
You could modify the selector to be more precise, selecting only fields flagged with validation classes (e.g. requiredemailnumber, etc). However, this gets messy when you consider the wide variety of classes that are valid for tagging elements with jQuery validation functionality.
Rather than be precisely specific, I rely on the fact that jQuery validation’s valid() method returns true for elements which are not configured for validation. So, even if we do end up checking the validation status of a few irrelevant input fields, it won’t adversely impact the outcome of the validation process.
There are performance penalties to performing validation on these unnecessary elements, but it is negligible for a reasonably sized form. The ancillary inputs would have to number in the thousands before the penalty were noticeable, at which no one will probably ever successfully complete it anyway!

Conclusion

There are even more enhancements to be had, but I think this brings the solution to a point that it’s useful. With multiple logical forms handled and the perennially pesky enter key tamed, the majority of use cases should be covered.
The most troublesome issue still remaining is that care should be taken to avoid nesting container elements with the validationGroup class on them. Otherwise, the Validate() function will search “too high” and possibly hinge validation on input fields that are not intended. It’s an edge case (fixable if necessary), but something to keep in mind.
Another edge case is that, unlike the ASP.NET Validators, these validation groups can’t overlap. For my own use, this has never been an issue. I’m curious if that’s a real-world problem for any of you.
Finally, an entirely different approach well worth considering is John Rummell’sxVal for Webforms. Using Data Annotations to specify validation rules is gaining a lot of popularity, so it’s worth investigating options like this one. At the minimum, it will help you be more familiar with how validation is handled in ASP.NET MVC.
Hope that helps. Be sure to take a look at the source download to see everything pulled together and one extra usability feature that I didn’t have time to cover.

Get the source

If you’d like to browse through a complete working example of what’s been covered in this post, take a look at the companion project at GitHub. Or, if you’d like to download the entire project and run it in Visual Studio to see it in action yourself, grab the ZIP archive.