XSL named parameter 'with-param' using 'apply-templates'

KevenK picture KevenK · Nov 18, 2010 · Viewed 41.9k times · Source

My questions are at the bottom of this post, if you wish to read them before the full explanation.

I'm converting an XML document to a pretty web page using XSL, and am having trouble with correctly passing a variable. I have many xsl:templates defined, and need to pass a specific parameter to just one of them. I was hoping that I would be able to pass a named parameter that would presumably be sent to all of the xsl:templates, but only be used by a single one and ignored by the others. However, when trying to test this for myself (and my limited understanding of XSL), I was unable to pass the parameter at all, let alone test if it was accidentally disturbing any other xsl:templates.

The following is simplified example code (typed up for this question, it may contain a typo or two). I have many many different xsl:templates defined to deal with nodes in the XML, and everything has been working fine until now. It is in adding a parameter to these templates that I appear to be having issues.

XML file:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="main.xsl"?>
<wrapperNode>
  <testNode>
    <subNode/>
  </testNode>
</wrapperNode>

main.xsl:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:import href="test.xsl"/>
<xsl:output method="html" indent="yes"/>

<xsl:template match="/">

<html>
  <body>
      <xsl:apply-templates>
        <xsl:with-param name="testParam">TEST_PARAMETER</xsl:with-param>
      </xsl:apply-templates>
  </body>
</html>

</xsl:template>
</xsl:stylesheet>

test.xsl:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="testNode">
  <xsl:param name="testParam" />
  TEST1
  <xsl:value-of select="$testParam" />
  TEST2
</xsl:template>
</xsl:stylesheet>

Output (actual):

TEST1 TEST2

Output (expected/desired):

TEST1 TEST_PARAMETER TEST2

My questions in regards to this:

  1. Is it possible to send a named parameter to all of my xsl:templates using an xsl:apply-templates with xsl:with-param, but select this value specifically by name= within the actual template so that it can be explicitly used in a single template and ignored by all others (even if I wanted to add other, differently named, parameters for other templates later)?

  2. What am I doing wrong with my current sample code that it does not seem to receive the parameter at all?

  3. Is there a better way to accomplish this?

Edit: I want to make it clear that due to other output within the test.xsl:testNode template, I know for sure that it IS being successfully called. It is ONLY the parameter part that is not working. I do not mean to waste people's time figuring out why that template is not being called. It is.

Update: In response to the answers I initially received, which pointed out that the example I made up was not completely correct (my mistake) and did not very clearly show the issue (ie: that the correct template is being called, but that only the parameter appears to not be working), I have replaced the examples with much better ones. This example more clearly shows that the testNode template is successfully being called, but that the parameter does not seem to be passed. I have tested this numerous times, before and after consideration of the previous answers to this question. I am absolutely stumped, as everything appears to be correct from what I have read elsewhere and what people have suggested so far.

Answer

Dimitre Novatchev picture Dimitre Novatchev · Nov 19, 2010

My questions in regards to this:

  1. Is it possible to send a named parameter to all of my xsl:templates using an xsl:apply-templates with xsl:with-param, but select this value specifically by name= within the actual template so that it can be explicitly used in a single template and ignored by all others (even if I wanted to add other, differently named, parameters for other templates later)?

Yes. In XSLT 2.0 one may use the so called "tunnel parameters", but in XSLT 1.0 this is the way to have some parameters reach some remote template down the chain.

Another way is to have global parameters, so that they wouldn't have to be passed through every template in the chain.

.2. What am I doing wrong with my current sample code that it does not seem to receive the parameter at all?

The reason is in the code you haven't shown to us. Or maybe the source XML document you have in your real case isn't the same as the one provided here. I ran the provided code and I couldn't repro the problem at all -- the desired output is produced.

My guess is that some other template is selected before the template that matches testNode. This template doesn't know anything about the passed parameter and it doesn't pass it to the templates that it, on its turn, applies. Thus the parameter is not passed at all to the template matching testNode.

My guess is that if you replace:

  <xsl:apply-templates> 
    <xsl:with-param name="testParam">TEST_PARAMETER</xsl:with-param> 
  </xsl:apply-templates> 

with

  <xsl:apply-templates select="testNode"> 
    <xsl:with-param name="testParam">TEST_PARAMETER</xsl:with-param> 
  </xsl:apply-templates> 

you could get the desired output.

Also, you could trace with an XSLT debugger (such as the one in Visual Studio) and see exactly which template is selected.

.3. Is there a better way to accomplish this?

As I said earlier, global parameters can be used as alternative -- I am not sure that this is better, though.

Finally, here is the code that I ran that cannot repro your problem:

XSLT stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes"/>

 <xsl:template match="/">
      This is text1
      <xsl:apply-templates>
        <xsl:with-param name="testParam">TEST_PARAMETER</xsl:with-param>
      </xsl:apply-templates>
      This is text2
 </xsl:template>

 <xsl:template match="testNode">
  <xsl:param name="testParam" />
  <xsl:value-of select="$testParam" />
 </xsl:template>
</xsl:stylesheet>

XML document:

<?xml-stylesheet type="text/xsl" href="main.xsl"?>
<testNode>
  <subNode/>
</testNode>

Result:

  This is text1
  TEST_PARAMETER
  This is text2

UPDATE:

The OP has provided more accurate information which prooves my guess.

Now it is obvious that the problem is caused by allowing the XSLT built-in template for element node to be selected for wrapperNode. This template, naturally, doesn't know about any parameters and it doesn't use the testParam parameter nor does it pass this parameter through. Thus, the <xsl:apply-templates/> in the built-in template causes the template matching testNode to be selected without passing any parameter to it. THis explains the reported behavior/result.

Solution: The solution is to specify a template matching wrapperNode that accepts a parameter named testParam and passes it through when it applies templates:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="html" indent="yes"/>

 <xsl:template match="/">
  <html>
    <body>
        <xsl:apply-templates>
          <xsl:with-param name="testParam" select="'TEST_PARAMETER'"/>
        </xsl:apply-templates>
    </body>
  </html>
 </xsl:template>

 <xsl:template match="testNode">
  <xsl:param name="testParam" />
  TEST1
  <xsl:value-of select="$testParam" />
  TEST2
 </xsl:template>

 <xsl:template match="wrapperNode">
  <xsl:param name="testParam" />

  <xsl:apply-templates>
   <xsl:with-param name="testParam" select="$testParam"/>
  </xsl:apply-templates>
 </xsl:template>
</xsl:stylesheet>

Now when this transformation is applied on the provided XML document, the expected result is produced:

<html>
<body>
  TEST1
  TEST_PARAMETER
  TEST2
 </body>
</html>