Outputting Code - Exploring Details of XSLT Code Generation
(Page 6 of 19 )
XSLT can output any type of text, including code in any language. You saw an introduction to how this works in Chapter 1, and this example dives deeper, including showing template organization and conditional code segments.
NOTE: XSLT code generation relies on XSLT, so if you’re unfamiliar with this technology, you’ll want to review Appendix A. |
Creating a Class
The best way to learn about XSLT code generation is to convert some sample code into an XSLT template. There’s a custom version of Order.vb in the XSLTExample folder of the Chapter 3 code file. I’ll convert this sample file into a working template and test it across the tables of the Northwind metadata. You may want to fire up Visual Studio and walk through this process as you read the text.
Open a new stylesheet and name it SimpleDataContainer.xslt. Create the folder in the Chapter 3/Test directory. This directory already exists if you unzipped the code. If you have updated the default stylesheet as described in Appendix A, you’re ready to go. If you didn’t, you’ll need to add the xsl prefix to the namespace, add the preserve space element, add the entry-level template, and update the stylesheet to reflect the xsl prefix. The metadata file created with the metadata extraction tool (discussed in Chapter 2) uses the dbs prefix, so you’ll also need to add this namespace and prefix. Before you start, your stylesheet should contain the following (bolded items represent changes from the .NET default):
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0"
xmlns :xsl=http://www.w3.org/1999/XSL/Transform
xmlns:xs=http://www.w3.org/2001/XMLSchema
xmlns:dbs="http://kadgen/DatabaseStructure" >
<xsl:import href="..\..\XFiles\Support.xslt"/>
<xsl:output method="text" encoding="UTF-8" indent="yes"/> <xsl:preserve-space elements="*" />
I’m using a supporting stylesheet imported as a separate file. This stylesheet contains utility templates that provide code reuse within XSLT. The processor has to find this utility template. You’ll need to modify this path if your directory layout differs from that in the download. I used relative paths to simplify moving your project.
XSLT provides both xsl:import and xsl:include mechanisms for adding XSLT files. They have different behaviors when template names conflict. You want to avoid conflicting template names to keep others sane, even if XSLT is flexible on the issue. Import has behavior that’s more predictable if you accidentally create a conflict, which is why I use it, but it has to be the first XSLT element in the file.
Specifying XSLT Parameters This template, along with many of the templates you’ll build, takes three parameters: the output filename, the date/time of file generation, and the name of what you’re generating. The earlier “Providing Parameters” section explains how the code generation harness supplies parameter values.
Within the XSLT template, you retrieve parameters using this:
<xsl:param name="Name"/>
<xsl:param name="fileName"/>
<xsl:param name="genDateTime"/>
You can access these parameters from any template within the stylesheet, including templates in the XSLT file accessed using xsl:import.
Creating the Entry-Level Template The purpose of the entry-level template is to start processing the stylesheet. All other templates in the stylesheet should have either a name or a mode to clarify their purpose. The entry-level template is brief and just shifts the context within the XML metadata file to the node you’re processing. In this entry-level template, the template processes table nodes with a Name attribute that matches the Name parameter passed:
<xsl:template match="/">
<xsl:apply-templates select=
"/dbs:DataStructures/dbs:DataStructure/dbs:Tables/dbs:Table[@Name=$Name]"
mode="BuildClasses" />
</xsl:template>
The xsl:apply-templates directive indicates that you want to process any template matching the select criteria. The XSLT engine processes every matching node with every template applicable to the dbs:Table node that also has a mode of BuildClasses (there’s only one). If the XPath statement in this code doesn’t make sense to you, review the XPath section of Appendix A.
Creating the High-Level Processing Template When doing code generation, it’s easiest to have the entry-level template process (or run) a single high-level processing template. The high-level processing template lays out the structure of the output file. I came up with the terms entry-level template and high-level processing template to make it easier to describe the purpose of these templates. You’re unlikely to find them used in other documentation.
The high-level processing templates make extensive use of additional templates. Templates act as subroutines if called using the xsl:call-templates directive. When called using the xsl:apply-templates directive, they provide what’s ultimately a looping operation. In XSLT, the subroutine style templates called using the xsl:call-templates directive are called named templates. The looping style templates called using the xsl:apply-templates directive are called match templates. It makes no difference whether these templates exist in this XSLT file or the files accessed via xsl:import or xsl:include (see footnote 2) because these additional XSLT files become part of the main template during processing.
Footnote 2. It makes no difference whether you access supporting stylesheets via xsl:import or xsl:include unless there’s a conflict. You can check the MSDN Help for these xsl elements for more information about how conflicts are resolved when you use them.
Just as in procedural programming, you want to know what each template does, so keep the focus of each template as narrow as practical and describe what the template accomplishes in its name or its mode. The high-level processing template, started by the entry-level template, looks like this:
<xsl:template match="dbs:Table" mode="BuildClasses">
<xsl:call-template name="FileOpen">
<xsl:with-param name="imports" select="'KADGEN, System.Data'" />
</xsl:call-template>
Public Class <xsl:value-of select="@SingularName"/>Collection
Inherits CollectionBase
<xsl:call-template name="CollectionConstructors" />
<xsl:call-template name="PublicAndFriend" />
End Class
Public Class <xsl:value-of select="@SingularName" />
Inherits RowBase
<xsl:call-template name="ClassLevelDeclarations" />
<xsl:call-template name="Constructors" />
<xsl:call-template name="BaseClassImplementation" />
<xsl: call-template name ="FieldAccessProperties" />
End Class
</xsl:template>
The structure of the output file emerges in the high-level processing template, which outputs a header and two classes—a collection class and a row class. Although these classes are more sophisticated than the example in Chapter 1, they’re still a simplified class to focus on the template structure. Chapter 8 offers a complete middle-tier template.
The first thing output to the file is a standard block defined in the FileOpen template. Although I’m not including the contents of this template, its name gives a good idea of what it accomplishes—outputting the header comments, option statements, imports, and so on that appear at the top of the output file. Within the code of this file, the collection class inherits from the CollectionBase class and contains the output of two named templates. The row class inherits from a different base class and calls four named templates.
Building the XSLT Template on Your Own You can build this template yourself by copying the entire target file (Orders.vb) into your new template. Replace the header with a call to a template named FileOpen that already exists in Support.xslt. If you want to build Support.xslt as well, copy the header into a template in your shell Support.xslt. You can clean that up later. Then place an xsl:call-template directive for each region in the high-level processing template. Give these templates names that match your region name, with spaces removed (you can either use capitalization, as I did, or use underscores). This is the structure of your high-level processing template. You hardly had to think to do it, and any two people within your workgroup would’ve created identical files.
Now create a named xsl:template for each xsl:call-template and copy the contents of the region into the corresponding template. Now you’re ready to use xsl:value-of to replace values with ones retrieved from the XML input file and use xsl:if, xsl:choose, xsl:for-each, and xsl:apply-templates to provide logic to your templates.
Regions are important when you’re debugging XSLT and brute-force code templates. The compiler will find a problem in your output (or you’ll determine where a change needs to be made), and you have to trace this back into your template file. By far the easiest way to do this is using regions to organize both your source code and templates.
Once you’ve got named templates corresponding to each region in your output code, scan the high-level processing template for any values that wouldn’t be the same for all output files. These are the items you need to replace using the xsl:value-of directive. In this case, there are two, both part of the class names. The original lines are as follows:
Public Class OrderCollection
...
Public Class Order
You can replace whole words or any part of words. Whether a name is singular or plural needs special attention. The metadata provides attributes for Name, OriginalName, SingularName, and PluralName. Although that may seem like a lot of names, it’s really the only way to ensure you’re getting the right name at the right location. (Table 2-2 shows the different names available for each table.) In this case, you want the singular name in both places:
Public Class <xsl:value-of select="@SingularName"/>Collection
...
Public Class <xsl:value-of select="@SingularName" / >
This chapter is from Code Generation in Microsoft .NET by Kathleen Dollard (Apress, 2004, ISBN: 1590591372). Check it out at your favorite bookstore today.
Buy this book now. |
Next: Creating Named Templates >>
More .NET Articles
More By Apress Publishing