Wednesday, July 8, 2009

ASP.NET Validation - Client-Side IsControlValid() Function

While working on a rather unique form for one of the websites I maintain at work (Rated Golf) I found myself needing to do some very case specific form validation on a collection of fields. All of the fields in question had a pair of ASP.NET validation controls attached to them and depending on what the client selected while filling out the form I needed to perform validation on a variable number of fields. Namely, the user could fill out either 9 or 18 of the fields in any order and provided that one of those conditions was met I would allow submission. I spent a good bit of time digging through the ASP.NET client-side validation API to get a feel for how to go about the task at hand and finally I decided on enumerating over the page's validator collection and making sure the correct number of fields checked out. I went to work on this approach only to soon find out there is no way to check if a given control is valid- only a given validator. Now I knew that each input control had exactly two validators and that would [most likely] never change so after some tooling around I slapped together a solution. The solution I came up with was not scalable when an input control had more than two validators, so after leaving work I came up with a slightly better, less specialized solution for this problem using some very basic JavaScript. I started by defining some simple ASP.NET markup to create a TextBox, a RequiredFieldValidator, and a CompareValidator.
<asp:TextBox ID="tbxValue" runat="server" ValidationGroup="vg1" Style="width: 400px;" />
<asp:RequiredFieldValidator ID="rfvValue" runat="server" ErrorMessage="Value is required."
ControlToValidate="tbxValue" Display="Dynamic" ValidationGroup="vg1" />
<asp:CompareValidator ID="cvValue" runat="server" ErrorMessage="Value must be a number."
ControlToValidate="tbxValue" Operator="DataTypeCheck" Type="Integer" Display="Dynamic"
ValidationGroup="vg2" />
Now that our page is setup we can jump into the JavaScript. The first place to start is figuring out how many validators exist for a given control. One of the convenient nuances of the Microsoft JavaScript framework is that input fields having validators are automatically decorated with a "Validators" array at run time, making it very simple to grab a list of validators for that control. In order to simplify working with the control and its validators after verifying its validity I chose to define an IsControlValidResult object to wrap the Control, its Validators, and an IsValid field. Of course this can easily be swapped out for a simple Boolean result if that better fits your implementation:
function IsControlValidResult() {
    this.IsValid = false;
    this.Control = null;
    this.Validators = new Array();
}
And finally:
function IsControlValid(controlId, validationGroup, displayErrors) {
    var result = new IsControlValidResult();
    var validatorCount = 0;
    var validCount = 0;
    if (controlId != null && typeof (controlId) == "string") {
        var control = document.getElementById(controlId);
        validatorCount = control.Validators.length;
        if (validatorCount > 0) {
            var i = validatorCount;
            while (i--) {
                if (validationGroup == null || IsValidationGroupMatch(control.Validators[i], validationGroup)) {
                    result.Validators.push(control.Validators[i]);
                    MyValidatatorValidate(control.Validators[i], validationGroup, null, displayErrors);
                    if (control.Validators[i].isvalid) {
                        validCount++
                        if (validCount >= validatorCount)
                            break;
                    }
                }
            }
        }
        result.Control = control;        
    }
    result.IsValid = validCount >= validatorCount;
    return result;
}
IsControlValid() function accepts a controlID string and an optional (read: nullable) validationGroup string in order to limit the validation to a given validationGroup for scenarios where more than one ValidationGroup is assigned to a control and its validators. The method is quite simple and works by iterating over the control's Validators array and for each validator it calls MyValidatorValidate(), which performs the actual validation. MyValidatorValidate() is identical to the framework function ValidatorValidate() aside from an additional Boolean variable controlling whether or not the error should be displayed graphically. I chose to do this because I wanted to check the validity of a validator without visually alerting the user that I had done so. Just be warned: even though MyValidatorValidate puts ValidatorUpdateDisplay() in a conditional statement, calling "val.evaluationfunction" will still set the state of the control as invalid and if you're using the AJAX Control Toolkit's "ValidatorCalloutExtender" it will render as soon as that function executes. I was not able to identify a workaround for this. Here's the code for MyValidatorValidate():
function MyValidatatorValidate(val, validationGroup, event, displayError) {
    val.isvalid = true;
    if ((typeof (val.enabled) == "undefined" || val.enabled != false) && IsValidationGroupMatch(val, validationGroup)) {
        if (typeof (val.evaluationfunction) == "function") {
            val.isvalid = val.evaluationfunction(val);
            if (displayError && (!val.isvalid && Page_InvalidControlToBeFocused == null && typeof (val.focusOnError) == "string" && val.focusOnError == "t")) {
                ValidatorSetFocus(val, event);
            }
        }
    }
    if (displayError)
        ValidatorUpdateDisplay(val);
}
If, after calling MyValidatorValidate, you have any ValidatorCalloutExtenders being rendered on the page that you would like hidden from the user, the following function will turn off all visual validation cues:
function Unvalidate(myValidationGroup) {
    // Remove the validator control(s) from display.
    var myValidators = Page_Validators;
    if ((typeof (myValidators) != "undefined") && (myValidators != null)) {
        for (i = 0; i < myValidators.length; i++) {
            var myValidator = myValidators[i];
            if (myValidationGroup == null || IsValidationGroupMatch(myValidator, myValidationGroup)) {
                if (myValidator.style.visibility.length > 0 && myValidator.style.display.length == 0) {
                    myValidator.style.visibility = 'hidden';
                }
                else if (myValidator.style.display.length > 0 && myValidator.style.visibility.length == 0) {
                    myValidator.style.display = 'none';
                }
                if ((typeof (myValidator.ValidatorCalloutBehavior) != "undefined") && (myValidator.ValidatorCalloutBehavior != null)) {
                    myValidator.ValidatorCalloutBehavior.hide();
                    if ((typeof (myValidator.ValidatorCalloutBehavior._highlightCssClass) != "undefined") && (myValidator.ValidatorCalloutBehavior._highlightCssClass != null) && (typeof (myValidator.ValidatorCalloutBehavior._elementToValidate) != "undefined") && (myValidator.ValidatorCalloutBehavior._elementToValidate != null))
                        Sys.UI.DomElement.removeCssClass(myValidator.ValidatorCalloutBehavior._elementToValidate, myValidator.ValidatorCalloutBehavior._highlightCssClass);
                }
            }
        }
    }

    // Remove the validator summary(ies) from display.
    if ((typeof (Page_ValidationSummaries) != "undefined") && (Page_ValidationSummaries != null)) {
        for (i = 0; i < Page_ValidationSummaries.length; i++) {
            var mySummary = Page_ValidationSummaries[i];
            if (myValidationGroup == null || IsValidationGroupMatch(mySummary, myValidationGroup)) {
                mySummary.style.display = 'none';
            }
        }
    }
}
And that's all there is to it. If you found this useful don't hesitate to let me know in the Comments!

1 comment:

w3c said...

Nice information, I really appreciate the way you presented.Thanks for sharing..

http://www.w3cvalidation.net/