Wednesday, March 23, 2011

Creating a Custom Validator Control in .Net

The ASP .Net has its own form validation framework. This framework consists of:
  • Base Validation Class, the BaseValidator
  • ASP Validation Controls
  • A Clientside Validation JavaScript Framework
The "Base Validation Class" is the base of all validation controls and is also used to create own custom controls. If you want to create your own validation control, you have to inherit from the base class.

The ASP validation controls are basic validation controls provided by Microsoft. These are:
  • RequiredFieldControl: Checks an input field if its empty
  • CompareValidator: Compares the value of a field with another value. This can be a value in another field or a constant value.
  • RangeValidator: Checks if the value in a form field is within a range
  • RegularExpressionValidator: RegularExpression validators are used to check if the value in a control matches a pattern defined by the regular expression.
  • CustomValidator: If none of the Validators are useful for a purpose, you can create a custom validator. You must handle a customvalidator in the code-behind of your site (aspx page). This means you write backend code for the validator.
All validation controls can validate a field in the frontend or in the backend. Therefore they need also an implementation in JavaScript. Microsoft provides a JavaScript solution for its own validation controls.

Creating a Custom Validation Control

When I talk about custom validation controls, I don't mean the standard "CustomValidator" or an implementation of it. I mean an independent validator like the reqularexpressionvalidator for a specific purpose, which can be used in multiple pages. As an example you want to create a date validator which has attributes to force the user to enter only dates in the past or in the future.

Before you begin to write code you should know that you have to write:
  • Logic for the Backend Validation
  • Logic for the Frontend Validation. This must correspond 1to1 to the backend logic.
  • Logic for the Frontend Validation Framework. The validation framework must know which code to use for validation when a custom validation control is created.


Understanding the client validation

As an example we have two form fields to validate. The first field is a date field and the second one a text field. The value of the date field must be in a date format whereas the text field should not be empty. Here is the simple code: (The date validation is not correct, it is just an example)

 <asp:Label ID="lblBirth" Text="Birthday:" runat="server" /><br />  
 <asp:TextBox ID="txtDate" Width="80" MaxLength="10" runat="server" />     
 <asp:RegularExpressionValidator runat="server"  
           ControlToValidate="txtDate"  
           ErrorMessage="Please check your date format (19.05.2001)"  
           EnableClientScript="true"  
           ValidationExpression="\d\d\.\d\d\.\d\d\d\d" />  
 <div class="spacer"></div>  
 <asp:Label ID="lblName" Text="Name:" runat="server" /><br />  
 <asp:TextBox ID="txtName" Width="200" runat="server" />  
 <asp:RequiredFieldValidator runat="server"  
           ControlToValidate="txtName"  
           ErrorMessage="Please enter a name"  
           EnableClientScript="true" />  

The clientvalidation is enabled, so the validation is made in the browser. But how is this done?
When you insert a validation control into your aspx page the validator inserts JavaScript Code
to the end of you site. When you look at the page source you'll find something like this:

 var Page_Validators = new Array(document.getElementById("ctl02"), document.getElementById("ctl03"));  

 var Page_Validators = new Array(document.getElementById("ctl02"), document.getElementById("ctl03"));  
 var ctl02 = document.all ? document.all["ctl02"] : document.getElementById("ctl02");  
 ctl02.controltovalidate = "txtDate";  
 ctl02.errormessage = "Please check your date format (19.05.2001)";  
 ctl02.evaluationfunction = "RegularExpressionValidatorEvaluateIsValid";  
 ctl02.validationexpression = "\\d\\d\\.\\d\\d\\.\\d\\d\\d\\d";  


The line

 var Page_Validators = new Array(document.getElementById("ctl02"), document.getElementById("ctl03"));  

creates an array "Page_Validators" that contains some DOM node. These Dom nodes are the rendered html of the validaton messages:

 <span id="ctl02" style="color:Red;visibility:hidden;">Please check your date format (19.05.2001)</span>  
 <span id="ctl03" style="color:Red;visibility:hidden;">Please enter a name</span>  

The following line creates a variable ct102 which contains a reference to the DOM nodes. In JavaScript everything is handled with references. Therefore a change in the variable ct102 changes the DOM node with the id "ct102". The variable ct102 is extended with the properties:
  • .controltovalidate: These property contains the id of the input field
  • .errormessage: This is the error message to display
  • .evaluationfunction: This is the name of the JavaScript function to call when validating. For the standard validators the functions are implemented in the JavaScript Validation Framework.
  • .initialvalue: If a field has a initial value, it is handled like the field does not contain a value.
These 4 properties "controltovalidate, errormessage, evaluationfunction and initialvalue" are used by all validators. But you can also extend these properties for your own custom validator, like the RegularExpressionValidator does with the "validationexpression" property.

When you click the send button to submit your form, a validation function "Page_ClientValidate" is called which iterates through the "Page_Validators" array and calls the function defined in the "evaluationfunction" property such as the built-in evaluation function for the RequiresFieldValidator:

 function RequiredFieldValidatorEvaluateIsValid(val) {  
   return (ValidatorTrim(ValidatorGetValue(val.controltovalidate)) != ValidatorTrim(val.initialvalue))  
 }  

These evaluation functions always return true or false. The returned value is saved in the ".isvalid" property of the validator. Also a global variable "Page_IsValid" is created which returns a boolean value. Is "Page_IsValid" valid is false the submit of the form is cancelled and the errors in the "errormessage" property of the validators are displayed.



Creating the Backend Code

When creating a custom validator you must inherit from the "BaseValidator" class. This class offers multiple properties and functions to override for creating a valid validator. The most important methods are:
  • bool ControlPropertiesValid(): You can use this function to check if the properties of your validator are valid.
  • bool EvaluateIsValid(): The real validation happens in this method.
and the most important properties:
  • ControlToValidate : ID of the control to validate
  • ErrorMessage: The error message string
  • EnableClientScript: If client validation is enabled or not

There are lot of more but these are enough for our example. As an example we create now a date validator:

1:  namespace ValidatorDemo.Validator  
2:  {  
3:  public class DateValidator : BaseValidator  
4:  {  
5:      #region Private Fields  
6:      private String mValueToValidate;  
7:      private Control mControlToValidate;  
8:      #endregion  
9:      #region Lifecycle Overrides  
10:      /// <summary>  
11:      /// Initialize the client script properties  
12:      /// </summary>  
13:      /// <param name="pE">Not used</param>  
14:      protected override void OnInit(EventArgs pE)  
15:      {  
16:          if (this.EnableClientScript)  
17:              RegisterClientValidationScript();  
18:      }  
19:      /// <summary>  
20:      /// Checks if the properties are correct  
21:      /// </summary>  
22:      /// <returns>True if properties are o.k.</returns>  
23:      protected override bool ControlPropertiesValid()  
24:      {  
25:          Control findControl = this.FindControl(this.ControlToValidate);  
26:          if (findControl is TextBox)  
27:          {  
28:              TextBox txtBox = ((TextBox)findControl);  
29:              mControlToValidate = txtBox;  
30:              mValueToValidate = txtBox.Text;         
31:              return true;  
32:          }  
33:          else  
34:          {  
35:              return false;  
36:          }  
37:      }  
38:      /// <summary>  
39:      /// Validates the date  
40:      /// </summary>  
41:      /// <returns>True if evaluation is valid, else false</returns>  
42:      protected override bool EvaluateIsValid()  
43:      {  
44:          try  
45:          {  
46:              string sDate = mValueToValidate;  
47:              if (sDate == string.Empty) return true;  
48:              CultureInfo ci = new CultureInfo("de-DE");  
49:              DateTime date = DateTime.Parse(sDate, ci);  
50:          }  
51:          catch  
52:          {  
53:              return false;  
54:          }  
55:          return true;  
56:      }  
57:      #endregion  
58:      #region Helper  
59:      /// <summary>  
60:      /// The Method provides the clientside validation script.  
61:      /// </summary>  
62:      private void RegisterClientValidationScript()  
63:      {  
64:          //Registering validator clientside javascript function  
65:          Page.ClientScript.RegisterExpandoAttribute(ClientID, "evaluationfunction", "ValidateDateBox", false);  
66:      }  
67:      #endregion  
68:  }  
69:  }  

In the method "OnInit" we check if the control property to activate the client script is set to true. If so the "RegisterClientValidationScript" method is called. It expands the DOM object with the property "evaluationfunction" and sets it to a custom JavaScript function "ValidateDateBox". The function "ValidateDateBox" must be implemented in the client code. Now following client code is generated when we insert this control:

 var ctl02 = document.all ? document.all["ctl02"] : document.getElementById("ctl02");  
 ctl02.evaluationfunction = "ValidateDateBox";  
 ctl02.controltovalidate = "txtDate";  
 ctl02.errormessage = "Enter a valid date (e.g. 05.06.1981)";  

In the "ControlPropertiesValid" method we check it the control we have to validate is a textbox control. All other controls are not accepted. If the control is a textbox we initialize to class variables to hold the controls and its value.

At least the "EvaluateIsValid" method is called in the lifecycle. It checks if the value is a correct date and returns a boolean value to indicate this.

As you can see it is very easy to create a validator control.

Now we create the client code for the validator. The function must have the name "ValidateDateBox" like how we defined it in the backend code. The evaluation function has one argument, the validator, so it can get the value to validate.

 function ValidateDateBox(val) {  
     var oDate = document.getElementById(val.controltovalidate);  
     var sDate = oDate.value;  
     // Not required field  
     if (sDate == "") {  
         return true;  
     }  
     // Check Date       
     return ParseDate(sDate);  
 }  
 function ParseDate(sDate) {  
     var iDay, iMonth, iYear;  
     var arrValues;  
     var today = new Date();      
     var fPoint = /^\d{1,2}\.\d{1,2}\.\d{4}$/;  
     if (fPoint.test(sDate)) {  
         arrValues = sDate.split(".");  
     } else {  
         return false;  
     }  
     iDay = arrValues[0];  
     iMonth = arrValues[1];  
     iYear = arrValues[2];  
     if ((iMonth == null) || (iYear == null)) {  
         return false;  
     }  
     if ((iDay > 31) || (iMonth > 12) || (iYear < 1900)) {  
         return false;  
     }  
     var dummyDate = new Date(iYear, iMonth - 1, iDay, 0, 0, 0);  
     if ((dummyDate.getDate() == iDay) && (dummyDate.getMonth() == iMonth - 1) && (dummyDate.getFullYear() == iYear)) {  
         return dummyDate;  
     }  
     return false;  
 }  

You can check the validation by entering a date like "40.11.2002" or "31.02.2001". Both should be invalid.


Extending the validator with custom properties

Now we want a property "OnlyFutureDate" that force users to enter a date in the future. This can be useful for traveling agencies or insurances. The validator now looks like that:

 <asp:Label ID="Label1" Text="Birthday:" AssociatedControlID="txtDate" runat="server" /><br />  
 <asp:TextBox ID="txtDate" Width="80" MaxLength="10" runat="server" />     
 <demo:DateValidator runat="server"  
     ControlToValidate="txtDate"  
     ErrorMessage="Enter a valid date (e.g. 05.06.1981)"  
     OnlyFutureDate="true"  
     EnableClientScript="true" />  

In the backend code we create a private property "mOnlyFuture" and a public property "OnlyFuture". The public property is the property used in the aspx page as control property.

 private Boolean mOnlyFutureDate = false;  
 /// <summary>  
 /// Allows only dates that lie in the future  
 /// </summary>  
 public Boolean OnlyFutureDate  
 {  
     set { mOnlyFutureDate = value; }  
 }  

We extend the "EvaluateIsValid" method to check if the property is set and the date is a date in the future:

 protected override bool EvaluateIsValid()  
 {  
     try  
     {  
         string sDate = mValueToValidate;  
         if (sDate == string.Empty) return true;  
         CultureInfo ci = new CultureInfo("de-DE");  
         DateTime date = DateTime.Parse(sDate, ci);  
         DateTime now = DateTime.Now;  
         DateTime today = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0);  
         // if only future is allowed  
         if (mOnlyFutureDate)  
             return (date >= today);  
     }  
     catch  
     {  
         return false;  
     }  
     return true;  
 }  

The clientcode must also be informed if the property is set or not. So we extend the DOM object with an additional property "onlyfuturedate".

 private void RegisterClientValidationScript()  
 {  
     //Registering validator clientside javascript function  
     Page.ClientScript.RegisterExpandoAttribute(ClientID, "evaluationfunction", "ValidateDateBox", false);  
     Page.ClientScript.RegisterExpandoAttribute(ClientID, "onlyfuturedate", mOnlyFutureDate.ToString().ToLower(), false);  
 }  

Following client code is generated:

 var ctl02 = document.all ? document.all["ctl02"] : document.getElementById("ctl02");  
 ctl02.evaluationfunction = "ValidateDateBox";  
 ctl02.onlyfuturedate = "true";  
 ctl02.controltovalidate = "txtDate";  
 ctl02.errormessage = "Enter a date in the future";  


At least we must change the client code, so the JavaScript can also accept the new property.

 function ValidateDateBox(val) {  
     var oDate = document.getElementById(val.controltovalidate);  
     var isOnlyFuture = val.onlyfuturedate;       
     var sDate = oDate.value;  
     // Not required field  
     if (sDate == "") {  
         return true;  
     }  
    var dummyDate = ParseDate(sDate);  
     var now = new Date();  
     var today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); // 23.09.2011 00:00:00  
     if (dummyDate != false) {  
         // Check if it's a future date  
         if (isOnlyFuture == "true") {  
             return (dummyDate >= today);  
         }  
     }  
     return (dummyDate != false);  
 }  

Because we extended the DOM object in the backend code with the property "onlyfuturedate" we can now use it to check if the property is set.

Updatepanels and Custom Validation Controls

All other validation controls are made in the form explained in this post. One thing you have to pay attention is when you use "UpdatePanels". When a custom validator sits in an updatepanel the expando attributes get lost when the panel is updated. Therefore you have to register the expando attribute in the ScriptMaganer Control. The propably easiest way to do this is to create a custom helper method, which check if a scriptmanager exists and registers than like that:


 public static void RegisterExpandoAttribute(this Control control, string controlId, string attributeName, string attributeValue, bool encode)  
 {  
     if (control.IsInsideUpdatePanel() && control.HasPageScriptManager())  
     {  
         ScriptManager.RegisterExpandoAttribute(control, controlId, attributeName, attributeValue, encode);  
     }  
     else  
     {  
         control.Page.ClientScript.RegisterExpandoAttribute(controlId, attributeName, attributeValue, encode);  
     }  
 }  


See following post for more info: http://thedalehawthorneeffect.blogspot.com/2009/05/another-way-to-handle-client-script.html

3 comments:

r3v said...

Una gran ayuda! gracias

shawn z said...

this really does work way better than the other system i was using. great info!

Rom said...

Why mControlToValidate property declared and textbox assigned to it when it is not used anyware.
Nice Article.

Thanks.