Coldfusion Master Pages

ColdFusion Master Pages Tutorial - Introduction

In ASP.NET 2, a new concept for controlling page appearance was introduced called Master Pages. It's a simple concept where you create a master file that contains the HTML for the page layout, but also include placeholders called ContentPlaceHolders. The pages in your website then reference the master page, and provide content for each ContentPlaceHolder. When the page is requested, the server plugs the content into the placeholders and renders the page. More information on master pages

Goals

I really like this technique and thought it would be interesting to implement in Coldfusion. The goals of this tutorial are:

  1. Show that CFMX can provide this functionality
  2. Use the same syntax as the ASP.NET version
Please note that the technique I'm using is not very efficient or extensible. It works great, but doesn't take efficiency or reusability into account. I'm just showing that the basic features and syntax can be duplicated.

Basic ASP.NET Master Page

If this page was saved as myMaster.master...
The content place holders are introduced into the master page with the following syntax:
<asp:contentplaceholder id="[name]"></asp:contentplaceholder>

If you're familiar with using cfimport to call your custom tags, you should notice that CFMX uses the same syntax.

Basic ASP.NET content page that uses the Master Page

Enough ASP, let's see the Coldfusion!

In order to build this in coldfusion, we need code for:

  • The master page itself
  • The contentPlaceHolder custom tag (to replicate <asp:contentplaceholder...> )
  • The content custom tag (to replicate <asp:content...> )
  • The page custom tag (to replicate <%@ page...> )

Since the master page concept is not built into ColdFusion, it has to be created. We need a way to perform the following actions:

  1. Save the generated content from the content tag
  2. Supply that content to the master page
  3. Inside the master page, plug the content into the ContentPlaceHolder
  4. Output the results

All files, both CFCs and custom tags will live in a 'UI' (for user interface) folder. This path is then used for the cfimport tags and the extends attribute of the CFCs.

Master Page Code

The master page consists of two ColdFusion Components. The baseMasterPage.cfc has all the logic for managing what the content is and where it gets plugged in. This CFC represents what .NET already has built-in. The basic concept is this: The CFC has a struct in the variables scope inside the CFC that holds the data generated by the content tags. It provides one method to set the content, and another to get it back out when the page is rendered. In addition, a debug method can be called to see what content has been set.

<cfcomponent displayname="baseMasterPage">
	<cffunction name="setContent" access="public" returntype="void" output="false" hint="saves arguments.content to variables.stContent with an arguments.contentID key">
		<cfargument name="contentID" type="string" required="true">
		<cfargument name="content" type="any" required="true">		
		<cfif not structkeyexists(variables,"stContent")>
			<!--- create a struct within the object to hold all the content --->
			<cfset variables.stContent = structnew()>
		</cfif>
		<!--- if the content ID doesn't exist, add it --->
		<cfif not structkeyexists(variables.stContent,arguments.contentID)>
			<cfset structinsert(variables.stContent,arguments.contentID,trim(arguments.content))>
		<cfelse>
			<!--- the contentID already exists, so just update the contents with the new value --->
			<cfset variables.stContent[arguments.contentID] = trim(arguments.content)>
		</cfif>
	</cffunction>
	
	<cffunction name="getContent" returntype="string" output="false" hint="returns the content from variables.stContent using the contentID key">
		<cfargument name="contentID" type="string" required="true">
		<cfset var generatedcontent = "">	
		<!--- If no content was provided for #arguments.contentID#, default to empty string --->
		<cfif structkeyexists(variables.stContent,arguments.contentID)>
			<!--- if the ID that is being requested exists, get the value --->
			<cfset generatedcontent = variables.stContent[arguments.contentID]>
		</cfif>
		<cfreturn generatedcontent />		
	</cffunction>
	
	<cffunction name="debug" output="true">
		<cfdump var="#variables.stContent#" label="variables.stContent">
	</cffunction>
	
	<cffunction name="render" access="public" returntype="void">
		You must override the render() method
	</cffunction>
</cfcomponent>
The Actual Master Page - Define HTML and Content Place Holders

The user created master page CFC has the actual page layout and can be called whatever you want. In this case we'll call it defaultmaster.cfc. It MUST inherit (extend) from the baseMasterPage.cfc. This allows you to create any number of master pages that all extend the baseMasterPage.cfc. Any time you want to add additional features to this technique you just have to edit baseMasterPage.cfc.

<cfcomponent extends="baseMasterPage">
	<cffunction name="render" access="public" returntype="void" output="true">
	<cfsilent><cfimport taglib="/ui" prefix="cfm"></cfsilent>
	<html>
	<head>
		<title>Master Page</title>
	</head>
	<body>
	<table border="1">
		<tr>
			<td><cfm:ContentPlaceHolder ID="leftContent"></cfm:ContentPlaceHolder></td>
			<td><cfm:ContentPlaceHolder ID="middleContent"></cfm:ContentPlaceHolder></td>
			<td><cfm:ContentPlaceHolder ID="rightContent"></cfm:ContentPlaceHolder></td>
		</tr>
	</table>
	</body>
	</html>
	</cffunction>
</cfcomponent>
example.cfm

The Content Page - This what all your content pages will look like.

<cfimport taglib="ui" prefix="cfm"/>
<cfm:page MasterPageFile="ui.defaultmaster">
	<cfm:Content contentID="leftContent">
		<ul>
			<li>Menu Item 1</li>
			<li>Menu Item 2</li>
		</ul>
	</cfm:Content>
	<cfm:Content contentID="middleContent">
		<p>This is the body.  The time is now <cfoutput>#timeformat(now(),"hh:mm:ss tt")#</cfoutput></p>
	</cfm:Content>
	<cfm:Content contentID="rightContent">
		<p>Buy Stuff</p>
		<p>Buy Different Stuff</p>
	</cfm:Content>
</cfm:page>

The closing tag on </cfm:page> is where the page actually gets rendered.

Custom tags

page.cfm

Creates the master page object in the opening tag, renders the page in the closing tag.

	<cfsilent>
		<!--- path to the cfc that is the master page --->
		<cfparam name="attributes.MasterPageFile" default="ui.baseMasterPage">
	</cfsilent>
	<cfif thisTag.HasEndTag>
		<cfif thistag.executionmode eq "start">		
			<cfsilent>
				<!--- In order to act like the master page logic is happening 'behind the scenes',
					an object is created with a fixed name, in this case request.oMaster.
					All other files rely on this variable name to be used.
				 --->
				<cfif not structkeyexists(request,"oMaster")>
					<cfset request.oMaster = createobject("component",attributes.MasterPageFile)>
				</cfif>
			</cfsilent>
		<cfelseif thistag.executionmode eq "end">
			<cfset request.oMaster.render()>
		</cfif>
	<cfelse>
		page.cfm End Tag Required<cfabort>
	</cfif>
	

content.cfm

Adds the generated content to the CFC.

	<cfsilent>
		<cfparam name="attributes.contentID" default="">
		<cfif thisTag.HasEndTag and thistag.executionmode eq "end">
			<cfset request.oMaster.setContent(attributes.contentID,thistag.GeneratedContent)>
		</cfif>
		<!--- since we are adding the content to the master page, don't show in in the browser.  
		it will show on the screen when oMaster.render is called --->
		<cfset thistag.GeneratedContent = "">
	</cfsilent>
	

ContentPlaceHolder.cfm

Called from inside the master page when render() is called - outputs the generated content in the right place.

	<cfsilent>
		<!--- called inside the master page cfc --->
		<cfparam name="attributes.ID" default="">
	</cfsilent>
	<cfif thisTag.HasEndTag and thistag.executionmode eq "end">
		<!--- outputs the saved string to the page --->
		<cfoutput>#request.oMaster.getContent(attributes.ID)#</cfoutput>
	</cfif>
	

Overview of execution steps


So how does it all work?

  1. When you browse example.cfm, it first runs the cfimport which makes all custom tags in the /UI folder accessible as tags prefixed by cfm:
  2. The page custom tag is called and the master page is set: <cfm:page MasterPageFile="ui.defaultmaster">
    • In page.cfm, request.oMaster is instantiated
  3. When the closing tag of all the </cfm:Content> tags are called, the generated content is added to the master page object.
  4. When the closing tag of the </cfm:page> tag is called, it calls the render method of the master page.
    • The master page also imports the /UI folder to have access to the custom tags
    • It calls the <cfm:ContentPlaceHolder> tags for each ID
      • The contentplaceholder.cfm tag is called which pulls the content out of the object and outputs it to the screen.

Wow, that's pretty ugly...

Your right - but this is just a proof of concept meant to use the same syntax as .NET master pages. The actual code is somewhat tricky but it enables us to use the syntax that we're targeting.

When you think about it, the only pages you'll edit are defaultmaster.cfc and then the content pages (like example.cfm), it really is a nice way to create pages. If your layout changes, you just edit defaultmaster.cfc and all pages are updated.

Download

Installation Instructions

  1. unzip the folder into your web root so the /UI folder can be called without a full directory path. Alternately, you can create a mapping to the UI folder so it could be placed anywhere.
  2. Browse master_page_example.cfm


All ColdFusion Tutorials By Author: Nathan Miller