AspAlliance.com LogoASPAlliance: Articles, reviews, and samples for .NET Developers
URL:
http://aspalliance.com/articleViewer.aspx?aId=1243&pId=-1
Using Localized Resources with Enterprise Library 3 Validation
page
by Brian Mains
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 37335/ 68

Introduction

[Download Sample Code]

Localization and Globalization are key terms in web development these days.  Companies are pushing their sites into global markets, creating a need to develop diverse web sites and client applications.  This requires the developer of an application to plan appropriately in the design and development of a site, which means the developer not only has to understand everything about the programming language and tools with which they are working, they also need to understand the structure and syntax of data in world cultures as well.  This can create many difficulties because it is hard to find that kind of information on the web that is also reliable and accurate.

To illustrate what capabilities you can build into components, I plan to continue my series on Enterprise Library 3 Validation by creating two components that utilize validation in multiple cultures:  ZipCodeValidator and PhoneNumberValidator classes.  You would not believe how hard it can be to plan these appropriately.  I will explain next.

Complexity with Globalization

Phone numbers are very tricky to validate.  The reason is that they come in a wide array of formats.  This is with every culture; for instance, in the United States you have home phone numbers, long distance phone numbers, toll free phone numbers, phone numbers with certain access codes, specialty numbers (like 911), and the like.  This is the same with other cultures; for instance, certain cultures have area codes ranging from two to five digits, with the higher-range area codes being specialty numbers.  This makes it really, really hard to process phone numbers.

However, in the approach I used, I plan to implement phone number validation for regular home phone use, including the multiple formats they appear in.  This makes it easier as a whole to process phone numbers.  Being that phone numbers are strictly digits makes it easier to process too.  That is where complexity for zip codes comes into play.

Zip codes can be alphanumeric, as is the case with Canada.  Canada has a six character/number zip code.  This makes it a challenge, but not as much as a challenge as phone numbers.  It was nice that there were several cultures used in this example that were very similar for zip codes, making it easier to implement.  There are some resources that did make developing these examples easier.

·         http://babelfish.altavista.com/

·         http://en.wikipedia.org/wiki/Lists_of_postal_codes

·         http://en.wikipedia.org/wiki/Telephone_numbering_in_Europe

·         http://www.wtng.info/

I apologize if the resource I used is wrong and that the translated text or the format is a message other than the intended one.

The Reason for Globalization

So some may wonder why bother going to this effort to develop an example for globalization with data that may or may not be exactly correct.  The first is, I wanted to see if I could do it myself, and learn how it all worked.  Secondly, it was a great learning experience about the world, in understanding another culture.  There is something about a challenge in being able to develop a site that could be used internationally.  I think it is a great challenge to be able to integrate this into your components so that it requires less work on the UI developer to implement these features, which is something to think about.

 

In addition, you take your message further by broadcasting it to a greater audience, whether it is a personal, corporate, or community site.  The world really is connected through the internet and a wide spectrum of users is available, meaning a bigger portion of fan base that could be available.

Understanding Assembly Resources

Built into Visual Studio 2005, this tool has the capability to generate localized assemblies automatically.  For our resource files, the default resource is ValidationResources.resx, with each culture defined as a file in the format of ValidationResources.fr.resx (resources in French) or ValidationResources.it.resx (resources in Italian).  These resource files get compiled into a DLL and placed into a language-specific folder in the bin folder.  So, for the Italian resources, there would be a folder \bin\it, with a ValidationResources.resources.dll file in it.

Each of the validation classes use these resource strings and whenever the current culture changes, so does the resource file used for that culture.  This makes it especially useful to target multiple audiences.  In addition, it is possible to create satellite assemblies by creating a separate assembly for each culture that you want to implement in, which is a more advanced topic that we won't discuss in this article.

Zip Code Validator Attribute

The first of two examples will be a zip code validator because zip code validation is easier.  Zip codes use a pattern that does not vary much, like phone numbers do.  Some countries have varying area codes lengths, which affect the number of digits in the rest of the number.  Zip Codes usually do not have that complexity.  I added some additional information, determining whether a zip code could or could not be null, through the following property.

Listing 1

public class ZipCodeValidator: Validator < string >
{
  private bool _allowNulls = false;
 
  public bool AllowNulls
  {
    get
    {
      return _allowNulls;
    }
    set
    {
      _allowNulls = value;
    }
  }
}

The template and validation regular expression are defined in properties, which can be overridden in the derived implementation if desired.  The implementation is below.

Listing 2

protected override string DefaultMessageTemplate
{
  get
  {
    return ValidationResources.Zip_DefaultMessage;
  }
}
 
protected virtual string ZipCodeRegex
{
  get
  {
    return ValidationResources.ZipCodeRegex;
  }
}

I have included information for three cultures shown in this example, which I will expand upon in the future.

Culture

Validation Expression

Default Message Template

France

\d{5}

{1} de '{0} 'n'est pas un code postal valide.

Italy

(IT?-)?\d{5}

{1} di '{0} 'non è un codice di postale valido.

Default

\d{5}

The {1} of '{0}' is not a valid zip code.

The validation of the zip code is actually really simple; an error is added to the validation result collection if the zip code provided is null, empty, or does not meet the validation expression, using the Regex.IsMatch method.  The validation method appears as:

Listing 3

protected override void DoValidate(string objectToValidate, object
  currentTarget, string key, ValidationResults validationResults)
{
  if (string.IsNullOrEmpty(objectToValidate))
  {
    if (!this.AllowNulls)
      base.LogValidationResult(validationResults, this.GetMessage
        (objectToValidate, key), currentTarget, key);
    return ;
  }
 
  if (!Regex.IsMatch(objectToValidate, this.ZipCodeRegex))
    base.LogValidationResult(validationResults, this.GetMessage
      (objectToValidate, key), currentTarget, key);
}

The Regex processing is really simple with the IsMatch static method.  The validation expression for US zip codes (and other five digit countries) is this:  ^\d{5}$.  It is important to have the ^ character at the beginning and the $ at the end because it denotes that the expression processes the whole string, not a portion of it.  If you are unfamiliar with regular expressions, there are many resources available on the internet.

Phone Number Validator

As I mentioned earlier, phone numbers have a lot of challenges.  Phone number digits, in certain countries, shift when the area code has more or less digits.  Phone number area codes could have many different amounts of digits.  There are long-distance codes or out-of-country codes that pose an additional problem.  In addition, phone numbers may contain dashes, but this could also be a space; however, the phone number validation also allows no spacing as well.  The solution right now is to ignore long-distance and specialty codes and target the widest audience of home and business phone numbers.

To counter additional illegal numbers with all of this variation, two length properties are defined: a minimum and maximum length that the phone number range must be between or a validation error is added.

Listing 4

protected virtual int MaximumLength
{
  get
  {
    return int.Parse(ValidationResources.PhoneNumberMaximumLength);
  }
}
 
protected virtual int MinimumLength
{
  get
  {
    return int.Parse(ValidationResources.PhoneNumberMinimumLength);
  }
}
 
protected virtual string PhoneNumberAreaCodeOnlyRegex
{
  get
  {
    return ValidationResources.PhoneNumberAreaCodeOnlyRegex;
  }
}
 
protected virtual string PhoneNumberRegex
{
  get
  {
    return ValidationResources.PhoneNumberRegex;
  }
}

The phone number validation regular expression looks like this: ^((\([2-9][0-8][0-9]\) ?)|([2-9][0-8][0-9][- ]))[2-9][0-9]{2}[- ][0-9]{4}$.  The ^ and $ denote the beginning and end.  Also note that it allows a format of (XXX) XXX-XXXX or XXX-XXX-XXXX.  Lastly, note that with area code rules state that the first digit is 2-9, the second is 0-8, and the third is 0-9, which is similar to the exchange format.  Also, there is an area code regular expression for getting and parsing only the area code, which is used to extract the digits from the area code and match them against the accepted area code list (if provided), which will be discussed in the next section.

These properties can be overloaded if inherited from.  All of these properties use resource strings, as well as the default messages.

Listing 5

protected override string DefaultMessageTemplate
{
  get
  {
    return ValidationResources.PhoneNumberValidator_DefaultMessage;
  }
}
 
protected virtual string DefaultInvalidAreaCodeMessageTemplate
{
  get
  {
    return
      ValidationResources.PhoneNumberValidator_InvalidAreaCodeDefaultMessage;
  }
}
 
protected virtual string DefaultInvalidLengthMessageTemplate
{
  get
  {
    return ValidationResources.PhoneNumberValidator_InvalidLengthDefaultMessage;
  }
}

To provide added functionality, this validator contains an array list of area codes, which can enforce which area codes a phone number can be in.  So, for instance, you can limit the input to a specific range of area code numbers, instead of allowing any area code.

Listing 6

public List < string > AcceptedAreaCodes
{
  get
  {
    if (_acceptedAreaCodes == null)
      _acceptedAreaCodes = new List < string > ();
    return _acceptedAreaCodes;
  }
}

Similarly, if no area codes are in the list, the zip code can be enforced or not enforced through another property called EnforceZipCode.

Listing 7

public bool EnforceAreaCode
{
  get
  {
    return _enforceAreaCode;
  }
  set
  {
    _enforceAreaCode = value;
  }

}
To construct the object, there are many overloads available to provide area codes, specify whether area codes are enforced, and the other default properties (like message and tag); because that is straightforward, I will not show those here.

The validation is not overly complex; however, it does require a lot of thought as to how it will work.  First, we have to determine how detailed error messages are rendered specific to the problem with the data.  This is why there are three default error messages, to expose all of the problems that there could be.  The method that actually renders a message is an overloaded version of GetMessage, which takes a message key (one of the several constant values) and renders the appropriate default or overridden message.

Listing 8

private const string INVALID_AREA_CODE_MESSAGE_KEY = "Area Code";
private const string INVALID_LENGTH_MESSAGE_KEY = "Length";
 
protected virtual string GetMessage(string objectToValidate, string key, string
  messageKey)
{
  if (!string.IsNullOrEmpty(this.DefaultInvalidAreaCodeMessageTemplate) &&
    messageKey == INVALID_LENGTH_MESSAGE_KEY)
    return string.Format(CultureInfo.CurrentCulture,
      this.DefaultInvalidAreaCodeMessageTemplate, objectToValidate, key);
  else if (!string.IsNullOrEmpty(this.DefaultInvalidLengthMessageTemplate) &&
    messageKey == INVALID_AREA_CODE_MESSAGE_KEY)
  return string.Format(CultureInfo.CurrentCulture,
    this.DefaultInvalidLengthMessageTemplate, new object[]
  {
    objectToValidate, key, this.MinimumLength, this.MaximumLength
  }
  );
  else
    return this.GetMessage(objectToValidate, key);
}

The call to it is as simple as below, which we will also see more about later.

Listing 9

this.GetMessage(objectToValidate, key, INVALID_AREA_CODE_MESSAGE_KEY)

Another method is also helpful to use when validating area codes.  For instance, is the character returned from the regular expression an area code character or something like the "(" in the area code of "(724)."  To determine this, the validation method loops through each character and validates the char to determine if it is valid and calls this method.

Listing 10

protected virtual bool IsValidAreaCodeValue(char value)
{
  return Char.IsDigit(value);
}

The validation method, DoValidate, processes the text in several ways.  First, if enforce area code is set to false, it does not validate the area code specifically.  Let us walk through the current validation process.  I validate the length to ensure it is valid.  If not, a length-specific message is rendered to the screen.

Listing 11

if (objectToValidate.Length < this.MinimumLength || objectToValidate.Length >
  this.MaximumLength)
  base.LogValidationResult(validationResults, this.GetMessage(objectToValidate,
    key, INVALID_LENGTH_MESSAGE_KEY), currentTarget, key);

Note the call to the overloaded GetMessage method.  Next, if enforcing the area code and we have a valid regular expression (some countries will not have one because they don’t area codes), we process it specially to ensure it is valid or exists within the range of area codes provided in the list.

Listing 12

if (this.EnforceAreaCode && !string.IsNullOrEmpty
  (this.PhoneNumberAreaCodeOnlyRegex))
{
  if (!Regex.IsMatch(objectToValidate, this.PhoneNumberAreaCodeOnlyRegex))
    base.LogValidationResult(validationResults, this.GetMessage
      (objectToValidate, key, INVALID_AREA_CODE_MESSAGE_KEY), currentTarget,
      key);
  else if (this.AcceptedAreaCodes.Count > 0)
  {
    string areaCodeText = Regex.Match(objectToValidate,
      this.PhoneNumberAreaCodeOnlyRegex).Value;
    string areaCode = string.Empty;
 
    foreach (char pos in areaCodeText)
    {
      if (this.IsValidAreaCodeValue(pos))
        areaCode += pos.ToString();
    }
 
    if (!this.AcceptedAreaCodes.Contains(areaCode))
      base.LogValidationResult(validationResults, this.GetMessage
        (objectToValidate, key, INVALID_AREA_CODE_MESSAGE_KEY), currentTarget,
        key);
  }
}

Lastly, the whole number if validated against a regular expression to ensure that it is correct and meets a specified regular expression match.

Listing 13

if (!Regex.IsMatch(objectToValidate, this.PhoneNumberRegex))
  base.LogValidationResult(validationResults, this.GetMessage(objectToValidate,
    key), currentTarget, key);
Unit Tests

Each validator has an attribute associated with it, but I left it out because there is not anything special done with the attribute classes, other than passing the validator values straight to the validator and exposing the validator properties in the attribute constructor.  Rather, I would like to move to the unit tests that validate the data.  Below are some of the unit tests run against the source and it works successfully.

To make the unit test validation easier, I created a base class that has these two helper methods.

Listing 14

protected void ChangeCulture(string cultureName)
{
  Thread.CurrentThread.CurrentCulture = new CultureInfo(cultureName);
  Thread.CurrentThread.CurrentUICulture = new CultureInfo(cultureName);
}
 
protected List < ValidationResult > GetListOfErrors(ValidationResults results)
{
  return new List < ValidationResult > ((IEnumerable < ValidationResult > )
    results);
}

From this, it makes it easier to process validation results and change the culture for culture-specific tests.  I have three categories of phone number tests: area code tests, correct number tests, and incorrect number tests, each of which is tested for the included cultures provided in the resource files.  A sample of each of the tests is shown below.

Listing 15

[Test]
public void TestEnglishValidatingAreaCodes()
{
  this.ChangeCulture("en-US");
  PhoneNumberValidator validator = new PhoneNumberValidator("717""724""814",
    "610""412");
  List < ValidationResult > results = this.GetListOfErrors(validator.Validate(
    "(724) 555-8712"));
  Assert.IsNotNull(results);
  Assert.AreEqual(0, results.Count);
 
  results = this.GetListOfErrors(validator.Validate("(212) 412-5432"));
  Assert.IsNotNull(results);
  Assert.AreEqual(1, results.Count);
}
 
[Test]
public void TestItalianValidatingCorrectData()
{
  this.ChangeCulture("it-IT");
  PhoneNumberValidator validator = new PhoneNumberValidator(true);
 
  List < ValidationResult > results = this.GetListOfErrors(validator.Validate(
    "23 345 6112"));
  Assert.IsNotNull(results);
  Assert.AreEqual(0, results.Count);
 
  results = this.GetListOfErrors(validator.Validate("234 345 112"));
  Assert.IsNotNull(results);
  Assert.AreEqual(0, results.Count);
}
 
[Test]
public void TestFrenchValidatingIncorrectData()
{
  this.ChangeCulture("fr-FR");
  PhoneNumberValidator validator = new PhoneNumberValidator(true);
 
  List < ValidationResult > results = this.GetListOfErrors(validator.Validate(
    "91 45 65 12 34"));
  Assert.IsNotNull(results);
  Assert.AreEqual(2, results.Count);
 
  results = this.GetListOfErrors(validator.Validate("07 23 34 45 67"));
  Assert.IsNotNull(results);
  Assert.AreEqual(2, results.Count);
 
  results = this.GetListOfErrors(validator.Validate("07 23 34 45 67 56"));
  Assert.IsNotNull(results);
  Assert.AreEqual(3, results.Count);
}

From this, we can ensure the range of tests of valid or invalid data is validated correctly and we can always refactor the tests to include more phone number combinations.  Some other data related points to note from the examples above.

·         Italian phone numbers can have a two-digit area code, which means it has a three digit exchange and a four digit suffix.  But it can have a three-digit area code, which means it has a three digit exchange/suffix.

·         French phone numbers have five sets of two digits, with the area code being a number from 01-09, excluding 07.  As you can see, some of the tests above include 07 phone numbers, which generates an error.

Getting the Source Code

To get a copy of this code, you can download the source off of this site or get the code from the project at the CodePlex web site.  The project name where I put all of my code is called Nucleo.NET and it is a free framework for developing software.  You can download it from http://www.codeplex.com/nucleo, which contains all of the examples above in the Nucleo project, in the Validaton folder, as well as getting access to the Tests in the Nucleo.Tests project, in the Validation folder.

Summary

In this article we have seen how to use localized resources in a custom Validator class with the help of source codes.


Product Spotlight
Product Spotlight 

©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-05-01 7:52:37 PM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search