为 .NET 生成字符串资源访问器






4.59/5 (15投票s)
2005年1月4日
9分钟阅读

127745

1194
本文介绍了一个用 Visual Basic .NET 编写的实用工具,它可以从 .resx 或 .resources 文件生成 C# 或 VB.NET 源文件。生成的类可以实现对资源字符串标识符名称和格式项数量的编译时检查。
引言
信息技术应用日益全球化。因此,软件开发人员在设计应用程序时,必须考虑到将其本地化为多种语言。理想情况下,以新语言发布设计良好的应用程序,只需创建一套新的本地化资源,并可能在编译时或运行时更改一个设置。.NET Framework 通过交换基于 XML 的 .resx 文件或二进制 .resources 文件以及部署和加载附属程序集,为这种情况提供了出色的支持。有关 .NET Framework 中本地化的良好介绍,请参阅 MSDN。
本地化的一个关键部分是字符串资源的管理。
.NET Framework 包含一个 ResourceManager
类,其中有一个 GetString
方法,您将想要检索的字符串资源的字面名称传递给它,然后它会根据上述文章中描述的规则返回适当的本地化字符串。
ResourceManager rm =
new System.Resources.ResourceManager("MyProjectName.MyResourceBaseName",
Assembly.GetExecutingAssembly());
string localized = rm.GetString( "MyStringResource" );
当然,如果您拼错了资源名称,GetString
将在运行时返回空字符串。根据应用程序的不同,此错误可能难以隔离。由于始终最好在编译时而不是运行时检测错误,因此过去我曾手动为资源中定义的每个字符串添加符号,如下所示
class MyStrings
{
public const string MyStringResource = "MyStringResource";
}
然后我将使用这些来代替
ResourceManager rm =
new System.Resources.ResourceManager("MyProjectName.MyResourceBaseName",
Assembly.GetExecutingAssembly());
string localized = rm.GetString( MyStrings.MyStringResource );
这是一种更安全的方法。然而,一旦项目涉及数百或数千个字符串,定义字符串常量就变得乏味,并且迫切需要自动化。在构建我自己的工具之前,我四处搜索,发现了另外两个解决此问题的尝试:一个命令行实用工具 [1] 和一个利用 CodeDOM 的 Visual Studio .NET 自定义工具 [2]。这两个工具都包含了许多好主意,我鼓励您去了解它们。我将不再花费大量时间解释它们以及它们为何不符合我的需求,而是直接讨论我的工具 StringClassGen。
StringClassGen 功能
StringClassGen.exe 是一个用 Visual Basic .NET 编写的命令行工具。命令语法将在下面的“StringClassGen 用法”部分进行解释。作为一个命令行工具,根据您的构建过程,您可以将其集成到您的构建批处理文件、NAnt 脚本或 Visual Studio .NET 预构建事件中,以将 .resx 和/或 .resources 文件转换为包含属性和方法的源文件,从而帮助您更轻松、更安全地访问您的资源字符串。StringClassGen 具有以下主要功能
- 生成 C# 或 Visual Basic .NET 代码.
- 创建一个单一的 ResourceManager.
- 将相关字符串分组到内部类中.
- 生成接受 IFormatProvider 的重载.
- 酌情生成属性和方法.
- 重复字符串名称检查.
生成 C# 或 Visual Basic .NET 代码
与 [2] 中描述的方法类似,StringClassGen 使用 CodeDOM 按需生成 C# 或 Visual Basic .NET 代码。由于使用 CodeDOM 的代码并不特别有趣(只是繁琐),我将在此处不展示任何内容。足以说明,生成的代码由一组静态属性和方法组成,下面将更详细地讨论。当此代码集成到 C# 或 Visual Basic .NET 项目中时,它以类型安全的方式抽象了从程序集资源加载字符串的过程。
创建一个单一的 ResourceManager
StringClassGen 生成的顶级类封装了一个 ResourceManager
的单例实例,该实例从正在执行的程序集加载字符串。(StringClassGen 的一个可能扩展是允许 ResourceManager
从除了正在执行的程序集之外的程序集加载。)请注意,根据 .NET Framework 文档,ResourceManager.GetString
方法是线程安全的。
将相关字符串分组到内部类中
在代码中为字符串资源创建标识符时,我常常发现将它们组织成组很有用,例如按它们所使用的网页或表单分组。内部类为这个概念提供了一个很好的封装。这就引出了在生成字符串类时如何识别所需组的问题。
如果您在 Visual Studio 中编辑过 .resx 文件,您可能已经注意到,每个字符串条目都关联一个名为“comment”的列,您可以在其中放置任何您想要的文本。如果您采用约定,即“comment”列的内容表示字符串标识符的所需组(内部类),那么在使用 .resx 文件时,StringClassGen 将创建适当的内部类并用正确的字符串标识符填充它们。(显然,这意味着在此方案中,“comment”列的内容必须是目标编程语言中有效的标识符名称。)
如何实现以及为什么它只适用于 .resx 文件值得讨论。 .NET Framework 提供了一个接口 IResourceReader
以及两个实现 ResourceReader
和 ResxResourceReader
,它们允许您分别从 .resources 和 .resx 文件中提取字符串资源名称-值对。如果您查看 .resx 文件格式的内部,您会发现它只是 XML,其中包含如下所示的字符串元素
<data name="String1">
<value>This is the first string.</value>
<comment>Class1</comment>
</data>
不幸的是,IResourceReader
接口没有提供访问 <comment>
的机制。实际上,对于 .resources 文件(.resx 文件的编译二进制格式),注释不再是文件数据的一部分。StringClassGen 使用 ResourceFileReader
处理 .resources 文件,如以下代码片段所示
Protected Overrides Sub ProduceStrings()
Dim reader As New ResourceReader(filename)
Try
Dim readerEnumerator As IDictionaryEnumerator = reader.GetEnumerator()
While readerEnumerator.MoveNext
AddString(readerEnumerator.Key.ToString(), _
readerEnumerator.Value.ToString())
End While
Catch ex As Exception
Finally
reader.Close()
End Try
End Sub
但是对于 .resx 文件,我们有另一种选择。由于 .resx 文件只是 XML,我们可以完全放弃使用 IResourceReader
接口,并将其作为普通 XML 文件进行处理。对于 StringClassGen,我选择使用 XPathNavigator
和 XPathDocument
Protected Overrides Sub ProduceStrings()
Dim doc As New XPathDocument(filename)
Dim nav As XPathNavigator
nav = doc.CreateNavigator
'// Sort by the comment field (which is used
' as the class name) so we'll know when to create
'// new classes as we process.
Dim exp As XPathExpression
exp = nav.Compile("//data")
exp.AddSort("comment", XmlSortOrder.Ascending, _
XmlCaseOrder.None, "", XmlDataType.Text)
Dim nodes As XPathNodeIterator = nav.Select(exp)
While nodes.MoveNext()
Dim comment As String
Dim value As String
nodes.Current.MoveToFirstChild()
value = nodes.Current.Value
nodes.Current.MoveToParent()
Dim commentNodes As XPathNodeIterator = _
nodes.Current.SelectDescendants("comment", "", False)
If commentNodes.Count > 0 Then
nodes.Current.MoveToFirstChild()
nodes.Current.MoveToNext()
comment = nodes.Current.Value
nodes.Current.MoveToParent()
Else
comment = ""
End If
AddString(nodes.Current.GetAttribute("name", _
nav.NamespaceURI), value, comment)
End While
End Sub
此例程最有趣的部分是按“comment”排序。这允许我们按顺序处理属于给定内部类的所有字符串,以便在使用 CodeDOM 生成标识符时,一次只有一个内部类处于“开放状态”。在 .resx 文件中定义了空注释的字符串将不会放置在内部类中,而是属于最外层生成的类。
生成接受 IFormatProvider 的重载
.NET Framework 中更有用的字符串操作功能之一是能够将“格式项”令牌嵌入到字符串中,并使用 String.Format
方法在运行时替换它们。通常,要在使用字符串资源时利用此功能,需要编写以下代码(其中已获取资源管理器)
formattedString =
String.Format( resourceManager.GetString( "StringWithParams" ),
"Param Value 1", "Param Value 2" );
其中与 StringWithParams
关联的值可能是:“这是第一个参数:{0}。这是第二个参数:{1}”。
为了简化此过程,StringClassGen 使用正则表达式匹配来尝试检测它处理的字符串中的格式项标记,如果找到任何标记,则生成类似于 [1] 中那些接受正确数量参数的包装方法。包装方法处理从资源中检索字符串和格式化。因此,生成以下代码
Public Overloads Shared Function StringWithParams(ByVal param0 _
As Object, ByVal param1 As Object) As String
Return [String].Format(CultureInfo.InvariantCulture, _
resources.GetString("StringWithParams"), param0, param1)
End Function
或
public static string StringWithParams(object param0, object param1) {
return String.Format(CultureInfo.InvariantCulture,
resources.GetString("StringWithParams"), param0, param1);
}
允许我们将上述代码简化为
formattedString =
GeneratedStringResource.StringWithParams( "Param Value 1", "Param Value 2" );
此外,正如 FxCop 喜欢指出的那样,当您使用 String.Format
时,您应该使用接受 IFormatProvider
的重载来提供特定于文化的格式信息,尤其是在对全球友好的应用程序中。因此,StringClassGen 实际上为每个字符串方法创建了两个重载,一个接受客户端提供的 IFormatProvider
,另一个使用您通过命令行选项指定的默认格式提供程序,可以是 InvariantCulture
、CurrentUICulture
或 CurrentCulture
。如果未指定格式提供程序,则默认使用 InvariantCulture
。
酌情生成属性和方法
正如我们在上一节中看到的,带有格式项令牌的字符串会导致生成带有一个或多个参数的**方法**。如果字符串不包含任何格式项令牌,则无需生成方法:**属性**更合适。因此,不带格式令牌的字符串将触发生成以下代码
Public Shared ReadOnly Property StringWithNoParams As String
Get
Return resources.GetString("StringWithNoParams")
End Get
End Property
或
public static string StringWithNoParams {
get {
return resources.GetString("StringWithNoParams");
}
}
重复字符串名称检查
当您在 Visual Studio .NET 或您选择的文本编辑器中编辑 .resx 文件时,没有任何东西可以阻止您为多个字符串使用相同的字符串名称。然而,这几乎肯定是一个错误,因为无法使用 ResourceManager
检索具有给定名称的多个字符串。因此,StringClassGen.exe 会跟踪已使用的名称列表,如果同一名称在资源文件中使用多次,则返回错误。
StringClassGen 用法
StringClassGen.exe 的行为可以通过几个命令行参数来控制
- (-vb|-cs) - 生成代码的语言,VB.NET 或 C#。默认是 C#。
- (-c) - 指定将 .resx 文件注释作为生成属性和方法的
<summary>
注释输出,而不是用它们将字符串分组到内部类中。如果您选择此选项,所有字符串都将定义在顶级类中。默认情况下此选项关闭。 - (-ns namespacename) - 生成的顶级字符串类所在的命名空间。默认是资源文件的名称(不含扩展名)加上字符串“Namespace”作为后缀。
- (-ic|-cuic|-cc) - 使用
InvariantCulture
、CurrentUICulture
或CurrentCulture
作为默认格式提供程序(默认为InvariantCulture
)。 - (-class classname) - 生成的字符串类的名称,默认为资源文件的名称减去扩展名。
- (-out outfilename) - 生成文件的名称。如果省略此选项,输出将发送到标准输出。
您应该将 StringClassGen 生成的源文件添加到您的程序集中,并将其与其余源代码一起编译。根据您使用的构建工具,您可能希望建立一个依赖项,以便 StringClassGen 仅在 .resx 文件比源文件新时才运行。
如果您在使用生成的源代码时遇到困难,最可能的问题是您向 StringClassGen 提供了错误的命名空间 (-ns)。这会在您第一次访问资源管理器时导致异常。Visual Studio .NET 默认将项目包含的 .resx 文件中的资源放入项目的默认命名空间中,这可以在项目属性中配置。您很可能希望将其作为 -ns 参数传递给 StringClassGen。(您可以通过在 ildasm.exe 中检查程序集清单来验证您特定资源的名称。但是,此过程的详细解释超出了本文的范围。)
演示项目
下载中包含 Visual Basic .NET 和 C# 中的示例应用程序,它们使用 StringClassGen 生成的类从资源中加载一些字符串。在每个应用程序中,您可以通过将条件编译指令“INNERCLASSES
”定义为 true,并包含相应版本的生成的 TestStringResource.cs/TestStringResource.vb,来切换是否使用内部类。以下是生成文件的示例命令行(相对于 StringClassGen 的构建输出目录)
- C#,INNERCLASSES:StringClassGen.exe ..\TestStringResource.resx -cs -ns CSTestApplication -out ..\CSTestApplication\TestStringResource
- C#,无 INNERCLASSES:StringClassGen.exe ..\TestStringResource.resx -cs -c -ns CSTestApplication -out ..\CSTestApplication\TestStringResource
- VB,INNERCLASSES:StringClassGen.exe ..\TestStringResource.resx -vb -ns VBTestApplication -out ..\VBTestApplication\TestStringResource
- VB,无 INNERCLASSES:StringClassGen.exe ..\TestStringResource.resx -vb -c -ns VBTestApplication -out ..\VBTestApplication\TestStringResource
历史
- 首次发布:2005年1月2日。
- 允许选择默认格式提供程序:2005年1月25日。