Asynchronous Validation with ASP.NET AJAX, CustomValidator and Web Services
A web form I am currently working on has a couple of simple requirements:
-
The user should be notified as soon as an incorrect value is entered.
-
The values entered need to be validated using a complicated set of rules in a back end system, available via a web service.
The first requirement is a perfect fit for a CustomValidator with ClientValidationFunction and the AJAX control toolkit ValidatorCalloutExtender, which is what we are using for simpler things like checking that a number field does not contain letters.
The second requirement doesn’t fit in so well - since a database connection is involved it is impossible to do client side, and a server side validation function won’t check the value until the form is submitted.
Initially I thought that I could simply call a web service from within the client side validation function. Unfortunately that isn’t as simple as it seems, because the ASP.NET AJAX ScriptManager can only call web services asynchronously, while validation methods are synchronous and must set args.IsValid before exiting.
My next idea was to make the web service call synchronous. I found a few useful posts on this, in particular SJAX Call, but most references I found were simply “don’t do it”. Since adding a custom XMLHttpExecutor is way more complexity than I was looking for, especially for something with potential delay issues for the user, so I decided to try a different approach.
Since making the web service synchronous is both an undesirable solution and too much work, I have gone with making the validation asynchronous with a combination of caching and a custom ValidationResult object.
First, the server side component:
[WebMethod]
public ValidationResult TestWebServiceValidation(string customValidatorId, string param1, string param2)
{
ValidationResult vr = new ValidationResult();
vr.CustomValidatorId = customValidatorId;
vr.Parameters = param1 + ";" + param2;
vr.IsValid = false;
vr.Result = "This is a test result";
return vr;
}
The Validation result entity is designed to be easily added to a cache, with the key being based on the validator id and the parameters (which will generally be the values of the controls being validated).
Result is an object that can be anything, as long as your client side code knows what to expect.
CustomValidatorId is passed in as a parameter and included in the returned object because otherwise it would be impossible to get the validator that triggered the call without writing a seperate callback method for every validator on the page.
On the client side, we start with the cache:
var ValidationResultCache =
new Array();
function GetValidationResultFromCache(customValidatorId, parameters)
{
for
(i =
0; i < ValidationResultCache.length; i++)
{
var vr = ValidationResultCache[i];
if
(vr.CustomValidatorId == customValidatorId && vr.Parameters == parameters)
{
return vr;
}
}
return
null;
}
This is about as simple as caching gets, so it has potential issues of course, but it at least performs the basic function - returning a ValidationResult without calling the web service if it has previously been called to validate the same values.
Next is the validation function. It checks the cache for a validation result and calls the web service only if an appropriate result in the cache. Note that this will break rather badly if the cache key calculated here is different from the one generated by the web service method.
When the web service needs to be called, IsValid is set to false. This is to ensure that the form cannot be submitted while the web service call is still being processed. An alternative option is to set IsValid to true, but make sure an equivalent server side validator is in place.
Note that there are some things in this function that are part of the larger validtion framework. In particular:
-
this function is called with args.IsValid = ValidateDuplicateInvoiceNumber…
-
SetError updates the message on both the custom validator and the callout extender
-
Message 8 is “{0} is a duplicate invoice number”
-
Message 12 is “{0} is being validated - please wait”
function ValidateDuplicateInvoiceNumber(sender, currentRules, value, fieldDescription)
{
if
(currentRules[8]
!=
null
&& value !=
""
&& value !=
null)
{
var result = GetValidationResultFromCache(sender.id, value+";param2")
if
(result ==
null)
{
ValidationTest2.UIService.TestWebServiceValidation(sender.id, value,
"param2", TestWebServiceSuccess,TestWebServiceFail);
SetError(sender,
12, fieldDescription);
return
false;
}
else
{
if
(!result.IsValid)
{
SetError(sender,
8, fieldDescription);
return
false;
}
}
}
return
true;
}
The callbacks passed as the last two parameters on the web service call are standard, and can be used for multiple validation web services:
function TestWebServiceSuccess(result, userContext, methodName)
{
//alert("success - "+result);
ValidationResultCache[ValidationResultCache.length]
= result;
var cv = document.getElementById(result.CustomValidatorId);
ValidatorValidate(cv,
null,
null)
}
function TestWebServiceFail(error, userContext, methodName)
{
alert("Validation web service error - "+error);
}
When the web service call finishes running, the ValidationResult is added to the cache. The validator that originally called the web service is retrieved using the id in the ValidationResult, and ValidatorValidate is called so that the original validation method (in this case ValidateDuplicateInvoiceNumber) will run again.
When run the second time, the result from the cache is used, and controls can be updated as happens with a standard client side validator.