Macromedia Coldfusion MX 7 Web Application Construction Kit

So far, we have concentrated on creating custom tags that display information, such as the <cf_ShowMovieCallout> custom tag. Many of the custom tags you will write are likely to be similar to it in that they will be in charge of wrapping up several display-related concepts (querying the database, including formatting, outputting the information, and so on).

However, you can also create custom tags that have different purposes in life: to process or gather information. This type of custom tag generally doesn't generate any HTML to be displayed on the current page. Instead, these tags perform some type of processing, often returning a calculated result to the calling template.

NOTE

You might call these tags nonvisual, or perhaps number crunchers, to set them apart from tags that generate something visual, such as the <cf_ ShowMovieCallout> and <cf_HelloWorldMessage> examples you have already seen.

Introducing the CALLER Scope

ColdFusion defines two special variable scopes that come into play only when you're creating custom tags:

  • The ATTRIBUTES Scope You have already learned about this scope, which passes specific information to a custom tag each time it is used.

  • The CALLER Scope Gives a custom tag a way to set and use variables in the template in which you're using the tag (the calling template).

The special CALLER scope is easy to understand and use. Within a custom tag template, you prefix any variable name with CALLER (using dot notation) to access the corresponding variable in the calling template. Through the CALLER scope, you have full read-write access to all variables known to the calling template, meaning that you can set variables as well as access their current values. For instance, you can set variables in the calling template using an ordinary <cfset> tag.

Returning Variables to the Calling Template

Let's say you're writing a custom tag called <cf_PickFeaturedMovie>, which will choose a movie from the list of available films. In the calling template, you plan on using the tag like so:

<cf_PickFeaturedMovie>

Inside the custom tag template (PickFeaturedMovie.cfm), you could set a variable using a special CAL

LER prefix, such as this:

<cfset CALLER.featuredFilmID = 5>

This prefix would make featuredFilmID available in the calling template as a normal variable. For instance, this code snippet would call the custom tag and then output the value of the variable it returns:

<cf_PickFeaturedMovie> <cfoutput> The featured Film ID is: #featuredFilmID# </cfoutput>

Of course, using these snippets, the value of featuredFilmID would always be 5, so the custom tag wouldn't be all that useful. Listing 23.10 shows how to expand the previous snippets into a useful version of the <cf_PickFeaturedMovie> custom tag. The purpose of the code is to select a single film's ID number in such a way that all films are rotated evenly on a per-session basis.

Listing 23.10. PickFeaturedMovie1.cfmSetting a Variable in the Calling Template

<!--- Filename: PickFeaturedMovie.cfm Author: Nate Weiss (NMW) Purpose: Creates the <CF_PickFeaturedMovie> custom tag ---> <!--- Tag Attributes ---> <!--- Use "ows" datasource by default ---> <cfparam name="ATTRIBUTES.dataSource" type="string" default="ows"> <cflock scope="session" type="exclusive" timeout="30"> <!--- List of movies to show (list starts out empty) ---> <cfparam name="SESSION.movieList" type="string" default=""> <!--- If this is the first time we're running this, ---> <!--- Or we have run out of movies to rotate through ---> <cfif SESSION.movieList eq ""> <!--- Get all current FilmIDs from the database ---> <cfquery name="getFilmIDs" datasource="#ATTRIBUTES.dataSource#"> SELECT FilmID FROM Films ORDER BY MovieTitle </cfquery> <!--- Turn FilmIDs into a simple comma-separated list ---> <cfset SESSION.movieList = valueList(getFilmIDs.FilmID)> </cfif> <!--- Pick the first movie in the list to show right now ---> <cfset thisMovieID = listFirst(SESSION.movieList)> <!--- Re-save the list, as all movies *except* the first ---> <cfset SESSION.movieList = listRest(SESSION.movieList)> </cflock> <!--- Return chosen movie to calling template ---> <cfset CALLER.featuredFilmID = thisMovieID>

The <cfparam> tag at the top of this custom tag template establishes a single optional attribute for the tag called dataSource, which will default to ows if not provided explicitly. We begin by using a <cflock> tag to ensure that the code here is thread-safe. Since we are reading and writing to a shared scope, it's important that we control how the code is accessed. We use <cfparam> to default the list of movies to an empty string. If the SESSION variable already exists, this line will do nothing. If the movieList variable equals an empty string, it means we either just created the variable, or the list of movies has already been used. If this is the case, we run a query to grab all the film IDs. We then use the valueList() function to copy out the list of IDs from the query. The listFirst() function is used to get the first film ID from the list, and listRest is used to remove the ID and resave it to the SESSION.movieList variable. Because the final <cfset> uses the special CALLER scope, the featured movie that the tag has chosen is available for the calling template to use normally.

Listing 23.11 shows how you can put this version of the <cf_PickFeaturedMovie> custom tag to use in actual code.

Listing 23.11. UsingPickFeaturedMovie1.cfmUsing a Variable Set by a Custom Tag

<!--- Filename: UsingPickFeaturedMovie1.cfm Author: Nate Weiss (NMW) Purpose: Shows how <cf_PickFeaturedMovie> can be used in a ColdFusion page ---> <html> <head><title>Movie Display</title></head> <body> <!--- Page Title and Text Message ---> <h2>Movie Display Demonstration</h2> <p>The appropriate "Featured Movie" can be obtained by using the <b>&lt;cf_PickFeaturedMovie&gt;</b> tag. The featured movie can then be displayed using the <b>&lt;cf_ShowMovieCallout&gt;</b> tag.<br> <!--- Pick rotating Featured Movie to show, via Custom Tag ---> <cf_PickFeaturedMovie> <!--- Display Film info as "callout", via Custom Tag ---> <cf_ShowMovieCallout film> </body> </html>

NOTE

The CALLER scope is for use only within custom tag templates. Don't use it in your ordinary ColdFusion templates. Doing so won't generate an error message, but it could produce unexpected results.

NOTE

If you are calling a custom tag from within another custom tag, the CALLER scope of the innermost tag will refer to the local variables in the first custom tag template, not the variables in the top-level page template. To access the variables of the top-level template from the innermost tag, you must use CALLER.CALLER.VariableName instead of CALLER.VariableName. In some cases, this can be a pain; the REQUEST scope provides an effective solution, as explained in the section "The REQUEST Scope," later in this chapter.

First, the <cf_PickFeaturedMovie> custom tag from Listing 23.10 is called. As the custom tag executes, it selects the film ID it feels is appropriate and saves the value in the featuredFilmID variable in the calling template (which, in this case, is Listing 23.11). Next, the featured movie is actually displayed to the user, using the <cf_ShowMovieCallout> custom tag presented earlier in this chapter.

NOTE

These two custom tags (<cf_PickFeaturedMovie> and <cf_ShowMovieCallout>) each do something useful on their own and can be used together, as shown here. As you design custom tags for your applications, this type of synergy between tags is a nice goal to shoot for.

Of course, to make Listing 23.11 work, you need to enable session management by using an Application.cfc file. See Chapter 19 for details.

NOTE

The more a custom tag relies on variables in the calling template, the less modular it becomes. So, although the CALLER scope gives you read-write access to variables in the calling template, you should use it mainly for setting new variables, rather than accessing the values of existing ones. If you find that you are accessing the values of many existing variables in the calling template, maybe you should just be writing a normal <cfinclude> style template rather than a custom tag, or maybe the values should be passed into the tag explicitly as attributes.

Variable Names as Tag Attributes

In the version of the <cf_PickFeaturedMovie> custom tag shown in Listing 23.10, the selected film ID is always returned to the calling template as a variable named featuredFilmID. Allowing a custom tag to accept an additional attribute often helps determine the name of the return variable in which the custom tag will place information.

For instance, for the <cf_PickFeaturedMovie> custom tag, you might add an attribute called returnVariable, which determines the calling template to specify the variable in which to place the featured film's ID number.

To use the tag, change this line from Listing 23.10:

<!--- Pick rotating featured movie to show via custom tag ---> <cf_PickFeaturedMovie>

to this:

<!--- Pick rotating featured movie to show via custom tag ---> <cf_PickFeaturedMovie returnVariable="FeaturedFilmID">

This makes the custom tag less intrusive because it doesn't demand that any particular variable names be set aside for its use. If for whatever reason the developer coding the calling template wants the selected film to be known as myFeaturedFilmID or showThisMovieID, he or she can simply specify that name for the returnVariable attribute. The calling template is always in control.

NOTE

Also, code that uses the <cf_PickFeaturedMovie> tag will be a bit more self-documenting and easier to understand because it is now evident from where exactly the FeaturedFilmID variable is coming.

NOTE

If you think about it, a number of ColdFusion's own CFML tags use the same technique. The most obvious example is the <cfquery> tag's name attribute, which tells the tag which variable to store its results in. The name attributes of the <cfdirectory> and <cfsearch> tags are similar, as are the OUTPUT attribute for <cfwddx> and the VARIABLE attributes for <cffile> and <cfsaveoutput>. See Appendix B, "ColdFusion Tag Reference," for details.

Using <cfparam> with type="variableName"

You have already seen the <cfparam> tag used throughout this chapter to make it clear which attributes a custom tag expects and to ensure that the data type of each attribute is correct. When you want the calling template to accept a variable name as one of its attributes, you can set the type of the <cfparam> tag to variableName.

Therefore, the next version of the <cf_PickFeaturedMovie> custom tag will include the following lines:

<!--- Variable name to return selected FilmID as ---> <cfparam name="ATTRIBUTES.returnVariable" type="variableName">

When the <cfparam> tag is encountered, ColdFusion ensures that the actual value of the attribute is a legal variable name. If it's not, ColdFusion displays an error message stating that the variable name is illegal. This makes for a very simple sanity check. It ensures that the tag isn't being provided with something such as returnValue="My Name", which likely would result in a much uglier error message later on because spaces aren't allowed in ColdFusion variable names.

NOTE

In ColdFusion, variable names must start with a letter, and the other characters can only be letters, numbers, and underscores. Any string that doesn't conform to these rules won't get past a <cfparam> of type="variableName". What's nice is that if the rules for valid variable names changes in a future version of ColdFusion, you won't have to update your code.

Setting a Variable Dynamically

After you've added the <cfparam> tag shown previously to the <cf_PickFeaturedMovie> custom tag template, the template can refer to ATTRIBUTES.returnVariable to get the desired variable name. Now the final <cfset> variable in Listing 23.10 just needs to be changed so that it uses the dynamic variable name instead of the hard-coded variable name of featuredFilmID. Developers sometimes get confused about how exactly to do this.

Here's the line as it stands now, from Listing 23.10:

<!--- Return chosen movie to calling template ---> <cfset CALLER.featuredFilmID = thisMovieID>

People often try to use syntax similar like the following to somehow indicate that the value of ATTRIBUTES.returnVariable should be used to determine the name of the variable in the CALLER scope:

<!--- Return chosen movie to calling template ---> <cfset CALLER.#ATTRIBUTES.returnVariable# = thisMovieID>

Or they might use this:

<!--- Return chosen movie to calling template ---> <cfset #CALLER.##ATTRIBUTES.returnVariable### = thisMovieID>

These are not legal because ColdFusion doesn't understand that you want the value of ATTRIBUTES.returnVariable evaluated before <cfset> is actually performed. ColdFusion will just get exasperated, and display an error message.

Using Quoted <cfset> Syntax

ColdFusion provides a somewhat odd-looking solution to this problem. You simply surround the left side of the <cfset> expression, the part before the equals (=) sign, with quotation marks. This forces ColdFusion to first evaluate the variable name as a string before attempting to actually perform the variable setting. The resulting code looks a bit strange, but it works very well and is relatively easy to read.

So, this line from Listing 23.10:

<!--- Return chosen movie to calling template ---> <cfset CALLER.featuredFilmID = thisMovieID>

can be replaced with this:

<!--- Return chosen movie to calling template ---> <cfset "CALLER.#ATTRIBUTES.returnVariable#" = thisMovieID>

Listing 23.12 shows the completed version of the <cf_PickFeaturedMovie> custom tag. This listing is identical to Listing 23.10, except for the first and last lines, which are the <cfparam> line and the updated <cfset> line shown previously.

Listing 23.12. PickFeaturedMovie2.cfmRevised Version of <cf_PickFeaturedMovie>

<!--- Filename: PickFeaturedMovie2.cfm Author: Nate Weiss (NMW) Purpose: Creates the <CF_PickFeaturedMovie> custom tag ---> <!--- Tag Attributes ---> <!--- Variable name to return selected FilmID as ---> <cfparam name="ATTRIBUTES.returnVariable" type="variableName"> <!--- Use "ows" datasource by default ---> <cfparam name="ATTRIBUTES.dataSource" type="string" default="ows"> <cflock scope="session" type="exclusive" timeout="30"> <!--- List of movies to show (list starts out empty) ---> <cfparam name="SESSION.movieList" type="string" default=""> <!--- If this is the first time we're running this, ---> <!--- Or we have run out of movies to rotate through ---> <cfif SESSION.movieList eq ""> <!--- Get all current FilmIDs from the database ---> <cfquery name="getFilmIDs" datasource="#ATTRIBUTES.dataSource#"> SELECT FilmID FROM Films ORDER BY MovieTitle </cfquery> <!--- Turn FilmIDs into a simple comma-separated list ---> <cfset SESSION.movieList = valueList(getFilmIDs.FilmID)> </cfif> <!--- Pick the first movie in the list to show right now ---> <cfset thisMovieID = listFirst(SESSION.movieList)> <!--- Re-save the list, as all movies *except* the first ---> <cfset SESSION.movieList = listRest(SESSION.movieList)> </cflock> <!--- Return chosen movie to calling template ---> <cfset "CALLER.#ATTRIBUTES.returnVariable#" = thisMovieID>

Listing 23.13 shows how to use this new version of the custom tag. This listing is nearly identical to Listing 23.11, except for the addition of the returnVariable attribute. Note how much clearer the cause and effect now are. In Listing 23.11, the featuredFilmID variable seemed to appear out of nowhere. Here, it's very clear where the showThisMovieID variable is coming from.

Listing 23.13. UsingPickFeaturedMovie2.cfmUsing the returnVariable Attribute

<!--- Filename: UsingPickFeaturedMovie2.cfm Author: Nate Weiss (NMW) Purpose: Shows how <cf_PickFeaturedMovie2> can be used in a ColdFusion page ---> <html> <head><title>Movie Display</title></head> <body> <!--- Page Title and Text Message ---> <h2>Movie Display Demonstration</h2> <p>The appropriate "Featured Movie" can be obtained by using the <b>&lt;cf_PickFeaturedMovie2&gt;</b> tag. The featured movie can then be displayed using the <b>&lt;cf_ShowMovieCallout&gt;</b> tag.<br> <!--- Pick rotating Featured Movie to show, via Custom Tag ---> <cf_PickFeaturedMovie2 returnVariable="showThisMovieID"> <!--- Display Film info as "callout", via Custom Tag ---> <cf_showMovieCallout film> </body> </html>

Using the setVariable() Function

Another way to solve this type of problem is with the setVariable() function. This function accepts two parameters. The first is a string specifying the name of a variable; the second is the value you want to store in the specified variable. (The function also returns the new value as its result, which isn't generally helpful in this situation.)

So, this line from Listing 23.12:

<!--- Return chosen movie to calling template ---> <cfset "CALLER.#ATTRIBUTES.returnVariable#" = thisMovieID>

could be replaced with this:

<!--- Return chosen movie to calling template ---> <cfset temp = setVariable("CALLER.#ATTRIBUTES.returnVariable#", thisMovieID)>

Because the result of the function is unnecessary here, this line can be simplified to:

<!--- Return chosen movie to calling template ---> <cfset setVariable("CALLER.#ATTRIBUTES.returnVariable#", thisMovieID)>

Using struct Notation

One more way a custom tag can return information to the calling document is to simply treat the CALLER scope as a structure.

So, this line from Listing 23.12:

<!--- Return chosen movie to calling template ---> <cfset "CALLER.#ATTRIBUTES.returnVariable#" = thisMovieID>

could be replaced with this:

<!--- Return chosen movie to calling template ---> <cfset CALLER[ATTRIBUTES.returnVariable] = thisMovieID>

Either methodthe quoted <cfset> syntax mentioned previously, the SetVariable() method shown, or struct notationproduces the same results. Use whichever method you prefer.

Категории