Sample XSLT and ASP.NET code to create web pages from your course feed

Once you have your XCRI CAP 1.2 course feed, you may well want to populate your website with pages generated from the XML.

Here is a set of ASP.NET web form and VB.NET program code, and XSLT stylesheets which create:

  1. a course details page, with a formatted URL;
  2. a course discovery page, with refinement links and paging.

The version of ASP.NET tested was 4.0, although most of the code should work in other versions. I am using the Sample ASP.NET code to take XCRI CAP 1.2 XML from database and publish it as a web feed example.

Contents

  1. Directory structure
  2. Site master page
  3. Course details page
  4. Course details XSLT
  5. URL routing with Global.asax
  6. Course discovery page
  7. Course discovery XSLT
  8. Notes
  9. Summary

Directory structure

The sample website has three files in the root directory: the master file, its code behind VB.NET file, and Global.asax. The course discovery page lives in the /courses directory, and the course details page lives in the /courses/course subdirectory. The XSLT stylesheets for each page are safely tucked away in /xml/xsl/t/courses.

  • courses
    • course
      • default.aspx
      • default.aspx.vb
    • default.aspx
    • default.aspx.vb
  • kelpiecollege.master
  • kelpiecollege.master.vb
  • Global.asax
  • xml
    • xsl
      • t
        • courses
          • xcricap1p2toxhtmlcoursediscovery.xslt
          • xcricap1p2toxhtmldetails.xslt

Site master page

You can use whatever site master page you want. The important thing for this demonstration is that it has the following placeholders, the first in the head and the following two in the body:

<asp:contentplaceholder id="HeadPlaceHolder" runat="server"> </asp:contentplaceholder> <asp:contentplaceholder id="NavigationPlaceHolder" runat="server"> </asp:contentplaceholder> <asp:contentplaceholder id="ContentPlaceHolder" runat="server"> </asp:contentplaceholder>

Course details page

The course details page will be an ASP.NET web form located in this test website at /courses/course/default.aspx (if "default.aspx" is set as one of your directory document file names, you can access the page simply by /courses/course). Here's the ASP.NET markup:

<%@ Page Title="A Kelpie College Course" Language="VB" MasterPageFile="~/kelpiecollege.master" AutoEventWireup="false" CodeFile="default.aspx.vb" Inherits="courses_course_default" %> <asp:Content ID="Content1" ContentPlaceHolderID="BreadcrumbPlaceHolder" runat="Server"> </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="NavigationPlaceHolder" runat="Server"> </asp:Content> <asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder" runat="Server"> <asp:Xml ID="xmlCourseDetails" runat="server" /> </asp:Content>

The common page elements and links to CSS files and so on are created by the master page (see above, saved in the site root as kelpiecollege.master). The distinctive content, taken from your XCRI CAP 1.2 feed, will replace the asp.Xml control. This is done using the VisualBasic.NET code behind file, located in the same directory at /courses/course/default.aspx.vb, here's its code:

Imports System Imports System.IO Imports System.Xml Imports System.Xml.XPath Imports System.Xml.Xsl Partial Class courses_course_default Inherits System.Web.UI.Page Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load Dim strPageTitle As String 'Construct the course identifier from the query string parameter. Dim strCourse As String = "http://kelpiecollege.org.uk/courses(" & Convert.ToString(Page.RouteData.Values("courseid")) & ")" ' Compile the style sheet. Dim xctTransform1 As New XslCompiledTransform() ' Choose the transform stylesheet Dim strStylesheetUri1 As String = "http://www.kelpiecollege.org.uk/xml/xsl/t/courses/xcricap1p2toxhtmldetails.xslt" ' Choose the source XML document. Dim strDocumentUri1 As String = "http://www.kelpiecollege.org.uk/courses/xcri/cap/1.2" xctTransform1.Load(strStylesheetUri1) ' Create the XsltArgumentList. Dim argList1 As XsltArgumentList = New XsltArgumentList() ' Add parameters argList1.AddParam("course", "", strCourse) ' Create a memory stream to carry the output of the transformation. Dim ms1 As MemoryStream = New MemoryStream() ' Transform the RDF to QTI Lite xctTransform1.Transform(strDocumentUri1, argList1, ms1) ' Load the results into an XPathDocument object. ms1.Seek(0, SeekOrigin.Begin) Dim xpdDoc1 As XPathDocument = New XPathDocument(ms1) Dim xpnDoc1 As XPathNavigator xpnDoc1 = xpdDoc1.CreateNavigator xmlCourseDetails.XPathNavigator = xpnDoc1 ' Set the page title to the course title. ' Add the namespace. Dim nsmgr As New XmlNamespaceManager(xpnDoc1.NameTable) nsmgr.AddNamespace("xhtml", "http://www.w3.org/1999/xhtml") strPageTitle = xpnDoc1.SelectSingleNode("/xhtml:div/xhtml:h1", nsmgr).Value Page.Title = strPageTitle End Sub End Class

What this code does (after the usual stuff) when the page loads is:

  1. gets the all-important course identifier from the URL, using routing as explained below;
  2. transforms the XCRI CAP 1.2 feed into a section of XHTML using an XSLT stylesheet, to which is passed a course identifier parameter;
  3. puts this XHTML into the page, replacing the asp:Xml control;
  4. gets the course title and puts it into the web page title.

Course details XSLT

The transformation above uses an XSLT stylesheet, and here's its code:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns="http://www.w3.org/1999/xhtml"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:xcri="http://xcri.org/profiles/1.2/catalog"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:xd="http://www.oxygenxml.com/ns/doc/xsl"
xmlns:mlo="http://purl.org/net/mlo"
xmlns:dcterms="http://purl.org/dc/terms/"
xmlns:credit="http://purl.org/net/cm"
xmlns:xcriTerms="http://xcri.org/profiles/1.2/catalog/terms"
exclude-result-prefixes="xhtml xcri xcriTerms xsi dc xd mlo dcterms credit">
<xsl:output method="xml" version="1.0" encoding="UTF-8" omit-xml-declaration="yes" indent="yes"/>
<xd:doc scope="stylesheet">
<xd:desc>
<xd:p><xd:b>Created on:</xd:b> 2012-04-08</xd:p>
<xd:p><xd:b>Author:</xd:b> Tavis Reddick</xd:p>
<xd:p>Modified on 2015-05-28 to add event markup to course presentation in HTML table row.</xd:p>
<xd:p>Takes an XCRI CAP 1.2 document and outputs an XHTML course details section, selected by course identifier
parameter.</xd:p>
<xd:p>This work is licensed under the Creative Commons Attribution 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by/3.0/ or send a letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.</xd:p>
</xd:desc>
</xd:doc>
<xsl:param name="course"/>
<xsl:template match="/">
<xsl:element name="div" namespace="http://www.w3.org/1999/xhtml">
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
<xsl:template match="xcri:catalog/xcri:provider/xcri:course">
<xsl:if test="dc:identifier = $course">
<xsl:element name="h1">
<xsl:value-of select="mlo:qualification/dc:title"/>
<xsl:if test="mlo:qualification/dc:title"><xsl:text> </xsl:text></xsl:if>
<xsl:value-of select="dcterms:title"/>
</xsl:element>
<xsl:element name="h2">
<xsl:text>Topic</xsl:text>
</xsl:element>
<xsl:copy-of select="dcterms:description[@xsi:type='xcriTerms:topic']/*"/>
<xsl:element name="h2">
<xsl:text>Career outcome</xsl:text>
</xsl:element>
<xsl:copy-of select="dcterms:description[@xsi:type='xcriTerms:careerOutcome']/*"/>
<xsl:if test="xcri:presentation">
<table>
<caption>List of course presentations</caption>
<tr>
<th scope="col">Start date</th>
<th scope="col">End date</th>
<th scope="col">Study mode</th>
<th scope="col">Location</th>
</tr>
<xsl:for-each select="xcri:presentation">
<xsl:variable name="venue" select="xcri:venue/xcri:provider/mlo:location" />
<tr class="vevent">
<td class="dtstart" title="{mlo:start}"><xsl:value-of select="substring(mlo:start, 1, 10)"/></td>
<td class="dtend" title="{xcri:end}"><xsl:value-of select="substring(xcri:end, 1, 10)"/></td>
<td class="summary"><xsl:value-of select="xcri:studyMode"/></td>
<td class="location"><xsl:value-of select="$venue/mlo:address"/><xsl:if test="$venue/mlo:address"><xsl:text>, </xsl:text></xsl:if>
<xsl:value-of select="$venue/mlo:street"/><xsl:if test="$venue/mlo:street"><xsl:text>, </xsl:text></xsl:if>
<xsl:value-of select="$venue/mlo:town"/>
</td>
</tr>
</xsl:for-each>
</table>
</xsl:if>
</xsl:if>
</xsl:template>
<!-- Do nothing with provider identifier and title, for now. -->
<xsl:template match="xcri:catalog/xcri:provider/dcterms:identifier"/>
<xsl:template match="xcri:catalog/xcri:provider/dcterms:title"/>
</xsl:stylesheet>

The result should look something like this:

Screenshot of the Conversational Cetacean Harbour Dolphin Dialect course details page on Kelpie College website.

Note that in the above XSLT code, each table row is markup up as a hCalendar event, which may appear in search engine result pages as distinct sub-items.

URL routing with Global.asax

We would like the URLs of our course details page to follow the format:

 /courses(XYZ)
  

where "XYZ" is an internal database identifier (actually in our sample database our id is a URI, but the "XYZ" part is the only part that changes).

There are a number of ways of achieving this, but a fairly simple one in ASP.NET 4.0 is to use URL routing in the Global.asax file, like so (only the relevant sections of code are shown):

<%@ Application Language="VB" %> <%@ Import Namespace="System.Web.Routing" %> <script runat="server"> Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs) RegisterRoutes(RouteTable.Routes) End Sub Shared Sub RegisterRoutes(routes As RouteCollection) routes.MapPageRoute("", "courses({CourseId})", "~/courses/course/default.aspx") End Sub </script>

Course discovery page

The course discovery page (located here at /courses/default.aspx) first lists all courses in a table with a list of refinement or filtering links beside it. These links when clicked pass parameters through the page into the XSLT stylesheet, and filter the results accordingly. Here's the ASP.NET markup, it's very similar to the course details page (you will want to set debug to false in a live website, of course):

<%@ Page Title="Kelpie College Courses" Language="VB" MasterPageFile="~/kelpiecollege.master" AutoEventWireup="false" CodeFile="default.aspx.vb" Inherits="courses_default" debug="true" %> <asp:Content ID="Content1" ContentPlaceHolderID="BreadcrumbPlaceHolder" runat="Server"> </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="NavigationPlaceHolder" runat="Server"> </asp:Content> <asp:Content ID="Content3" ContentPlaceHolderID="ContentPlaceHolder" runat="Server"> <asp:Xml ID="xmlCourseList" runat="server" /> </asp:Content>

and here is the VB.NET code behind that does more work and calls the XSLT with parameters:

Imports System.IO Imports System.Xml Imports System.Xml.XPath Imports System.Xml.Xsl Partial Class courses_default Inherits System.Web.UI.Page Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load ' Get parameters from query string. Dim strQualification As String = Request.QueryString("qualification") Dim strStudyMode As String = Request.QueryString("studymode") Dim strSubject As String = Request.QueryString("subject") Dim strPage As String = Request.QueryString("page") Dim strPageSize As String = Request.QueryString("pagesize") ' Compile the style sheet. Dim xctTransform1 As New XslCompiledTransform() ' Choose the transform stylesheet Dim strStylesheetUri1 As String = "http://www.kelpiecollege.org.uk/xml/xsl/t/courses/xcricap1p2toxhtmlcoursediscovery.xslt" ' Choose the source XML document. Dim strDocumentUri1 As String = "http://www.kelpiecollege.org.uk/courses/xcri/cap/1.2" xctTransform1.Load(strStylesheetUri1) ' Create the XsltArgumentList. Dim argList1 As XsltArgumentList = New XsltArgumentList() ' Add parameters If Not strQualification = "" Then argList1.AddParam("qualification", "", strQualification) End if If Not strStudyMode = "" Then argList1.AddParam("studyMode", "", strStudyMode) End if If Not strSubject = "" Then argList1.AddParam("subject", "", strSubject) End if If Not strPage = "" Then argList1.AddParam("page", "", strPage) End if If Not strPageSize = "" Then argList1.AddParam("pageSize", "", strPageSize) End if ' Create a memory stream to carry the output of the transformation. Dim ms1 As MemoryStream = New MemoryStream() ' Transform the XCRI CAP 1.2 to XHTML xctTransform1.Transform(strDocumentUri1, argList1, ms1) ' Load the results into an XPathDocument object. ms1.Seek(0, SeekOrigin.Begin) Dim xpdDoc1 As XPathDocument = New XPathDocument(ms1) Dim xpnDoc1 As XPathNavigator xpnDoc1 = xpdDoc1.CreateNavigator xmlCourseList.XPathNavigator = xpnDoc1 End Sub End Class

Course discovery XSLT

This stylesheet renders a table of courses, the columns taken from XCRI CAP 1.2 course and presentation elements; and a list of refinement links. Here's the code:

<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xd="http://www.oxygenxml.com/ns/doc/xsl" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xcri="http://xcri.org/profiles/1.2/catalog" xmlns:fn="http://www.w3.org/2005/xpath-functions" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mlo="http://purl.org/net/mlo" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:credit="http://purl.org/net/cm" xmlns:xcriTerms="http://xcri.org/profiles/1.2/catalog/terms" exclude-result-prefixes="xs xd dc xhtml xcri fn xsi mlo dcterms credit xcriTerms" version="2.0"> <xsl:output method="xml" version="1.0" encoding="UTF-8" omit-xml-declaration="yes" indent="yes"/> <xd:doc scope="stylesheet"> <xd:desc> <xd:p><xd:b>Created on:</xd:b> 2012-04-08</xd:p> <xd:p><xd:b>Author:</xd:b> Tavis Reddick</xd:p> <xd:p>Takes an XCRI CAP 1.2 document and outputs an XHTML section containing results and filtering links.</xd:p> <xd:p>Based on an earlier (2011-05-22) XCRI CAP 1.1 version, but moved from XPath 2.0 to 1.0 for greater compatibility with older frameworks.</xd:p> <xd:p>This work is licensed under the Creative Commons Attribution 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by/3.0/ or send a letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.</xd:p> </xd:desc> </xd:doc> <xsl:param name="qualification" /> <xsl:param name="studyMode" /> <xsl:param name="subject" /> <xsl:param name="headingLevel" select="'h1'"/> <xsl:param name="linkGroupHeadingLevel" select="'h2'"/> <xsl:param name="page" select="1" /> <xsl:param name="pageSize" select="20" /> <xsl:variable name="queryStringBase" select="concat('?qualification=', $qualification, '&studymode=', $studyMode, '&subject=', $subject, '&pagesize=', $pageSize)" /> <!-- The $courses variable holds the currently selected subset of courses, filtered by the parameters. --> <xsl:variable name="courses" select="/xcri:catalog/xcri:provider/xcri:course[($qualification = '' or $qualification = mlo:qualification/dc:title) and ($studyMode = '' or $studyMode = xcri:presentation/xcri:studyMode/@identifier) and ($subject = '' or $subject = dc:subject)]" /> <!-- If using XPath 2.0, you can use fn:distinct-values, but if you are stuck with XPath 1.0, an alternative is the preceding or preceding-sibling check. --> <xsl:variable name="qualifications" select="$courses/mlo:qualification/dc:title[not(. = preceding::*/mlo:qualification/dc:title)]"/> <xsl:variable name="studyModes" select="$courses/xcri:presentation/xcri:studyMode[not(. = preceding::*/xcri:presentation/xcri:studyMode)]"/> <xsl:variable name="subjects" select="$courses[not(dc:subject = preceding-sibling::*/dc:subject)]/dc:subject"/> <xsl:template match="/"> <xsl:element name="div"> <xsl:element name="{$headingLevel}"> <xsl:text>Courses from </xsl:text> <xsl:value-of select="xcri:catalog/xcri:provider/dcterms:title"/> </xsl:element> <!-- Create a section to hold the filtering links. --> <div style="float: left;"> <!-- Qualification links. --> <xsl:element name="{$linkGroupHeadingLevel}"><xsl:text>Qualifications</xsl:text></xsl:element> <xsl:if test="$qualifications"> <ul> <xsl:if test="not($qualification = '')"> <li><a href="{concat('?qualification=&studymode=', $studyMode, '&subject=', $subject, '&pagesize=', $pageSize)}">All qualifications</a></li> </xsl:if> <xsl:for-each select="$qualifications"> <xsl:sort data-type="text" order="ascending" /> <!-- Create a link with existing parameter values, with the addition of each existing qualification value in the current course selection. --> <xsl:variable name="queryString" select="concat('?qualification=', ., '&studymode=', $studyMode, '&subject=', $subject, '&pagesize=', $pageSize)" /> <li><a href="{$queryString}"><xsl:value-of select="." /></a></li> </xsl:for-each> </ul> </xsl:if> <!-- Study mode links. --> <xsl:element name="{$linkGroupHeadingLevel}"><xsl:text>Study Modes</xsl:text></xsl:element> <xsl:if test="$studyModes"> <ul> <xsl:if test="not($studyMode = '')"> <li><a href="{concat('?qualification=', $qualification, '&studymode=&subject=', $subject, '&pagesize=', $pageSize)}">All study modes</a></li> </xsl:if> <xsl:for-each select="$studyModes"> <xsl:sort data-type="text" order="ascending" /> <!-- Create a link with existing parameter values, with the addition of each existing study mode value in the current course selection. --> <xsl:variable name="queryString" select="concat('?qualification=', $qualification, '&studymode=', @identifier, '&subject=', $subject, '&pagesize=', $pageSize)" /> <li><a href="{$queryString}"><xsl:value-of select="." /></a></li> </xsl:for-each> </ul> </xsl:if> <!-- Subject links. --> <xsl:element name="{$linkGroupHeadingLevel}"><xsl:text>Subjects</xsl:text></xsl:element> <xsl:if test="$subjects"> <ul> <xsl:if test="not($subject = '')"> <li><a href="{concat('?qualification=', $qualification, '&studymode=', $studyMode, '&subject=&pagesize=', $pageSize)}">All subjects</a></li> </xsl:if> <xsl:for-each select="$subjects"> <xsl:sort data-type="text" order="ascending" /> <!-- Create a link with existing parameter values, with the addition of each existing subject value in the current course selection. --> <xsl:variable name="queryString" select="concat('?qualification=', $qualification, '&studymode=', $studyMode, '&subject=', ., '&pagesize=', $pageSize)" /> <li><a href="{$queryString}"><xsl:value-of select="." /></a></li> </xsl:for-each> </ul> </xsl:if> </div> <!-- Create a section to hold the results. --> <!-- Create links to other pages of results as required. --> <p style="display: inline;">Page: </p> <ul style="display: inline;"> <xsl:for-each select="$courses[(position() - 1) mod $pageSize = 0]"> <xsl:choose> <xsl:when test="position() = $page"> <li style="display: inline; padding: 1em;"><xsl:value-of select="position()" /></li> </xsl:when> <xsl:otherwise> <li style="display: inline; padding: 1em;"><a href="{concat($queryStringBase, '&page=', position())}"><xsl:value-of select="position()" /></a></li> </xsl:otherwise> </xsl:choose> </xsl:for-each> </ul> <!-- Create a table with course information. --> <table> <thead> <th>Course</th> <th>Qualification</th> <th>Subject</th> <th>Study modes</th> </thead> <tbody> <xsl:for-each select="$courses"> <xsl:sort select="dcterms:title"/> <xsl:if test="position() > $pageSize * ($page - 1) and position() < ($pageSize * $page) + 1"> <tr> <td> <xsl:element name="a"> <xsl:attribute name="href"> <!-- Link to course details page. --> <xsl:value-of select="mlo:url" /> </xsl:attribute> <xsl:value-of select="dcterms:title"/> </xsl:element> </td> <td><xsl:value-of select="mlo:qualification/dc:title"></xsl:value-of></td> <td><xsl:value-of select="dc:subject"></xsl:value-of></td> <td><xsl:for-each select="xcri:presentation/xcri:studyMode[not(. = preceding-sibling::xcri:studyMode)]"><xsl:value-of select="concat(@identifier, '; ')" /></xsl:for-each></td> </tr> </xsl:if> </xsl:for-each> </tbody> </table> </xsl:element> </xsl:template> </xsl:stylesheet>

When you first visit the web page, it will look something like this:

Screenshot of Courses from Kelpie College web page.

and then when you click on a link (here, the subject link "interspecies linguistics") to filter your selection (hardly needed in this example with 5 courses, but helpful when you have hundreds), you get a subset, like so:

Screenshot of filtered course page from Kelpie College.

Notes

  • Important! In all this code, replace any references to "http://www.kelpiecollege.org.uk/" with your own site domain name, or better still fix the code to use a relative address.
  • This is not optimized, production-ready code.
  • There is no error handling.
  • This method leaves an unwanted XML declaration stuck in middle of web page.
  • This example uses some titles instead of identifiers for parameters. One great thing about XCRI CAP vocabularies is that you will be able to pass short tokens like 'FT' in URLs instead of long, URL-encoded labels.
  • CSS styles are not included. You could really improve the display by adding design flair, colour, icons and so on.
  • Text search is not included. You might want to incorporate search into your discovery solution, by providing a text input and passing that as a parameter too.

Summary

This sample set of working code builds a small, test website using ASP.NET 4.0 and XSLT, and an XCRI CAP 1.2 XML feed, to create a course discovery web page on which users can read and filter a list of courses, and then click through to a course details page, also created here in code.

If you spot any errors or have any comments, please supply feedback using the Contact method on this website.

A working demonstration of the web pages built from this code scan be found on the fictional Kelpie College website courses section.

For more information on the standard, see the XCRI Knowledge Base.

Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 Unported License.

Back to Code and Examples for XCRI CAP 1.2.

Last updated: .