Friday, March 28, 2008

Simple Form Validation - A Reflection based approach

Are you tired of placing multiple Validation controls on Form? If you are bored of following scenario like me, keep on reading the post:

Validators

A simple Email address validation can consist of whether

  • The field is empty
  • Longer than limit
  • Email address format is invalid
  • Already in use

Ordinary solution to this problem is placing multiple validation controls for a single TextBox. You can simply it by replacing all with a single Custom Validator. Our goal is to reduce amount of controls on the form to keep it simple. To do that, we would have to write code for Custom Validator that does it all. We also would like to write minimum code to validate the control without compromising manageability. Let us assume we would write the following code inside the ServerValidate of that control:

protected void cvEmailAddress_ServerValidate(object source, ServerValidateEventArgs args)
{
ValidationController.ValidateControl<ProfileValidator>(cvEmailAddress, ProfileValidator.Fields.EmailAddress.ToString(), args);
}

Let us declare a ValidationErrorResult object that contains error messages and text to display in the UI:

public sealed class ValidationErrorResult
{
public string ErrorMessage { get; set; }
public string Text { get; set; }
}

And an Attribute which would be used to tag a specific method which would be responsible for validation of particular control:

[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class ValidationMethodAttribute : Attribute
{
public ValidationMethodAttribute(string fieldName)
{
this.FieldName = fieldName;
}

public string FieldName { get; private set; }
}

If you are already familiar with Attirbute based programming, I hope you know the attribute of this piece of code is in fact ValidationMethod. We will soon see how to use this. The following is the method that checks the value and make a list of ValidationErrorResult that consists of which rules got failed. Notice that the ValidationMethod attribute contains the field name of the object which determines no matter whatever your method name is, that field name helps Validation controller to find this method out for validation.

[ValidationMethod("Email")]
public static List<ValidationErrorResult> ValidateEmail(object value)
{
var email = value as string;
var results = new List<ValidationErrorResult>();

// Blank
if (string.IsNullOrEmpty(email))
results.Add(new ValidationErrorResult()
{
ErrorMessage = "You did not provide an Email Address.",
Text = "Cannot be left blank"
});

// Length 128
if (email.Length > 128)
results.Add(new ValidationErrorResult()
{
ErrorMessage = "You exceeded length limit.",
Text = "Keep it less than 129 characters"
});

// Valid Email Address
if (!Regex.IsMatch(email, "^[\\w\\.\\-]+@[a-zA-Z0-9\\-]+(\\.[a-zA-Z0-9\\-]{1,})*(\\.[a-zA-Z]{2,3}){1,2}$"))
results.Add(new ValidationErrorResult()
{
ErrorMessage = "You provided an invalid Email Address.",
Text = "Invalid Email Address"
});

// Is Already In Use
if (IsAlreadyInUse(email))
results.Add(new ValidationErrorResult()
{
ErrorMessage = "You provided an invalid Email Address.",
Text = "Invalid Email Address"
});

return results;
}

Here is the ValidationController which goes through the Validation class and looks for the method that has the attribute which validates the control's value.

public class ValidationController
{
public static List<ValidationErrorResult> Validate<T>(string fieldName, object value)
{
var results = new List<ValidationErrorResult>();
var type = typeof(T);
var methods = type.GetMethods(BindingFlags.Static | BindingFlags.Public);

var method = methods.Single<MethodInfo>(delegate(MethodInfo m)
{
return ((ValidationMethodAttribute[])m.GetCustomAttributes(typeof(ValidationMethodAttribute), false))[0].FieldName == fieldName;
});

return (List<ValidationErrorResult>)method.Invoke(null, new object[] { value });
}

public static void ValidateControl<T>(CustomValidator validator, string fieldName, ServerValidateEventArgs args)
{
var results = Validate<T>(fieldName, args.Value);

if (!(args.IsValid = !(results.Count > 0)))
{
validator.ErrorMessage = results[0].ErrorMessage;
validator.Text = results[0].Text;
}
}
}

No comments: