VB XML Cookbook, Recipe 3: Identity Transforms (Doug Rothaus)
An identity transform in XSLT does just what the name implies: replace the identity of an element or attribute with a new identity. Identity transforms are especially critical when dealing with XML markup that is less rigid in its structure. Consider a documentation structure for news articles. Whenever a title is referred to within an article, it is highlighted with italics. However, the XML schema for the article does not specify italics, it specifies that the term is some type of title so that different transforms can handle the term in their own fashion. When you transform XML such as this into a readable format, such as HTML, you need to preserve the structure of the text around the highlighted term so that the original intent remains intact. That is…
<Paragraph sequenceID=”1”>This week a remastered version of the movie <title type=”movie”>Raider’s of the Lost Ark</title> was released.</Paragraph>
becomes…
<p>This week a remastered version of the movie <i>Raider’s of the Lost Ark</i> was released.</p>
In a browser you see…
This week a remastered version of the movie Raider’s of the Lost Ark was released.
(Note: I have no idea if they are really releasing a remastered version of Raider’s of the Lost Ark, I just typed the first thing that popped into my head).
You can perform an identity transform using Visual Basic and XML Literals by combining XML Axis properties and the ReplaceWith method for LINQ to XML objects.
Let’s look at a more complete example. In the AdventureWorks contacts source document that we used in previous recipes (you can download the XML document and related schemas from the Recipe 1 post), there is an <AdditionalContactInfo> element that contains information about the contact such as phone numbers, shipping and billing addresses, and so on. To keep things simple, we’ll just look at the <eMail> element. The <eMail> element can show up in a number of places in the contents of the <AdditionalContactInfo> element. As a result, you can use the XML Descendant axis property discussed in Recipe 2 to retrieve all of the references to <eMail> elements. You can then iterate through the query results and call the ReplaceWith method for each <eMail> element and replace it with a new identity. For example:
Dim emails = (From email In _
xmlDoc.<Contacts>.<Contact>.<aci:AdditionalContactInfo>...<act:eMail>).ToList()
For Each email In emails
TransformEmail(email)
Next
In this example, the output is HTML and the <eMail> element is replaced with an anchor element that specifies a mailto: link to the e-mail address. The previous code snippet passes the <eMail> XElement object to the function TransformEmail, which does the actual replacing.
Private Sub TransformEmail(ByVal email As XElement)
Dim emailHtml = <div class="Email">
<a href=<%= "mailto:" & email.<act:eMailAddress>.Value %>>
<%= email.<act:eMailAddress>.Value %>
</a>
</div>
email.ReplaceWith(emailHtml)
End Sub
A simple class that creates the HTML document using these samples is shown here.
Imports <xmlns="http://SampleSchema/AWContacts">
Imports <xmlns:aci="http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/ContactInfo">
Imports <xmlns:act="http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/ContactTypes">
Public Class Recipe3
Public Function GetContactsHtml(ByVal xmlDoc As XDocument) As XElement
' Replace e-mail address tags with mailto links.
Dim emails = (From email In xmlDoc.<Contacts>.<Contact>.<aci:AdditionalContactInfo>...<act:eMail>).ToList()
For Each email In emails
TransformEmail(email)
Next
' Create the HTML document
Return <html>
<body>
<table border="1">
<%= From contact In xmlDoc.<Contacts>.<Contact> _
Select <tr>
<td valign="top">
<%= contact.<FirstName>.Value & " " & contact.<LastName>.Value %>
</td>
<td valign="top">
<%= contact.<aci:AdditionalContactInfo> %>
</td>
</tr> _
%>
</table>
</body>
</html>
End Function
Private Sub TransformEmail(ByVal email As XElement)
Dim emailHtml = <div class="Email">
<a href=<%= "mailto:" & email.<act:eMailAddress>.Value %>>
<%= email.<act:eMailAddress>.Value %>
</a>
</div>
email.ReplaceWith(emailHtml)
End Sub
End Class
In XSLT, the same transform would look like the following:
<?xml version='1.0'?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:aw="http://SampleSchema/AWContacts"
xmlns:aci="http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/ContactInfo"
xmlns:act="http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/ContactTypes">
<xsl:output method="html" indent="yes"/>
<xsl:template match="aw:Contacts">
<html>
<body>
<table border="1">
<xsl:apply-templates select="aw:Contact" />
</table>
</body>
</html>
</xsl:template>
<xsl:template match="aw:Contact">
<tr>
<td valign="top">
<xsl:value-of select="aw:FirstName"/>
<xsl:text> </xsl:text>
<xsl:value-of select="aw:LastName"/>
</td>
<td valign="top">
<xsl:apply-templates select="aci:AdditionalContactInfo" />
</td>
</tr>
</xsl:template>
<xsl:template match="aci:AdditionalContactInfo">
<div class="AdditionalInfo">
<xsl:copy>
<xsl:apply-templates select="@* | node()" />
</xsl:copy>
</div>
</xsl:template>
<xsl:template match="act:eMail">
<div class="Email">
<a>
<xsl:attribute name="href">
<xsl:text>mailto:</xsl:text>
<xsl:value-of select="act:eMailAddress" />
</xsl:attribute>
<xsl:value-of select="act:eMailAddress" />
</a>
</div>
</xsl:template>
</xsl:stylesheet>
Grouping by Element Type
One thing that you can take advantage of in Visual Basic is the ability to group transforms by element type. For example, the ContactTypes.xsd file that we have been using in our AdventureWorks samples defines an address type, and e-mail type, and a phone number type. ContactTypes.xsd also identifies three different element names that are the address type and five different element names that are the phone number type. In XSLT, you would create a template for each address type and end up with three different templates for information that is, essentially, formatted the same. In Visual Basic, you can easily create a query for all different address or phone number elements and pass that to the same VB function, which acts as your XSLT template. For example:
Dim addresses = (From addr In info...<act:homePostalAddress>).Union( _
From addr In info...<act:physicalDeliveryOfficeName>).Union( _
From addr In info...<act:registeredAddress>).ToList()
For Each address In addresses
TransformAddress(address)
Next
...
Private Sub TransformAddress(ByVal address As XElement)
Dim addressHtml = _
<div class="Address">
<%= address.<act:Street>.Value %><br/>
<%= address.<act:City>.Value & ", " %> 
<%= address.<act:StateProvince>.Value %> 
<%= address.<act:PostalCode>.Value %><br/>
<%= address.<act:CountryRegion>.Value %><br/>
<%= _
GetSpecialInstructions(address.<act:SpecialInstructions>.ToList()) %>
</div>
address.ReplaceWith(addressHtml)
End Sub