Validators in the ASP.NET 2.0 Framework
Validators are an incredibly useful part of the ASP.NET 2.0 framework. Validation is traditionally done in a number of ways: through server side code following a postback, or via client-side Javascript without a postback. What's difficult when building your own validation is keeping the two parts in sync, and in creating common code for commonly used validation types. The ASP.NET validation framework presents several advantages: it provides a declarative way to implement validation, it separates validation from the rest of your page's logic, it provides a way to bundle client- and server-side validation code into a custom control, it provides an easy way to style and customize the presentation of your error messages. The separation of validation from the main page logic is quite useful: validation happens as a separate step in the page lifecycle and can in many cases be implemented in an entirely declarative way. The ASP.NET framework provides a number of stock validators, which cover many standard scenarios, but also provides a couple of ways to extend the validation framework: the CustomValidator class and by implementing a custom validator. This article deals with extending the validation framework using both methods.
The first method, declaring a CustomValidator, is a matter of simply declaring your validator the way you would ordinarily do it. Let's say you wanted to verify that a control contained a valid http: or https: URI. You would declare this in two parts, first in your page:
<asp:CustomValidator runat="server" ID="urlValidator" ErrorMessage="You must provide a valid http: or https: URL" />
And then, in your OnInit method, hook up a handler to the ServerValidate event:
protected override void OnInit(EventArgs e)
{
this.urlValidator.ServerValidate += new ServerValidateEventHandler(URLValidator_ServerValidate);
}
private void URLValidator_ServerValidate(object source, ServerValidateEventArgs args)
{
string potentialURI = args.Value;
Uri uri1;
if (Uri.TryCreate(potentialURI, UriKind.Absolute, out uri1))
{
result = (uri1.Scheme == Uri.UriSchemeHttp) || (uri1.Scheme == Uri.UriSchemeHttps);
}
else
{
args.IsValid = false;
}
}
Similarly, the ClientValidationFunction property can be used to give the name of a javascript method that will perform client-side validation. Naturally, you'll also have to register that script function, either declared directly in the page or through the RegisterClientScript function. As a rule, if you're not going to provide client side validation, you should disable client side validation for the CustomValidator by setting the EnableClientScript property to false, if you do not do this, you'll see odd behavior on the form - for example, if you have a form returned with invalid input on a control subject to custom validation, you correct the input and then click on the submit button, the custom validator's error message will be cleared from the form, but the form won't actually submit.
The second method, creating a custom validator, involves subclassing the BaseValidator class. You use your validator the way you'd use any other custom control: register your own namespace prefix and declare the control in your page. The implementation requirements are pretty minimal: the AddAttributesToRender, which is where you write out the code to register your client-side javascript, EvaluateIsValid, which is where you implement the server-side validation code, and OnPreRender, which is where you write out your client-side validation code.
The AddAttributesToRender method minimally registers an attribute evaluationfunction using the ClientScript.RegisterExpandoAttribute function. The value for this attribute is the name of the Javascript function that you'd like to call to do client side validation. You shouldn't bother registering this function if this.RenderUplevel is false, and if you've decided not to implement client side validation, you certainly shouldn't bother. I'll get to the actual Javascript validation code in a bit.
The EvaluateIsValid method simply implements the validation. You can use base.GetControlValidationValue to get the value of the control to be validated. GetControlValidationValue is an interesting method. This method looks up a control given its ID, and then searches the control for an attribute flagged with the ValidationPropertyAttribute attribute. In this way, controls pick the value that is used for validation through metadata.
In your client side Javascript, you will declare a function to provide validation to go along with your validator declaration. In general, your function will be declared to take a single argument which will be the DOM node for the validator control that's currently validating. The DOM node has a few additional "expando" attributes added onto it, along with whatever attributes you register in AddAttributesToRender. These attributes are how you access the control's properties at runtime. The controltovalidate property is the most important, as this will hold the ID of the control you're validating at runtime. Remember that due to name mangling, you won't know the control's ID at runtime, even though you declare it in your page. ASP.NET provides a few utility functions that are automatically available to you. The most important of these is ValidatorGetValue(id), which takes the ID of a control to validate. ValidatorTrim(value) is another useful method for trimming whitespace off of input. ValidatorConvert(op, dataType, val) is used to convert the value (given in op) to a given datatype (one of "Integer", "Currency", "Double", or "Date"). The val structure passed in the last argument is the same structure passed to your custom validation function.
Let's take a moment to look at a custom validator. I'll show a validator that restricts the length of a text input to a given length, and also makes the input required. Most validators don't make their input required; when they do, you apply a 2nd validator of the type RequiredFieldValidator. It's fine to create a validator that also makes the input required, but you might think that you could inherit the functionality from RequiredFieldValidator. This is not the case; when creating a custom validator, you can't inherit from anything other than BaseValidator. Let's say you want to implement a validator on a field that is both required and restricts the length of an input. You might think that you could subclass RequiredFieldValidator and let that control handle the required input restriction, but the problem you'll encounter is that you'll call base.AddAttributesToRender, which will register an evaluation function, and then you'll do the same using something like this:
Page.ClientScript.RegisterExpandoAttribute(clientID, "evaluationfunction", "RestrictedLengthValidatorEvaluateIsValid");
This will result in a runtime ArgumentException "An entry with the same key already exists.", because the base class already used "evaluationfunction" as a key - this is a well-known attribute on validator javascript objects used by the validation framework, because the RequiredFieldValidator has already registered the evaluationfunction attribute. You could probably work around this, but you're better off just implementing your own validation and not relying on a base class.
The code for the custom validator is fairly concise: you need to declare any custom attributes (in this case, MaxLength), then write them out, along with the evaulationfunction in AddAttributesToRender. You then write your server-side validation in EvaluateIsValid and finally, write out the client-side script block in OnPreRender.
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.ComponentModel;
using System.Web.UI.WebControls;
namespace EightyTwenty.Controls
{
[AspNetHostingPermission(System.Security.Permissions.SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
[AspNetHostingPermission(System.Security.Permissions.SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
[System.Web.UI.ToolboxData(@"<{0}:RestrictedLengthValidator runat='server' />")]
public class RestrictedLengthValidator : BaseValidator
{
private int maxLength;
[Browsable(true)]
[Category("Behavior")]
[System.Web.UI.Themeable(false)]
[DefaultValue("")]
[Description("Maximum number of characters to allow ")]
public int MaxLength
{
get { return maxLength; }
set
{
if (value < 1) throw new ArgumentOutOfRangeException("maxLength cannot be negative");
maxLength = value;
}
}
/// <summary>
/// Write out client-side attributes for validator.
/// </summary>
/// <param name="writer"></param>
protected override void AddAttributesToRender(System.Web.UI.HtmlTextWriter writer)
{
base.AddAttributesToRender(writer);
if (this.RenderUplevel)
{
string clientID = this.ClientID;
Page.ClientScript.RegisterExpandoAttribute(clientID, "evaluationfunction", "RestrictedLengthValidatorEvaluateIsValid");
Page.ClientScript.RegisterExpandoAttribute(clientID, "maxlength", MaxLength.ToString());
}
}
/// <summary>
/// Server side evaluation function for RestrictedLength Validator
/// </summary>
/// <returns>True if the input is valid.</returns>
protected override bool EvaluateIsValid()
{
string validationValue = GetControlValidationValue(ControlToValidate);
return (validationValue.Length <= MaxLength);
}
/// <summary>
/// Write out client side script block
/// </summary>
/// <param name="e"></param>
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
if (base.RenderUplevel)
{
if (!Page.ClientScript.IsClientScriptBlockRegistered("RestrictedLengthValidator"))
{
this.Page.ClientScript.RegisterClientScriptBlock(typeof(RestrictedLengthValidator), "RestrictedLengthValidator", @"<script type=""text/javascript"">
<!--
function RestrictedLengthValidatorEvaluateIsValid(val) {
var value = ValidatorTrim(ValidatorGetValue(val.controltovalidate));
var result = (value.length <= val.maxlength);
if (result == false) {
var errorMsg = ""This control can have a maximum of "" + val.maxlength + "" characters, you have entered "" + ValidatorTrim(value).length + "". Please remove the extra characters."";
if (val.innerText) {
val.innerText = errorMsg;
} else {
val.textContent = errorMsg;
}
}
return result;
}
//-->
</script>");
}
}
}
}
}
One housekeeping issue: in OnPreRender, I'm writing the script inline in the page. You may wish to keep all your javascript in a separate file, which would have the effect of linking your script using the <script> tag's src attribute. To do this, you create a separate file for your client script, make it part of your project, store that on the assembly manifest (by changing the BuildAction on the file's properties to "EmbeddedResource"), and then use a statement like Page.ClientScript.RegisterClientScriptResource(typeof(RestrictedLengthValidator), "EightyTwenty.Controls.WebUIValidationExtended.js") in place of RegisterClientScriptBlock. If you do this, you'll also need to apply the assembly-level attribute Page.ClientScript.RegisterClientScriptResource(typeof(RestrictedLengthValidator), "EightyTwenty.Controls.WebUIValidationExtended.js") to get the framework to expose your script through WebResource.axd.
There are some restrictions on the types of controls that can be validated. Checkboxes are one specific example, and while it seems like validation on a checkbox is trivial, checkboxes are often used for critical inputs like accepting a site's terms of service, and it's useful to be able to treat a checkbox like any other control in the validation structure. One way to do this is to use the CustomValidator. You could also write your own validator control if this was a component you would be re-using.
One other consideration: there are times that you may want to consider several controls when performing validation. The framework's CompareValidator is an example of this. Other possibilities might be on an address form when you want to make sure that a user has selected a State or Province when the user's country is the USA or Canada, or where you want a user to fill out at least one of a set of controls (say, you want a user to provide either a telephone number or email address for contact info). The BaseValidator isn't really set up to consider multiple controls when performing validation, validators are really designed to work on a single control. That doesn't mean you can't do this, however. The MultipleFieldsValidator at CodeProject is a good example of this.