Monday, April 8, 2013

Improved compiletime supported validators

After a the initial post on the compile-time supporting webform validators, a few itches started to surface. Mainly the extendability and cleanliness. With some slight changes it was possible to categorize the validators (thus enforcing more of the Single Responsibility Principle).

Usage


The following code demonstrates the usage of this API. Note that there are breaking changes if you are replacing the previous version.
protected override void OnInit(EventArgs e)
{
    base.OnInit(e);

    this.UsernameField.Validation().RequiredField("username");
    this.PasswordField.Validation().RequiredField("password");
}
Read the original post for implementation details.
The ControlValidatorInjector is what could be considered the heart of the concept. It's responsible for the injection of the validator controls.
using System;
using System.Globalization;
using System.Web;
using System.Web.UI.WebControls;

public class ControlValidatorInjector
{
 private readonly WebControl control;

 public ControlValidatorInjector(WebControl control)
 {
  this.control = control;
 }

 /// <summary>Creates and adds a specified validator to the page to validate a control. </summary>
 /// <typeparam name="TValidator"></typeparam>
 /// <param name="errorMessage">The error message.</param>
 /// <returns></returns>
 public TValidator AddValidator<tvalidator>(string errorMessage) where TValidator : BaseValidator, new()
 {
  var validator = new TValidator();
  validator.Display = ValidatorDisplay.Dynamic;
  validator.ErrorMessage = errorMessage;
  validator.ControlToValidate = this.control.ID;
  validator.Text = errorMessage;

  this.AddValidatorToPageAfterControl(validator);

  return validator;
 }
 
 /// <summary> Adds the validator to page after control to validate. </summary>
 /// <param name="validator">The validator.</param>
 private void AddValidatorToPageAfterControl(BaseValidator validator)
 {
  var parent = this.control.Parent;
  int indexOfControlInParent = parent.Controls.IndexOf(this.control);

  try
  {
   parent.Controls.AddAt(indexOfControlInParent + 1, validator);
  }
  catch (HttpException ex)
  {
   throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
    "Sorry, you have encountered a rare .NET bug. " +
    "The list of controls that contains the  '{0}', does also contain '<% %>' tags. " +
    "In that case the control collection in '{1}' cannot be modified. " +
    "To solve this problem, you could put the '{0}' control in another parent. {2}",
    this.control.ID, parent.ID, ex.Message, ex));
  }
 }

 /// <summary> Gets the position before the control to validate. </summary>
 /// <param name="control">The control to be validated.</param>
 /// <returns></returns>
 private static int GetPositionBeforeControl(WebControl control)
 {
  // NOTE: Indexed value must be increased by 1.
  return control.Parent.Controls.IndexOf(control) + 1;
 }
}
The ControlExtensions class will add extension methods on your server controls so you can add validation more fluently.
public static class ControlExtensions
{
 /// <summary> Provides validation methods for control validation. </summary>
 /// <param name="control">The control to validate. </param>
 /// <returns> The created validator. </returns>
 public static ControlValidatorInjector Validation(this WebControl control)
 {
  return new ControlValidatorInjector(control);
 }   
}

The ControlValidationExtensions class extends the ControlValidatorInjector so it can use the core directly to add common validators.
public static class ControlValidationExtensions
    {
 /// <summary>Validates an email the address.</summary>
 /// <param name="controlValidator">The control validator.</param>
 /// <returns></returns>
 public static RegularExpressionValidator EmailAddress(this ControlValidation controlValidator)
 {
  var validator =
   controlValidator.AddValidator<RegularExpressionValidator >("E-mail address is not valid.");

  validator.ValidationExpression = 
   @"^(?("""")("""".+?""""@)|(([0-9a-zA-Z]((\.(?!\.))"
   + @"|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-zA-Z])@))(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])"
   + @"|(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,6}))$";

  return validator;
 }

 public static RegularExpressionValidator BasicPassword(this ControlValidation controlValidator)
 {
  var validator =
   controlValidator.AddValidator<RegularExpressionValidator >(
   "The password does not meet the minimum requirements " 
   + "(minimal 6 to 20 characters, at least 1 digit and 1 uppercase letter).");

  validator.ValidationExpression = @"(?!^[0-9]*$)(?!^[a-zA-Z]*$)^([a-zA-Z0-9]{6,20})$";

  return validator;
 }  

 public static RegularExpressionValidator AsUrl(this ControlValidation controlValidator)
 {
  var validator = controlValidator.AddValidator<RegularExpressionValidator >(
   "The URL is not in a valid format (for example: http://www.example.com).");

  validator.ValidationExpression = 
    @"^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_]*)?$";

  return validator;
 }  
 /// <summary> Adds a validator to the control and registers it in the page. </summary>
 /// <param name="control">The control to validate.</param>
 /// <param name="friendlyName">Friendly name of the required field.</param>
 public static RequiredFieldValidator RequiredField(
    this ControlValidation controlValidator, 
    string friendlyName)
 {
  return controlValidator.AddValidator<RequiredFieldValidator>(
    "The field '" + friendlyName + "' is required.");
 }

 public static CompareValidator CompareToControlValue(
  this ControlValidation controlValidator,
  WebControl controlToCompare)
 {
  var validator = controlValidator.AddValidator<CompareValidator>(
    "The compared control value does not match.");
  validator.ControlToCompare = controlToCompare.ID;
  validator.Operator = ValidationCompareOperator.Equal;
  validator.Type = ValidationDataType.String;

  return validator;
 }
 
 /// <summary> Adds a validator to the control and registers it in the page. </summary>
 /// <param name="control">The control to validate.</param>
 /// <param name="validator">The validator to add.</param>
 /// <returns> The created <see cref="CompareValidator"/> object.</returns>
 public static CompareValidator CompareToControl(
  this ControlValidation controlValidator, 
  WebControl controlToCompare,
  ValidationCompareOperator compareOperator,
  ValidationDataType type)
 {
  var validator = controlValidator.AddValidator<CompareValidator>(
    "The compared control value does not match.");
  validator.ControlToCompare = controlToCompare.ID;
  validator.Operator = compareOperator;
  validator.Type = type;

  return validator;
 }

 /// <summary> Adds a validator to the control and registers it in the page. </summary>
 /// <param name="type">The data type to validate.</param>
 /// <returns> The created <see cref="CompareValidator"/> object. </returns>
 public static CompareValidator AsInteger(this ControlValidation controlValidator)
 {
  return ValidateAsDataType(controlValidator, ValidationDataType.Integer);
 }

 /// <summary> Adds a validator to the control and registers it in the page. </summary>
 /// <param name="type">The data type to validate.</param>
 /// <returns> The created <see cref="CompareValidator"/> object. </returns>
 public static CompareValidator AsDateTime(this ControlValidation controlValidator)
 {
  return ValidateAsDataType(controlValidator, ValidationDataType.Date);
 }

 /// <summary> Adds a validator to the control and registers it in the page. </summary>
 /// <param name="type">The data type to validate.</param>
 /// <returns> The created <see cref="CompareValidator"/> object. </returns>
 public static CompareValidator AsCurrency(this ControlValidation controlValidator)
 {
  return ValidateAsDataType(controlValidator, ValidationDataType.Currency);
 }

 /// <summary> Adds a validator to the control and registers it in the page. </summary>
 /// <param name="type">The data type to validate.</param>
 /// <returns> The created <see cref="CompareValidator"/> object. </returns>
 public static CompareValidator AsDouble(this ControlValidation controlValidator)
 {
  return ValidateAsDataType(controlValidator, ValidationDataType.Double);
 }

 /// <summary> Adds a validator to the control and registers it in the page. </summary>
 /// <param name="type">The data type to validate.</param>
 /// <returns> The created <see cref="CompareValidator"/> object. </returns>
 public static CompareValidator AsString(this ControlValidation controlValidator)
 {
  return ValidateAsDataType(controlValidator, ValidationDataType.String);
 }
 
 /// <summary> Adds a validator to the control and registers it in the page. </summary>
 /// <param name="type">The data type to validate.</param>
 /// <returns> The created <see cref="CompareValidator"/> object. </returns>
 private static CompareValidator ValidateAsDataType(
    ControlValidation controlValidator,
    ValidationDataType type)
 {
  var validator = controlValidator.AddValidator<comparevalidator>(
   "The entered value must be a type of " + type + ".");
  validator.Operator = ValidationCompareOperator.DataTypeCheck;
  validator.Type = type;

  return validator;
 }
}

Localization

If you put these classes in your webforms project, it might be interesting to use resources to add multilingualism to the validation.

No comments:

Post a Comment