简体   繁体   English

XslCompiledTransform忽略XPathNodeIterator的顺序

[英]XslCompiledTransform ignores ordering of XPathNodeIterator

I have an XSLT stylesheet that consumes a document and outputs a SOAP message, where the body is in a specific format defined by a WCF data contract (not specified here). 我有一个XSLT样式表,该样式表使用文档并输出SOAP消息,其中主体采用WCF数据协定(此处未指定)定义的特定格式。 The issue is that WCF has a peculiar notion of what constitutes 'alphabetical' ordering and considers the following order to be correct: 问题在于,WCF对什么构成“字母”顺序具有奇特的概念,并认为以下顺序是正确的:

  • AC AC
  • Ab 抗体

This is because it uses ordinal string comparison internally. 这是因为它在内部使用序数字符串比较。 The details are not interesting, suffice to say that XSLT <sort> doesn't natively support this ordering but in order to transform an input document whose format may vary, into an acceptable SOAP message, the stylesheet must be able to order the output elements according to this peculiar ordering. 细节并不有趣,足以说XSLT <sort>本机不支持这种排序,但是为了将格式可能会变化的输入文档转换为可接受的SOAP消息,样式表必须能够对输出元素进行排序根据这个特殊的顺序。 I've therefore decided to implement node sorting in a script block. 因此,我决定在脚本块中实现节点排序。 This is part of a C# solution and uses XslCompiledTransform and therefore msxsl:script is available. 这是C#解决方案的一部分,并且使用XslCompiledTransform ,因此msxsl:script使用msxsl:script

Given a stylesheet: 给定一个样式表:

<?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>

And an input document: 和一个输入文件:

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

I would expect the output to order the innermost elements as follows: 我希望输出对最里面的元素进行排序,如下所示:

  • AIS AIS
  • Age 年龄
  • BMI BMI
  • Something 某物

This does not happen. 这不会发生。 Instead the elements are emitted in the order they went in. Debugging into the stylesheet as it executes I can see the OrdinalSort function is called, and the iterator it returns does enumerate the elements in the desired order, but the XSLT processor somehow ignores this and emits the elements in the order they were encountered. 而是按照它们进入的顺序发出元素。在执行样式时调试到样式表中,我可以看到调用了OrdinalSort函数,并且返回的迭代器确实按所需顺序枚举了元素,但是XSLT处理器以某种方式忽略了这一点,并且按照遇到的顺序发出元素。

I have verified additionally that parsing the document in a console application and running the same iterator query emits elements in the right order. 我还验证了在控制台应用程序中解析文档并运行相同的迭代器查询会以正确的顺序发出元素。

Why, and what can I do about it? 为什么,对此我该怎么办? The only hunch I have at the moment is that the XSLT engine is interpreting the parent Navigator of the iterator (which hasn't changed from what was passed into the sort function) as the element to reproduce, and ignoring the contents of the iterator. 我目前唯一的预感是XSLT引擎将迭代器的父导航器(与传递给sort函数的父导航器一样)解释为要重现的元素,而忽略了迭代器的内容。

I am not sure how to solve that with XPathNodeIterator or XPathNavigator , I went as far as creating an XPathNavigator[] from the XPathNodeIterator , to avoid any lazy evaluation effects, but somehow I always ended up with the same result as you. 我不确定如何使用XPathNodeIteratorXPathNavigator解决该问题,甚至可以避免从XPathNodeIterator创建XPathNavigator[] ,以避免任何懒惰的评估效果,但总而言之,我总会得到与您相同的结果。

So as an alternative I have written some code using the DOM implementation in the .NET framework to create some new nodes in the right sort order: 因此,作为替代方案,我使用.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>

Using that approach then the result is 使用该方法,结果是

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

I have slightly streamlined the code to 我已经稍微简化了代码

<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>

Having to construct XmlNode s in the extension function C# "script" seems like an overhead but I am not sure how to solve it otherwise. 必须在扩展功能C#“脚本”中构造XmlNode似乎是一项开销,但是我不确定如何解决。

I have devised a hideous workaround to the original problem - which was to make XSLT support character ordinal sorting. 我已经为原始问题设计了一个可怕的解决方法-使XSLT支持字符序数排序。 I consider this an answer, but definitely not a good one. 我认为这是一个答案,但绝对不是一个好答案。 The following snippet illustrates this solution: 以下代码段说明了此解决方案:

<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>

Where the extension function GetOrdinal is as follows: 其中扩展函数GetOrdinal如下:

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

This is quite frankly shameful, and I would not advocate ever doing anything as shoddy as this. 坦率地说,这是可耻的,我不主张做任何这样卑鄙的事情。 But it works . 但这有效

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM