Introduction
This article presents a very simple enhancement of the TransformCodeGenerator tool posted here on CodeProject by Chris Stefano. In order to get the tool to perform well with real-life problems that we face when generating commercial code, the tool has undergone a small redesign to become more usable. Newcomers to the use of XSLT generation should read the original article first, before reading this one.
Using the Code
In order to compile the custom tool, you will need to install the Visual Studio SDK on your machine. You also need the AltovaXML engine installed on your machine. Once you do, compile the project. Open the VS command prompt, go to the output directory and type "regasm TransformCodeGenerator.dll". That's it – Visual Studio is ready to use the plug-in.
I made the tool for Visual Studio 2005, but it can be compiled for other versions by changing the VisualStudioVersion
variable in the code.
The Problem
TransformCodeGenerator
is great for doing a transform from XML into C# via an XSLT stylesheet. However, when we started using the tool for a fairly large application, we ran into the following problems:
- The tool was using XSLT 1.0, since this is all that .NET supports. Apparently, the
System.XML
namespace will never support XSLT 2.0 since Microsoft is betting on XQuery
instead. At any rate, we needed the XSLT 2.0 functionality, particularly string conversion functions which are useful for variable naming. - The tool could apply only a single stylesheet to the XML file, and thus generated only a single C# file. In a typical usage scenario, we define a data structure which then produces UI, an ORM class and some business logic to boot. This means that from a file Person.xml we would generate Person.cs (the form code, often containing an instance of the ORM class and some routed event handlers), Person.Designer.cs (the form with
InitializeComponent()
), Person.Entity.cs (the ORM stuff, typically just a class that lists all the members as well as some routine SQL commands & helper functions) and Person.Logic.cs for stuff like data binding & validation. - Sometimes we needed to do a transform on several XML files. For example, we might have an XML file with Help resources which then need to be applied as tooltips for the generated UI elements. Currently, this is impossible.
In addition to these features, I've also added an event logging function, which should make the custom tool a bit easier to debug.
The Solution
The most direct solution to the problem described above was to write a Visual Studio plug-in in order to handle the specialized generation. However, the simplicity of a custom tool is evident, so I stuck with that. Here are the solutions to the problems outlined above.
XSLT 2.0
Seeing how Microsoft does not support XSLT 2.0, I looked at other free XSLT transformation engines which could be used instead. I tried Saxon.NET first but, unfortunately, it didn't work on my system at all, which is none too surprising considering it runs in a .NET-based Java VM and throws Java exceptions. Subsequently, I located another engine, made by a company called Altova, that had what I wanted. The AltovaXML engine is free, and supports .NET by installing a suitable assembly in the GAC when you install the package. It has a very simple API which I used in order to do the final transform, which the code snippet below demonstrates:
IXSLT2 xslt = new ApplicationClass().XSLT2;
try
{
xslt.InputXMLFileName = tempPath;
xslt.XSLFileName = xslPath;
result += xslt.ExecuteAndGetResultAsString();
AddLogEntry(string.Format("TransformCodeGenerator.GetTransformedResult:" +
" transformation executed and yielded{0}{0}{1}",
Environment.NewLine, result));
}
One thing to note here is that, unlike in TransformCodeGenerator
, the bytes that are the result of the transformation are returned in an encoding of choice. ASCII doesn't work very well if you write applications in Russian.
Multiple Output
In order to do multiple transformations, I decided to extend the TransformCodeGenerator
syntax without breaking existing applications. So, at the root element of the XML file we still need the name of the primary transformation stylesheet. A result of this transformation is a C# file that has the same name as the XML file (minus the extension, of course). Additional transformation stylesheets are also defined at the root element as transformation2
, transformation3
and so on.
The naming scheme for the generated files is simple. For the primary transform, Person.xml becomes Person.cs. For all other transforms, the names of the XML and XSL files are combined, so that transforming Person.xml via Entity.xslt yields Person.Entity.cs.
Well, this is the syntax of the transformation, but in order to actually code it, I used the VsMultipleFileGenerator by Adam Langley. I have adapted the API to our problem of code generation, but have kept the original generator intact, save for the COM registration/unregistration functions. Let's look briefly at VsMultipleFileGenerator
and how our program handles the transform.
First of all, our custom tool needs to provide an enumeration that will later serve as criteria for the transformation. In our case, we provide a list of filenames for subsequent processing. System.Xml
API is used to extract the values from the XML file:
public override IEnumerator<string> GetEnumerator()
{
XmlDocument doc = new XmlDocument();
doc.Load(InputFilePath);
XmlNode node;
for (int i = 2; i < 100; ++i)
{
node = doc.DocumentElement.Attributes["transformer" + i];
if (node != null)
{
AddLogEntry(string.Format
("TransformCodeGenerator.GetEnumerator() yielded {0}",
node.Value));
yield return node.Value;
}
else break;
}
}
Now, we need to provide a function which determines the filename. Using the convention I described above, the following implementation should make sense:
protected override string GetFileName(string element)
{
return string.Format("{0}.{1}.cs",
Path.GetFileNameWithoutExtension(InputFilePath),
Path.GetFileNameWithoutExtension(element));
}
Code generation itself, which must appear in the GenerateContent
function is delegated to another function entirely. The reason for this is that this function handles the transformation using all stylesheets except the primary one.
public override byte[] GenerateContent(string element)
{
return GetTransformResult(element);
}
Now we're left with our primary transformation, something that VsMultipleFileGenerator
calls Summary Content. Since we're not using it for summaries, but rather C#, we do the same thing as we do with the extra stylesheets:
public override byte[] GenerateSummaryContent()
{
XmlDocument doc = new XmlDocument();
doc.Load(InputFilePath);
XmlNode node = doc.DocumentElement.Attributes["transformer"];
if (node != null)
return GetTransformResult(node.Value);
else
return encoding.GetBytes(string.Format(
"#error {0} is missing the 'transformer' attribute at root level.",
InputFilePath));
}
One interesting thing to note here is the way that error handling is implemented. Since the resulting files are of a C# nature, a missing transformer
attribute will yield a generated result that starts with #error
, which means the C# compiler will present it better than some cleverly formatted multiline text message.
The VsMultipleFileGenerator
also has a function that wants to know the default extension for our generated code.
public override string GetDefaultExtension()
{
return defaultExtension;
}
In our case, the defaultExtension
variable contains a constant value of '.cs
'.
XML File Merging
By requiring the transformer
attributes in our XML files, we have already defined a convention for our source data. I have extended this convention by adding the notion that an <include>
element, when placed anywhere within the source file, will for the purposes of transformation, contain the contents of the actual file. Here is an example:
Let A.xml contain:
<strings>
<string>Text</string>
</strings>
and B.xml contain:
<root>
<include file="A.xml"/>
</root>
Then, when the transformation runs on B.xml, it will use a file that looks like this:
<root>
<strings>
<string>Text</string>
</strings>
</root>
You're probably wondering how the actual substitution takes place. The procedure is very simple - all we do is find all the <include file="..."/>
strings and replace them by the contents of the files they refer to. You probably want to see the code, so here it is:
private byte[] GetTransformResult(string xslFileName)
{
AddLogEntry(string.Format("TransformCodeGenerator.GetTransformResult({0})",
xslFileName));
string path = InputFilePath.Substring(0, InputFilePath.LastIndexOf('\\') + 1);
string result = string.Empty, xslPath = path + xslFileName,
ifc = InputFileContents;
string tempPath = xslPath + ".temp";
int start;
while ((start = ifc.IndexOf("<include")) != -1)
{
int end = ifc.IndexOf(">", start);
string entry = ifc.Substring(start, end - start + 1);
string[] parts = entry.Split("\"".ToCharArray());
ifc = ifc.Replace(entry, File.ReadAllText(path + parts[1]));
}
File.WriteAllText(tempPath, ifc);
AddLogEntry(string.Format("TransformedCodeGenerator.GetTransformedResult: " +
"Temporary file {0} created and contains{1}{1}{2}",
tempPath, Environment.NewLine, ifc));
IXSLT2 xslt = new ApplicationClass().XSLT2;
try
{
xslt.InputXMLFileName = tempPath;
xslt.XSLFileName = xslPath;
result += xslt.ExecuteAndGetResultAsString();
AddLogEntry(string.Format("TransformCodeGenerator.GetTransformedResult:" +
" transformation executed and yielded{0}{0}{1}",
Environment.NewLine, result));
}
catch (Exception x)
{
result += string.Format(
"Exception while calling GenerateSummaryContent: {0}\r\n\r\n" +
"Transformer is:\r\n\r\n{1}\r\n\r\nXML is:\r\n\r\n{2}",
x, xslPath, InputFilePath);
} finally
{
File.Delete(tempPath);
}
return encoding.GetBytes(result);
}
The reason why we do a manual search-replace and create a (completely needless) temporary file is due to bugs inherent in Regex
and the Altova engine. If you can get it to work with these features – great!
History
- 14 November 2007 — Initial Release