- Developing Components
- Developing Controls
- Resources and Internationalization
- Summary
8.3 Resources and Internationalization
.NET supports a different model of resources from that supported by Win32. In Win32, resources are held in a section that is part of the PE (portable executable) file format; the resources are embedded within this segment. .NET resources are part of an assembly, but they can be embedded within the assembly or supplied as separate files. In this section I'll explain how resources are generated with Visual Studio.NET and how your code can access them.
8.3.1 Resources and .NET
.NET has been designed with internationalization in mind. Imagine that you download an application from a Web site that you trust and the Web site is in a locale different from yours. You would expect the application's developers to have created the application in their own locale. However, if the language is different from yours, you will hope that the application has been localized to your locale and that the Web site gives you the option of downloading different localized versions. Win32 applications typically used this scheme. It is possible in Win32 to create resource DLLs for locale-specific resources, but this means that the developer has to explicitly load the resource from the DLL.
.NET allows you to create locale-specific resources, but it is far more sophisticated than Win32 because the Framework Class Library provides a class (Re-sourceManager) that will automatically load the resources for the current locale. These resources can be part of the current assembly, or they can be part of a separate assembly called a satellite assembly.
8.3.2 Locales, Cultures, and Languages
.NET uses the naming convention defined in RFC 1766. Cultures are named with the following pattern: xx-yy, where the two letters xx represent a language (e.g., en for English, de for German, or fr for French), and yy represents an area where that language is used (e.g., GB for the United Kingdom, AU for Aus-tralia, and US for the United States). Together, a language and an area represent a particular culture, so en-US represents English spoken in the US and implies hamburgers, Coke, and baseball. Whereas en-GB is the Queen's English and implies roast beef, tea in china cups, and cricket. (Well, you get the idea.) Without the area (e.g., en), a resource is area neutral; without a language, a resource is both language and area neutral. Most cultures can be represented by this four-letter style, but if further delineation is required, you can add extra pairs of letters.
The Framework Class Library provides the CultureInfo class to represent a particular culture. You can initialize this class by passing to the constructor either the RFC 1766 string or a locale ID (LCID). As I mentioned in Chapter 2, a culture can be used to format items like dates:
// C# CultureInfo ci = CultureInfo("en-GB"); Console.WriteLine(DateTime.Now.ToString( "F", ci.DateTimeFormat));
Here the date is printed at the command line in the UK format. Because different cultures that use the same language have different rules for formatting, the CultureInfo class must be initialized with enough information, and a language identifier is not enough. If you do not specifically use a culture in format code, the current culture will be used. This culture is a per-thread value and is a read/write property of the current thread:
// C# CultureInfo ci = CultureInfo("en-GB"); System.Threading.Thread .CurrentThread.CurrentCulture = ci; Console.WriteLine(DateTime.Now.ToString());
.NET resources are not as strict as formatting code, so the ResourceMan-ager class (which is used to locate and load locale-specific resources) allows you to provide resources that are totally neutral, area neutral, or culture specific. Again, this information is set on a per-thread basis through the Thread.Cur-rentUICulture property.
8.3.3 Creating Resources
Assemblies contain either compiled resources or uncompiled resources, and these can be either embedded within the assembly or supplied as a separate file and a link provided within the manifest of the assembly. Resources in an assembly are named. For example, here is some IL:
// IL .assembly App { .hash algorithm 0x00008004 .ver 0:0:0:0 } .mresource public MyRes.resources { } .module App.exe
This code indicates that an assembly called App.exe has a resource called MyRes.resources, which can contain several items, but ILDASM does not decompile the resource format, so these resources are not shown in IL. If the resource is a compiled resource, it can be read with the classes in System.Resources, as I'll explain in a moment. Otherwise the resource should be read as a single item, through the assembly object.
A resource can be embedded in an assembly with the C# compiler through the /res switch:
csc /res:MyRes.resources app.cs
This command will compile a C# file called app.cs and embed an already compiled resource called MyRes.resources in the assembly. The resource in the assembly will also be called MyRes.resources. If this is not what you want, you can append the switch with the name of the resource (separated by a comma).
The C++ linker has an /assemblyresource switch that you can use to embed a resource in an assembly. The resource will have the name of the resource file that you embed, and unlike the C# compiler, you cannot rename it through the switch:
link /out:app.exe /assemblyresource:MyRes.resources app.obj
If the resource is not compiled, it can be read only by explicit access of the resource through the assembly manifest:
// C# Assembly assem = Assembly.GetCallingAssembly(); Stream stm; stm = assem.GetManifestResourceStream( "MyRes.resources");
This code will return a stream that has all of the resource. It does not matter whether this resource is compiled, uncompiled, linked, or embedded. The following code will print out this stream to the command line:
// C# while(true) { int i = stm.ReadByte(); if (i == -1) break; if (i < 32 || i > 127) Console.Write("."); else Console.Write((char)i); } Console.WriteLine();
An assembly can have a link to a resource. You can create this link with the C# compiler using the /linkres switch:
csc /linkres:data.txt,MyRes.resources app.cs
This command will compile the file app.cs, add a link to the file data.txt, and call the resource MyRes.resources. Clearly, if the resource is linked, it must be available through the link at runtime. Here is the IL produced by the preceding code:
// IL .file nometadata data.txt .hash = (1E 7B 82 95 E5 DA 4B 04 7A 56 47 DE EE C2 E7 7E 1D 19 26 90 ) .mresource public MyRes.resources { .file data.txt at 0x00000000 }
The resgen tool is used to compile or decompile resources. When resources are being compiled, the input can be either a text file (with the extension .txt) or an XML file (with the extension .resx). The text file can be used only for string resources; it is structured as a series of name/value pairs with the two separated by an equal sign. Here is an example:
; text resource file ErrNoFile=File {0} cannot be found MsgStarted=Application has started
The code that uses the resources refers to the first string with the identifier ErrNoFile. If you want to use binary resources (e.g., images), you have to use XML resources. The XML file equivalent to the name/value pairs just shown is as follows:
<?xml version="1.0" encoding="utf-8"?> <!-- schema --> <root> <data name="MsgStarted"> <value>Application has started</value> </data> <data name="ErrNoFile"> <value>File {0} cannot be found</value> </data> <resheader name="ResMimeType"> <value>text/microsoft-resx</value> </resheader> <resheader name="Version"> <value>1.0.0.0</value> </resheader> <resheader name="Reader"> <value> System.Resources.ResXResourceReader </value> </resheader> <resheader name="Writer"> <value> System.Resources.ResXResourceWriter </value> </resheader> </root>
The <resheader> nodes give information about the format of the resources and the names of the classes used to read and write the resources. All of these <resheader> nodes except the Version node are required. For space reasons, I have not shown the schema, but in any case it is not needed. Although it is possible to write .resx files by hand, it is much easier to use the
VS.NET IDE, especially when you consider binary resources. Binary resources still have to have <value> nodes in the XML file, and to do this they must be converted to a readable format by something like base64 encoding. It is much easier to allow the IDE to do this for you, as I'll show in the next section. resgen can also be used to decompile resources. If the input file has the extension .resources, resgen knows that it has to decompile resources. It determines the format that you require by the extension of the output file you specify. The general process of compiling resources is shown in Figure 8.10.
Figure 8.10 Resource compilation process
8.3.4 Managed C++ and Resources
Managed C++ projects allow you to add resources through the Solution Explorer or Class View window, but these will be Win32 resources. If you want to add your own .NET resources, you need to edit the project settings. Here are the steps: First you need to add an XML file to your project. To do this you should use the Add New Item dialog of Solution Explorer, and ensure that the extension of the file is .resx (the resgen utility insists that XML resource files have this extension). If you forget to give the file this extension, you will have to remove the file from the project, rename it using Windows Explorer, and add the renamed file to the project with Add Existing Item from the C++ Solution Explorer context menu. The reason is that the C++ Solution Explorer (unlike the C# Solution Explorer) does not allow you to rename a file that has been added to a project.
Once you have added the .resx file to the project, you should add the bare minimum of resource file contents: the <root> node and the three <res-header> nodes I mentioned earlier: ResMimeType, Reader, and Writer. After that it makes sense to add at least one <data> node (essentially as a template), and then you can edit the resource file using the XML designer.
The next task is to add the .resx file to the build. To do this you should select properties of this file from the Solution Explorer context menu by selecting General Configuration Properties and making sure that the Tool property option selected is Custom Build Tool. You can then set the tools command line through the Custom Build Step option (Table 8.2).
Table 8.2 Custom Build Step properties for an .resx file
Property |
Value |
|
Command Line |
resgen $(InputFileName) $(OutDir)\$(InputName).resources |
|
Description |
Building .NET resources |
|
Outputs |
$(OutDir)\$(InputName).resources |
Choosing Custom Build Step will allow you to build the resource; however, you also need to embed the resource in the assembly, and to do this you need to edit the linker options. You select the properties of the project through the Solution Explorer window, and then in the Property Pages dialog you select the Linker node and then the Input node. Within the grid you'll see a property called Embed Managed Resource File; you change the value of this property as follows:
$(OutDir)\$(InputName).resources
This parameter assumes that the name of the .resx file that was compiled had the same name as the project. Once you have made these changes, you should be able to add string resources to the project through the .resx file.
Image files are not so easy; the problem is that you have to encode image files into a format that can be put in an XML file. A utility called resxgen will allow you to do this; it is supplied as an example in the .NET Framework Samples folder. However, the problem with this tool is that it will generate an entire .resx file from a single binary file. You cannot use it to add a binary resource to an existing .resx file.
8.3.5 C# and Resources
In this section I will give just a basic overview of using resources in C# projects; the sections that follow will go into more detail. To add a resource to a C# project you use the Add Class dialog of Solution Explorer. The Resources category shows that you can add bitmaps, icons, cursors, and string resource files. The resource files that it mentions here are .resx files that you'll typically use to add strings to the assembly, similar to adding a string table in a Win32 resource file. .resx files are XML files and are used as an input to the resource compiler, resgen, which I'll cover later. These resource files can also contain binary data like icons, but the data is stored in the .resx file as base64 encoded.
When you add one of the image files to the project, you can use the item's properties to see how the resource will be added to the assembly. Build Action gives the options of None, Compile, Content, and Embedded Resource.
Content does not add the resource to the assembly, but it does indicate that the file should be deployed with the output of the project; Compile requires that you specify the compile tool through the Custom Tool property, and Embedded Resource will add the resource to the assembly without compiling.
For example, if you add an icon to your project and change its Build Action value to Embedded Resource, you will get the following IL when you build the assembly:
// IL .mresource public myAssem.myIcon.ico { }
Here the icon file is called myIcon.ico, and the assembly is called myAssem You can read this resource using Assembly.GetManifest-ResourceStream() and pass the stream as a construction parameter to the Icon class. For example, the following code loads an embedded resource as an icon for the NotifyIcon class that is used to create a tray icon:
// C# // NotifyIcon trayIcon is a private class member // this code is in constructor and components // is the Container created in InitializeComponents trayIcon = new NotifyIcon(components); Assembly assem = Assembly.GetExecutingAssembly(); // assembly is called Tray; icon file is called MyIcon.ico trayIcon.Icon = new Icon( assem.GetManifestResourceStream( "Tray.MyIcon.ico"));
Other resources, such as bitmaps and cursors, can also be loaded in this way. If you add a resource to a project as Content, it will be distributed with the output of the project as a separate file. Note that this is not the same as being part of a multifile assembly, as I mentioned in Chapter 1. When you add a link to an external resource file (through the /linkres switch to csc), the compiler will add a hash of the resource file to metadata in the assembly's manifest. To get the names of all such resources, you can use Assembly.GetMani-festResourceNames(), and the names returned can be passed to the constructor of Icon, Cursor, or Bitmap to load the resource. When you specify that the Build Action value of a resource is Content, there will be no information about this in the assembly's manifest, so your code needs to know the name of the file.
As you'll see in a moment, the icon for a form is shown as a property for that form when viewed in the Designer window. However, the Cursor property shows only standard cursors in the Properties window. If you want to use a custom cursor, you can simply add a cursor as an embedded resource and use code similar to that shown here to load the cursor and make it the cursor for the form.
When you add an icon to a project, the wizard will show a 32_32 icon with 16 colors. This size is fine for the large-icon view in Windows Explorer, but it is too large for the form's icon. Icon files can contain images of different sizes and color depth, and it turns out that the icon created by the wizard also has a 16_16 icon image with 16 colors. To switch between the two sizes, you should select Current Icon Image Types on the Image menu (or use the Im-age.MoreIcons command, which will list all the icons). If you want to add another icon type to the icon, there is a New Icon Type menu icon (for the command Image.NewImageType).
Form icons are a different situation. When you add a form to a C# project, the IDE will create a .resx file with the same name as the form specifically for the resources that the form will use. One of these resources, of course, is the form's icon. Normally you will not see this .resx file in the Solution Explorer window because it will be a hidden file. To view this file you need to click on the Show All Files button, and you need to close the form in the Windows Forms designer.
To add an icon to a form, first you have to add an icon to the project as I have shown here, but leave the icon's Build Action value as Content. Next you should select the form's properties in the Windows Forms designer and click on the form's Icon property. This will bring up a dialog that will allow you to browse for the icon you just created. When you have done this, the IDE will insert the icon as a node in the .resx file. In a similar way, if you add a background image (the BackgroundImage object) to the form, the image file will be added to the .resx file. These are just special cases: They are resources required by the form, so they have to be stored along with the form.
8.3.6 Forms and Localization
Every form has a property called Localizable. This is not a property inherited from the Form base class; it is a pseudoproperty created by the Properties window for Form objects and UserControl objects (but not Control objects). When you change this property from the default of False to True, the Properties window will copy all the form's properties to the form's .resx file. The .resx file with the form's name will have the default values for the form.
When you change the Language property (another pseudoproperty), the IDE will create a .resx file for the selected language, named according to the language (so if the form is called myForm, the UK English resource file will be called myForm.en-GB.resx). This resource file will contain the difference between the default resource and the localized resource. So if you have set the Icon property in the default resources, this value will be used by all cultures unless you explicitly change it for a specific culture. Thus, localizing your forms is as simple as generating the default resource for the form, then specifying the values for only those properties that you want to localize by changing the Language property in the Properties window, and fi-nally changing the property to its localized value in the Properties window.
The effect of Localizable is recursive, so if you have controls on a form, you can change the properties of those controls for a specific culture, and those properties will be written to the appropriate .resx file. You are most likely to use this option if the form has a menu. You add a menu to a form by adding a MainMenu control, and the Windows Forms designer allows you to add sub-menus, handles, and embellishments like check marks and radio buttons. When you develop an application, you should start by building up the menu using the default Language. And once you have created the menu layout and the handlers, you can localize the menus by changing the form's Language property to a language other than the default and then changing the menu items' text values. The values that you change will be written to the resource file for the culture.
Of course, the Windows Forms designer generates code. When the form is not localized, the designer will add code to assign the property value to the property in the InitializeComponent() method. When you localize the form, the designer changes the code to use a ResourceManager object. I will go into more detail about this class in the next section, but as I have already mentioned, this class will locate the appropriate resources section in the assembly (or in satellite assemblies) and give access to the values. For example, here is some code that is generated for you:
// C# private void InitializeComponent() { System.Resources.ResourceManager resources = new System.Resources.ResourceManager( typeof(myForm)); // some properties omitted this.Icon = ((System.Drawing.Icon) (resources.GetObject("$this.Icon"))); this.Text = resources.GetString("$this.Text"); this.Visible = ((bool) (resources.GetObject("$this.Visible"))); }
As you can see, ResourceManager is initialized with the type of the form, which gives the class one part of the information it needs to locate the resource in the assembly. This class reads the UI culture of the current thread, and using this and the name of the form, it can determine the name of the form's resources (i.e., it will search myForm.resources for the default resources, and myForm.en-GB.resources for UK English resources). It then accesses the string resources using GetString() (as shown here, with the Text property), and all other resources are accessed through GetObject(). The designer uses the convention of naming each resource $this.<propertyname>. Because ResourceManager determines the appropriate resources for the current locale, you do not need to write this locale-specific code.
When you compile the form, the project will add the default resources to the assembly that contains the form and will generate a satellite assembly for each of the other resource files. These satellites will be named according to the satellite convention: <formAssem>.resources.dll, where <formAssem> is the name of the form's assembly. Each satellite will be located in a folder named according to the locale of the satellite, as I'll describe later.
8.3.7 Resource Classes
The System.Resources namespace has the classes that are needed to read and write compiled resources. ResourceReader enumerates resources and gives access to them through an IDictionaryEnumerator interface. The constructor parameter takes either the name of a file or an already opened stream, which, conveniently, is what Assembly.GetManifestResource-Stream() will return:
// C# System.Reflection.Assembly assem; assem = Assembly.GetExecutingAssembly(); Stream stm; stm = assem.GetManifestResourceStream( "myAssem.myResources.resources"); ResourceReader reader = new ResourceReader(stm); foreach (DictionaryEntry de in reader) Console.WriteLine(de.Key +" = "+de.Value);
This code will look for a resource called myAssem.myResources.resources and will print the name/value pairs contained in it.
The ResourceWriter class is used to write compiled .resource files. It takes as a construction parameter either the name of the file or, if you have an already open file, a writable stream. You can then use one of the overloaded AddResource() methods to add a string or a binary value to the resource. If you choose to write a binary value, you can pass either a byte[] array with the object already serialized or a reference to the object. If you pass an object reference, it must be an instance of a serializable class. The actual resource is not created until you call the Generate() method, which is also called by the Close() method that closes the output stream. Thus you can write code like this:
// C# ResourceWriter rw = new ResourceWriter("myResources.resources"); rw.AddResource("TestString", "Some string data"); byte[] b = new byte[]{0, 1, 2, 3, 4}; rw.AddResource("TestData", b); rw.Generate(); rw.Close();
This code will add the resources to a file called myResources.re-sources. The ability to write resource files is useful when you consider the Re-sourceManager class. This class is used to provide convenient access to localized resources bound to an assembly or to satellite assemblies, or located in separate files. As I have already mentioned, this class will read the UI culture of the current thread, and using this and a base name for the resource, it will locate the resource in the local file or in a satellite assembly. Typically you will add a resource for a form, so the base name of the resource will be derived from the form's type. This is why the type of the form was used as the constructor parameter in the code I showed earlier.
If you add a separate resource file to your project, you need to provide to the ResourceManager constructor the name of the resource that will be derived from the resource file's name. For example, if you add a resource file called myRes.resources to the project, the resource will be named myRes.resources, and the base name will be myRes. Thus the following code will initialize a ResourceManager object to load these resources:
// C# ResourceManager rm = new ResourceManager("myRes", assem);
The assem reference is the Assembly object that contains the resource (or an assembly that has satellites that contain localized resources). You can get a reference through the type of an existing object (e.g., in a form you can call this.GetType().Assembly) or through the static members of Assembly: GetAssembly() to get the assembly for a particular type, GetCallingAs-sembly() for the assembly that loaded the current assembly, or GetExecut-ingAssembly() to get the current assembly.
If you have created resource files using a ResourceWriter object, you can load these resources using the static CreateFileBasedResourceManager() method of the ResourceManager class. In the previous example, then, you can load the resources in myResources.resources with the following code:
// C# ResourceManager rm. ResourceManager.CreateFileBasedResourceManager( "myResources", ".", null); Console.WriteLine(rm.GetString("TestString"));
The first parameter is the name of the resource; the second parameter is the folder where the resources are located. You can use localized files, but note that the location of these files differs from how ResourceManager locates satellite files. If you localize the resources in myResources for, say, French spoken in France, the resource file will be called myResources.fr-FR.resources. Yet you load this resource using the same code I showed earlier (assuming that the UI culture of the thread is fr-FR). Because the culture is part of a resource file's name, you do not need to place the file in a separate localized folder, as you do with satellites.
The final parameter passed to CreateFileBasedResourceManager() is the type of the resource set (identified in ResourceSet) that will be used. In this case I have used null, which indicates that System.Resources.Re-sourceSet should be used. ResourceManager uses the resource set to read the resources from the resource file (the type of the resource set that this class uses is accessed through its ResourceSetType property). A resource set has a resource reader to do the actual reading; this reader is accessed through the resource set's Reader field. You create your own resource set class so that you can use resources that are held in a format other then the compiled format produced by resgen.
Resource sets contain only the resources for a specific culture. You can create a resource set through its constructor (by passing a resource stream or the name of a resource file), or you can obtain it through ResourceManager by calling GetResourceSet() and pass a CultureInfo object. Because resource sets are specific to a culture, there is no "fallback" to a neutral culture if the specified culture does not exist. When you create a resource set, it will load all the resources and cache them in a hash table.
In addition to classes for accessing resgen-compiled resources, the Sys-tem.Resources namespace has classes for reading and writing .resx XML files: ResXResourceReader and ResXResourceWriter, respectively. It also has an implementation of ResourceSet called ResXResourceSet.
8.3.8 Satellite Assemblies
As the name suggests, a satellite assembly is separate from the assembly that will use its resources. Do not confuse satellite assemblies with modules. Modules are constituent parts of an assembly and hence are subject to the versioning of the assembly to which they belong. A satellite assembly is an assembly in its own right, but unlike normal assemblies, it does not have code and hence does not have an entry point. To create a satellite assembly, you use the assembly builder tool, al.exe. For example, imagine that you have resources localized to German in a resource file called App.de.resources. This file is embedded into a satellite assembly as a result of the following command line:
al /t:lib /embed:App.de.resources /culture:de /out:App.resources.dll
This command creates a library assembly called App.resources.dll localized to German. If you choose, you can create an empty code file with the [AssemblyVersion]attribute to give the satellite assembly a version:
// C#, file: ver.cs [ assembly:System.Reflection .AssemblyVersion("1.0.0.1") ]
The assembly is now compiled with the following:
csc /t:module ver.cs al /t:lib /embed:App.de.resources /c:de /out:App.resources.dll ver.netmodule
The assembly is still resource-only because the module that is linked in has only metadata. You could do the same thing with the [AssemblyCompany] and [AssemblyDescription] attributes to add information about the company that created the assembly and a description. The problem with this approach is that there are now two files to deploy: App.resources.dll and ver.net-module. To get around this problem, the assembly builder tool allows you to pass some of this information through command-line switches, which are listed in Table 8.3.
Using the /version switch, you can tell the assembly builder to specify the version of the assembly. In the absence of other version switches (/filever-sion, /productversion), the version you specify will be used to provide a Win32 FILEVERSION resource in the library and will be the basis of the PRO-DUCTVERSION and FILEVERSION fields.
Table 8.3 Assembly builder switches used to change the assembly's metadata
Switch |
Attribute Equivalent |
Description |
/company |
[AssemblyCompany] |
The company that created the assembly |
/configuration |
[AssemblyConfiguration] |
Typically Retail or Debug |
/copyright |
[AssemblyCopyright] |
Your copyright notice |
/culture |
[AssemblyCulture] |
The culture of the assembly |
/delaysign |
[AssemblyDelaySign] |
Specification of whether the assembly can be signed later by sn.exe |
/description |
[AssemblyDescription] |
A description of the assembly |
/fileversion |
[AssemblyFileVersion] |
The Win32 version of the library |
/keyfile |
[AssemblyKeyFile] |
The name of the file with the key |
/keyname |
[AssemblyKeyName] |
The name of the key in a cryptographic container |
/product |
[AssemblyProduct] |
The product's name |
/productversion |
[AssemblyInformationalVersion] |
The version of the product |
/title |
[AssemblyTitle] |
The friendly name of the assembly |
/trademark |
[AssemblyTrademark] |
Your trademark |
/version |
[AssemblyVersion] |
The assembly version |
Now imagine that you have resources for the same application localized to French in App.fr.resources. This data is embedded into a satellite assembly by the following command line:
al /t:lib /embed:App.fr.resources /culture:fr /out:App.resources.dll
This command also creates a library assembly called App.resources.dll. The code in ResourceManager does not use the name of the assembly to determine its culture; instead it uses the [AssemblyCulture] attribute (the .locale metadata) to locate the correct satellite assembly. Because the satellites have the same name, they should be installed in subfolders of the folder containing the assembly that uses the satellite. These folders should have the name of the locale of the satellite; for example, the German resources should be in a folder called de, and the French resources should be in a folder called fr.
If your satellite files are to be shared by several applications, you should install the satellites in the GAC (global assembly cache). If you do this, the satellites should have a strong name. Remember that the full name of an assembly includes its culture, version, and public key, so there are no problems with installing several satellite files in the GAC because although the short name of the assembly will be the same, the full names will differ by the culture element.
When a ResourceManager object is created and tries to locate localized resources, the runtime first looks in the GAC for the satellite assembly with the correct culture and checks whether it has the resource. If this check fails, the current folder is checked for the culture-specific assembly in a named folder. If this search fails, the runtime starts the search again, but this time for an assembly that has the appropriate "fallback" culturefirst in the GAC, and then in the current directory. Each culture will have a fallback culture that will be searched in this way, until finally the runtime will attempt to locate the resource in the default resources for the assembly, which will be in the main assembly. If this search fails, the resource cannot be found and an exception will be thrown.
Because satellite assemblies can have a different version from the version of the main assembly, satellite versions can get out of sync with the main assembly. To get around this problem, the main assembly can specify a base version of the satellite assemblies that it uses; it does this with an assembly-level attribute:
[assembly: SatelliteContractVersion("1.0.0.0")]
Unlike versions applied through [AssemblyVersion], the string version must have all four parts. The satellite assembly can be versioned independently from the main version, and the changes can be reflected in the application's con-figuration file or, if it is installed in the GAC, through a publisher policy file.
The name that I have used for the satellite assembly is <assem>.re-sources.dll. This is a standard naming convention and is vital to how the Re-sourceManager class works. The <assem> part is the name of the assembly that will use these resources. This is the only mechanism that exists to tie a satellite to the assembly with which it is used.2 If you use satellite assemblies, I urge you to make sure that you also provide locale-neutral resources. A locale-neutral resource is named without a locale and is bound to the assembly that uses the resource. In the preceding example, the main assembly will be compiled with a command line that looks like this:
csc /res:App.resources /out:App.exe app.cs
The assembly is called App.exe. When it is run, ResourceManager will check for an appropriate resource for the current culture within the satellite assemblies, and if that resource is not present, it will load the locale-neutral resource from the main assembly. Locating satellite assemblies is not part of Fusion's work, so if ResourceManager cannot find a satellite assembly, there will be no binding-error message in the Fusion log (viewable with Fus-LogVW.exe), and if your main assembly has a locale-neutral resource, you'll have no indication that there has been a problem. (If you do not have locale-neutral resources, ResourceManager will throw a MissingManifest-ResourceException exception.)
8.3.9 The Event Log, Again
So I am back to the event log again. As I mentioned earlier in this book, .NET has a poor implementation of the classes to write messages to the event log. The principal reason I say this is that these classes put the onus on the user of the EventLog class to localize the messages that are written to the event log rather than taking the correct approach, which is to put the onus on the reader of the event log. If the writer is responsible for localizing messages, the messages can be read in only one locale, which is fine if your distributed application runs in only one locale. In these days of globalization, however, your application could conceivably have components running in different locales, and if a message is localized when it is generated, it ties that message to that locale. Unfortunately, there is little one can do with the current framework classes, and one can only hope that this horrible throwback to the broken event log classes that were present in VB6 will be fixed in a later version of the Framework Class Library.
To localize event log messages, your code merely creates localized format strings in a resource file and then at runtime uses a resource manager to load the appropriate string:
// C# ResourceManager rm = new ResourceManager(typeof(myForm)); string errMsg = String.Format( rm.GetString("errNoFile"), strFileName); EventLog el = new EventLog("Application"); el.WriteEntry(errMsg);
8.3.10 Win32 Resources
Assemblies are PE files, so they can have Win32 resources. For a C++ developer this is not a problem because managed C++ projects use the standard linker, which will link a compiled Win32 (.res) file into a PE file. Indeed, as I mentioned earlier, when you add a resource to a managed C++ project, that resource will be a Win32 resource and not a .NET resource. C# developers can also include Win32 resources using either the C# compiler (csc.exe) or the assembly builder tool (al.exe). Both of these tools have a /win32res and a / win32icon switch, the first of which allows you to add an already compiled resource to an assembly. It is probably best to do this within a C++ project, where you can edit an .rc file and use the Resource View window to add and edit the resources. The unmanaged resource compiler, rc.exe, is used to compile a .rc file into a .res file.
One type of resource that can be described in an .rc file is the icon. The first icon in a file's resources will be used by Windows Explorer when displaying the file, and if the file is an executable, this icon will be the application icon.
The /win32icon switch is the only way that you can set the icon for an assembly. This switch takes the name of the icon (.ico) file; you do not compile it. It is prudent to add both a 32_32 bit image and a 16_16 bit image so that you can determine the icon that will be shown in Windows Explorer, no matter what view the user chooses.
.NET code does not understand Win32 resources, so if you need to read Win32 resources, you have to resort to interop through Platform Invoke. The code is straightforward, and there is even a sample in the Framework SDK samples (called TlbGen) that shows how to add a Win32 resource to an assembly programmatically, using the Win32 resource APIs.