Click here to Skip to main content
Click here to Skip to main content

Template Messages Using XSL Transformations and XML Serialization

, 23 Aug 2007 CPOL
Rate this:
Please Sign up or sign in to vote.
This article shows how you can implement templated messages, such as email templates

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:

<?xml version="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:

<?xml version="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.

<?xml version="1.0" encoding="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:

<?xml version="1.0" encoding="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.

<?xml version="1.0" encoding="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.

<?xml version="1.0" encoding="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.

<?xml version="1.0" encoding="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.

<?xml version="1.0" encoding="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

License

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

Share

About the Author

Cvetomir Todorov
Software Developer
Bulgaria Bulgaria
Cvetomir Todorov is a student in the Sofia University. His interests include Object Oriented Programming and Design, Design Patterns, Producing High-Quality Code, Refactoring, Unit Testing, Test Driven Development etc. The technology he's using is .NET with C# language.

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.1411022.1 | Last Updated 23 Aug 2007
Article Copyright 2007 by Cvetomir Todorov
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid