.NET 2 the Max
Make the Best of .NET Resource Files
Drill down on some under-documented techniques for using resource files in your .NET apps, and learn to customize these files for different customers.
Technology Toolbox: VB.NET, C#
Relatively few .NET developers are familiar with resource files. After all, why should you bother storing a string in a resource file when you can simply type it in code or, at worst, load it from a text file or a database field?
The truth is, there are plenty of reasons for using resource files. First, they let you localize the application for different locales easily. Second, they let you customize the application (to an extent, at least) for different customers. And third, resource files enable you to change the user interface and some other features of an application without redeploying the actual executable.
Last month, we showed you how to store resources in the assembly's manifesta technique that enables you to store files and images right in the executable so that the end user can't delete them accidentally. Manifest resources, however, don't offer all the benefits that separate resource files do.
Use Localizable Forms
Windows developers typically use resource files to create multilanguage applications. The problem with resource files is that they don't lend themselves well to the Rapid Application Development (RAD) approach. For example, prior to Visual Basic .NET, you could use only one language for the user interface. You could support additional languages only if you authored a resource file yourself, compiled it by running a command-line tool, and wrote the code that extracts each string or image. Fortunately, the Visual Studio .NET form designer solves this problem in a simple, elegant, and effective way.
Take, for example, a simple form that contains three strings and one image that should be localized (see Figure 1). In general, the localization process can involve more than simply changing the visible strings in the user interface or strings used from code. For example, you might need to move a control to a different location or make it invisible under some localized versions. At any rate, you should test your form thoroughly before you make it localizable because any change you make to its user interface afterward will require more coding and efforts.
The first step in localizing a form is to set its Localizable property to True. This property tells the designer's code generator that the values of the form and the controls' properties are to be loaded from a resource file instead of hard-coded in the source code.
Next, set the form's Language property to the alternative locale you want to support. This property is available only at design time, and you can assign any of the locales that .NET supports to it. The form designer continues to display the same user interface as before, but now you can change all the properties of the form and its controls (including their position, size, visibility, and so on). All the values that you set from now on are associated with the alternative locale just selected (see Figure 2 for how the form might look to an Italian end user). Of course, you can repeat this procedure with any language you want to support.
Visual Studio .NET's code generator performs this magic in conjunction with resource files. In fact, all the properties you use for the nondefault locales are stored in resource-only satellite DLLs named appname.resources.dll and in subdirectories under the application's main folder. (Each directory is named after one of the locales that your program managesfor example, it-IT for Italian resources.)
The code Visual Studio .NET generates inside a localizable form uses a ResourceManager object to read all the values assigned to the properties of the form and its controls. The ResourceManager object is aware of the current UI thread's locale and looks for a DLL stored in the relevant subdirectory. The ResourceManager uses the value stored in the main application if it doesn't find that DLL or doesn't find a given value in the DLL.
Test Your Localized Forms
The great thing about localized forms is that you can simply forget about them in most cases. You run an application containing localized forms as you'd run a regular application. If the culture of the current user matches one of the languages you have defined, the form and its controls use the properties you've set for that language; otherwise, the default language is used.
A minor problem with localized forms is that they require additional testing and debugging. The default locale for a given system is the language used for the operating system's visual elements, and you can't change it easily. Unless you have a different Windows computer for each and every culture you want to support (a rather expensive solution), you must change the UI thread's culture so that the ResourceManager object picks the resource from the right satellite assembly.
Changing the current UI thread's culture is as easy as assigning a suitable CultureInfo object to the CurrentUICulture property of the Thread.CurrentThread object. The ResourceManager tests this property when it's deciding which set of localized properties it should use:
' [Visual Basic]
Thread.CurrentThread.CurrentUICulture= _
New CultureInfo("it-IT")
// [C#]
Thread.CurrentThread.CurrentUICulture =
new CultureInfo("it-IT");
It's essential that you run this code in the form's constructor, before the code generated by VS.NET instantiates the ResourceManager object.
Interestingly, you can also retrieve a resource for a specific culture by passing a CultureInfo object to the GetString or GetObject method of the ResourceManager class. This code retrieves the string labeled as "White" from the Spanish resource files (or reverts to the default culture if this resource isn't in the file):
' [Visual Basic]
Dim resMan As New ResourceManager( _
GetType(Form1))
Dim ci As New CultureInfo("es-ES")
Dim color As String = _
resMan.GetString("White", ci)
// [C#]
ResourceManager resMan =
new ResourceManager(typeof(Form1))
CultureInfo ci =
new CultureInfo("es-ES");
string color =
resMan.GetString("White",ci);
Don't Burn Strings in Code
According to Microsoft's guidelines, you should always place all your user-interface strings in a resource file rather than burning them in code. The reason: Keeping all your strings in a centralized repository makes it simpler to spellcheck them or localize them to a different language.
As you've seen, Visual Studio creates a resource file that stores all the strings (and numbers and images) assigned to form and control properties. However, you still need to create a resource file manually for other UI elements, such as the title and text in a message box, or the Message property of an Exception object (if you plan to display this property to the user when the exception is thrown). You need to embed and use a set of string resources in your application.
First, select the Add New Item command from the Project menu, highlight the Assembly Resource File element from the template gallery, type the name of the resource file you want to create, and click on the Open buttonfor example, Strings.resx. Next, create one or more string resources in the file you just created (see Figure 3). You can assign each resource a name of your choice. Specify System.String as the type of each element.
Finally, reference a string stored as a resource in the Strings.resx file (see Listing 1). It's usually a good idea to store the ResourceManager reference in a variable that the entire application can access, so that you don't have to re-create it every time you need to retrieve a resource.
An important note: You must form the filename passed to the ResourceManager constructor by concatenating the name of the project's default (root) namespace and the name of the resource file as it appears in the Solution Explorer window, but without the .resx extension. However, if the RESX file is stored in a project folder, naming rules depend on the language in use (see the sidebar, "Resource Filenames are Language-Sensitive"). Unlike manifest resources, however, RESX files are searched in a case-insensitive way.
Resource files can hold more than just strings; for example, you can use them for images, icons, and audio and video files. VS.NET doesn't provide a tool to let you add nonstring resources to RESX files easily. However, you can use several freeware tools to add nonstring data to a resource file. This code shows how you can display an image named Logo.bmp stored in a resource file named Bitmaps.resx:
' [Visual Basic]
Dim resFile As String = _
"CodeArchitects.Bitmaps"
Dim resMan As New _
ResourceManager(resFile, _
[Assembly].GetExecutingAssembly())
PictureBox1.Image = DirectCast( _
resMan.GetObject("Logo.bmp"), _
Bitmap)
// [C#]
string resFile =
"CodeArchitects.Bitmaps";
ResourceManager resMan =
new ResourceManager(resFile,
Assembly.GetExecutingAssembly());
pictureBox1.Image = (Bitmap)
resMan.GetObject("Logo.bmp");
Use Satellite Assemblies for Localized Resources
Once you know how to add a resource file to your project, you're ready to use satellite assemblies to hold all the strings and other resources that your main application uses, such as images and WAV files. You must mark satellite assemblies with an AssemblyCulture attribute and avoid placing any executable code in them. The name of a compiled satellite assembly should include the .resources word (as in MyApp.resources.dll for a main assembly named MyApp.dll).
You can create one or more satellite assemblies with localized resources from inside VS.NET. In this example, you'll create a satellite assembly holding Italian resources for an application named MyApp. First, create a new class library project either in the same solution as the main application or in a different solution. The name of this project isn't important, but you might want to use a name formed by appending the name of the main project and the culture identifier. For example, you might name the project MyApp_it_IT, because the satellite assembly will contain resources for the it-IT culture. Delete the Class1.vb or Class1.cs file that .NET creates for a class library project.
Go to the General page of the new project, change the assembly name to match the name of the main application (MyApp, in this example), and change the default namespace (C#) or the root namespace (Visual Basic) to match the default or root namespace of the main application. In general, the namespace should match your company's name (in this example, it's CodeArchitects).
Go to the Build page of the Project Properties dialog box, and change the Output path value to point to the subdirectory where you create the main application's assembly. For example, if the main project is stored in the C:\Projects\MyApp folder, the output directory for both the main executable and its satellite assemblies should be C:\Projects\MyApp\bin.
Next, add an AssemblyCulture attribute that specifies the culture of the satellite assembly in the AssemblyInfo.vb or AssemblyInfo.cs file:
' [Visual Basic]
<Assembly: AssemblyCulture("it-IT")>
// [C#]
[assembly: AssemblyCulture("it-IT")]
Now you're ready to add one or more resources to the satellite assembly, by following the procedure outlined in the "Don't Burn Strings in Code" section. However, it's essential that all the RESX files you create embed the culture name in their name. For example, a file containing all the Italian strings should be named Strings.it-IT.resx. Don't place the resource file in a C# project subfolder, because this action affects the name you use to reference the resource file.
Now you can compile the satellite assembly as usual. VS.NET recognizes the AssemblyCulture attribute and correctly creates an assembly named MyApp.resources.dll. This assembly is created in a subdirectory named after the assembly culture, under the output folder. In this example, the path of the folder is C:\Projects\MyApp\bin\it-IT. The .NET runtime will look for Italian resources in this folder.
Notice that Visual Studio .NET also creates the "standard" MyApp.dll assembly in the output folder (C:\Projects\MyApp\bin, in this example). This assembly contains no code and no resources, and you can delete it before deploying the application.
You can go back to the main application and run the same code used to read a resource from a resource file embedded in the main application (see Listing 1). If the code is running under an Italian version of Windows, or if you've changed the locale assigned to the current UI thread to request Italian resources, then the ResourceManager uses the resources stored in the satellite assembly in the it-IT subdirectory.
Speed Up Resource Loading
A direct consequence of the way the ResourceManager object looks for resources is that resources related to the default locale are located only after checking that no subdirectory for that culture exists. This means, for example, that all the resources for an application whose default culture is en-US are loaded only after checking that no en-US folder exists under the app's main directory.
You can avoid this little overhead by applying the NeutralResourcesLanguage attribute to the main assembly to inform the resource manager about the language for the neutral resources that are embedded in the main assembly:
' [Visual Basic]
<Assembly: _
NeutralResourcesLanguage("en-US")>
// [C#]
[assembly:
NeutralResourcesLanguage("en-US")]
Bear in mind that you should never use AssemblyCultureAttribute in the main assembly. The main assembly should contain only neutral resourcesthat is, strings and bitmaps related to the default language.
Portions of this column have been excerpted from Francesco and Giuseppe's upcoming book, Practical Guidelines and Best Practices for Microsoft Visual Basic and Microsoft C# Developers [Microsoft Press, 2005, ISBN: 0735621721].