12. Custom Resource Managers

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.

ResourceManager.CreateFileBasedResourceManager

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.

Incorporating resgen into the Build Process for Windows Forms

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

image

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.

Incorporating resgen into the Build Process in ASP.NET 2.0

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 Build Process in ASP.NET 1.1

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

image

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

image

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.

ResourceManager.CreateFileBasedResourceManager in Practice

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.

ResourceManager Exposed

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

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

ResourceManager.GetString

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

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


default culture to CultureInfo.CurrentUICulture

get ResourceSet for the given culture from
ResourceManager.InternalGetResourceSet

if a ResourceSet is returned and ResourceSet.GetString returns
a string for the given key
    return the string

while (culture is not either invariant
culture or neutral resources language)
    change culture to the culture's parent

    get ResourceSet for the culture from
    ResourceManager.InternalGetResourceSet

    if the ResourceSet is null
        return null

    if ResourceSet.GetString returns a string for the given key
        return the string
end while

return null

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

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

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

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

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

ResourceManager.GetString Example

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

Table 12.1. ResourceManager.GetString Example Data

image

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 “enResourceSet, so it goes around the while loop once more, getting the ResourceSet for the invariant culture (i.e., the parent of “en”), and returns “Hi”.

ResourceManager Constructors

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

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


ResourceManager resourceManager =
    new ResourceManager("WindowsApplication1.Strings",
    Assembly.GetExecutingAssembly());

System.Resources.ResourceManager resources =
    new System.Resources.ResourceManager(typeof(Form1));

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

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

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

ResourceManager.InternalGetResourceSet

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

Assembly-Based Resource Managers

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


if _neutralResourcesCulture is null
    assign ResourceManager.GetNeutralResourcesLanguage to it
    set _fallbackLoc to fallback location specified in attribute

if the given culture is the neutral resources language culture
and the fallback location is the main assembly
    assign the invariant culture to the given culture

if the culture is the invariant culture
    if _fallbackLoc is Satellite
        assembly = GetSatelliteAssembly(neutral resources culture)
    else
        assembly = MainAssembly
else if TryLookingForSatellite(given culture)
    assembly = GetSatelliteAssembly(given culture)
else
    assembly = null

resource 'filename' = ResourceManager.GetResourceFileName

load a stream for the given resource 'filename'
using Assembly.GetManifestResourceStream

if this fails to load
    try loading the stream using
    ResourceManager.CaseInsensitiveManifestResourceStreamLookup

if the stream is not null
    create a ResourceSet from the stream using
    ResourceManager.CreateResourceSet

    add the ResourceSet to the Hashtable

    return the ResourceSet

return null

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

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

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

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

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

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

File-Based Resource Managers

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


assert unrestricted FileIOPermission

get the filename and path of the resource
using ResourceManager.FindResourceFile

if a file is found
    create a resource set from the file
    using ResourceManager.CreateResourceSet

    if there is not already an entry in the Hashtable for the culture
        add the ResourceSet to the Hashtable

    return the ResourceSet

if culture is the invariant culture
    throw an exception

get resource set from ResourceManager.InternalGetResourceSet
passing the culture's parent

if the resource set is not null and there is not already an
entry in the Hashtable for the culture
    add the ResourceSet to the Hashtable

    return the ResourceSet

return null

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

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

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

ComponentResourceManager Exposed

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

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

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


public void ApplyResources(object value, string objectName)

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

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

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

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

Custom Resource Managers Examples

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

image

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).

DbResourceManager

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

image

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.

image

And voilà—you have a read-only database resource manager.

ResourcesResourceManager and ResXResourceManager

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)
    {
    }
}

Writeable Resource Managers

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

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();
    }
}

Writeable ResourcesResourceManager

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.

TranslationResourceManager

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.

StandardPropertiesResourceManager

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.

ResourceManagerProvider

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.

Using Custom Resource Managers in Windows Forms

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

image

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.

Generating Strongly-Typed Resources for Sources Other Than resx Files

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.

Generating Strongly-Typed Resources Which Use ResourceManagerProvider

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.

Using Custom Resource Managers in ASP.NET 2.0

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.

The Resource Provider Model

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

image

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

image

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.

Setting the ResourceProviderFactory

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.

ResourceManagerResourceProviderFactory

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

image

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.

DbResourceManagerResourceProviderFactory

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.

Where Are We?

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.34.85