VS.NET 基于 CodeDOM 的字符串资源管理自定义工具






4.52/5 (11投票s)
2004年6月23日
13分钟阅读

126296

1066
一个 VS.NET 自定义工具,通过 CodeDOM 和 EnvDTE 创建,用于在 VS.NET 环境中通过 IntelliSense 和错误检查来简化资源字符串的管理。
引言
本地化大型应用程序可能是一项繁琐的任务。当面对成百上千个本地化字符串时,很容易在 ResourceManager.GetString
方法中输入错误。另一类错误可能源于不同语言版本的资源文件之间的不一致(例如,与英文文件相比,德语文件中缺少几个字符串)。最糟糕且难以追踪的 bug 可能源于在 String.Format
方法中使用资源字符串,并且在不同文件中,同一个资源拥有不同数量的参数({0}, {1}, ...
)。所有这些问题都可以通过 ResourceClassGenerator
自定义工具来消除。
首先,我想强调的是,这个基本概念并非我原创。您可以在这里找到其出处
我认为作者的想法非常出色,并且我一直在使用他的原始自定义工具。基本原理是——当您运行自定义工具时,会从 .resx 文件生成一个类文件,其中每个字符串都由一个类表示。每个类都有一个静态 GetString
方法,该方法将调用传递给基类 ResourceFormatter
,然后由该类处理实际的 ResourceManager.GetString
调用。因此,例如,如果您为生成的 .resx 文件设置的命名空间是 Resources.Strings
,则您将在代码中使用 Resources.Strings.Label1.GetString()
表达式来访问 Label1
资源。Whidbey 中已经引入了类似的功能,您可以在这里找到更多信息
这确保了 IntelliSense,从而消除了输入错误。如果某个特定资源使用了 String.Format
参数,则 GetString
方法签名会得到相应调整,因此必须使用正确数量的参数来调用它。
修改
所有这些功能都已在原始工具中实现,但随着我使用得越多,我发现它缺少某些功能,并且我渴望改进它。以下是主要的修改
- 如果生成的类具有与默认命名空间不同的命名空间,则该工具无法正常工作,因此有必要生成一种自动查找
ResourceManager
实例所需的BaseName
的方法。这是通过EnvDTE
(VS.NET 中用于自动化和扩展的对象模型)实现的。MSDN 中有大量关于EnvDTE
的信息。如果您不熟悉它,可以尝试将以下文章作为起点 - 原始版本中实现的第二个高度要求的功能是比较同一资源集的不同语言版本。生成的类文件是在基本语言资源文件下创建的,但它也旨在与其他语言版本一起使用。例如,预期 Strings.de.resx 和 Strings.en.resx 具有相同数量的字符串资源和相同数量的参数。我通过在运行工具时比较失败时抛出异常来实现此功能,这会在 VS.NET 环境中显示一个错误消息框(该异常也会作为任务列表窗口中的一个构建错误出现)。如果发生异常,则不会生成资源类文件。在这种情况下,合理的假设是属于同一资源集的所有文件都放置在同一个文件夹中。
- 原始工具仅在 C# 环境中工作。当然,可以遵循相同的逻辑编写 VB.NET 版本,但真正的解决方案呼唤 CodeDOM。因此,我使用 CodeDOM 代码生成原理从头重写了整个工具。使工具与语言无关需要一种检测当前工作环境类型(C#、VB.NET 等)的方法,这也通过
EnvDTE
实现。
自定义工具基础
有关 VS.NET 自定义工具的良好定义和解释可以在以下位置找到:Visual Studio .NET 的十大酷炫功能助您从技术爱好者变身专家。
其中一段引用
"自定义工具是一个组件,它会在输入文件每次更改并保存时运行。自定义工具通过IVsSingleFileGenerator
COM 接口上的Generate
方法接收输入文件的名称以及文件中的字节。此方法负责生成将写入 C# 或 Visual Basic .NET 的字节,具体取决于项目类型。"
遵循本文所述的原则,以下是创建自定义工具所需的几个基本步骤的简要说明
- 而不是实现
IVsSingleFileGenerator
接口,而是派生自托管类Microsoft.VSDesigner.BaseCodeGeneratorWithSite
。该类在 VS.NET 2002 的 Microsoft.VSDesigner.dll 中是公开的,但在 VS.NET 2003 中已设为内部。幸运的是,MS 在GotDotNet 用户示例:BaseCodeGeneratorWithSite 上发布了其源代码。可以在那里找到 1.0 和 1.1 版本。构建所需版本并将其 .dll 引用到您的自定义工具项目中。
- 将
Guid
属性添加到派生类,该属性对于向 COM 公开是必需的。 - 重写
GenerateCode
方法。
工具构建后,必须执行某些操作才能使用它,这在文章底部的“安装和用法”部分有描述。如果您只想使用该工具,并且对 EnvDTE
和 CodeDOM 的代码、问题和技巧不感兴趣,请转到该部分,并跳过后面带有详细代码解释的部分。
深入代码
自定义工具的起点是重写的 GenerateCode
方法
[Guid("8442788C-A1A7-4475-A0C6-C2B83BD4221F")]
public class ResourceClassGenerator : BaseCodeGeneratorWithSite
{
protected override byte[] GenerateCode(string fileName,
string fileContents)
{
...
为了在所需的资源文件上实例化 ResourceManager
,需要在构造函数中提供 baseName
参数。根据 MSDN 的说法,baseName
表示资源的根名称。实际上,它等于
[资源文件的默认命名空间].[资源文件的基本文件名(不含扩展名)]
资源文件的默认命名空间等于其父文件夹的默认命名空间。问题在于,在 VB 项目中,所有文件夹的命名空间都与其项目的DefaultNamespace 相同。在 C# 项目中,它取决于项目层次结构中文件夹的位置。这时 EnvDTE
就非常有用了。在 EnvDTE
对象模型中,每个文件和文件夹都由 EnvDTE.ProjectItem
类表示。它包含一组 EnvDTE.Property
类,这些类对于不同的文件类型(.cs、.resx 等)有所不同。它包含了文件属性网格中可见的所有属性,但也包含其他属性。其中一个属性是 DefaultNamespace
属性,但它仅在文件夹的 Properties
集合中可用。因此,有必要找到资源文件的父文件夹 ProjectItem
来构造 BaseName
。
从自定义工具中获取当前的 EnvDTE
对象模型非常容易。表示自定义工具正在运行的文件(的)的 ProjectItem
对象可以通过以下方式获取
ProjectItem projItem = (ProjectItem)GetService(typeof(ProjectItem));
这是生成资源文件 BaseName
的其余代码
Project proj = projItem.ContainingProject;
string baseName;
//If resource file resides in the project root,
//then the file's namespace is identical to the project's DefaultNamespace
if(Path.GetDirectoryName(proj.FileName)==Path.GetDirectoryName(fileName))
{
baseName = (string)proj.Properties.Item("DefaultNamespace").Value;
}
//If not, then find the file's parent folder and get its DefaultNamespace
else
{
//The parent folder can't be reached directly through some property.
//Instead, it's necessary to go down through
//the whole hierarchy from the project root.
//The hierarchy is fetched from the resource file's full filename
string[] arrTemp = Path.GetDirectoryName(fileName).Replace(
Path.GetDirectoryName(proj.FileName) +
"\\","").Split(new char[] {'\\'});
ProjectItems projItems = proj.ProjectItems;
for (int i = 0; i < arrTemp.Length; i++)
{
projItems = projItems.Item(arrTemp[i]).ProjectItems;
}
baseName = (string)
((ProjectItem)projItems.Parent).Properties.Item("DefaultNamespace").Value;
}
//BaseName equals [resource file's default namespace]
// .[the base filename(without extension)]
baseName = baseName + "." +
Helper.GetBaseFileName(Path.GetFileName(fileName));
Helper
类对于获取基本文件名(移除可能存在的附加点)是必需的。如果,例如,一个人不想使用默认资源文件,则这是必需的。例如,如果期望的行为是在 Strings.de.resx 文件中出现缺失资源时抛出错误,而不是显示默认 Strings.resx 的内容,则可能需要这样做。在这种情况下,将没有默认(大概是英语)的 Strings.resx,而是使用 Strings.en.resx。因此,存在附加点的可能性。
internal class Helper
{
public static string GetBaseFileName(string fileName)
{
if (fileName.IndexOf(".")==-1)
{
return fileName;
}
else
{
return fileName.Substring(0, fileName.IndexOf("."));
}
}
}
重写的 GenerateCode
方法提供了 fileName
和 fileContents
参数。它们代表基本语言文件(自定义工具正在运行的文件)的名称和内容。为了与其他属于同一资源集的文件进行比较,有必要也获取它们的内容,遍历每个资源,并执行必要的验证。通过正则表达式检查每个字符串中参数的数量是否匹配。
//Compare all different-language resource files and
//raise an error in case of inconsistencies with the base resource file
string ParameterMatchExpression = @"(\{[^\}\{]+\})";
string path = Path.GetDirectoryName(fileName);
DirectoryInfo folder = new DirectoryInfo(path);
//Create a sorted list of string resources from the base resource file
//Sorted list is used to perform validation in alphabetical order
ResXResourceReader reader = new ResXResourceReader(fileName);
IDictionaryEnumerator enumerator = reader.GetEnumerator();
SortedList baseList = new SortedList();
while (enumerator.MoveNext())
{
MatchCollection mc =
Regex.Matches(enumerator.Value.ToString(),
ParameterMatchExpression);
//The resource name is the key argument and
//the number of String.Format arguments is the value argument
baseList.Add(enumerator.Key, mc.Count);
}
reader.Close();
//Get all other .resx files in the same folder
foreach(FileInfo file in folder.GetFiles("*.resx"))
{
//Consider only files with the same name
//(for instance - Strings.de.resx has
// the same name as Strings.en.resx)
if ((file.FullName!=fileName) &&
(Helper.GetBaseFileName(file.Name) ==
Helper.GetBaseFileName(Path.GetFileNameWithoutExtension(fileName))))
{
//Create a sorted list of string resources
//from the found resource file
reader = new ResXResourceReader(file.FullName);
enumerator = reader.GetEnumerator();
SortedList list = new SortedList();
while (enumerator.MoveNext())
{
MatchCollection mc =
Regex.Matches(enumerator.Value.ToString(),
ParameterMatchExpression);
list.Add(enumerator.Key, mc.Count);
}
reader.Close();
enumerator = baseList.GetEnumerator();
try
{
//iterate through the sorted list created
//from a base resource file and
//compare its key and value with the ones from
//the sorted list created from the other file
while (enumerator.MoveNext())
{
if (list.ContainsKey(enumerator.Key)==false)
{
throw new Exception("Resource " +
enumerator.Key.ToString() +
" is missing in the file " + file.Name);
}
if (!list[enumerator.Key].Equals(enumerator.Value))
{
throw new Exception("Resource " +
enumerator.Key.ToString() +
" in the file " + file.Name +
" has incorrect number of arguments.");
}
}
}
catch (Exception ex)
{
//The exception will be thrown in the VS.NET environment
throw (ex);
}
}
}
找到 BaseName
并执行验证后,一切都已准备好进行代码生成,除了选择代码生成的语言。同样,EnvDTE
使这项任务变得容易。
CodeDomProvider provider = null;
switch(proj.CodeModel.Language)
{
case CodeModelLanguageConstants.vsCMLanguageCSharp:
provider = new Microsoft.CSharp.CSharpCodeProvider();
break;
case CodeModelLanguageConstants.vsCMLanguageVB:
provider = new Microsoft.VisualBasic.VBCodeProvider();
break;
}
所选提供程序用于 CodeDOM 代码生成过程。所有与此过程相关的代码都打包到一个名为 CodeDomResourceClassGenerator
的独立类中。重写的 GenerateCode
方法中的其余代码将所有必要的参数传递给静态 CodeDomResourceClassGenerator.GenerateCode
方法,并从中获取生成的代码。它返回一个字节数组,该数组将用于构造类文件,该文件将位于项目层次结构中的 .resx 文件下方。
//This is the value of the .resx file "Custom Tool Namespace" property
string classNameSpace = FileNameSpace != String.Empty ?
FileNameSpace : Assembly.GetExecutingAssembly().GetName().Name;
return CodeDomResourceClassGenerator.GenerateCode(
provider,
fileName,
fileContents,
baseName,
classNameSpace
);
CodeDomResourceClassGenerator.GenerateCode
方法构建 CodeDOM 图,这是一种语言无关的蓝图,它告诉选定的 CodeDomProvider
如何生成代码。在生成代码字符串后,将其解析为所需的字节数组。
internal class CodeDomResourceClassGenerator
{
public static byte[] GenerateCode(CodeDomProvider provider,
string fileName,
string fileContents,
string baseName,
string classNameSpace)
{
CodeCompileUnit compileUnit = BuildGraph(fileName,
fileContents, baseName, classNameSpace);
ICodeGenerator gen = provider.CreateGenerator();
using(StringWriter writer = new StringWriter())
using(IndentedTextWriter tw = new IndentedTextWriter(writer))
{
gen.GenerateCodeFromCompileUnit(compileUnit,
tw, new CodeGeneratorOptions());
string code = writer.ToString().Trim();
return System.Text.Encoding.ASCII.GetBytes(code);
}
}
...
所有代码生成的核心都包含在 BuildGraph
方法中,该方法将在下一节中进行更详细的解释。
构建 CodeDOM 图
在深入 CodeDOM 建模之前,必须知道生成的文件的预期外观。以下 C# 示例显示了资源工具生成文件的预期设计
using System;
using System.Resources;
using System.Reflection;
// ------------------------------------------------------------------------
// <autogeneratedinfo>
// This code was generated by:
// ResourceClassGenerator custom tool for VS.NET
//
// It contains classes defined from the contents of the resource file:
// [.resx file location]
//
// Generated: [Date & Time of generation]
// </autogeneratedinfo>
// ------------------------------------------------------------------------
namespace CSharpTest.Resources {
/// <summary>
/// Provides access to an assembly's string resources
/// </summary>
class ResourceFormatter
{
private static System.Resources.ResourceManager _ResourceManager;
/// <summary>
/// ResourceManager property with lazy initialization
/// </summary>
/// <value>An instance of the ResourceManager class.</value>
private static System.Resources.ResourceManager ResourceManager {
get
{
if ((_ResourceManager == null))
{
_ResourceManager = new System.Resources.ResourceManager(
[BaseName],
System.Reflection.Assembly.GetExecutingAssembly());
}
return _ResourceManager;
}
}
/// <summary>
/// Loads an unformatted string
/// </summary>
/// <param name="resourceId">Identifier of string resource</param>
/// <returns>string</returns>
public static string GetString(string resourceId)
{
return ResourceFormatter.ResourceManager.GetString(resourceId);
}
/// <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, object[] args)
{
string format =
ResourceFormatter.ResourceManager.GetString(resourceId);
return string.Format(format, args);
}
}
/// <summary>
/// Access to resource identifier lbl1
/// </summary>
class lbl1
{
public static string GetString()
{
return ResourceFormatter.GetString("lbl1");
}
}
/// <summary>
/// Access to resource identifier lbl2
/// </summary>
class lbl2
{
public static string GetString(object arg0, object arg1)
{
return ResourceFormatter.GetString("lbl2",
new object[] {arg0, arg1});
}
}
...
所有这些代码语句都需要转换为适当的 CodeDOM 表达式。代码树中的顶级对象是 CodeCompileUnit
。
private static CodeCompileUnit BuildGraph(string fileName,
string fileContents,
string baseName,
string classNameSpace)
{
CodeCompileUnit compileUnit = new CodeCompileUnit();
...
CodeCompileUnit
包含一组 CodeNamespace
对象,这些对象代表生成代码中的命名空间。大多数编程元素在 CodeDOM
对象模型中都有相应的表示,类可以通过 CodeTypeDeclaration
对象添加到命名空间,字段可以通过 CodeMemberField
对象添加到类中,依此类推。关键在于,CodeDOM 只能在处理所有语言中都存在的元素时,才能生成所有语言有效的代码。因此,例如,不支持 VB.NET 的 With
语句,也不支持预处理器指令(包括广受欢迎的 region
指令)。这可以通过某些称为片段的 CodeDom 类(如 CodeSnippetStatement
、CodeSnippetExpression
等)来实现,根据 MSDN 的说法,这些类被解释为直接包含在源代码中的字面代码片段,而无需修改。但是,这可能会破坏语言的中立性,并且 CodeDOM 可能无法为某些语言生成有效的代码。
考虑到所有这些,我决定不使用片段并保持语言的独立性。这就是为什么生成的代码设计不包含 region
指令。它们可能有助于将基类 ResourceFormatter
与众多资源类分开,但这并不重要,因为整个资源工具的理念是基于“设置好就不用管”的原则,无需浏览或更改生成的代码。只需在资源更改时重新生成即可。
尽管如此,即使遵循严格的语言兼容性规则,也存在一些棘手的问题需要解决。默认情况下,VB.NET CodeDOM 提供程序输出 Option Strict Off
(控制是否允许隐式类型转换)和 Option Explicit On
(控制是否需要声明变量)。每个有自尊的 VB 程序员都会始终将这两个选项都设置为 On
。我在一本出色的书——Kathleen Dollard 的《Code Generation in Microsoft .NET》中找到了解决此问题的方法。以下是其内容
//Just for VB.NET
compileUnit.UserData.Add("AllowLateBound", false);
compileUnit.UserData.Add("RequireVariableDeclaration", true);
确实,我用Reflector 进行了检查,这在 Microsoft.VisualBasic.VBCodeGenerator.GenerateCompileUnitStart
方法(在 System.dll 中)中是硬编码的,而在 Microsoft.CSharp.CSharpCodeGenerator.GenerateCompileUnitStart
中没有类似的东西。另一个重要问题的答案也可以在这些方法中找到。使用 CodeDOM 生成的每个文件顶部都有如下注释
//------------------------------------------------------
// <AUTOGENERATED>
// This code was generated by a tool.
// Runtime Version: [Version number]
//
// Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated.
// </AUTOGENERATED>
//------------------------------------------------------
由于我想添加更多代码生成注释(例如源 .resx 文件位置和生成时间),我认为在这里添加它们很合适,但这不可能,因为这在 VBCodeGenerator
和 CSharpCodeGenerator
的 GenerateCompileUnitStart
方法中也是硬编码的。因此,我使用了不同的 <autogeneratedinfo>
注释标签。
前面提到的那本书中的另一个绝佳建议是关于 VB.NET Imports
和 C# using
语句。它们通过 CodeNamespace.Imports
属性添加,该属性代表 CodeNamespaceImportCollection
。VB 提供程序始终将 Imports
语句放在文件顶部,命名空间之外,并且在 VB.NET 中 Imports
适用于整个文件。在 C# 中,using
语句可以放在每个命名空间下方,C# 提供程序会将其放在命名空间声明下方。如果想要完全相同的结果,应该采用 VB.NET 的方法,并将 C# using
语句放在命名空间之外。书中的绝妙技巧是添加一个以空字符串作为命名空间名称的 CodeNamespace
,这样就不会输出命名空间声明
//Dummy namespace, so the Import statements would appear
//above the main namespace declaration
CodeNamespace dummyNamespace = new CodeNamespace("");
compileUnit.Namespaces.Add(dummyNamespace);
//Namespace Import
dummyNamespace.Imports.Add(new CodeNamespaceImport("System"));
dummyNamespace.Imports.Add(new CodeNamespaceImport("System.Resources"));
dummyNamespace.Imports.Add(new CodeNamespaceImport("System.Reflection"));
现在所有先决条件都已满足,剩下的就是纯粹的 CodeDOM 建模。一个更复杂的编程语句可能需要十几个 CodeDOM 语句。因此,下面的代码进行了注释,显示了上面设计的资源工具类文件中的每个语句如何用相应的 CodeDOM 语句块表示。
//Namespace
CodeNamespace nSpace = new CodeNamespace(classNameSpace);
compileUnit.Namespaces.Add(nSpace);
//Namespace comments
nSpace.Comments.Add(new CodeCommentStatement(
"-----------------------------" +
"------------------------------------------------"));
nSpace.Comments.Add(new
CodeCommentStatement(" <AUTOGENERATEDINFO>"));
nSpace.Comments.Add(new
CodeCommentStatement(" This code was generated by:"));
nSpace.Comments.Add(new
CodeCommentStatement(" ResourceClassGenerator" +
" custom tool for VS.NET"));
nSpace.Comments.Add(new CodeCommentStatement(""));
nSpace.Comments.Add(new CodeCommentStatement(
" It contains classes defined from the" +
" contents of the resource file:"));
nSpace.Comments.Add(new CodeCommentStatement(" "
+ fileName));
nSpace.Comments.Add(new CodeCommentStatement(""));
nSpace.Comments.Add(new CodeCommentStatement(
" Generated: " + DateTime.Now.ToString("f",
new System.Globalization.CultureInfo("en-US"))));
nSpace.Comments.Add(new CodeCommentStatement(" </AUTOGENERATEDINFO>"));
nSpace.Comments.Add(new CodeCommentStatement(
"-----------------------------------" +
"------------------------------------------"));
//Class ResourceFormatter
CodeTypeDeclaration cResourceFormatter =
new CodeTypeDeclaration("ResourceFormatter");
//This is automatically internal,
//only nested classes can be private
cResourceFormatter.TypeAttributes =
System.Reflection.TypeAttributes.NotPublic;
//ResourceFormatter Comments
cResourceFormatter.Comments.Add(new
CodeCommentStatement("<SUMMARY>", true));
cResourceFormatter.Comments.Add(new CodeCommentStatement(
"Provides access to an assembly's string resources", true));
cResourceFormatter.Comments.Add(new
CodeCommentStatement("</SUMMARY>", true));
nSpace.Types.Add(cResourceFormatter);
//Field _ResourceManager
CodeMemberField fResourceManager =
new CodeMemberField(typeof(System.Resources.ResourceManager),
"_ResourceManager");
fResourceManager.Attributes =
(MemberAttributes)((int)MemberAttributes.Static +
(int)MemberAttributes.Private);
cResourceFormatter.Members.Add(fResourceManager);
//Property ResourceManager
CodeMemberProperty pResourceManager = new CodeMemberProperty();
pResourceManager.Name = "ResourceManager";
pResourceManager.Type = new
CodeTypeReference(typeof(System.Resources.ResourceManager));
pResourceManager.Attributes =
(MemberAttributes)((int)MemberAttributes.Static
+ (int)MemberAttributes.Private);
//It is read-only property
pResourceManager.HasSet = false;
//ResourceManager Comments
pResourceManager.Comments.Add(new
CodeCommentStatement("<SUMMARY>", true));
pResourceManager.Comments.Add(new CodeCommentStatement(
"ResourceManager property with lazy initialization", true));
pResourceManager.Comments.Add(new
CodeCommentStatement("</SUMMARY>", true));
pResourceManager.Comments.Add(new CodeCommentStatement(
"<VALUE>An instance of the ResourceManager" +
" class.</VALUE>", true));
CodeVariableReferenceExpression _ResourceManager =
new CodeVariableReferenceExpression("_ResourceManager");
//ResourceManager assignment line inside the if block
CodeAssignStatement assignResourceManager = new CodeAssignStatement
(
_ResourceManager,
new CodeObjectCreateExpression
(
typeof(System.Resources.ResourceManager),
new CodeExpression[]
{
new CodePrimitiveExpression(baseName),
new CodeMethodInvokeExpression
(
new
CodeTypeReferenceExpression(typeof(System.Reflection.Assembly)),
"GetExecutingAssembly",
new CodeExpression[] {}
)
}
)
);
//ResourceManager if block - for lazy initialization
CodeConditionStatement ifBlock = new CodeConditionStatement
(
new CodeBinaryOperatorExpression
(
_ResourceManager,
CodeBinaryOperatorType.IdentityEquality,
new CodePrimitiveExpression(null)
),
new CodeStatement[]
{
assignResourceManager
}
);
pResourceManager.GetStatements.Add(ifBlock);
pResourceManager.GetStatements.Add(new
CodeMethodReturnStatement(_ResourceManager));
cResourceFormatter.Members.Add(pResourceManager);
//GetString method
CodeMemberMethod mGetString = new CodeMemberMethod();
mGetString.Name = "GetString";
mGetString.Attributes = (MemberAttributes)
((int)MemberAttributes.Static +
(int)MemberAttributes.Public);
mGetString.ReturnType = new CodeTypeReference(typeof(string));
mGetString.Parameters.Add(new
CodeParameterDeclarationExpression(typeof(string),
"resourceId"));
//GetString method comments
mGetString.Comments.Add(new
CodeCommentStatement("<SUMMARY>", true));
mGetString.Comments.Add(new
CodeCommentStatement("Loads an unformatted string", true));
mGetString.Comments.Add(new CodeCommentStatement("</SUMMARY>", true));
mGetString.Comments.Add(new
CodeCommentStatement("<PARAM name='\"resourceId\"'>Identifier"
+ " of string resource</PARAM>", true));
mGetString.Comments.Add(new
CodeCommentStatement("<RETURNS>string</RETURNS>", true));
//GetString method statements
CodePropertyReferenceExpression propExp =
new CodePropertyReferenceExpression(new
CodeTypeReferenceExpression("ResourceFormatter"),
"ResourceManager");
CodeMethodInvokeExpression invokeResourceManager =
new CodeMethodInvokeExpression(propExp,
"GetString",
new CodeExpression[] {new
CodeArgumentReferenceExpression("resourceId")});
mGetString.Statements.Add(new
CodeMethodReturnStatement(invokeResourceManager));
cResourceFormatter.Members.Add(mGetString);
//The second overloaded GetString method
mGetString = new CodeMemberMethod();
mGetString.Name = "GetString";
mGetString.Attributes =
(MemberAttributes)((int)MemberAttributes.Static
+ (int)MemberAttributes.Public);
mGetString.ReturnType = new CodeTypeReference(typeof(string));
mGetString.Parameters.Add(new
CodeParameterDeclarationExpression(typeof(string),
"resourceId"));
CodeParameterDeclarationExpression objects =
new CodeParameterDeclarationExpression(typeof(object[]), "args");
mGetString.Parameters.Add(objects);
//The second GetString method comments
mGetString.Comments.Add(new
CodeCommentStatement("<SUMMARY>", true));
mGetString.Comments.Add(new
CodeCommentStatement("Loads a formatted string", true));
mGetString.Comments.Add(new
CodeCommentStatement("</SUMMARY>", true));
mGetString.Comments.Add(new
CodeCommentStatement("<PARAM name='\"resourceId\"'>Identifier"
+ " of string resource</PARAM>", true));
mGetString.Comments.Add(new
CodeCommentStatement("<PARAM name='\"objects\"'>Array" +
" of objects to be passed to string.Format</PARAM>", true));
mGetString.Comments.Add(new
CodeCommentStatement("<RETURNS>string</RETURNS>", true));
//The second GetString method statements
mGetString.Statements.Add(
new CodeVariableDeclarationStatement(typeof(string),
"format", invokeResourceManager));
CodeMethodInvokeExpression invokeStringFormat =
new CodeMethodInvokeExpression(
new CodeTypeReferenceExpression(typeof(string)),
"Format",
new CodeExpression[]
{
new CodeArgumentReferenceExpression("format"),
new CodeArgumentReferenceExpression("args")
}
);
mGetString.Statements.Add(new CodeMethodReturnStatement(invokeStringFormat));
cResourceFormatter.Members.Add(mGetString);
//Iterate through every resource and create
//the static class with the appropriate GetString method
using(Stream inputStream = new
MemoryStream(System.Text.Encoding.UTF8.GetBytes(fileContents)))
using(ResXResourceReader resReader =
new ResXResourceReader(inputStream))
{
IDictionaryEnumerator resEnumerator = resReader.GetEnumerator();
SortedList sList = new SortedList();
while(resEnumerator.MoveNext())
{
sList.Add(resEnumerator.Key, resEnumerator.Value);
}
resEnumerator = sList.GetEnumerator();
// Create a class definition for each string entry
while(resEnumerator.MoveNext())
{
string resKey = (string)resEnumerator.Key;
//Resource class
CodeTypeDeclaration cResource = new CodeTypeDeclaration(resKey);
//Class is automatically internal, only nested classes can be private
cResource.TypeAttributes = System.Reflection.TypeAttributes.NotPublic;
//Resource class comments
cResource.Comments.Add(new
CodeCommentStatement("<SUMMARY>", true));
cResource.Comments.Add(new
CodeCommentStatement("Access to resource identifier "
+ resKey, true));
cResource.Comments.Add(new
CodeCommentStatement("</SUMMARY>", true));
nSpace.Types.Add(cResource);
mGetString = new CodeMemberMethod();
mGetString.Name = "GetString";
mGetString.Attributes =
(MemberAttributes)((int)MemberAttributes.Static
+ (int)MemberAttributes.Public);
mGetString.ReturnType = new CodeTypeReference(typeof(string));
//It is necessary to know how many replaceable
//parameters the string has
string ParameterMatchExpression = @"(\{[^\}\{]+\})";
MatchCollection mc = Regex.Matches(resEnumerator.Value.ToString(),
ParameterMatchExpression);
//Don't include duplicates in count
// as String.Format argument can be specified
//more than once, ie: "First: {0},
// Second: {1}, First again: {0}"
StringCollection parameters = new StringCollection();
foreach(Match match in mc)
{
if(!parameters.Contains(match.Value))
{
parameters.Add(match.Value);
}
}
CodeExpression[] getStringParams;
if (parameters.Count>0)
{
CodeExpression[] args = new CodeExpression[parameters.Count];
//Create the argument lists
for(int i = 0; i < parameters.Count; i++)
{
mGetString.Parameters.Add(
new CodeParameterDeclarationExpression(typeof(object),
"arg" + i.ToString()));
args[i] = new
CodeArgumentReferenceExpression("arg" + i.ToString());
}
getStringParams = new CodeExpression[2];
getStringParams[1] = new
CodeArrayCreateExpression(typeof(object), args);
}
else
{
getStringParams = new CodeExpression[1];
}
//The first parameter(key) is allways the same regardless of
//whether additional args exist or not
getStringParams[0] = new CodePrimitiveExpression(resKey);
//Resource class statements
CodeMethodInvokeExpression invokeGetString =
new CodeMethodInvokeExpression
(
new CodeTypeReferenceExpression("ResourceFormatter"),
"GetString",
getStringParams
);
mGetString.Statements.Add(new
CodeMethodReturnStatement(invokeGetString));
cResource.Members.Add(mGetString);
}
}
就是这样,在 BuildGraph
方法的末尾,一个填充好的 CodeCompileUnit
会被返回到 GenerateCode
方法。
安装和用法
以下是 ResourceClassGenerator
自定义工具安装所需的步骤
- 由于我不能且无意在可下载的源代码中分发
BaseCodeGeneratorWithSite
,请从上面提到的位置(GotDotNet 用户示例:BaseCodeGeneratorWithSite)下载其源代码,将其包含在ResourceClassGenerator
项目的“引用”列表中,并进行构建。重要提示: 请务必根据您打算使用
ResourceClassGenerator
的 VS.NET 版本(2002/2003)包含正确版本的 BaseCodeGeneratorWithSite.dll,否则该工具可能无法正常工作。 - 使用 regasm 将已构建的
ResourceClassGenerator
版本注册到 COM。 - 设置适当的注册表设置,以便 VS.NET 能够识别它。这些设置的位置类似于 HKLM\Software\Microsoft\VisualStudio\7.x\Generators\packageguid。C# 环境 ({FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}) 与 VB.NET 环境 ({164B10B9-B200-11D0-8C61-00A0C91E29D5}) 的 packageguid 不同。应在这些位置添加一个新密钥以及适当的值。例如,对于 VS.NET 2003 C# 环境,这些注册表设置将如下所示
- 键
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\7.1\Generators\{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}\ResourceClassGenerator]
- 默认值
@="C# 代码生成器用于字符串资源类"
Guid
属性的值"CLSID"="{8442788C-A1A7-4475-A0C6-C2B83BD4221F}"
- 最后
"GeneratesDesignTimeSource"=dword:00000001
- 键
您无需手动执行最后两个步骤,可下载项目已包含一个 Setup.bat 文件。它会自动检测当前的 VS.NET 版本,将 ResourceClassGenerator
注册到 COM,并为 C# 和 VB.NET 设置适当的注册表设置。还有一个 Uninstall.bat 文件,用于撤消所有这些步骤。这两个文件都位于项目输出路径(当前设置为 redist 文件夹)中,并且始终需要位于已构建的 .dll 所在的文件夹中。如果您同时安装了 VS.NET 2002 和 VS.NET 2003,则该工具将始终为 2003 版本安装,并且您需要对这些 .bat 文件进行少量修改,才能在此特定情况下为 2002 版本安装/卸载它。
安装后,可以通过在所需 .resx 文件的“属性”窗口中设置 CustomTool
属性来轻松使用该工具。其值应设置为 ResourceClassGenerator。通过右键单击 .resx 文件并选择“运行自定义工具”选项来启动它。