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

Visual Studio 的多文件 XSL 转换自定义工具

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.65/5 (6投票s)

2007年11月17日

CPOL

6分钟阅读

viewsIcon

55926

downloadIcon

346

Visual Studio 的 XSL 转换自定义工具

Multiple File XSL Generator Screenshot

引言

本文介绍了一个对 Chris Stefano 在 CodeProject 上发布的 TransformCodeGenerator 工具进行的非常简单的增强。为了让该工具能够很好地处理我们在生成商业代码时面临的实际问题,该工具经过了小型重新设计,变得更加易用。初次使用 XSLT 生成功能的新手,应首先阅读原始文章,然后再阅读本文。

Using the Code

为了编译自定义工具,您需要在计算机上安装 Visual Studio SDK。您还需要在计算机上安装 AltovaXML 引擎。安装完成后,编译项目。打开 VS 命令提示符,导航到输出目录,然后键入“regasm TransformCodeGenerator.dll”。这样,Visual Studio 就可以使用该插件了。

我为 Visual Studio 2005 制作了这个工具,但可以通过更改代码中的 VisualStudioVersion 变量来为其他版本编译。

问题

TransformCodeGenerator 非常适合通过 XSLT 样式表将 XML 转换为 C#。然而,当我们开始将该工具用于一个相当大的应用程序时,我们遇到了以下问题:

  • 该工具使用的是 XSLT 1.0,因为这是 .NET 支持的全部内容。显然,System.XML 命名空间永远不会支持 XSLT 2.0,因为 Microsoft 将赌注押在了 XQuery 上。无论如何,我们需要 XSLT 2.0 的功能,特别是字符串转换函数,这对于变量命名很有用。
  • 该工具只能将单个样式表应用于 XML 文件,因此只能生成单个 C# 文件。在典型的使用场景中,我们定义一个数据结构,该结构然后会生成 UI、ORM 类以及一些启动业务逻辑。这意味着,从一个文件 Person.xml,我们会生成 Person.cs(表单代码,通常包含 ORM 类的实例和一些路由事件处理程序)、Person.Designer.cs(包含 InitializeComponent() 的表单)、Person.Entity.cs(ORM 相关内容,通常只是一个列出所有成员的类,以及一些常规 SQL 命令和帮助函数)以及 Person.Logic.cs(用于数据绑定和验证等)。
  • 有时我们需要对多个 XML 文件进行转换。例如,我们可能有一个包含帮助资源的 XML 文件,然后需要将这些资源作为工具提示应用于生成的 UI 元素。目前,这是不可能实现的。

除了这些功能之外,我还添加了一个事件日志记录功能,这应该会让自定义工具更容易调试。

解决方案

解决上述问题的最直接方法是编写一个 Visual Studio 插件来处理专门的生成。然而,自定义工具的简洁性是显而易见的,所以我坚持使用了它。以下是上述问题的解决方案。

XSLT 2.0

考虑到 Microsoft 不支持 XSLT 2.0,我研究了其他可以替代使用的免费 XSLT 转换引擎。我首先尝试了 Saxon.NET,但遗憾的是,它在我的系统上根本无法工作,考虑到它运行在基于 .NET 的 Java VM 中并抛出 Java 异常,这并不奇怪。随后,我找到了另一家公司 Altova 制造的引擎,它具有我需要的功能。AltovaXML 引擎是免费的,并且通过在安装包时将相应的程序集安装到 GAC 中来支持 .NET。它具有一个非常简单的 API,我使用它来进行最终转换,下面的代码片段对此进行了演示。

IXSLT2 xslt = new ApplicationClass().XSLT2;
try
{
  xslt.InputXMLFileName = tempPath;
  xslt.XSLFileName = xslPath;
  result += xslt.ExecuteAndGetResultAsString();
  AddLogEntry(string.Format("TransformCodeGenerator.GetTransformedResult:" +
        " transformation executed and yielded{0}{0}{1}",
        Environment.NewLine, result));
}

这里需要注意的一点是,与 TransformCodeGenerator 不同,转换结果的字节是以选择的编码返回的。如果您用俄语编写应用程序,ASCII 的效果不是很好。

多重输出

为了实现多重转换,我决定扩展 TransformCodeGenerator 语法,而不破坏现有应用程序。因此,在 XML 文件的根元素中,我们仍然需要主转换样式表的名称。此转换的结果是一个与 XML 文件同名的 C# 文件(当然,不带扩展名)。其他转换样式表也在根元素中定义为 transformation2transformation3 等等。

生成文件的命名方案很简单。对于主转换,Person.xml 变成 Person.cs。对于所有其他转换,XML 文件名和 XSL 文件名将被组合,因此通过 Entity.xslt 转换 Person.xml 会得到 Person.Entity.cs

好了,这是转换的语法,但要实际编码,我使用了 Adam Langley 的 VsMultipleFileGenerator。我已经将 API 调整为适合我们的代码生成问题,但保留了原始生成器不变,仅除了 COM 注册/取消注册函数。让我们简要看一下 VsMultipleFileGenerator 以及我们的程序如何处理转换。

首先,我们的自定义工具需要提供一个枚举,该枚举稍后将用作转换的标准。在本例中,我们提供一个文件名列表以供后续处理。System.Xml API 用于从 XML 文件中提取值。

public override IEnumerator<string> GetEnumerator()
{
  XmlDocument doc = new XmlDocument();
  doc.Load(InputFilePath);

  XmlNode node;
  for (int i = 2; i < 100; ++i)
  {
    node = doc.DocumentElement.Attributes["transformer" + i];
    if (node != null)
    {
      AddLogEntry(string.Format
            ("TransformCodeGenerator.GetEnumerator() yielded {0}",
        node.Value));
      yield return node.Value;
    }
    else break;
  }
}

现在,我们需要提供一个确定文件名的函数。根据我上面描述的约定,以下实现应该是有意义的。

protected override string GetFileName(string element)
{
  return string.Format("{0}.{1}.cs",
    Path.GetFileNameWithoutExtension(InputFilePath),
    Path.GetFileNameWithoutExtension(element));
}

代码生成本身必须出现在 GenerateContent 函数中,它被完全委托给另一个函数。这样做的原因是,此函数处理除主样式表之外的所有样式表的转换。

public override byte[] GenerateContent(string element)
{
  return GetTransformResult(element);
}

现在剩下我们的主转换,这是 VsMultipleFileGenerator 称为摘要内容的部分。由于我们不将其用于摘要,而是用于 C#,因此我们与处理附加样式表的方式相同。

public override byte[] GenerateSummaryContent()
{
  XmlDocument doc = new XmlDocument();
  doc.Load(InputFilePath);

  XmlNode node = doc.DocumentElement.Attributes["transformer"];
  if (node != null)
    return GetTransformResult(node.Value);
  else
    return encoding.GetBytes(string.Format(
      "#error {0} is missing the 'transformer' attribute at root level.",
      InputFilePath));
}

这里一个值得注意的有趣之处是错误处理的实现方式。由于结果文件是 C# 类型的,因此缺少 transformer 属性将导致生成的代码以 #error 开头,这意味着 C# 编译器将比一些巧妙格式化的多行文本消息更好地呈现它。

VsMultipleFileGenerator 还有一个函数,它需要知道我们生成代码的默认扩展名。

public override string GetDefaultExtension()
{
  return defaultExtension;
}

在本例中,defaultExtension 变量包含常量值 '.cs'。

XML 文件合并

通过要求 XML 文件中存在 transformer 属性,我们已经为源数据定义了一个约定。我通过添加一个概念来扩展这个约定:当 <include> 元素出现在源文件的任何位置时,出于转换目的,它将包含实际文件的内容。这是一个例子:

假设 A.xml 包含

<strings>
  <string>Text</string>
</strings>

B.xml 包含

<root>
  <include file="A.xml"/>
</root>

那么,当在 B.xml 上运行转换时,它将使用一个看起来像这样的文件:

<root>
  <strings>
    <string>Text</string>
  </strings>
</root>

您可能想知道实际的替换是如何发生的。这个过程非常简单——我们所做的就是找到所有 <include file="..."/> 字符串,并将它们替换为它们引用的文件的内容。您可能想看代码,所以这里是:

private byte[] GetTransformResult(string xslFileName)
{
  AddLogEntry(string.Format("TransformCodeGenerator.GetTransformResult({0})", 
        xslFileName));
  // get the path

  string path = InputFilePath.Substring(0, InputFilePath.LastIndexOf('\\') + 1);
  string result = string.Empty, xslPath = path + xslFileName, 
        ifc = InputFileContents;
  string tempPath = xslPath + ".temp";

  // process the includes

  int start;
  while ((start = ifc.IndexOf("<include")) != -1)
  {
    int end = ifc.IndexOf(">", start);
    string entry = ifc.Substring(start, end - start + 1);
    string[] parts = entry.Split("\"".ToCharArray());
    ifc = ifc.Replace(entry, File.ReadAllText(path + parts[1]));
  }

  File.WriteAllText(tempPath, ifc);
  AddLogEntry(string.Format("TransformedCodeGenerator.GetTransformedResult: " +
    "Temporary file {0} created and contains{1}{1}{2}",
    tempPath, Environment.NewLine, ifc));

  IXSLT2 xslt = new ApplicationClass().XSLT2;
  try
  {
    xslt.InputXMLFileName = tempPath;
    xslt.XSLFileName = xslPath;
    result += xslt.ExecuteAndGetResultAsString();
    AddLogEntry(string.Format("TransformCodeGenerator.GetTransformedResult:" +
          " transformation executed and yielded{0}{0}{1}",
          Environment.NewLine, result));
  }
  catch (Exception x)
  {
    result += string.Format(
      "Exception while calling GenerateSummaryContent: {0}\r\n\r\n" +
      "Transformer is:\r\n\r\n{1}\r\n\r\nXML is:\r\n\r\n{2}",
      x, xslPath, InputFilePath);
  } finally
  {
    File.Delete(tempPath);
  }
  return encoding.GetBytes(result);
}

我们之所以进行手动搜索-替换并创建一个(完全不必要的)临时文件,是因为 Regex 和 Altova 引擎固有的错误。如果您能够通过这些功能使其正常工作——那就太好了!

历史

  • 2007 年 11 月 14 日 — 初始发布
© . All rights reserved.