Prerequisites
Before you continue, you should have a basic knowledge of XML, XSL and XPath. Here are useful links to a quick tutorial and reference of XSLT elements and functions, as well as a quick tutorial about XPath and reference of the functions:
It's good to know about XML serialization in .NET, too, but it's not necessary. The examples are pretty clear and you will understand them easily if you haven't already tried it.
Introduction
This article is going to explain how to write templated messages. This, for instance, can be an email. The solution is very general and is applicable in any situation in which you convert your .NET object serialized in XML to plain text, HTML and other XML, too. This article is not about XSLT. This article is not about .NET serialization. It contains useful information about those two techniques. This article is about combining those technologies to accomplish a real world task.
Filling the missing elements in a templated message sounds pretty easy. For example, you can have an email excerpt that looks like this:
Dear {0},You owe us {1}$ for buying {2}.
This can be very easily implemented:
string emailText = string.Format(templateEmailText, "Peter", 12, "T-Shirt");
What if we have to loop some container and, for every item, place a row in our templated message? That is a foreach construct.
What if we have to check the value of some object and show different text in our templated message depending on it? That is an if-then-else or switch construct.
What if we want to change the templated message without recompiling the whole application? What if the templated message is changing too often and the deployment costs us precious time and resources?
For the first two questions, we can take advantage of the built-in XSLT <xsl:for-each>
and <xsl:if>
constructs. For the last two questions, we just use external XSLT files that can be easily changed in both their content and the files themselves.
The Steps
What are we doing then?
- We create a .NET class containing all data needed for the templated message.
- We add the attributes, if needed, to control how this object is serialized to XML.
- We create an XSLT file that transforms the serialized XML object to plain text, HTML or other XML.
We, of course, use .NET to:
- Create the .NET object with the required data and serialize it to XML.
- Create the .NET XSL transformation class using the XSLT file.
- Perform the transformation itself, writing the result to some stream.
Pretty easy, isn't it?
Using the Code
Let's begin with the already discussed example: writing an email template. We have a website that sells some products online. Before committing a products request, we must send the customer an email through which he views the ordered product and agrees with the order by going to our website, using a link in the email he receives. Let's follow the steps needed to complete the task.
Writing the .NET Class and Preparing it for Serialization
First of all we should have a Product
class, which should look like this.
[[XmlRoot("product")]
public class Product
{
private int mId;
private string mName;
private decimal mPrice;
[XmlAttribute("id")]
public int Id
{
get { return mId; }
set { mId = value; }
}
[XmlElement("name")]
public string Name
{
get { return mName; }
set { mName = value; }
}
[XmlElement("price")]
public decimal Price
{
get { return mPrice; }
set { mPrice = value; }
}
}
When we serialize a concrete instance of Product
in XML, it should look like this:
="1.0"
<product id="1">
<name>melon soap</name>
<price>2.3</price>
</orderedProduct>
By default, the root element should take the class name and all properties should be written as XML elements using their names in the .NET class. We can change both their names and their types using the XmlRoot
, XmlAttribute
and XmlElement
.NET attributes.
Note that to serialize a .NET object in XML you don't need the Serializable
attribute. It's a good idea to apply it anyway because you may have to serialize using BinaryFormatter
sometime. The other important thing is that your class must be public
and all the data that you need serialized should be public
as well.
The next data class is our ProductsOrderMailData
class. It should be the main class that contains all data for the email template message.
[XmlRoot("mailData")]
public class ProductsOrderMailData
{
private string mCustomerName;
private DateTime mOrderDate;
private int mOrderId;
private List<Product> mProducts = new List<Product>();
[XmlElement("name")]
public string CustomerName
{
get { return mCustomerName; }
set { mCustomerName = value; }
}
[XmlAttribute("orderId")]
public int OrderId
{
get { return mOrderId; }
set { mOrderId = value; }
}
[XmlElement("orderDate")]
public DateTime OrderDate
{
get { return mOrderDate; }
set { mOrderDate = value; }
}
[XmlArray("orderedProducts")]
[XmlArrayItem("orderedProduct")]
public List<Product> Products
{
get { return mProducts; }
}
}
Once created, filled with some data and serialized, this is how it looks:
="1.0"
<mailData orderId="12321">
<name>Peter</name>
<orderDate>2007-08-18T10:00:53.109375Z</orderDate>
<orderedProducts>
<orderedProduct id="1">
<name>melon soap</name>
<price>2.3</price>
</orderedProduct>
<orderedProduct id="2">
<name>shampoo</name>
<price>5.5</price>
</orderedProduct>
</orderedProducts>
</mailData>
When we have a collection of objects, we can control how their node names will display using the XmlArray
and XmlArrayItem
.NET attributes. For example, we have set the name of the node collection to be orderedProducts
and each item in it to be orderedProduct
. Notice, also, that the orderedProduct
name overrides the XmlRoot("product")
.NET attribute of the Product
class.
Creating the XSLT File
Visual Studio allows us to create an XSLT file and provides us with some auto-complete. After it's created, there is an xsl:stylesheet
tag in our XSLT file. The next thing we want to add is the xsl:template
tag and specify the match
attribute to associate our template with the XML file. This is an XPath expression. XSLT uses XPath, which is a language for navigating in XML documents.
="1.0"="UTF-8"
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
And here we place our template text
</xsl:template>
</xsl:stylesheet>
Foreach Constructs
To enumerate the contents of our Product
collection, we have to do something like this:
="1.0"="UTF-8"
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<ul>
<xsl:for-each select="mailData/orderedProducts/orderedProduct">
<li>
<xsl:value-of select="name"/>
-
<xsl:value-of select="price"/>
</li>
</xsl:for-each>
</ul>
</xsl:template>
</xsl:stylesheet>
If-then-else Constructs
We have to check if we want to send a free hat to the customer. To do this, we must be sure that he has bought at least 3 products. We will use the xsl:if
construct and the count
function.
="1.0"="UTF-8"
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:if test="count(mailData/orderedProducts/orderedProduct) > 2">
<p>
You ordered more than 2 products and you
will receive a <b>free hat</b>.
</p>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
XSL Variables
When we have to do some calculations for a summary, for example, we will call a function such as sum
. If we need the result of the calculation twice, though, it's not a bad idea to save our result in an xsl:variable
.
="1.0"="UTF-8"
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:variable name="totalSum"
select="sum(mailData/orderedProducts/orderedProduct/price)"/>
<ul>
<xsl:for-each select="mailData/orderedProducts/orderedProduct">
<li>
<xsl:value-of select="name"/>
-
<xsl:value-of select="price"/>
</li>
</xsl:for-each>
</ul>
Total price: <xsl:value-of select="$totalSum"/>
</xsl:template>
</xsl:stylesheet>
Formatting Values
The XSL language has support for formatting numbers. For a complete reference about number formatting, use the links in the Prerequisites section.
="1.0"="UTF-8"
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:value-of select="format-number(20, '#.00')"/>
</xsl:template>
</xsl:stylesheet>
There will definitely be cases when there aren't built-in supports for the formatting we want. Then we have to place the formatting code in our .NET class. For example, we want to display our date in dd MMM yyyy HH:mm
format, which looks like this: 18 Aug 2007 13:48. What if there isn't built-in support in XSL for our task, or we don't know it, or we are experiencing a problem with it?
We can add an extra string field in our .NET class and keep the formatted value in it. Of course, in the XSLT file, we will use that value. Only the changes of the class are displayed here.
[XmlRoot("mailData")]
public class ProductsOrderMailData
{
private DateTime mOrderDate;
private string mFormattedOrderDate;
[XmlIgnore]
public DateTime OrderDate
{
get { return mOrderDate; }
set
{
mOrderDate = value;
mFormattedOrderDate = mOrderDate.ToString("dd MMM yyyy HH:mm");
}
}
[XmlElement("orderDate")]
public string FormattedOrderDate
{
get { return mFormattedOrderDate; }
set { mFormattedOrderDate = value; }
}
}
We don't need the DateTime
field to be serialized now. We tell XmlSerializer
not to serialize it with the XmlIgnore
.NET attribute. Another interesting thing is how to display a hyperlink using the data from our serialized object. We use the xsl:attribute
tag.
="1.0"="UTF-8"
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<p>
To confirm your order please follow
<a>
<xsl:attribute name="href">
http://www.ourwebsite.com/orders.aspx?orderId=
<xsl:value-of select="mailData/@orderId"/>
&customerName=
<xsl:value-of select="mailData/name"/>
</xsl:attribute>
this link.
</a>
</p>
</xsl:template>
</xsl:stylesheet>
Accessing the XML attributes of an XML element using XPath is achieved by the @
symbol. In the previous example, we are accessing the orderId
attribute of the mailData
element.
Connecting All Using .NET
Now after all the XSL stuff comes the .NET code. We create a mail data object, use its properties to fill it with the required information and serialize it.
ProductsOrderMailData data = new ProductsOrderMailData();
Product soap = CreateProduct(1, "melon soap", (decimal)2.3);
Product shampoo = CreateProduct(2, "shampoo", (decimal)5.5);
Product towel = CreateProduct(5, "cotton towel", 15);
data.CustomerName = "Peter";
data.OrderId = 12321;
data.OrderDate = DateTime.UtcNow;
data.Products.Add(soap);
data.Products.Add(shampoo);
data.Products.Add(towel);
Stream serializationStream = new MemoryStream();
XmlSerializer serializer = new XmlSerializer(data.GetType());
serializer.Serialize(serializationStream, data);
After that, we must create an instance of the XslCompiledTransform
class and load the XSLT file in it.
Stream styleSheetStream = new FileStream("ourXslt.xslt", FileMode.Open);
XmlReader styleSheetReader = XmlReader.Create(styleSheetStream);
XslCompiledTransform xslTransformer = new XslCompiledTransform();
xslTransformer.Load(styleSheetReader);
styleSheetReader.Close();
Now we're ready to transform the serialized object stream to another stream.
Stream serializationStream = SerializeObject(serializableObject);
serializationStream.Position = 0;
XmlReader reader = XmlReader.Create(serializationStream);
Stream outputStream = new FileStream("output.html", FileMode.Create);
XmlWriter writer = XmlWriter.Create(outputStream);
xslTransformer.Transform(reader, writer);
It's important to set the Position
property of the Stream
class when you use MemoryStream
to serialize the object to. That's because you can't expect the framework to read the serialized object from the position after the last byte of its representation. And that's all. You can use one of the numerous methods of XslCompiledTransform
and find the one suitable for you, but basically these are the overloads you might use.
Other Solutions
We can, of course, use the XsltArgumentList
class to pass and use the objects directly into the XSLT template. However, we lack the foreach construct support. Actually, I was inspired by this article on this great website to write mine. I think that in most cases, XsltArgumentList
will do the job. In the cases where foreach and if constructs are needed, we'd better follow the principles written here.
I've also seen solutions in which even "the wheel is reinvented." That means the whole scripting language is implemented. Syste.Reflection
gives us such power, but isn't it better to use something that's already created? Parsing an XSLT file and serializing a .NET object to XML aren't the fastest operations. However, generating a class at runtime and creating a dynamic assembly are surely slower. And, of course, you don't want to debug that, too!
History
- Version 1.0 sent on 19.08.2007