![]() |
Platforms, Frameworks & Libraries »
.NET Framework »
Utilities
Intermediate
License: The Code Project Open License (CPOL)
Multi-file XSL Transformation Custom Tool for Visual StudioBy Dmitri NesterukAn XSL Transformation Custom Tool for Visual Studio |
C#, XSLT, Windows, .NET, Visual-Studio, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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.
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.
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:
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.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.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 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.
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.
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'.
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));
// get the path
string path = InputFilePath.Substring(0, InputFilePath.LastIndexOf('\\') + 1);
string result = string.Empty, xslPath = path + xslFileName,
ifc = InputFileContents;
string tempPath = xslPath + ".temp";
// process the includes
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!
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+PgUp/PgDown to switch pages.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 17 Nov 2007 Editor: Deeksha Shenoy |
Copyright 2007 by Dmitri Nesteruk Everything else Copyright © CodeProject, 1999-2010 Web20 | Advertise on the Code Project |