Thursday, January 6, 2011

ASP.NET Compile Time Supported Validators

UPDATED VERSION

Click here for the latest blog.

With.NET 3.5 extension methods were introduced, these methods provide a great way built features into 'closed' objects or doing a way of IOC.

Either way, the following example shows the usages a 'simple' validation framework. It provides extensions to validate web controls with compile time support. Which is quiet nice if don't have to write any markup code anymore.
Consider the following code:
protected override void OnInit(EventArgs e)
{
    base.OnInit(e);

    this.UsernameField.Validation().AddRequiredField("username");
    this.PasswordField.Validation().AddRequiredField("password");
}

What you see here are two textboxes that are being checked for a required value. So calling the Validation() method returns a Validation object that is initiated with the textbox and provides several validation methods like 'AddRequiredField'.

On a side note: There is no support for extension properties, that's why syntax is written with parentheses instead of 'this.UsernameField.Validation.AddRequiredField("username")', which I personally would prefer.

Since I wanted to have control over the field name being displayed, it is required to give a name for the field. Any thing else is already configured.

So what advantages are we getting here? In one fluent line the required validation is specified, there is standardized way of creating the validations, readable when you specify validation in one place, very clean markup code(!), there is compile time support between the control and validator. The last one is the most pain when it comes to validation in the markup.

Here is the internal workings of the API. At first there is the static extension class (ControlValidationExtenter), that creates the Validation() extension method for a WebControl. Then there is the ControlValidation class that provides the methods to create the validators.
using System;
using System.Globalization;
using System.Web;
using System.Web.UI.WebControls;
using CuttingEdge.Conditions;

/// <summary> Defines the extension methods for adding validators to controls. </summary>
public static class ControlValidationExtenter
{
    /// <summary> Provides an instance of <see cref="ControlValidation"/>
    /// to add validators the specified control. </summary>
    /// <param name="control">The control to validate.</param>
    /// <returns></returns>
    public static ControlValidation Validation(this WebControl control)
    {
        return new ControlValidation(control);
    }
}

/// <summary> Provides the methods (API) to inject validators in to a page to validate
/// the specified control. </summary>
public sealed class ControlValidation
{
    private readonly WebControl controlToValidate;

    /// <summary> Initializes a new instance of the <see cref="ControlValidation"/> class. </summary>
    /// <param name="controlToValidate">The control to validate.</param>
    public ControlValidation(WebControl controlToValidate)
    {
        Condition.Requires(controlToValidate, "control").IsNotNull();

        this.controlToValidate = controlToValidate;
    }

    /// <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 RequiredFieldValidator AddRequiredField(string friendlyName)
    {
        Condition.Requires(friendlyName, "friendlyName").IsNotNullOrEmpty();

        return CreateValidatorInPage<RequiredFieldValidator>("The field '" + friendlyName + "' is required.");
    }

    /// <summary> Ensures the input is a DateTime value. </summary>
    /// <param name="friendlyName">Name of the friendly.</param>
    /// <returns> The created <see cref="CompareValidator"/> object.</returns>
    public CompareValidator AddDateTimeValidator(string friendlyName)
    {
        Condition.Requires(friendlyName, "friendlyName").IsNotNullOrEmpty();

        var validator = CreateValidatorInPage<CompareValidator>(
           "Please enter a valid date and time for '" + friendlyName + "'.");
        validator.Operator = ValidationCompareOperator.DataTypeCheck;
        validator.Type = ValidationDataType.Date;

        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 CompareValidator AddCompareValidatorForControl(WebControl controlToCompare,
        ValidationCompareOperator compareOperator, ValidationDataType type)
    {
        Condition.Requires(controlToCompare, "controlToCompare").IsNotNull();

        var validator = CreateValidatorInPage<CompareValidator>("The compared control values do not match.");
        validator.ControlToCompare = controlToCompare.ID;
        validator.Operator = compareOperator;
        validator.Type = type;

        return validator;
    }


    /// <summary> Adds a compare validator to compare against string values. </summary>
    /// <param name="valueToCompare"> The value to compare. </param>
    /// <param name="compareOperator"> The compare operator. </param>
    /// <returns></returns>
    public CompareValidator AddCompareValidatorForValue(string valueToCompare,
        ValidationCompareOperator compareOperator)
    {
            return AddCompareValidatorForValue(valueToCompare, compareOperator, ValidationDataType.String);
    }

    /// <summary> Adds a compare validator to compare against integer values. </summary>
    /// <param name="valueToCompare"> The value to compare. </param>
    /// <param name="compareOperator"> The compare operator. </param>
    /// <returns></returns>
    public CompareValidator AddCompareValidatorForValue(int valueToCompare,
        ValidationCompareOperator compareOperator)
    {
        return AddCompareValidatorForValue(valueToCompare.ToString(), compareOperator,
                ValidationDataType.Integer);
    }

    /// <summary> Adds a compare validator to compare against double values. </summary>
    /// <param name="valueToCompare"> The value to compare. </param>
    /// <param name="compareOperator"> The compare operator. </param>
    /// <returns></returns>
    public CompareValidator AddCompareValidatorForValue(double valueToCompare,
        ValidationCompareOperator compareOperator)
    {
        return AddCompareValidatorForValue(valueToCompare.ToString(), compareOperator,
            ValidationDataType.Double);
    }

    /// <summary> Adds a compare validator to compare against DateTime values. </summary>
    /// <param name="valueToCompare"> The value to compare. </param>
    /// <param name="compareOperator"> The compare operator. </param>
    /// <returns></returns>
    public CompareValidator AddCompareValidatorForValue(DateTime valueToCompare,
        ValidationCompareOperator compareOperator)
    {
        return AddCompareValidatorForValue(valueToCompare.ToString(), compareOperator,
            ValidationDataType.Date);
    }

    /// <summary> Adds a compare validator to compare against decimal values. </summary>
    /// <param name="valueToCompare"> The value to compare. </param>
    /// <param name="compareOperator"> The compare operator. </param>
    /// <returns></returns>
    public CompareValidator AddCompareValidatorForValue(
        decimal valueToCompare, ValidationCompareOperator compareOperator)
    {
        return AddCompareValidatorForValue(valueToCompare.ToString(), compareOperator,
            ValidationDataType.Currency);
    }

    /// <summary> Adds a compare validator to compare against custom specified values. </summary>
    /// <param name="valueToCompare">The value to compare.</param>
    /// <param name="compareOperator">The compare operator.</param>
    /// <param name="type">The data type to validate against.</param>
    /// <returns></returns>
    private CompareValidator AddCompareValidatorForValue(string valueToCompare,
        ValidationCompareOperator compareOperator, ValidationDataType type)
    {
        Condition.Requires(valueToCompare, "valueToCompare").IsNotNull();

       var validator = CreateValidatorInPage<CompareValidator>("The compared values do not match.");
        validator.Operator = compareOperator;
        validator.ValueToCompare = valueToCompare;
        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 CompareValidator ValidateAsDataType(ValidationDataType type)
    {
        var validator = CreateValidatorInPage<CompareValidator>(
            "The entered value must be a type of " + type + ".");
        validator.Operator = ValidationCompareOperator.DataTypeCheck;
        validator.Type = type;

        return validator;
    }

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

        this.AddValidatorToPageAfterControlToValidate(validator);

        return validator;
    }

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

        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.controlToValidate.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;
    }
}

Although this API is still in beta, I'd like to share it to keep it as a reminder for further development and usage.

A few things to consider when using this code. The messages put into the validators need to be more specific according to specified settings.

No comments:

Post a Comment