tags:

views:

86

answers:

4

I'm working on rebuilding the UI portion of our website which is all heavily javascript/ajax based (for no good reason and in a fairly inefficient way) such that the backend will now do most of the content generation. It is a C# .net application.

Almost all of our pages (which there are probably 40-50 pages) have the same basic layout. I am brand new to XSLT but I've done a lot of work with MVC frameworks such as Spring (java, using Sitemesh for layout), Symfony (PHP), a bit of rails as well as a few others. I love having the ability to have one or several common templates and then have a specific "content" section where the page specific stuff goes. I can't figure out how this is done with XSLT. In the case of this application I have a value available to me in the xml backing the xslt page, lets call it ContentXSL, who's value is the name of the xsl file I want to use for the content section of the page. I know it's not possible but it would be nice to use:

 <xsl:call-template name="{$ContentXSL}" />

Then I could simply put that in the content section.. However this isn't possible, so instead I will need a massive choose statement that calls the correct template based on the ContentPage variable.. This also means in my Layout.xsl file I would have to include all 40-50 xsl documents.. I would think the overhead would be pretty big, but I'm not sure about that. Is this a reasonable thing to do if the site gets a lot of traffic?

What are other common ways of doing this? It seems like most modern frameworks allow you to use this pattern to decorate content. In the case of Symfony it worked really well and was pretty flexibile (with slots and all that).

I know the other potential solution is to have 40 independant files that all have similar markup and include special sections like the header and footer. This means if I want to change the overall structure of the layout of my site I'd have to edit all 40-50 pages though (very annoying).

Update -- More Explanation

I want to further explain this because I have certain requirements which would take considerable engineering to change. First of all, the backend is going to pass to me some xml which will let me know of query args are in the URL of the website.. Also, it will pass to me the data I need to build my page (data in the form of business data, no html or anything like that). The data looks similar to this:

<xml>
 <section>Blogs</section>
 <page>showAll</section>
  <data>
   <blogs>
     <blog>
       <author>somebody</author>
       <title></title>
       <content>..</content>
    </blog>
    </blog>..</blog>
   </blogs>    
  </data>
</xml>

Now what want is to have a page template like this:

<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns:msxsl='urn:schemas-microsoft-com:xslt'>  
 <xsl:output omit-xml-declaration='yes' method='html' media-type='text/html' indent='yes' />
 <xsl:include href="Header.xsl"/>
 <xsl:include href="Nav.xsl"/> 
 <xsl:template name='MainLayout' match='*'>
 <html>
  <head>
   <title></title>
  </head>
  <body>
    <div id="header"><xsl:call-template name="Header" /></div>
    <div id="nav"><xsl:call-template name="Nav" /></div>
    <div id="content">
      [here is where i want to use the xsl from {/xml/section}/{/xml/page}.xsl]
    </div>
  </body>
</html>
</xsl:template>    
</xsl:stylesheet>

Now in for the content of this page I would have the following file: Blogs/showAll.xsl

<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns:msxsl='urn:schemas-microsoft-com:xslt'>  
      <xsl:output omit-xml-declaration='yes' method='html' media-type='text/html' indent='yes' />
<xsl:template name='Blogs_ShowAll'>
  <div id="blogs-showAll">
   ..iterate over /xml/data/blogs converting to html
  </div>
</xsl:template>
</xsl:stylesheet>

The solutions so far have been good but only one of them was I able to fully digest (the one which mentions including all the xsl files and using a xsl:choose to select the right one). I am not sure how to apply the FXSL method to the problem at hand. Note that I would not be opposed to using a sitemesh type approach which I specify the html/body tags and all that in the child and have it replace what I have in the body section of the child into the layout's content div (also, if there is a title tag in the child replace the title tag in the layout -- stuff like that).

+3  A: 
 <xsl:call-template name="{$ContentXSL}" />

While this is syntactically illegal in all versions of XSLT, using XSLT templates as higher-order functions has been implemented and used in the FXSL library since ten years ago.

Here is a somewhat simplified idea of how this can be achieved:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output method="text"/>

 <xsl:param name="pFunction1">
   <fun name="increment"/>
 </xsl:param>

 <xsl:param name="pFunction2">
   <fun name="double"/>
 </xsl:param>

 <xsl:variable name="vFunIncrement" select=
 "document('')/*/xsl:param[@name='pFunction1']/*"/>

 <xsl:variable name="vFunDouble" select=
 "document('')/*/xsl:param[@name='pFunction2']/*"/>

 <xsl:variable name="vInput" select="."/>

 <xsl:template match="/">
  increment(<xsl:value-of select="$vInput"/>) = <xsl:text/>

  <xsl:apply-templates select="$vFunIncrement">
    <xsl:with-param name="parg1" select="$vInput"/>
  </xsl:apply-templates>

  double(<xsl:value-of select="$vInput"/>) = <xsl:text/>

  <xsl:apply-templates select="$vFunDouble">
    <xsl:with-param name="parg1" select="$vInput"/>
  </xsl:apply-templates>
 </xsl:template>

 <xsl:template match="fun[@name='double']">
  <xsl:param name="parg1"/>

  <xsl:value-of select="2*$parg1"/>
 </xsl:template>

 <xsl:template match="fun[@name='increment']">
  <xsl:param name="parg1"/>

  <xsl:value-of select="$parg1+1"/>
 </xsl:template>
</xsl:stylesheet>

when this transformation is applied on the following XML document:

<num>2</num>

the result is:

  increment(2) = 3

  double(2) = 4

Do note:

  1. The <fun> elements can be passed externally to the transformation through global-level parameters. This means that the transformation doesn't know what functions will be executed.

  2. Functions are simulated by templates matching fun elements that have a specific value for their name attribute.

In case if you want to read and understand FXSL, these are the two best materials:

  1. FXSL 1.x (for XSLT 1.0)
  2. FXSL 2.0 (for XSLT 2.0)
Dimitre Novatchev
This is interesting but I'm still not sure how I would use it to dynamically include another xslt stylesheet.. Would I make a function that includes another template and call that? Also how easy is it to use FXSL with XSLT? Do i just include a dll into my project and it will work?
Matt Wolfe
@Matt-Wolfe: I am not recommending at all to attempt to dynamically include a stylesheet module. What I have briefly shown is much more powerful -- this allows you to use templates as functions and to pass functions/templates dynamically as parameters. Also, to return dynamically functions/templates as the result of other functions/templates. This is exactly what you were trying to achieve with `<xsl:call-template name="{$ContentXSL}" />`.
Dimitre Novatchev
@Matt-Wolfe: [How easy is it to use FXSL with XSLT?]: You just import the FXSL stylesheet modules that you need, then call one of its templates -- or just use the same implementation idea that FXSL uses. If you download FXSL 1.x (for EXSLT), you can run any test file with XslCompiledTransform or directly in Visual Studio.
Dimitre Novatchev
@Dimitre: Wow! 10 years for FXSL! +1
Alejandro
I've updated my original post. Can you give me some clarification of how this would apply to what I am trying to do, I still don't get it. Also I don't see where you are importing the FXSL stylesheets into your document, if that is the case why do I even need those. I'm not trying to do math with this is basic layout/template stuff..
Matt Wolfe
@Matt-Wolfe: I have now written a separate answer that targets your updated problem definition. This solves exactly your problem and is based on the same ideas as FXSL -- using the power of `<xsl:template>` and `<xsl:apply-templates>` to the maximum. You need to understand these ideas in order to be able to utilize them in your projects.
Dimitre Novatchev
A: 

Dimitre's example is good..

Here is a way to do this also.. a little ugly solution but does the trick

primary.xsl

<xsl:variable name="ContentXSL" select="/your/xml/settings/@content" />

<!-- Reference templates -->
<xsl:include href="template1.xsl" />
<xsl:include href="template2.xsl" />
<xsl:include href="template3.xsl" />
<xsl:include href="template4.xsl" />

<xsl:template match="/">
  <html>
    <head>
      <title>..</title>
    </head>
  </html>
  <body>
    <xsl:call-template name="getcontent" />
  </body>
</xsl:template>

<xsl:template name="getcontent">
  <xsl:choose>
    <xsl:when test="$ContentXSL = 'template1'">
      <xsl:apply-templates match="/your/xml/structure" mode="template1" />
    </xsl:when>
    <xsl:when test="$ContentXSL = 'template2'">
      <xsl:apply-templates match="/your/xml/structure" mode="template2" />
    </xsl:when>
    <xsl:when test="$ContentXSL = 'template3'">
      <xsl:apply-templates match="/your/xml/structure" mode="template3" />
    </xsl:when>
    <xsl:when test="$ContentXSL = 'template4'">
      <xsl:apply-templates match="/your/xml/structure" mode="template4" />
    </xsl:when>
    <xsl:otherwise>
      <!-- Default template? -->
      <xsl:apply-templates match="/your/xml/structure" mode="template1" />
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

template1.xsl

<xsl:template match="/your/xml/structure" mode="template1">
  Template 1<br />
</xsl:template>

template2.xsl

<xsl:template match="/your/xml/structure" mode="template2">
  Template 2<br />
</xsl:template>

template3.xsl

<xsl:template match="/your/xml/structure" mode="template3">
  Template 3<br />
</xsl:template>

template4.xsl

<xsl:template match="/your/xml/structure" mode="template4">
  Template 4<br />
</xsl:template>
This is exactly what I am doing right now because it was the only way I could figure out how to do it. My main issue would be scalability.. If i have say 100 child pages how much overhead will doing the xsl:include for all of those be.. The added markup is annoying but not a dealbreaker, performance could be a dealbreaker though if it doesn't scale
Matt Wolfe
Actually this is slightly different than what I'm doing but achieves the same thing.. Instead of using xsl:apply-templates i'm just calling the template by name using xsl:call-template. Those templates are named and do not have a match element so it achieves the same thing.. Not sure if there is a benefit of the above solution
Matt Wolfe
A: 

Besides excellent Dimitre's answer recommending a method to implement higher-order functions, you could also use a method with master pages and child pages combining with some sort of code behind, like this:

MasterContent.xml:

<title>Test for XSLT</title>

MasterLayout.xml:

<html>
    <head>
        <title></title>
    </head>
    <body>
        <p>This is master page</p>
    </body>
</html>

Master.xsl:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:param name="pMasterLayout" select="document('MasterLayout.xml')"/>
    <xsl:param name="pMasterContent" select="document('MasterContent.xml')"/>
    <xsl:output method="xml"/>
    <xsl:template match="/">
        <xsl:apply-templates select="$pMasterLayout/*"/>
    </xsl:template>
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="title">
        <xsl:copy>
            <xsl:value-of select="$pMasterContent/title"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

ChildLayout:

<html>
    <head>
        <title></title>
    </head>
    <body>
        <h1></h1>
    </body>
</html>

So, this transformation ("Child.xsl"):

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:include href="Master.xsl"/>
    <xsl:param name="pChildLayout" select="document('ChildLayout.xml')"/>
    <xsl:param name="pChildContent" select="/"/>
    <xsl:template match="body">
        <xsl:copy>
            <xsl:apply-templates select="$pChildLayout/html/body/*"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="h1">
        <xsl:copy>
            <xsl:value-of select="$pChildContent/header"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

With this input ("ChildContent"):

<header>Child Content</header>

Output:

<html>
    <head>
        <title>Test for XSLT</title>
    </head>
    <body>
        <h1>Child Content</h1>
    </body>
</html>

Note:

Check a better living example at aranedabienesraices.com.ar I recommend to use @id as anchors to populate the layout with content (you can strip those with empty templates). This method does not tie you to any vendor IDE (with notion of XSLT) to built your layout pages.

Alejandro
Hmm this is interesting. Reminds me a bit of sitemesh. Sitemesh though takes care of doing the replacements with minimal setup and this requires me to figure out how/where I will allow replacements.. I certainly do like this idea though, it could potentially be much more flexible than my original method since my master template could be more flexible.
Matt Wolfe
+1  A: 

The OP has provided additional details of his problem and this answer provides the additional solution that is now requested.

I. The idea:

This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:template match="/">
  <html>
   <xsl:apply-templates select="*/page"/>
  </html>
 </xsl:template>

 <xsl:template match="page[. = 'showAll']">
   <!-- Transform all data to html -->
   <xsl:apply-templates select="../*/blogs" mode="showAll"/>
 </xsl:template>

 <xsl:template match="page[. = 'showBrief']">
   <!-- Transform the data to Summary html -->
      <xsl:apply-templates select="../*/blogs" mode="showBrief"/>
 </xsl:template>

 <xsl:template match="blogs" mode="showAll">
  <h1>All Blogs: </h1>
  <table border="1">
    <xsl:apply-templates mode="showAll"/>
  </table>
 </xsl:template>

 <xsl:template match="blog" mode="showAll">
  <tr>
    <td>Blog of <xsl:value-of select="author"/></td>
    <td><xsl:value-of select="title"/></td>
  </tr>
  <tr>
    <td colspan="2"><xsl:apply-templates select="content/node()" mode="showAll"/></td>
  </tr>
  <xsl:if test="not(position()=last())">
   <tr><td colspan="2">&#xA0;</td></tr>
  </xsl:if>
 </xsl:template>

  <xsl:template match="blogs" mode="showBrief">
  <h1>Blogs Summary: </h1>
  <table border="1">
    <xsl:apply-templates mode="showBrief"/>
  </table>
  </xsl:template>

   <xsl:template match="blog" mode="showBrief">
     <tr>
      <td>
        <xsl:value-of select="concat(author, ': ', title)"/>
      </td>
     </tr>
   </xsl:template>

</xsl:stylesheet>

when applied on this XML document (based on the provided XML text, but making it well-formed and more substantial):

<xml>
 <section>Blogs</section>
 <page>showAll</page>
 <data>
   <blogs>
     <blog>
       <author>John Smith</author>
       <title>All about golden fish</title>
       <content>
       Here I publish my latest achievements
       in raising golden fish.
       </content>
    </blog>
     <blog>
       <author>Mary Jones</author>
       <title>Knitting, Knitting, Knitting</title>
       <content>
       How to knit a sharf.
       </content>
    </blog>
   </blogs>
 </data>
</xml>

produces the desired "show-all" type of output:

<html>
   <h1>All Blogs: </h1>
   <table border="1">
      <tr>
         <td>Blog of John Smith</td>
         <td>All about golden fish</td>
      </tr>
      <tr>
         <td colspan="2">
            Here I publish my latest achievements
                   in raising golden fish.

         </td>
      </tr>
      <tr>
         <td colspan="2">&nbsp;</td>
      </tr>
      <tr>
         <td>Blog of Mary Jones</td>
         <td>Knitting, Knitting, Knitting</td>
      </tr>
      <tr>
         <td colspan="2">
                   How to knit a sharf.

         </td>
      </tr>
   </table>
</html>

Now we change the XML document and replace the page element with this one:

 <page>showBrief</page>

When the same transformation is applied on the updated XML document, it now produces the desired summary output:

<html>
   <h1>Blogs Summary: </h1>
   <table border="1">
      <tr>
         <td>John Smith: All about golden fish</td>
      </tr>
      <tr>
         <td>Mary Jones: Knitting, Knitting, Knitting</td>
      </tr>
   </table>
</html>

II. The next step

In practice all templates in a given mode will be in their separate xsl file and will be imported by the primary stylesheet:

The transformation (primary stylesheet) thus becomes:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:import href="showAll.xsl"/>
 <xsl:import href="showBrief.xsl"/>

 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:template match="/">
  <html>
   <xsl:apply-templates select="*/page"/>
  </html>
 </xsl:template>
</xsl:stylesheet>

Do note:

  1. The transformation doesnt know in advance what templates will be applied -- the transformation is completely data-driven.

  2. Templates that do not exist now can be written in the future and will be applied without the need to change the primary stylesheet.

  3. There is no conditional logic, <xsl:choose> instructions etc. This is the true power of xsl templates in action.

  4. This transformation is based on the same idea on which FXSL is based on.

Dimitre Novatchev
Question, what does <xsl:apply-templates select="content/node()" mode="showAll"/>do in the above code.. I would think this essentially prints out the content but does using it in xs:apply-templates rather than a xsl:value of have any different behavior?
Matt Wolfe
@Matt-Wolfe: The idea is that the `<content>` node may have markup in its body and there might be other templates that match these markup nodes and transform them in some way, that we are not even aware of. This is *very different* from `<xsl:value-of select="content"/>` -- this will just output the concatenation of all text-node descendents of `<content>`.
Dimitre Novatchev