Building A Dynamic XPath Statement In XSLT

By leen3o on Dec 22 2009 | 3 Comments

I had the most frustrating problem recently when I was trying to extend Tim's locator package – I wanted to build up a dynamic XPath select statement based on QueryString values, for example if one value was present then select nodes based on the value but if another QueryString value was present select nodes based on the value of that.

So I tried what I thought was very simple..  I create a variable called $thesearch and build up the XPath statement within the variable like so

  1. <xsl:variable name="thesearch">
  2.   <xsl:text>/root//node [string-length(./data [@alias = 'distance']) &gt; 0 and number(./data [@alias = 'distance']) &lt; $radius</xsl:text>
  3.   <xsl:if test="string(umbraco.library:Request('area')) != '' ">
  4.     <xsl:text> and string(./data [@alias ='eventCounty']) = </xsl:text>
  5.     <xsl:text>'</xsl:text>
  6.     <xsl:value-of select="umbraco.library:Request('area')" />
  7.     <xsl:text>'</xsl:text>
  8.   </xsl:if>
  9.   <xsl:text>]</xsl:text>
  10. </xsl:variable>

And then just use the variable in my select statement

  1. <xsl:for-each select="$thesearch">

Yep simple… BUT… Nooooooooooooooo!!!!!!!!!!!  God damn XSLT…. Grrrrrrrr

According to XSLT this variable is a ‘String’ and NOT ‘XPath’ and the ‘String’ would have to be evaluated as valid XPath (Even if I take the completed string, put it directly in the foreach select and it works??!!)…  Anyway who am I to question this wonderful language (Said in a sarcastic tone).  As you might have guessed this flummoxed me slightly and after hours on the Umbraco forum and Googling various things, I turned to the XSLT Jedi (Doug) & his rather clever wife.  They created a nice little helper method in C# for me which I could insert directly into the XSLT file like so

Place the below after the closing template tag </xsl:stylesheet>

  1. <msxml:script language="C#" implements-prefix="ps">
  2. <![CDATA[
  3.     public string evaluateXpath(XPathNodeIterator nodes, string xpath){
  4.         /* this is intended for a SINGLE node to be passed in, although the type is XPathNodeIterator; if multiple
  5.         nodes are passed in, only the first one will be evaluated */
  6.         try{
  7.             while (nodes.MoveNext()){
  8.                 XPathNavigator n = nodes.Current as XPathNavigator;
  9.                 return n.Evaluate(xpath).ToString();
  10.             }
  11.         }catch{
  12.             return null;
  13.         }
  14.         return null;
  15.     }
  16. ]]>
  17. </msxml:script>

You need to make sure you have added a namespace in the top of the XSLT

  1. xmlns:ps="urn:anynamehere:xslt"

And then add the ‘ps’ to the ‘exclude-result-prefixes’ attribute like so

  1. exclude-result-prefixes="ps msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets"

Now we can use this method on our dynamic ‘String’ to turn it into valid ‘XPath’ like so

  1. <!-- ############  Build up the xpath portions
  2.     (note: any variables or umbraco.library functions or other extensions must be inserted with xsl:value-of)
  3.                         ############## -->
  4. <xsl:variable name="distanceXpath">
  5.     string-length(./data [@alias = 'distance']) &gt; 0
  6.     and number(./data [@alias = 'distance']) &lt; <xsl:value-of select="$radius"/>
  7.     <!-- just keep adding more conditions here to build up your xpath -->
  8. </xsl:variable>
  9. <!-- ############  End xpath portions ############## -->

Now we use the new method to process the xpath statement

  1. <!-- once you've got the xpath string, use ps:evaluateXpath and check that it returns
  2. 'True' to actually process the xpath statement -->
  3.    <xsl:variable name="searchResults" select="/root//node [ps:evaluateXpath(., $distanceXpath) = 'True']"/>

And finally we can use the final variable as you normally would

  1. <xsl:for-each select="$searchResults">
  2. </xsl:for-each>

In my final code I actually had to do a date compare so the method had to be modified to use date comparisons – But this will help anyone wanting to build up and dynamic XPath select statement

Post info

Tags:
Categories: Umbraco

Comments

Sean Mooney
Sean Mooney on 12/22/2009 9:46 PM You can also use the umbraco.library extension GetXmlNodeByXPath.

I have built a query much the same way as you, but then used to following to loop through:

<xsl:for-each select="umbraco.library:GetXmlNodeByXPath($xpath)/node">


ryan
ryan United States on 12/23/2009 12:20 AM Very nice solution! I have been battling with this problem for some time now and basically gave up and built it another way which is not dynamic. I will consider this next time!
Dan
Dan United Kingdom on 2/22/2010 10:11 AM Just had to say a big THANKS for this!

Dan