Introduction
Localizing a large application can be a painful task. When facing hundreds or thousands of localized strings, it's easy to make typing mistakes with ResourceManager.GetString
method. Another set of errors can be caused by inconsistencies between different language versions of a resource file (for example, several strings missing in the German file compared to the English one). The worst and really hard-to-trace bugs can be caused when using resource strings with the String.Format
method and having different number of arguments ({0}, {1}, ...
) for the same resource in different files. All these problems are eliminated with the ResourceClassGenerator
custom tool.
First, I want to emphasize that the basic concept for this is not mine. You can find the origin here:
I find the author's idea a truly brilliant one and have been using his original custom tool for quite some time. The basic principle is - when you run the custom tool, a class file is generated from the .resx file where each string is represented by a class. Each class has a static GetString
method which passes the call to the base ResourceFormatter
class which then handles the actual ResourceManager.GetString
call. Thus, for example, if you have given the namespace Resources.Strings
to your generated file, you would access the Label1
resource in your code with Resources.Strings.Label1.GetString()
expression. Something similar has already been introduced in Whidbey, you can find more info here:
This ensures IntelliSense and thus eliminates typing mistakes. If String.Format
arguments are used in a particular resource, a GetString
method signature is adjusted properly, so it must be called with the correct number of arguments.
Modifications
All these features are implemented in the original tool, but the more I used it, I found it missing a certain functionality and I was tempted to improve it. These are the main modifications:
- The tool isn't working properly if generated classes are given the namespace different from the default one, so it was necessary to produce an automated way of finding the resource file
BaseName
necessary for ResourceManager
instantiation. This is done via EnvDTE
(an object model for automation and extensibility in VS.NET). There is a lot of info about EnvDTE
in MSDN. If you are unfamiliar with it, try the following article as a starting point:
- The second highly required feature not implemented in the original version is the comparison between the different language versions of one resource set. The generated file is created under the base language resource file, but it is meant to work with the other language versions as well. For example, the same number of string resources and the same number of arguments for each resource are expected in the Strings.de.resx compared to the Strings.en.resx. I implemented this by throwing an exception if comparison fails when running the tool, which causes an error message box to appear in VS.NET environment (the exception is also visible as a build error in the task list window). If an exception occurs, a resource class file is not generated. The reasonable assumption in this case is that all the files belonging to one resource set are placed in the same folder.
- The original tool works only in the C# environment. Of course, a VB.NET version could have been written following the same logic, but the true solution screamed towards the CodeDOM. Thus, I rewrote the whole tool from scratch using the CodeDOM code generation principles. Making the tool language independent required a means of detecting the type of current working environment (C#, VB.NET, ...), this is also done via
EnvDTE
.
Custom Tool Basics
A good definition and explanation of VS.NET custom tools can be found at: Top Ten Cool Features of Visual Studio .NET Help You Go From Geek to Guru.
A quote from there:
"A custom tool is a component that is run every time the input file changes and is saved. The custom tool is handed the name of the input file as well as bytes in the file via the Generate
method on the IVsSingleFileGenerator
COM interface. This method is responsible for producing bytes that will be dumped into C# or Visual Basic .NET, depending on what kind of a project it is."
Following the principles described in this article, here is a short description of the basic steps necessary for the custom tool creation:
After the tool is built, certain operations have to be performed before it can be used, this is described in the Installation & Usage section at the bottom of the article. If you just want to use the tool and you are not interested in the code, and the problems and tricks when working with EnvDTE
and CodeDOM, then go to this section and skip the following sections with the detailed code explanation.
Delving Into Code
The starting point of the custom tool is the overridden GenerateCode
method:
[Guid("8442788C-A1A7-4475-A0C6-C2B83BD4221F")]
public class ResourceClassGenerator : BaseCodeGeneratorWithSite
{
protected override byte[] GenerateCode(string fileName,
string fileContents)
{
...
For the ResourceManager
instantiation on the desired resource file, it's necessary to provide the baseName
parameter in the constructor. The baseName
, according to MSDN, represents the root name of the resources. It is actually equal to something like this:
[Default namespace of the resource file].[Base filename(without extension) of the resource file]
Default namespace of the resource file is equal to the default namespace of its parent folder. The problem is that in VB projects, the namespace for all folders is the same as the project's DefaultNamespace. In C# projects, it depends on the folder location in the project hierarchy. This is where EnvDTE
proves to be extremely helpful. In EnvDTE
object model, every file and folder is represented with the EnvDTE.ProjectItem
class. It contains a collection of EnvDTE.Property
classes, which is different for different file types (.cs, .resx, ...). It contains all the properties visible in the file's property grid, but there are other properties as well. One of these is the DefaultNamespace
property, but it's only available in the folder's Properties
collection. Thus, it's necessary to find the resource file's parent folder ProjectItem
in order to construct the BaseName
.
Getting the current EnvDTE
object model from the custom tool is rather easy. The ProjectItem
object, representing the file the custom tool is being run on, can be fetched like this:
ProjectItem projItem = (ProjectItem)GetService(typeof(ProjectItem));
Here's the rest of the code which produces resource file BaseName
:
Project proj = projItem.ContainingProject;
string baseName;
if(Path.GetDirectoryName(proj.FileName)==Path.GetDirectoryName(fileName))
{
baseName = (string)proj.Properties.Item("DefaultNamespace").Value;
}
else
{
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));
The Helper
class is necessary for getting the base filename with eventual additional dots removed. This is necessary if, for example, one doesn't want to use a default resource file. For instance, this may be necessary if the desired behavior is to throw an error in case of a missing resource in the Strings.de.resx file, instead of displaying the contents from the default Strings.resx. In that case, one wouldn't have a default (presumably English) Strings.resx, but would be using the Strings.en.resx instead. Hence the additional dots possibility.
internal class Helper
{
public static string GetBaseFileName(string fileName)
{
if (fileName.IndexOf(".")==-1)
{
return fileName;
}
else
{
return fileName.Substring(0, fileName.IndexOf("."));
}
}
}
The overridden GenerateCode
method provides the fileName
and fileContents
arguments. They represent the name and the content of the base language file, the one that the custom tool is being run on. For comparison with other files belonging to the same resource set, it's necessary to fetch their content too, iterate through each resource, and perform necessary validation. The matching number of arguments in each string is checked via regular expressions.
string ParameterMatchExpression = @"(\{[^\}\{]+\})";
string path = Path.GetDirectoryName(fileName);
DirectoryInfo folder = new DirectoryInfo(path);
ResXResourceReader reader = new ResXResourceReader(fileName);
IDictionaryEnumerator enumerator = reader.GetEnumerator();
SortedList baseList = new SortedList();
while (enumerator.MoveNext())
{
MatchCollection mc =
Regex.Matches(enumerator.Value.ToString(),
ParameterMatchExpression);
baseList.Add(enumerator.Key, mc.Count);
}
reader.Close();
foreach(FileInfo file in folder.GetFiles("*.resx"))
{
if ((file.FullName!=fileName) &&
(Helper.GetBaseFileName(file.Name) ==
Helper.GetBaseFileName(Path.GetFileNameWithoutExtension(fileName))))
{
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
{
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)
{
throw (ex);
}
}
}
After the BaseName
is found and validation performed, everything is prepared for code generation, except for the choice of language the code will be generated in. Again, EnvDTE
makes this an easy task.
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;
}
The selected provider is used in the CodeDOM code generation process. All code related to this process is packed into a separate class called CodeDomResourceClassGenerator
. The rest of the code in the overridden GenerateCode
method passes all the necessary parameters to the static CodeDomResourceClassGenerator.GenerateCode
method and gets the generated code from it. It returns the byte array which will be used in the construction of the class file which will reside under the .resx file in the project hierarchy.
string classNameSpace = FileNameSpace != String.Empty ?
FileNameSpace : Assembly.GetExecutingAssembly().GetName().Name;
return CodeDomResourceClassGenerator.GenerateCode(
provider,
fileName,
fileContents,
baseName,
classNameSpace
);
The CodeDomResourceClassGenerator.GenerateCode
method builds the CodeDOM graph, a sort of a language independent blueprint which tells the selected CodeDomProvider
how to generate code. After the code string is generated, it is parsed into the required byte array:
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);
}
}
...
The essence of all the code generation is contained in the BuildGraph
method, which will be explained more closely in the following section.
Building the CodeDOM Graph
Before stepping into the CodeDOM modeling, one must know how the generated file is supposed to look like. Here's the C# example which shows the desired design of the resource tool generated file:
using System;
using System.Resources;
using System.Reflection;
namespace CSharpTest.Resources {
class ResourceFormatter
{
private static System.Resources.ResourceManager _ResourceManager;
private static System.Resources.ResourceManager ResourceManager {
get
{
if ((_ResourceManager == null))
{
_ResourceManager = new System.Resources.ResourceManager(
,
System.Reflection.Assembly.GetExecutingAssembly());
}
return _ResourceManager;
}
}
public static string GetString(string resourceId)
{
return ResourceFormatter.ResourceManager.GetString(resourceId);
}
public static string GetString(string resourceId, object[] args)
{
string format =
ResourceFormatter.ResourceManager.GetString(resourceId);
return string.Format(format, args);
}
}
class lbl1
{
public static string GetString()
{
return ResourceFormatter.GetString("lbl1");
}
}
class lbl2
{
public static string GetString(object arg0, object arg1)
{
return ResourceFormatter.GetString("lbl2",
new object[] {arg0, arg1});
}
}
...
All these code statements need to be translated into appropriate CodeDOM expressions. The top-level object in the code tree is the CodeCompileUnit
.
private static CodeCompileUnit BuildGraph(string fileName,
string fileContents,
string baseName,
string classNameSpace)
{
CodeCompileUnit compileUnit = new CodeCompileUnit();
...
CodeCompileUnit
contains a set of CodeNamespace
objects, which represent namespaces in the generated code. Most of the programming elements have their counterparts in the CodeDOM
object model, classes can be added to namespaces via CodeTypeDeclaration
objects, fields can be added to classes via CodeMemberField
objects, etc. The catch is that CodeDOM can create code valid in all languages only if working with elements found in all languages. Thus, for example, VB.NET With
statement is not supported, as well as the preprocessor directives (including the widely popular region
directive). This is possible to achieve via certain CodeDom classes called snippets (like CodeSnippetStatement
, CodeSnippetExpression
, ...), which, according to MSDN, are interpreted as a literal code fragment that will be included directly in the source without modification. However, this may break the language neutrality, and the CodeDOM may not generate a valid code for some languages.
Considering all this, I decided not to use snippets and keep the language independency. That's why the generated code design doesn't contain region
directives. They may have been useful to separate the base ResourceFormatter
class from the numerous resource classes, but it's not that important, since the whole resource tool idea is meant to work on the "set it and forget it" principle, without the need to browse or change the generated code. It just needs to be regenerated when the resources are changed.
Still, even when following the strict language compatibility rules, there are some tricky issues that needed to be resolved. By default, VB.NET CodeDOM provider outputs Option Strict Off
(controls whether implicit type conversions are allowed) and Option Explicit On
(controls whether variable declarations are required). Every self-respecting VB programmer would always set both of these options to On
. I've found the solution to this problem in an excellent book - Code Generation in Microsoft .NET by Kathleen Dollard. Here it is:
compileUnit.UserData.Add("AllowLateBound", false);
compileUnit.UserData.Add("RequireVariableDeclaration", true);
Indeed, I checked it with Reflector, this is hard coded in the Microsoft.VisualBasic.VBCodeGenerator.GenerateCompileUnitStart
method (in System.dll) and there is nothing similar in the Microsoft.CSharp.CSharpCodeGenerator.GenerateCompileUnitStart
. An answer to another important issue can be found in these methods as well. Every file generated with CodeDOM has a comment like this at its top:
Since I wanted to add more code generation comments (like source .resx file location and time of generation), I thought it would be appropriate to add it here, but it's impossible since this is also hard coded in the GenerateCompileUnitStart
method of both the VBCodeGenerator
and CSharpCodeGenerator
. Hence, I used a different <autogeneratedinfo>
comment tag.
Another great advice from the previously mentioned book concerns the VB.NET Imports
and C# using
statements. They are added via the CodeNamespace.Imports
property, which represents the CodeNamespaceImportCollection
. The VB provider always places the Imports
statements at the top of the file, outside the namespace, and in VB.NET Imports
applies to the whole file. In C#, the using
statements can be placed separately under each namespace and the C# provider will put them under the namespace declaration. If one wants identical results, a VB.NET approach should be used and the C# using
statements should be placed outside the namespace. The great trick in the book consists of adding a CodeNamespace
with an empty string as the namespace name, which doesn't output the namespace declaration:
CodeNamespace dummyNamespace = new CodeNamespace("");
compileUnit.Namespaces.Add(dummyNamespace);
dummyNamespace.Imports.Add(new CodeNamespaceImport("System"));
dummyNamespace.Imports.Add(new CodeNamespaceImport("System.Resources"));
dummyNamespace.Imports.Add(new CodeNamespaceImport("System.Reflection"));
Now all the prerequisites have been made and all that's left is the pure CodeDOM modeling. A more complicated programming statement may require a dozen of CodeDOM statements. Therefore, the following code is commented in a way that shows how every statement from the pre-designed resource tool class file shown above is represented with the appropriate code block of CodeDOM statements.
CodeNamespace nSpace = new CodeNamespace(classNameSpace);
compileUnit.Namespaces.Add(nSpace);
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(
"-----------------------------------" +
"------------------------------------------"));
CodeTypeDeclaration cResourceFormatter =
new CodeTypeDeclaration("ResourceFormatter");
cResourceFormatter.TypeAttributes =
System.Reflection.TypeAttributes.NotPublic;
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);
CodeMemberField fResourceManager =
new CodeMemberField(typeof(System.Resources.ResourceManager),
"_ResourceManager");
fResourceManager.Attributes =
(MemberAttributes)((int)MemberAttributes.Static +
(int)MemberAttributes.Private);
cResourceFormatter.Members.Add(fResourceManager);
CodeMemberProperty pResourceManager = new CodeMemberProperty();
pResourceManager.Name = "ResourceManager";
pResourceManager.Type = new
CodeTypeReference(typeof(System.Resources.ResourceManager));
pResourceManager.Attributes =
(MemberAttributes)((int)MemberAttributes.Static
+ (int)MemberAttributes.Private);
pResourceManager.HasSet = false;
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");
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[] {}
)
}
)
);
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);
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"));
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));
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);
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);
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));
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);
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();
while(resEnumerator.MoveNext())
{
string resKey = (string)resEnumerator.Key;
CodeTypeDeclaration cResource = new CodeTypeDeclaration(resKey);
cResource.TypeAttributes = System.Reflection.TypeAttributes.NotPublic;
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));
string ParameterMatchExpression = @"(\{[^\}\{]+\})";
MatchCollection mc = Regex.Matches(resEnumerator.Value.ToString(),
ParameterMatchExpression);
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];
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];
}
getStringParams[0] = new CodePrimitiveExpression(resKey);
CodeMethodInvokeExpression invokeGetString =
new CodeMethodInvokeExpression
(
new CodeTypeReferenceExpression("ResourceFormatter"),
"GetString",
getStringParams
);
mGetString.Statements.Add(new
CodeMethodReturnStatement(invokeGetString));
cResource.Members.Add(mGetString);
}
}
That's it, at the end of the BuildGraph
method, a populated CodeCompileUnit
is returned to the GenerateCode
method.
Installation & Usage
These are the steps necessary for the ResourceClassGenerator
custom tool installation:
- Since I'm not allowed and do not intend to distribute the
BaseCodeGeneratorWithSite
along with my downloadable source, please download its source from the location mentioned above (GotDotNet User Sample: BaseCodeGeneratorWithSite), include it in the References list of the ResourceClassGenerator
project, and build it.
Important: Be careful to include the right version of the BaseCodeGeneratorWithSite.dll depending on the VS.NET version (2002/2003) you intend to use ResourceClassGenerator
with, otherwise the tool may not work.
- Register the built version of the
ResourceClassGenerator
with COM via regasm.
- Set the appropriate registry settings for VS.NET to become aware of it. The location of these settings looks like HKLM\Software\Microsoft\VisualStudio\7.x\Generators\packageguid. The packageguid is different for C# environment ({FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}) compared to the VB.NET environment ({164B10B9-B200-11D0-8C61-00A0C91E29D5}). A new key should be added to these locations along with the appropriate values. For example, for VS.NET 2003 C# environment, these registry settings would look like:
- key:
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\7.1\Generators\{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}\ResourceClassGenerator]
- default value:
@="C# Code Generator for String Resource Classes"
- The value of the
Guid
attribute:
"CLSID"="{8442788C-A1A7-4475-A0C6-C2B83BD4221F}"
- and finally:
"GeneratesDesignTimeSource"=dword:00000001
You don't need to do the last two steps manually, a Setup.bat file is included in the downloadable project. It automatically detects the current VS.NET version, registers the ResourceClassGenerator
with COM, and sets the appropriate registry settings for both C# and VB.NET. There is also the Uninstall.bat file which takes care of undoing all these steps. Both files are placed in the project's output path (currently set to redist folder) and always need to be in the folder where the built .dll resides. If you have both VS.NET 2002 and VS.NET 2003 installed, the tool will always be installed for 2003 version and you'll need to make small modifications to these .bat files if you want to install/remove it for the 2002 version in this particular situation.
After installation, the tool can be easily used by setting the CustomTool
property in the Properties window of the desired .resx file. Its value should be set to ResourceClassGenerator. It is started by right-clicking on the .resx file and clicking on the Run Custom Tool option.