Delphi Clinic C++Builder Gate Training & Consultancy Delphi Notes Weblog Dr.Bob's Webshop
Dr.Bob's Delphi Notes Dr.Bob's Delphi Clinics Dr.Bob's Delphi Courseware Manuals
 Dr.Bob Examines... #29
See Also: other Dr.Bob Examines columns or Delphi articles

WebSnap XSLPageProducer
The DataSnap and specifically ClientDataSet components in Delphi and Kylix can represent their data content in the XML format. But how can I convert this XML format to another format using XSL? And how do I write XSL in the first place?
In this article I will show how we can use a new component in Delphi 6 and Kylix 2, called the TXSLPageProducer, to convert any XML data packet or document (using XSL Transformation rules). I will also show where to get a few XSLT examples that are a bit "hidden" in Delphi 6.

WebSnap XSLPageProducer
An XML document contains content as well as definition, but little information on how the XML document should be displayed (for example inside a browser). We can use an XML EXtensible Stylesheet Language (XSL) template or transformation file in order to transform an XML document to another (usually HTML compliant) XML document, which we'll do in this article using the XSLPageProducer component from the WebSnap tab of Delphi 6 or Kylix 2 Enterprise.

WebSnap
Web Server applications can be build in different ways. Delphi always had the WebBroker Technology, which was extended with InternetExpress in Delphi 5 and with WebSnap in Delphi 6. The WebSnap tab of the component palette contains the XSLPageProducer component, although I will show in this section that we can even use this component in a "normal" WebBroker environment - without WebSnap.

ClientDataSets
Let's first build a data module that actually contains some XML data packets. And while we're at it, let's make it a bit more useful than a single data packet - let's make it a master-detail (with customer records and order records in a nested dataset). I assume you've started Delphi 6 with a Web Server application. The empty web module is in fact just a special data module, and we'll use it at first as container for two tables. Since I want Kylix 2 developers to be able to follow us along for a while, we'll be using two TClientDataSet components from the Data Access tab of the component palette. Name them cdsCustomer and cdsOrders, and assign their filename properties to resp. customer.cds and orders.cds (which can be found in the C:\Program Files\Common Files\Borland Shared\Data directory). Both datasets have quite a number of fields, but I don't want to see them all, so I right-click on each of the ClientDataSets and only select the few most important fields (that's CustNo, Company, Addr1, Addr2, City and Country for cdsCustomer and OrderNo, CustNo, and ItemsTotal for cdsOrders).

Master Detail
Anyway, In order to create a master-detail relationship between the cdsCustomer and cdsOrders, you must add a TDataSource component, assign its DataSet property to cdsCustomer, and then assign the MasterSource property of cdsOrders to this DataSource component. Now click on the ellipsis for the MasterFields property of cdsOrders and use the Field Link Designer to make sure that the CustNo fields of both cdsCustomer and cdsOrders are "linked" to create this master-detail relationship. The next step involves two more components: a TDataSetProvider component, which gets its DataSet property assigned to cdsCustomer, and a TXMLBroker component (from the InternetExpress tab of the Component Palette), which gets its ProviderName property assigned to the DataSetProvider component.

XSLPageProducer
Let's continue with the data module and drop a XSLPageProducer component from the WebSnap tab of the component palette. The web module should now look as follows:

Web Module with XSLPageProducer1

Right-click in the Web Module and start the Actions Editor in order to add a new Web Action Item with PathInfo /xsl, and in the OnAction event handler for this item write the following code:
  procedure TDataModule2.WebDispatcher1WebActionItem2Action(Sender: TObject;
    Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
  begin
    Response.Content := XSLPageProducer1.Content
  end;
Note that we can't simple assign the XSLPageProducer component to the Producer property of the WebActionItem (for some reason, it doesn't seem to be endorsed to use the XSLPageProducer inside a "normal" WebBroker application like we're doing right now).

XML and XSL
Anyway, the XSLPageProducer component can be used to turn an XML document (or XML string) into another XML string, using an EXtensible Stylesheet Language (XSL) template. Note that the Delphi 6 on-line help still calls it the TXMLPageProducer component instead of the TXSLPageProducer component. The XSLPageProducer component has a number of properties that can easily be confused with each other (like the FileName, XML and XMLData properties, as I will make clear in a moment):

Object Inspector and XSLPageProducer1

First of all, the only way to specify the input XML content is by using the XMLData property. You cannot use the XML property, since this is used to specify the XSL template (just like the FileName is used to point to an external XSL template file). I wonder why the XML property isn't called XSL, but I guess that's the same reason why the on-line help refers to it as the TXMLPageProducer instead the TXSLPageProducer component - it may have been renamed late in the development process of Delphi 6 (and Borland may have forgot the XML property - or would break existing code and examples by changing it).

XMLData property
Anyway, the XMLData property can point to any component that implements the IXMLDocument interface (like the TXMLDocument component) or the IGetXMLStream interface (like the TXMLBroker component). Since we're already using the data module with a TXMLBroker component, we can use that one. Just click on the XMLData property of the XSLPageProducer component and select XMLBroker1 to connect it to the XMLBroker component (which in turn connects to the XMLTransformProvider component).

FileName property
The FileName property points to an external XSL template file, which should contain XSL Transformations (XSLT) using XPath and XPointer (a bit more about that in a moment). It may be handy to use the FileName property, but you can also use the XML property to make sure the XSL template is embedded within the XSLPageProducer component (and hence the web server application itself). Unfortunately, when you click on the ellipsis to start the property editor for the FileName property, you get a File Open dialog that by default starts to look for XML files. You have to open the "Files of type" combobox in order to specify that you're looking for XSL files instead.

XML property
The XML property is yet another "strange one". You may think that this one can be used as alternative for the XMLData property, but that's not the case. The XML property is actually an alternative for the FileName property, and should in that function contain the XSL template, and not the XML data (so the "XML" property is a bit misleading). Warning: if you click on the XML property and enter some XSL Transformations, be aware that you'll clear the FileName property! It appears that the FileName and XML property are mutual exclusive. If you set a value to one, you clear the other. And this can be especially painful if you enter a new FileName property and accidentally clear the XML property (containing a potentially long list of XSL Transformations). In our example, I'll fill the XML property with the following set of XSL Transformations:

  <?xml version="1.0"?>
  <xsl:stylesheet xmlns:xsl="http://www.w3.org/TR/WD-xsl">
    <xsl:template match="/">
      <html>
      <body>
      <xsl:apply-templates/>
      </body>
      </html>
    </xsl:template>

    <xsl:template match="DATAPACKET">
      <table border="1">
      <xsl:apply-templates select="METADATA/FIELDS"/>
      <xsl:apply-templates select="ROWDATA/ROW"/>
      </table>
    </xsl:template>

    <xsl:template match="FIELDS">
      <tr>
      <xsl:apply-templates/>
      </tr>
    </xsl:template>

    <xsl:template match="FIELD">
      <th>
      <xsl:value-of select="@attrname"/>
      </th>
    </xsl:template>

    <xsl:template match="ROWDATA/ROW">
      <tr>
      <xsl:for-each select="@*">
        <td>
        <xsl:value-of/>
        </td>
      </xsl:for-each>
      </tr>
    </xsl:template>

  </xsl:stylesheet>
This XSL Transformation template above is especially designed to handle data packets that are coming from the XMLBroker component, and can originally be found in the XSLProducer WebSnap demo directory of Delphi 6 itself. Tip: if for some reason you can't locate this example, then you can use Delphi 6 to produce it for you as example template. Do File | New | Other, go to the WebSnap tab of the Object Repository and double-click on the WebSnap Page Module icon. In the dialog that follows, select XSLPageProducer as Producer Type. Now, make sure the XSL "New File" option is checked, and select the type of template from the combobox (standard, blank or data packet). For our example, select "data packet". Ignore all other options on the dialog, because we are only interested in the generated XSL template file. Click on OK to generate a new WebSnap Page Module. Click on the new Uni1.xsl tab, copy the contents and paste it inside the XML property of the XSLPageProducer. Alternately, you may save the contents in file datapacket.xsl and assign the FileName property to datapacket.xsl (note that the property editor starts to look for .xml files first, you need to make sure to put the .xsl file inside it). Make sure that you don't save the Unit1.pas or .dfm itself, since we now need to remove the useless WebSnap Page Module from the WebBroker project (do View | Project Manager, and remove Unit1 from the WebBroker project). And finally, in case you're interested, the standard XSL template produces the following three lines (ready for you to enter your own custom XSL):
  <?xml version="1.0"?>
  <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/TR/WD-xsl">
  </xsl:stylesheet>
and the blank XSL template is indeed just an empty file.

XSL Transformations
After you've enter placed the XSL template inside the XML property or have pointed the FileName property to datapacket.xsl, it's time to compile and deploy the web server application. The result can be seen below:

First XSLPageProducer output in Browser

The screenshot above shows the selected fields from cdsCustomer, but an empty column for cdsOrders. And I want to see all orders that belong to a customer. Note that the output doesn't seem right. The cdsCustomer table contains a number of empty fields (like Addr2), but unfortunately, the fields with a value (like the second last field Country and cdsOrders) are shifted to the left to "align" with the other fields that have values. This way, the actual data for Country is presented in the wrong column in a lot of places. If you look at the XSL, you'll notice that it will just enumerate all field-value pairs that are in the XML data packet. And unfortunately (and this is the cause), whenever a field has an empty value, there is no XML fieldname-value pair generated in the XML data packet. An optimisation that is usually helpful, but will now be causing misalignments of our columns.

Enhanced XSL Template
So far, we've seen the Customer fields (with shifting columns), but nothing (yet) for the Orders nested dataset fields. To solve both issues, we need to modify the XSL Transformation template. First thing, we need to explicitly mention all fieldnames for the cdsCustomer, and second, we need to scan for and process the cdsOrders as well. The new XSL template is defined as follows:

  <?xml version="1.0"?>
  <xsl:stylesheet xmlns:xsl="http://www.w3.org/TR/WD-xsl">
    <xsl:template match="/">
      <html>
      <body>
      <xsl:apply-templates/>
      </body>
      </html>
    </xsl:template>

    <xsl:template match="DATAPACKET">
      <table cols="5" border="1">
      <xsl:apply-templates select="METADATA/FIELDS"/>
      <xsl:apply-templates select="ROWDATA/ROW"/>
      </table>
    </xsl:template>

    <xsl:template match="FIELDS">
      <tr>
      <xsl:apply-templates/>
      </tr>
    </xsl:template>

    <xsl:template match="FIELD">
      <th>
      <xsl:value-of select="@attrname"/>
      </th>
    </xsl:template>

    <xsl:template match="ROWDATA/ROW">
      <tr>
      <td valign="top"><xsl:value-of select="@CustNo"/></td>
      <td valign="top"><xsl:value-of select="@Company"/></td>
      <td valign="top"><xsl:value-of select="@Addr1"/></td>
      <td valign="top"><xsl:value-of select="@Addr2"/></td>
      <td valign="top"><xsl:value-of select="@City"/></td>
      <td valign="top"><xsl:value-of select="@Country"/></td>
      <td>
        <table cols="5" bgcolor="ffffff">
          <xsl:apply-templates select="cdsOrders/ROWcdsOrders"/>
        </table>
      </td>
      </tr>
    </xsl:template>

    <xsl:template match="cdsOrders/ROWcdsOrders">
      <tr>
      <xsl:for-each select="@*">
        <td bgcolor="ffffcc">
          <xsl:value-of/>
        </td>
      </xsl:for-each>
      </tr>
    </xsl:template>

  </xsl:stylesheet>
The new output shows no more shifting of empty columns (the Addr2 field is empty where it should be empty, and the Country field is always listed in the Country column), as well as a nested table for the cdsOrders entries. Note that you can now customise the XSL Template file even further to make the second table a bit nicer (show only the fields you want, include table headers, etc.), but I'm sure you'll get the idea by now.

Enhanced XSLPageProducer output in browser

As you've seen, we can use the XSLPageProducer in a "normal" WebBroker environment, without the need for WebSnap Page Modules. The XSL Template language using XPath and XPointer can be used to specify the transformation from XML to HTML-compliant XML that can be shown in the browser.

For more recent information on this topic, check out my Delphi 2010 XML, SOAP & Web Services courseware manual.


This webpage © 2002-2010 by Bob Swart (aka Dr.Bob - www.drbob42.com). All Rights Reserved.