Expand and Debug a Custom BuildProvider

Yesterday I described how to create a Custom BuildProvider, that automatically creates a string class with static string members from an xml file, containing localized text for a webpage.

I wanted to generalize the class, so that i could use it for different purposes, for instance an "Urls" class to contain all site urls defined also in an xml file.

I decided to define what to generate in the builder.stringsbuildprovider file, that also triggers the codegeneration like so:

<?xml version="1.0" encoding="utf-8" ?> <!-- Builds static key/value "strings" classes from the defined values filename: which XML file contains the data classname: define the name of the class you want namespace: in which namespace should the class be created nodename: what is the single node name that defines the data attributename: which name from xml attribute do you want as the lookupvalue valuename: which attribute from xml contains the value --> <classes> <class filename="~/language/da-DK/Resource.xml" classname="Strings" namespace="Amino.Børs" nodename="Resource" attributename="name" valuename="name"/> <class filename="~/SiteUrls.xml" classname="Urls" namespace="Amino.Børs" nodename="SiteUrls/location" attributename="name" valuename="path"/> </classes>

Using the following class:

/// <summary> /// This class builds a class with static /// strings with specifications provided /// in the builder.stringsbuildprovider file /// </summary> class StringsBuildProvider : BuildProvider { public override void GenerateCode(AssemblyBuilder assemblyBuilder) { CodeCompileUnit generated = GenerateUnit(base.VirtualPath); assemblyBuilder.AddCodeCompileUnit(this, generated); base.GenerateCode(assemblyBuilder); } private CodeCompileUnit GenerateUnit(string virtualPath) { CodeCompileUnit unit = new CodeCompileUnit(); // Load the builderfile XmlDocument d = new XmlDocument(); using (Stream s = VirtualPathProvider.OpenFile(virtualPath)) { d.Load(s); } /// We iterate the builder file and process all class entries /// turning them into classes in the wanted namespace /// with the content of the resource file as static strings foreach (XmlNode builder in d.SelectSingleNode("classes")) { CodeNamespace nspace = new CodeNamespace(builder.Attributes ["namespace"].Value); unit.Namespaces.Add(nspace); // prepare the class CodeTypeDeclaration cls = new CodeTypeDeclaration(builder.Attributes ["classname"].Value); // Load the file XmlDocument map = new XmlDocument(); using (Stream s = File.Open(ABContext.Current.MapPath(builder.Attributes ["filename"].Value), FileMode.Open)) { d.Load(s); } /// Run through each item in the file, and create the static string field /// and add it to the codetypedeclaration foreach (XmlNode n in d.SelectSingleNode(builder.Attributes["nodename"].Value)) { if (n.NodeType != XmlNodeType.Comment) { cls.Members.Add(CreateField(n.Attributes[builder.Attributes ["attributename"].Value].Value, n.Attributes[builder.Attributes["valuename"].Value].Value)); } } /// add the codetypdeclaration to the namespace, which was added to the unit nspace.Types.Add(cls); } // we return the unit / generated code return unit; } private CodeMemberField CreateField(string fieldName, string valuename) { // Create a string with the given name CodeMemberField field = new CodeMemberField(typeof(string), string.Format("{0}", fieldName)); // Make it public and static field.Attributes = MemberAttributes.Public | MemberAttributes.Static; // Set the value field.InitExpression = new CodeSnippetExpression(string.Format("\"{0}\"", valuename)); return field; } }

In the process I ran into some problems, where the classes were not available to me. My output window during compilation hinted som builderrors, so I set a breakpoint at the GenerateCode method, but it didn't hit it, which makes sense.

But the question now was, how do i debug a buildprovider? Well the only answer I could think of, was to launch another instance of Visual Studio with the same solution and setting the breakpoint there and then attach the debugger to the original instance of Visual Studio that is going to build the project - and then build.

And it works, although not very practical. Watch out that you don't overwrite your changes from the second Visual Studio when editing in the first.

Posted October 27, 2007 by Joachim Lykke Andersen
In

Comments [0]   

Generate code with the BuildProvider

Now and then you find yourself between a rock and a hard place in the attempt to make your code easy to understand, and easily maintainable at the same time.

I am currently working on a fairly comprehensive refactoring of a site, or more accurately, a series of similar sites. Amongst other things I have implemented the possibility of localizing these sites.

The localized text is placed in Resource.xml files, that are handled by a custom ResourceManager, and looks like this:

<?xml version="1.0" encoding="utf-8" ?> <Resource> <!-- General terms --> <item name="Wanted">Købes</item> <item name="For_Sale">Sælges</item> <item name="Merge">Fusion</item> </Resource>

On the basepage i have a GetString method that uses the ResourceManager to get the localized text. I works nicely. No locked files, no sattelite assemblies and very transparent.

But one thing bugged me a little. All the strings were kind of magical, and hidden in the xml files - I like intellisense on everything for speed, accuracy and transparency. But on the other hand, I don't want to manually add all texts to a Strings class as well.

So i wrote a little simple code-generation. It is an inherited BuildProvider, which in a rather simple way reads your language file, and creates a Strings class with all the string references.

Heres the code. Its a first draft, so it might need a little work.

public class StringsBuildProvider : BuildProvider { public override void GenerateCode(AssemblyBuilder assemblyBuilder) { string fileName = base.VirtualPath; CodeCompileUnit generated = GenerateUnit(fileName); assemblyBuilder.AddCodeCompileUnit(this, generated); } private CodeCompileUnit GenerateUnit(string fileName) { CodeCompileUnit unit = new CodeCompileUnit(); CodeNamespace nspace = new CodeNamespace("Amino.Børs"); unit.Namespaces.Add(nspace); CodeTypeDeclaration classDeclaration = CreateClass(); nspace.Types.Add(classDeclaration); return unit; } private CodeTypeDeclaration CreateClass() { string file = ABContext.Current.MapPath("~/language/da-DK/resource.xml"); CodeTypeDeclaration cls = new CodeTypeDeclaration("Strings"); XmlDocument d = new XmlDocument(); d.Load(file); foreach (XmlNode n in d.SelectSingleNode("Resource")) { if (n.NodeType != XmlNodeType.Comment) { cls.Members.Add(CreateField(n.Attributes["name"].Value)); } } return cls; } private CodeMemberField CreateField(string fieldName) { CodeMemberField field = new CodeMemberField(typeof(string), string.Format("{0}", fieldName)); field.Attributes = MemberAttributes.Public | MemberAttributes.Static; field.InitExpression = new CodeSnippetExpression(string.Format("\"{0}\"", fieldName)); return field; } }

Then you just add it to the buildProviders section in the web.config. As i have several xml files i decided to create a custom extension as singular match for the buildprovider, and simply added an empty file called builder.stringsbuildprovider.

<buildProviders> <add extension=".stringsbuildprovider type="Amino.Bors.CodeGeneration.StringsBuildProvider, Amino.Bors"/> </buildProviders>

So now i have all my resource reference strings at hand at all times without having to open the xml document.

Posted October 26, 2007 by Joachim Lykke Andersen
In

Comments [0]