XslCompiledTransform忽略XPathNodeIterator的顺序

时间:2015-03-20 20:32:05

标签: c# xml visual-studio xslt soap

我有一个XSLT样式表,它使用文档并输出SOAP消息,其中正文是由WCF数据协定(此处未指定)定义的特定格式。问题是WCF有一个特殊的概念,即什么构成“按字母顺序”排序,并认为以下顺序是正确的:

  • AC
  • 抗体

这是因为它在内部使用序数字符串比较。细节不是很有趣,足以说XSLT <sort>本身不支持这种排序,但为了将格式可能不同的输入文档转换为可接受的SOAP消息,样式表必须能够订购根据这种特殊的顺序输出元素。因此,我决定在脚本块中实现节点排序。这是C#解决方案的一部分,使用XslCompiledTransform,因此msxsl:script可用。

给出样式表:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:fn="urn:functions"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt"
                exclude-result-prefixes="msxsl exsl"
                xmlns:exsl="http://exslt.org/common"
>
  <msxsl:script implements-prefix="fn" language="C#">
    <![CDATA[

      public class OrdinalComparer : IComparer
      {
          public int Compare(object x, object y)
          {
              return string.CompareOrdinal((string)x, (string)y);
          }
      }

      public XPathNodeIterator OrdinalSort(XPathNavigator source)
      {
        var query = source.Compile("/*");
        query.AddSort(source.Compile("local-name()"), new OrdinalComparer());
        return source.Select(query);
      }    
    ]]>
  </msxsl:script>

  <xsl:template match="Stuff">
    <xsl:element name="Body">
      <xsl:element name="Request">
        <xsl:variable name="sort">
          <xsl:apply-templates select="*"/>
        </xsl:variable>
        <xsl:for-each select="fn:OrdinalSort($sort)">
          <xsl:copy-of select="."/>
        </xsl:for-each>
      </xsl:element>
    </xsl:element>
  </xsl:template>

  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

输入文件:

<?xml version='1.0' encoding='utf-8'?>
<Root>
  <Stuff>
    <Age></Age>
    <AIS></AIS>
    <Something></Something>
    <BMI></BMI>
  </Stuff>
</Root>

我希望输出按如下顺序排列最内层元素:

  • AIS
  • 年龄
  • BMI
  • 东西

这不会发生。相反,元素按照它们进入的顺序发出。在执行时调试样式表我可以看到OrdinalSort函数被调用,它返回的迭代器会按所需顺序枚举元素,但XSLT处理器以某种方式忽略了这个和按照遇到的顺序发出元素。

我还验证了在控制台应用程序中解析文档并运行相同的迭代器查询会以正确的顺序发出元素。

为什么,我能做些什么呢?我目前唯一的预感是XSLT引擎正在解释迭代器的父导航器(它没有从传递给sort函数的内容中改变)作为要重现的元素,并忽略迭代器的内容。

2 个答案:

答案 0 :(得分:1)

我不确定如何使用XPathNodeIteratorXPathNavigator解决这个问题,我从XPathNavigator[]创建XPathNodeIterator,以避免任何懒惰的评估效果,但不知怎的,我总是得到与你相同的结果。

作为替代方案,我已经使用.NET框架中的DOM实现编写了一些代码,以正确的排序顺序创建一些新节点:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:mf="urn:functions"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt"
                exclude-result-prefixes="msxsl exsl mf"
                xmlns:exsl="http://exslt.org/common"
>
  <msxsl:script implements-prefix="mf" language="C#">
    <![CDATA[

      public class OrdinalComparer : IComparer
      {
          public int Compare(object x, object y)
          {
              return string.CompareOrdinal((string)x, (string)y);
          }
      }

      public XPathNavigator OrdinalSort(XPathNavigator source)
      {
        var query = source.Compile("/root/*");
        query.AddSort("local-name()", new OrdinalComparer());
        XPathNodeIterator result = source.Select(query);
        XmlDocument resultDoc = new XmlDocument();
        XmlDocumentFragment frag = resultDoc.CreateDocumentFragment();
        foreach (XPathNavigator item in result)
        {
          frag.AppendChild(resultDoc.ReadNode(item.ReadSubtree()));
        }
        return frag.CreateNavigator();
      }    
    ]]>
  </msxsl:script>

  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="Stuff">
    <Body>
      <Request>
        <xsl:variable name="sort-rtf">
          <root>
            <xsl:copy-of select="*"/>
          </root>
        </xsl:variable>
        <xsl:variable name="sort" select="exsl:node-set($sort-rtf)"/>
        <xsl:variable name="sorted" select="mf:OrdinalSort($sort)"/>
        <xsl:copy-of select="$sorted"/>
      </Request>
    </Body>
  </xsl:template>

</xsl:stylesheet>

使用该方法,结果是

<Root>
  <Body><Request><AIS /><Age /><BMI /><Something /></Request></Body>
</Root>

我稍微简化了代码

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:mf="urn:functions"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt"
                exclude-result-prefixes="msxsl exsl mf"
                xmlns:exsl="http://exslt.org/common"
>
  <msxsl:script implements-prefix="mf" language="C#">
    <![CDATA[

      public class OrdinalComparer : IComparer
      {
          public int Compare(object x, object y)
          {
              return string.CompareOrdinal((string)x, (string)y);
          }
      }

      public XPathNavigator OrdinalSort(XPathNavigator source)
      {
        var query = source.Compile("*");
        query.AddSort("local-name()", new OrdinalComparer());
        XPathNodeIterator result = source.Select(query);
        XmlDocument resultDoc = new XmlDocument();
        XmlDocumentFragment frag = resultDoc.CreateDocumentFragment();
        foreach (XPathNavigator item in result)
        {
          frag.AppendChild(resultDoc.ReadNode(item.ReadSubtree()));
        }
        return frag.CreateNavigator();
      }    
    ]]>
  </msxsl:script>

  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="Stuff">
    <Body>
      <Request>
        <xsl:variable name="sorted" select="mf:OrdinalSort(.)"/>
        <xsl:copy-of select="$sorted"/>
      </Request>
    </Body>
  </xsl:template>

</xsl:stylesheet>

必须在扩展函数C#&#34; script&#34;中构造XmlNode s。似乎是一个开销,但我不知道如何解决它。

答案 1 :(得分:0)

我为原始问题设计了一个 hideous 解决方法 - 这是为了使XSLT支持字符顺序排序。我认为这是一个的答案,但绝对不是一个好的答案。以下代码段说明了此解决方案:

<xsl:template match="Stuff">
    <xsl:element name="Body">
      <xsl:element name="Request">

        <xsl:variable name="source">
          <xsl:apply-templates select="*"/>
        </xsl:variable>

        <xsl:for-each select="exsl:node-set($source)/*">
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 1, 1))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 2, 2))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 3, 3))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 4, 4))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 5, 5))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 6, 6))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 7, 7))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 8, 8))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 9, 9))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 10, 10))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 11, 11))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 12, 12))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 13, 13))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 14, 14))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 15, 15))"/>
          <xsl:copy-of select="."/>
        </xsl:for-each>
      </xsl:element>
    </xsl:element>
  </xsl:template>

扩展函数GetOrdinal如下所示:

    public int GetOrdinal(string s)
    {
        return s.Length == 1 ? (char)s[0] : 0;
    }

这是非常可耻的,我不会主张做任何像这样粗制滥造的事情。但它有效