Click here to Skip to main content
Click here to Skip to main content

VS.NET CodeDOM-Based Custom Tool for String Resource Management

, 22 Jun 2004
Rate this:
Please Sign up or sign in to vote.
A VS.NET custom tool, created with the help of CodeDOM and EnvDTE, used to facilitate management of resource strings via IntelliSense and error checking in VS.NET environment.

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:

  • Instead of implementing the IVsSingleFileGenerator interface, derive from a managed class Microsoft.VSDesigner.BaseCodeGeneratorWithSite. This class was public in Microsoft.VSDesigner.dll of VS.NET 2002 and made internal in the VS.NET 2003. Fortunately, MS published its source at GotDotNet User Sample: BaseCodeGeneratorWithSite.

    Both 1.0 and 1.1 versions can be found there. Build the desired version and reference the .dll into your custom tool project.

  • Add the Guid attribute to the derived class necessary for exposition to COM.
  • Override the GenerateCode method.

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 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));

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.

//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);
        }
    }
}

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.

//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
);

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;

// ------------------------------------------------------------------------
// <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});
    }
}

...

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:

//Just for VB.NET
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:

//------------------------------------------------------
// <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>
//------------------------------------------------------

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:

//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"));

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.

//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);
    }
}

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:

  1. 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.

  2. Register the built version of the ResourceClassGenerator with COM via regasm.
  3. 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Jasmin Muharemovic
Software Developer Mono Ltd
Croatia Croatia
No Biography provided

Comments and Discussions

 
QuestionRemoving the autogenerated comments PinmemberRoman197916-Nov-06 0:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.1411023.1 | Last Updated 23 Jun 2004
Article Copyright 2004 by Jasmin Muharemovic
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid