Need Quality Code? Get Silver Backed

Localised Validation

7thJul

0

by Gary H

The logic of validation allows us to move between the two limits of dogmatism and skepticism.

Paul Ricoeur

Validation is important. It's the first line of defence between your pristine data source and the slings and arrows of accidentally fat fingered input. Validation is an integral concept in the .Net framework with the advent of the 3.5 release and the birth of the Data Annotations namespace. Whilst it contains many useful and powerful validators it sometimes falls short in tricky scenarios - like validating an address across different countries. In this article we will look at how we can tackle that whilst remaining sympathetic with the framework.

The Problem

Let's start by framing our problem. We have an MVC web application that has been wildly successful at selling widgets in the UK. So successful in fact that we intend to expand into Europe. Our app uses the Regular Expression attribute to decorate our customers postcodes and to ensure that the postcode format is as expected for the UK. In europe though, the pattern needs to change, take Germany for example where the postcode is represented by a 5 digit number with no letters.

How can we update our app to support different postcode formats for different countries whilst staying with the idiomatic style of the data validation attributes? Ideally we want to mark our postcode field with a [Postcode] attribute and have it work as seamlessly as the existing validators.

The Approach

We will create our own custom validator inheriting the ValidationAttribute so that the framework knows we're in the business of validation. We will also implement IClientValidatable so that we can hook into unobtrusive validation for a more pleasant experience on the UI. Finally, we will add a small lump of JavasScript to handle validation on the client UI. So, lets get started!

The Caveat

Purists will say that "Attributes are metadata! They shouldn't alter functionality!" It's a fine statement and a worthy goal but I see what we're doing more as providing metadata that changes on a case by case basis. If we could do the same thing for regex patterns as we already can for error messages from resources we wouldn't need to do anything like this.

The Attribute and Adapter

Step one is to create the postcode attribute. This is a simple validation attribute using the same techniques as the Regular Expression attribute to validate the contents of a string. We will use Inversion of Control (IoC) to neaten up the aquisition of postcode patterns and to add the dynamism we need. We also create an adapter which will allow us to hook into client side validation.

[AttributeUsage(AttributeTargets.Property | 
AttributeTargets.Field | AttributeTargets.Parameter, 
AllowMultiple = false)]
public class PostcodeAttribute 
	: ValidationAttribute, IClientValidatable
{
	static PostcodeAttribute()
	{
		DataAnnotationsModelValidatorProvider
			.RegisterAdapter(
				typeof(PostcodeAttribute), 
				typeof(PostcodeAttributeAdapter));
	}

	public override bool IsValid(object value)
	{
		var stringValue = Convert.ToString(value);

		if (String.IsNullOrEmpty(stringValue))
		{
			return true;
		}
		var pattern = LoadRegex();

		if (string.IsNullOrEmpty(pattern))
		{
			throw new InvalidOperationException(
			  "Postcode RegEx pattern may not be null or empty");
		}

		var regex = new Regex(LoadRegex());
		var m = regex.Match(stringValue);

		/* We are looking for an exact match, not just a hit. 
		This matches what the RegularExpressionValidator 
		control does */
		return (m.Success 
			&& m.Index == 0 
			&& m.Length == stringValue.Length);
	}

	public static string LoadRegex()
	{
		var userCulture = IocContainerAccess
			.Resolve<IUserCultureProvider>();
		var postcodeRegex = IocContainerAccess
			.Resolve<IPostcodeRegexService>();

		return postcodeRegex
			.GetPatternForCountry(
				userCulture.GetSelectedUserCountry().Id);
	}

	public IEnumerable<ModelClientValidationRule> 
		GetClientValidationRules(
			ModelMetadata metadata, 
			ControllerContext context)
	{
		var rule = new ModelClientValidationRule
		{
			ErrorMessage = FormatErrorMessage(metadata.DisplayName),
			ValidationType = "postcoderegex"
		};

		rule.ValidationParameters["pattern"] = LoadRegex();
			
		yield return rule;
	}
}

public class PostcodeAttributeAdapter 
	: DataAnnotationsModelValidator<PostcodeAttribute>
{
	public PostcodeAttributeAdapter(
		ModelMetadata metadata, 
		ControllerContext context, 
		PostcodeAttribute attribute) 
			: base(metadata, context, attribute)
	{
	}

	public override IEnumerable<ModelClientValidationRule> 
		GetClientValidationRules()
	{
		return new[] { 
			new ModelClientValidationRegexRule(
				ErrorMessage, PostcodeAttribute.LoadRegex()) 
			};
	}
}

Client Side Validation

To complete the picture we use client side validation to hook into unobtrusive validation and provide a cleaner user experience.

(function ($) {
	$.validator.unobtrusive.adapters.add(
		"postcoderegex", 
		["pattern"], 
		function (options) {
			options.messages['postcoderegex'] = options.message;
			options.rules['postcoderegex'] = options.params;
		});

		$.validator.addMethod("postcoderegex", 
			function (value, element, params) {
				var match;
				if (this.optional(element)) {
					return true;
				}

				var reg = new RegExp(params.pattern);
				match = reg.exec(value);
				return (
					match && 
					(match.index === 0) && 
					(match[0].length === value.length));
		});
})(jQuery);

And that's it! The MVC framework recognises the Validator due to it inheriting from the ValidationAttribute. The adapter and client script gives us a nice experience on the client and because we are using the ValidationAttribute as a base we can use resources to provide our error messages.

Find this post useful? Follow us on Twitter

C# , i18n

Comments are Locked for this Post