ResourceManager Exposed


Before we write our custom resource managers, let's take a moment to explore how the ResourceManager class works internally. Armed with this knowledge, we will be better prepared to write our own resource managers. The following discussion refers equally to the ComponentResourceManager, as the ComponentResource Manager simply inherits from ResourceManager and adds just a single new method without modifying existing methods. See the subsequent section, "Component ResourceManager Exposed," for details that are specific to the Component ResourceManager.

The description of ResourceManager in this section is aimed at the .NET Framework 2.0. ResourceManager in .NET Framework 1.1 is almost identical to that of 2.0, with the exceptions that 1.1 has no concept of a resource fallback location other than the main assembly, and 2.0 includes marginally more stringent checking.

ResourceManager.GetString

Let's start our investigation from the point at which the application code wants to get a resource string. If you want to follow along very closely, you might like to decompile ResourceManager.GetString using Reflector (a .NET decompiler, http://www.aisto.com/roeder/dotnet/), but this isn't necessary to understand how it works.

The ResourceManager.GetString(string) method calls ResourceManager.GetString(string, CultureInfo) and passes null for the CultureInfo. The pseudo code for this method is shown here:

 default culture to CultureInfo.CurrentUICulture get ResourceSet for the given culture from ResourceManager.InternalGetResourceSet if a ResourceSet is returned and ResourceSet.GetString returns a string for the given key     return the string while (culture is not either invariant culture or neutral resources language)     change culture to the culture's parent     get ResourceSet for the culture from     ResourceManager.InternalGetResourceSet     if the ResourceSet is null         return null     if ResourceSet.GetString returns a string for the given key         return the string end while return null 


ResourceManager.InternalGetResourceSet is called to get the ResourceSet for the given culture. A ResourceSet is a set of resource keys and values for a single culture for a single resource name. The ResourceManager keeps a list of resource sets that have been previously found, so the ResourceManager might have a list that includes resource sets for the "en-GB", "en", and invariant cultures. In practice, this list is unlikely to exceed more than three resource sets (i.e., the specific culture, the neutral culture, and the invariant culture) because most applications typically use only one language at a time.

If a ResourceSet is returned, the ResourceSet.GetString(string, bool) method is called to get the string for the given resource key name. If the returned string is not null, the string is returned from ResourceManager.GetString.

ResourceManager.GetString executes a while loop, checking that the culture is not either the invariant culture or the neutral resources culture (obtained from the NeuTRalResourcesLanguageAttribute). The culture is changed to the culture's parent. So if ResourceManager.GetString was passed a culture of "en-GB", then at this point, it would be checking a culture of "en". Similarly, if it was passed "en", then at this point, it would be checking the invariant culture (assuming that the NeuTRalResourcesLanguageAttribute wasn't "en"). So this loop keeps falling back through the culture hierarchy until it gets to either the invariant culture or the neutral resources language.

The loop executes a similar process as before, where it calls Resource Manager.InternalGetResourceSet to get a ResourceSet and calls Resource Set.GetString to get the string for the given resource key name. The only significant difference from the previous process is that if ResourceManager.Internal-GetResourceSet returns null for the ResourceSet, then ResourceManager. GetString returns null.

ResourceManager.GetObject is the exact same code as ResourceManager. GetString, with the sole exception that it calls ResourceSet.GetObject instead of ResourceSet.GetString.

ResourceManager.GetString Example

Suppose that we have a resource key name of "GoodMorning". In the invariant culture, we have given it the value of "Hi", in a small attempt to give the invariant culture as wide an audience as possible. In the English ("en") culture, we have given it the value of "Good Morning". In the English (United Kingdom) ("en-GB") culture, we have given it the value of "Chim chim cheroo, old chap" (which means "Good Morning"). Assume also that we have values for "GoodAfternoon" and "GoodEvening", so the complete resource listing is shown in Table 12.1.

Table 12.1. ResourceManager.GetString Example Data

Resource Key Name

Invariant

English

English (United Kingdom)

GoodMorning

Hi

Good Morning

Chim chim cheroo, old chap

GoodAfternoon

Hi

Good Afternoon

 

GoodEvening

Hi

  


If CultureInfo.CurrentCulture is "en-GB" and we call Resource Manager.GetString("GoodMorning"), the very first call to ResourceManager. InternalGetResourceSet gets the ResourceSet with the anglicized string that is returned immediately. If we call ResourceManager.GetString("GoodAfternoon"), the call to ResourceManager.InternalGetResourceSet gets a ResourceSet for "en-GB", but this ResourceSet doesn't have an entry for "GoodAfternoon", so it continues on to the while loop. The "parent" of "en-GB" is "en", so Resource Manager.InternalGetResourceSet returns a ResourceSet with two entries ("Good Morning" and "Good Afternoon"). The key is found and returned immediately. Finally, if we call ResourceManager.GetString("GoodEvening"), it gets as far as it did for "GoodAfternoon", but the "GoodEvening" key is not found in the "en" ResourceSet, so it goes around the while loop once more, getting the ResourceSet for the invariant culture (i.e., the parent of "en"), and returns "Hi".

ResourceManager Constructors

Before we get into the InternalGetResourceSet method, we need to set the scene and show how a few values used by InternalGetResourceSet get initialized. ResourceManager has three public constructors. All of these result in a resource manager that reads resources from an assembly, so I refer to these as "assembly" constructors. ResourceManager also has the static CreateFileBasedResourceManager method, which calls a private constructor to create a resource manager that reads resources from .resources files in a directory; I refer to this as a "file" constructor. The difference between these types of constructors is saved in two private Boolean fields called UseManifest and UseSatelliteAssem, which are TRue for the public constructors and false for the private constructor. As the UseManifest and Use SatelliteAssem fields are private and always hold the same values, I guess that there was once an intention to take this ResourceManager a little further, but it didn't happen or was undone. The InternalGetResourceSet method uses UseManifest and UseSatelliteAssem to determine where to initialize ResourceSets from.

The "assembly" constructors also set a protected Assembly field called MainAssembly. MainAssembly is the location of the resource. The following are two examples of invoking "assembly" constructors. The first explicitly passes an assembly that gets assigned to MainAssembly. The second passes a type in which the assembly that holds this type gets assigned to MainAssembly.

 ResourceManager resourceManager =     new ResourceManager("WindowsApplication1.Strings",     Assembly.GetExecutingAssembly()); System.Resources.ResourceManager resources =     new System.Resources.ResourceManager(typeof(Form1)); 


The "assembly" constructors also set a private UltimateResourceFallback Location field, _fallbackLoc, to UltimateResourceFallbackLocation. MainAssembly. This field can potentially be changed to SatelliteAssembly later in the InternalGetResourceSet method. The private _fallbackLoc field has a protected property wrapper called FallbackLocation.

The "file" constructor sets a private string field called "moduleDir" to the value passed for the path to the .resources files.

All of the constructors set a protected string field called BaseNameField, which is the name used to identify the resource without the culture suffix or resources extension. In the first example earlier, this would be "WindowsApplication1.Strings"; in the second example, it would be derived from the type and would be "Windows-Application1.Form1" (assuming that the application's namespace is "Windows Application1"). The constructors also set a protected Hashtable field called ResourceSets, which is a cache of ResourceSets that have been found and loaded. Finally, all of the constructors that accept a Type parameter for the ResourceSet type assign this value to a private Type field called _userResourceSet.

ResourceManager.InternalGetResourceSet

InternalGetResourceSet looks through its protected ResourceSets Hashtable for an entry for the given culture. If such an entry exists, it is returned. We cover the rest of this method in two stages: how it works for an assembly-based resource manager and how it works for a file-based resource manager.

Assembly-Based Resource Managers

The pseudo code for the assembly-based resource manager part of Resource Manager.InternalGetResourceSet is:

 if _neutralResourcesCulture is null     assign ResourceManager.GetNeutralResourcesLanguage to it     set _fallbackLoc to fallback location specified in attribute if the given culture is the neutral resources language culture and the fallback location is the main assembly     assign the invariant culture to the given culture if the culture is the invariant culture     if _fallbackLoc is Satellite         assembly = GetSatelliteAssembly(neutral resources culture)     else         assembly = MainAssembly else if TryLookingForSatellite(given culture)     assembly = GetSatelliteAssembly(given culture) else     assembly = null resource 'filename' = ResourceManager.GetResourceFileName load a stream for the given resource 'filename' using Assembly.GetManifestResourceStream if this fails to load     try loading the stream using     ResourceManager.CaseInsensitiveManifestResourceStreamLookup if the stream is not null     create a ResourceSet from the stream using     ResourceManager.CreateResourceSet     add the ResourceSet to the Hashtable     return the ResourceSet return null 


First, InternalGetResourceSet checks whether the private CultureInfo field, _neutralResourcesCulture, is null and, if it is, assigns to it the return result from ResourceManager.GetNeutralResourcesLanguage. The GetNeutralResources Language receives the _fallbackLoc field by reference, and if the assembly has a NeutralResourcesLanguage attribute and the UltimateFallbackLocation has been specified in the attribute, _fallbackLoc is set to the specified location (i.e., either MainAssembly or Satellite). This method reads the NeutralResourc LanguageAttribute from the main assembly. This is one of the reasons why resource managers created by ResourceManager.CreateFileBasedResourceManager do not respect the NeuTRalResourceLanguageAttribute. This is important if you intend to write custom resource managers because it means that, to mimic this behaviour, you need an assembly. Consequently, even if you don't intend to get resources from an assembly, you still need a reference to the assembly to get the attribute from.

If the culture passed to InternalGetResourceSet is the neutral resources language culture and the fallback location is the main assembly, the culture parameter is changed to be the invariant culture.

The next step is to determine which assembly the ResourceManager should use to find the resource. If the culture is the invariant culture, the assembly is found either from calling GetSatelliteAssembly or from the MainAssembly field, depending on whether the fallback location is Satellite or MainAssembly, respectively. Get SatelliteAssembly calls the internal Assembly.InternalGetAssembly method which is the same as the public Assembly.GetSatelliteAssembly method, except that it accepts a Boolean parameter indicating whether to throw an exception if the satellite assembly cannot be found. The Assembly.GetSatelliteAssembly method passes true for this parameter, whereas ResourceManager. GetSatellite Assembly passes false. (If you are having trouble loading a resource assembly that you know has the right name and is in the right place, you should examine this method; the resource assembly must have a name, location, public key, flags, and version that match the MainAssembly.) If the culture is not the invariant culture, it tries to look for a satellite assembly using the tryLookingForSatellite method. If a satellite assembly is not found, the assembly is assigned null.

Having decided upon the assembly, a "filename" is generated using Resource Manager.GetResourceFileName. The "filename" is a concatenation of the Base NameField, the culture name, and "resources", so if the culture is "en-GB", our earlier examples would be "WindowsApplication1.Strings.en-GB.resources" and "WindowsApplication1.Form1.en-GB.resources", respectively.

With the assembly loaded and a "filename" identified, ResourceManager.Inter-nalGetResourceSet now loads the resource using Assembly.GetManifest Resource-Stream. If this fails, it tries its own private CaseInsensitiveManifest ResourceStreamLookup method.

If the stream is not null and the createIfNotExists parameter is TRue (which it always is when called from ResourceManager), it calls ResourceManager.Create ResourceSet(Stream) to create a ResourceSet from the stream, adds the Resourc Set to its Hashtable, and returns it. The ResourceManager.CreateResourceSet method respects the resource set type parameter, which can be passed to the Resource-Manager constructors, so if you specify this parameter, the resource set will be created using your resource set type. Unfortunately, and this is particularly relevant for the custom resource managers in this chapter, you cannot control the resource set creation process itself; if your resource set constructors accept different parameters to the ResourceSet constructors, you will not be able to use this mechanism.

File-Based Resource Managers

The pseudo code for the file-based resource manager part of ResourceManager.InternalGetResourceSet is:

 assert unrestricted FileIOPermission get the filename and path of the resource using ResourceManager.FindResourceFile if a file is found     create a resource set from the file     using ResourceManager.CreateResourceSet     if there is not already an entry in the Hashtable for the culture         add the ResourceSet to the Hashtable     return the ResourceSet if culture is the invariant culture     throw an exception get resource set from ResourceManager.InternalGetResourceSet passing the culture's parent if the resource set is not null and there is not already an entry in the Hashtable for the culture     add the ResourceSet to the Hashtable     return the ResourceSet return null 


Fortunately, the InternalGetResourceSet method is a fair bit simpler for file-based resource managers. After asserting that it has unrestricted FileIO Permission, it calls ResourceManager.FindResourceFile(CultureInfo) to find the name and path of the resource file. FindResourceFile gets the name of the resources file using the ResourceManager.GetResourceFileName method discussed earlier and combines this name with the private moduleDir field set in the constructor. If the resulting file exists, the filename is returned; otherwise, null is returned.

If FindResourceFile returns a filename, it is loaded into a ResourceSet using ResourceManager.CreateResourceSet(String). If there is not already an entry in the Hashtable for the culture, the ResourceSet is added to the Hashtable and returned to the caller.

If FindResourceFile returns a null and the TRyParents parameter is TRue (which it always is when called from ResourceManager), it checks the culture. If it is the invariant culture, it throws an exception because there are no further cultures to check. If it isn't the invariant culture, it calls InternalGetResourceSet again with the culture's parent culture. Clearly, this will continue until either a resource file is found or we have worked our way back to the invariant culture and no resource file is found. The NeutralResourcesLanguageAttribute is never checked because no assembly information is passed to the ResourceManager.CreateFileBasedResourceManager method. This cycling through the parent cultures is, however, redundant because the ResourceManager.GetString method already does this. So what happens in practice is that ResourceManager.GetString calls Internal GetResourceSet, which cycles through the parents and either finds a resource set and returns it, or doesn't find a resource set and throws an exception. Either way, the steps that the GetString method performs to cycle through the parent cultures are not used for file-based resource managers.

ComponentResourceManager Exposed

The .NET Framework 1.1 and 2.0 both include the System.ComponentModel. ComponentResourceManager class. This class inherits from ResourceManager and adds the ApplyResources method used in Visual Studio 2005 Windows Forms applications. Although the class is available in the .NET Framework 1.1, Visual Studio 2003 never uses it. If you are writing Visual Studio 2005 Windows Forms applications, this section is for you.

As you know, the ApplyResources method uses reflection to assign all of the values in a resource to a component. To do this, it first reads all of the entries in the resource into a SortedList and then iterates through those entries, applying them to the given object. This section explains how it works.

The ApplyResources method has two overloads, in which the first calls the second and passes null for the CultureInfo parameter:

 public void ApplyResources(object value, string objectName) public virtual void ApplyResources(     object value, string objectName, CultureInfo culture) 


ApplyResources defaults the CultureInfo parameter to CultureInfo. CurrentUICulture. ComponentResourceManager has a private field called ResourceSets, which is a Hashtable of SortedLists. ApplyResources calls a private FillResources method to fill ResourceSets with data. Each entry in the Hashtable represents the complete "flattened" resources for a given culture for the resource. So if the culture is en-GB, for example, the Hashtable will contain one entry keyed on the en-GB CultureInfo object. The value of this entry will be a Sort-edList containing all of the resources from the fallback culture after the resources for the en culture and en-GB culture have been applied in succession. So if your form's InitializeComponent method calls ApplyResources for the Button1 component, all of the resources for en-GB (and, therefore, the en and invariant cultures) for Form1 will be loaded. Obviously, this is wasteful in the context of just a single component, but it is efficient if ApplyResources is subsequently called to initialize all of the other components on the form (which it is in InitializeComponent).

Having retrieved a suitable SortedList containing all of the flattened resources for the given culture, ApplyResources iterates through the entries in the SortedList. For each entry, it looks for a property of the given component that has the same name as the key and the same type as the value. So if the objectName passed to ApplyResources is "Button1" and the entry name is "Button1.Text", ApplyResources looks for a property of the given object called "Text" that is the same type as the value of the resource entry (i.e., almost certainly a string). It uses TypeDescriptor.GetProperties to find the property and checks its type using PropertyDescriptor.PropertyType. If a match is found, the value is assigned using PropertyDescriptor.SetValue.

The good news from the point of view of writing custom resource managers is that the FillResources method calls GetresourceSet to create and fill Resource Sets, and GetresourceSet calls InternalGetResourceSet, and this is the method that we override. So if you inherit from ComponentResourceManager and override its InternalGetResourceSet method, the ApplyResources method will behave correctly without any change.




.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