Generating Strongly-Typed Resources for Sources Other Than resx Files


Way back in Chapter 3, "An Introduction to Internationalization," I introduced strongly typed resources. You might remember that they are new in .NET Framework 2.0, but as they are such a good idea, I wrote a similar utility for the .NET Framework 1.1 so that everyone could share in the joy that is strongly typed resources. You might also recall that resgen, the console application that generates strongly typed resources, accepts input only from resx files. Having spent all of this time writing custom resource managers, you might wonder how we can generate strongly typed resources from a source other than a resx file. The answer is to build our own resgen utility. In the .NET Framework 2.0, this isn't as difficult as it sounds (I return to the .NET Framework 1.1 in a moment). The .NET Framework 2.0 class StronglyTypedResourceBuilder, upon which resgen is built (among other classes), has overloaded Create methods that accept different parameters to identify resources. One overloaded Create clearly accepts resx files as input, and this is of no value to us. However, another overloaded Create accepts an IDictionary of resources, and it is this method that solves our problem. Here is a bare-bones implementation of ResClassGen, a replacement for resgen that just generates strongly typed classes from resources.

 using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Collections; using System.Resources; using System.Resources.Tools; using System.CodeDom; using System.CodeDom.Compiler; using Microsoft.CSharp; namespace Tools.ResClassGen {     class Program     {         static void Main(string[] args)         {             if (args.GetLength(0) < 1 || !File.Exists(args[0]))                 ShowSyntax();             else             {                 string nameSpace;                 if (args.GetLength(0) > 1)                     nameSpace = args[1];                 else                     nameSpace = String.Empty;                 StronglyTypedResourceBuilderHelper.GenerateClass(                     args[0], nameSpace);             }         }         private static void ShowSyntax()         {             Console.WriteLine("Syntax:");             Console.WriteLine(                 "ResClassGen <ResxFilename> [<NameSpace>]");             Console.WriteLine("Example:");             Console.WriteLine(                 "ResClassGen Strings.resx WindowsApplication1");         }     } } 


This skeleton simply checks the parameters and calls the Strongly TypedResourceBuilderHelper.GenerateClass method to do the work:

 public static void GenerateClass(     string resxFilename, string nameSpace) {     string[] unmatchable;     Hashtable resources = GetResources(resxFilename);     string className =         Path.GetFileNameWithoutExtension(resxFilename);     CodeDomProvider codeDomProvider = new CSharpCodeProvider();     CodeCompileUnit codeCompileUnit =         StronglyTypedResourceBuilder.Create(         resources, className, nameSpace,         codeDomProvider, false, out unmatchable);     string classFilename = Path.ChangeExtension(resxFilename, ".cs");     using (TextWriter writer = new StreamWriter(classFilename))     {         codeDomProvider.GenerateCodeFromCompileUnit(             codeCompileUnit, writer, new CodeGeneratorOptions());     } } 


The GenerateClass method calls Getresources to get a Hashtable of resources. (I return to Getresources in a moment.) It passes the Hashtable, which supports the IDictionary interface, to StronglyTypedResourceBuilder. Create. The remaining parameters to this Create method simply identify the name of the class, its namespace, the CodeDom provider used to generate the class, whether the class is internal, and a parameter into which all of the unmatchable resources are placed. The return result from the Create method is a CodeCompileUnit that contains the complete CodeDom graph (a tree of code instructions) from which the code can be generated. The GenerateClass method creates a TextWriter to write out the code and calls CodeDomProvider.GenerateCodeFromCompileUnit to output the code to the TextWriter.

In the .NET Framework 1.1, the GenerateCodeFromCompileUnit method is not available directly from the CodeDomProvider. Instead, you can create an ICode-Provider using CodeDomProvider.CreateGenerator, and call the same method with the same parameters from the resulting ICodeProvider.

The only question remaining is how to load the resources:

 private static Hashtable GetResources(string resxFilename) {     ResXResourceReader reader = new ResXResourceReader(resxFilename);     Hashtable resources = new Hashtable();     try     {         IDictionaryEnumerator enumerator = reader.GetEnumerator();         while (enumerator.MoveNext())         {             resources.Add(                 enumerator.Key.ToString(), enumerator.Value);         }     }     finally     {         reader.Close();     }     return resources; } 


This implementation simply uses a ResXResourceReader to read the resources from the specified resx file. Consequently, this implementation offers no benefits beyond the resgen implementation. However, you can see that by changing this method to use, say, a DbResourceReader instead of a ResXResourceReader, the ResClassGen utility would be able to read from your own resources.

If you are using the .NET Framework 1.1, there is no StronglyTypedResource Builder class, but recall from Chapter 3 that I wrote an equivalent class so that no one had to miss out. So the previous code works equally well in both versions of the framework.

Generating Strongly-Typed Resources Which Use ResourceManagerProvider

There is one more issue to attend to. If you have an excellent memory, you might remember one of the lines in the code that gets generated for the strongly typed resource class:

 System.Resources.ResourceManager temp =     new System.Resources.ResourceManager(     "WindowsApplication1.strings", typeof(strings).Assembly); 


Clearly, this line isn't very helpful to those of us who write custom resource managers because the code uses the System.Resources.ResourceManager class. We want it to be this:

 System.Resources.ResourceManager temp =     Internationalization.Resources.ResourceManagerProvider.     GetResourceManager(     "WindowsApplication1.strings", typeof(strings).Assembly); 


The bad news is that the StronglyTypedResourceBuilder class has no facility for allowing us to specify what resource manager class to use or how to create a resource manager. If you modify the generated code, it will be overwritten the next time the resource is generated. However, all is not lost. The StronglyTypedResource Builder.Create method generates a CodeCompileUnit that, as we have seen, is a collection of all of the instructions from which the strongly typed resource class is generated. The solution lies in modifying the resulting CodeCompileUnit before it is passed to the CodeDomProvider.GenerateCodeFromCompileUnit method. To follow this code, you need a little familiarity with CodeDom. We start by adding a line immediately after the call to StronglyTypedResourceBuilder.Create:

 ChangeResourceManager(className, codeCompileUnit); 


This represents our point at which we start altering the CodeDom graph. ChangeResourceManager looks like this:

 private static void ChangeResourceManager(     string className, CodeCompileUnit codeCompileUnit) {     CodeNamespace codeNamespace = codeCompileUnit.Namespaces[0];     CodeTypeDeclaration codeTypeDeclaration = codeNamespace.Types[0];     CodeMemberProperty codeMemberProperty = GetCodeMemberProperty(         codeTypeDeclaration, "ResourceManager");     if (codeMemberProperty != null)         ChangeResourceManagerGetStatements(codeNamespace,             codeTypeDeclaration, codeMemberProperty); } 


We take an educated guess that the first namespace in the CodeDom graph contains the generated class and that the first type in the namespace is the resource class; these guesses are accurate, given the current state of StronglyTypedResource-Builder. We call GetCodeMemberProperty to get the ResourceManager property (GetCodeMemberProperty simply iterates through all of the members looking for a property called "ResourceManager"). If we get the property, we call ChangeResourceManagerGetStatements, which actually modifies the CodeDom statements for the ResourceManager's get method:

 private static void ChangeResourceManagerGetStatements(     CodeTypeDeclaration codeTypeDeclaration,     CodeMemberProperty codeMemberProperty) (     CodeTypeReference resourceManagerTypeReference =         new CodeTypeReference(typeof(ResourceManager));     CodeFieldReferenceExpression resMgrFieldReferenceExpression =         new CodeFieldReferenceExpression(null, "resourceMan");     CodeExpression ifExpression =         new CodeBinaryOperatorExpression(         resMgrFieldReferenceExpression,         CodeBinaryOperatorType.IdentityEquality,         new CodePrimitiveExpression(null));     CodePropertyReferenceExpression typeOfExpression =         new CodePropertyReferenceExpression(         new CodeTypeOfExpression(         new CodeTypeReference(codeTypeDeclaration.Name)),         "Assembly");     CodeExpression[] resourceManagerParameterExpressions =         new CodeExpression[2]         {             new CodePrimitiveExpression(             codeNamespace.Name + "." + codeTypeDeclaration.Name),             typeOfExpression         };     CodeExpression newResourceManagerExpression =         new CodeMethodInvokeExpression(         new CodeTypeReferenceExpression(         "Internationalization.Resources.ResourceManagerProvider"),         "GetResourceManager",         resourceManagerParameterExpressions);     CodeStatement[] ifStatements = new CodeStatement[2]     {         new CodeVariableDeclarationStatement(         resourceManagerTypeReference, "temp",         newResourceManagerExpression),         new CodeAssignStatement(resMgrFieldReferenceExpression,         new CodeVariableReferenceExpression("temp"))     };     CodeStatementCollection statements =         new CodeStatementCollection();     statements.Add(         new CodeConditionStatement(ifExpression, ifStatements));     statements.Add(new CodeMethodReturnStatement(         resMgrFieldReferenceExpression));     codeMemberProperty.GetStatements.Clear();     codeMemberProperty.GetStatements.AddRange(statements); } 


This code doesn't worry about what the existing CodeDom instructions are for the get method; it simply throws them away and replaces them with a new set. The new set is very similar to the previous set, with the exception that the resource manager is created from ResourceManagerProvider.GetResourceManager instead of System.Resources.ResourceManager.




.NET Internationalization(c) The Developer's Guide to Building Global Windows and Web Applications
.NET Internationalization: The Developers Guide to Building Global Windows and Web Applications
ISBN: 0321341384
EAN: 2147483647
Year: 2006
Pages: 213

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net