SO FAR, ALL OF THE EXAMPLES THAT YOU have seen have used one of the two resource managers that are included with the .NET Framework 1.1 and 2.0—namely, System.Resources.ResourceManager
and System.ComponentModel.ComponentResourceManager
. In this chapter, I show how you can write your own resource managers. You might want to do this to read/write resources from a different format (e.g., a database) or to provide additional functionality to a resource manager. I start by explaining how the “file-based” resource manager embedded in the ResourceManager
class works. This feature has been largely ignored until now, as its implementation is somewhat flawed. However, the basic idea is sound, and knowledge of what it does helps us to understand how the ResourceManager
class works. It also paves the way for writing a replacement resource manager.
I go on to explain how the ResourceManager
works internally, as this will help us to write our own resource managers. Our first custom resource manager reads resources from a database. This is a relatively simple and useful resource manager that we cover in two stages: reading resources from a database and, later, writing resources to a database. We follow this with two related resource managers to read/write file-based resources in .resources
and .resx
files. The former offers the same functionality as the file-based resource manager hidden in ResourceManager
, but without the limitations. With these resource managers in place, we write a “translation” resource manager. This resource manager uses the translation engine that we wrote in Chapter 9, “Machine Translation,” to translate missing resources on the fly. This resource manager is particularly useful for resources that frequently change where it is impractical to return to the development team for new releases whenever content changes. The last resource manager example is a resource manager to set properties such as Font
, ImeMode
, and RightToLeft
on an application-wide basis.
Our job does not end with the writing of the resource managers, though. I introduce a resource manager provider class that does for resource managers what the DbProviderFactories
class does for data providers in the .NET Framework 2.0. The last piece of the jigsaw is to use the custom resource manager. Both Windows Forms developers and ASP.NET developers have separate additional steps that must be taken to successfully use a custom resource manager. Windows Forms developers have the problem that the code generated in a form’s InitializeComponent
method includes a hard-coded reference to the ResourceManager
or ComponentResourceManager
classes. I show how to create a component to overcome this problem. ASP.NET developers must use ASP.NET’s resource provider mechanism to get ASP.NET to adopt the new custom resource manager. Finally, we return to the subject of strongly typed resources and show how to generate strongly typed resource classes for our new resource managers.
But before we begin on our journey, a word of warning: If performance is an issue to you, you might want to stay with the ResourceManager
or Component-ResourceManager
classes. These classes outperform all of the solutions in this chapter, and it is unlikely that other solutions will offer better performance than the existing resource managers already do. Of course, they don’t offer the same functionality as custom resource managers, so you need to weigh the benefits.
Embedded inside the ResourceManager
class is a second resource manager that, instead of reading resources from an assembly, reads resources from stand-alone resource files. If you have seen Harry Potter and the Sorcerer’s Stone, you can think of this resource manager as the parasitic Lord Voldemort sharing the same body as Professor Quirrell. To make a distinction between the two resource managers that inhabit the same ResourceManager
class, I refer to them as “assembly-based” resource managers and “file-based” resource managers. This file-based resource manager can be created using the static ResourceManager.CreateFileBasedResourceManager
method:
ResourceManager resourceManager =
ResourceManager.CreateFileBasedResourceManager(
"Strings", ".", null);
The first parameter is the base name of the resources file. This is the filename without the culture and without the file extension. The second parameter is the directory in which the file is located. The third parameter is a Type
that is used to create new ResourceSets
. Hereafter, this file-based resource manager is used and behaves in the exact same way as an assembly based resource manager, so to get a string, we call GetString
:
MessageBox.Show(resourceManager.GetString("Hello"));
If the CurrentUICulture
hasn’t been set, this looks for a file called “Strings.resources
” in the same directory as the executable. If the CurrentUI-Culture
is “fr-FR
”, it looks for a file called “Strings.fr-FR.resources
”.
Of course, you need to create the resources file. If you are starting from a resx file, you can create it using the .NET Framework resgen utility:
resgen Strings.resx
This creates Strings.resources
. You then need to move the resources file into the same directory as the executable assembly. The problem with this approach is that it requires the developer to invoke resgen manually and to copy the resulting file. It’s a minor effort for a single file, but it is easily forgotten. A better solution is one that is part of the build process. The next three sections cover this process for Windows Forms, ASP.NET 2.0, and ASP.NET 1.1, respectively.
You can incorporate resgen into the build process so that it is run automatically. In either Visual Studio 2005 or Visual Studio 2003, right-click the project in Solution Explorer, select Properties, and click on the Build Events tab (see Figure 12.1).
Figure 12.1. The Project’s Build Events in Visual Studio 2005
In the “Post-build event command line:,” you can enter a command to execute after the build process. Enter the following command (on a single line):
"C:Program FilesMicrosoft Visual Studio 8SDKv2.0Bin
esgen"
C:ExamplesWindowsApplication1Strings.resx
C:ExamplesWindowsApplication1indebug
Strings.resources
(For Visual Studio 2003, use the .NET Framework SDK 1.1 bin
folder, which is typically C:Program FilesMicrosoft Visual Studio .NET 2003SDKv1.1Bin
).
Click OK. From here on, whenever you do a successful build, this command will be run after the build has completed. Of course, this uses hard-coded paths, so a better solution is to use macros to refer to the project directory and the target directory:
"C:Program FilesMicrosoft Visual Studio 8SDKv2.0Bin
esgen"
$(ProjectDir)Strings.resx
$(TargetDir)Strings.resources
This is fine if you just want to compile a single resource, but that’s probably not the case for most applications. A better approach is to create a script file and put this command along with the others in the script file.
ASP.NET 2.0 applications do not have a project file; consequently, the solution of using Build Events cannot be used. The best option for ASP.NET 2.0 applications is to use Web Deployment Projects. Web Deployment Projects are separate projects that are added to ASP.NET 2.0 solutions. A Web Deployment Project is an MSBuild project for a Web site. Among the many additional features that Web Deployment Projects offer is the capability to add custom pre- and post-build steps. Web Deployment Projects are an add-in package for Visual Studio 2005 and can be downloaded from http://msdn.microsoft.com/asp.net/reference/infrastructure/wdp/default.aspx. To add steps to call resgen to compile resx files, install Web Deployment Projects. In Visual Studio 2005, open the Web site, select Solution Explorer, right-click the site, and select the new menu item “Add Web Deployment Project...”. In the Add Web Deployment Project dialog, enter a name and location for the Web Deployment Project and click OK. The Web Deployment Project is an MSBuild file with an extension of .wdproj
. To open this file, right-click it in Solution Explorer and select Open Project File. Toward the bottom of the file, you will find the following section:
<Target Name="BeforeBuild">
</Target>
By default, this section is commented out, so move it outside the commented region. Finally, you can add the GenerateResource
build task to this section to compile the resources. GenerateResource
is the MSBuild task that offers similar functionality to resgen.
Incorporating resgen into the ASP.NET 1.1 build process requires a little trickery with creating a Visual C++ project, but don’t worry; there’s no C++ involved in this process. Although this approach works equally well in Windows Forms application in both Visual Studio 2005 and Visual Studio 2003, I recommend using Visual Studio Build Events instead. To incorporate resgen into the build process for a solution, right-click the solution; select Add, New Project...; in Project Types, select Visual C++ Projects; in Templates, select Makefile Project; in Name, enter BuildResources (see Figure 12.2); and click OK.
Figure 12.2. Add New Makefile Project in Visual Studio 2003
Click Finish to close the wizard. In Solution Explorer, you can delete the “Source Files
”, “Header Files
”, and “Resource Files
” folders and readme.txt
. Add a new text file to the project (right-click the BuildResources
project; select Add, New Item...; select Text File (.txt); enter BuildResources.bat
in the name; and click Open. In BuildResources.bat
, add one of the following two commands, according to your version of Visual Studio:
call "C:Program FilesMicrosoft Visual Studio .NET 2003SDK
v1.1Bin
esgen"
C:InetpubwwwrootWebApplication1Strings.resx
C:InetpubwwwrootWebApplication1inStrings.resources
call "C:Program FilesMicrosoft Visual Studio 8SDK
v2.0Bin
esgen"
C:InetpubwwwrootWebApplication1Strings.resx
C:InetpubwwwrootWebApplication1inStrings.resources
Right-click the BuildResources
project, select Properties, select NMake, and enter BuildResources.bat
in the “Build Command Line” and “Rebuild All Command Line” (see Figure 12.3).
Figure 12.3. Specifying NMake Properties
Now, whenever you do a build in Visual Studio, this new make project will be built and BuildResources.bat
will be executed, running resgen and building the resources files.
So what the file-based resource manager gives us is the capability to read from .resources
files instead of from an assembly. Unfortunately, there is a limitation to this approach, and that is that the .resources
files are kept open after they have been read. This means that they cannot be updated until either the ResourceManager.Release AllResources
method is called or the underlying ResourceSets
get garbage collected. In a Web application, typically this means having to shut down the Web server; therefore, this file-based resource manager is impractical.
In addition to this limitation, another problem faces Windows Forms applications. Recall that when you set Form.Localizable
to true
, a new line of code is added to the InitializeComponent
method to initialize a resource manager:
// Visual Studio 2003
System.Resources.ResourceManager resources = new
System.Resources.ResourceManager(typeof(Form1));
// Visual Studio 2005
System.ComponentModel.ComponentResourceManager resources = new
System.ComponentModel.ComponentResourceManager(typeof(Form1));
Clearly, this is initializing an assembly-based resource manager, not a file-based resource manager. There are two solutions to this problem. The first is to create a linked resource assembly; the second is covered later in this chapter in the ResourceManagerProvider
section. A linked resource assembly is an assembly that contains links to stand-alone .resources
file. The resource assemblies that we have seen and used so far all contain embedded resources; that is, the resources have been embedded in the assembly, and the stand-alone .resources
files are not needed at runtime. A linked assembly doesn’t embed the resource files, but instead references or “links” to them. See Chapter 14, “The Translator,” for details of creating a linked assembly. Functionally, there is no difference between the two solutions, but it does solve the problem of Visual Studio hard-wiring a reference to the resource manager class. Now we can let Visual Studio write code to create a ResourceManager
/ ComponentResourceManager
and have it use our linked assembly. The linked assembly still uses the stand-alone .resources
files, but we haven’t had to call the ResourceManager.CreateFileBasedResourceManager
method to use them; therefore, Visual Studio’s generated code still works. Unfortunately, using a linked resource assembly doesn’t solve the problem that the resource manager still locks the .resources
files.
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.
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
.
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
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
”.
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
.
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.
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.
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.
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.
Before we embark on writing our first custom resource manager, you might like to look at the demo program in the source code for this book. The Custom Resource Managers Examples application (see Figure 12.4) enables you to experiment with the completed resource managers, and you may find this helpful in visualizing their operation and behavior.
Figure 12.4. Custom Resource Managers Examples Application
To use the DbResourceManager
, you first need to create the required SQL Server database and fill it with data. You can do this by clicking on the Create Example Database button. This creates the “CustomResourceManagersExample
” database. You can verify that the resource manager is reading from the database by changing the contents of the ResourceSets
table and observing the changes in the demo program. This is particularly effective when changing the entries for “Form2
” (you can see Form2
by clicking on the “Show Form2” button).
The first of our custom resource managers is a resource manager that loads resources from a database. We approach this resource manager in two stages: reading from the database and, later in this chapter, writing to the database. The second writing phase won’t be necessary for everyone, so if you intend to maintain your resources database yourself, you can skip the writing stage.
The first decision that you need to make when writing any resource manager is which class to inherit from. If you intend your resource managers to be used in Visual Studio 2005 Windows Forms applications, you should inherit from ComponentResourceManager
because it contains the vital ApplyResources
method used by the InitializeComponents
method. For all other scenarios, the Component ResourceManager
offers nothing beyond ResourceManager
; to avoid dragging in unnecessary baggage, you should inherit from ResourceManager
. In this chapter, I have chosen to inherit from ComponentResourceManager
only so that the resource managers have the broadest possible appeal, but feel free to change this decision.
The basic implementation of our DbResourceManager
(missing one method, which we shall come to) can be seen here:
public class DbResourceManager: ComponentResourceManager
{
private string baseNameField;
private static string connectionString =
"server=localhost;database=CustomResourceManagersExample;"+
"trusted_connection=true";
public static string ConnectionString
{
get {return connectionString;}
set {connectionString = value;}
}
protected virtual void Initialize(
string baseName, Assembly assembly)
{
this.baseNameField = baseName;
ResourceSets = new Hashtable();
}
public DbResourceManager(string baseName, Assembly assembly)
{
Initialize(baseName, assembly);
}
public DbResourceManager(string baseName)
{
Initialize(baseName, null);
}
public DbResourceManager(Type resourceType)
{
Initialize(resourceType.Name, resourceType.Assembly);
}
}
DbResourceManager
inherits from ResourceManager
. As there is no resource manager interface and no resource manager base class, we are forced to inherit from a working implementation of a resource manager. In this example, we want most of the ResourceManager
methods intact, so this isn’t so terrible. DbResourceManager
has three constructors, which all call the protected Initialize
method. These constructors match three of the ResourceManager
constructors quite deliberately. I have taken the approach that the resource manager constructors should maintain as many common constructor signatures as possible. This commonality allows us to create a resource manager provider class later in this chapter. So even though the assembly parameter isn’t used, it is still accepted. You might notice, though, that there is no constructor that allows us to pass a type for the resource set class. As the very purpose of this class is to change this type, it defeats the purpose of the class to pass this parameter.
The Initialize
method assigns the baseName
parameter to the private base-NameField
field and initializes the protected ResourcesSets
field (inherited from ResourceManager
) to a Hashtable
.
You can also see from this initial implementation that DbResourceManager
has a private static string field called connectionString
, which is exposed through a public static string property called ConnectionString
. This is the connection string that is used to connect to the database. Strictly speaking, this should be a parameter passed to the DbResourceManager
constructors, not a static property. If it were passed as a parameter and stored in an instance field, you would be able to have different resource managers that use different databases within the same application. I chose not to adopt this approach because (1) it would require a constructor signature that is specific to DbResourceManager
and, therefore, would make it awkward to construct a DbResourceManager
generically, and (2) I felt that it was unlikely that a single application would use two different resource databases simultaneously.
The only other method to implement is the InternalGetResourceSet
method:
protected override ResourceSet InternalGetResourceSet(
CultureInfo cultureInfo, bool createIfNotExists, bool tryParents)
{
if (ResourceSets.Contains(cultureInfo.Name))
return ResourceSets[cultureInfo.Name] as ResourceSet;
else
{
DbResourceSet resourceSet =
new DbResourceSet(baseNameField, cultureInfo);
ResourceSets.Add(cultureInfo.Name, resourceSet);
return resourceSet;
}
}
This method looks in the ResourceSets Hashtable
cache to see if a Resource Set
has already been saved and, if it has, returns it. Otherwise, it creates a new DbResourceSet
object, adds it to the cache, and returns that. The most obvious difference between this implementation and the ResourceManager
implementation is that this implementation creates DbResourceSet
objects, whereas the Resource Manager
implementation, by default, creates RuntimeResourceSet
(a subclass of ResourceSet
) objects. Given that we know that ResourceManager
has a constructor that accepts a ResourceSet
type from which new resource sets can be created, you might wonder why we don’t simply pass our DbResourceSet
type to the constructor and save ourselves the trouble of overriding the InternalGetResourceSet
method. The problem is that the ResourceManager.InternalGetResourceSet
method performs both tasks of getting the resource stream and creating a new resource set. We don’t want the InternalGetResourceSet
method to get the resource stream, so we are forced to override it to prevent this from happening.
Note that there is no need in this class to override the GetString
or GetObject
methods, as they provide us with the functionality that we need.
The first implementation of our DbResourceSet
looks like this:
public class DbResourceSet: ResourceSet
{
public DbResourceSet(
string baseNameField, CultureInfo cultureInfo):
base(new DbResourceReader(baseNameField, cultureInfo))
{
}
}
We will be modifying this class later when we add write functionality. The constructor accepts the baseNameField
and culture passed from the InternalGet ResourceSet
method. The constructor calls the base class constructor and passes an IResourceReader
, which is taken from the newly created DbResourceReader
object. If you read through the ResourceSet
documentation, you will find two methods, GetDefaultReader
and GetDefaultWriter
, which expect to be overridden and to return the Type
of the resource reader and writer, respectively. I haven’t implemented these yet because they aren’t used anywhere in the .NET Framework. However, this is only to illustrate that they aren’t necessary, and because you could consider this sloppy programming, I implement them in the second incarnation of this class.
The DbResourceReader
looks like this:
public class DbResourceReader: IResourceReader
{
private string baseNameField;
private CultureInfo cultureInfo;
public DbResourceReader(
string baseNameField, CultureInfo cultureInfo)
{
this.baseNameField = baseNameField;
this.cultureInfo = cultureInfo;
}
public System.Collections.IDictionaryEnumerator GetEnumerator()
{
}
public void Close()
{
}
System.Collections.IEnumerator
System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
public void Dispose()
{
}
}
The DbResourceReader
implements the IResourceReader
interface, which looks like this:
public interface IResourceReader : IEnumerable, IDisposable
{
void Close();
IDictionaryEnumerator GetEnumerator();
}
The IResourceReader
interface allows a caller to get an enumerator for the resources and to close the resource when it is finished with it. Because these two operations are distinct, it allows the resource reader to keep the source open and to read from it as needed. This is exactly what the ResourceEnumerator
returned from ResourceReader.GetEnumerator
does. This is why .resources
files are kept open by file-based resource managers. In our database implementation, it doesn’t make sense to read the resource item by item, so we have no implementation for the Close
method. The DbResourceReader.GetEnumerator
method looks like this:
public System.Collections.IDictionaryEnumerator GetEnumerator()
{
Hashtable hashTable = new Hashtable();
using(SqlConnection connection =
new SqlConnection(DbResourceManager.ConnectionString));
{
connection.Open();
using (SqlCommand command = GetSelectCommand(connection))
{
SqlDataReader dataReader = command.ExecuteReader();
while (dataReader.Read())
{
object resourceValue =
dataReader["ResourceValue"].ToString();
object resourceType = dataReader["ResourceType"];
if (resourceType != null &&
resourceType.ToString() != String.Empty &&
resourceType.ToString() != "System.String")
resourceValue = GetResourceValue((string)
resourceValue, resourceType.ToString());
hashTable.Add(dataReader["ResourceName"].ToString(),
resourceValue);
}
dataReader.Close();
}
}
return hashTable.GetEnumerator();
}
We create a Hashtable
to hold the resource retrieved from the database. We do this so that we can close the connection (or at least return it to the connection pool). Of course, this approach is wasteful if you use a resource manager to retrieve a single string and then discard the resource manager, but hopefully your resource managers are used for retrieving multiple resource values.
We create a new connection passing the static DbResourceManager.ConnectionString
. I have chosen to hard-wire the references to SqlClient
classes in this example so that it is simple to read and work with all versions of the .NET Framework. If you are using .NET Framework 2.0, you might like to replace these classes with appropriate calls to DbProviderFactory
methods.
The DbResourceReader.GetEnumerator
method simply opens a connection; creates a data reader; enumerates through the result set, adding the entries to the local Hashtable
; closes the connection; and returns the Hashtable
’s enumerator. The GetResourceValue
method is responsible for converting the resource’s value from a string to the appropriate primitive, enum, struct, or class, according to the resource’s type:
protected virtual object GetResourceValue(
string resourceValue, string resourceTypeName)
{
string className = resourceTypeName.Split(',')[0];
string assemblyName =
resourceTypeName.Substring(className.Length + 2);
Assembly assembly = Assembly.Load(assemblyName);
Type resourceType = assembly.GetType(className, true, true);
if (resourceType.IsPrimitive)
return Convert.ChangeType(resourceValue, resourceType);
else if (resourceType.IsEnum)
return Enum.Parse(resourceType, resourceValue, true);
else
{
// the type is a struct or a class
object[] parameterValues =
StringToParameterValues(resourceValue);
return Activator.CreateInstance(
resourceType, parameterValues);
}
}
So, for example, if the resourceTypeName
is “System.Drawing.Content Alignment, System.Drawing, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
”, then the className
would be “System.Drawing.ContentAlignment
” and the assemblyName
would be “System.Drawing, Version=1.0.5000.0, Culture=neutral, PublicKeyToken= b03f5f7f11d50a3a
”. The Type
, resourceType
, would be loaded from the assembly, and resourceType.IsEnum
would be true
. The string value would then be converted to the ContentAlignment
enum using Enum.Parse
.
The GetSelectCommand
method is:
public virtual SqlCommand GetSelectCommand(SqlConnection connection)
{
SqlCommand command;
if (cultureInfo.Equals(CultureInfo.InvariantCulture))
{
string commandText =
"SELECT ResourceName, ResourceValue, ResourceType "+
"FROM ResourceSets WHERE ResourceSetName=" +
"@resourceSetName AND Culture IS NULL";
command = new SqlCommand(commandText, connection);
command.Parameters.Add("@resourceSetName",
SqlDbType.VarChar, 100).Value = baseNameField;
}
else
{
string commandText =
"SELECT ResourceName, ResourceValue, ResourceType "+
"FROM ResourceSets WHERE ResourceSetName=" +
"@resourceSetName AND Culture=@culture";
command = new SqlCommand(commandText, connection);
command.Parameters.Add("@resourceSetName",
SqlDbType.VarChar, 100).Value = baseNameField;
command.Parameters.Add("@culture",
SqlDbType.VarChar, 20).Value = cultureInfo.ToString();
}
return command;
}
We create a command to retrieve the resources from the database. The command string will be one of the following values, depending on whether a culture is passed:
SELECT ResourceName, ResourceValue, ResourceType FROM ResourceSets
WHERE ResourceSetName=@resourceSetName AND Culture IS NULL
SELECT ResourceName, ResourceValue, ResourceType FROM ResourceSets
WHERE ResourceSetName=@resourceSetName AND Culture=@culture
The SQL Server ResourceSets
table is created from:
CREATE TABLE [ResourceSets] (
[ResourceID] [int] IDENTITY (1, 1) NOT NULL ,
[ResourceSetName] [varchar] (50) NOT NULL ,
[Culture] [varchar] (10) NULL ,
[ResourceName] [varchar] (100) NOT NULL ,
[ResourceValue] [varchar] (200) NOT NULL ,
[ResourceType] [varchar] (250) ,
CONSTRAINT [PK_ResourceSets] PRIMARY KEY CLUSTERED
([ResourceID]) ON [PRIMARY]
) ON [PRIMARY]
Figure 12.5 shows the ResourceSets
table filled with the example data.
Figure 12.5. ResourceSets Table with Example Data
The SELECT
statement simply retrieves ResourceName
and ResourceValue
fields for the given culture and the given resource set name. If the ResourceSetName
is “CustomResourceManagersExample.Greetings
” and the Culture
is “en
”, the result set is shown in the following table.
And voilà—you have a read-only database resource manager.
The ResourcesResourceManager
and the ResXResourceManager
allow resources to be read from stand-alone .resources
and .resx
files, respectively. In this respect, they offer the same functionality as the ResourceManager.CreateFile BasedResourceManager
method, with the exceptions that ResXResourceManager
reads resx files and, more important, both resource managers read resources in their entirety into memory and, therefore, do not keep locks on the files. As such, these resource managers offer you another strategy for translating your application:
You can ship a version of your application that uses the ResourcesResource Manager
or ResXResourceManager
to the translator. The translator can update resources/resx files using whatever utilities you provide. The translator can immediately see the changes in the application without having to return these changes to the developers. When the translator is satisfied with the translations, the translator can ship the translated resources/resx files back to the developers, who can build a satellite assembly from the result. Everyone wins. The translator gets immediate feedback on their work, developers get to package their resources more neatly into satellite assemblies for the release version, and the users do not suffer the performance hit of a “slower” resource manager.
The ResourcesResourceManager
includes the whole functionality for both resource managers. The ResXResourceManager
simply inherits from Resource ResourceManager
and sets an “extension” field. The ResourcesResourceManager
class is almost identical to the DbResourceManager
class, with the following differences:
• The ResourcesResourceManager
doesn’t have a connectionString
field or ConnectionString
property.
• The ResourcesResourceManager
has a private string field called extension
, which is initialized to “resources
”.
• The ResourcesResourceManager.InternalGetResourceSet
method creates a ResourcesResourceSet
object instead of a DbResourceSet
object, and passes a third parameter to the constructor—namely, the extension field.
The ResourcesResourceSet
class is equally as simple as the DbResourceSet
class:
public class ResourcesResourceSet: CustomResourceSet
{
public ResourcesResourceSet(string baseNameField,
CultureInfo cultureInfo, string extension):
base(new ResourcesResourceReader(
baseNameField, cultureInfo, extension))
{
}
}
As before with the DbResourceSet
class, we could implement the GetDefault Reader
and GetDefaultWriter
methods, but as it isn’t necessary for our purposes, I leave this until later.
The ResourcesResourceReader
class also follows the blueprint laid down by the DbResourceReader
class. Here is the ResourcesResourceReader
class (without the GetEnumerator
method):
public class ResourcesResourceReader: IResourceReader
{
private string baseNameField;
private CultureInfo cultureInfo;
private string extension;
public ResourcesResourceReader(string baseNameField,
CultureInfo cultureInfo, string extension)
{
this.baseNameField = baseNameField;
this.cultureInfo = cultureInfo;
this.extension = extension;
}
protected virtual string GetResourceFileName()
{
if (cultureInfo.Equals(CultureInfo.InvariantCulture))
return baseNameField + "." + extension;
else
return baseNameField + "." +
cultureInfo.Name + "." + extension;
}
protected virtual IResourceReader GetResourceReader(
string fileName)
{
if (extension == "resx")
return new ResXResourceReader(fileName);
else if (extension == "resources")
return new ResourceReader(GetResourceFileName());
else
throw new ArgumentException(String.Format(
"Unknown resource extension ({0})", extension));
}
public void Close()
{
}
System.Collections.IEnumerator
System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
public void Dispose()
{
}
}
The constructor assigns the base name, culture, and extension to their respective private fields. The GetResourceFileName
method returns the filename for the resource. The filename is constructed from the base name, culture, and extension, so in our earlier example, the name would be “CustomResourceManagers Example.Greetings.resources
” for the invariant culture when the extension is “resources
” and “CustomResourceManagersExample.Greetings.en-GB.resx
” for the “en-GB
” culture when the extension is “resx
”.
Unlike the ResourceManager.CreateFileBasedResourceManager
method, I have assumed that the resource files are in the executable’s working directory. If this doesn’t follow your implementation, you can modify the GetResourceFileName
method to support your model.
The GetResourceReader
method gets an IResourceReader
for the given file based upon the extension. If you wanted to support extensions other than .resources
and .resx
, you would modify the GetResourceFileName
and Get ResourceReader
methods. The GetEnumerator
method is:
public System.Collections.IDictionaryEnumerator GetEnumerator()
{
Hashtable hashTable = new Hashtable();
string fileName = GetResourceFileName();
if (File.Exists(fileName))
{
IResourceReader reader = GetResourceReader(fileName);
try
{
IDictionaryEnumerator enumerator =
reader.GetEnumerator();
while (enumerator.MoveNext())
{
hashTable.Add(enumerator.Key, enumerator.Value);
}
}
finally
{
reader.Close();
}
}
return hashTable.GetEnumerator();
}
This simple method gets the resource filename and, if the resource file exists, gets an IResourceReader
to read the resource file and enumerates through the whole resource, loading all the items into a Hashtable
. Herein lies the difference between this implementation and the file-based ResourceManager
implementation: The resource is read in its entirety and then closed, whereas the ResourceManager
class reads the resource as necessary and leaves it open. In exchange for this improved functionality, you may take a performance hit, depending upon whether your resource managers get reused often or are created and disposed of frequently.
The ResXResourceManager
class simply uses all of the functionality offered in the ResourcesResourceManager
class and changes the extension to “resx
”:
public class ResXResourceManager: ResourcesResourceManager
{
protected override void Initialize(
string baseName, Assembly assembly)
{
Extension = "resx";
base.Initialize(baseName, assembly);
}
public ResXResourceManager(string baseName, Assembly assembly):
base(baseName, assembly)
{
}
public ResXResourceManager(string baseName): base(baseName)
{
}
public ResXResourceManager(Type resourceType): base(resourceType)
{
}
}
The custom resource managers that we have looked at so far read resources but have no capability for writing to resources. This solution is adequate if you intend to maintain the resources yourself. However, the next custom resource manager is the TranslationResourceManager
. This resource manager performs translations onthe-fly for missing resources and needs to write back these translations to the original source. To do this our resource managers must have a write capability, and that’s what this section is about.
Our first attempts at the DbResourceSet
and ResourcesResourceSet
classes were minimalist, to say the least. The solution to the problem of writing to resources lies in modifying these classes. In the previous discussion, I briefly mentioned that ResourceSet
has two methods, GetDefaultReader
and GetDefaultWriter
, which allow us to specify which Types
are used to create reader and writer objects for the resource. These are implemented as follows:
public override Type GetDefaultReader()
{
return typeof(DbResourceReader);
}
public override Type GetDefaultWriter()
{
return typeof(DbResourceWriter);
}
Unfortunately, these methods do not help us solve our problem. Nothing in the .NET Framework ever calls these methods. They exist solely to provide a means to allow generic code to create readers and writers as necessary. Certainly, this is what we want to do, but this approach isn’t sufficient for our purposes. The problem with simply specifying a Type
to create a new object from is that you are at the mercy of the caller to call the constructor signature that you want called. The following generic code illustrates the problem:
ResourceSet resourceSet = new ResourceSet(fileName);
Type resourceReaderType = resourceSet.GetDefaultReader();
IResourceReader resourceReader = (IResourceReader)
Activator.CreateInstance(resourceReaderType,
new object[] {fileName});
In this example, a new IResourceReader
is being created from the resource reader type using Activator.CreateInstance
. The generic code has determined that it will use the resource reader constructor, which accepts a single string parameter. Both of the IResourceReader
classes that we have implemented so far (DbResourceReader
and ResourcesResourceReader
) do not support this constructor. Furthermore, they do not share a common constructor signature at all:
public DbResourceReader(
string baseNameField, CultureInfo cultureInfo)
public ResourcesResourceReader(string baseNameField,
CultureInfo cultureInfo, string extension)
It is for this reason that I have implemented a slightly more versatile solution. Both ResourceSet
classes now inherit from a new class, CustomResourceSet
:
public class CustomResourceSet: ResourceSet
{
public CustomResourceSet(IResourceReader resourceReader):
base(resourceReader)
{
}
public virtual IResourceReader CreateDefaultReader()
{
Type resourceReaderType = GetDefaultReader();
return (IResourceReader)
Activator.CreateInstance(resourceReaderType);
}
public virtual IResourceWriter CreateDefaultWriter()
{
Type resourceWriterType = GetDefaultWriter();
return (IResourceWriter)
Activator.CreateInstance(resourceWriterType);
}
public virtual void Add(string key, object value)
{
Table.Add(key, value);
}
public new Hashtable Table
{
get {return base.Table;}
}
}
CustomResourceSet
implements two new methods: CreateDefaultReader
and CreateDefaultWriter
. These methods can be overridden by subclasses to create IResourceReader
and IResourceWriter
objects using whatever constructor the developer sees fit. You can also see a new method, Add
, and a public property, Table
, which we return to later. The DbResourceSet
now becomes:
public class DbResourceSet: CustomResourceSet
{
private string baseNameField;
private CultureInfo cultureInfo;
public DbResourceSet(
string baseNameField, CultureInfo cultureInfo):
base(new DbResourceReader(baseNameField, cultureInfo))
{
this.baseNameField = baseNameField;
this.cultureInfo = cultureInfo;
}
public override Type GetDefaultReader()
{
return typeof(DbResourceReader);
}
public override Type GetDefaultWriter()
{
return typeof(DbResourceWriter);
}
public override IResourceReader CreateDefaultReader()
{
return new DbResourceReader(baseNameField, cultureInfo);
}
public override IResourceWriter CreateDefaultWriter()
{
return new DbResourceWriter(baseNameField, cultureInfo);
}
}
The DbResourceSet
constructor works as it did before, creating a new DbResourceReader
object. In addition, it saves the parameters passed to two private fields. The CreateDefaultReader
and CreateDefaultWriter
methods are overridden, and the base name field and culture are passed to the DbResourceReader
and DbResourceWriter
constructors. The GetDefaultReader
and GetDefaultWriter
methods are redundant, but I have implemented them anyway for completeness. All that remains for the DbResourceSet
class now is for us to implement the new DbResourceWriter
class.
DbResourceWriter
is an implementation of the IResourceWriter
interface:
public interface IResourceWriter : IDisposable
{
void AddResource(string name, object value);
void AddResource(string name, string value);
void AddResource(string name, byte[] value);
void Close();
void Generate();
}
A consumer of this interface simply calls the various AddResource
methods adding string, object, or byte[] resources. It then calls Generate
to create the resource and finally Close
to close the resource. You can approach this interface in one of two ways:
• You can write the resources to the target with every call to AddResource
, and then either commit the changes when Generate
is called or roll them back if Close
is called without a Generate
.
• Alternatively, you can collect all of the resources added using AddResource
in a cache and write the resource in its entirety in the Generate
method.
I have chosen the latter approach because we will need to open a connection to the database, and I want the connection to be open for as short a duration as possible. The DbResourceWriter
class (without the Generate
method) is:
public class DbResourceWriter: IResourceWriter
{
private string baseNameField;
private CultureInfo cultureInfo;
private SortedList resourceList;
public DbResourceWriter(
string baseNameField, CultureInfo cultureInfo)
{
this.baseNameField = baseNameField;
this.cultureInfo = cultureInfo;
resourceList = new SortedList();
}
public void Close()
{
Dispose(true);
}
public void Dispose()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (disposing && resourceList != null)
Generate();
}
public void AddResource(string name, object value)
{
if (name == null)
throw new ArgumentNullException("name");
if (resourceList == null)
throw new InvalidOperationException(
"InvalidOperation_ResourceWriterSaved");
resourceList.Add(name, value);
}
public void AddResource(string name, string value)
{
AddResource(name, (Object) value);
}
public void AddResource(string name, byte[] value)
{
AddResource(name, (Object) value);
}
}
The DbResourceWriter
simply assigns the incoming base name and culture parameters to private fields and initializes a resourceList
private field to a SortedList
. The resourceList
is a temporary bucket into which all of the resources are added until the Generate
method is called.
As the name implies, the Generate
method is responsible for generating the resource. In the case of the .NET Framework ResourceWriter
and ResX ResourceWriter
classes, this means writing a new .resources
or .resx
file. The new file overwrites the old file. In the case of a database, this means deleting all of the existing resource entries for the given resource name and inserting a new row for each resource key.
public void Generate()
{
if (resourceList == null)
throw new InvalidOperationException(
"InvalidOperation_ResourceWriterSaved");
using (SqlConnection connection =
new SqlConnection(DbResourceManager.ConnectionString))
{
connection.Open();
SqlTransaction transaction = connection.BeginTransaction();
try
{
DeleteExistingResource(transaction);
foreach(DictionaryEntry dictionaryEntry in resourceList)
{
if (dictionaryEntry.Value != null)
InsertResource(transaction,
dictionaryEntry.Key.ToString(),
dictionaryEntry.Value);
}
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
resourceList = null;
}
The Generate
method opens a connection, creates a transaction, deletes all of the existing resources, and, for each resource that has been added to resourceList
, inserts a new row into the database. Finally, the transaction is either committed or rolled back, and the connection is closed.
DbResourceManager.DeleteExistingResource
and DbResourceManager. InsertResource
are:
protected virtual void DeleteExistingResource(
SqlTransaction transaction)
{
// delete all of the existing resource values
if (cultureInfo.Equals(CultureInfo.InvariantCulture))
{
string deleteCommandText =
"DELETE FROM ResourceSets WHERE ResourceSetName="+
"@resourceSetName AND Culture IS NULL";
using (SqlCommand deleteCommand = new SqlCommand(
deleteCommandText, transaction.Connection, transaction))
{
deleteCommand.Parameters.Add("@resourceSetName",
SqlDbType.VarChar, 100).Value = baseNameField;
deleteCommand.ExecuteNonQuery();
}
}
else
{
string deleteCommandText =
"DELETE FROM ResourceSets WHERE ResourceSetName="+
"@resourceSetName AND Culture=@culture";
using (SqlCommand deleteCommand = new SqlCommand(
deleteCommandText, transaction.Connection, transaction))
{
deleteCommand.Parameters.Add("@resourceSetName",
SqlDbType.VarChar, 100).Value = baseNameField;
deleteCommand.Parameters.Add("@culture",
SqlDbType.VarChar, 20).Value =
cultureInfo.ToString();
deleteCommand.ExecuteNonQuery();
}
}
}
protected virtual void InsertResource(SqlTransaction transaction,
string resourceName, object resourceValue)
(
string insertCommandText;
if (cultureInfo.Equals(CultureInfo.InvariantCulture))
{
if (resourceValue is System.String)
insertCommandText = "INSERT INTO ResourceSets "+
"(ResourceSetName, ResourceName, "+
"ResourceValue) VALUES (@resourceSetName, "+
"@resourceName, @resourceValue)";
else
insertCommandText = "INSERT INTO ResourceSets "+
"(ResourceSetName, ResourceName, "+
"ResourceValue, ResourceType) VALUES "+
"(@resourceSetName, @resourceName, "+
"@resourceValue, @resourceType)";
}
else
{
if (resourceValue is System.String)
insertCommandText = "INSERT INTO ResourceSets "+
"(ResourceSetName, Culture, ResourceName, "+
"ResourceValue) VALUES (@resourceSetName, "+
"@culture, @resourceName, @resourceValue)";
else
insertCommandText = "INSERT INTO ResourceSets "+
"(ResourceSetName, Culture, ResourceName, "+
"ResourceValue, ResourceType) VALUES "+
"(@resourceSetName, @culture, @resourceName, "+
"@resourceValue, @resourceType)";
}
using (SqlCommand insertCommand = new SqlCommand(
insertCommandText, transaction.Connection, transaction))
{
insertCommand.Parameters.Add(new SqlParameter(
"@resourceSetName", baseNameField));
if (! cultureInfo.Equals(CultureInfo.InvariantCulture))
insertCommand.Parameters.Add(new SqlParameter(
"@culture", cultureInfo.ToString()));
insertCommand.Parameters.Add(new SqlParameter(
"@resourceName", resourceName));
insertCommand.Parameters.Add(new SqlParameter(
"@resourceValue", resourceValue.ToString()));
if (! (resourceValue is System.String))
insertCommand.Parameters.Add(new SqlParameter(
"@resourceType",
resourceValue.GetType().AssemblyQualifiedName));
insertCommand.ExecuteNonQuery();
}
}
Writing to .resources
and .resx
files is a fair bit easier than writing to a database, as the necessary classes (ResourceWriter
and ResXResourceWriter
) are part of the .NET Framework. All that we have to do is use them. The revised Resources ResourceSet
class follows the same pattern as the DbResourceSet
class:
public class ResourcesResourceSet: CustomResourceSet
{
private string baseNameField;
private CultureInfo cultureInfo;
private string extension;
public ResourcesResourceSet(string baseNameField,
CultureInfo cultureInfo, string extension):
base(new ResourcesResourceReader(
baseNameField, cultureInfo, extension))
{
this.baseNameField = baseNameField;
this.cultureInfo = cultureInfo;
this.extension = extension;
}
public override Type GetDefaultReader()
{
return typeof(ResourcesResourceReader);
}
public override Type GetDefaultWriter()
{
if (extension == "resx")
return typeof(ResXResourceWriter);
else if (extension == "resources")
return typeof(ResourceWriter);
else
throw new ArgumentException(String.Format(
"Unknown resource extension ({0})", extension));
}
public override IResourceReader CreateDefaultReader()
{
return new ResourcesResourceReader(
baseNameField, cultureInfo, extension);
}
protected virtual string GetResourceFileName()
{
if (cultureInfo.Equals(CultureInfo.InvariantCulture))
return baseNameField + "." + extension;
else
return baseNameField + "." +
cultureInfo.Name + "." + extension;
}
public override IResourceWriter CreateDefaultWriter()
{
if (extension == "resx")
return new ResXResourceWriter(GetResourceFileName());
else if (extension == "resources")
return new ResourceWriter(GetResourceFileName());
else
throw new ArgumentException(String.Format(
"Unknown resource extension ({0})", extension));
}
}
Once again, the parameters passed to the constructor are saved in private fields. The GetDefaultReader
, GetDefaultWriter
, CreateDefaultReader
, and Create DefaultWriter
methods all check the extension private field and use ResourceReader
/ResourceWriter
or ResXResourceReader
/ResXResource Writer
classes, as necessary, or throw an exception for an unknown extension.
The resource managers that we have encountered so far all assume that the content of the application is static enough that there is time to have it translated. Whereas this will be true for many applications, it is not true for all applications. Web sites with dynamic, rapidly changing content may need a different approach. Enter the TranslationResourceManager
. The TranslationResourceManager
acts as a proxy for another resource manager. It accepts incoming GetString
requests and forwards them to an internal resource manager to fulfill the request. If, however, the internal resource manager is unable to fulfill the request, the TranslationResourceManager
steps in to translate the string on-the-fly. The TranslationResourceManager
can optionally write back the translated string to the source of the resource, ensuring that subsequent requests (from any user) do not suffer the performance hit of translating the string. In this way, the TranslationResourceMan-ager
can “learn” translations. The actual translation process is performed by the Translation
engine that we wrote in Chapter 9, and its translator classes use hard-coded algorithms or Web services, as necessary, to perform the translations.
So the important point to grasp here is that the TranslationResourceManager
is only a conduit or a filter. It accepts requests and passes them through to another resource manager to actually do the work. It steps in only when the other resource manager cannot fulfill the request. To implement this, we need some way of getting this “internal” resource manager. You could pass this resource manager as a parameter to the TranslationResourceManager
constructor. This would be a flexible solution and would relieve the TranslationResourceManager
from the responsibility of this problem. I decided against this approach, for two reasons: (1) It seems unlikely to me that a single application would use different resource manager classes within the same application, so the capability to use an internal DbResource Manager
class in one instance and a ResourcesResourceManager
in the next is not useful, and (2) this would require a nonstandard constructor. For reasons that will become apparent in the section on the ResourceManagerProvider
class, using a nonstandard constructor must be avoided. So the implementation uses a public static Type
property. The consumer of the TranslationResourceManager
specifies the resource manager Type
at the beginning of the application:
TranslationResourceManager.ResourceManagerType =
typeof(DbResourceManager);
From here on, the TranslationResourceManager
creates DbResourceManager
objects for reading and writing the resources. We also need to provide the TranslationResourceManager
with a TranslatorCollection
(a collection of ITranslator
objects). You might like to refer back to Chapter 9 for a recap of the Translator Collection
class. We provide this collection by assigning the collection to the TranslationResourceManager
’s public static Translators
property:
TranslationResourceManager.Translators = new TranslatorCollection();
TranslationResourceManager.Translators.Add(
new PseudoTranslator());
TranslationResourceManager.Translators.Add(
new Office2003ResearchServicesTranslator());
TranslationResourceManager.Translators.Add(
new WebServiceXTranslator());
TranslationResourceManager.Translators.Add(
new CloserFarTranslator());
This code adds a Pseudo Translator (to provide a pseudo English translation), an Office 2003 Research Services translator (to provide translations to and from numerous languages), a WebServiceX translation (to provide translation for Latin languages, Chinese, Japanese, and Korean), and a CloserFar
translation (to provide translation to Arabic).
The TranslationResourceManager
inherits from a SurrogateResource Manager
, which provides the functionality of passing requests through to the “workhorse” resource manager:
public class SurrogateResourceManager: ComponentResourceManager
{
private static Type resourceManagerType;
private ResourceManager resourceManager;
private string baseName;
protected virtual void Initialize(
string baseName, Assembly assembly)
{
if (resourceManagerType == null)
throw new ArgumentException(
"SurrogateResourceManager.ResourceManagerType "+
"is null");
this.baseName = baseName;
MainAssembly = assembly;
resourceManager = CreateResourceManager(baseName, assembly);
}
public SurrogateResourceManager(
string baseName, Assembly assembly)
{
Initialize(baseName, assembly);
}
public SurrogateResourceManager(string baseName)
{
Initialize(baseName, null);
}
public SurrogateResourceManager(Type resourceType)
{
Initialize(resourceType.FullName, resourceType.Assembly);
}
public static Type ResourceManagerType
{
get {return resourceManagerType;}
set {resourceManagerType = value;}
}
protected ResourceManager ResourceManager
{
get {return resourceManager;}
}
protected virtual ResourceManager CreateResourceManager(
string baseName, Assembly assembly)
{
try
{
return (ResourceManager) Activator.CreateInstance(
resourceManagerType,
new object[] {baseName, assembly});
}
catch (TargetInvocationException exception)
{
throw exception.InnerException;
}
}
public override object GetObject(
string name, System.Globalization.CultureInfo culture)
{
return resourceManager.GetObject(name, culture);
}
public override ResourceSet GetResourceSet(
CultureInfo culture, bool createIfNotExists, bool tryParents)
{
return resourceManager.GetResourceSet(
culture, createIfNotExists, tryParents);
}
public override string GetString(
string name, System.Globalization.CultureInfo culture)
{
return resourceManager.GetString(name, culture);
}
}
The Initialize
method is called by all of the constructors and calls Create ResourceManager
to create a new resource manager from the resourceManager Type
. CreateResourceManager
makes the assumption that all resource managers support a constructor that accepts a string and an assembly. This is one of the reasons why the DbResourceManager
and ResourcesResourceManager
classes used static properties to allow nonstandard parameters to be set—to ensure that the class constructors could be called generically.
Before we move on, notice the GetObject
, GetString
, and GetResourceSet
methods. These methods simply call the corresponding methods on the internal resource manager. These are the simplest examples of passing requests on to the internal resource manager. Because the TranslationResourceManager
is concerned only with translating strings, the GetObject
and GetResourceSet
methods can be inherited as-is, and only the GetString
method needs to be overridden.
Now that all of the infrastructure is in place, we can take a look at the initial implementation of the TranslationResourceManager
:
public class TranslationResourceManager: SurrogateResourceManager
{
private static TranslatorCollection translators;
private static int translationWriteThreshold = 1;
private CultureInfo neutralResourcesCulture;
private int translationWriteCount = 0;
public TranslationResourceManager(
string baseName, Assembly assembly):
base(baseName, assembly)
{
}
public TranslationResourceManager(string baseName):
base(baseName)
{
}
public TranslationResourceManager(Type resourceType):
base(resourceType)
{
}
public static TranslatorCollection Translators
{
get {return translators;}
set {translators = value;}
}
public static int TranslationWriteThreshold
{
get {return translationWriteThreshold;}
set {translationWriteThreshold = value;}
}
}
Toward the top of the class declaration there is a private static integer field called translationWriteThreshold
that is initialized to 1 and a corresponding public static integer property called TranslationWriteThreshold
. The translation write threshold is the number of translations that can occur before the TranslationResourceManager
will attempt to write the translations back to the original resource. The initial value of 1
means that each translation will be written back to the original resource immediately. This is a heavy performance penalty but is immediately beneficial to other users who might need this resource. Setting the value to, say, 5
means that there will be five translations before the values are written back to the original resource. Setting the value to 0
means that values will never be written back to the original resource. This last setting effectively turns off the persistence of translated resources and results in a completely dynamically localized solution.
Before we move on to the meat of the class (i.e., the GetString
method), let’s briefly cover the NeutralResourcesCulture
protected property:
protected virtual CultureInfo NeutralResourcesCulture
{
get
{
if (neutralResourcesCulture == null)
{
if (MainAssembly == null)
// We have no main assembly so we cannot get
// the NeutralResourceLanguageAttribute.
// We will have to make a guess.
neutralResourcesCulture = new CultureInfo("en");
else
{
neutralResourcesCulture = ResourceManager.
GetNeutralResourcesLanguage(MainAssembly);
if (neutralResourcesCulture == null ||
neutralResourcesCulture.Equals(
CultureInfo.InvariantCulture))
// we didn't manage to get it from the main
// assembly or it was the invariant culture
// so make a guess
neutralResourcesCulture = new CultureInfo("en");
}
}
return neutralResourcesCulture;
}
}
This property initializes the neutralResourcesCulture
private field. If the MainAssembly
was set in the constructor, the static ResourceManager.Get NeutralResourceLanguage
is used to get the value from the assemblies’ Neutral ResourcesLanguageAttribute
. If there is no such attribute or it is the invariant culture, neutralResourcesCulture
is set to the English
culture. If the MainAssembly
was not set, we take a guess at the English
culture. Unlike other resource managers, the TranslationResourceManager
needs to know what language the fallback assembly uses; if it can’t find out, it has to take a guess. It needs to do this because the translators, not unreasonably, need to know what language they are translating from.
The GetString
method is:
public override string GetString(
string name, System.Globalization.CultureInfo culture)
{
if (culture == null)
culture = CultureInfo.CurrentUICulture;
if (culture.Equals(NeutralResourcesCulture) ||
culture.Equals(CultureInfo.InvariantCulture))
// This is the fallback culture –
// there is no translation to do.
return ResourceManager.GetString(name, culture);
// get (or create) the resource set for this culture
ResourceSet resourceSet =
ResourceManager.GetResourceSet(culture, true, false);
if (resourceSet != null)
{
// get the string from the resource set
string resourceStringValue =
resourceSet.GetString(name, IgnoreCase);
if (resourceStringValue != null)
// the resource string was found in the resource set
return resourceStringValue;
}
// The string was not found in the resource set or the
// whole resource set was not found and could not be created.
// Get the corresponding string from the invariant culture.
string invariantStringValue =ResourceManager.GetString(
name, CultureInfo.InvariantCulture);
if (invariantStringValue == null ||
invariantStringValue == String.Empty)
// there is no equivalent in the invariant culture or
// the invariant culture string is empty or null
return invariantStringValue;
// the invariant string isn't empty so it
// should be possible to translate it
CultureInfo fallbackCultureInfo = NeutralResourcesCulture;
if (fallbackCultureInfo.TwoLetterISOLanguageName ==
culture.TwoLetterISOLanguageName)
// The languages are the same.
// There is no translation to perform.
return invariantStringValue;
if (! IsOkToTranslate(fallbackCultureInfo, culture, name,
invariantStringValue))
return invariantStringValue;
ITranslator translator = translators.GetTranslator(
fallbackCultureInfo.ToString(), culture.ToString());
if (translator == null)
throw new ApplicationException(String.Format(
"No translator for this language combination "+
"({0}, {1})", fallbackCultureInfo.ToString(),
culture.ToString()));
string translatedResourceStringValue = translator.Translate(
fallbackCultureInfo.ToString(), culture.ToString(),
invariantStringValue);
if (translatedResourceStringValue != String.Empty &&
resourceSet != null)
{
// put the new string back into the resource set
if (resourceSet is CustomResourceSet)
((CustomResourceSet) resourceSet).Add(
name, translatedResourceStringValue);
else
ForceResourceSetAdd(resourceSet,
name, translatedResourceStringValue);
WriteResources(resourceSet);
}
return translatedResourceStringValue;
}
GetString
defaults the culture to the CurrentUICulture
. If the culture is the invariant culture, it calls the internal resource manager’s GetString
method and returns that string. (If it is the invariant culture, there is no translation to perform.) Next, we try to get a ResourceSet
from the internal resource manager. The internal resource manager returns a null
if there is no such resource. For example, if we ask for an Italian resource from a ResourcesResourceManager
class and the relevant “it.resources
” file does not exist, a null
will be returned. If the ResourceSet
is not null
, we search it for the key that we are looking for; if it is found, we return it. This would happen if it has already been translated or if the Translation ResourceManager
has previously encountered this key, translated it, and saved it back to the original resource.
If the ResourceSet
was null
or the key wasn’t found, we have to translate it. The first step in the process is to go back to the fallback assembly and find the original string that should be translated. We can get this from the internal resource manager GetString
method passing the invariant culture. If this string is empty, we don’t bother going to the effort of translating it, as it will be another empty string.
Using the NeutralResourcesLanguage
property, we check that the language that we are trying to translate to is different from the language of the fallback assembly by comparing the CultureInfo.TwoLetterISOLanguageName
properties. If the languages are different, we can proceed with translation.
Before we perform the translation, we perform a final check to ensure that we really want to translate this key:
if (! IsOkToTranslate(
fallbackCultureInfo, culture, name, invariantStringValue))
return invariantStringValue;
The IsOkToTranslate
method always returns true
. I have included it only because you might encounter string properties of components that you do not want translated, and this represents your opportunity to filter out these properties. If this happens, you would modify the IsOkToTranslate
method to return false
for these properties.
The next line gets the ITranslator
, which can handle the translation from the fallback assembly language to the language of the culture we are trying to translate to:
ITranslator translator = translators.GetTranslator(
fallbackCultureInfo.ToString(), culture.ToString());
So an example of this line would be:
ITranslator translator = translators.GetTranslator("en","it");
This line gets a translator to translate from English to Italian. The translator then performs this translation (which might well result in a call to a Web service), and we are left with a translated string. The final piece of the jigsaw before returning the string is to keep a copy of it:
if (translatedResourceStringValue != String.Empty &&
resourceSet != null)
{
// put the new string back into the resource set
if (resourceSet is CustomResourceSet)
((CustomResourceSet) resourceSet).Add(
name, translatedResourceStringValue);
else
ForceResourceSetAdd(
resourceSet, name, translatedResourceStringValue);
WriteResources(resourceSet);
}
Our first challenge is to put the string back into the resource set. The second challenge is to persist the resource set. The problem with the first challenge is that the ResourceSet
class lacks a public facility for adding new entries to the resource set. If you cast your mind back to the CustomResourceSet
class that we wrote earlier, you will recall an Add
method and a public Table
property that I added to allow exactly this. So we check to see that the class is a CustomResourceSet
, and if it is, we dutifully call the Add
method. If it isn’t, we call the rather nasty ForceResourceSetAdd
method, which takes the brute-force approach of getting the protected Table
field using reflection and calling its Add
method. It’s not nice, but it works.
The WriteResources
method is as follows:
protected virtual void WriteResources(ResourceSet resourceSet)
{
translationWriteCount++;
if (translationWriteThreshold > 0 &&
translationWriteCount >= translationWriteThreshold &&
resourceSet is CustomResourceSet)
{
// the current number of pending writes is greater
// than or equal to the write threshold so
// it is time to write the values back to the source
CustomResourceSet customResourceSet =
((CustomResourceSet) resourceSet);
IResourceWriter resourceWriter =
customResourceSet.CreateDefaultWriter();
// copy all of the existing resources to the
// resource writer
foreach(DictionaryEntry dictionaryEntry in
customResourceSet.Table)
{
resourceWriter.AddResource(
dictionaryEntry.Key.ToString(),
dictionaryEntry.Value);
}
resourceWriter.Generate();
resourceWriter.Close();
translationWriteCount = 0;
}
}
It increments the “translation write count” and compares it with the “translation write threshold” to see if the time has come to update the original resource. If it has and the resource set is a CustomResourceSet
, we need to write the resource. If the resource set is not a CustomResourceSet
, the original resource doesn’t get updated. This would be true if the internal resource manager was a ResourceManager
object. The ResourceManager
reads resources from an assembly, and I have taken the approach that writing back to the original assembly is not practical in this scenario.
The last obstacle to overcome is that IResourceWriter
expects to write the resource in its entirety. This means that we have to load the complete resource set into the resource writer before we generate it. In other words, we can’t simply save just the one new item that we have just added.
Congratulations, you are now the proud owner of a TranslationResource Manager
.
There is one last possibility that you might like to consider. The Translation ResourceManager
performs an “automatic” translation. That is, there is no human intervention in the translation process. An alternative to this approach would be to create a “manual” translation resource manager. This would be a variation of the automatic version, but before each string was returned, a dialog would pop up, offering a translator the original language translation and the machine translation, and allowing the translator to correct the translation and finally save the corrected translation. This manual translation resource manager would only ever be used by the translator and would be considered to be part of the permanent translation process. As such, the translator would receive a new version of the application and run the application; as the translator used the application, it would prompt for every new string that hadn’t already been translated in previous translation runs. Seems like a good idea in theory, but I suspect that it would be impractical in practice, as the translator would see all of the strings out of context and would get prompted only for strings encountered during the translator’s use of the application. There would be no guarantee that all strings had been encountered and, therefore, translated.
The StandardPropertiesResourceManager
exists to solve a problem identified in previous chapters. Several properties (e.g., Font
, ImeMode
, RightToLeft
) sometimes need to be set on an application-wide basis. That is, if you are localizing your application for Arabic, Hebrew, or Persian (Farsi), you should set the Right ToLeft
property of each control to Yes
(or Inherit
for controls and Yes
for forms). Your translator/localizer can set these values for every control or form individually, but not only is this labour-intensive, it is also error prone; it is easy to miss a few controls, especially during the maintenance phase, when new controls are added. For this reason, if your application is such that an application-wide setting is appropriate (in other words, there are no places in your application where some controls should look or behave differently from the majority of the application), the StandardPropertiesResourceManager
offers a suitable solution.
Like the TranslationResourceManager
in the previous section, the Standard PropertiesResourceManager
uses a “workhorse” resource manager to perform the majority of the work of a resource manager. The part that the Standard PropertiesResourceManager
adds to the process is that for specific properties it steps in and overrides the workhorse resource manager’s value and sets its own value.
Here’s how it works. Like the TranslationResourceManager
, the Standard PropertiesResourceManager
inherits from the SurrogateResourceManager
to handle the job of passing requests on to the “workhorse” resource manager. The essential structure looks like this:
public class StandardPropertiesResourceManager:
SurrogateResourceManager
{
private static Font font;
private static ImeMode imeMode = (ImeMode) -1;
private static RightToLeft rightToLeft = (RightToLeft) -1;
public StandardPropertiesResourceManager(
string baseName, Assembly assembly):
base(baseName, assembly)
{
}
public StandardPropertiesResourceManager(string baseName):
base(baseName)
{
}
public StandardPropertiesResourceManager(Type resourceType):
base(resourceType)
{
}
public static Font Font
{
get {return font;}
set {font = value;}
}
public static ImeMode ImeMode
{
get {return imeMode;}
set {imeMode = value;}
}
public static RightToLeft RightToLeft
{
get {return rightToLeft;}
set {rightToLeft = value;}
}
}
There are a few static properties (Font
, ImeMode
, RightToLeft
) that map onto equivalent static fields to hold the application-wide settings. You can see from this code that it would be a simple matter to add new application-wide settings to this list. The remainder of the class simply overrides the GetObject
method to substitute requests for application-wide properties:
protected virtual bool SubstituteValue(string name,
CultureInfo culture, object value, object substituteValue,
string propertyName)
{
if (value == null)
return name.EndsWith("." + propertyName) &&
substituteValue != null;
else
return substituteValue != null &&
value.GetType().IsSubclassOf(substituteValue.GetType());
}
protected virtual bool SubstituteValue(
string name, CultureInfo culture, int value,
int substituteValue, string propertyName)
{
return name.EndsWith("." + propertyName) &&
substituteValue != -1;
}
public override object GetObject(string name, CultureInfo culture)
{
object obj = base.GetObject(name, culture);
if (SubstituteValue(name, culture, obj, font, "Font"))
return font;
if (SubstituteValue(name, culture,
(int) obj, (int) imeMode, "ImeMode"))
return imeMode;
if (SubstituteValue(name, culture,
(int) obj, (int) rightToLeft, "RightToLeft"))
return rightToLeft;
return obj;
}
The GetObject
method gets the requested value from the workhorse resource manager. It then checks to see whether it is one of the application-wide values that it should substitute; if it is, it substitutes its own value.
You have learned from this chapter so far that the ResourceManager
and ComponentResourceManager
classes provided with the .NET Framework are not the only resource managers in the world. Maybe you will choose to use one of the resource managers in this chapter, or maybe you have been inspired to write one of your own. What is a certainty, however, is that there is too much potential in this idea to live with committing your project to a specific resource manager. Furthermore, everything changes, and what is certain is that new resource managers will be written either by Microsoft or by third parties, and you will want to throw away your old, worn-out yesteryear resource managers in favour of the latest widget. This leaves us with a new problem: How can we use resource managers where we do not commit ourselves to a given resource manager class?
This is the same problem that faces ADO.NET: How can you use ADO.NET classes without committing to a given data provider? ADO.NET solves this problem in ADO.NET 2 using DbProviderFactory
classes, and we implement a similar concept here. For ASP.NET 2.0 applications, be sure to read the section “Using Custom Resource Managers in ASP.NET 2.0,” later in this chapter.
The ResourceManagerProvider
class has only static methods, a static field, and a static property:
public class ResourceManagerProvider
{
private static Type resourceManagerType =
typeof(ComponentResourceManager);
public static ComponentResourceManager
GetResourceManager(Type type)
{
return (ComponentResourceManager) Activator.CreateInstance(
resourceManagerType, new object[] {type});
}
public static ComponentResourceManager GetResourceManager(
string baseName, Assembly assembly)
{
return (ComponentResourceManager) Activator.CreateInstance(
resourceManagerType, new object[] {baseName, assembly});
}
public static ComponentResourceManager GetResourceManager(
string baseName, Assembly assembly, Type usingResourceSet)
{
return (ComponentResourceManager) Activator.CreateInstance(
resourceManagerType, new object[]
{baseName, assembly, usingResourceSet});
}
public static Type ResourceManagerType
{
get {return resourceManagerType;}
set
{
if (value.FullName ==
"System.ComponentModel.ComponentResourceManager" ||
value.IsSubclassOf(typeof(ComponentResourceManager)))
resourceManagerType = value;
else
throw new ApplicationException(
"ResourceManagerType must be a sub class of "+
"System.ComponentModel."+
"ComponentResourceManager");
}
}
}
The public static ResourceManagerType
property maps onto the private static resourceManagerType
field and holds the Type
used to create new resource managers. The private field defaults to the ComponentResourceManager Type
, but the public property can be set like this:
ResourceManagerProvider.ResourceManagerType =
typeof(DbResourceManager);
You would make this assignment in the application’s startup process.
The remaining static GetResourceManager
methods all create a new resource manager from the specific resource manager type and differ only in the parameters that they accept and pass on to the resource manager constructors.
So instead of writing this:
ResourceManager resources =
new ResourceManager(typeof(CustomerBusinessObject));
you could write this:
ResourceManager resources =
ResourceManagerProvider.GetResourceManager(
typeof(CustomerBusinessObject));
The benefit of this approach is that you can change an entire application’s resource manager classes to a different class by changing a single line of code:
ResourceManagerProvider.ResourceManagerType =
typeof(TranslationResourceManager);
A translator, for example, could use the ResourcesResourceManager
class during the translation process, and the live version of the application could use the ComponentResourceManager
class.
If you like the idea of using the ResourceManagerProvider
, you might wonder whether you have managed to track down all of the cases of creating a new resource manager object throughout your application and changed them to use Resource ManagerProvider
, or whether you have let any fall through the net. If so, take a look at Chapter 13, “Testing Internationalization Using FxCop,” and the “ResourceManager not provided by provider” rule that exists to find these rogue bits of code.
The ResourceManagerProvider
is all fine and dandy, but Windows Forms developers have an additional hurdle to overcome if it is to be used in a Windows form. The problem lies in the very first line of the form’s InitializeComponent
method when Form.Localizable
is true
:
// Visual Studio 2003
System.Resources.ResourceManager resources =
new System.Resources.ResourceManager(typeof(Form1));
// Visual Studio 2005
System.ComponentModel.ComponentResourceManager resources =
new System.ComponentModel.ComponentResourceManager(
typeof(Form1));
Clearly, the Visual Studio designer doesn’t respect our new ResourceManager Provider
class and blindly uses good old ResourceManager
or Component ResourceManager
. This really doesn’t help us much if we want to get our resources from, say, a database. What we need is for Visual Studio to generate code that uses our ResourceManagerProvider
instead of ResourceManager
/Component ResourceManager
. There are two possible solutions to this problem. The first is that we can write a Visual Studio add-in that modifies or replaces the code generator for the Windows Forms designer. I decided against this approach because the second solution is much easier. The second solution is that we can resign ourselves to the fact that Visual Studio is going to write code that we don’t want it to, but we can try to correct the problem before the resource manager gets used. This is the solution that we implement in this chapter.
It is not very well known that it is possible to inject completely new code into the InitializeComponent
method. The goal of our solution, therefore, is to inject the following new line of code into the InitializeComponent
method:
resources = Internationalization.Resources.
ResourceManagerProvider.GetResourceManager(typeof(Form1));
(“Form1
” is the name of the form class.) This code assigns a new value to the local resources variable. It is true that the InitializeComponent
method will first create a redundant ResourceManager
or ComponentResourceManager
that will not be used and will be dereferenced when we subsequently assign a new resource manager from our ResourceManagerProvider.GetResourceManager
method, and this is wasteful. But it is also true that it solves our problem.
The secret to injecting this new code into InitializeComponent
lies in creating a component that has custom serialization code. You can achieve this with the DesignerSerializer
attribute:
[DesignerSerializer(typeof(ResourceManagerSetterSerializer),
typeof(CodeDomSerializer))]
public class ResourceManagerSetter : System.ComponentModel.Component
{
}
Our new ResourceManagerSetter
class inherits from Component
and, therefore, sits in the nonvisual area of the form designer. It has no properties, so there is nothing to serialize. Its only presence so far is that, like any component, the form class has a private field:
private Internationalization.Resources.ResourceManagerSetter
resourceManagerSetter1;
And the field is initialized in InitializeComponent
:
this.resourceManagerSetter1 =
new Internationalization.Resources.ResourceManagerSetter();
The DesignerSerializer
attribute tells the form designer to serialize this component using the ResourceManagerSetterSerializer
class, and that this class is a kind of CodeDomSerializer
. CodeDom
(“code document object model”) is a .NET technology that enables you to create classes from which code can be generated. It is a great technology that is used throughout Visual Studio, and although it can require a little thought to get started, it has enormous potential and is well worth mastering. The ResourceManagerSetter
class is now complete.
All of the action occurs in the ResourceManagerSetterSerializer
class:
public class ResourceManagerSetterSerializer : CodeDomSerializer
{
public override object Deserialize(
IDesignerSerializationManager manager, object codeDomObject)
{
CodeDomSerializer baseSerializer = (CodeDomSerializer)
manager.GetSerializer(typeof(ResourceManagerSetter).
BaseType, typeof(CodeDomSerializer));
return
baseSerializer.Deserialize(manager, codeDomObject);
}
public override object Serialize(
IDesignerSerializationManager manager, object value)
{
CodeDomSerializer baseSerializer = (CodeDomSerializer)
manager.GetSerializer(typeof(ResourceManagerSetter).
BaseType, typeof(CodeDomSerializer));
object codeObject = baseSerializer.Serialize(manager, value);
if (codeObject is CodeStatementCollection)
{
CodeStatementCollection statements =
(CodeStatementCollection) codeObject;
CodeExpression leftCodeExpression =
new CodeVariableReferenceExpression("resources");
CodeTypeDeclaration classTypeDeclaration =
(CodeTypeDeclaration) manager.GetService(
typeof(CodeTypeDeclaration));
CodeExpression typeofExpression =
new CodeTypeOfExpression(classTypeDeclaration.Name);
CodeExpression rightCodeExpression =
new CodeMethodInvokeExpression(
new CodeTypeReferenceExpression(
"Internationalization.Resources."+
"ResourceManagerProvider"),
"GetResourceManager",
new CodeExpression[] {typeofExpression});
statements.Insert(0, new CodeAssignStatement(
leftCodeExpression, rightCodeExpression));
}
return codeObject;
}
}
This class overrides the Deserialize
method, which creates a new CodeDom Serializer
and returns it. This is a standard implementation of the Deserialize
method, and this implementation offers nothing new. The Serialize
method returns a collection of CodeDom
statements. These statements can be anything. The result is placed in the InitializeComponent
method verbatim. Our implementation simply generates a single statement. The statement is an assignment statement that invokes the ResourceManagerProvider.GetResourceManager
method and assigns the result to a variable called “resources
”. The majority of the code in this method is CodeDom
code; you might want to study the documentation on CodeDom
if there is anything that you don’t follow. The following line, however, is worth pointing out:
CodeTypeDeclaration classTypeDeclaration =
(CodeTypeDeclaration) manager.GetService(
typeof(CodeTypeDeclaration));
Recall from the line of code that we want to generate that we need to know what class we are generating the resource manager for. We need to be able to generate something like “typeof(Form1)
”, but we don’t know what the name of the form class is. This line uses the GetService
method of the IDesignerSerializationManager
object passed to the Serialize
method to get the CodeTypeDeclaration
for the form.
The only caveat for the ResourceManagerSetter
component is that it must be the first component on the form; otherwise, the assignment to the “resources
” variable will occur too late and the “temporary” ResourceManager
/ComponentResourceManager
will not be so temporary, as it gets used to retrieve resources until our component kicks in. You can see the problem here where a Button
gets created before the ResourceManagerSetter
has had a chance to assign a new value to the “resources
” variable:
System.Resources.ResourceManager resources =
new System.Resources.ResourceManager(typeof(Form1));
this.button1 = new System.Windows.Forms.Button();
this.resourceManagerSetter1 =
new Internationalization.Resources.ResourceManagerSetter();
this.SuspendLayout();
//
// button1
//
this.button1.AccessibleDescription =
resources.GetString("button1.AccessibleDescription");
this.button1.AccessibleName =
resources.GetString("button1.AccessibleName");
etc.
etc.
this.button1.Text = resources.GetString("button1.Text");
this.button1.TextAlign = ((System.Drawing.ContentAlignment)
(resources.GetObject("button1.TextAlign")));
this.button1.Visible =
((bool)(resources.GetObject("button1.Visible")));
resources = Internationalization.Resources.
ResourceManagerProvider.GetResourceManager(typeof(Form1));
The call to resources.GetString
to assign the button’s Text
property will use the assembly-based resource manager.
One minor fly in the ointment with the ResourceManagerSetter
approach is that Visual Studio spots that we have done something unusual and reports in the Task List (see Figure 12.6). You can just ignore this warning.
Figure 12.6. Visual Studio 2005 Objecting to ResourceManagerSetter
Finally, if you use WinRes to open forms that have a ResourceManagerSetter
, remember that WinRes must be able to find all of the assemblies that are used by the form. So WinRes must be able to find the ResourceManagerSetter
assembly; otherwise, it will display its unhelpful “Object reference not set to an instance of an object” error. Of course, there would be little value in using WinRes to open such forms anyway because WinRes cannot read from resources other than resx files. This limitation lends more weight to the argument to write a WinRes replacement.
Way back in Chapter 3, “An Introduction to Internationalization,” I introduced strongly typed resources. You might remember that they are new in .NET Framework 2.0, but as they are such a good idea, I wrote a similar utility for the .NET Framework 1.1 so that everyone could share in the joy that is strongly typed resources. You might also recall that resgen, the console application that generates strongly typed resources, accepts input only from resx files. Having spent all of this time writing custom resource managers, you might wonder how we can generate strongly typed resources from a source other than a resx file. The answer is to build our own resgen utility. In the .NET Framework 2.0, this isn’t as difficult as it sounds (I return to the .NET Framework 1.1 in a moment). The .NET Framework 2.0 class StronglyTypedResourceBuilder
, upon which resgen is built (among other classes), has overloaded Create
methods that accept different parameters to identify resources. One overloaded Create
clearly accepts resx files as input, and this is of no value to us. However, another overloaded Create
accepts an IDictionary
of resources, and it is this method that solves our problem. Here is a bare-bones implementation of ResClassGen
, a replacement for resgen that just generates strongly typed classes from resources.
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Collections;
using System.Resources;
using System.Resources.Tools;
using System.CodeDom;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
namespace Tools.ResClassGen
{
class Program
{
static void Main(string[] args)
{
if (args.GetLength(0) < 1 || !File.Exists(args[0]))
ShowSyntax();
else
{
string nameSpace;
if (args.GetLength(0) > 1)
nameSpace = args[1];
else
nameSpace = String.Empty;
StronglyTypedResourceBuilderHelper.GenerateClass(
args[0], nameSpace);
}
}
private static void ShowSyntax()
{
Console.WriteLine("Syntax:");
Console.WriteLine(
"ResClassGen <ResxFilename> [<NameSpace>]");
Console.WriteLine("Example:");
Console.WriteLine(
"ResClassGen Strings.resx WindowsApplication1");
}
}
}
This skeleton simply checks the parameters and calls the Strongly TypedResourceBuilderHelper.GenerateClass
method to do the work:
public static void GenerateClass(
string resxFilename, string nameSpace)
{
string[] unmatchable;
Hashtable resources = GetResources(resxFilename);
string className =
Path.GetFileNameWithoutExtension(resxFilename);
CodeDomProvider codeDomProvider = new CSharpCodeProvider();
CodeCompileUnit codeCompileUnit =
StronglyTypedResourceBuilder.Create(
resources, className, nameSpace,
codeDomProvider, false, out unmatchable);
string classFilename = Path.ChangeExtension(resxFilename, ".cs");
using (TextWriter writer = new StreamWriter(classFilename))
{
codeDomProvider.GenerateCodeFromCompileUnit(
codeCompileUnit, writer, new CodeGeneratorOptions());
}
}
The GenerateClass
method calls GetResources
to get a Hashtable
of resources. (I return to GetResources
in a moment.) It passes the Hashtable
, which supports the IDictionary
interface, to StronglyTypedResourceBuilder. Create
. The remaining parameters to this Create
method simply identify the name of the class, its namespace, the CodeDom
provider used to generate the class, whether the class is internal, and a parameter into which all of the unmatchable resources are placed. The return result from the Create
method is a CodeCompileUnit
that contains the complete CodeDom
graph (a tree of code instructions) from which the code can be generated. The GenerateClass
method creates a TextWriter
to write out the code and calls CodeDomProvider.GenerateCodeFromCompileUnit
to output the code to the TextWriter
.
In the .NET Framework 1.1, the GenerateCodeFromCompileUnit
method is not available directly from the CodeDomProvider
. Instead, you can create an ICode-Provider
using CodeDomProvider.CreateGenerator
, and call the same method with the same parameters from the resulting ICodeProvider
.
The only question remaining is how to load the resources:
private static Hashtable GetResources(string resxFilename)
{
ResXResourceReader reader = new ResXResourceReader(resxFilename);
Hashtable resources = new Hashtable();
try
{
IDictionaryEnumerator enumerator = reader.GetEnumerator();
while (enumerator.MoveNext())
{
resources.Add(
enumerator.Key.ToString(), enumerator.Value);
}
}
finally
{
reader.Close();
}
return resources;
}
This implementation simply uses a ResXResourceReader
to read the resources from the specified resx file. Consequently, this implementation offers no benefits beyond the resgen implementation. However, you can see that by changing this method to use, say, a DbResourceReader
instead of a ResXResourceReader
, the ResClassGen
utility would be able to read from your own resources.
If you are using the .NET Framework 1.1, there is no StronglyTypedResource Builder
class, but recall from Chapter 3 that I wrote an equivalent class so that no one had to miss out. So the previous code works equally well in both versions of the framework.
There is one more issue to attend to. If you have an excellent memory, you might remember one of the lines in the code that gets generated for the strongly typed resource class:
System.Resources.ResourceManager temp =
new System.Resources.ResourceManager(
"WindowsApplication1.strings", typeof(strings).Assembly);
Clearly, this line isn’t very helpful to those of us who write custom resource managers because the code uses the System.Resources.ResourceManager
class. We want it to be this:
System.Resources.ResourceManager temp =
Internationalization.Resources.ResourceManagerProvider.
GetResourceManager(
"WindowsApplication1.strings", typeof(strings).Assembly);
The bad news is that the StronglyTypedResourceBuilder
class has no facility for allowing us to specify what resource manager class to use or how to create a resource manager. If you modify the generated code, it will be overwritten the next time the resource is generated. However, all is not lost. The StronglyTypedResource Builder.Create
method generates a CodeCompileUnit
that, as we have seen, is a collection of all of the instructions from which the strongly typed resource class is generated. The solution lies in modifying the resulting CodeCompileUnit
before it is passed to the CodeDomProvider.GenerateCodeFromCompileUnit
method. To follow this code, you need a little familiarity with CodeDom
. We start by adding a line immediately after the call to StronglyTypedResourceBuilder.Create
:
ChangeResourceManager(className, codeCompileUnit);
This represents our point at which we start altering the CodeDom
graph. ChangeResourceManager
looks like this:
private static void ChangeResourceManager(
string className, CodeCompileUnit codeCompileUnit)
{
CodeNamespace codeNamespace = codeCompileUnit.Namespaces[0];
CodeTypeDeclaration codeTypeDeclaration = codeNamespace.Types[0];
CodeMemberProperty codeMemberProperty = GetCodeMemberProperty(
codeTypeDeclaration, "ResourceManager");
if (codeMemberProperty != null)
ChangeResourceManagerGetStatements(codeNamespace,
codeTypeDeclaration, codeMemberProperty);
}
We take an educated guess that the first namespace in the CodeDom
graph contains the generated class and that the first type in the namespace is the resource class; these guesses are accurate, given the current state of StronglyTypedResource-Builder
. We call GetCodeMemberProperty
to get the ResourceManager
property (GetCodeMemberProperty
simply iterates through all of the members looking for a property called “ResourceManager
”). If we get the property, we call ChangeResourceManagerGetStatements
, which actually modifies the CodeDom
statements for the ResourceManager
’s get
method:
private static void ChangeResourceManagerGetStatements(
CodeTypeDeclaration codeTypeDeclaration,
CodeMemberProperty codeMemberProperty)
(
CodeTypeReference resourceManagerTypeReference =
new CodeTypeReference(typeof(ResourceManager));
CodeFieldReferenceExpression resMgrFieldReferenceExpression =
new CodeFieldReferenceExpression(null, "resourceMan");
CodeExpression ifExpression =
new CodeBinaryOperatorExpression(
resMgrFieldReferenceExpression,
CodeBinaryOperatorType.IdentityEquality,
new CodePrimitiveExpression(null));
CodePropertyReferenceExpression typeOfExpression =
new CodePropertyReferenceExpression(
new CodeTypeOfExpression(
new CodeTypeReference(codeTypeDeclaration.Name)),
"Assembly");
CodeExpression[] resourceManagerParameterExpressions =
new CodeExpression[2]
{
new CodePrimitiveExpression(
codeNamespace.Name + "." + codeTypeDeclaration.Name),
typeOfExpression
};
CodeExpression newResourceManagerExpression =
new CodeMethodInvokeExpression(
new CodeTypeReferenceExpression(
"Internationalization.Resources.ResourceManagerProvider"),
"GetResourceManager",
resourceManagerParameterExpressions);
CodeStatement[] ifStatements = new CodeStatement[2]
{
new CodeVariableDeclarationStatement(
resourceManagerTypeReference, "temp",
newResourceManagerExpression),
new CodeAssignStatement(resMgrFieldReferenceExpression,
new CodeVariableReferenceExpression("temp"))
};
CodeStatementCollection statements =
new CodeStatementCollection();
statements.Add(
new CodeConditionStatement(ifExpression, ifStatements));
statements.Add(new CodeMethodReturnStatement(
resMgrFieldReferenceExpression));
codeMemberProperty.GetStatements.Clear();
codeMemberProperty.GetStatements.AddRange(statements);
}
This code doesn’t worry about what the existing CodeDom
instructions are for the get
method; it simply throws them away and replaces them with a new set. The new set is very similar to the previous set, with the exception that the resource manager is created from ResourceManagerProvider.GetResourceManager
instead of System.Resources.ResourceManager
.
As we saw in Chapter 5, “ASP.NET Specifics,” Microsoft has made significant advances in ASP.NET 2.0, particularly in the area of internationalization. The issue of interest to us in this chapter is the introduction of a resource provider model. This section discusses how the existing model works and how we can write resource providers that plug into this model to use the resource managers that we have created in this chapter. We start with a description of ASP.NET’s Resource Provider Model; then we implement a new resource provider that mimics the behavior of the existing provider. Finally, we implement a provider for the DbResourceManager
from this chapter. From these examples, you should understand the mechanism sufficiently to write a resource provider for any of the custom resource managers in this chapter.
ASP.NET 2.0 uses the ResourceManager
class, by default, for retrieving all resources from resource assemblies, both fallback and satellite. The model described in Chapter 3 is still true for ASP.NET 2.0. However, ASP.NET 2.0 allows developers to specify a resource provider that is responsible for providing localized resources. By default, the existing provider returns ResourceManager
objects, but the model allows us to override this behavior. The essential processes that the Resource Manager
class executes are still true for ASP.NET:
• Resources are created from resx/resources/restext/txt files.
• Resources are embedded in an assembly.
• Resources are accessed using the ResourceManager
class.
• The ResourceManager uses the fallback process we are familiar with.
There are, however, two differences that ASP.NET 2.0 must cope with:
• ASP.NET applications are compiled to temporary directories with generated names, so code that loads resources from these assemblies needs to use these generated names.
• ASP.NET applications have both global and local resources, and these resources are placed in separate assemblies.
Before we get into the resource provider mechanism, let’s put some flesh on what this means to an ASP.NET application. In Visual Studio 2005, create a new WebSite; add a button and some controls to the Default page; and select Tools, Generate Local Resource to generate local resources. Create a French version of the page by adding a Default.aspx.fr.resx
file to the App_LocalResources
folder and include a new entry called Button1Resources.Text
for the French version of the button. Now add a new global resource file called ProductInfo.resx
to the App_Global Resources
folder. Add a key called Name
and give it a value. Add a second resource file called ProductInfo.fr.resx
to the App_GlobalResources
folder, with a French value for the Name
key. Set your browser’s Language Preference to French. When the Web site runs, the resources will be compiled into resource assemblies. In our example, there will be four separate resource assemblies:
• The Global fallback resource assembly
• The Global French resource assembly
• The Local fallback resource assembly
• The Local French resource assembly
The Global resource assembly path is determined by the following formula:
<Temporary ASP.NET Folder><WebSiteName><GeneratedName1>
<GeneratedName2>App_GlobalResources.<GeneratedName3>.dll
So if <Temporary ASP.NET Folder>
is “C:WINDOWSMicrosoft.NETFrameworkv2.0.50727Temporary ASP.NET Files
” and the <WebSiteName>
is “WebSite1
”, the Global fallback assembly could be something like this:
C:WINDOWSMicrosoft.NETFrameworkv2.0.50727Temporary ASP.NET
Fileswebsite162a3b0ac1072591cApp_GlobalResources.w1qus9s2.dll
The Global French resource assembly is placed relative to the fallback assemblies’ folder in the fr
folder and given the “.resources.dll
” extension; in this example, it would be:
C:WINDOWSMicrosoft.NETFrameworkv2.0.50727Temporary ASP.NET
Fileswebsite162a3b0ac1072591cfrApp_GlobalResources.w1qus9s2.resources.dll
The Local fallback resource assembly path is determined by a similar formula:
<Temporary ASP.NET Folder><WebSiteName><GeneratedName1>
<GeneratedName2>App_LocalResources.<FolderName>.<GeneratedName4>.dll
The <FolderName>
is the name of folder where the .aspx
files reside, where “root
” is used for the Web site’s root. So the Local fallback assembly could be something like this:
C:WINDOWSMicrosoft.NETFrameworkv2.0.50727Temporary ASP.NET
Fileswebsite162a3b0ac1072591cApp_LocalResources.root.ogd3clye.dll
Following the same practice as the Global satellite resource assembly, the Local French resource assembly is placed in the fr
folder and given the “.resources.dll”
extension; in this example, it would be:
C:WINDOWSMicrosoft.NETFrameworkv2.0.50727Temporary ASP.NET
Fileswebsite162a3b0ac1072591cfrApp_LocalResources.root.ogd3clye.resources.dll
With these challenges in mind, we can look at how the ASP.NET Resource Provider model works.
The Resource Provider story starts with the ResourceProviderFactory
abstract class. This class has a single implementation in the .NET Framework 2.0—namely, ResXResourceProviderFactory
(see Figure 12.7).
Figure 12.7. ResourceProviderFactory Class Hierarchy
ResXResourceProviderFactory
is the default factory and is the factory that has been in use in all of the ASP.NET 2.0 Web sites in this book up to this point. The ResourceProviderClass
has two methods that must be overridden by the subclass:
public abstract IResourceProvider CreateGlobalResourceProvider(
string classKey);
public abstract IResourceProvider CreateLocalResourceProvider(
string virtualPath);
These methods return an IResourceProvider
interface. IResourceProvider
is a simple interface:
public interface IResourceProvider
{
object GetObject(string resourceKey, CultureInfo culture);
IResourceReader ResourceReader { get; }
}
So the ResourceProviderFactory
must return objects that support a GetObject
method and a ResourceReader
property. The ResXResourceProviderFactory
creates a new GlobalResXResourceProvider
object when its CreateGlobalResource Provider
method is called and a LocalResXResourceProvider
object when its CreateLocalResourceProvider
method is called.
Figure 12.8 shows the class hierarchy for the classes that support IResource Provider
in the .NET Framework 2.0.
Figure 12.8. Implementations of IResourceProvider
The BaseResXResourceProvider
implements the GetObject
method and ResourceReader
property required by the IResourceReader
. The GetObject
method calls an abstract method called CreateResourceManager
to create a ResourceManager
object and store it in a private field, and then calls the Resource Manager
’s GetObject
method. The GlobalResXResourceProvider
and Local ResXResourceProvider
classes both override the CreateResourceManager
method to create a ResourceManager
, using the correct resource name and the correct assembly. The GlobalResXResourceProvider
overrides the ResourceReader
property to throw a NotSupportedException
. This doesn’t affect the normal execution of a Web site because the IResourceProvider.ResourceReader
property is not called by the .NET Framework 2.0 for global resources. The LocalResX ResourceProvider
overrides the ResourceReader
property to return a Resource Reader
to read the relevant resource from the assembly.
The ResourceProviderFactory
class can be set in the web.config
’s globalization section using the resourceProviderFactoryType
attribute. The syntax is:
<globalization resourceProviderFactoryType=
[FullClassName[, Assembly]]/>
So in the next example, our ResourceProviderFactory
class is Internationalization.Resources.Web.ResourceManagerResourceProviderFactory
, and it is in an assembly called ResourceProviderFactories
; the complete web.config
is:
<configuration
xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<appSettings/>
<connectionStrings/>
<system.web>
<globalization resourceProviderFactoryType=
"Internationalization.Resources.Web.
ResourceManagerResourceProviderFactory,
ResourceProviderFactories"/>
</system.web>
</configuration>
Note that the assembly name must not include the .dll
extension, and the Resource ProviderFactories
assembly must be available to the Web site, so it should be either installed in the GAC or added to the Web site’s bin
folder (you can do this by adding the ResourceProviderFactories
project to the Web site’s references).
Alternatively, if you include the factory class in the Web site’s App_Code
folder, you do not need to specify the assembly in the resourceProviderFactoryType
setting.
To see how this works in practice, we’ll create a ResourceManagerResource ProviderFactory
. This class mimics the behavior of the ResXResourceProvider Factory
and gives us an insight into how the default provider solves its problems. In the subsequent section, we create a provider factory for the DbResourceManager
class that we wrote in this chapter. The ResourceManagerResourceProviderFactory
class is as follows:
public class ResourceManagerResourceProviderFactory:
ResourceProviderFactory
{
public ResourceManagerResourceProviderFactory()
{
}
public override IResourceProvider
CreateGlobalResourceProvider(string classKey)
{
return new GlobalResourceManagerResourceProvider(classKey);
}
public override IResourceProvider
CreateLocalResourceProvider(string virtualPath)
{
return new LocalResourceManagerResourceProvider(virtualPath);
}
}
This simple class returns a new GlobalResourceManagerResourceProvider
object and a new LocalResourceManagerResourceProvider
object for its two methods. The classKey
parameter provided to the CreateGlobalResourceProvider
method will be “ProductInfo
” in our example. The virtualPath
parameter provided to the CreateLocalResourceProvider
method will be “/WebSite1/Default.aspx
” in our example. Figure 12.9 shows the class hierarchy of the IResourceProvider
implementations required for our ResourceManager
and DbResourceManager
implementations.
Figure 12.9. Class Hierarchy of Custom Implementations of IResourceProvider
Both of the IResourceProvider
implementations inherit indirectly from the abstract BaseResourceProvider
class:
public abstract class BaseResourceProvider : IResourceProvider
{
private ResourceManager resourceManager;
protected ResourceManager ResourceManager
{
get
{
if (resourceManager == null)
resourceManager = CreateResourceManager();
return resourceManager;
}
}
protected abstract ResourceManager CreateResourceManager();
public object GetObject(string resourceKey,
System.Globalization.CultureInfo culture)
{
return ResourceManager.GetObject(resourceKey, culture);
}
public System.Resources.IResourceReader ResourceReader
{
get { throw new NotSupportedException(); }
}
}
BaseResourceProvider
has a ResourceManager
property that initializes a private resourceManager
field by calling the abstract CreateResourceManager
method. It implements the GetObject
method to call the ResourceManager
’s GetObject
method, and it implements the ResourceReader
property to throw a NotSupported Exception
. The BaseResourceProvider
class is used in this example and also the next example to create a ResourceProviderFactory
for the DbResourceManager
class. The BaseResourceManagerResourceProvider
class implements the Create ResourceManager
method and provides a GetInternalStaticProperty
method:
public abstract class BaseResourceManagerResourceProvider :
BaseResourceProvider
{
protected override ResourceManager CreateResourceManager()
{
Assembly resourceAssembly = GetResourceAssembly();
if (resourceAssembly == null)
return null;
ResourceManager resourceManager =
new ResourceManager(GetBaseName(), resourceAssembly);
resourceManager.IgnoreCase = true;
return resourceManager;
}
protected abstract string GetBaseName();
protected abstract Assembly GetResourceAssembly();
protected static object GetInternalStaticProperty(
Type type, string propertyName)
{
PropertyInfo propertyInfo =
type.GetProperty(propertyName,
System.Reflection.BindingFlags.Static |
System.Reflection.BindingFlags.NonPublic);
if (propertyInfo == null)
return null;
else
return propertyInfo.GetValue(null, null);
}
}
The CreateResourceManager
method calls the abstract GetBaseName
method to get the name of the resource, and the abstract GetResourceAssembly
to get the assembly that contains the resources. These two methods represent the only differences between the “global” resource manager and the “local” resource manager. The GetInternalStaticProperty
method is a workaround for BuildManager
and BuildResult
classes, hiding information from us that we need to implement this solution. It uses reflection to obtain the value of internal static properties.
With this infrastructure in place, the GlobalResourceManagerResource Provider
class is simple:
public class GlobalResourceManagerResourceProvider :
BaseResourceManagerResourceProvider
{
private string classKey;
public GlobalResourceManagerResourceProvider(string classKey)
{
this.classKey = classKey;
}
protected override string GetBaseName()
{
return "Resources." + classKey;
}
protected override Assembly GetResourceAssembly()
{
return (Assembly) GetInternalStaticProperty(
typeof(BuildManager), "AppResourcesAssembly");
}
}
The GetBaseName
returns “Resources
” plus the classKey
, so if classKey
is “ProductInfo
”, then the base name will be “Resources.ProductInfo
”. The GetResourceAssembly
method gets the resource assembly from the Build Manager
’s internal static AppResourcesAssembly
property. The BuildManager
is the class that is responsible for building the Web site when it is run.
The LocalResourceManagerResourceProvider
class isn’t quite so simple. Here is an abbreviated version of it (see the source code for the book for the complete version):
public class LocalResourceManagerResourceProvider :
BaseResourceManagerResourceProvider
{
private string virtualPath;
public LocalResourceManagerResourceProvider(
string virtualPath)
{
this.virtualPath = virtualPath;
}
protected override string GetBaseName()
{
return Path.GetFileName(virtualPath);
}
protected override Assembly GetResourceAssembly()
{
string virtualPathParent = GetVirtualPathParent();
string localAssemblyName =
GetLocalResourceAssemblyName(virtualPathParent);
Object buildResult = GetBuildResultFromCache(cacheKey);
if (buildResult != null)
return GetBuildResultResultAssembly(buildResult);
return null;
}
}
The GetBaseName
method returns the base name from the virtual path. So if the virtual path is “/WebSite1/Default.aspx
”, the base name is “Default
”. The GetResourceAssembly
method has the job of finding the local resource assembly, given that its path and part of its name has been generated on the fly. We’ll take it line by line using our example. GetVirtualPathParent
returns “/WebSite1
”. GetLocalResourceAssemblyName
returns “App_LocalResources.root
”, assuming that the .aspx
files are located in the root. GetBuildResultAssembly
returns the Assembly
object from the assembly name. Each of these methods is implemented in the LocalResourceManagerResourceProvider
class. Our implementation of a ResourceProviderFactory
and its associated classes is complete. Our class mimics the behavior of the .NET Framework 2.0’s ResXResourceProviderFactory
.
Our DbResourceManagerResourceProviderFactory
solution isn’t nearly as complex as the ResourceManagerResourceProviderFactory
solution. The main difference between the two implementations lies in a decision that the ResourceSets
table in our localization database will contain both global and local resources, so it is not necessary for us to make a distinction between the two. So in this example, we need to implement only one IResourceProvider
class because the one class will suffice for both global and local resources. Here is the DbResourceManager ResourceProviderFactory
:
public class DbResourceManagerResourceProviderFactory :
ResourceProviderFactory
{
public DbResourceManagerResourceProviderFactory()
{
}
public override IResourceProvider
CreateGlobalResourceProvider(string classKey)
{
return new DbResourceManagerResourceProvider(classKey);
}
public override IResourceProvider
CreateLocalResourceProvider(string virtualPath)
{
string classKey = Path.GetFileName(virtualPath);
if (classKey.ToUpper().EndsWith(".ASPX"))
// strip off the .aspx extension
classKey = classKey.Substring(0, classKey.Length - 5);
return new DbResourceManagerResourceProvider(classKey);
}
}
The CreateGlobalResourceProvider
method simply returns a new DbResource ManagerResourceProvider
object, passing in the class key (e.g., “ProductInfo
”). The CreateLocalResourceProvider
method needs to convert the virtualPath
(e.g., “/WebSite1/Default.aspx
”) into a class key (e.g., “Default
”) by stripping off the path and the .aspx
extension. The DbResourceManagerResourceProvider
class inherits from the BaseResourceProvider
class that we created in the previous section; therefore, it only needs to implement the CreateResourceManager
method:
public class DbResourceManagerResourceProvider :
BaseResourceProvider
{
private string classKey;
public DbResourceManagerResourceProvider(string classKey)
{
this.classKey = classKey;
}
protected override ResourceManager CreateResourceManager()
{
DbResourceManager resourceManager =
new DbResourceManager(classKey);
resourceManager.IgnoreCase = true;
return resourceManager;
}
}
The CreateResourceManager
method simply creates a new DbResource Manager
and passes it the class key. Our implementation is complete. Armed with these examples, you should be able to create a ResourceProviderFactory
and its associated classes for any custom resource manager.
In this chapter, you learned how to create custom resource managers. Simple resource managers require only a relatively small effort, but as the complexity increases and the need to create writeable resource managers arises, a greater depth of ResourceManager
internals is required. Unfortunately, as good as these facilities are in the .NET Framework, only ASP.NET 2.0 has a concept of a resource manager provider. Windows Forms applications are unable to easily make use of custom resource managers. The code generated by Visual Studio for Windows Forms needs care and attention; there is no provider class for resource managers as there is for ADO.NET classes, and WinRes is closed beyond help. In addition, the Strongly TypedResourceBuilder
class and resgen utility need additional work to make them viable for non-resx resources. With a little effort and trickery, we can overcome these limitations and give applications better functionality. Finally, we looked at ASP.NET 2.0’s resource provider model, how it works, and how to create a custom resource provider.
3.144.34.85