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). |