Globalization and Localization Support

The Compact Framework includes support for globalizing applications and then localizing them for specific cultures and regions. Like many things in the Compact Framework, the features to accomplish this are a subset of those found in the desktop Framework. Specifically, when designing and implementing a world-ready application, there are three primary features of the Compact Framework that must be understood: cultures, localized data, and the use of satellite assemblies.

Understanding Cultures

In order to identify various cultures and regions, the Compact Framework and its desktop cousin both use the hierarchical nomenclature laid out in RFC 1766. This document specifies that each language code is specified by a two letter code[2] that acts as the parent of a two- or three-letter country or region code.[3] Thus, "en" is the code for English and "US" is the code for the United States. Together the culture (or locale) name "en-US" is used to refer to English, United States. Table 8-1 lists some of the western European culture names. These names are hierarchical in nature in the form language/country or region.

[2] Derived from ISO 639-1

[3] Defined in either ISO 3166 or ISO 639-2

Table 8-1. Culture Names

Culture Name

Language?Country/Region

Locale/Culture Identifier

En

English

0x0009

en-US

English?United States

0x0409

en-GB

English?United Kingdom

0x0809

en-CA

English-Canada

0x1009

Fr

French

0x00C

fr-FR

French-France

0x040C

fr-CA

French-Canada

0x0C0C

De

German

0x0007

de-AT

German-Austria

0x0C07

de-DE

German-Germany

0x0407

As you can see from Table 8-1, the bilevel nature of the culture names allows for those cases where only a language group, such as English, is identified and others where a country, such as Canada, where both English and French are spoken and which contains regional differences in those languages, is identified. As a result, the culture names without country or region specified are referred to as neutral, whereas the presence of country or region code makes them specific.[4] In addition to specifying the language, a culture name also identifies the formats for time, currency, and numeric data, as well as the calendar used.

[4] There are some exceptions. For example, "zh-CHS" refers to simplified Chinese, which is a neutral culture. A complete list of cultures supported by the desktop Framework can be found in the Visual Studio .NET 2003 online help.

You'll also notice that Table 8-1 includes the locale/culture identifier (LCID) that uniquely identifies the culture to the Compact Framework and that is also used by Windows operating systems such as Windows XP and Windows CE. For example, the Pocket PC 2002 supports over 90 cultures. The culture to use on a device such as the Pocket PC 2002 can be chosen from the Regional Settings application as shown in Figure 8-1.

Figure 8-1. Setting the Culture. The culture to be used on the device may be set from the Regional Settings application. Note that the culture identifies not only the language, but also the number, currency, time, and date formats that can be customized by the user.

graphics/08fig01.jpg

In addition to neutral and specific cultures, the Compact Framework also defines the invariant culture, which is identified by an empty string ("") and LCID 0x007F, is associated with the English language, and is used to represent culturally nonsensitive data. Conceptually, the invariant culture is the parent of all the neutral cultures.

Retrieving and Setting the Culture

In the Compact Framework, as in the desktop Framework, cultural information is represented by the CultureInfo class in the System.Globalization namespace. This class contains static (shared in VB) methods to retrieve the CurrentCulture and the CurrentUICulture. Both properties are set based on the regional settings defined on the device as shown in Figure 8-1. Although both of these properties are set identically, the CurrentCultureUI property is present because operating systems that have a multilanguage user interface (MUI), such as special versions of Windows 2000 and Windows XP, can set this property to differ from CurrentCulture.

To determine the current culture programmatically, a developer can access the methods and properties of the CultureInfo object exposed by the CurrentCulture property. For example, to print the English name of the current culture, its date-time format, and whether it is a neutral culture, the following code could be added to an SDP:


MessageBox.Show(CultureInfo.CurrentCulture.EnglishName)
MessageBox.Show( _ CultureInfo.CurrentCulture.DateTimeFormat.FullDateTimePattern)
MessageBox.Show(CultureInfo.CurrentCulture.IsNeutralCulture.ToString())

graphics/key point_icon.gif

However, in the Compact Framework the CurrentCulture property is read-only, unlike in the desktop Framework, where it can be modified and where each thread can execute using a different culture, as exposed through the CurrentCulture property of the System.Threading.Thread class. This restriction is in place because smart devices such as the Pocket PC are single-user devices and, therefore, don't require multiple threads of execution that service different clients to use different cultures.

However, a developer can create instances of the CultureInfo class and then store the instances in class variables for use in the application (for example, if the device is shared by multiple users[5]). This technique can be used to allow the user to switch the culture without changing the regional settings, or when the default culture for a specific user is stored in a database. For example, to create a business class that can be customized to use different cultures dynamically, the constructor of the class can be overloaded to accept a culture name, LCID, or a CultureInfo object as shown in Listing 8-1.

[5] This may especially be the case when the application is used in regions such as Belgium, where multiple official languages are the norm.

Listing 8-1 Using Specific Cultures. This listing shows a business class that accepts a specific culture in its constructor and stores that culture in a private class variable.
Public Class SomeBusinessClass

    Private _culture As CultureInfo

    Public Sub New()
        ' Do other work
        _culture = CultureInfo.CurrentCulture
    End Sub

    Public Sub New(ByVal culture As String)
        ' Do other work
        Try
            _culture = New CultureInfo(culture, True)
        Catch e As PlatformNotSupportedException
            ' Default to current culture
            _culture = CultureInfo.CurrentCulture
        End Try
    End Sub

    Public ReadOnly Property Culture() As String
        Get
            Return _culture.EnglishName
        End Get
    End Property

    ' Other overloaded constructors can accept LCID or a CultureInfo
End Class

In this case the default constructor simply picks up the current culture from the CurrentCulture property, while the parameterized constructor accepts the culture name. Note that the CultureInfo class will throw a PlatformNotSupportedException if the culture name is not valid for the Compact Framework or for the underlying operating system. Finally, you can see that when the CultureInfo object is created, the second argument to this version of the constructor specifies whether to use the user-customized values for the culture. By specifying True, as in this case, if a user has customized the time format in the regional settings on the device, for example, those settings will be used. By specifying False, the Compact Framework will use its default values.

As a result, the class could be instantiated as follows to use the en-CA (English-Canada) culture:


Dim biz As New SomeBusinessClass("en-CA")

NOTE

This technique of implementing a constructor in a class that sets the culture to be used can also be employed to great effect in an abstract or base class. In this way, all derived classes (for example, those in the data-services or business-services layers) can benefit from this functionality.


Localizing Data

One of the important steps developers must take when globalizing an application is to check how data (dates and times, strings, currency, and other numbers) is displayed to the user. Fortunately, the Compact Framework does much of the work based on the current culture, although there are a few items that architects and developers need to be aware of.

Handling Dates and Times

Displaying dates and times (as represented by the System.DateTime data type) appropriately when different culture settings are used is handled automatically by the Compact Framework, when the ToString, ToShortDateString, ToShortTimeString, ToLongDateString, or ToLongTimeString methods are called on a DateTime object.[6] For example, when the culture is set to en-US, the ToShortDateString method returns 4/8/2003; however, when en-CA is used, the date is displayed as 08/04/2003.[7]

[6] When the ToString method is called, the resulting string contains the concatenation of the short date and short time strings.

[7] The four-digit year is returned here even though in Windows CE, two-digit years are the default for short dates. This is because the Compact Framework has its own set of default values that override those of the underlying operating system.

Developers can also format dates and times using a specific culture by using the overloaded ToString method. One of the overloads accepts a format string, while another accepts an object that implements the IFormatProvider interface. The CultureInfo class exposes a DateTimeFormat property (actually a DateTimeFormatInfo object) that implements the IFormatProvider interface and then exposes string properties, such as ShortDatePattern and FullDateTimePattern, that return the various patterns for dates and times for the specific culture. By passing one of the pattern properties, or simply a string that contains a custom pattern, to the ToString method, the DateTime object is formatted. By passing the DateTimeFormatInfo object to ToString, the short date is used. Additionally, the ToString method accepts one-character identifiers that map to the various patterns; for example, "d" maps to the ShortDatePattern, while "t" maps to ShortTimePattern. The pattern and the DateTimeFormatInfo objects can be used together in ToString to produce a formatted DateTime value for a specific culture:


string s = startDate.ToString("d", CultureInfo.InvariantCulture);

In the reverse, the DateTime class exposes Parse and ParseExact methods that can be used to convert a formatted string to a DateTime value. By passing the format string or the DateTimeFormatInfo object to the methods along with the string, the DateTime will be created.

For example, the class shown in Listing 8-1 could include a StartDateString property like the following:


Private _startDate As DateTime

Public Property StartDateString() As String
    Get
       Return _ _startDate.ToString(_culture.DateTimeFormat.ShortDatePattern)
    End Get
    Set(ByVal Value As String)
       _startDate = DateTime.Parse(Value, _culture.DateTimeFormat)
    End Set
End Property

The property shown in this snippet accepts a string and uses the DateTime.Parse method to interpret the string using the ShortDatePattern, which is the default when the DateTimeFormatInfo object is passed to the method. If the string is invalid for the format, a FormatException will be thrown. The property then returns the DateTime formatted as a short date using the ToString method.

graphics/key point_icon.gif

A second issue that arises when handling date and time values in a world-ready application is that different users operate in different time zones. For example, you can easily foresee a scenario where a DateTime value is input by a user on a device with its clock set to use U.S. central standard time, and the device synchronizes with a SQL Server using merge replication. Later, another device operating in U.S. eastern standard time pulls down the record using merge replication and displays it to the user. In this case, the user in the eastern time zone will not be viewing a local time, and, unless the application has some other means of tracking where the date was recorded, it will be difficult to reconcile the problem.

To handle this situation the DateTime value should be stored in SQL Server CE as a universal DateTime value. And, as a best practice, the DateTime value should be translated to a universal format anytime the value is persisted. Fortunately, the Compact Framework makes this easy by exposing the ToUniversalTime and ToLocalTime methods on the DateTime structure. These methods convert local DateTime values to DateTime values that use coordinated universal time (UTC) or Greenwich mean time (GMT) and back again. This universal value can then be stored in SQL Server CE's shortdatetime and datetime data types and, when read back, converted to the local time for display.

The DateTimeFormatInfo object also contains a UniversalSortableDateTimePattern property that can be used with the ToString method to create a string that can be persisted and still sorted appropriately. Of course, the patterns associated with the InvariantCulture can also be used with ToString to produce culture-independent strings.

As an example of storing DateTime values in a universal format, the StartDateString property of the class shown in Listing 8-1 could be amended to store the start date internally as a universal date time, so that when it was saved to a database or file, it would already be in the universal format.


' Stored as universal datetime
Private _startDate As DateTime

Public Property StartDateString() As String
    Get
        Return _startDate.ToLocalTime.ToString( _
         _culture.DateTimeFormat.ShortDatePattern)
    End Get
    Set(ByVal Value As String)
        _startDate = DateTime.Parse(Value, _
         _culture.DateTimeFormat).ToUniversalTime
    End Set
End Property

Now, you'll notice that the ToUniversalTime method is called when the string is parsed, in order to save the DateTime value as universal, and the ToLocalTime method is called to convert it back to the time zone configured on the device for display.

The final issue to consider in terms of localizing date and time information is the use of calendars. Simply put, each culture as represented by a CultureInfo object exposes a Calendar property that contains an object derived from Calendar. The Compact Framework supports only calendars based on the Gregorian calendar (the one used in the United States and the Western world), which includes the GregorianCalendar, JapaneseCalendar, ThaiBuddhistCalendar, KoreanCalendar, and TaiwanCalendar. The desktop Framework additionally supports non-Gregorian calendars, such as the Julian, Hebrew, and Hirij calendars. For Gregorian-based calendars, typically only the year number and era differ. Each calendar object supports methods and properties to return the year, era, and other information. For example, to return the current year using the ThaiBuddhistCalendar, the following code could be written to return 2546 for the Gregorian year 2003.


ThaiBuddhistCalendar c = new ThaiBuddhistCalendar();
MessageBox.Show(c.GetYear(DateTime.Now));

As discussed previously, the best practice for dealing with varying calendars is to accept input based on the calendar associated with the current culture and then persist the value in the universal format. When converting back again using the ToLocalTime method, the correct calendar will be used to display the year and era.

Handling Strings

As with date and time values, the Compact Framework automatically takes into consideration the current culture when dealing with strings in terms of fonts, comparisons, and character casing. For example, when the String.Compare, Array.Sort, TextInfo.ToUpper, and TextInfo.ToLower methods are called, the current locale is consulted. The information about string comparisons is stored in the CompareInfo property of the CultureInfo object, which exposes methods that are used by methods such as String.Compare.

NOTE

As you might have guessed, if the application needs to perform string comparisons that are culture independent (for example, those that are used for security to activate or deactivate features in the application), the InvariantCulture.CompareInfo methods should be used so that the behavior will be consistent, regardless of the current culture.


graphics/key point_icon.gif

However, because string comparisons and, therefore, sort orders differ between cultures,[8] a world-ready application should perform sorting in the application code rather than in a database like SQLCE. For example, rather than return a sorted SqlCeDataReader using a SQL statement (described in Chapter 5) with a WHERE clause that is then used to bind to a ComboBox, a developer would want to read the data into an ArrayList and then sort the data based on the culture before binding the ArrayList to the ComboBox, as follows:

[8] For example, the umlaut character (as in Ä), sorts differently in different cultures.


ArrayList ar = new ArrayList();

// Read the data into an ArrayList
while (dr.Read())
{
    ar.Add(dr["ProductName"]);
}

// Sort using the current culture automatically
ar.Sort();

// Bind
ComboBox1.DataSource = ar;

This is the best practice because using a WHERE clause relies on the collation defined for the SQLCE database. Because SQLCE's collation may cause the sort order to differ from that of the culture and because SQLCE does not support case-sensitive sorting in any case, the resulting sort order will not be the same as for the culture.

In addition to relying on the current culture, developers can make culture-specific comparisons using the overloaded versions of the String.Compare, TextInfo.ToUpper, and TextInfo.ToLower methods. For example, if a class contained a private variable that referenced the culture, the following code could be written to compare two strings:


Dim intResult As Integer = _
  String.Compare(productA, productB, False, _culture)

In this case, the string comparison is done based on the CultureInfo object referenced in the _culture variable; it returns ?1 if the first string should be sorted before the second, 1 if the second should be sorted before the first, and 0 if the strings are equal. The second argument in the call to Compare indicates that the comparison should be case sensitive.

This concept can be put to more general use when developers create custom classes used to represent data. For example, the simple Player class used to hold information about baseball players, shown in Listing 8-2, implements the IComparable interface and, hence, the CompareTo method. As with the class shown in Listing 8-1, it accepts a specific culture in one of its overloaded constructors and stores it in a class variable. When instances of the Player class are placed into an Array or ArrayList and sorted, the CompareTo method is invoked, and in this case uses the String.Compare method to specify that the collection should be sorted on the player's name in a case-insensitive fashion, using the appropriate culture settings.

Listing 8-2 Sorting Custom Classes. This listing shows a custom class that implements the IComparable interface to do custom sorting based on the culture the class uses.
Public Class Player
    Implements IComparable

    Public Name As String
    Public AB, R, H, D, T, HR, RBI, BB, SB As Integer

    Private _culture As CultureInfo

    Public Sub New(ByVal name As String)
        Me.Name = name
        _culture = CultureInfo.CurrentCulture
    End Sub

    Public Sub New(ByVal name As String, ByVal culture As CultureInfo)
        Me.Name = name
        _culture = culture
    End Sub

    ' Used when the Sort method of the collection is called
    Public Function CompareTo(ByVal o As Object) As Integer _
     Implements IComparable.CompareTo
        Dim p As Player = CType(o, Player)
        Return String.Compare(Me.Name, p.Name, True, _culture)
    End Function

End Class

TIP

More general techniques for implementing sorting for custom classes using strongly typed collection classes and custom sort classes can be found in the article listed in the "Related Reading" section at the end of this chapter.


Handling Currency and Other Numbers

The support for formatting and displaying numeric data for specific cultures in the Compact Framework is analogous to that for dates and times. The CultureInfo object exposes a NumberFormat property that references an instance of the NumberFormatInfo class. This class contains all the properties that together define how numeric data should be displayed.

As with the DateTime structure, the numeric data types shown in Table 2-2, including System.Int32, System.Double, System.Decimal, and System.Single, expose an overloaded ToString method that by default uses the current culture, but they can also accept a format string, an IFormatProvider, or both. The overloaded ToString method can then be used by developers to format numeric data for specific cultures. For example, the following code snippet uses the French (France) culture to display a System.Double and would in this case display the value 0,302:


CultureInfo c = new CultureInfo("fr-FR");
double avg = 0.302;

MessagBox.Show(avg.ToString(c));

This works because the CultureInfo class implements the IFormatProvider interface. Alternatively, hard-coded format strings can be passed to the ToString methods where "g" (or "G") represents a general number, "d" decimal format, "e" floating point, "c" currency, "x" hexadecimal, and "n" number format. And so, for example, the line of code below produces the same result as that above:


MessageBox.Show(avg.ToString("g", c));

Unlike the pattern properties for the DateTimeFormatInfo class, however, the pattern properties for NumberFormatInfo are integers, rather than strings, and map to hard-coded ways of representing numbers. For example, the NumberNegativePattern property can be set to numbers from 0 to 4 and determines where the negation symbol is placed relative to the number. These properties can be modified on the fly and will therefore affect how the numbers are displayed.[9]

[9] Consult the VS .NET 2003 online help for the complete list of these mappings.

A developer might then use the ToString method to provide a read-only property on a class that returns the culture appropriate format for a numeric value. For example, in the Player class shown in Listing 8-2, the developer might create a read-only Avg property that calculates and returns the player's batting average like so:


Public ReadOnly Property Avg() As String
    Get
        Return (H / AB).ToString("g3", _culture)
    End Get
End Property

In this case, the format string "g3" is used to show only the first three decimal places, as is standard with batting averages.

As with DateTime values, the reverse can also be done using the Parse method exposed by the various numeric data types. This overloaded method (in addition to accepting the string value to be converted) can accept one or more values from the System.Globalization.NumberStyles enumeration, which specifies what is permitted in the string value and an IFormatProvider object. This method can be used, for example, to convert currency values captured as strings to System.Decimal values:


Dim salary As Decimal
salary = Decimal.Parse(txtSal.Text, NumberStyles.Currency)

In this case the current culture will be used, but, optionally, a CultureInfo object could be passed as the third argument.

On its face, handling currency values in world-ready applications is no different than handling other numeric data. The "c" format string allows for formatting a numeric value as currency, and the currency properties of the NumberFormatInfo class, such as CurrencyPositivePattern and CurrencySymbol, contain the formatting information; however, unlike other numbers that should be automatically formatted for the specific culture, currency values, once assigned, are usually always associated with the particular currency. This is the case, of course, because "$1,567.55" and "£1,567.55" are not equivalent, and their true values relative to each other fluctuate dynamically. As a result, unless the developer has access to currency exchange rates (which on a smart device will rarely if ever be the case, although a judicious use of Web Services would solve this problem), currency values should be displayed in the currency in which they were entered. This entails persisting the currency in a database such as SQLCE or simply forcing all currency values to be entered in a standard way, for example, using U.S. dollars.

graphics/key point_icon.gif

The other issue that developers need to be aware of when using currency values is the use of the euro. The Compact Framework defaults the respective currency symbols to "graphics/jukcy.gif " for nations that have adopted the euro, including France, Germany, Belgium, Spain, Greece, Italy, and Austria, among others. The confusing aspect is that the various operating systems on which the Compact Framework runs may default the currency symbols to their national currencies, including "F" for France, "DM" for Germany, and "pta" for Spain. For example, the Pocket PC 2000 devices will default to the national currencies, while Pocket PC 2002 will default to the euro. To insure that the euro is used when dealing with specific cultures, the second argument to the CultureInfo constructor can be set to False to indicate that the regional settings on the device are to be ignored, like so:


Dim c As CultureInfo = New CultureInfo("fr-FR", False)

Using Resources and Satellite Assemblies

The key to separating the UI from the code in a world-ready application is to use resource files. Fortunately, the Compact Framework, like its desktop cousin, does much of the work for developers by automatically creating resource-specific assemblies (complete with a fallback system), referred to as satellite assemblies, which are culture-specific and can be deployed with the application. The Compact Framework also provides the ResourceManager class in the System.Resources namespace to read these resources from the satellite assemblies.

Creating Resource Files

As many developers are aware, resources are simply string or encoded binary values that can be collected in a resource file associated with a project. In Visual Studio .NET 2003, these resource files have a .resx extension and are stored as XML. Developers can create them simply by choosing to add a new item to a project and choosing Assembly Resource File. The resulting resource file can then be edited through a built-in resource editor, as shown in Figure 8-2.

Figure 8-2. Editing a Resource File. This screen shot shows the VS .NET IDE's resource editor. It can be used to add simple string resources.

graphics/08fig02.jpg

Although the built-in resource editor can be used to add string resources, to add binary resources developers may instead want to use the ResEditor sample application that ships with VS .NET 2003. This application, along with several other localization samples, ships in the \Program Files\Visual Studio .NET 2003\SDK\v1.1\samples\tutorials directory. This more robust editor works in a stand-alone fashion and can be used to add bitmaps, icons, strings, and image lists to a resource file.[10]

[10] For example, there is another sample called ResXGen which generates a resource file from a single image.

After the resource file has been created and the resources added to the file, VS .NET will automatically compile the resources into a .resource file and embed them in the application's assembly as a type when it is built, because its Build Action property, shown in the properties window, will default to Embedded Resource. By default, the resources in this file are the fallback resources. In other words, if culture-specific resources are not found, those in the fallback .resx file compiled into the assembly will be used.

graphics/key point_icon.gif

To create culture-specific resources to perform the task of localization, a developer would then create a resource file for each culture that contains the same resource identifiers as those in the fallback resource file, name the file with the same name as the fallback .resx file, and append the culture name. For example, if the fallback resource file was named MyResources.resx, the resource file containing French resources (a neutral culture) would be called MyResources.fr.resx, and those for the specific culture French (Canada) would be MyResources.fr-CA.resx. As mentioned previously, these resource files should contain culture-specific strings, such as error messages, prompts, labels, titles, and button captions, as well as any bitmaps, icons, and other images that vary.

By simply creating these culture-specific resource files, at build time VS .NET will create a subdirectory with the culture name under the appropriate directory (depending on whether the Debug or Release build is chosen) for each culture-specific resource file. In this directory it places a satellite assembly that contains only the resources in the culture-specific resource file. In the example used above, VS .NET would create an fr-FR directory into which it would place the satellite assembly. As a result, the satellite assembly can be deployed with the application using the same relative pathing structure.

Retrieving Resources

Creating the culture-specific or localized resource files is only half the battle, however. The resource must then be extracted from the resource files at runtime by application code that is globalized. This can be accomplished using the ResourceManager class.

To use the ResourceManager class, a developer needs to create an instance of the class and pass overloaded constructor information to it about the resources to retrieve. For example, the following code could be used to create a ResourceManager instance needed to retrieve resources from the MyResources.resx file:


Dim res As New ResourceManager("MyAppName.MyResources", _
  [Assembly].GetExecutingAssembly)

In this case the constructor accepts the fully qualified type of the resources, which defaults to the root (or default) namespace of the project (here, MyAppName) and the name of the resource file (MyResources). The second argument is a Type object that identifies the assembly in which to find the resources, in this case simply the currently executing assembly.

TIP

One of the overloaded constructors of the ResourceManager class allows developers to use their own custom resource file format. This can be done by passing a reference to the type of a class derived from ResourceSet that developers can use to read and write their own formats.


Once a reference to a ResourceManager object has been created, its GetString and GetObject methods can be used to extract the case-sensitive string and binary resources from the file. For example, if there is a string resource called "frmMainCaption" in the resource file, the following line of code would be used to extract it and set the form's caption:


frmMain.Text = res.GetString("frmMainCaption")

In an analogous way, binary resources such as images can be read using the GetObject method:


pbLogo.Image = CType(res.GetObject("Logo"), Image)

In this case the Image property of a PictureBox control named pbLogo is populated. Because GetObject returns the type System.Object, it must be cast to the Image data type.

As with the methods used to localize strings and numbers, both the GetString and GetObject methods are overloaded to support passing in a specific culture using a CultureInfo object, as in the following snippet, where _culture holds a reference to a CultureInfo object.


pbLogo.Image = CType(res.GetObject("Logo", _culture), Image)

From a design perspective, since the ResourceManager instance may need to be available to multiple forms or classes, a good technique would be to expose the instance as a Singleton object using the Singleton design pattern discussed in Chapter 7. A typical Singleton class might look like that shown in Listing 8-3.

Listing 8-3 Exposing a ResourceManager. This class implements a Singleton that exposes the ResourceManager object in a shared property.
Public NotInheritable Class MyResources

    Private Shared _res As ResourceManager

    Private Sub New()
    End Sub

    Shared Sub New()
        ' Create the instance of the resource manager
        _res = New ResourceManager("MyAppName.MyResources", _
         [Assembly].GetExecutingAssembly)
    End Sub

    Public Shared ReadOnly Property Instance() As ResourceManager
        Get
            Return _res
        End Get
    End Property

End Class

In Listing 8-3 the MyResources class exposes a single shared (static in C#) property called Instance that is initialized in the shared constructor and that exposes the ResourceManager reference. In this way, the remainder of the application need not create ResourceManager objects and can instead use the Singleton as shown here:


frmMain.Text = MyResources.Instance.GetString("frmMainCaption")

Another technique for architects to consider is to encapsulate as much as possible of both the ResourceManager class and calls to GetString and GetObject. For example, a UI helper class might expose read-only properties whose Get blocks use the appropriate ResourceManager object and make calls to GetString to retrieve the localized resources.