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:
- a course details page, with a formatted URL;
- 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
- Directory
structure
- Site master
page
- Course details
page
- Course details
XSLT
- URL routing with
Global.asax
- Course discovery
page
- Course discovery
XSLT
- Notes
- 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:
- gets the all-important course identifier from the URL, using routing as
explained below;
- transforms the XCRI CAP 1.2 feed into a section of XHTML
using an XSLT stylesheet, to which is passed a course identifier parameter;
- puts this XHTML into the page, replacing the asp:Xml control;
- 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:
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:
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:
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.
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:
.