|
By Matt Reider
Master Instructor
Macromedia, Inc.
Click
here to download a .zip file of this article.
This document was created for the purpose of explaining a personal programming technique using ColdFusion. The document explains an example Web site in terms of logic, ColdFusion architecture, and file structure. This document is technical in nature and a strong understanding of ColdFusion is required.
Once you have reviewed this guide, you should be able to:
- Maintain independent ColdFusion logic with minimal impact on other pages
- Easily find the pieces of logic without having to sift through thousands of lines of code
- Combine applications with existing or future systems
This guide uses a real Web site for all examples. This Web site is used by Allaire instructors and is called "TERA," or Trainers and Education Resource Area.
Note: This methodology is not an "official" practice, nor is it modeled after other ColdFusion methodologies such as Fusebox, cfObjects, or SmartObjects.
Basic Architecture
 |
Tera exists in the webroot under a directory named "IC" (for Instructor Central).
Almost every directory is named after an "object" in TERA. Inside of each object
directory are a bunch of files with names such as "getAnnouncements.cfm"
or "delete.cfm." Inside of the Announcement directory,
the following files exist: |
 |
As you can see, each file is the name of some "method" for that object. For this object
(announcements), we have a number of methods that do things for the announcement.
For instance, the delete method will eliminate a certain announcement. The
GetInArray method will get a list of announcements for a certain page and return
them in the correct order. |
Flow of Processing
If you look at the previous directory structure, you will notice a directory called
docs. Unlike most of the directories, this one is not the name of an object.
This is the directory where all of the visual pages will be stored. If a user clicks on
a link in the Web site, they will go to a page in docs such as
/tera/docs/index.cfm, which is the home page for the Web site.
(The /tera/ preface is a ColdFusion mapping that represents the
directory IC).
When a page runs in the docs directory, it typically invokes methods on objects. The most
common methods are display methods. For instance, the announcment.cfm page invokes the display
method on the news object.
So how are methods invoked? Methods are invoked by a file called invoker.cfm which
lives in the root directory (ic or /tera/ through the mapping). The invoker is
called by using the CFMODULE tag as follows:
announcement.cfm (in docs)
<CFMODULE
TEMPLATE="/tera/invoker.cfm"
OBJECT="announcement"
METHOD="display"
AnnouncementID = "6">
The news.cfm page is now invoking the display method on the news object.
Under the covers, the logic inside of the invoker is relatively straightforward.
Here is the code inside of invoker.cfm
invoker.cfm (in IC or /tera/)
<!--- this is the invoker. it calls objects.
Every object has its own folder.
The methods are the file names --->
<CFIF thisTag.executionMode is "start">
<!--- send attributes down to the custom tag --->
<CFMODULE
TEMPLATE="/tera/#attributes.object#/#attributes.method#.cfm"
stAttributes = "#attributes#">
</CFIF>
Now it is clear how the directory structure and the invoker.cfm page are inter-related.
When an object is invoked, the object name passed to the invoker.cfm file is used as a
folder name and the method is used as a file name. The attributes passed to the invoker.cfm
page are subsequently passed onto the method file as a structure called stAttributes.
The final file to analyze would be the display.cfm file in the news directory. This file
displays news from the database. Yet there may be some attributes that were passed from the
invoker that must now be available to display.cfm as normal attributes. Sure, we
could have just left them as stAttributes, but this is not the normal ColdFusion way
of dealing with attributes, so we change them back again. We do this by initializing the method.
At the top of display.cfm is the initialization:
display.cfm (for announcement object)
<CFMODULE template="/tera/InitializeMethod.cfm">
The initialize method template simply takes all of the attributes from the passed
stAttributes structure and turns it back into the ATTRIBUTES structure.
This code lives in every method page.
The other thing that the InitializeMethod.cfm page does is it performs necessary security.
Since all of these object folders are in the webroot (they don't have to be, but in this case
they are), it is important to halt processing if someone opens them by themselves. Sure, it
would be hard for a user to know what the directories are called and such, but it's better to
be safe than sorry.
The entire code for InitializeMethod.cfm looks like this:
InitializeMethod.cfm (in IC or /tera/)
<!--- security - this must be called as a custom tag --->
<CFIF NOT isDefined("caller")>
<CFABORT>
<CFELSE>
<!--- make sure caller is a structure… otherwise they could have passed it in the URL --->
<CFIF NOT isStruct(caller)><CFABORT></CFIF>
</cfif>
<CFIF NOT StructKeyExists(caller.attributes,"stAttributes")>
<CFSET caller.attributes.stAttributes = StructNew()>
</CFIF>
<CFLOOP COLLECTION="#caller.attributes.stAttributes#" ITEM="i">
<CFSET 'caller.attributes.#i#' = caller.attributes.stAttributes[i]>
</CFLOOP>
Once the method has been initialized, the code can run and the display method can do its
thing with normal ATTRIBUTES instead of the stAttributes structure which was passed
from the invoker.
display.cfm (for announcement object)
<CFMODULE template="/tera/initializeMethod.cfm">
<CFQUERY NAME="request.Announcements" DATASOURCE="#request.dsn#">
select * from announcements where announcementID = #attributes.announcementID#
</CFQUERY>
<CFOUTPUT etc…
Note that most methods do not get data and display it - most of them do one or the other.
So you would typically call one method to get the info and another to show it. Also, note that
this example named the query request.announcements. We use the REQUEST scope so we don't
have to worry about scoping - the query is available to any page that calls this method. The only
difference between this and the real world scenario is that the name is hardcoded here and in most
cases, we allow our user to name the query.
Either way, however, we want to use the request scope instead of the caller scope to get the
query back. So in the real world, a getAnnouncements method would be called like this:
Announcement.cfm (in docs)
<CFMODULE
TEMPLATE="/tera/invoker.cfm"
OBJECT="getAnnouncement"
METHOD="display"
AnnouncementID = "6"
r_query = "qGetAnnouncements">
This version of announcement.cfm declares the name of the returned query (r_query attribute)
as qGetAnnouncements. This name is then placed in the request scope so that it
may be printed on the page.
Announcement.cfm (in docs)
<CFMODULE
TEMPLATE="/tera/invoker.cfm"
OBJECT="announcement"
METHOD="display"
AnnouncementID = "6"
r_query = "qGetAnnouncements">
<CFOUTPUT query = "request.qGetAnnouncements" etc…
It may not be obvious how this query is returned with the name qGetAnnouncements,
so we will not modify the display method to return the query as the name as it was passed
in the attribute r_query.
getAnnouncement.cfm (method for announcement object)
<CFMODULE template="/tera/initializeMethod.cfm">
<CFQUERY NAME="request.#attributes.r_query#" DATASOURCE="#request.dsn#">
select * from announcements where announcementID = #attributes.announcementID#
</CFQUERY>
By naming the query "request.#attributes.r_query#" we are returning the results of the query in the
request scope so that other pages can read them. Now it is up to you whether you want to print the query
from the announcement.cfm page (as we just did in the example above) or create another method for
announcements that displays them based on a query passed as an attribute. The full example of this could
look like this:
Announcement.cfm (in docs)
<CFMODULE
TEMPLATE="/tera/invoker.cfm"
OBJECT="announcement"
METHOD="getAnnouncement"
AnnouncementID = "6"
r_query = "qGetAnnouncements">
<CFMODULE
TEMPLATE="/tera/invoker.cfm"
OBJECT="announcement"
METHOD="display"
query = "#request.qGetAnnouncements#">
getAnnouncement.cfm (method for announcement object)
<CFMODULE template="/tera/initializeMethod.cfm">
<CFQUERY NAME="request.#attributes.r_query#" DATASOURCE="#request.dsn#">
select * from announcements where announcementID = #attributes.announcementID#
</CFQUERY>
display (method for announcement object)
<CFMODULE template="/tera/initializeMethod.cfm">
<CFOUTPUT query = "request.qGetAnnouncements" etc…
The clear advantage of this type of methodology is that a different query could be retrieved using
one method and displayed differently depending on the display method that is called.
Content Management Using This Approach
Content management is typically handled by administrators of a Web site who perform the following tasks:
- Update information on the Web site quickly and easily without knowing HTML or CFML
- Schedule content to be published to the Web site by selecting content and dates for publishing them
- Change the look and feel of the Web site as needed by redesigning the site visually
To perform these functions, the content of the Web site must be stored in a database. The database
schema strongly matches that of the directory structure of the site. Looking at the announcement
table, for example, it is a very typical relational table:
To create a new announcement and assign it to a page, the user must be an
admin user and they must also be in design mode.
The design mode is something we will make up to allow admin users to create content
for the site. The user can enter design mode by clicking on a special link that
appears in the title of every page. Let's take a look:
Every page has a header and a footer. The header and footer are methods in the
page object (showHeader and showFooter would be good
examples of methods here, although I just named them header and footer). To show
the header, we simply invoke it the same way we did the announcements (using the
invoker).
Inside of the showHeader (or header) method, we will give admin users the
ability to enter design mode with a link like this:
Code from showHeader.cfm
<CFIF session.ADMIN AND NOT session.design>
<CFOUTPUT><A HREF="#cgi.script_name#?design=1">design mode</A>
<CFELSEIF session.ADMIN and session.design>
<CFOUTPUT><A HREF="#cgi.script_name#?design=0">design mode OFF</A>
</CFIF>
Code from application.cfm
<CFPARAM NAME="design" DEFAULT="0">
<CFIF session.ADMIN AND design>
<CFET session.design = 1>
<CFELSEIF not design>
<CFSET session.design = 0>
</CFIF>
Now we can control whether or not the user is in this thing we called design mode, and we
can give them links to turn the design mode on or off. If the user clicks on the design mode
link the application.cfm will create a session variable for them showing they are in design mode.
When they turn it off, the design mode will be shut off.
We will look at the purpose of this design mode later. First we need to look at something
more important!
Alleviating the Locking Problem
While we are on the subject of session variables, let me stray from the main topic for a
moment and turn towards locking in ColdFusion. Our design mode uses SESSION variables.
If you have ever built a ColdFusion application, you probably acquainted yourself with the
<CFLOCK> tag. The idea of the <CFLOCK> tag is that it must be used to lock all
shared variables in ColdFusion such as SESSION, APPLICATION, or CLIENT scopes. The
reason for this has to do with the fact that ColdFusion is a multi-threaded server and two
pages can run at the same time. If two pages run simultaneously, one page could set a SESSION
variable while the other page simultaneously RESETS the same SESSION variables to other values.
Without locking your shared variables, a large ColdFusion application will eventually fail.
An easy answer to this problem is to dump the entire SESSION or APPLICATION scope into the
REQUEST scope. ColdFusion does not 'share' the REQUEST scope between threads and it does not
need to be locked. So by dumping all the shared variables to this scope, we can lock our
variables once and then forget about it. Actually, we have to lock it twice: once in the
application.cfm and once in the OnRequestEnd.cfm. Here is the code:
Code from application.cfm
<CFSET request.session = structNew()>
<CFLOCK scope="SESSION" timeout="30" type="READONLY">
<CFLOOP COLLECTION="#session#" ITEM="i">
<CFSET request.session[i] = session[i]>
</cfloop>
</CFLOCK>
<CFSET request.application = structNew()>
<CFLOCK scope="APPLICATION" timeout="30" type="READONLY">
<CFLOOP COLLECTION="#application#" ITEM="i">
<CFSET request.application[i] = application[i]>
</cfloop>
</CFLOCK>
Code from OnRequestEnd.cfm
<CFLOCK scope="SESSION" timeout="30" type="EXCLUSIVE">
<CFLOOP COLLECTION="#request.session#" ITEM="i">
<CFSET session[i]=request.session[i]>
</cfloop>
</CFLOCK>
<CFLOCK scope="APPLICATION" timeout="30" type="EXCLUSIVE">
<CFLOOP COLLECTION="#request.application#" ITEM="i">
<CFSET application[i]=request.application[i] >
</cfloop>
</CFLOCK>
Now that we have dumped the session variables and the application variables into the
REQUEST scope (and then returned them to their prospective scopes at the end of the request),
we need not lock our shared variables ever again. So, from now on, instead of referencing
session.designmode we will reference request.session.designmode.
Before we move on from here, note that the SESSION and APPLICATION variables will be lost
if the request does not finish entirely. If <CFLOCATION> is used at all, it will
interrupt the request and the SESSION and APPLICATION scopes will not be refreshed in the
OnRequestEnd.cfm. The answer to this is to create a separate page - I named mine
closeSession.cfm - and <CFINCLUDE> it before you do any <CFLOCATION>
procedure. The closeSession.cfm is an exact replica of the logic we have in
OnRequestEnd.cfm above.
Design Mode Explored
When announcements show up in the announcement.cfm page, they do so through the
display method. We already covered that. What we did not look at is that all display
methods have links in them that allow administrators - who are in design mode - to modify
the application.
The following code is inside of the display method for announcements:
Code from display.cfm (method for announcement object)
<!--- this takes place SOMEPLACE on the page. This is not the entire page --->
<A HREF="#attributes.url#"
onclick="return popup('admin.cfm?object=announcement&method=edit&id=#attributes.announcementID#')>
click here to edit announcement
</A>
As you can see, this code references a JavaScript function called "popup," which must
be in the header of your page. We put that code in the header method of our
page object. The JavaScript looks like this:
This logic needs to go in the header of the page:
function open_popup(page) {
window.open(page,'newConsole','width=700,resizable=yes,toolbar=no,
location=yes,directories=yes,status=no,menubar=no,scrollbars=yes');
return false;}
This code will open an admin window. In reality, I made the popup link in
display.cfm a custom tag, and named it "grapple.cfm" which takes the url
and text as attributes. Either way, it doesn't matter. The important part is that
we are opening a page called admin.cfm and we are passing an object
(announcement), and a method (edit), and an ID which is the
announcementID.
So what is going on in this popup admin window? Well, the admin window simply
calls the method for the object that was sent to it in the URL. The method is edit
and this method shows a form and allows the user to edit the announcement based on
the ID that was passed.
admin.cfm (in docs - appears as a popup window in design mode)
<CFIF request.session.ADMIN AND request.session.DESIGN>
<body onunload="opener.location.href =
opener.location.href;" ONLOAD="this.focus()">
<CFIF LEN(TRIM(object))>
<CFMODULE
TEMPLATE ="/tera/invoker.cfm"
object="#object#"
METHOD="#method#"
ID="#id#">
</CFIF>
</body>
</CFIF>
So this admin page just calls the method and passes the ID along to it. Since all of
my edit/admin/design methods seem to only require one ID (the thing I am editing) I made
it inflexible to other attributes. In other words, we could have built this so that the
admin.cfm page could pass other attributes along as well, but the popup window is now
limited to one thing: the ID, which is always the primary key of the thing we are looking
up and editing. In this case, that thing is an announcement.
Notice that the JavaScript in the admin window will refresh the Web site over and over
again (look at the onunload event in the body tag). This will allow your user to go back
and see changes immediately and will help avoid confusion. Also, note that the window will
aways get focus (look at the onload event in the body tag). I put that in because users
kept minimizing the window and then wondering why it did not pop up the next time they went
to edit some content.
By now, you should understand how the basic content management works in the application.
There is, however, one last topic to consider and that is how a page receives its menus look
and feel.
Page Data
The last topic we will discuss is the look and feel of the Web site. Should menu items be
static? Of course not! Menus change as the content changes. All of these changes should be
handled through the same design mode capabilities that we focused on a few minutes ago.
Let's look at the menu systems in TERA. The menu information in TERA are stored in two
database tables: topMenu and sideMenu. The actual links in these tables
are contained in a table called links. These tables are not completely normalized.
True database experts would be appalled at my storing a list in the tables. It was, however,
a quick solution to a complicated problem: ordering the appearance of links in each menu.
Every page in the application is registered in the database. To that end, each page
much have a unique name in this application. When a page runs, two things happen. First,
the page gets its own information from the Page table to see what topMenu and
SideMenu it has. Then it calls the display methods for those menus. The display methods
can be as sophisticated as needed. In TERA, they are sophisticated enough to look at the
PageID and highlight that link differently based on the page we are on. This is the reason
for the highlightOn column in the link table.
Instead of looking at both the top and the side menus, let's analyze the side menu only,
since the two are basically the same idea.
The menu code in Announcement.cfm (in doc)
<!--- we already called a method called getPageData which returned the side menu
for this page in request.stPage.sideMenuID --->
<CFMODULE
TEMPLATE="/tera/invoker.cfm"
object="sideMenu"
METHOD="getMenuLinks"
sideMenuID="#request.stPage.sideMenuID#"
r_aLinks = "aLinks">
<CFMODULE
TEMPLATE="/tera/invoker.cfm"
object="sideMenu"
METHOD="showMenuLinks">
getMenuLinks (method of sideMenu)
<CFMODULE TEMPLATE="/tera/initializeMethod.cfm">
<CFPARAM NAME="attributes.r_alinks" DEFAULT="aLinks">
<CFQUERY NAME="qGetLinkIDList" DATASOURCE="#request.teraDSN#">
select sideMenu.name, sideMenu.linkIDList from sideMenu
where sideMenuID = #attributes.sideMenuID#
</CFQUERY>
<CFQUERY NAME="qGetLinkData" DATASOURCE="#request.teraDSN#">
select * from link where linkID in (#valuelist(qGetLinkIDLIst.linkIDList)#) and type = 'side'
</CFQUERY>
<!--- put 'em in the right order --->
<CFSET stOrder = StructNew()>
<CFLOOP FROM="1" TO="#qGetLinkData.recordcount#" INDEX="p">
<CFSET stOrder[qGetLinkData.linkID[p]] = StructNew()>
<CFSET stOrder[qGetLinkData.linkID[p]].linkID=qGetLinkData.linkID[p]>
<CFSET stOrder[qGetLinkData.linkID[p]].name=qGetLinkData.name[p]>
<CFSET stOrder[qGetLinkData.linkID[p]].url=qGetLinkData.url[p]>
<CFSET stOrder[qGetLinkData.linkID[p]].highlightON=qGetLinkData.highlightON[p]>
</CFLOOP>
<CFSET aTemp =ArrayNew(1)>
<CFSET counter=1>
<CFLOOP LIST="#valueList(qGetLinkIDList.linkIDList)#" INDEX="o">
<CFSET aTemp[counter] = stOrder[o]>
<CFSET counter=counter+1>
</CFLOOP>
<CFSET 'request.#attributes.r_aLinks#' = aTemp>
showMenuLinks.cfm (method of sideMenu)
<CFMODULE TEMPLATE="/tera/initializeMethod.cfm">
<CFIF request.session.ADMIN AND request.session.DESIGN>
<A HREF="#attributes.url#" onclick="return popup('admin.cfm?object=sideMenu&method=edit&id=#request.stPage.sideMenuID#')>
click here to edit the side menu
</A>
<!--- we don't look at the edit screens for these menu's but they just let you modify / reorder --->
</CFIF>
<CFIF NOT ArrayLen(request.alinks)>
<BR>
<!--- no links assigned --->
<CFELSE>
<CFLOOP FROM="1" TO="#arrayLen(request.aLinks)#" INDEX="i">
<CFOUTPUT>
<TABLE border="0" cellpadding="1" cellspacing="0">
<TR>
<TD NOWRAP>
<A HREF="#request.alinks[i].url#">#request.alinks[i].name#</A>
</td>
</tr>
</table>
</CFOUTPUT>
</CFLOOP>
</CFIF>
Conclusion
This document has been an introductory guide to an alternative programming technique using
ColdFusion. There are many different avenues that can be traveled using this technique and
techniques similar to it. There is never one way to build an application, and this is especially
true on the Web, where you often combine four or five different technologies such as JavaScript,
HTML, server logic, and SQL to make any application work.
If you have any questions about this material, you can send questions to:
talkback@macromedia.com.
|