Macromedia Coldfusion MX 7 Web Application Construction Kit
We have already has covered a lot of ground in this chapter. You have learned about cookies and client variables and how they can be used to make an application aware of its individual users and what they are doing. ColdFusion's Web application framework provides one more type of persistent variable to discuss: session variables. What Are Session Variables?
Session variables are similar to client variables in that they are stored on the server rather than in the browser's memory. Unlike client variables, however, session variables persist only for a user's current session. Later you'll learn exactly how a session is defined, but for now, think of it as synonymous with a user's visit to your site. So session variables should be seen as per-visit variables, whereas client variables are per-user variables intended to persist between each user's visits. Session variables aren't stored physically in a database or the server's Registry. Instead, they are stored in the server's RAM. This makes sense, considering that they are intended to persist for only a short time. Also, because ColdFusion doesn't need to physically store and retrieve the variables, you can expect session variables to work a bit more quickly than client variables. Enabling Session Variables
As with client variables, you must enable session variables using a Application.cfc file before you can use them in your code. Table 20.4 lists the additional attributes relevant to session variables. In general, all you need to do is specify a name and then set sessionManagement="Yes".
For example, to enable session management, you might use something such as this in your Application.cfc file: <!--- Name application and enable Session and Application variables ---> <cfset this.name="OrangeWhipSite"> <cfset this.sessionManagement="Yes"> NOTE Session variables can be disabled globally (for the entire server) in the ColdFusion Administrator. If the Enable Session Variables option on the Memory Variables page of the Administrator has been unchecked, you will not be able to use session variables, regardless of what you set the sessionManagement attribute to.
The CD-ROM for this book includes an Application3.cfc template, which enables session management. It is identical to the Application2.cfc template used earlier to enable client variables (Listing 20.5), except that sessionManagement is set to Yes, rather than to clientManagement. After you have enabled session variables using sessionMangement, you can start using them in your code. ColdFusion provides a special SESSION variable scope, which works similarly to the CLIENT and COOKIE scopes you are already familiar with. You can set and use session variables simply by using the SESSION prefix in front of a variable's name. For instance, instead of the CLIENT.lastSearch used in the SearchForm.cfm examples above, you could call the variable SESSION.lastSearch. The examples would still work in essentially the same way. The only difference in behavior would be that the memory interval of each user's last search would be short (until the end of the session), rather than long (90 days, by default). For something such as search results, the shorter memory provided by using session variables might feel more intuitive for the user. That is, a user might expect the search page to remember their last search phrase during the same visit, but they might be surprisedor irritatedif it remembered search criteria from weeks or months in the past. You will often find yourself using session and client variables together in the same application. Generally, things that should be remembered for only the current visit belong in session variables, whereas things that should be remembered between visits should be kept in client variables. Using Session Variables for Multiple-Page Data Entry
Session variables can be especially handy for data-entry processes that require the user to fill out a number of pages. Let's say you have been asked to put together a data-entry interface for Orange Whip Studios' intranet. The idea is for your users to be able to add new film records to the studio's database. A number of pieces of information will need to be supplied by the user (title, director, actors, and so on). The most obvious solution would be to just create one long, complex form. However, suppose further that you have been specifically asked not to do this because it might confuse the interns the company hires to do its data-entry tasks. After carefully considering your options, you decide to present the data-entry screens in a familiar wizard format, with Next and Back buttons the users can use to navigate between steps. However, it's important that nothing actually be entered into the database until the user has finished all the steps. This means the wizard must remember everything the user has entered, even though they may be moving freely back and forth between steps. Hmm. You could pass everything from step to step as hidden form fields, but that sounds like a lot of work, and it feels wrong to put the burden of remembering all that data on the client. You'd like to keep the information on the server side. You could create some type of temporary tables in your database, and keep updating the temporary values until the user is finished, but that also sounds like a lot of work. Plus, how would you keep the values separate for each user? And what if the user abandons the wizard partway through? The answer, of course, is to use session variables, which are perfect for this type of situation. You only need to track the information for a short time, so session variables are appropriate. Also, session variables aren't kept permanently on the server, so you won't be storing any excess data if the user doesn't finish the wizard. Maintaining Structures in the SESSION Scope
The following code snippet creates a new structure called SESSION.movWiz. It contains several pieces of information, most of which start out blank (set to an empty string). Because the variable is in the SESSION scope, a separate version of the structure is kept for each user, but only for the user's current visit. The stepNum value is in charge of tracking which step of the data-entry wizard each user is currently on: <cfif not isDefined("SESSION.movWiz")> <!--- If structure is undefined, create/initialize it ---> <cfset SESSION.movWiz = structNew()> <!--- Represents current wizard step; start at one ---> < cfset SESSION.movWiz.stepNum = 1> <!--- We will collect these from user; start blank ---> <cfset SESSION.movWiz.movieTitle = ""> <cfset SESSION.movWiz.pitchText = ""> <cfset SESSION.movWiz.directorID = ""> <cfset SESSION.movWiz.ratingID = ""> <cfset SESSION.movWiz.actorIDs = ""> <cfset SESSION.novWiz.starActorID = ""> </cfif> Updating the values in the SESSION.movWiz structure is simple enough. Assume for the moment that the wizard contains Back and Next buttons named goBack and goNext, respectively. The following snippet would increment the stepNum part of the structure by 1 when the user clicks the Next button, and decrement it by 1 if the user clicks Back: <!--- If user clicked Back button, go back a step ---> <cfif isDefined("FORM.goBack")> <cfset SESSION.movWiz.stepNum = SESSION.movWiz.stepNum - 1> <!--- If user clicked Next button, go forward one ---> <cfelseif isDefined("FORM.goNext")> <cfset SESSION.MovWiz.stepNum = SESSION.movWiz.stepNum + 1> </cfif>
The other values in the movWiz structure can be accessed and updated in a similar way. For instance, to present the user with a text-entry field for the new movie's title, you could use something such as this: <cfinput name="MovieTitle" value="#SESSION.movWiz.movieTitle#">
The input field will be pre-filled with the current value of the movieTitle part of the movWiz structure. If the previous snippet was in a form and submitted to the server, the value the user typed could be saved back into the movWiz structure using the following line: <cfset SESSION.movWiz.movieTitle = FORM.movieTitle>
Putting It All Together
The code in Listing 20.9 combines all the previous snippets into a simple, intuitive wizard interface that users will find familiar and easy to use. The listing is a bit longer than usual, but each part is easy to understand. The idea here is to create a self-submitting form page that changes depending on which step of the wizard the user is on. The first time the user comes to the page, they see Step 1 of the wizard. They submit the form, which calls the template again, they see Step 2, and so on. This data-entry wizard will collect information from the user in five steps, as follows:
The following examples use variables in the SESSION scope without locking the accesses by way of the <cflock> tag. While extremely unlikely, it is theoretically possible that simultaneous visits to this template from the same browser could cause the wizard to collect information in an inconsistent manner. See the section "Locking Revisited," later in this chapter. Listing 20.9. NewMovieWizard.cfm Using Session Variables to Guide through a Multistep Process
<!--- Filename: NewMovieWizard.cfm Created by: Nate Weiss (NMW) Please Note Session variables must be enabled ---> <!--- Total Number of Steps in the Wizard ---> <cfset numberOfSteps = 5> <!--- The SESSION.movWiz structure holds users' entries ---> <!--- as they move through wizard. Make sure it exists! ---> <cfif not isDefined("SESSION.movWiz")> <!--- If structure undefined, create/initialize it ---> <cfset SESSION.movWiz = structNew()> <!--- Represents current wizard step; start at one ---> <cfset SESSION.movWiz.stepNum = 1> <!--- We will collect these from user; start blank ---> <cfset SESSION.movWiz.movieTitle = ""> <cfset SESSION.movWiz.pitchText = ""> <cfset SESSION.movWiz.directorID = ""> <cfset SESSION.movWiz.ratingID = ""> <cfset SESSION.movWiz.actorIDs = ""> <cfset SESSION.movWiz.starActorID = ""> </cfif> <!--- If user just submitted MovieTitle, remember it ---> <!--- Do same for the DirectorID, Actors, and so on. ---> <cfif isDefined("FORM.movieTitle")> <cfset SESSION.movWiz.movieTitle = FORM.movieTitle> <cfset SESSION.movWiz.pitchText = FORM.pitchText> <cfset SESSION.movWiz.ratingID = FORM.ratingID> <cfelseif isDefined("FORM.directorID")> <cfset SESSION.movWiz.directorID = FORM.directorID> <cfelseif isDefined("FORM.actorID")> <cfset SESSION.movWiz.actorIDs = FORM.actorID> <cfelseif isDefined("FORM.starActorID")> <cfset SESSION.movWiz.starActorID = FORM.starActorID> </cfif> <!--- If user clicked "Back" button, go back a step ---> <cfif isDefined("FORM.goBack")> <cfset SESSION.movWiz.stepNum = URL.stepNum - 1> <!--- If user clicked "Next" button, go forward one ---> <cfelseif isDefined("FORM.goNext")> <cfset SESSION.movWiz.stepNum = URL.stepNum + 1> <!--- If user clicked "Finished" button, we're done ---> <cfelseif isDefined("FORM.goDone")> <cflocation url="NewMovieCommit.cfm"> </cfif> <html> <head><title>New Movie Wizard</title></head> <body> <!--- Show title and current step ---> <cfoutput> <b>New Movie Wizard</b><br> Step #SESSION.movWiz.StepNum# of #NumberOfSteps#<br> </cfoutput> <!--- Data Entry Form, which submits back to itself ---> <cfform action="NewMovieWizard.cfm?StepNum=#SESSION.movWiz.stepNum#" method="POST"> <!--- Display the appropriate wizard step ---> <cfswitch expression="#SESSION.movWiz.stepNum#"> <!--- Step One: Movie Title ---> <cfcase value="1"> <!--- Get potential film ratings from database ---> <cfquery name="getRatings" datasource="ows"> SELECT RatingID, Rating FROM FilmsRatings ORDER BY RatingID </cfquery> <!--- Show text entry field for title ---> What is the title of the movie?<br> <cfinput name="MovieTitle" SIZE="50" VALUE="#SESSION.movWiz.MovieTitle#"> <!--- Show text entry field for short description ---> <p>What is the "pitch" or "one-liner" for the movie?<br> <cfinput name="pitchText" size="50" value="#SESSION.movWiz.pitchText#"> <!--- Series of radio buttons for movie rating ---> <p>Please select the rating:<br> <cfloop query="getRatings"> <!--- Re-select this rating if it was previously selected ---> <cfset isChecked = ratingID EQ SESSION.movWiz.ratingID> <!--- Display radio button ---> <cfinput type="radio" name="ratingID" checked="#isChecked#" value="#ratingID#"><cfoutput>#rating#<br></cfoutput> </cfloop> </cfcase> <!--- Step Two: Pick Director ---> <cfcase value="2"> <!--- Get list of directors from database ---> <cfquery name="getDirectors" datasource="ows"> SELECT DirectorID, FirstName+' '+LastName As FullName FROM Directors ORDER BY LastName </cfquery> <!--- Show all Directors in SELECT list ---> <!--- Pre-select if user has chosen one ---> Who will be directing the movie?<br> <cfselect size="#getDirectors.recordCount#" query="getDirectors" name="directorID" display="fullName" value="directorID" selected="#SESSION.movWiz.directorID#"/> </cfcase> <!--- Step Three: Pick Actors ---> <cfcase value="3"> <!--- get list of actors from database ---> <cfquery name="getActors" datasource="ows"> SELECT * FROM Actors ORDER BY NameLast </cfquery> What actors will be in the movie?<br> <!--- For each actor, display checkbox ---> <cfloop query="GetActors"> <!--- Should checkbox be pre-checked? ---> <cfset isChecked = listFind(SESSION.movWiz.actorIDs, actorID)> <!--- Checkbox itself ---> <cfinput type="checkbox" name="actorID" value="#actorID#" checked="#isChecked#"> <!--- Actor name ---> <cfoutput>#nameFirst# #nameLast#</cfoutput><br> </cfloop> </cfcase> <!--- Step Four: Who is the star? ---> <cfcase value="4"> <cfif SESSION.movWiz.actorIDs EQ ""> Please go back to the last step and choose at least one actor or actress to be in the movie. <cfelse> <!--- Get actors who are in the film ---> <cfquery name="getActors" DATASOURCE="ows"> SELECT * FROM Actors WHERE ActorID IN (#SESSION.movWiz.ActorIDs#) ORDER BY NameLast </cfquery> Which one of the actors will get top billing?<br> <!--- For each actor, display radio button ---> <cfloop query="getActors"> <!--- Should radio be pre-checked? ---> <cfset isChecked = SESSION.movWiz.starActorID EQ actorID> <!--- Radio button itself ---> <cfinput type="radio" name="starActorID" value="#actorID#" checked="#isChecked#"> <!--- Actor name ---> <cfoutput>#nameFirst# #nameLast#</cfoutput><br> </cfloop> </cfif> </cfcase> <!--- Step Five: Final Confirmation ---> <cfcase value="5"> You have successfully finished the New Movie Wizard.<br> Click the Finish button to add the movie to the database.<br> Click Back if you need to change anything.<br> </cfcase> </cfswitch> <p> <!--- Show Back button, unless at first step ---> <cfif SESSION.movWiz.stepNum GT 1> <input type="submit" name="goBack" value="<< Back"> </cfif> <!--- Show Next button, unless at last step ---> <!--- If at last step, show "Finish" button ---> <cfif SESSION.movWiz.stepNum lt numberOfSteps> <input type="submit" name="goNext" value="Next >>"> <CFELSE> <input type="submit" name="goDone" value="Finish"> </cfif> </cfform> </body> </html>
NOTE To help keep this code as clear as possible, Listing 20.9 doesn't prevent the user from leaving various form fields blank. See Listing 20.11 for a version that validates the user's entries, using the techniques introduced in Chapter 12, "Form Data Validation."
First, a variable called numberOfSteps is defined, set to 5. This keeps the 5 from needing to be hardcoded throughout the rest of the template. Next, the SESSION.movWiz structure is defined, using the syntax shown in the first code snippet that appeared before this listing. The structure contains a default value for each piece of information that will be collected from the user. Next, a <cfif> / <cfelseif> block is used to determine whether the step the user just completed contains a form element named movieTitle. If so, the corresponding value in the SESSION.movWiz structure is updated with the form's value, thus remembering the user's entry for later. The other possible form fields are also tested for this block of code in the same manner. Next, the code checks to see whether a form field named goBack was submitted. If so, it means the user clicked the Back button in the wizard interface (see Figure 20.5). Therefore, the stepNum value in the movWiz structure should be decremented by 1, effectively moving the user back a step. An equivalent test is performed for fields named goNext and goFinish. If the user clicks goFinish, they are redirected to another template called NewMovieCommit.cfm, which actually takes care of inserting the records in the database. Figure 20.5. Session variables are perfect for creating wizard-style interfaces.
The rest of the code displays the correct form to the user, depending on which step they are on. If it's step 1, the first cfcase tag kicks in, displaying form fields for the movie's title and short description. Each of the form fields is prefilled with the current value of the corresponding value from SESSION.movWiz. That means the fields will be blank when the user begins, but if they later click the Back button to return to the first step, they will see the value that they previously entered. That is, a session variable is being used to maintain the state of the various steps of the wizard. The other <cfcase> sections are similar to the first. Each presents form fields to the user (check boxes, radio buttons, and so on), always prefilled or preselected with the current values from SESSION.movWiz. As the user clicks Next or Back to submit the values for a particular step, their entries are stored in the SESSION.movWiz structure by the code near the top of the template. The last bit of code simply decides whether to show Next, Back, and Finish buttons for each step of the wizard. As would be expected, the Finish button is shown only on the last step, the Next button for all steps except the last, and the Back button for all steps except the first. Deleting Session Variables
Like the CLIENT scope, SESSION values are treated like a struct. This means the structDelete() function can be used to delete SESSION values. For instance, to delete the SESSION.movWiz variable, you could use the following line: <cfset structDelete(SESSION, "movWiz")>
TIP Don't use the structClear() function on the SESSION scope itself, as in structClear(SESSION). This erases the session itself, rather than all session variables, which can lead to undesirable results. TIP If you need to delete all variables from the SESSION scope at once, see the section "Expiring a Session Programmatically," later in this chapter.
Listing 20.10 is the NewMovieCommit.cfm template, which is called when the user clicks the Finish button on the last step of the New Movie Wizard (refer to Listing 20.9). Most of this listing is made up of ordinary <cfquery> code, simply inserting the values from the SESSION.MovWiz structure into the correct tables in the database. After all of the records are inserted, the movWiz variable is removed from the SESSION structure, using the syntax shown previously. At that point, the user can be directed back to the NewMovieWizard.cfm template, where they can enter information for another movie. The wizard code will see that the movWiz structure no longer exists for the user, and therefore will create a new structure, with blank initial values for the movie title and other information. Listing 20.10. NewMovieCommit.cfmDeleting Unnecessary Session Variables
<!--- Filename: NewMovieCommit.cfm Created by: Nate Weiss (NMW) Purpose: Inserts new movie and associated records into database. Gets called by NewMovieWizard.cfm ---> <!--- Insert film record ---> <cftransaction> <cfquery datasource="ows"> INSERT INTO Films( MovieTitle, PitchText, RatingID) VALUES ( '#SESSION.MovWiz.MovieTitle#', '#SESSION.MovWiz.PitchText#', #SESSION.MovWiz.RatingID# ) </cfquery> <!--- Get ID number of just-inserted film ---> <cfquery datasource="ows" name="getNew"> SELECT Max(FilmID) As NewID FROM Films </cfquery> </cftransaction> <!--- Insert director record ---> <cfquery datasource="ows"> INSERT INTO FilmsDirectors(FilmID, DirectorID, Salary) VALUES (#getNew.NewID#, #SESSION.MovWiz.DirectorID#, 0) </cfquery> <!--- Insert actor records ---> <cfloop list="#SESSION.movWiz.actorIDs#" index="thisActor"> <cfset isStar = iif(thisActor eq SESSION.movWiz.starActorID, 1, 0)> <cfquery datasource="ows"> INSERT INTO FilmsActors(FilmID, ActorID, Salary, IsStarringRole) VALUES (#getNew.newID#, #thisActor#, 0, #isStar#) </cfquery> </cfloop> <!--- Remove MovWiz variable from SESSION structure ---> <!--- User will be started over on return to wizard ---> <cfset structDelete(SESSION, "movWiz")> <!--- Display message to user ---> <html> <head><title>Movie Added</title></head> <body> <h2>Movie Added</h2> <p>The movie has been added to the database.</p> <!--- Link to go through the wizard again ---> <p><a href="NewMovieWizard.cfm">Enter Another Movie</a></p> </body> </html>
NOTE When we insert the movie into the database, we follow it up with a query to get the ID of the last inserted record. It is possible that multiple people could run this code at the same time. In order to prevent a situation where the ID returned is not the ID of the movie we just created, we use the <cftransaction> tag to "lock" our code. You can read more about <cftransaction> in Chapter 30, "More on SQL and Queries."
One interesting thing about the wizard metaphor is that users expect wizards to adapt themselves based on the choices they make along the way. For instance, the last step of this wizard (in which the user indicates which of the movie's stars gets top billing) looks different depending on the previous step (in which the user lists any number of stars in the movie). You also could decide to skip certain steps based on the film's budget, add more steps if the director and actors have worked together before, and so on. This would be relatively hard to do if you were collecting all the information in one long form. As you can see in Listing 20.10, this version of the wizard doesn't collect salary information to be inserted into the FilmsActors and FilmsDirectors tables. Nor does it perform any data validation. For instance, the user can leave the movie title field blank without getting an error message. If you want, take a look at the NewMovieWizard2.cfm and NewMovieCommit2.cfm templates (Listings 20.11 and 20.12). This slightly expanded version of the wizard adds some data validation for the form elements and adds another step in which the user enters financial information. The following examples use variables in the SESSION scope without locking the accesses with the <cflock> tag. This is an acceptable practice here; however, in other situations it would be advisable to add locks to prevent undesired concurrent requests. See the section "Locking Revisited," later in this chapter. Listing 20.11. NewMovieWizard2.cfm Expanded Version of New Movie Wizard
<!--- Filename: NewMovieWizard2.cfm Created by: Nate Weiss (NMW) Please Note Session variables must be enabled ---> <!--- Total Number of Steps in the Wizard ---> <cfset NumberOfSteps = 6> <!--- The SESSION.movWiz structure holds users' entries ---> <!--- as they move through wizard. Make sure it exists! ---> <cfif not isDefined("SESSION.movWiz")> <!--- If structure undefined, create/initialize it ---> <cfset SESSION.movWiz = structNew()> <!--- Represents current wizard step; start at one ---> <cfset SESSION.movWiz.stepNum = 1> <!--- We will collect these from user; start blank ---> <cfset SESSION.movWiz.movieTitle = ""> <cfset SESSION.movWiz.pitchText = ""> <cfset SESSION.movWiz.directorID = ""> <cfset SESSION.movWiz.directorSal = ""> <cfset SESSION.movWiz.ratingID = ""> <cfset SESSION.movWiz.actorIDs = ""> <cfset SESSION.movWiz.staractorID = ""> <cfset SESSION.movWiz.miscExpense = ""> <cfset SESSION.movWiz.actorSals = structNew()> </cfif> <!--- If user just submitted movieTitle, remember it ---> <!--- Do same for the directorID, Actors, and so on. ---> <cfif isDefined("Form.movieTitle")> <cfset SESSION.movWiz.movieTitle = Form.movieTitle> <cfset SESSION.movWiz.pitchText = Form.pitchText> <cfset SESSION.movWiz.ratingID = FORM.ratingID> <cfelseif isDefined("Form.directorID")> <cfset SESSION.movWiz.directorID = Form.directorID> <cfelseif isDefined("Form.actorID")> <cfset SESSION.movWiz.actorIDs = Form.actorID> <cfelseif isDefined("Form.starActorID")> <cfset SESSION.movWiz.starActorID = Form.starActorID> <cfelseif isDefined("Form.directorSal")> <cfset SESSION.movWiz.directorSal = Form.directorSal> <cfset SESSION.movWiz.miscExpense = Form.miscExpense> <!--- For each actor now in the movie, save their salary ---> <cfloop LIST="#SESSION.movWiz.actorIDs#" index="thisActor"> <cfset SESSION.movWiz.actorSals[thisActor] = FORM["actorSal#thisActor#"]> </cfloop> </cfif> <!--- If user clicked "Back" button, go back a step ---> <cfif isDefined("FORM.goBack")> <cfset SESSION.movWiz.stepNum = URL.stepNum - 1> <!--- If user clicked "Next" button, go forward one ---> <cfelseif isDefined("FORM.goNext")> <cfset SESSION.movWiz.stepNum = URL.stepNum + 1> <!--- If user clicked "Finished" button, we're done ---> <cfelseif isDefined("FORM.goDone")> <cflocation url="NewMovieCommit2.cfm"> </cfif> <html> <head><title>New Movie Wizard</title></head> <body> <!--- Show title and current step ---> <cfoutput> <b>New Movie Wizard</b><br> Step #SESSION.movWiz.stepNum# of #numberOfSteps#<br> </cfoutput> <!--- Data Entry Form, which submits back to itself ---> <cfform action="NewMovieWizard2.cfm?StepNum=#SESSION.movWiz.StepNum#" method="POST"> <!--- Display the appropriate wizard step ---> <cfswitch expression="#SESSION.movWiz.stepNum#"> <!--- Step One: Movie Title ---> <cfcase value="1"> <!--- Get potential film ratings from database ---> <cfquery name="getRatings" datasource="ows"> SELECT ratingID, Rating FROM FilmsRatings ORDER BY ratingID </cfquery> <!--- Show text entry field for title ---> What is the title of the movie?<br> <cfinput name="movieTitle" size="50" required="Yes" message="Please don't leave the movie title blank." value="#SESSION.movWiz.movieTitle#"> <!--- Show text entry field for title ---> <p>What is the "pitch" or "one-liner" for the movie?<br> <cfinput name="pitchText" size="50" required="Yes" message="Please provide the pitch text first." value="#SESSION.movWiz.pitchText#"> <!--- Series of radio buttons for movie rating ---> <p>Please select the rating:<br> <cfloop query="getRatings"> <!--- Re-select this rating if it was previously selected ---> <cfset isChecked = ratingID EQ SESSION.movWiz.ratingID> <!--- Display radio button ---> <cfinput type="radio" name="ratingID" checked="#isChecked#" value="#ratingID#"><cfoutput>#rating#<br></cfoutput> </cfloop> </cfcase> <!--- Step Two: Pick Director ---> <cfcase value="2"> <!--- Get list of directors from database ---> <cfquery name="getDirectors" datasource="ows"> SELECT directorID, FirstName+' '+LastName As FullName FROM Directors ORDER BY LastName </cfquery> <!--- Show all Directors in SELECT list ---> <!--- Pre-select if user has chosen one ---> Who will be directing the movie?<br> <cfselect size="#getDirectors.recordCount#" query="getDirectors" name="directorID" display="fullName" value="directorID" required="Yes" message="You must choose a director first." selected="#SESSION.movWiz.directorID#"/> </cfcase> <!--- Step Three: Pick Actors ---> <cfcase value="3"> <!--- Get list of actors from database ---> <cfquery name="getActors" datasource="ows"> SELECT * FROM Actors ORDER BY NameLast </cfquery> What actors will be in the movie?<br> <!--- For each actor, display checkbox ---> <cfloop query="getActors"> <!--- Should checkbox be pre-checked? ---> <cfset isChecked = listFind(SESSION.movWiz.actorIDs, actorID)> <!--- Checkbox itself ---> <cfinput type="checkbox" name="actorID" value="#actorID#" required="Yes" message="You must choose at least one actor first." checked="#isChecked#"> <!--- Actor name ---> <cfoutput>#nameFirst# #nameLast#</cfoutput><br> </cfloop> </cfcase> <!--- Step Four: Who is the star? ---> <cfcase value="4"> <cfif SESSION.movWiz.actorIDs EQ ""> Please go back to the last step and choose at least one actor or actress to be in the movie. <cfelse> <!--- Get actors who are in the film ---> <cfquery name="getActors" datasource="ows"> SELECT * FROM Actors WHERE actorID IN (#SESSION.movWiz.actorIDs#) ORDER BY NameLast </cfquery> Which one of the actors will get top billing?<br> <!--- For each actor, display radio button ---> <cfloop query="getActors"> <!--- Should radio be pre-checked? ---> <cfset isChecked = SESSION.movWiz.StaractorID EQ actorID> <!--- Radio button itself ---> <cfinput type="radio" name="staractorID" value="#actorID#" required="Yes" message="Please select the starring actor first." checked="#isChecked#"> <!--- Actor name ---> <cfoutput>#NameFirst# #NameLast#</cfoutput><br> </cfloop> </cfif> </cfcase> <!--- Step Five: Expenses and Salaries ---> <cfcase value="5"> <!--- Get actors who are in the film ---> <cfquery name="getActors" datasource="ows"> SELECT * FROM Actors WHERE actorID IN (#SESSION.movWiz.actorIDs#) ORDER BY NameLast </cfquery> <!--- Director's Salary ---> <p>How much will we pay the Director?<br> <cfinput type="text" size="10" name="directorSal" required="Yes" validate="float" message="Please provide a number for the director's salary." value="#SESSION.movWiz.directorSal#"> <!--- Salary for each actor ---> <p>How much will we pay the Actors?<br> <cfloop query="getActors"> <!--- Grab actors's salary from ActorSals structure ---> <!--- Initialize to "" if no salary for actor yet ---> <cfif not structKeyExists(SESSION.movWiz.actorSals, actorID)> <cfset SESSION.movWiz.actorSals[actorID] = ""> </cfif> <!--- Text field for actor's salary ---> <cfinput type="text" size="10" name="actorSal#actorID#" required="Yes" validate="float" message="Please provide a number for each actor's salary." value="#SESSION.movWiz.actorSals[actorID]#"> <!--- Actor's name ---> <cfoutput>for #nameFirst# #nameLast#<br></cfoutput> </cfloop> <!--- Additional Expenses ---> <p>How much other money will be needed for the budget?<br> <cfinput type="text" name="miscExpense" required="Yes" validate="float" message="Please provide a number for additional expenses." size="10" value="#SESSION.movWiz.miscExpense#"> </cfcase> <!--- Step Six: Final Confirmation ---> <cfcase value="6"> You have successfully finished the New Movie Wizard.<br> Click the Finish button to add the movie to the database.<br> Click Back if you need to change anything.<br> </cfcase> </cfswitch> <p> <!--- Show Back button, unless at first step ---> <cfif SESSION.movWiz.stepNum gt 1> <INPUT type="Submit" NAME="goBack" value="<< Back"> </cfif> <!--- Show Next button, unless at last step ---> <!--- If at last step, show "Finish" button ---> <cfif SESSION.movWiz.stepNum lt numberOfSteps> <INPUT type="Submit" NAME="goNext" value="Next >>"> <cfelse> <INPUT type="Submit" NAME="goDone" value="Finish"> </cfif> </cfform> </body> </html>
Listing 20.12. NewMovieCommit2.cfmExpanded Version of Wizard Commit Code
<!--- Filename: NewMovieCommit2.cfm Created by: Nate Weiss (NMW) Date Created: 2/18/2001 ---> <!--- Compute Total Budget ---> <!--- First, add the director's salary and miscellaneous expenses ---> <cfset TotalBudget = SESSION.movWiz.miscExpense + SESSION.movWiz.directorSal> <!--- Now add the salary for each actor in the movie ---> <cfloop list="#SESSION.movWiz.ActorIDs#" index="ThisActor"> <cfset thisSal = SESSION.movWiz.ActorSals[thisActor]> <cfset totalBudget = totalBudget + thisSal> </cfloop> <!--- Insert Film Record ---> <cftransaction> <cfquery datasource="ows"> INSERT INTO Films( MovieTitle, PitchText, RatingID, AmountBudgeted) VALUES ( '#SESSION.movWiz.movieTitle#', '#SESSION.movWiz.pitchText#', #SESSION.movWiz.ratingID#, #totalBudget#) </cfquery> <!--- Get ID number of just-inserted film ---> <cfquery datasource="ows" name="getNew"> SELECT Max(FilmID) As NewID FROM Films </cfquery> </cftransaction> <!--- Insert director record ---> <cfquery datasource="ows"> INSERT INTO FilmsDirectors(FilmID, DirectorID, Salary) VALUES (#getNew.newID#, #SESSION.movWiz.directorID#, #SESSION.movWiz.directorSal#) </cfquery> <!--- Insert actor records ---> <cfloop list="#SESSION.movWiz.actorIDs#" index="thisActor"> <cfset isStar = iif(thisActor EQ SESSION.movWiz.starActorID, 1, 0)> <cfquery datasource="ows"> INSERT INTO FilmsActors(FilmID, ActorID, Salary, IsStarringRole) VALUES (#getNew.newID#, #thisActor#, #SESSION.movWiz.actorSals[thisActor]#, #isStar#) </cfquery> </cfloop> <!--- Remove movWiz variable from SESSION structure ---> <!--- User will be started over on return to wizard ---> <cfset structDelete(SESSION, "movWiz")> <!--- Display message to user ---> <html> <head><title>Movie Added</title></head> <body> <h2>Movie Added</h2> <p>The movie has been added to the database.</p> <!--- Link to go through the wizard again ---> <p><a href="NewMovieWizard2.cfm">Enter Another Movie</a></p> </body> </html> One item of note in these slightly expanded versions is that the new actorSals part of the SESSION.movWiz structure is itself a structure. The fact that you can use complex data types such as structures and arrays is one important advantage that session variables have over client variables and cookies.
When Does a Session End?
Developers often wonder when exactly a session ends. The simple answer is that by default, ColdFusion's Session Management feature is based on time. A particular session is considered to be expired if more than 20 minutes pass without another request from the same client. At that point, the SESSION scope associated with that browser is freed from the server's memory. That said, ColdFusion provides a few options that you can use to subtly change the definition of a session, and to control more precisely when a particular session ends. J2EE Session Variables and ColdFusion MX
ColdFusion includes an option that allows you to use J2EE session variables. This new option is different from the "classic" ColdFusion implementation of session variables, which have been available in previous versions. The traditional implementation uses ColdFusion-specific cfid and cftoken cookies to identify the client (that is, the browser). Whenever a client visits pages in your application within a certain period of time, those page requests are considered to be part of the same session. By default, this time period is 20 minutes. If more than 20 minutes pass without another page request from the client, the session "times out" and the session information is discarded from the server's memory. If the user closes her browser, then reopens it and visits your page again, the same session will still be in effect. That is, ColdFusion's classic strategy is to uniquely identify the machine, then define the concept of "session" solely in terms of time. The J2EE session variables option causes ColdFusion to define a session somewhat differently. Instead of using cfid and cftoken cookies, which persist between sessions, to track the user's machine, it uses a different cookie, called jSessionID. This cookie isn't persistent, and thus expires when a user closes her browser. Therefore, if the user reopens their browser and visits your page again, it is an entirely new session. To enable the J2EE session variables feature, select the Use J2EE Session Variables check box on the ColdFusion Administrator's Memory Variables page. Once you enable this option, sessions will expire whenever the user closes their browser, or when the session timeout period elapses between requests (whichever comes first). NOTE The use of the jSessionID cookie is part of the Java J2EE specification. Using J2EE session variables makes it easy to share session variables with other J2EE code that may be working alongside your ColdFusion templates, such as Java Servlets, Enterprise JavaBeans, JSPs, and so on. In other words, telling ColdFusion MX to use J2EE session variables is a great way to integrate your ColdFusion code with other J2EE technologies so they can all behave as a single application. The assumption here is that the closing of a browser should be interpreted as a desire to end a session.
Default Behavior
Again, by default, a session doesn't automatically end when the user closes her browser. You can see this yourself by visiting one of the session examples discussed in this book, such as the New Movie Wizard (refer to Listing 20.9). Fill out the wizard partway, then close your browser. Now reopen it. Nothing has happened to your session's copy of the SESSION scope, so you still are on the same step of the wizard that you were before you closed your browser. As far as ColdFusion is concerned, you just reloaded the page. Adjusting the Session Time-out Period
You can adjust the session time-out period for your session variables by following the same basic steps you take to adjust the time-out period for application variables. That is, you can adjust the default time-out of 20 minutes using the ColdFusion Administrator, or you can use the sessionTimeout attribute in the This scope defined in your Application.cfc file to set a specific session time-out for your application.
Expiring a Session Programmatically
If you want a session in your code to expire, there are a few ways to handle it. In the past, you could use the <cfapplication> tag with a sessionTimeout value of 0 seconds. A more appropriate way, however, would be to simply remove the session values by hand using the structDelete() function. For instance, if you wanted to give your users some type of log-out link, you could use structDelete() to remove the session variables you set to mark a user as being logged on. Ending the Session when the Browser Closes
The simplest way to make session variables expire when the user closes their browser is by telling ColdFusion to use J2EE session variables, as explained earlier in the "J2EE Session Variables and ColdFusion MX" section. When in this mode, the ColdFusion server sets a nonpersistent cookie to track the session (as opposed to the traditional session-variable mode, in which persistent cookies are set). Thus, when the browser is closed, the session-tracking cookie is lost, which means that a new session will be created if the user reopens their browser and comes back to your application. Assuming that you aren't using J2EE session variables, one option is to set the setClientCookies attribute in the This scope in your Application.cfc file to No, which means that the cfid and cftoken cookies ColdFusion normally uses to track each browser's session (and client) variables will not be maintained as persistent cookies on each client machine. If you aren't using client variables, or don't need your client variables to persist after the user closes their browser, this can be a viable option. If you do decide to set setClientCookie="No", you must manually pass the cfid and cftoken in the URL for every page request, as if the user's browser did not support cookies at all. See the section "Using Client Variables Without Requiring Cookies," earlier in this chapter, for specific instructions. If you want to use setClientCookie="No" but don't want to pass the cfid and cftoken in every URL, you could set the cfid and cftoken on your own, as nonpersistent cookies. This means the values would be stored as cookies on the user's browser, but the cookies would expire when the user closes their browser. The most straightforward way to get this effect is to use two <cfset> tags in your Application.cfc's onSessionStart method, as follows: <!--- Preserve Session/Client variables only until browser closes ---> <cfset Cookie.cfid = SESSION.cfid> <cfset Cookie.cftoken = SESSION.cftoken>
This technique essentially causes ColdFusion to lose all memory of the client machine when the user closes their browser. When the user returns next time, no cfid will be presented to the server, and ColdFusion will be forced to issue a new cfid value, effectively abandoning any session and client variables that were associated with the browser in the past. The expiration behavior will be very similar to that of J2EE session variables. NOTE Please note that by setting the cfid and cftoken cookies yourself in this way, you will lose all session and client variables for your application when the user closes their browser. If you are using both client and session variables, and want the client variables to persist between sessions but the session variables to expire when the user closes their browser, then you should use either the technique shown next or J2EE session variables as discussed earlier.
A completely different technique is to set your own nonpersistent cookie, perhaps called COOKIE.browserOpen. If a user closes the browser, the cookie no longer exists. Therefore, you can use the cookie's nonexistence as a cue for the session to expire programmatically, as discussed in the previous section. Unfortunately, there is a downside to this technique. If the browser doesn't support cookies or has had them disabled, the session will expire with every page request. However, as long as you know that cookies will be supported (for instance, in an intranet application), it will serve you well. Using Session Variables without Requiring Cookies
Unless you take special steps, the browser must accept a cookie or two in order for session variables to work correctly in ColdFusion. If your application needs to work even with browsers that don't (or won't) accept cookies, you need to pass the value of the special SESSION.URLToken variable in each URL, just as you need to pass CLIENT.urlToken to allow client variables to work without using cookies. This will ensure that the appropriate cfid, cftoken, or jSessionID values are available for each page request, even if the browser can't provide the value as a cookie. See "Using Client Variables Without Requiring Cookies," earlier in this chapter, for specific instructions; just pass SESSION.urlToken instead of CLIENT.urlToken. TIP If you are using both client and session variables in your application, you can just pass either SESSION.urlToken or CLIENT.urlToken. You don't need to worry about passing both values in the URL. If you do pass them both, that's fine too.
Other Examples of Session Variables
A number of other examples in this book use session variables. You might want to skim through the code listings outlined here to see some other uses for session variables:
|