Click here to Skip to main content
15,885,953 members
Articles / Programming Languages / XSLT
Article

Multi-file XSL Transformation Custom Tool for Visual Studio

Rate me:
Please Sign up or sign in to vote.
4.65/5 (6 votes)
17 Nov 2007CPOL6 min read 54.4K   328   32   6
An XSL Transformation Custom Tool for Visual Studio
Multiple File XSL Generator Screenshot

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:

C#
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:

C#
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:

C#
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.

C#
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:

C#
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.

C#
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:

XML
<strings>
  <string>Text</string>
</strings>

and B.xml contain:

XML
<root>
  <include file="A.xml"/>
</root>

Then, when the transformation runs on B.xml, it will use a file that looks like this:

XML
<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:

C#
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!

History

  • 14 November 2007 — Initial Release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Founder ActiveMesa
United Kingdom United Kingdom
I work primarily with the .NET technology stack, and specialize in accelerated code production via code generation (static or dynamic), aspect-oriented programming, MDA, domain-specific languages and anything else that gets products out the door faster. My languages of choice are C# and C++, though I'm open to suggestions.

Comments and Discussions

 
GeneralReporting progress of XSLT Transformation Pin
Marco Scalone21-Aug-08 8:02
Marco Scalone21-Aug-08 8:02 
GeneralRe: Reporting progress of XSLT Transformation Pin
Dmitri Nеstеruk19-Aug-13 2:29
Dmitri Nеstеruk19-Aug-13 2:29 
GeneralVisual Studio 2008 - compile issues Pin
tourist.tam6-May-08 6:15
tourist.tam6-May-08 6:15 
NewsLatest Version Here Pin
Dmitri Nеstеruk6-May-08 23:13
Dmitri Nеstеruk6-May-08 23:13 
GeneralImprovements Pin
Ronson190917-Nov-07 23:43
Ronson190917-Nov-07 23:43 
GeneralRe: Improvements Pin
Dmitri Nеstеruk17-Nov-07 23:51
Dmitri Nеstеruk17-Nov-07 23:51 

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

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