Background Color:
 
Background Pattern:
Reset
Search
Home Recent Changes Show All Pages

Part 4. Transform Parameters

Not Rated Yet

This article is the 4th of a 5 part course in complex data-binding with namespaces, making choices, dynamic parameters and dynamic data.

 

Course Contents

 

Using XSLT to customise and abstract the content is easy with scryber. We can even go further to alter the output at will with parameters and XSLT arguments.

 

Article Contents

 

Changing the XSLT

 

The outcome of the Part 3. Choices and Conditions article was that we were hard coding business logic into our template, and was not a good thing. This article extends our solution to pass parameters back to the XSLT and generate complex styled output based upon these parameters.

Our previous XSLT generated a wrapper element, copying the data content into it, then extended the output to include some new calculated values. Now we want to have the sales values compared within the XSLT to target figures and break-even figures, and also give a flag to dictate if the totals should be generated in the report.

To do this we can create the parameters easily within the top level of the XSLT and we can now use these parameters within our xslt to generate the required output.

 

	<?xml version="1.0" encoding="utf-8"?>
	<xsl:stylesheet version="1.0"
	    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
	    xmlns:s="http://sample.xml.com/sales"
	    xmlns:env="http://sample.xml.com/sales/envelope"
	>
	  <xsl:output method="xml" indent="yes" standalone="yes" />
	  <xsl:param name="break-even">20000</xsl:param>
	  <xsl:param name="target">25000</xsl:param>
	  <xsl:param name="show-totals">Yes</xsl:param>



	  <xsl:template match="/" >
	    <env:wrapper>
	      <env:copy>
	        <xsl:apply-templates select="@* | node()"/>
	      </env:copy>
	     <!-- Check to see if we should output the totals -->
	     <xsl:if test="$show-totals = 'Yes'">
	        <env:totals>
	          <xsl:variable name="quarter-target" select="count(//s:region) * $target"/>
	          <xsl:call-template name="quarter" >
	            <xsl:with-param name="q" >1</xsl:with-param>
	          </xsl:call-template>
	          <xsl:call-template name="quarter" >
	            <xsl:with-param name="q" >2</xsl:with-param>
	          </xsl:call-template>
	          <xsl:call-template name="quarter" >
	            <xsl:with-param name="q" >3</xsl:with-param>
	          </xsl:call-template>
	          <xsl:call-template name="quarter" >
	            <xsl:with-param name="q" >4</xsl:with-param>
	          </xsl:call-template>
	        </env:totals>
	      </xsl:if>
	    </env:wrapper>
	  </xsl:template>

	  <xsl:template match="@* | node()" priority="1">
	    <xsl:copy>
	      <xsl:apply-templates select="@* | node()"/>
	    </xsl:copy>
	  </xsl:template>

		<!-- Match each of the region totals in the passed parameter and add a 'rank' 
	   attribute based on the region_sales value compared to our target and break-even parameters -->
	  <xsl:template match="s:region" priority="100" >
	    <xsl:variable name="region_sales" >
	      <xsl:value-of select="(s:quarter[@number='1']/@books_sold 
			+ s:quarter[@number='2']/@books_sold + s:quarter[@number='3']/@books_sold 
			+ s:quarter[@number='4']/@books_sold)" />
	    </xsl:variable>
	    <region books_sold="{$region_sales}" xmlns="http://sample.xml.com/sales" >
	      <xsl:attribute name="rank" >
	        <xsl:choose>
	          <xsl:when test="$region_sales &gt; ($target * 4)" >above</xsl:when>
	          <xsl:when test="$region_sales &lt; ($break-even * 4)" >loss</xsl:when>
	          <xsl:otherwise>below</xsl:otherwise>
	        </xsl:choose>
	      </xsl:attribute>
	      <xsl:apply-templates select="@* | node()"/>
	    </region>
	  </xsl:template>

		<!-- Match each of the quarters in the source and add a 'rank' attribute based on the 
		books_sold value compared to our target and break-even parameters -->
	  <xsl:template match="s:quarter" priority="100" >
	    <quarter number="{@number}" books_sold="{@books_sold}" xmlns="http://sample.xml.com/sales">
	      <xsl:attribute name="rank" >
	        <xsl:choose>
	          <xsl:when test="@books_sold &gt; $target" >above</xsl:when>
	          <xsl:when test="@books_sold &lt; $break-even" >loss</xsl:when>
	          <xsl:otherwise>below</xsl:otherwise>
	        </xsl:choose>
	      </xsl:attribute>
	    </quarter>
	  </xsl:template>

		<!-- Match each of the quarter totals in the passed parameter and add a 'rank' 
	   attribute based on the sum value compared to our target and break-even parameters -->
	  <xsl:template name="quarter" >
	    <xsl:param name="q" ></xsl:param>
	    <xsl:param name="target">10000</xsl:param>
	    <xsl:variable name="sum" >
	      <xsl:call-template name="sumProducts">
	        <xsl:with-param name="pNodes" select="s:sales/s:data/s:region/s:quarter[@number=$q]"/>
	      </xsl:call-template>
	    </xsl:variable>
	    <env:quarter>
	      <xsl:attribute name="number" >
	        <xsl:value-of select="$q"/>
	      </xsl:attribute>

	      <xsl:attribute name="books_sold">
	        <xsl:value-of select="$sum"/>
	      </xsl:attribute>
	      <xsl:attribute name="rank" >
	        <xsl:choose>
	          <xsl:when test="$sum &gt; $target" >above</xsl:when>
	          <xsl:when test="$sum &lt; $break-even" >loss</xsl:when>
	          <xsl:otherwise>below</xsl:otherwise>
	        </xsl:choose>
	      </xsl:attribute>
	    </env:quarter>
	  </xsl:template>

	  <xsl:template name="sumProducts">
	    <xsl:param name="pNodes" select="/.."/>
	    <xsl:param name="pAccum" select="0"/>

	    <xsl:choose>
	      <xsl:when test="not($pNodes)">
	        <xsl:value-of select="$pAccum"/>
	      </xsl:when>
	      <xsl:otherwise>
	        <xsl:call-template name="sumProducts">
	          <xsl:with-param name="pNodes" select="$pNodes[position() >1]"/>
	          <xsl:with-param name="pAccum" select=
	      "$pAccum + $pNodes[1]/@books_sold"/>
	        </xsl:call-template>
	      </xsl:otherwise>
	    </xsl:choose>
	  </xsl:template>

	</xsl:stylesheet>

 

If we execute this transform against our source sales data we should get out a new xml document with the following structure

 

	<?xml version="1.0" encoding="utf-8" standalone="yes"?>
	<env:wrapper xmlns:env="http://sample.xml.com/sales/envelope" xmlns:s="http://sample.xml.com/sales">
	  <env:copy>
	    <sales xmlns="http://sample.xml.com/sales">
	  	<summary>
		    <heading>Lucerne Publishing</heading>
		    <subhead>Regional Sales Report</subhead>
		    <description>Sales report for the West Coast, Central and East Coast regions.</description>
		  </summary>
		  <data>
		    <region books_sold="127630" rank="above">
		      <name>West Coast</name>
		      <quarter number="1" books_sold="24000" rank="below" />
		      <quarter number="2" books_sold="38600" rank="above" />
		      <quarter number="3" books_sold="44030" rank="above" />
		      <quarter number="4" books_sold="21000" rank="below" />
		    </region>
		    <region books_sold="81080" rank="below">
		      <name>Central</name>
		      <quarter number="1" books_sold="11000" rank="loss" />
		      <quarter number="2" books_sold="16080" rank="loss" />
		      <quarter number="3" books_sold="25000" rank="below" />
		      <quarter number="4" books_sold="29000" rank="above" />
		    </region>
		    <region books_sold="128500" rank="above">
		      <name>East Coast</name>
		      <quarter number="1" books_sold="27000" rank="above" />
		      <quarter number="2" books_sold="31400" rank="above" />
		      <quarter number="3" books_sold="40100" rank="above" />
		      <quarter number="4" books_sold="30000" rank="above" />
		    </region>
		  </data>
		</sales>
	  </env:copy>
	  <env:totals>
	    <env:quarter number="1" books_sold="62000" rank="below" />
	    <env:quarter number="2" books_sold="86080" rank="above" />
	    <env:quarter number="3" books_sold="109130" rank="above" />
	    <env:quarter number="4" books_sold="80000" rank="above" />
	  </env:totals>
	</env:wrapper>

 

Adding the parameters

 

We could use this xslt as is, and modify the parameters within the file as required. But this is not very dynamic and would not support changes very well. The XMLDataSource helps in this case, as we can pass parameters to the XSLT from within the the template.

    <data:XMLDataSource id="MyStyledSalesData" source-path="./Data/Sales.xml">
      <Transform path="./Data/SalesToTotalsWithRange.xslt" >
        <data:XsltArgument name="show-totals" value="No"/>
        <data:XsltItemArgument item-name="break-even" name="break-even" default-value="20000" />
      </Transform>
      <Namespaces>
        <data:Xmlns prefix="s" namespace="http://sample.xml.com/sales"/>
        <data:Xmlns prefix="e" namespace="http://sample.xml.com/sales/envelope"/>
      </Namespaces>
    </data:XMLDataSource>

 

The first one is an explicit argument that is always sent across for the 'show-totals' param, and in this case we are changing the default value to No (so the totals will not be generated in the transformed output)

The second argument is an XsltItemArgument. And the value will be taken from the PDFDocument Items collection that will always be available within any context and is the same instance throughout the life cycle of the document - Item and Query String binding. Because the Item argument has a default value, this will be used and passed to the transform, if no value is present in the items collection.
@item-name and @name are both required, with the name referring to the name of the parameter in the transform.

Just for explanation, we have not declared a parameter for the $target parameter, so it will use the default value set in the template. We can add it later if required.

Having these two parameters in place we can modify our document template to use the new transformed output

 

    <pdf:Page id="MyStyledOutput" outline-title="Styled" >
      <Content>

        <data:ForEach select="e:wrapper" datasource-id="MyStyledSalesData"  >
          <Template>

            <!-- Separate template for the env:copy/s:sales/s:summary-->
            <data:ForEach select="e:copy/s:sales/s:summary" >
              <Template>
                <pdf:Div id="titles" style:margins="10pt">
                  <pdf:H1 id="heading" text="{xpath:s:heading}" />
                  <pdf:H3 id="subhead" text="{xpath:s:subhead}" />
                  <pdf:Div id="desc" >
                    <pdf:Label text="{xpath:s:description}" />
                  </pdf:Div>
                </pdf:Div>
              </Template>
            </data:ForEach>

            <pdf:Table id="salesgrid" style:full-width="true" style:font-size="14pt" style:margins="10pt" >
              <pdf:Header-Row >
                <pdf:Header-Cell>Region</pdf:Header-Cell>
                <pdf:Header-Cell> Q1</pdf:Header-Cell>
                <pdf:Header-Cell> Q2</pdf:Header-Cell>
                <pdf:Header-Cell> Q3</pdf:Header-Cell>
                <pdf:Header-Cell> Q4</pdf:Header-Cell>
                <pdf:Header-Cell>Total</pdf:Header-Cell>
              </pdf:Header-Row>

              <!-- All region rows in sales -->
              <data:ForEach select="e:copy/s:sales/s:data/s:region" >
                <Template>
                  <pdf:Row>
                    <pdf:Cell>
                      <pdf:Label text="{xpath:s:name}" />
                    </pdf:Cell>
                    <data:ForEach select="s:quarter" >
                      <Template>
                        <!-- concatenate the rank with a class name -->
                        <pdf:Cell style:class="{xpath:concat('sales-',@rank)}">
                          <pdf:Number value="{xpath:@books_sold}" style:number-format="C" />
                        </pdf:Cell>
                      </Template>
                    </data:ForEach>
                    <pdf:Cell style:class="{xpath:concat('sales-',@rank)}">
                      <pdf:Number value="{xpath:(@books_sold)}" style:number-format="C" />
                    </pdf:Cell>
                  </pdf:Row>
                </Template>
              </data:ForEach>

              <!-- footer row from env:totals 
              will not be generated if the element is not present-->
              <data:ForEach select="e:totals" >
                <Template>
                  <pdf:Footer-Row style:font-bold="true">
                    <pdf:Footer-Cell>
                      Total
                    </pdf:Footer-Cell>
                    <data:ForEach select="e:quarter" >
                      <Template>
                        <pdf:Cell style:class="{xpath:concat('sales-',@rank)}">
                          <pdf:Number value="{xpath:@books_sold}" style:number-format="C" />
                        </pdf:Cell>
                      </Template>
                    </data:ForEach>
                    <pdf:Cell>
                      <pdf:Number value="{xpath:(e:quarter[@number='1']/@books_sold 
                                          + e:quarter[@number='2']/@books_sold 
                                          + e:quarter[@number='3']/@books_sold 
                                          + e:quarter[@number='4']/@books_sold)}" style:number-format="C" />
                    </pdf:Cell>
                  </pdf:Footer-Row>
                </Template>
              </data:ForEach>

            </pdf:Table>
          </Template>
        </data:ForEach>
      </Content>
    </pdf:Page>

 

Our template is now looking much cleaner again, and we can see the concatenation of our @style:class attribute to include the rank values that are returned from the transformation output - based on the parameters that we are putting in.
A nice controllable loop, so all we need to do is add some matching style information that will be picked up at layout and render time to alter the appearance.

 

<pdf:Document xmlns:pdf="Scryber.Components, Scryber.Components, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"
          xmlns:style="Scryber.Styles, Scryber.Styles, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"
          xmlns:data="Scryber.Data, Scryber.Components, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"
          auto-bind="true">
  <Viewer page-display="Outlines"/>
  <Styles>
    <style:Style applied-type="pdf:Cell" >
      <style:Padding all="2pt"/>
      <style:Border style="Solid" color="gray" />
    </style:Style>

    <style:Style applied-class="sales-loss" applied-type="pdf:Cell" >
      <style:Background color="#FFAAAA"/>
    </style:Style>

    <style:Style applied-class="sales-below" applied-type="pdf:Cell" >
      <style:Background color="#AAAAAA"/>
    </style:Style>

    <style:Style applied-class="sales-above" applied-type="pdf:Cell" >
      <style:Background color="#AAFFAA"/>
    </style:Style>

  </Styles>
  <Pages>
  .
  .
  .

 

Building the Document

 

So how does it look? What's the output like? Does it work? (we wouldn't write an article if it didn't)

 


 

What's next

 

In our final article Part 5. Assigning Dynamic Data we can use the Item parameters to dynamically alter the output at runtime, and also load the data dynamically

 

See Also.

 



  Rating
Rate This Page: Poor Great   |  Rate Content |
Average rating:  No Ratings Yet   
Number of Ratings : 0
  Comments
Add Comment
No Comments Yet