使用 XSL 转换和 XML 序列化的模板消息






4.33/5 (5投票s)
本文介绍如何实现模板消息,例如电子邮件模板。
必备组件
在继续之前,您应该对 XML、XSL 和 XPath 有基本的了解。以下是 XSLT 元素和函数快速教程及参考,以及 XPath 快速教程和函数参考的有用链接。
了解 .NET 中的 XML 序列化也很有益,但并非必要。示例非常清晰,即使您尚未尝试过,也能轻松理解它们。
引言
本文将解释如何编写模板消息。例如,这可以是一封电子邮件。该解决方案非常通用,适用于任何将 .NET 对象序列化为 XML 并转换为纯文本、HTML 以及其他 XML 的情况。本文不讨论 XSLT。本文也不讨论 .NET 序列化。它包含有关这两种技术的信息。本文的重点是将这些技术结合起来以完成实际任务。
填充模板消息中缺失的元素听起来很简单。例如,您可能有一个如下所示的电子邮件片段:
Dear {0},You owe us {1}$ for buying {2}.
这可以很容易地实现。
string emailText = string.Format(templateEmailText, "Peter", 12, "T-Shirt");
如果我们必须循环某个容器,并为每个项在模板消息中放置一行,该怎么办?这就是 foreach 结构。
如果我们必须检查某个对象的值,并根据该值在模板消息中显示不同的文本,该怎么办?这就是 if-then-else 或 switch 结构。
如果我们想在不重新编译整个应用程序的情况下更改模板消息,该怎么办?如果模板消息更改过于频繁,部署会耗费我们宝贵的时间和资源,该怎么办?
对于前两个问题,我们可以利用内置的 XSLT <xsl:for-each>
和 <xsl:if>
结构。对于后两个问题,我们只需使用外部 XSLT 文件,可以轻松更改其内容和文件本身。
步骤
那么我们要做什么呢?
- 我们创建一个 .NET 类,其中包含模板消息所需的所有数据。
- 如有需要,我们添加属性来控制该对象如何序列化为 XML。
- 我们创建一个 XSLT 文件,该文件将序列化的 XML 对象转换为纯文本、HTML 或其他 XML。
我们当然会使用 .NET 来:
- 创建具有所需数据的 .NET 对象并将其序列化为 XML。
- 使用 XSLT 文件创建 .NET XSL 转换类。
- 执行转换本身,将结果写入某个流。
很简单,不是吗?
Using the Code
让我们从前面讨论的示例开始:编写电子邮件模板。我们有一个在线销售产品的网站。在提交产品请求之前,我们必须向客户发送一封电子邮件,通过该电子邮件,客户可以查看订购的产品,并通过单击收到的电子邮件中的链接来同意订单。让我们按照完成任务所需的步骤进行。
编写 .NET 类并准备进行序列化
首先,我们应该有一个 `Product` 类,它应该看起来像这样。
[[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; }
}
}
当我们序列化 `Product` 的具体实例到 XML 时,它应该看起来像这样:
<?xml version="1.0"?>
<product id="1">
<name>melon soap</name>
<price>2.3</price>
</orderedProduct>
默认情况下,根元素应采用类名,所有属性应使用 .NET 类中的名称作为 XML 元素写入。我们可以使用 `XmlRoot`、`XmlAttribute` 和 `XmlElement` .NET 属性更改它们的名称和类型。
请注意,要将 .NET 对象序列化为 XML,您**不需要** `Serializable` 属性。最好还是应用它,因为有时您可能需要使用 `BinaryFormatter` 进行序列化。另一个重要的事情是,您的类必须是 `public`,并且您需要序列化的所有数据也必须是 `public`。
下一个数据类是我们的 `ProductsOrderMailData` 类。它应该是包含电子邮件模板消息所有数据的类。
[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; }
}
}
创建、填充一些数据并序列化后,它看起来是这样的:
<?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>
当我们有一个对象集合时,我们可以使用 `XmlArray` 和 `XmlArrayItem` .NET 属性来控制它们的节点名称如何显示。例如,我们将节点集合的名称设置为 `orderedProducts`,并将其中的每个项设置为 `orderedProduct`。另外请注意,`orderedProduct` 名称会覆盖 `Product` 类的 `XmlRoot("product")` .NET 属性。
创建 XSLT 文件
Visual Studio 允许我们创建 XSLT 文件并提供一些自动完成功能。创建后,我们的 XSLT 文件中会有一个 `xsl:stylesheet` 标签。接下来要添加的是 `xsl:template` 标签,并指定 `match` 属性将其与 XML 文件关联。这是一个 XPath 表达式。XSLT 使用 XPath,这是一种导航 XML 文档的语言。
<?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 结构
要枚举 `Product` 集合的内容,我们必须这样做:
<?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 结构
我们必须检查是否要向客户发送免费帽子。为此,我们必须确保他至少购买了 3 件商品。我们将使用 `xsl:if` 结构和 `count` 函数。
<?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 变量
当我们需要进行一些计算(例如汇总)时,我们将调用一个像 `sum` 这样的函数。但是,如果我们两次需要计算结果,那么将结果保存在 `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>
格式化值
XSL 语言支持格式化数字。有关数字格式化的完整参考,请使用**前提条件**部分中的链接。
<?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>
肯定会有内置支持无法满足我们想要的格式化的情况。那时我们必须在 .NET 类中放置格式化代码。例如,我们想以 `dd MMM yyyy HH:mm` 格式显示日期,看起来像这样:**18 Aug 2007 13:48**。如果 XSL 中没有内置支持我们任务的功能,或者我们不知道,或者我们遇到了问题,该怎么办?
我们可以在 .NET 类中添加一个额外的字符串字段,并将格式化后的值保存在其中。当然,在 XSLT 文件中,我们将使用该值。此处仅显示类的更改。
[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; }
}
}
现在我们不需要将 `DateTime` 字段序列化了。我们使用 `XmlIgnore` .NET 属性告诉 `XmlSerializer` 不要序列化它。另一个有趣的事情是如何使用我们序列化对象中的数据显示超链接。我们使用 `xsl:attribute` 标签。
<?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>
使用 XPath 访问 XML 元素的 XML 属性是通过 `@` 符号实现的。在前面的示例中,我们正在访问 `mailData` 元素的 `orderId` 属性。
使用 .NET 连接所有内容
现在,在所有 XSL 部分之后是 .NET 代码。我们创建一个邮件数据对象,使用其属性填充所需信息并进行序列化。
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);
之后,我们必须创建一个 `XslCompiledTransform` 类的实例,并将 XSLT 文件加载到其中。
Stream styleSheetStream = new FileStream("ourXslt.xslt", FileMode.Open);
XmlReader styleSheetReader = XmlReader.Create(styleSheetStream);
XslCompiledTransform xslTransformer = new XslCompiledTransform();
xslTransformer.Load(styleSheetReader);
styleSheetReader.Close();
现在我们可以将序列化的对象流转换为另一个流。
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);
当您使用 `MemoryStream` 将对象序列化为流时,设置 `Stream` 类的 `Position` 属性非常重要。因为您不能期望框架在最后一个字节表示之后的位置读取序列化的对象。就是这样。您可以使用 `XslCompiledTransform` 的众多方法之一,找到适合您的方法,但基本上这些是您可能使用的重载。
其他解决方案
当然,我们可以使用 `XsltArgumentList` 类将对象直接传递并使用到 XSLT 模板中。但是,我们缺少 foreach 结构支持。实际上,我受到了这个很棒的网站上这篇文章的启发,才写了我的。我认为在大多数情况下,`XsltArgumentList` 都可以胜任。在需要 foreach 和 if 结构的情况下,我们最好遵循此处编写的原则。
我还看到过一些甚至“重复发明轮子”的解决方案。这意味着实现了整个脚本语言。`Syste.Reflection` 给了我们这样的能力,但使用已有的东西难道不是更好吗?解析 XSLT 文件和将 .NET 对象序列化为 XML 并非最快的操作。但是,在运行时生成类和创建动态程序集肯定更慢。当然,您也不想调试它们!
历史
- 版本 1.0 于 2007 年 8 月 19 日发布