Accessibility
 
Home / Developer Center / ColdFusion MX Application Developer Center

ColdFusion Article

Icon or Spacer Icon or Spacer Icon or Spacer
Ben Forta photo
Ben Forta
Senior Product Evangelist
for ColdFusion
www.forta.com
 
Previous Columns
· Using ColdFusion Tags in CFScript by Kevin Towes
· Use CFCs Properly by Ben Forta
· Building a Macromedia Flash interface for content management
by Kevin Towes
· Security best practices: Authenticating and authorizing against
NT domains with ColdFusion MX

by Rob Rusher
 
On using XML well: Creating a dynamic XML menu


Why XML?

Not long ago, I started every presentation on XML explaining that it was not a new page markup language and that it did not in and of itself replace HTML. Fortunately, we've made some progress of late–developers now understand that XML is nothing more than a data sharing mechanism–simple as that. What makes XML exciting is that it makes sharing data safe and very portable–and that's it.

In other words, while you wouldn’t format web pages in XML, you may definitely store content in XML, especially when different systems need to share, access, or manipulate content.

But manipulating XML data is not trivial, unless you are using Macromedia ColdFusion MX. ColdFusion MX has built-in support for manipulating XML, and in true form, manipulating CFML-based XML is anything but complex. This means that ColdFusion developers can choose XML as a very real and very viable option for storing and sharing data.

When To Use XML, and When Not To
So, when do you use XML? Obviously, you wouldn’t want to store entire user directories in XML files. That would make accessing the content in them difficult (and cause performance problems, too)–databases and LDAP servers are far better suited for this task. Similarly, you wouldn’t want to store catalogs, products, and customer data in plain XML files–again, databases are a far better choice for this type of data.

But what if you have simpler data? For example, what if you need to store a few configuration options for your application, such as: data source, timeout values, and other settings? You wouldn’t want those in a database (besides, you couldn't store the name of a data source in a database anyway). Is XML an option for this type of data? Possibly. Look at the following:

 
 
<application name="MyApp">
    <datasource>MyDSN</datasource> 
    <timeout>90</timeout>
</application>

This is a simple set of configuration information. You could store this information in a file (perhaps named config.xml) so that your application could read it (likely in the Application.cfm file), parse it, and save it.

That works, but is it worth the effort? There are simpler file formats, such as the INI type file (popularized by Microsoft Windows):

[MyApp]

datasource=MyDSN

timeout=90

You could save the above information in an INI file (perhaps config.ini). Then, with a single function call to GetProfileString(), you can read the file, parse it, extract the value, and return it so the application can save it in a variable. Of the two options I mention, the INI file option is cleaner and simpler.

I am not suggesting that any file format is always better than any other, but I am definitely suggesting that XML is not always the right answer to every problem–despite the hype.

So when does XML make sense? Let's look at a problem.

The Problem
Every application needs a menu, be it a simple menu with a few options, or a complex menu with numerous sub-menus and options. Usually, you create menus in HTML, DHTML, Macromedia Flash MX, or another client-side technology. A menu consists of two things: data (the options and selections) and formatting (the client-side presentation code).

What if you want to create a flexible menuing system that …

  • … supports unlimited options and levels of nesting?
  • … allows you to change content easily without requiring changes to UI code?
  • … allows you to completely replace the UI without affecting the content?
  • … could even generate multiple UIs driven by the same content (maybe Macromedia Flash and DHTML versions)?

To do this, first you would separate the content (the menu options) from the presentation. Then, you would store the menu data. And finally, you would need to figure out a simple way to parse and process the data so that any client technology can use it.

XML to the Rescue
Let's start with a simple menu–an unordered list–with three options, as seen in Figure 1.

 
Figure 1: A simple menu with three options
Figure 1: A simple menu with three options

.
The HTML for the simple menu might look like this:

<UL>
<LI><A HREF="link1.cfm">Item 1</A></LI>
<LI><A HREF="link2.cfm">Item 2</A></LI>
<LI><A HREF="link3.cfm">Item 3</A></LI>
</UL>

If there are three menu items and a link for each, how should you store this information? It could go in a database, but once you add sub-menus, the relationship between the items and menus becomes rather complex–which brings us to choosing XML. Look at the following:

<menu>
<item>
<text>Item 1</text>
<link>link1.cfm</link>
</item>
<item>
<text>Item 2</text>
<link>link2.cfm</link>
</item>
<item>
<text>Item 3</text>
<link>link3.cfm</link>
</item>
</menu>

This XML snippet defines a menu (using the <menu> and </menu> tags) and includes three items in it. Each <item> tag defines the text and link for each link menu item.

Why is this format better than a database table or an INI file? Consider this next example, which is a menu with a nested sub-menu:

<menu name="Menu">
<item>
<text>Home</text>
<link>/index.cfm</link>
</item>
<menu name="Products">
<item>
<text>ColdFusion</text>
<link>/products/cf.cfm</link>
</item>
<item>
<text>Flash</text>
<link>/products/flash.cfm</link>
</item>
</menu>
<item>
<text>Search</text>
<link>/search.cfm</link>
</item>
<item>
<text>Login</text>
<link>/logout.cfm</link>
</item>
</menu>

In this example, the top level menu has four menu options–the second menu option is a sub-menu, which has two menu options of its own. As you can see, using XML to store data in this example provides infinite flexibility–you can nest menus and items as you need to, indefinitely. In other words, XML provides the necessary structure to store the data and the required flexibility to store any combination of that data.

Processing XML
Now that you have a workable XML data format, you’ll learn how to write code that can access the data. As I already mentioned, ColdFusion MX features powerful XML processing support, and it only takes a single function call to read the XML:

<CFSET menu=XMLParse(menu_xml)>

The XMLParse() function takes an XML document (such as the one above, presumably already read into a ColdFusion variable using the cffile tag), and returns a structure containing all of the XML data. You can use the cfdump tag to display the complete XML structure (as seen in Figure 2).

 
Figure 2:  Using the XMLParse() function to read the menu.
Figure 2: Using the XMLParse() function to read menu_xml and the cfdump tag to display the structure’s contents.
 

For example, if you save the above XML as menu.xml, you could use the following CFML code to read and output the contents with the cfdump tag:

<!--- menu.xml in current directory --->
<CFSET cur_dir=
GetDirectoryFromPath(GetTemplatePath())>
<CFSET menu_file="#cur_dir#menu.xml">
<!--- Read XML file --->
<CFFILE ACTION="read"
FILE="#menu_file#"
VARIABLE="menu_data">
<!--- Parse it --->
<CFSET menu=XMLParse(menu_data)>
<!--- Dump it --->
<CFDUMP VAR="#menu#">

Reading the XML file and turning it into a usable CFML structure is simple. Parsing and processing all the data complicate the process, however. To get specific values requires that you loop (with the cfloop tag) through them a lot for an unknown number of loops (and nested loops) at that.

For example, if you were at the top-level menu, you would loop through its values to find each item. Then you must traverse through the sub-menu and its items, and possibly its sub-menus with their items and so on. In other words, you need code that can process any number of levels of nesting. This is a job for recursion:

<CFFUNCTION NAME="GetItem"
RETURNTYPE="string"
OUTPUT="no">
<CFARGUMENT NAME="menu" REQUIRED="yes">
<!--- Local variables --->
<CFSET VAR i1=0>
<CFSET VAR i2=0>
<CFSET VAR item_name="">
<CFSET VAR item_link="">
<!--- Loop through menu items --->
<CFLOOP FROM="1"
TO="#ArrayLen(menu)#"
INDEX="i1">
<!--- Is this an ITEM or a MENU? --->
<CFIF menu[i1].XMLName IS "item">
<!--- It's an ITEM --->
<CFLOOP FROM="1"
TO="#ArrayLen(menu[i1].XMLChildren)#"
INDEX="i2">
<CFIF menu[i1].XMLChildren[i2].XMLName
IS "text">
<CFSET item_name=
menu[i1].XMLChildren[i2].XMLText>
<CFELSEIF menu[i1].XMLChildren[i2].XMLName
IS "link">
<CFSET item_link=
menu[i1].XMLChildren[i2].XMLText>
</CFIF>
</CFLOOP>
<!--- Display it --->
<CFOUTPUT>
<A HREF="# item_link#"># item_name#</A>
</CFOUTPUT>
<!--- It's a SUBMENU --->
<CFELSEIF menu[i1].XMLName IS "menu">
<!--- Start submenu --->
<CFOUTPUT>#menu[i1].XMLAttributes.name#<CFOUTPUT>
<!--- Recurse to get child submenu items --->
<CFSET GetItem(menu[i1].XMLChildren)>
</CFIF>
</CFLOOP>
</CFFUNCTION>

In this example, the GetItem() function takes a menu structure as a parameter. It loops through the menu (treating it as an array) and looks at each item. If the item is a menu item (XMLName IS "item"), then it displays it. But if it is a menu (shown in the code above as: XMLName IS "menu"), then the GetItem() function calls the GetItem() function, passing it the sub-menu. This is an example of recursion. The second call to the GetItem() function will process the sub-menu (calling the GetItem() function again if needed), and then return to the caller GetItem() function when it completes.

By the time the cfloop tag finishes processing, it will have read and displayed all menu items.

Putting it All Together
But we're not done yet. The code above actually writes the menu output. When I started, I said that the menuing system had to be UI independent–capable of rendering all sorts of UI to any client, and does not depend on any one UI.

The menu parsing code can traverse menus to locate and extract items and sub-menus. It must do that regardless of what it finally outputs–the processing stays the same, but the menu changes based on changes to the data. If the menu code builds an HTML unordered list, then it must insert the <UL> and <LI> tags, if it builds a more complex menu, then it must insert other code. But that should have no impact on processing the core XML data.

Imagine if you were to call this XML processing function, then pass to it the name of another separate function that applies the processing. To do this, you would write a ColdFusion UDF (user-defined function) that formats the menu as you wish, and then pass that function to the menuing UDF. Each time the menuing UDF finds a menu or item, it makes a call back to the formatting UDF so that it can render the content. Does it sound complicated? It isn't. What I’ve described is a “callback function.”

This leads us to one of the most powerful (and least understood) features of ColdFusion UDFs–the ability to pass a function as a parameter to a function.

The following menuing code is not tied to (does not depend on) any UI at all. Any file that must generate a menu includes this file (menufunc.cfm):

<!---
menufunc.cfm
XML based menuing functions.
Do NOT invoke GetItem() directly, the only entry point
should be BuildMenu().

Ben Forta
--->

<!---
GetItem
Parses XML and gets each item, called recursivley
for nested menus.
This function should not be called directly, it should
only be called by the primay menu interface and by
itself.
--->
<CFFUNCTION NAME="GetItem" RETURNTYPE="string" OUTPUT="no">
	<CFARGUMENT NAME="menu" REQUIRED="yes">
	<CFARGUMENT NAME="callback" REQUIRED="yes">

	<!--- Local variables --->
	<CFSET VAR result="">
	<CFSET VAR i1=0>
	<CFSET VAR i2=0>
	<CFSET VAR item_name="">
	<CFSET VAR item_link="">
<!--- Loop through menu items --->
<CFLOOP FROM="1"
TO="#ArrayLen(menu)#"
INDEX="i1">
<!--- Is this an ITEM or a MENU? --->
<CFIF menu[i1].XMLName IS "item">
<!--- It's an ITEM, loop to get TEXT and LINK --->
<CFLOOP FROM="1"
TO="#ArrayLen(menu[i1].XMLChildren)#"
INDEX="i2">
<CFIF menu[i1].XMLChildren[i2].XMLName IS "text">
<CFSET item_name=menu[i1].XMLChildren[i2].XMLText>
<CFELSEIF menu[i1].XMLChildren[i2].XMLName IS "link">
<CFSET item_link=menu[i1].XMLChildren[i2].XMLText>
</CFIF>
</CFLOOP>
<!--- Now pass this one to the callback function --->
<CFSET result=result & callback("item",
item_name,
item_link)>
<!--- It's a SUBMENU --->
<CFELSEIF menu[i1].XMLName IS "menu">
<!--- Start submenu --->
<CFSET result=result &
callback("submenu_start", menu[i1].XMLAttributes.name)>
<!--- Recurse to get child submenu items --->
<CFSET result=result & GetItem(menu[i1].XMLChildren, callback)>
<!--- End submenu --->
<CFSET result=result & callback("submenu_end")>
</CFIF>
</CFLOOP>
<!--- And return it --->
<CFRETURN result>
</CFFUNCTION> <!---
BuildMenu
This is the menu entry point. Pass it the XML for the menu
and a callback function and it does the rest. It returns
a complete menu as a string (built by the callback function
calls).
--->
<CFFUNCTION NAME="BuildMenu" RETURNTYPE="string" OUTPUT="no">
<CFARGUMENT NAME="menu_xml" TYPE="string" REQUIRED="yes">
<CFARGUMENT NAME="callback" REQUIRED="yes">
<!--- Local variables --->
<CFSET VAR menu=XMLParse(menu_xml)>
<CFSET VAR meta_data="">
<CFSET VAR proceed="yes">

<!--- Make sure "callback" is a valid UDF --->
<CFIF NOT IsCustomFunction(callback)>
<CFTHROW MESSAGE="Callback function must be a UDF">
</CFIF>
<!--- Get callback meta data --->
<CFSET meta_data=GetMetaData(callback)>
<!--- Now make sure callback returns right type --->
<CFIF meta_data.returntype IS NOT "string">
<CFTHROW MESSAGE="Callback function must return a string">
</CFIF>

<!--- Make sure it accepts the right params --->
<CFIF ArrayLen(meta_data.parameters) IS NOT 3
OR meta_data.parameters[1].type IS NOT "string"
OR meta_data.parameters[2].type IS NOT "string"
OR meta_data.parameters[3].type IS NOT "string">
<CFTHROW MESSAGE="Callback function must accept three string arguments">
</CFIF>

<!--- Build and return menu --->
<CFRETURN callback("menu_start") &
GetItem(menu.menu.XMLChildren, callback) &
callback("menu_end")>
</CFFUNCTION>

I’ll start explaining this example from the bottom. The BuildMenu() function is the entry point to the menu system. To build a menu, call the BuildMenu() function and pass it the XML structure and the name of a callback function. First, the BuildMenu() function parses the XML. Next, it uses the IsCustomFunction() function to verify that the callback is indeed a UDF, and that it used GetMetaData() to obtain information about the function–this verifies that it is valid (by valid, it must accept three strings as parameters, and return a string when done). The BuildMenu() function throws error messages if any of the checks fail. If it doesn’t fail, the BuildMenu() function calls the callback function and informs it that it is starting the menu, calls the GetItem() function to process the items (passing it the callback function), and then calls the callback function for a second time to inform it that it has completed menu processing.

Note: Note my special thanks to Mr. UDF (otherwise known as Ray Camden) for lots of useful UDF advice and pointers. Ray wrote Writing User-Defined Functions in ColdFusion MX, in the ColdFusion MX Application Developer Center in April.

The GetItem() function loops through the menu. Each time it finds an item, it passes it to the callback function for processing (and the callback function returns a processed string); and each time it finds a menu, it calls the callback function and then calls itself recursively (and retrieves a completed sub-menu that the recursive GetItem() call creates).

By the time the BuildMenu() function finishes processing, the callback code has incrementally built a complete string containing an entire menu.

All that is left is the callback and invocation code. Here is the menutest.cfm file:

<!--- Include menuing functions --->
<CFINCLUDE TEMPLATE="menufunc.cfm">

<!---
MenuAsList
This is the callback function, this one creates a nested
unordered list, it can be replaced with any other function
(the name of which must be passed as a parameter to
BuildMenu().
When called three parameters will be passed to it:
TYPE: One of the following:
MENU_START = start of menu
MENU_END = end of menu
SUBMENU_START = start of submenu
SUBMENU_END = end of submenu
ITEM = menu item
TEXT: Item text (only for SUBMENU_START and ITEM)
LINK: Link URL (only for ITEM)
--->
<CFFUNCTION NAME="MenuAsList" RETURNTYPE="string" OUTPUT="no">
<CFARGUMENT NAME="type" TYPE="string" DEFAULT="">
<CFARGUMENT NAME="text" TYPE="string" DEFAULT="">
<CFARGUMENT NAME="link" TYPE="string" DEFAULT="">

<!--- Locla variable for result --->
<CFSET VAR result="">

<!--- Build result based on type --->
<CFSWITCH EXPRESSION="#type#">
<CFCASE VALUE="menu_start">
<CFSET result="<UL>">
</CFCASE>
<CFCASE VALUE="menu_end">
<CFSET result="</UL>">
</CFCASE>
<CFCASE VALUE="submenu_start">
<CFSET result="<LI>#text#<UL>">
</CFCASE>
<CFCASE VALUE="submenu_end">
<CFSET result="</UL></LI>">
</CFCASE>
<CFCASE VALUE="item">
<CFSET result="<LI><A HREF=""#link#"">#text#</A></LI>">
</CFCASE>
</CFSWITCH>

<!--- And return it --->
<CFRETURN result>
</CFFUNCTION>

<!---
Here's the test menu code.
First get the path to the menu.xml file, then read it,
then pass it to BuildMenu() to do just that, and then
finally display it.
--->

<!--- menu.xml in current directory --->
<CFSET cur_dir=GetDirectoryFromPath(GetTemplatePath())>
<CFSET menu_file="#cur_dir#menu.xml">

<!--- Read XML file --->
<CFFILE ACTION="read"
FILE="#menu_file#"
VARIABLE="menu_data">

<!--- Do it --->
<CFSET menu=BuildMenu(menu_data, MenuAsList)>

<!--- Display it --->
<CFOUTPUT>#menu#</CFOUTPUT>

First, the example uses the cfinclude tag to include the menuing functions.

Next, I’ll explain my callback function, MenuAsList() (which, as its name suggests, creates menus as lists). My code never calls the MenuAsList() function. Rather, my code passes it to BuildMenu() (which in turn passes it to GetItem()) and calls it from the function as needed. MenuAsList() accepts three parameters (which the menuing code passes to it) – the type of call (such as, the start of the menu, the end of the menu, the item, and so forth), the text, and link (if appropriate). The bulk of the function is a cfswitch tag statement which builds lists based on the type. Therefore, if the type was "item," and the text were "text 1," and the link "link1.cfm," the MenuAsList() would return the following:

<LI><A HREF="link1.cfm">text 1</A></LI>

The callback function never writes output. Rather, it returns formatted output. The distinction is important. The BuildMenu() call does just that, it builds a menu, it does not display it (since displayed content varies based on which client technology uses it).

To build the menu, use the following code:

<!--- Do it --->
<CFSET menu=BuildMenu(menu_data, MenuAsList)>

The BuildMenu() function returns a complete menu as a string, which you can display as needed like this:

<!--- Display it --->
<CFOUTPUT>#menu#</CFOUTPUT>

Here is the menu.xml file (which contains eleven options and menus three levels deep):

<menu name="Menu">
<item>
<text>Home</text>
<link>/index.cfm</link>
</item>
<menu name="Products">
<item>
<text>JRun</text>
<link>/products/jrun.cfm</link>
</item>
<menu name="ColdFusion">
<item>
<text>ColdFusion Professional</text>
<link>/products/cfpro.cfm</link>
</item>
<item>
<text>ColdFusion Enterprise</text>
<link>/products/cfent.cfm</link>
</item>
<item>
<text>ColdFusion for J2EE</text>
<link>/products/cfj2ee.cfm</link>
</item>
</menu>
<item>
<text>Flash</text>
<link>/products/flash.cfm</link>
</item>
<item>
<text>Dreamweaver</text>
<link>/products/dw.cfm</link>
</item>
</menu>
<item>
<text>Search</text>
<link>/search.cfm</link>
</item>
<item>
<text>Login</text>
<link>/logout.cfm</link>
</item>
</menu>

And what HTML does it generate?

<UL><LI><A HREF="/index.cfm">Home</A></LI><LI>Products<UL><LI><A HREF="/products/jrun.cfm">JRun</A></LI><LI>ColdFusion<UL><LI><A HREF="/products/cfpro.cfm">ColdFusion Professional</A></LI><LI><A HREF="/products/cfent.cfm">ColdFusion Enterprise</A></LI><LI><A HREF="/products/cfj2ee.cfm">ColdFusion for J2EE</A></LI></UL></LI><LI><A HREF="/products/flash.cfm">Flash</A></LI><LI><A HREF="/products/dw.cfm">Dreamweaver</A></LI></UL></LI><LI><A HREF="/search.cfm">Search</A></LI><LI><A HREF="/logout.cfm">Login</A></LI></UL>

Executing the test code displays a complete nested menu (as seen in Figure 3).

 
Figure 3
Figure 3: Dynamic XML menu with menu items and nested menu items.
 

To add new menu items, add entries to menu.xml. And for alternate client formats, you only need an alternate callback function. In fact, you could create multiple callback functions, allowing you to dynamically pick the one you want, based on the UI you need to render.

It's as simple as that.

As seen here, XML is suitable for storing data when you need to balance structure and flexibility. XML is not always the right fit for every problem. Resist the urge to use XML where it does not fit. XML fit the menuing system problem discussed in this article well, and a set of UDFs (utilizing recursion and function callbacks) make the system clean and highly reusable (while hiding the complexity involved in manipulating highly dynamic XML data).

You can download the files for this article here.

Source files:

Download the components xml_menu (3K)
 

Note: You may be wondering why I did not use XSL to provide formatting. Well, I could have done so. But, this interface is much simpler and cleaner. It is also far more flexible, and frankly, CFML is way more fun than XSLT too.

 

About the author
Ben Forta is Macromedia's senior product evangelist and the author of numerous books, including ColdFusion 5 Web Application Construction Kit and its sequel, Advanced ColdFusion 5 Development. Ben is working on several new titles on ColdFusion MX. For more information visit www.forta.com. You can contact Ben at ben@forta.com.