Extending the CultureInfo Class


As sophisticated as the CultureInfo class and its supporting classes are, it does not cover every globalization issue that your application will encounter. There are more globalization issues that you might need to address that are outside of the scope of the existing CultureInfo class. Such issues include:

  • Postal code formats differ from country to country (not all countries even use a postal code).

  • Address formats differ from country to country.

  • Preferred paper sizes differ from country to country. (Imagine how irritating it would be to users in the United States if their .NET application defaulted to A4 every time it printed.)

  • Units of measure differ from country to country (temperature, distance, volumes, etc.).

Furthermore, the CultureInfo class includes only basic information about the language/country. Information about the following is absent:

  • The country's continent

  • The IANA Top Level Domains used by the country

  • The time zones that span the country

  • The country's International Olympics Committee (IOC) code

  • The International Distance Direct Dialing code used by the country

  • The country's demographics (such as population, literacy, religions)

  • The bumper sticker code used on vehicles of that country

  • The country's capital city (in English and native language)

These examples would all extend a RegionInfo class instead of a CultureInfo class, but you can see that the need exists; it is probably only a matter of time before you find your own reasons why you want to extend the CultureInfo class, so we tackle this issue here. We extend the CultureInfo class in two stages: First we create a new CultureInfoEx class that can be extended. Then we extend it using an example of attaching postal code formats to a culture.

Initially, extending the CultureInfo class looks simple. Simply inherit from CultureInfo and implement the same constructors as the CultureInfo class:

 public class CultureInfoEx: CultureInfo {     public CultureInfoEx(int culture): base(culture)     {     }     public CultureInfoEx(string name): base(name)     {     }     public CultureInfoEx(int culture, bool useUserOverride):         base(culture, useUserOverride)     {     }     public CultureInfoEx(string name, bool useUserOverride):         base(name, useUserOverride)     {     } } 


The problems lie with culture's parents and the invariant culture. Whenever you use the CultureInfo.Parent property, it returns a new instance of a CultureInfo object. So in this example, we start with a new CultureInfoEx object, but when we get its parent, we get a CultureInfo object, not a CultureInfoEx object:

 CultureInfoEx cultureInfo = new CultureInfoEx("en-GB"); CultureInfo parentCultureInfo = cultureInfo.Parent; 


To solve this problem, we need to implement a new Parent property in our CultureInfoEx class:

 public new CultureInfoEx Parent {     get     {         CultureInfo parent = base.Parent;         if (CultureInfo.InvariantCulture.Equals(parent))             return CultureInfoEx.InvariantCulture;         else             // change the type of the parent to CultureInfoEx             return new CultureInfoEx(parent.Name, UseUserOverride);     } } 


The get method starts by getting the base class's Parent. This will be a regular CultureInfo object. We check to see whether this is the invariant culture; if it is, we replace it with our own CultureInfoEx invariant culture (more on this in a moment). If it isn't the invariant, we need to build a new CultureInfoEx object from the name of the original parent CultureInfo object. We also pass the UseUserOverride property to the new CultureInfoEx's constructor, to ensure that it adopts the user's settings if it should do so.

The second problem is the invariant culture. CultureInfo.InvariantCulture returns a CultureInfo object, not a CultureInfoEx object. We want our CultureInfoEx objects to be polymorphic, so the invariant culture must be changed to be a CultureInfoEx object as well. For this, we implement a new static Invariant Culture property:

 private static CultureInfoEx invariantCulture; public new static CultureInfoEx InvariantCulture {     get     {         if (invariantCulture == null)         {             invariantCulture = new CultureInfoEx(0x7f, false);         }         return invariantCulture;     } } 


0x7F is the locale ID of the invariant culture. The InvariantCulture property is simply a wrapper and initializer for the private static invariantCulture field. Unfortunately, in this case, Microsoft is very fond of encapsulation, and encapsulation is opposed to inheritance. Our new invariant culture is not quite the same as CultureInfo.InvariantCulture (apart from the obvious difference in classes). The difference is that the CultureInfo.InvariantCulture is read-only, whereas CultureInfoEx.InvariantCulture is read/write. The field that holds the read-only state is internal and, therefore, prevents inheritance from working effectively. One solution to this problem would be to use Type.GetField to get the FieldInfo for the internal m_IsReadOnly field, and call its SetValue method to set it to true. It is aesthetically unpleasing, but encapsulation often presents inheritors with no other choice.

Now that our Parent and InvariantCulture properties have been implemented, there is one more issue that we should look at. Consider the following code, which starts with a specific culture ("en-US") and walks through its parents ("en" and then the invariant culture), adding the culture names to a list box:

 CultureInfoEx cultureInfo = new CultureInfoEx("en-US"); listBox1.Items.Add(cultureInfo.Name); while (! CultureInfo.InvariantCulture.Equals(cultureInfo)) {     cultureInfo = cultureInfo.Parent;     listBox1.Items.Add(cultureInfo.Name); } 


Notice that the while loop checks to see if the current culture is the invariant culture. More specifically, it checks to see if it is the CultureInfo invariant culture and not the CultureInfoEx invariant culture. You might expect this to either enter an infinite loop or else crash when you get the parent of the invariant culture (although that wouldn't happen because the parent of the invariant culture is the invariant culture). In fact, this code works just the way that you want it to because the test for equality is based on object references and the culture name in the .NET Framework 2.0 and the locale ID (only) in the .NET Framework 1.1. So when you compare a CultureInfo.InvariantCulture with a CultureInfoEx.InvariantCulture, the result is true because the object references/culture names (in the .NET Framework 2.0) or locale IDs (in the .NET Framework 1.1) are the same. This simple fact is essential in successfully extending the CultureInfo class because this test is exactly what the ResourceManager class does when it goes through its resource fallback process: The fallback process stops when it reaches the invariant culture. If the CultureInfoEx.InvariantCulture wasn't equal to the CultureInfo.InvariantCulture, the ResourceManager would enter an infinite loop.

The replacement of the CultureInfo class with the extended CultureInfoEx class lends more weight to the suggestion earlier in this chapter to use a CultureInfoProvider class to provide culture objects. In this case, the overloaded CultureInfoProvider.GetCultureInfo methods would create CultureInfoEx objects instead of CultureInfo objects.

The second stage of extending the CultureInfo class is to provide some new functionality. The example I use is to attach postal code format information to the culture. The PostalCodeInfo class is a simple example that enables us to focus on the model of extending the CultureInfo class instead of the details of postal codes. Postal code formats vary from country to country. The .NET Framework 2.0 MaskedTextBox control has a property called Mask that can be set to a mask to restrict input. This kind of control is ideal for helping with data such as postal codes, which obey a fixed format. The MaskedTextBox even has an Input Mask dialog that offers a set of input masks based upon the current culture of the development machine. Unfortunately, the correct input mask can be determined only at runtime, not at development time. Consequently, we need to have some facility that we can interrogate to get the right format for a culture. Enter the PostalCodeInfo class. This class and its supporting infrastructure in the CultureInfoEx class are loosely modeled on the DateTimeFormatInfo class and its supporting structure in the CultureInfo class. In its simplest form, the PostalCodeInfo class can be used like this:

 PostalCodeInfo postalCodeInfo = new PostalCodeInfo("en-US"); maskedTextBox1.Mask = postalCodeInfo.Mask; 


If you are using Visual Studio 2003, there are at least two controls that offer a similar functionality to the .NET Framework 2.0's MaskedTextBox. The first is Microsoft's Masked Edit Control in msmask32.ocx. To add the control to your toolbox, right-click your toolbox, select Add/Remove Items..., select the COM Components tab, scroll down to the Microsoft Masked Edit Control component, check it, and click OK. The second is the MaskedEditTextBox C# control, which offers similar functionality using regular expressions at http://msdn.microsoft.com/vcsharp/downloads/samples/23samples/default.aspx. See the link entitled "Creating a Masked Edit Control using .NET Framework Regular Expressions with C#."


An overloaded PostalCodeInfo constructor accepts a culture name, and the Mask property contains the correct postal code mask for the locale (a US ZIP code, in this example). This isn't how it is expected to be used, but for now we will just look at how it is implemented and come back to a more common usage in a moment. The PostalCodeInfo class looks like this:

 public class PostalCodeInfo {     protected static Hashtable masks;     static PostalCodeInfo()     {         masks = new Hashtable();         masks.Add("en-US", "00000-9999");         masks.Add("en-GB", "L?90? 9??");         masks.Add("en-AU", "LLL 0000");     }     public static string GetMask(string cultureName)     {         if (masks.ContainsKey(cultureName))             return (string) masks[cultureName];         else             return null;     }     public static void SetMask(string cultureName, string mask)     {         if (masks.ContainsKey(cultureName))             masks[cultureName] = mask;         else             masks.Add(cultureName, mask);     }     public PostalCodeInfo()     {     }     public PostalCodeInfo(string cultureName)     {         this.mask = GetMask(cultureName);     }     private string mask;     public string Mask     {         get {return mask;}         set {mask = value;}     } } 


It has a protected static field called masks, which is a Hashtable of all the masks for every culture. The field is initialized by the static constructor to the "known" values of postal code formats. The list can be modified using the static SetMask method to change incorrect values or to add new cultures that are not part of the original list. This last issue is important for supporting custom cultures that this class cannot know about at design time. The PostalCodeInfo constructor accepts a culture name and performs a lookup in its mask's Hashtable to find the corresponding postal code for the culture name. It assigns this mask to its private mask field, which has a public Mask property wrapper. The class itself is not overly complex. The next step is to make the CultureInfoEx class aware of it in the following addition to CultureInfoEx:

 private PostalCodeInfo postalCodeInfo; public PostalCodeInfo PostalCode {     get     {         CheckNeutral(this);         if (postalCodeInfo == null)             postalCodeInfo = new PostalCodeInfo(Name);         return postalCodeInfo;     }     set {postalCodeInfo = value;} } protected static void CheckNeutral(CultureInfo culture) {     if (culture.IsNeutralCulture)     {         throw new NotSupportedException(             EnvironmentEx.GetResourceString(             "Argument_CultureInvalidFormat",             new object[1] { culture.Name }));     } } 


The private postalCodeInfo field holds a reference to the PostalCodeInfo object associated with the culture. The public PostalCodeInfo property's get method initializes this field. First it calls CheckNeutral to assert that the culture is not neutral. I have taken the approach that a postal code cannot belong to just a languageit can be associated only with a country/region. Then the field is initialized from a PostalCodeInfo object matching the culture name (the CultureInfo.Name property) of the culture.

The normal use of the PostalCodeInfo class, therefore, is more akin to this:

 CultureInfoEx cultureInfo = new CultureInfoEx("en-US"); maskedTextBox1.Mask = cultureInfo.PostalCode.Mask; 


From this simple postal code model, you should be able to extend the CultureInfo class to meet your own globalization requirements.




.NET Internationalization(c) The Developer's Guide to Building Global Windows and Web Applications
.NET Internationalization: The Developers Guide to Building Global Windows and Web Applications
ISBN: 0321341384
EAN: 2147483647
Year: 2006
Pages: 213

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net