Thursday, September 17, 2009

Creating a self validating ASP.Net control

It started with a simple request for a custom control to perform some validation. Seems easy enough, but it is not. Sure, there is an easy solution and that is to create a composite control, containing the original control and a normal validator, but that means you don't inherit from either control and you have to copy all the properties and methods you are interested in. If, as I did, you do not like this solution, you will soon find out that there are a lots of obstacles to be conquered if you want all the features of a validator, the first of which being that a control cannot easily change the Controls collection of its parent. It would be bad design. Therefore simply adding a Validator from inside the control does not work.

Let's start with what others did: Self Validating ASP.NET Text Box. This CodeProject article shows how you can build a control that is validated only on the server side and implements the IValidator interface. However, you will notice that it did not implement the ValidationGroup behaviour. And that is due to a simple fact: there is a method of the Page class called GetValidators(string validationGroup) that is used all around ASP.Net. It looks kind of like this:

public ValidatorCollection GetValidators(string validationGroup)
{
if (validationGroup == null)
{
validationGroup = string.Empty;
}
ValidatorCollection validators = new ValidatorCollection();
if (this._validators != null)
{
for (int i = 0; i < this.Validators.Count; i++)
{
BaseValidator validator = this.Validators[i] as BaseValidator;
if (validator != null)
{
if (string.Compare(validator.ValidationGroup,
validationGroup, StringComparison.Ordinal) == 0)
{
validators.Add(validator);
}
}
else if (validationGroup.Length == 0)
{
validators.Add(this.Validators[i]);
}
}
}
return validators;
}
Notice anything? There is no mention of IValidator, instead only BaseValidators are sought and the reason the IValidator approach works at all is the line before last where a validator is added to the list if the validationGroup parameter is empty. That is, it will work with IValidators but only on Buttons or other postback controls that have an empty ValidationGroup.

So let's start from the idea of the original article: an IValidator TextBox control with server validation only. For simplicity I will compare the value of the control with a constant string and fail the validation if the comparison fails.

Click to expand


You notice that the entire logic of this method is that the validators in the Page.Validators collection will be validated when need arises. In order to implement the ValidationGroup behaviour, we need to either fool the GetValidators method or to actually add a BaseValidator to the collection, not the IValidator control. I would have preferred the first method, but GetValidators is executed not only in Page.Validate but, being public, can also be executed in other situations (and in fact it is called by every control that wants to cause a postback).

My first attempt was to simply add a dummy validator to the collection, with the proper ValidationGroup set. It worked for the client side (we'll get to that) but then it failed miserably when Page.Validate tried to loop through the collection. I was forced to create my own validator control that inherits from BaseValidator. Do not worry, though, it is very lightweight and does not need to be added to the Controls collection. Here is the source:
Click to expand

public class DummyValidator:BaseValidator
{
private readonly IValidator _realValidator;

public DummyValidator(IValidator realValidator)
{
_realValidator = realValidator;
}

protected override bool EvaluateIsValid()
{
_realValidator.Validate();
return _realValidator.IsValid;
}

protected override bool ControlPropertiesValid()
{
return true;
}
}


Note: the following code has a property called ValidationGroup. The TextBox already has it, for its own AutoPostBack behaviour. I left it there, though, because you might want to use this on a control that doesn't have it natively.

Now the code for the ValidatedTextBox class looks like this:
Click to expand

private DummyValidator _dummyValidator;

protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if (Page != null)
{
_dummyValidator = new DummyValidator(this){ValidationGroup = ValidationGroup};
Page.Validators.Add(_dummyValidator);
//Page.Validators.Add(this);
}
}

protected override void OnUnload(EventArgs e)
{
if (Page != null)
{
//Page.Validators.Remove(this);
Page.Validators.Remove(_dummyValidator);
}
base.OnUnload(e);
}

private string _validationGroup;

public string ValidationGroup
{
get
{
if (_validationGroup == null)
{
if (ViewState["ValidationGroup"] == null)
ValidationGroup = "";
else
ValidationGroup = (string) ViewState["ValidationGroup"];
}
return _validationGroup;
}
set
{
_validationGroup = value;
ViewState["ValidationGroup"] = value;
}
}


So the ValidationGroup is now working on the server side since there is an actual BaseValidator in the Validators collection calling the Validate method of our control. Since the DummyValidator class doesn't implement any specific behaviour, it can be reused for any type of validated control.

Now for the client side part. The client validation scripts work with a javascript array called Page_Validators. The validators are rendered as span elements (where the error message is displayed) and the array holds all these elements, with some extra properties like isValid, errorMessage, evaluationfunction, controltovalidate, validationGroup, focusOnError, enabled and display. The first three are like the IValidator members, only in javascript, controltovalidate is the client id of the control to be validated while focusOnError is used to scroll the page/container in order to see the invalid control without looking all over the page to find it.

All the ASP.Net validation functions are in a javascript file called WebUIValidation.js and embedded in the System.Web.dll library. We need to load it in order for validation to work. Then we need to render a span element and set its attributes. Lastly, but most important of all, we must create a function that will evaluate the validity of the control.

I will just post the entire source, since all the client code is practically copied from the BaseValidator code.

Click to expand


There are several things you need to take care of.
One of them is the client display of the validators. Normally, a validator's span element is rendered with the ErrorMessage as inner html. The ASP.Net framework then changes the visibility of the span depending on the validity of the control. I have chosen to not render the error message, as I wanted a different behaviour (in this case, a red background). The span element is still shown and hidden, but being empty, there is no visible effect.
Another thing is when you would use validation summaries. The error message is being rendered as a property of the span. It WILL be used in the summary, even if it is not normally displayed.

This code will work with almost no change for any control, so you could encapsulate it into a ValidatorHelper class or something. The only variants are the Validate method and evaluatefunction function.

Also note that having inherited from BaseValidator, I bypassed the normal functionality of the default validator controls. If you want to inherit from one of those, like RegularExpressionValidator, you might encounter problems, especially for controls that are not normally validated and for which the normal validator controls try to get their value. Take a look at the ValidationProperty attribute that you might need to use to decorate your control in order to work with default validators. Also, the javascript behaviour is to look for a control that has a value attribute. If not found it searches deeper into the children. So if you want to use a default validator, you might want to add script to keep a value attribute updated on the validated control HTML element.

And that was all.

0 comments: