Sunday, April 6, 2008

.Net 3.5 Validator Extensibility - Part Two

Starting from this week, I will be showing some examples of how to create your own .net validation control starting with Phone validator.

Issue

Often we as developer receive requests like handling phone number in three textboxes instead of one. and the required number of validators grows from two (the required field validator and regular expression validator) to six (the two previously mentioned validators for each textbox). Although it is not a complicated task, a ui developer still need to spend time to drag and drop textboxes and respective validators accordingly. It could be considerd as boring.

Solution

Therefore, a solution is advised and plan is set to create two controls; one PhoneNumber TextBox control and PhoneNumber Validation control.

First Step - Create PhoneNumber Control

Obviously, a PhoneNumber control needs to be able to render three TextBoxes. These TextBoxes will be placing as PhoneNumber control's child controls. But there is a catch in terms of its renderation. In order to hook up the ControlToValidate property of PhoneNumber validator, which will be created in the later part of this article, with PhoneNumber TextBox control, these three textboxes need to be placed under a div or a span block. The best way to do this is to create this PhoneNumber control inherited from WebCotrol and override some of its properties and key methods to make it render the result we want. There is also another feature that I would like to add to this control which will allow a jump to next textbox when input is valid. We can do so by attaching a javascript library to this control. It can be done in several ways; but here, I would like to implement this control with IScriptControl interface.(Details about how to implement a ScriptControl class, please consult the online tutorials on www.asp.net/ajax.)

Here are its programming and javascript library.:


[DefaultProperty("PhoneNumber")]

[ToolboxData("<{0}:PhoneTextBox runat=server>")]

public class PhoneTextBox : WebControl, IScriptControl

{

private TextBox _tbxAreaCode;

private TextBox _tbxPrefix;

private TextBox _tbxSuffix;

private ScriptManager _sm;



[Bindable(true)]

[Category("General")]

[DefaultValue("")]

[Localizable(true)]

public string PhoneNumber

{

get

{



String s = (String)ViewState["PhoneNumber"];

return ((s == null) ? string.Empty : s);

}

set

{

if (value.Length == 0)

ViewState["PhoneNumber"] = "";

else

{

Regex regex = new Regex("^\\d{10}$");

string tmp = value.Replace("(", "").Replace(")", "").Replace("-", "").Replace(" ", "");

if (!regex.IsMatch(tmp) && tmp.Length > 0)

throw new ArgumentException("Phone number must be 10 digits of number", "PhoneNumber");



ViewState["PhoneNumber"] = tmp;

if (value.Length == 10)

{

_tbxAreaCode.Text = tmp.Substring(0, 3);

_tbxPrefix.Text = tmp.Substring(3, 3);

_tbxSuffix.Text = tmp.Substring(6, 4);

}

}

}

}

[Bindable(true)]

[Category("General")]

[DefaultValue("")]

[Localizable(true)]

public string ControlToJumpTo

{

get

{



String s = (String)ViewState["ControlToJumpTo"];

return ((s == null) ? string.Empty : s);

}

set

{

ViewState["ControlToJumpTo"] = value;

}

}



[Bindable(true)]

[Category("Validation")]

[DefaultValue("")]

[Localizable(true)]

public string ValidationGroup

{

get

{

String s = (String)ViewState["ValidationGroup"];

return ((s == null) ? string.Empty : s);

}



set

{

ViewState["ValidationGroup"] = value;

}

}



protected override HtmlTextWriterTag TagKey

{

get

{

return HtmlTextWriterTag.Span;

}

}



protected override string TagName

{

get

{

return "span";

}

}



protected override void OnInit(EventArgs e)

{

EnsureChildControls();

base.OnInit(e);

}



protected override void CreateChildControls()

{

base.CreateChildControls();

_tbxAreaCode = new TextBox();

_tbxAreaCode.ID = "_tbxAreaCode";

_tbxAreaCode.MaxLength = 3;

_tbxAreaCode.Width = new Unit("30px");

Controls.Add(_tbxAreaCode);

_tbxPrefix = new TextBox();

_tbxPrefix.ID = "_tbxPrefix";

_tbxPrefix.MaxLength = 3;

_tbxPrefix.Width = new Unit("30px");

Controls.Add(_tbxPrefix);

_tbxSuffix = new TextBox();

_tbxSuffix.ID = "_tbxSuffix";

_tbxSuffix.MaxLength = 4;

_tbxSuffix.Width = new Unit("40px");

Controls.Add(_tbxSuffix);

_v = new PhoneValidator();

_v.ID = ID + "_v";

_v.ControlToValidate = ID;

_v.IsPhoneRequired = IsRequired;

_v.ValidationGroup = ValidationGroup;

_v.ErrorMessageForInvalidPhoneNumber = PhoneInvalidErrorMessage;

_v.ErrorMessageForMissingPhoneNumber = PhoneMissingErrorMessage;

_v.Text = "*";

Controls.Add(_v);

}



protected override void OnLoad(EventArgs e)

{

base.OnLoad(e);

if (Page.IsPostBack Page.IsCallback)

{

PhoneNumber = _tbxAreaCode.Text + _tbxPrefix.Text + _tbxSuffix.Text;

}

else

{

if (!string.IsNullOrEmpty(PhoneNumber))

{

_tbxAreaCode.Text = PhoneNumber.Substring(0, 3);

_tbxPrefix.Text = PhoneNumber.Substring(3, 3);

_tbxSuffix.Text = PhoneNumber.Substring(6, 4);

}

}

}



protected override void OnPreRender(EventArgs e)

{

base.OnPreRender(e);

if (!this.DesignMode)

{

// Test for ScriptManager and register if it exists

_sm = ScriptManager.GetCurrent(Page);



if (_sm == null)

throw new HttpException("A ScriptManager control must exist on the current page.");



_sm.RegisterScriptControl(this);

}

}



protected override void Render(HtmlTextWriter writer)

{

if (!this.DesignMode)

_sm.RegisterScriptDescriptors(this);

base.Render(writer);

}



protected override void RenderContents(HtmlTextWriter writer)

{

_tbxAreaCode.RenderControl(writer);

writer.Write(" - ");

_tbxPrefix.RenderControl(writer);

writer.Write(" - ");

_tbxSuffix.RenderControl(writer);

_v.RenderControl(writer);

}



#region IScriptControl Members



public IEnumerable GetScriptDescriptors()

{

ScriptControlDescriptor descriptor = new ScriptControlDescriptor("Patmos.Web.UI.WebControls.PhoneTextBox", this.ClientID);

descriptor.AddElementProperty("Areacode", _tbxAreaCode.ClientID);

descriptor.AddElementProperty("Prefix", _tbxPrefix.ClientID);

descriptor.AddElementProperty("Suffix", _tbxSuffix.ClientID);

descriptor.AddElementProperty("ControlToJumpTo", Page.FindControl(ControlToJumpTo).ClientID);

yield return descriptor;

}



public IEnumerable GetScriptReferences()

{

yield return new ScriptReference("PathTo.PhoneTextBoxFunction.js", this.GetType().Assembly.FullName);

}

}



Type.registerNamespace("Patmos.Web.UI.WebControls");
Patmos.Web.UI.WebControls.PhoneTextBox = function(element) {

Patmos.Web.UI.WebControls.PhoneTextBox.initializeBase(this, [element]);
this._areacode = null;
this._prefix = null;
this._suffix = null;
this._jumpTo = null;
this._keyUpHandler = Function.createDelegate(this, this._onKeyUp);

}

Patmos.Web.UI.WebControls.PhoneTextBox.prototype = {

initialize: function() {
Patmos.Web.UI.WebControls.PhoneTextBox.callBaseMethod(this, 'initialize');
$addHandler(this._areacode, 'keyup', this._keyUpHandler);
$addHandler(this._prefix, 'keyup', this._keyUpHandler);
$addHandler(this._suffix, 'keyup', this._keyUpHandler);

},

dispose: function() {
//Add custom dispose actions here
Patmos.Web.UI.WebControls.PhoneTextBox.callBaseMethod(this, 'dispose');
},

get_Areacode: function() {
return this._areacode;
},

set_Areacode: function(value) {
this._areacode = value;
this.raisePropertyChanged("Areacode");
},

get_Prefix: function() {
return this._prefix;
},

set_Prefix: function(value) {
this._prefix = value;
this.raisePropertyChanged("Prefix");
},

get_Suffix: function() {
return this._suffix;
},

set_Suffix: function(value) {
this._suffix = value;
this.raisePropertyChanged("Suffix");
},

get_ControlToJumpTo: function() {
return this._jumpTo;
},

set_ControlToJumpTo: function(value) {
this._jumpTo = value;
this.raisePropertyChanged("ControlToJumpTo");
},

_onKeyUp: function(e) {
if (e.target.id == this._areacode.id)
{
if (this._areacode.value.length == 3)
{
this._prefix.select();
}
}
else if (e.target.id == this._prefix.id)
{
if (this._prefix.value.length == 3)
{
this._suffix.select();
}
}
else
{
if (this._suffix.value.length == 4)
this._jumpTo.focus();
}
}
}

Patmos.Web.UI.WebControls.PhoneTextBox.registerClass('Patmos.Web.UI.WebControls.PhoneTextBox', Sys.UI.Control);




Second Step - Create PhoneNumber Validator

First of all, I create a class derived from BaseValidator and reprogram some of its properties and methods to make it work in our favor. Because I also want this validator to be able to do same things as RequiredField and RegularExpression validators, I added three properties to handle it and there are MissingPhoneNumberErrorMessage, InvalidPhoneNumberErrorMessage and IsPhoneRequired. Second, I implement EvaluateIsValid method (It is required because it's a abstract method in BaseValidator). This is actually a server side validation method and it is triggered when Page.Validate is called. I override ControlPropertiesValid to make sure that the control to validate is of the type of PhoneNumber TextBox control. Finally, I override AddAttributesToRender and OnPreRender methods. In OnPreRender event, I register its javascript library using client script manager. In AddAttributesToRender, I register its associated properties that will eventually be used in its client validation function. Without registering these attributes, the client validation will not engage.

Here are its programming and javascript library:


public class PhoneValidator : BaseValidator
{
private string _error;

public new string ErrorMessage
{
get { return _error; }
set { throw new NotSupportedException("ErrorMessage is not supported. Please use ErrorMessageForMissingPhoneNumber if phone number is required, or ErrorMessageForInvalidPhoneNumber if phone number contains illegal characters."); }
}

[Browsable(true)]
[Category("Behavior")]
[Themeable(false)]
[DefaultValue(true)]
[Description("Is Phone is required?")]
public bool IsPhoneRequired
{
get
{
return (bool)(ViewState["IsPhoneRequired"] ?? true);
}
set
{
ViewState["IsPhoneRequired"] = value;
}
}

[Browsable(true)]
[Category("Behavior")]
[Themeable(false)]
[DefaultValue("Phone number is required.")]
[Description("Error message for missing areacode.")]
public string ErrorMessageForMissingPhoneNumber
{
get
{
return (string)(ViewState["ErrorMessageForMissingPhoneNumber"] ?? string.Empty);
}
set
{
ViewState["ErrorMessageForMissingPhoneNumber"] = value;
}
}

[Browsable(true)]
[Category("Behavior")]
[Themeable(false)]
[DefaultValue("Phone number is invalid.")]
[Description("Error message for invalid phone number.")]
public string ErrorMessageForInvalidPhoneNumber
{
get
{
return (string)(ViewState["ErrorMessageForInvalidPhoneNumber"] ?? string.Empty);
}
set
{
ViewState["ErrorMessageForInvalidPhoneNumber"] = value;
}
}

protected override void AddAttributesToRender(System.Web.UI.HtmlTextWriter writer)
{
base.AddAttributesToRender(writer);

if (this.RenderUplevel)
{
string clientID = this.ClientID;
PhoneTextBox c = (PhoneTextBox)Page.FindControl(ControlToValidate);
Page.ClientScript.RegisterExpandoAttribute(clientID, "evaluationfunction", "PhoneValidatorEvaluateIsValid");
Page.ClientScript.RegisterExpandoAttribute(clientID, "controlstovalidate", string.Format("{0},{1},{2}", c.AreacodeClientID, c.PrefixClientID, c.SuffixClientID));
Page.ClientScript.RegisterExpandoAttribute(clientID, "areacodecontroltovalidate", c.AreacodeClientID);
Page.ClientScript.RegisterExpandoAttribute(clientID, "prefixcontroltovalidate", c.PrefixClientID);
Page.ClientScript.RegisterExpandoAttribute(clientID, "suffixcontroltovalidate", c.SuffixClientID);
Page.ClientScript.RegisterExpandoAttribute(clientID, "setfocusonerror", this.SetFocusOnError.ToString());
Page.ClientScript.RegisterExpandoAttribute(clientID, "isphonerequired", this.IsPhoneRequired.ToString());
Page.ClientScript.RegisterExpandoAttribute(clientID, "missingphonenumber", this.ErrorMessageForMissingPhoneNumber);
Page.ClientScript.RegisterExpandoAttribute(clientID, "invalidphonenumber", this.ErrorMessageForInvalidPhoneNumber);
Page.ClientScript.RegisterExpandoAttribute(clientID, "validationgroup", this.ValidationGroup);
Page.ClientScript.RegisterExpandoAttribute(clientID, "errormessage", "");
}
}

protected override bool ControlPropertiesValid()
{
if (!(Page.FindControl(ControlToValidate) is PhoneTextBox))
throw new NotSupportedException("ControlToValidate property must point to PhoneTextBox.");
return true;
}

protected override bool EvaluateIsValid()
{
string value = GetPhoneNumber();
if (IsPhoneRequired)
{
if (string.IsNullOrEmpty(value))
{
_error = ErrorMessageForMissingPhoneNumber;
return false;
}
}

if (!string.IsNullOrEmpty(value))
{
Regex regex1 = new Regex("^\\d{10}$");
if (!regex1.IsMatch(value))
{
_error = ErrorMessageForInvalidPhoneNumber;
return false;
}
}
return true;
}

private string GetPhoneNumber()
{
PhoneTextBox c = (PhoneTextBox)Page.FindControl(ControlToValidate);
return c.PhoneNumber;
}

protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);

if (base.RenderUplevel)
{
this.Page.ClientScript.RegisterClientScriptResource(typeof(PhoneValidator), "PathTo.PhoneValidationFunction.js");
}
}
}



function PhoneValidatorEvaluateIsValid(val) {
var isRequired = val.isphonerequired;
if (isRequired.toLowerCase() == 'true')
{
if ((ValidatorTrim(ValidatorGetValue(val.areacodecontroltovalidate)) == '') || (ValidatorTrim(ValidatorGetValue(val.prefixcontroltovalidate)) == '') || (ValidatorTrim(ValidatorGetValue(val.suffixcontroltovalidate)) == ''))
val.errormessage = val.missingphonenumber;
return false;
}

var pattern1 = /^\d{10}$/;

var value = ValidatorTrim(ValidatorGetValue(val.areacodecontroltovalidate)) + ValidatorTrim(ValidatorGetValue(val.prefixcontroltovalidate)) + ValidatorTrim(ValidatorGetValue(val.suffixcontroltovalidate));

if (value.length > 0)
{
if (!pattern1.test(value))
{
val.errormessage = val.invalidphonenumber;
return false;
}
}
return true;
}

Stay tuned for next week's article. I will be showing an example of how to extend client validation functions (You will find out what it means next week!).

No comments: