65.9K
CodeProject 正在变化。 阅读更多。
Home

生成 .NET 资源字符串访问权限

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (12投票s)

2003 年 6 月 22 日

8分钟阅读

viewsIcon

100026

downloadIcon

2224

本文介绍了一种在 C# 项目中访问字符串资源替代方法,通过为每个资源标识符生成访问类。

概述

本文介绍了一种在 C# 项目中访问字符串资源的替代方法。它提供了一种将每个资源标识符(与字符串相关联)视为具有公共接口用于检索字符串本身的对象的方法。虽然我最初的目的是仅仅提供字符串资源标识符的编译时验证,但最终结果确实是一种更好的访问项目字符串资源的方法。

还有一些额外的优势

  1. 字符串资源检索语法简单 - 只需调用命名为所需资源标识符的对象上的方法即可
  2. IntelliSense 提供检索方法名称,并提醒您格式化字符串所需的任何参数
  3. ResourceManager 操作已封装
  4. 格式化资源字符串时参数数量不正确的情况会在编译时捕获

引言

最近,我接到一项任务,需要将 C# 服务项目中的许多文件中的字符串字面量移到一个单独的资源文件中,以便将来进行本地化。在描述完需要做什么之后,我的经理给了我一本关于 .NET 资源的图书,说这会有帮助。

他一离开,我就说:“我不需要那些该死的书!我以前作为 C++ Windows 开发人员,已经做过几十次了——这应该没什么区别。” 我很快就意识到我确实需要那本书。

.NET 框架彻底改版了资源处理方式。这种新模型为全球化和资源序列化提供了出色的支持,但要求您忘记所有关于 Win32 资源的知识。

Win32 中的资源标识符

从 C++ 转向 C# 后,我才真正体会到 Win32 资源模型中对资源标识符进行编译时检查的方面。

在 Win32 中,资源标识符是数字常量,通常在头文件中定义,并在资源文件中映射到资源。可以通过引用相应的标识符从项目的任何位置访问这些资源。标识符首先在资源脚本编译时进行验证,然后在编译使用这些资源的应用程序时再次进行验证。如果在资源脚本中引用了未定义的标识符,资源将无法编译。同样,在代码中引用未定义的标识符也会导致应用程序无法成功编译。这提供了一层保护,可以防止开发人员轻易地对资源标识符进行的更改。当然,这并不能解决当现有资源标识符被更改且未删除旧版本时所遇到的问题,也不能解决资源头文件中未引用标识符造成的混乱,但那是另一个故事了。

下面是有关如何在 Win32 中加载字符串资源 IDS_RESOURCE1 的示例

TCHAR szString[256];
LoadString( GetModuleHandle(NULL), IDS_RESOURCE1,szString,
sizeof(szString)/sizeof(TCHAR) );

.NET 框架中的资源标识符

在 .NET 框架中,资源标识符是与特定资源相关联的字符串,两者都在同一个资源文件中定义。对于编译器而言,代码中引用的资源标识符仅仅是字符串字面量,因此不会对这些标识符及其各自资源之间的关系进行任何编译时验证。无效的标识符直到运行时尝试加载资源时抛出异常才会显现。

下面是有关如何在 .NET 中加载字符串资源 Resource1 的示例

ResouceManager rm = new ResourceManager( "AssemblyName.Resources",
Assembly.GetExecutingAssembly() ); string temp = rm.GetString( "Resource1" );

生成器工具 – 版本 1

鉴于这一切,我决定创建一个工具,通过生成与资源标识符同名的类来提供编译时验证。然后将使用这些类从代码中访问资源。为此,我创建了一个命令行实用程序,它接受资源文件名、输出 C# 文件名以及生成类的命名空间作为输入。将生成一个 C# 文件,其中包含与指定资源文件中的每个资源标识符对应的访问类。此实用程序可以作为预构建步骤运行,因此在编译项目时就存在访问类。

例如,假设一个资源文件包含以下资源

ResourceId1="This is ResourceId1"

生成的 C# 文件如下所示

namespace SpecifiedNamespaceGoesHere
{
    class ResourceFormatter
    {
        public static string GetString( ResourceManager rm, 
string resourceId ) { return rm.GetString( resourceId ); } } class ResourceId1 { public static string GetString( ResourceManager rm ) { return ResourceFormatter.GetString( rm, "ResourceId1" ); } } }

要访问资源 "ResourceId1",您将使用以下代码

ResourceManager rm = new ResourceManger();
String result = ResourceId1.GetString( rm );

此工具设计的核心是 ResourceFormatter 类,它负责加载和格式化字符串。所有访问类都只调用 ResourceFormatter 类的 GetString 方法,从而减少了代码重复。

改进

此时,我拥有了一个可以实现预期功能的可用版本,但仍有改进空间。

我想要做的第一件事是改变调用者每次请求资源时都要负责管理 ResourceManager 实例并将其传递给访问类的事实。这似乎给检索资源字符串这一看似简单的任务增加了不必要的复杂性。尽管我最初的目标是提供资源标识符的编译时验证,但我希望我的方法尽可能易于使用。

我决定通过包含一个 ResourceManager 单例作为生成器 ResourceFormatter 类的内部类来解决这个问题,该单例负责实际加载资源。这使得用户无需创建和管理 ResourceManager 的实例。现在,生成的 .cs 文件包含了此版本的 ResourceFormatter

class ResourceFormatter
{
    /// <summary>
    /// Loads an unformatted string
    /// </summary>
    /// <PARAM name="resourceId">Identifier of string resource</PARAM>
    /// <returns>string</returns>
    public static string GetString( string resourceId )
    {
        return ResourceManagerSingleton.Instance.GetString( resourceId );
    }

    /// <summary>
    /// Singleton responsible for creating an instance of the ResourceManager 
/// class
/// </summary> private class ResourceManagerSingleton { /// <summary> /// Class constructor /// </summary> /// <remarks>Private to keep class from being instantiated</remarks> private ResourceManagerSingleton() {} /// <summary> /// Static method that creates an instance of the ResourceManager /// </summary> /// <returns>ResourceManager</returns> protected static ResourceManager CreateInstance() { string assemblyName
= Assembly.GetExecutingAssembly().GetName().Name; // Assembly name.Resource File string baseName = string.Format( "{0}.BaseResources", assemblyName ); return new ResourceManager( baseName,
Assembly.GetExecutingAssembly() ); } /// <summary> /// Store for the Instance property /// </summary> private static ResourceManager _instance; /// <summary> /// Instance property /// </summary> /// <value>Read-only access to the ResourceManager instance</value> public static ResourceManager Instance { get { // Allow access from multiple threads lock( typeof(ResourceManagerSingleton) ) { if( _instance == null ) _instance = CreateInstance(); } return _instance; } } } }

这使得只需一个简单的语句即可检索所需的字符串

String result = ResourceIdentifier.GetString();

而不是

ResourceManager rm = new ResourceManger();
String result = ResourceIdentifier.GetString( rm );

更多改进

当我添加对包含格式说明符的字符串资源的支持时,我最初的目标是消除在格式化之前先从资源文件中检索格式字符串的需要。在我进展不久之前,我意识到这是一个进一步推进并提供安全保障的绝佳机会,以防止我在处理此类字符串资源时多次遇到的问题。

无论使用的是 Win32 还是 .NET,一种直到运行时才会出现的错误是在格式化字符串时使用了不正确的参数数量。如果现有字符串资源中的格式说明符数量发生更改,但所有使用该资源的地方都没有相应更改,就会出现问题。

例如,假设我们过去有一个包含单个格式说明符的字符串资源。我们的应用程序会加载该字符串(使用先前实现的访问方法)并进行格式化,传入一个字符串来替换占位符

ResourceId2="The Customer’s name is: {0}"
        
String format = ResourceId2.GetString();
String result = String.Format( format, "Corey" );

后来,我决定将其更新为包含第一个和最后一个名字的单独格式说明符。我更改了资源文件,但在更新执行格式化的代码之前就被打断了。编译时不会生成任何错误,直到使用不正确参数数量的格式化代码运行时才会出现问题。

ResourceId2="The customer’s name is: {0} {1}"

String format = ResourceId2.GetString();
String result = String.Format( format, "Corey" ); // Oops!

这是一个相当普遍的问题,尤其是在大型开发团队和将字符串资源外包给第三方机构进行本地化的项目中。

我真正想要的是让 GetString 方法能够要求正确数量的参数,您懂得,就是编译时检查。使用 .NET 正则表达式支持,可以轻松地确定每个字符串资源中的格式说明符数量,并基于此信息生成 GetString 方法。

对于以下字符串资源

ResourceId2="This is ResourceId2. It contains a replaceable parameter: {0}"

现在将生成此访问类

class ResourceId2
{
    public static string GetString( object arg0 )
    {
        return ResourceFormatter.GetString( "ResourceId2", arg0 );
    }
}

此更新要求向 ResourceFormatter 类添加一个 GetString 方法的重载,该重载可以处理任意数量的参数

class ResourceFormatter
{
    …

    /// <summary>
    /// Loads a formatted string
    /// </summary>
    /// <param name="resourceId">Identifier of string resource</param>
    /// <param name="objects">Array of objects to be passed to string.Format
/// </param>
/// <returns>string</returns> public static string GetString( string resourceId, params object[] objects ) { string format
= ResourceManagerSingleton.Instance.GetString( resourceId ); return string.Format( format, objects ); } }

这使得检索所需的资源字符串并使用单个语句进行格式化成为可能

String result = ResourceIdentifier.GetString( param1, param2 );

而不是

String format = ResourceIdentifier.GetString();
String result = String.Format( format, param1, param2 );

此外,IntelliSense 在您键入语句时会显示预期的参数数量(图 1)。如果您忽略此提示并提供错误的参数列表,代码将无法编译。

图 1:IntelliSense 参数列表

示例应用

示例解决方案包含以下项目

    演示应用程序 – 一个简单的 Windows 应用程序,它使用反射将生成器工具创建了相应访问类的资源字符串填充到列表框中。GenResourceKeys – 生成器工具。PreBuildStep – 包含一个批处理文件,该文件在构建过程中运行生成器工具,并在其中一个依赖文件过时时创建资源访问类。

我不会深入探讨演示应用程序的细节,因为它唯一的目的是展示 GenResourceKeys 项目的功能。

我需要包含 Genghis Group 开发的 CommandLineParser 类的版权声明,该类被生成器用来解析命令行参数

部分版权 © 2002 The Genghis Group

仍有改进空间

为了摆脱因生成文件而引起的烦人的源代码控制提示,我创建了一个作为 Visual Studio .NET 自定义工具运行的版本。由于自定义工具不是本文的重点,因此我将其作为示例项目包含在内,但将其留给读者自行探索。

在创建自定义工具时,我引用了 MSDN 文章“Visual Studio .NET 十大酷功能助您从极客变大师”,以及文章中讨论的 CollectionGen 工具。

您可以通过点击本文顶部的“下载自定义工具版本”来下载该工具。

© . All rights reserved.