Nearly every production quality application needs to make uses of resources. A resource is any non-executable data that is logically deployed with an application. This data can take any number of forms, for example: string literals, images and persisted objects. A large part of the job of making applications easily localizable involves making these resources changeable without changing the executable content. The runtime explicitly does not support localizing component metadata. Instead, resource data is persisted in separate resource files and/or in a designated section of a PE file.
The low-level resource model described in NGWS Runtime Resources Specification is most efficient at a coarse granularity. However, clients often want to retrieve resources at a fine granularity (a particular String for example). For this reason, the runtime provides a mechanism to encapsulate fine grain data such as Strings, persisted objects, etc. into larger chunks that can be more efficiently managed by the low-level resource infrastructure. The resources are serializable data consisting of name/value pairs, where the name is a string that identifies a resource and the value is an object such as a string literal. As an example of how localizable resource files are used, the base class library uses them to localize the message strings carried by exceptions, and they can be used to localize the text and images that appear in GUI objects. The Base Class Library resource classes use the low-level APIs to retrieve resources.
The primary goal of the Base Class Library classes is to provide easy access to culture correct data at runtime. The classes provides a means by which to create, manage, and read resource files that are persisted in the default resource file format. These managed classes impose some policy and consistency to how resources are stored and retrieved. The classes also provide extensibility points for advanced scenarios.
Resources should be ideally created for every culture, or at the minimum a meaningful subset of the cultures. The resource file names follow the naming convention basename.cultureName.resources. basename can be the name of the application, or depending on the granularity desired, the name of a class. cultureName is determined from the Name property of CultureInfo. A resource file for the language neutral culture (returned by CultureInfo.InvarientCulture) should be named basename.resources. If a subculture is specified, the naming convention for is basename.mainCultureName-subCultureName.resources.
The Base Class Library provides the following classes and interfaces in the System.Resources namespace:
ResourceManager. Encapsulates all the resources in a given application.
ResourceSet. A cache for the resources for a given culture. There is one ResourceSet per culture.
IResourceReader. An abstraction to read resources from a stream.
IResourceWriter. An abstraction to write resources to a stream.
ResourceReader. A default implementation of IResourceReader that reads resources in the runtime default binary resource file format.
ResourceWriter. A default implementation of IResourceWriter that writes resources in the runtime default binary resource file format.
How to Use the ResourceManager:
The ResourceManager class provides several constructors that are useful in different scenarios. This section describes how to use the ResourceManager class effectively.
To retrieve resources in a flat namespace using the assembly manifest, use:
[C#] ResourceManager(String baseName, Assembly assembly, Type usingResourceSet, bool usingSatelliteAssemblies)
On large projects, this may lead to collisions in resource names. The individual resources can either all be contained in the specified main assembly, or they can be contained in satellite assemblies.
To retrieve resources in a type qualified namespace using the manifest of a specified type’s Assembly, use:
[C#] ResourceManager(String baseName, Type locationInfo, Type usingResourceSet, bool usingSatelliteAssemblies)
On large projects, the use of type qualified namespace will reduce collisions in resource names. The individual resources can either all be contained in the specified main assembly, or they can be contained in satellite assemblies.
To retrieve resources in a type qualified namespace in the directory of the specified type’s module, use:
[C#] ResourceManager(String baseName, Type locationInfo, Type usingResourceSet)
To retrieve resources in a specified directory, use:
[C#] ResourceManager(String baseName, String resourceDir, Type usingResourceSet)
In each of the above constructors, the usingResourceSet
parameter allows you to specify a implementation to be used. If you do not need a specific ResourceSet implementation but would like to use a custom resource file format, you should subclass ResourceSet, override ResourceSet.GetDefaultReader and ResourceSet.GetDefaultWriter, and pass that type to this constructor.
The last two constructors do not use the assembly cache. The resource files are assumed to reside in the specified directory and the constructors are used to access the resource files in the specified directory.
How to Create and use Resources:
To create a .resources file:
To use a .resources file:
If you call GetString(String key), the resource that is returned is localized for the culture determined by the current regional settings of the current thread (this is accomplished via the Thread.CurrentUICulture property). If the resource has not been localized for that culture, the resource that is returned is localized for a "best match" (this is accomplished via the CultureInfo.Parent property). Otherwise, a null reference is returned.
If you call GetString(String key, CultureInfo culture), the resource that is returned is localized for the specified culture. If the resource has not been localized for that culture, the resource that is returned is localized for a "best match" (this is accomplished the CultureInfo.Parent property). Otherwise, a null reference is returned.
How to Localize Exception Strings:
internal static String GetResourceString(String key)
{
if (_rm == null) {
InitResourceManager();
}
String s = _rm.GetString(key);
return s;
}
throw new NotSupportedException(Environment.GetResourceString(“Unsupported”);
Where Unsupported
is the name of the resource associated with the exception string.
throw new ArgumentException(String.Format(Environment. GetResourceString(“InvalidDirectory”), dir));
Where InvalidDirectory
is the name of the resource associated with the exception string.
The Default Resource File Format:
There are three sections to the default file format. The first is the ResourceManager header, which consists of a magic number that identifies this as a Resource file, and a ResourceSet class name. The class name is written here to allow users to provide their own implementation of a ResourceSet (and a matching ResourceReader) to control policy. If objects greater than a certain size or matching a certain naming scheme shouldn't be stored in memory, users can tweak that with their own subclass of ResourceSet.
The second section in the system default file format is the RuntimeResourceSet specific header. This contains the version number of the resource file, the number of resources in the .resources file, the name and virtual offset of each resource, the number of different class types contained in the file, and a list of fully qualified class names. The class table allows the runtime to read multiple classes from the same file, including user defined types, in a more efficient way than would be possible by using Serialization provided the .resources file contains a reasonable proportion of base data types such as String and int. Serialization is used for all the non-intrinsic types.
The third section in the file is the data section, which consists of a type and a blob of bytes for each item in the file. The type is an integer index into the class table. The data is specific to that type, but may be a number written in binary format, a String, or a serialized Object.
The system default file format is as follows:
Item | Type of Data |
---|---|
ResourceManager header | |
Magic Number (0xBEEFCACE) | Int32 |
Class name of ResourceSet to parse this file | String |
RuntimeResourceSet header | |
Version number | Int32 |
Number of resources in the file | Int32 |
Name & virtual offset of each resource | Set of (String, Int32) pairs |
Number of classes in the class table | Int32 |
Name of each class | Set of Strings |
ResourceReader Data Section | |
Type and Value of each resource | Set of (Int32, blob of bytes) pairs |
The default implementation, when used with the default ResourceReader class, loads all the names from the resource file then loads the values on demand, storing them for later use. This implementation uses less memory than the ResourceSet class, assuming not all resources are loaded, but getting an individual resource will take longer. If a different IResourceReader is passed in, all the resource names and values are loaded on the first lookup.
In addition, the default implementation supports object serialization in a similar fashion. The runtime builds an array of class types contained in this file, and writes it to the ResourceReader header section of the file. Every resource will contain its type (as an index into the array of classes) with the data for that resource. The runtime will use the runtime's serialization support for this.
All strings in the file format are written with Stream.WriteUTF(), which writes out the length of the String as an Int32 then the contents as little endian Unicode chars.
The offsets of each resource string are relative to the beginning of the Data section of the file. This way, if a tool decided to add one resource to a file, it would only need to increment the number of resources, add it to the end of the name and offset list, possibly add it to the list of class types (and increase the number of items in the class table), and add the value at the end of the file. The other offsets would not need to be updated to reflect the longer header section.
Resource files are currently limited to 2 gigabytes due to these design parameters.
See Also
NGWS Runtime Resources Specification