【问题标题】:XslCompiledTransform ignores ordering of XPathNodeIteratorXslCompiledTransform 忽略 XPathNodeIterator 的排序
【发布时间】:2015-03-20 20:32:05
【问题描述】:

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

  • 交流
  • 抗体

这是因为它在内部使用了序号字符串比较。细节并不有趣,可以说 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
  • 年龄
  • 体重指数
  • 某事

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

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

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

【问题讨论】:

    标签: c# xml visual-studio xslt soap


    【解决方案1】:

    我不知道如何用XPathNodeIteratorXPathNavigator 解决这个问题,我甚至从XPathNodeIterator 创建了一个XPathNavigator[],以避免任何懒惰的评估效果,但不知何故我总是以结果和你一样。

    因此,作为替代方案,我使用 .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#“脚本”中构造 XmlNodes 似乎是一种开销,但我不知道如何解决它。

    【讨论】:

      【解决方案2】:

      我已经为原始问题设计了一个可怕 解决方法 - 即让 XSLT 支持字符序号排序。我认为这个一个答案,但绝对不是一个好的答案。下面的 sn-p 说明了这个解决方案:

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

      坦率地说,这很可耻,我不提倡做任何像这样粗制滥造的事情。但它有效

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-01-23
        • 2011-10-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-05-26
        • 2018-06-28
        相关资源
        最近更新 更多