65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 LINQ 和 XSL 进行文档和代码生成

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.87/5 (8投票s)

2008年8月3日

CPOL

15分钟阅读

viewsIcon

32496

downloadIcon

356

本文介绍如何生成源代码以及填充 Excel 电子表格。

引言

有时,您可能需要在不允许使用第三方工具和应用程序的环境中工作。俗话说,“勤奋胜过懒惰”。通过利用 .NET 框架中现有的技术,您可以更聪明地工作。我将引导您完成实现实际解决方案所需的步骤,并通过提供一种生成批量发票报表的方法来完成本文。

pic_0.jpg

背景

本文假定您对 XML、XSL 和 LINQ 以及 C# 和 VB.NET 有一些基础知识。这些技术是本文的基础。我们将探讨如何利用它们来实现我们的目标:创建动态文件。

代码

Zip 文件中包含四个项目

  • 联系人列表 - XML 字面量类型的演示。
  • LINQ 代码生成 - 此项目将展示如何使用 LINQ XML 字面量和 XslCompiledTransform 生成 C# 业务对象。
  • 调用外部 DLL - 此项目将展示如何在 XSL C# 脚本中调用外部 DLL。
  • 账单代码 - 此项目展示了如何使用 LINQ XML 字面量和 XslCompiledTransform 创建 Microsoft Office 2007 Excel 文档以生成发票。

一般信息

XslCompiledTransform 类是在 .NET v2.0 中引入的,旨在取代 .NET v1.1 中可用的 XslTransform 类。此类将使用 XSL 样式表转换 XML 数据以生成新的输出格式。此类是本文的核心,并且在 LINQ XML 字面量类型的帮助下得到了极大的增强。

LINQ XML 字面量类型是 .NET v3.5 新增的功能,目前仅存在于 VB.NET 中。它允许您以更自然的方式编写 XML。以下两个语句是等效的。第一个可以在 C# .NET 中编写,而第二个则不能。第二个是新的 LINQ XML 字面量类型。

Dim xml = New XElement("Person", _
   New XElement("Address", _
       New XAttribute("Street", "123 Main Street"), _
       New XAttribute("City", "Tampa"), _
       New XAttribute("State", "FL")))

Dim xml2 = <Person>
        <Address Street="123 Main Street" City="Tampa" State="FL"/>
       </Person>

通过新的 XML 字面量类型,编写自动生成代码和文档的代码变得更加容易、更清晰,并且将为维护提供更清晰的理解。

演示 1 - 联系人列表

此演示将一个名称列表转化为 XML 输出。<%=%> 之间的语法(包括这两个符号)是一种新的语法,即 LINQ 查询语言。在本文的其余部分,我将坚持使用这种最简单的形式。XML 查询语言的内容非常丰富,理解其更复杂的方面对于理解本文并非必要。一旦您掌握了本文和 LINQ 查询的基础,就可以执行一些非常高级的查询。

以下代码将遍历一个名称列表并生成 XML 结构。语法类似于 SQL 查询语言,但区别在于 Select 语句位于最后,并且您查询的内容不必是数据库表。数据源只需可通过迭代器访问。

Module Module1
   Sub Main()
      Dim names As String() = {"Dave", "Kelly", "Tom"}
      Dim xml = <Contacts>
                   <Names>
                      <%= From r In names _
                         Select <Name><%= r %></Name> %>
                   </Names>
                </Contacts>

      Console.Out.WriteLine(xml)

      Console.WriteLine(String.Format("{0}{0}{1}", _
         Environment.NewLine, "Press Enter To Continue..."))
      
      Console.ReadLine()
    End Sub
End Module

<Contacts>
  <Names>
    <Name>Dave</Name>
    <Name>Kelly</Name>
    <Name>Tom</Name>
  </Names>
</Contacts>

Press Enter To Continue...

演示 2 - LINQ 代码生成

演示 1 展示了使用 LINQ 轻松创建动态 XML 的便捷性。此演示将介绍 XslCompiledTransform 类,它将允许我们在 XSL 样式表中使用 C# 代码来生成动态输出。

虽然代码生成是一个持续的研究领域,并且有许多实现方式和不同的完成目标的想法,但我也将介绍另一种方法。虽然本文并不直接针对如何生成代码,但它是介绍一些概念以使其更容易的最好方式。

在创建代码生成器时要记住的关键是循序渐进。从最终产品开始,然后慢慢回溯。这是演示 2 所采用的方法。您将看到我是如何通过测试 1、2、3,然后是最终项目来分解它的。

首先,我将创建一个 XElement 对象,以最终输出为起点。

Dim xslt = <class_1>
           --- Cut and paste the whole class here. ---
       </class_1>

这是将通过 XSL 样式表使用 XslCompiledTransform 类转换的数据。

XslCompiledTransform 类至少需要一个 XML 样式表。样式表可以保存在磁盘上,也可以在内存中创建。如果样式表在内存中,可以通过实现 IXPathNavigable 的类(如 XElement)将其加载到 XslCompiledTransform 中。XElement 是 LINQ 语言的另一个新产品。虽然您没有意识到,但您已经在演示 1 中使用过它。变量 xml 是以其真实数据类型 XElement 创建的。我将在此演示中使用新的 XElement 数据类型。数据和 XSL 都将加载到 XElement 数据类型中。

再次,采取简单的方法,我将创建一个 XElement 数据结构用于 XSL。XSL 本身将是最简单的形式。减少复杂性将带来更易于理解和更好的可重用性。

XSL 如下所示:

Dim xslt = <xsl:stylesheet version="1.0"
        xmlns:xsl=http://www.w3.org/1999/XSL/Transform
        xmlns:msxsl="urn:schemas-microsoft-com:xslt"
        xmlns:user="urn:my-scripts">
        
        <xsl:template match="class_1">
            <xsl:value-of select="."/>
        </xsl:template>
       </xsl:stylesheet>

以下代码将用于根据 XSL 样式表转换数据并将其输出到控制台。此时启用脚本并非必要,但稍后会用到。

// Enable scripting
XsltSettings settings = new XsltSettings(true, true);

// Create the XSL Transformation class
XslCompiledTransform xslt = new XslCompiledTransform();
xslt.Load(xsltScript.CreateReader(), settings, null);

// Load the data
XPathDocument doc = new XPathDocument(dataScript.CreateReader());

// Output to console using XmlTextWriter
XmlTextWriter writer = new XmlTextWriter(Console.Out);
writer.Formatting = Formatting.Indented;

// Transform the data with the XSLT stylesheet
xslt.Transform(doc.CreateNavigator(), writer);
writer.Close();

运行代码后,您应该会在控制台中看到原始类输出。虽然这并不激动人心,但它为进一步构建提供了基础。我们在此之后添加的任何内容都应限于小的更改,并重新测试以验证我们没有引入错误。

作为下一步,测试 2,在您想要输出为文本的内容和想要生成为数据的内容周围添加更多 XML 标记。例如,<data_2> 标签,并将新标签添加到 XSL 样式表中。

<data_2>
private Guid _id;
private int _street;
private string _address;
private string _city;
private string _state;
</data_2>


<xsl:template match="class_1">
   <xsl:value-of select="data_1"/>
   <xsl:value-of select="data_2"/>
   <xsl:value-of select="data_3"/>
   <xsl:value-of select="data_4"/>
   <xsl:value-of select="data_5"/>
   <xsl:value-of select="data_6"/>
   <xsl:value-of select="data_7"/>
</xsl:template>

我将在项目的其余部分使用通用的命名策略。这将从测试 2 继续到测试 3 和最终项目。此时,运行项目将产生相同的最终输出。

测试 3 开始组合 LINQ 和 XSL。我们将 <data_2 /> XML 标签中的类字段替换为以下 LINQ 代码:

<data_2>
   <%= From r In table.Rows _
     Select <data_2a>
             private <%= r(1) %><%= " " %><%= r(0) %>;
        </data_2a> %>
</data_2>

LINQ 代码使用 DataTable 进行迭代。对于表中的每一行,LINQ 将输出一个 XML 标签 <data_2a />,文本“private”后跟第一列的值,一个空格,然后是第 0 列的值,最后是闭合标签。

在 XSL 样式表中,我们将 XSL 标签 <value-of select="data_2" /> 替换为以下内容:

<xsl:for-each select="data_2/data_2a">
   <xsl:value-of select="."/>
</xsl:for-each>

这次运行项目时,我们会得到不符合私有字段标准命名约定的 SQL Server 数据类型和字段。我们将在最终项目中解决此问题。

我们通过编写一些常规的 C# 代码来开始最终项目。这些代码将用于帮助我们以结构化的方式输出代码,并将 SQL Server 数据类型转换为 C# 数据类型。

public static string CreateProperty(string fieldType, string fieldName)
{
    StringBuilder sb = new StringBuilder();

    sb.Append("\n");
    sb.Append("        /// <summary>\n");
    sb.Append("        /// Get/Set for " + PropertyName(fieldName) + "\n");
    sb.Append("        /// </summary>\n");
    sb.Append("        public " + FieldType(fieldType) + " " + 
                       PropertyName(fieldName) + "\n");
    sb.Append("        {\n");
    sb.Append("            get { return (" + FieldName(fieldName) + "); }\n");
    sb.Append("            set { " + FieldName(fieldName) + " = value; }\n");
    sb.Append("        }\n");

    return (sb.ToString());
}

public static string FieldType(string fieldtype)
{
    switch (fieldtype.ToLower())
    {
        case "smallint":
            return "short";
        case "uniqueidentifier":
            return "Guid";
        case "varchar":
            return "string";
        default:
            return fieldtype;
    }
}

在完成转换和格式化代码后,我们将代码复制到 XSL 样式表中的新部分 <msxsl:script>

Dim xslt = <xsl:stylesheet version="1.0"
        xmlns:xsl=http://www.w3.org/1999/XSL/Transform
        xmlns:msxsl="urn:schemas-microsoft-com:xslt"
        xmlns:user="urn:my-scripts">
            <msxsl:script language="C#" implements-prefix="user">
               <![CDATA[
               
                Insert C# Code here
                
               ]]>
            
            <xsl:template match="class_1">
               <xsl:value-of select="data_1"/>

               <xsl:for-each select="data_2/data_2a">
                   <xsl:value-of select="text()"/>
                   <xsl:value-of select="user:FieldType(fieldtype)"/>
                   <xsl:value-of select="user:InsertText(' ')"/>
                   <xsl:value-of select="user:FieldName(fieldname)"/>
                   <xsl:value-of select="user:InsertText(';')"/>
               </xsl:for-each>
                .
                .
                .
            </xsl:template>
       </xsl:stylesheet>

另一个变化是针对 data_2 的 XSL 标签。Select 语句具有一些不寻常的语法,我们将在稍后详细介绍。Select 语句的构成方式如下:

  • user: - 这是一个 XML 前缀。它被定义为 <script> 标签的一个属性。值“user”可以被定义为“foo”、“bar”或任何您想要的其他值。唯一的限制是该值必须在 <msxsl:script> 和 XSL 用法中匹配。
  • FieldType - 这是之前已定义并放置在 XSL 标签 <script> 的文本部分中的 C# 方法的名称。
  • (fieldtype) - 这是将作为参数传递给 C# 方法的 XML 数据字段的名称。

XSL <data_2 /> 标签再次被以下内容替换:

<data_2>
   <%= From r In table.Rows _
       Select <data_2a>
        private <fieldtype><%= r(1) %></fieldtype><%= " " %>
                <fieldname><%= r(0) %></fieldname>;</data_2a> %>
</data_2>

在此代码部分,您会注意到 XML 标签 <fieldtype>,它在上一段中讨论过,并用作 C# 方法的输入参数。更具体地说,传递给方法的参数是该标签的值。在这种情况下,这意味着 r(1),它等同于 DataTable 当前行的第 1 列的值。

如果您使用 C# 方法输出包含被替换字符(如“&”、“>”等)的文本,则需要在 <xsl:value> 标签中添加一个属性,如下所示。这将阻止字符被替换为其 HTML 等效项。

<xsl:for-each select="data_6/data_6a">
   <xsl:value-of select="user:CreateProperty(fieldtype, fieldname)" 
                 disable-output-escaping="yes"/>
</xsl:for-each>

在进行最终更改后,您就可以最后一次运行它了。如果一切顺利,您应该会看到您期望的格式输出。

演示 3 - 调用外部 DLL

下一个演示是我在完成本文代码时遇到的。一位同事向我提出了一个问题。问题是需要在运行时生成、编译和使用代码。另一部分要求是代码可能需要再次更改。这意味着需要引入 AppDomain。那些以前使用过 AppDomain 的人现在可能在发抖。

经过几次提问,我意识到这符合本文的参数范围,但还有一个额外需求。即调用一个已存在的外部 DLL。我们在演示 1 和 2 中学到的一切都将在此处应用。

以下代码演示了实现解决方案所需的各个部分:

<msxsl:script language="C#" implements-prefix="user">
        
    <msxsl:assembly href="..\Some.Other.Library\Bin\Debug\Some.Other.Library.dll" />
    <msxsl:using namespace="Some.Other.Library.Test.Code"/>
        
    <![CDATA[
    public string Area(int length)
    {
        int square = length * length;

        Helper helper = new Helper();
        
        return (
            helper.Print("The Length Of A Square Is:", length) + "\n" + 
            helper.Print("The Area Of A Square Is:  ", square)
            );
    }
    ]]>

</msxsl:script>

<msxsl:assembly> 语句有两个属性:namehref。一次只能使用其中一个。我选择使用 href,因为它允许我包含 DLL 的路径。name 属性只允许使用名称,并假定 DLL 在 GAC 或当前目录中。要记住的是,DLL 的路径是相对于 XSL 文件位置的。

<msxsl:using> 语句定义了您想要使用的命名空间,并且该命名空间在 DLL 中定义。在上面的代码示例中,Helper 类定义在另一个 DLL 中,并且有一个名为 Print 的方法。

至于我同事的最后一个要求,它必须在 .NET v1.1 中完成。这带来了一些问题。XslCompiledTransform 类在 .NET v1.1 中不存在。XslCompiledTransform 类取代了 XslTransform。令我非常沮丧,并且耗费了大量时间,我无法提供一个使用 XslTransform 类在 .NET v1.1 中工作的解决方案。

目前,此演示不得不暂停,除了必须是 .NET v2.0 及以上版本这一例外。

演示 4 - 创建 XML 发票报表

最后一个演示将引导我们进行一个中等复杂度的实现,这是最复杂的实现。虽然这听起来像一个矛盾,但如果您跟随我并执行我为解决此问题所采取的步骤,最终解释就会变得清晰。

这不适合胆小的人。这需要决心和极大的耐心。因此,我将解释我为实现此示例所采取的高层步骤,并将其留给读者自行更详细地检查代码。我还会限制对问题的回复,因为无法为个人项目提供答案。

1. 构建 Excel 模板文档

正如本文开头所述,我们从最终目标开始。

使用 Office 2007 创建 Excel 文档。Excel 文档应包含您在实际使用时需要的一切。这包括格式、文本、公式等。将文档另存为 .XLSX 文件类型。

我创建的模板位于“Sample Docs”目录中,名为 Invoice.xlsx

下一步是复制 Excel 文档,打开文档,并填写字段,就像您在生成实际文件一样。应按最合理的顺序填写字段。

为了使事情更轻松,我将为模板文件使用以下名称:

  1. Invoice.xlsx - 这是用于创建实际文档的模板。
  2. DataFile.xlsx - 此模板用于确定需要更改哪些内容才能使 Invoice.xlsx 参数化。

2. 打开 Excel 文档

Microsoft Office 2007 使用新的标准化文件格式。我将解释一些我学到的内容,但您可以在 此处阅读更多内容。非常简短的概述是,它是一个标准的 .ZIP 文件,其中包含目录和 .XML 文件。

以下是一些关于文件格式的信息:MSDN,但总结一下:

c = Cell 
r = Cell Address 
s = Style Information 
t = Text   "s" = string 
v = Value 
f = Function

将您创建的模板文件的扩展名更改为 .ZIP,或者将其添加到末尾。使用您喜欢的 ZIP 提取工具,提取两个模板的内容。

您应该会看到以下目录和 .XML 文件:

07/12/2008  09:38 PM    <DIR>          .
07/12/2008  09:38 PM    <DIR>          ..
07/12/2008  09:38 PM    <DIR>          docProps
07/12/2008  09:38 PM    <DIR>          xl
01/01/1980  12:00 AM             1,582 [Content_Types].xml
07/12/2008  09:38 PM    <DIR>          _rels

3. 找出差异

使用您喜欢的工具进行文件比较,比较两个模板之间的所有文件和文件夹。

在我的演示中,我需要处理的两个文件是 xl\sharedStrings.xmlxl\worksheets\sheet1.xml。我还需要使用的另一个文件是 xl\styles.xml

4. 开始更改

  1. 首先将 DataFile\xl\styles.xml 文件复制到 Invoice\xl\styles.xml。我通过大量的试错发现我需要复制此文件,而无需进一步处理它。
  2. 创建 xl\sharedStrings.xml 文件。首先将 Invoice\xl\sharedStrings.xml 的内容复制到一个新的 VB.NET 模块中,该模块接受一个 DataSet。在我的例子中,我需要添加客户信息(姓名、地址等),然后是我工作的时间。我通过使用 XML 字面量来做到这一点,就像我们以前一样,使用来自 DataSet 的数据。
  3. 此文件中的字符串是索引的。新字符串应以有意义的顺序排列,并可以在以后按顺序引用。如果您创建一个示例 DataSet 并填写用于创建 DataFile.xlsx 的值,则可以使用 diff 工具查看您离生成文件的镜像有多近。作为最后一步,您需要更改 XML 根节点中的一些属性。演示中包含了您需要填写的字段。

    将文件保存到 Invoice\xl\styles.xml

  4. 创建 xl\worksheets\sheet1.xml 文件。同样,首先将原始文件的内容复制到一个新的 VB.NET 模块中。这次的不同之处在于,我们将使用 DataFile\xl\worksheets\sheet1.xml 来复制数据。如果您将 Invoice 文件与 DataFile 文件进行比较,您应该会明白这样做的原因。有很多格式参数是必需的。最好从该文件开始,并将值用作自定义方法的参数。
  5. 这些专用方法需要嵌入到 XML 字面量中。正是在这一点上,我非常喜欢 XML 字面量。在查找和替换完生成功能齐全的 Excel 文档所需的值后,它仍然看起来很干净。

    将文件保存到 Invoice\xl\worksheets\sheet1.xml

5. 压缩文件和文件夹

为了测试并确保一切顺利,您需要将文件和文件夹压缩。然后,您可以将 ZIP 文件重命名为原始的 XLSX 扩展名。此时,您应该可以在 Excel 中打开该文件并看到您的数据已填充。

pic_1.jpg

在压缩文件时有一点需要注意;您不能包含顶层文件夹。如果您尝试按以下图片所示进行压缩:

pic_2.jpg

您将收到一个错误:

pic_3.jpg

如果单击“是”,您将得到:

pic_4.jpg

要解决此问题,您需要导航到 Invoice 目录,按 CTRL-A 或选择所有文件,然后压缩文件。

pic_5.jpg

6. 重复操作

对于您自己的项目,您在最初的十几次尝试中成功通过此步骤的可能性可能很小。您需要反复执行步骤 3 和 4 不厌其烦,直到所有问题都得到解决。此时没有魔术公式,只有 diff、修复、测试,然后重复。

7. 最终想法和问题

开始理解所有内容如何协同工作的最好方法是设置断点并使用调试器进行单步调试。理解前三个演示如何工作将提供可在此演示中重用的基础。一旦您掌握了基础知识,就可以专注于理解 Open Office XML 规范的复杂性。

我在处理 Excel XML 规范时发现的唯一问题是日期格式不寻常。在处理日期时,您需要将其转换为整数。最简单的方法是使用以下方法:

Public Function ConvertDate(ByVal strDate As String) As Int32
    Dim newDate As DateTime = DateTime.Parse(strDate)
    Return newDate.ToOADate()
End Function

结论

希望您从本文中学习到了一些有价值的东西,并将其应用于生成代码、文档或全新的东西。

© . All rights reserved.