Wednesday, January 9, 2008

ASP.NET Validators Unclouded

Introduction
It makes no difference what kind of development you are working on, user input validation is always an important and often troublesome issue.

What data needs to be validated? What are the valid conditions? Do we need to report every error in one pass or allow the user to correct one error at a time? Do we validate on the client or on the server? The questions go on.

Worse than that, the code can quickly get unmanageable, especially where we try to validate everything in one pass. Nested ifs, deconstructing strings, building up lists of error messages, endless comments trying to make some sense of it.

It is my personal opinion that everything that can be updated by anyone else but me should be validated at every opportunity and that we should always validate everything in one pass. But that does not mean that I have never tried to dodge the bullet on a tight deadline.

In some respects, it can be worse for the web page developer. It is important to validate as much as possible at the client to save on bandwidth, but HTTP requests are so easy to fake; the same data needs to be validated again on the server, if you want any kind of security.

Another problem can arise if the customer wants you to mark the invalid fields and then list the errors in one place (a common user-friendly convention in web-page design).

It is hard to imagine someone who enjoys writing validation code, and that goes double for classic ASP developers. Thankfully, the .NET framework has gone a long way to alleviate the problem for the ASP.NET developer with the introduction of six all-encompassing and developer-friendly validation controls:

RequiredFieldValidator Ensure that a mandatory field is populated.
CompareValidator Compare one control with another or with a literal.
RangeValidator Check that data is within a given range.
RegularExpressionValidator Use where data must match a given Regex format.
CustomValidator Use a custom validation function to validate a control.
ValidationSummary Collate the results of an entire form's validation.

This article assumes a basic knowledge of adding controls to a web form and setting the properties and event handlers. It attempts to give a general overview of these controls and how to use them to perform simple practical tasks.

All examples use C# with GridLayout pages. But anything in this article should easily translate to VB.NET and / or FlowLayout pages.

BaseValidator controls
All of the above, with the single exception of ValidationSummary, are derived from the BaseValidator class and share some functionality.

The first four validation controls are tailored for specific purposes and designed to validate an identical condition on both the client and the server. Each generates some purpose-specific JavaScript (using WebUIValidation.js, in the aspnet_client web) to validate the input control that you point to against the condition that the given validation control is designed to handle.

The CustomValidator allows you to define your own validation routines, using an event handler on the client, server or both.

The actual client-side validation takes place in events triggered by a change of value in the control you are validating and when you try to submit the form. The server-side validation takes place when you post back the form and the validation results can be accessed using the Form class's .IsValid property.

Each control's properties look reasonably similar. Apart from the standard font and formatting properties, each of the standard controls has the following common properties, derived from BaseValidator:

.ControlToValidate Name of the control we are validating.
.ErrorMessage The error message related to an invalid condition.
.Text Text to display in this control if invalid.
.EnableClientScript Validate client-side and server-side.
.IsValid The validation result of this control

ControlToValidate
A string containing the name of the Control object that the validation control will examine. This control must be developed using the ValidationPropertyAttribute.

Of the standard controls, the only ones marked with a ValidationProperty are:

TextBox
ListBox
DropDownList
RadioButtonList
HtmlInputText
HtmlInputFile
HtmlSelect
HtmlTextArea
It is worth bearing this in mind if you are ever developing a control that may require validation. You should always prefix the class definition with [ValidationProperty("MyProperty")] (where MyProperty is the name of the property that can be changed by the user input, usually text).

ErrorMessage
This defines the message related to the invalid condition. This can be displayed in the control itself or passed to a ValidationSummary control.

Text
The text to be displayed in the control when the condition is not valid. If left blank, the control will display the ErrorMessage text.

If the difference between .ErrorMessage and .Text is confusing, read on. It should make sense by the time we examine the ValidationSummary control. Until then, we will only use the .ErrorMessage property (.Text should be left blank)

EnableClientScript
By default this is set to true and the .ControlToValidate will be validated twice, at the client and at the server.

If the .EnableClientScript property is set to false, then validation will be limited to the server.

IsValid
The only property not available to the WebForm designer, .IsValid contains the validation result for this control after the form's validation has taken place.

This can be useful if you want to react differently when only part of the page is invalid.

Examples


The following sections will refer to a very simple example. As shown in the screenshot, this example will consist of a single TextBox being validated. You know the kind of thing: Leave your E-mail address here and we'll send you something really nice for free. (and then sell your email address to five hundred Email spammers)

Create a web form, containing only a Label ("Email Address:"), an empty TextBox, a submit Button and whichever -Validator control we are looking at. (see sample image)

The ControlToValidate property should always point to the TextBox and the ErrorMessage should contain something relevant to the validation we are working with.

Include some code in a Button_Click handler to redirect to another page when the button is clicked, but only if the page is valid. For example:

private void myButton_Click(object sender, System.EventArgs e)
{
if (this.IsValid)
Response.Redirect("anvokay.aspx", true);
}

Or... if you are feeling lazy, you can download the sample code at the top of the page and unzip it into a new or existing web.

NOTE: There is a strange behavior quirk in the interaction between web browsers and the .NET framework which means that if you hit Enter in a Single-TextBox form it will post back to the server but the Button_Click handler is not executed.

This is apparently a legacy browser issue where, for a single textbox form, the button name is not sent as part of the response, leaving .NET with no idea what triggered the postback.

Possible workarounds are:

Always physically click the Submit button rather than pressing Enter.
Include a second TextBox that does nothing, just to avoid this "feature".
Download and use MetaBuilders' Default Buttons Control to safely link the Submit Button to the TextBox's enter key.
(Thanks to Andy Smith for helping me understand this)

The RequiredFieldValidator
As the name suggests, the RequiredFieldValidator is normally used to validate that a mandatory field is populated. It can also be customized so that blank is a valid response and some other text means that nothing has been entered.

There is only one property unique to the RequiredFieldValidator:

.InitialValue Consider this text to be "no change" and blank to be "updated".

Example
Validate our simple form, using a RequiredFieldValidator, as seen in the example diagram above. If you hit the Submit button without entering any text, you can see that the form does not post back to the server, the JavaScript on the client-side catches the error and displays the error message that you gave it.

If you set .EnableClientScript to false and run the application again, then you will see that the submit button now forces a postback but the field is still validated, so if a malicious user attempts to forge the HTTP request to bypass your validation, it will not be successful.

And there you have it. You have now successfully implemented ASP.NET validation with one control and one line of code (if (this.IsValid)). How easy can it be?

InitialValue
The .InitialValue property gives you an opportunity to enter some text into the TextBox and still ensure that it is edited before submission.

Try changing both the RequiredFieldValidator.InitialValue and TextBox.Text to "EMAIL ADDRESS" and running the application again.

You should find that if you do not change this text and simply hit "Submit", you will get the error message as specified, but now if you blank the field then it is considered valid.

In many ways, this is a functionality overlap with the CompareValidator, as will become clear later on. But in some cases it is more sensible (i.e.. more readable) to use the RequiredFieldValidator - for example, validating that a DropDownList has changed from the "Please Select One..." default.

BaseCompareValidator controls
The CompareValidator and RangeValidator both compare values to literals or other user-entered data. The two controls inevitably contain some common functionality and thus are derived from the BaseCompareValidator class.

The BaseCompareValidator only offers us one property in addition to those offered by the BaseValidator class:

.Type Allows strings of specific formats to be compared accurately.

This one property, however, is very important and includes several "gotchas" that you might need to be aware of.

Type
The .Type property can be used to define how a string is treated. If a textbox is used for numeric, date or currency values, you can use one of the BaseCompareValidator controls to validate the type of data entered or compare it to a value of the same type more accurately.

e.g.. If compared directly as strings, "90" is greater than "100"; but if compared as numbers, "90" is clearly less than "100".

But be aware that both the Client and the Server validation will deal with the input data using the locale of the Server machine, unless you set the page or application Culture/UICulture properties (see globalization in MSDN).



If you do not set the Culture and, like me, you develop primarily on an English locale machine (date format: DD/MM/YYYY) and then run your sites live on a US locale server (date format: MM/DD/YYYY) then, as you can see from the screenshot above, the data will be validated differently in development from live.

The 10th of January is before the 1st of October, after all, whichever country you come from.

It is always worth noting on your page which format the user is expected to use (remembering that YYYY/MM/DD is considered culture-independent), but the more idiot-proof you make things, the bigger idiot the internet produces. You cannot guarantee that the user will pay attention to instructions and even if you ask for YYYY/MM/DD, you will still get someone trying to use MM/DD/YYYY without even suspecting that your machine has an English Locale.

It is strongly recommended that you always (even if you develop on the live machine, or one very similar) specify a Culture in the Web.Config file, for example:






requestEncoding="utf-8"
responseEncoding="utf-8"
culture="en-US"
uiCulture="en-US" />




Once you have done this, you know exactly where you stand. You can request the date in format MM/DD/YYYY format and if the user chooses to ignore that and use a culture-independent style then there is no harm done.

If you do not specify a culture, even in the @Page clause, then you need to be especially careful when comparing to a literal, which may be treated differently in development from production (in fact may cause an exception, if the locale is different and the value becomes invalid). You must then use a culture-independent style for literal dates.

All of this applies equally to numerics in countries which take a comma (,) as a decimal point and to systems using different currency symbols.

Another thing to watch out for is that if any of the values being compared are not of the type the validation is expecting (and in the case of dates, this includes DD-MMM-YYYY, eg. 10-Jan-2003), the form data will be considered valid. The reason for this is that an extra CompareValidator (using the DataTypeCheck operator described below) gives you a way out of this and you would generally expect the error message to be different.

The CompareValidator
The CompareValidator is, at first glance, the least useful of the standard validation controls. However it does have its place and has plenty of designer properties (including the BaseCompareValidator.Type property, see above) to tweak its functionality in interesting ways.

.ControlToCompare Compare the value in this control to the value of another control.
.Operator In what way should we compare the values?
.ValueToCompare Compare the value with a literal value.

Example
In the simple example, we can use this control to ensure that no one uses a specific E-mail address in the form (I mean, we don't want someone registering us for our own spam E-mail scam, do we?).

Drop a CompareValidator in place of the RequiredFieldValidator; set the .Operator to NotEqual (why Equal would ever be the default is beyond me!) and the .ValueToCompare to your own E-mail address. Do not forget, of course, to set the .ControlToValidate and .ErrorMessage properties, as before.

When you run the application and try to enter your own email address into the form, the validator should give you a message as soon as you leave the TextBox. It should also refuse to let you submit the form to the server.

As pointed out before, this is not entirely useful in itself; for the rare occasions you would use this, you would not mind hacking the RequiredFieldValidator to do your bidding.

Only when you start considering the combinations of the unique properties listed above do you realize how powerful this is.

ControlToCompare
Instead of comparing a control with a literal value, you can compare it to another user-entered string. You do this by pointing the .ControlToCompare property to the control you wish to validate.

If this and .ValueToCompare are both set, .ValueToCompare will be ignored.

Operator
It is not only possible to compare values for equality (or inequality), it is possible to look for GreaterThan, GreatThanEqual, LessThan or LessThanEqual.

In all cases, if the condition is true then the .ControlToValidate is considered valid.

Alternatively, you can even check that the input data can be converted to a certain type, using the DataTypeCheck value in this property. This is essential, as pointed out above, because a comparison including an invalid value will always be treated as valid.

For example, when comparing two dates, if "F" is compared to "01/01/2003" then it is simultaneously considered to be greater than, less than and equal to. But when checking "F" using a CompareValidator where .Operator = DataTypeCheck and .Type = Date, validation will fail.

When using .Operator = DataTypeCheck, the .ValueToCompare and .ControlToCompare properties are both ignored and the .Type property is used to define the valid type.

The RangeValidator
The RangeValidator is much like the CompareValidator control (hence the common base class), except that it verifies that the user data falls between two constant values, rather than comparing it to a single value (constant or variable).

With that in mind, the control's unique properties (not part of BaseCompareValidator) are much as you might expect.

.MaximumValue Value may not be greater than this.
.MinimumValue Value may not be less than this.

Example
Referring to our simple form, we can easily use a RangeValidator to verify that the email address entered begins with a lower-case alpha character. Of course, this is not much validation for an email address, but it is a good demonstration of the RangeValidator.

Replace whichever validator you are using with a RangeValidator, set the .ControlToValidate and .ErrorMessage as for any validator. Then set .MinimumValue to a and .MaximumValue to z.

Now any string entered that begins with a lower-case alpha character will be considered valid. Note again that if the TextBox is left blank, it is still considered valid. A separate RequiredFieldValidator must be used if the field is mandatory.

MaximumValue
The value entered by the user may not be greater than this, but the two values can be equal.

MinimumValue
The value entered by the user may not be less than this value (unless blank), but the two values can be equal.

The RegularExpressionValidator
Arguably the biggest, most versatile weapon in the ASP.NET validation arsenal has to be the RegularExpressionValidator.

With just one custom property over those provided by the BaseValidator class, a world of options become available.

.ValidationExpression Value must be in the format described.

Example
In our simple example, replace the current validator with a RegularExpressionValidator and set the .ControlToValidate and .ErrorMessage as usual.

Set the .ValidationExpression to \w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*. If you are using Visual Studio .NET, you can click the ellipses (...) button alongside the property and select Internet Email Address from the default list.

Whilst not perfect email address validation (see below), this is a pretty good approximation. It ensures that the address consists only of "word" characters (A-Z,a-z,0-9,_) with exactly one @ symbol and at least one period (.) after it, allowing for variations which include hyphenations in mid-word and extra periods before and after the @ symbol.

Again, notice that a blank field is always valid unless you also use a RequiredFieldValidator. And, as with all validation controls, no code is required to perform an identical validation on both the Client and the Server.

ValidationExpression
The data entered, if not blank, must match the format specified in regular expression format.

This article will not attempt to cover regular expressions in detail, much has been written elsewhere on this site and others on this subject.

Frankly, I do not claim to understand it that well, I usually manage with the default values provided by Visual Studio .NET or others that I find through Google (there are some fascinating expressions out there, if you look hard enough).

The Visual Studio .NET default values can be found via the ellipses (...) button to the right of the property (within the property sheet) and offer the following validations:

US / French / German / Japanese / PRC Phone Number
US / French / German / Japanese / PRC French Postal Code
US / PRC Phone Number
Internet Email Address
Internet URL
There are some surprising omissions here (certain nationalities, US State Abbreviations, ISBN, Credit Cards, Long Date Formats), and some of the default expressions (inc. Email address) are not 100% accurate.

The Regular Expression Library is the best resource I have found for most of those that are missing and for improving those that exist.

If anyone has other resources, I would be happy to list them with the next update.

The CustomValidator
Sooner or later (probably later, to be honest), you are going to need to validate something and the standard validators are not going to offer it. Maybe you need to access a database, or perform some complex validation involving numerous controls.

If you are going to need to do it repeatedly, you might want to consider creating your own validation control, but for the one-off oddity you can use the CustomValidator.

The CustomValidator allows you to add code to be run on the client and / or the server and set a value showing the result.

You can populate the .ControlToValidate property, passing the value of the control to the event handler or script. But, unlike the other validation controls, the CustomValidator will not throw an exception if .ControlToValidate is empty.

Other than that, it acts exactly the same as any other validation control, either showing the .ErrorMessage in the control itself or passing it to a ValidationSummary control.

The CustomValidator has only one unique property and a unique event handler.

.ClientValidationFunction Name of HTML-scripted function to execute, on validation
.ServerValidate Event handler for server-side validation.

Example
Using our simple example once more, replace the validation control with a CustomValidator. Set the .ControlToValidate and .ErrorMessage properties as before. Ignore the .ClientValidationFunction property for now and add an event handler for the .ServerValidate event.

You can validate the value entered against a database, if you wish. For simplicity, I have chosen to simulate this with a simple if statement.

private void alreadyInUse_ServerValidate(object source,
System.Web.UI.WebControls.ServerValidateEventArgs args)
{
// Default Value
args.IsValid = true;

// Simulating a check against a database
if (args.Value == "pdriley@santt.com")
args.IsValid = false;
}

The validator will always post back to the server to the ClickEvent handler which the requests the results of the validation. But if you enter one of the "existing" E-mail addresses (in my case, my own address) then the error message will be reported back and the Response will not be redirected.

NOTE: The ServerValidate event itself, in fact all server-side validation, is executed after the Page_Load event and before any other events. But the result of the validation is irrelevant unless you later check that the form is valid. The Button_Click event, in this case, is still executed.

ServerValidate (Event)
As you can see in the example above, the ServerValidate event handler should accept an object (the CustomValidator) and arguments of type ServerValidateEventArgs.

The ServerValidateEventArgs class exposes two properties: .IsValid and .Value. All you have to do in most cases is check that .Value is valid and set .IsValid accordingly.

If there is no .ControlToValidate property set for the CustomControl then args.Value will default to String.Empty, but you can still validate the values contained in the controls.

ClientValidationScript
Writing a JavaScript handler is conveniently similar to writing the server-side handler. It still exposes the same two arguments, source and args.

The sample C# code above would directly translate as:

function alreadyInUse_Validate (source, args)
{
// Default Value
args.IsValid = true;

// Simulating a check against a database
if (args.Value == "pdriley@santt.com")
args.IsValid = false;
}

All that is left to be done is set the .ClientValidationScript property to alreadyInUse_Validate and the client-side validation is complete.

The ValidationSummary
It should be fairly clear by now that many web pages are going to include a lot of validation controls. No user is going to be happy with all those error messages popping up at random positions around the page, but it is always nice to have something marking the invalid fields.

And this is where the ValidationSummary control comes into play.

The ValidationSummary derives directly from WebControl and is not related to the other controls except by symbiotic functionality.

When a page is validated, all of the .ErrorMessage properties from validation controls in the same container (e.g.. Form or DataGrid row) that fail validation are passed to the ValidationSummary control to be displayed in an unordered list.

This finally explains the .Text property, common across the validation controls. If .Text is populated then it will be displayed in place of the validation control, but still .ErrorMessage will be used to populate the ValidationSummary.

The ValidationSummary control has a number of unique properties that affect the way messages are displayed to the user.

.DisplayMode How the unordered list is rendered.
.EnableClientScript Should the error list be updated during client validation?
.HeaderText Defines the text that heads the error list.
.ShowMessageBox Show error message list in a javascript "alert".
.ShowSummary Render the error messages on the page itself.

Example
To demonstrate the full power of the ValidationSummary, it is necessary to come up with something a little more complicated than the simple example that is used throughout the rest of the article.

Consider a simple registration form for, say, a chat room.

The customer requesting this form has asked for the following:

User ID - mandatory, must be 6-8 characters
Password - mandatory, must be 8 characters with at least two numeric, also confirmed
Name - mandatory
E-mail - mandatory, must be valid format
Sex - mandatory, drop down list, must not default
Date of birth - not mandatory but must be valid if entered


Set the form up yourself, complete with validators, using the rest of this article as a guide. As well as giving each control an error message, change the .Text properties to * and drop the new (much smaller) control next to the control it is validating.

Although the Visual Studio .NET IDE will not allow you to drop validation controls directly on top of each other, you can edit the HTML view to force the controls into the same position.

Along with the diagram, here is a clue: there are 12 validation controls in total:

6 x RequiredFieldValidator
4 x RegularExpressionValidator
2 x CompareValidator
Drop a ValidationSummary at the bottom of the page, next to a submit button. The only property you might want to set is the .HeaderText. (In the example, it is set to Please fix the following errors:)

Run the form and you can now see how a combination of validation controls and a ValidationSummary control can be used to create a very secure and yet flexible and most importantly usable Web Form.

If you get stuck then, again, the example is included in the downloadable zip file at the top of the article.

DisplayMode
The list can be rendered in any of the following DisplayMode styles:

BulletList (list items in an unordered list)
List (delimited only by a new line)
SingleParagraph (delimited by space)
The default option is BulletList.

EnableClientScript
By default, .EnableClientScript is true and the list of errors will be shown when the form is submitted.

If .EnableClientScript is set to false then the ValidationSummary will not be shown until the server reports the errors back to the client machine.

This is useful if (and only if) the .EnableClientScript property of all the validation controls are also false. If they are not then the form will not post back to the server and the ValidationSummary will never report the errors.

HeaderText
Allows the definition of text to describe the list of errors. This will be posted directly before the list of error messages, regardless of the .DisplayStyle value.

ShowMessageBox
If set to true, the ShowMessageBox property creates a JavaScript, alert() call to display all of the error messages in a message box.

The message box will emulate the style defined in .DisplayMode as closely as possible.

ShowSummary
If set to true, the ShowSummary property renders the list of error messages into the HTML at the position of the Label.

Either ShowMessageBox or ShowSummary should always be true, but they should only both be true if absolutely necessary.

Bypassing validation
If you have two postback events (e.g.. "Next" and "Back" buttons), and one requires validation while the other does not, you can set the .CausesValidation property of one control to true and the other to false.

This will bypass both client and server validation. You can still check this.IsValid, if you want to use some common code for handling both events, and validation will succeed even if there is no valid data entered.

Conclusion
This is a very powerful set of controls with very few obvious dangers. As an added bonus, they are incredibly easy to implement. In Visual Studio .NET, it is just drag-drop and select a couple of properties.

There is a lot of information to absorb in one pass, but hopefully this article will serve as a reasonably simple reference guide as well as a "How to" guide.

Once you have used the validation controls a few times, you will be dropping them into pages just for fun.

Downloads

No comments: