|By Curtis Schlak||
|August 8, 2001 12:00 AM EDT||
Techno-evangelists have been proclaiming for years that the Internet will change the world, bring people closer, and provide the ultimate forum for sharing information and exchanging opinions. Yet something as simple as filling out a Web-based form remains, in large part, about as much fun as filling out your tax forms with a crayon.
A good option when Web surfers are required to input information is a wizard. Wizards make it easy for Web surfers to input information into Web-based forms, particularly when that information is complex, lengthy, or when it varies based on the user who is providing it. Wizards are more intuitive than typical HTML-based forms, and ColdFusion's functionality gives programmers tremendous flexibility when it comes to manipulating the information into the ColdFusion database.
In this article we'll build the code that enables a wizard to enter a recipe - a seemingly harmless task that, without a wizard, becomes devastatingly complex for the developer and the user. This is a simplified version of a recipe application that I built in 1998. The code may seem like overkill, but when faced with a six-step process, it becomes invaluable.
Purpose of a Wizard
In the early days of Web-based forms (we're talking 1997 here), surfers would often scroll through forms up to eight screens long before finding a "Submit" button. Someone finally got the bright idea to break that form up into multiple pages. The concept of the Web-based wizard was born.
Sectioning related groups of requested information onto separate pages gives the user a better experience. Every modern e-commerce checkout process involves a wizard. Signing up for a free magazine on the Web usually involves a wizard. Whenever you come across the words, "Step 3 of 5," you're in a wizard.
Advantages of Using Wizards on the Web
The most obvious reason to use a wizard is to chop up that long form you've created. But there are subtler reasons to do this. You can lead the user through complicated data input that would be frustrating or confusing to do on a single, long page.
Disadvantages of Using Wizards
There is one disadvantage to doing anything over the Web: slow Internet access. You know the site: some designer thought it would be groovy to fill the home page with 150Kb of images because they thought it would be pretty. Unusable. Undesirable. Unacceptable.
Thirty-five million Web surfers now access the Internet at 56Kbps or less. A little more than 5% of consumers have access to DSL, cable, or other high-bandwidth connectivity. The number of modem connections to the Web is expected to increase through 2004. We, as developers, must take it all into consideration when writing.
If it takes longer for a page to download than it takes the user to fill out requested information, the person gripping the mouse is not going to be happy. Planning the wizard well will minimize the problems that Joe Q. Surfer will have on his WebTV console.
Planning the Wizard
We're on the Web. We've got to get the information that someone has put into a form, and then has hit the "Submit" button. Now what?
Where Am I?
It's a fundamental question. I'm in my living room, having just finished watching another episode of Law & Order, which always inspires me to write efficient code. In coding, as in life, it's always a good thing to know where you are.
I use a variable named CurrentStep throughout my wizards. It lets my application know what page the user is coming from and what information to expect. Sometimes it's a hidden form field. Sometimes it's in the URL. The structure of delivery doesn't matter since ColdFusion is smarter than your average application server. I just need to let my application know what to expect and when.
Table 1 represents the recipe wizard's different steps and the information collected from each one.
We'll use this information to make our application smart about catching information.
Building a Structure
ColdFusion structures are a perfect place to put information. If you're unfamiliar with structures, refer to
for some documentation. Since we're building a wizard that must collect information and maintain it in an organized manner, the structure is the perfect storage type.
Our structure will have the form and recipe information shown in Table 2 stored in it.
ColdFusion allows two syntaxes to access information within a structure: stStructureName[ "KeyName" ] and stStructure-Name.KeyName. You can mix and match the syntaxes as well. To access the food item in the second ingredient of the fourth procedure, I could use any of the following:
stRecipe[ "Procedure" ][ 4 ][ "Ingredients" ][ 2 ][ "FoodItem"]
stRecipe.Procedure[ 4 ].Ingredients[ 2 ].FoodItem
stRecipe[ "Procedure" ][ 4 ].Ingredients[ 2 ][ "FoodItem"]
Throughout this article I'll use the square bracket notation, unless I'm referring to the Application, Session, Client, Attributes, or Caller scopes. A typical Session variable that contains a structure would look like this:
Session.stMyStructure[ "Akey" ]
Now that we've got a structure planned, we can start building code to populate it.
Storing the Structure
State management is a discussion that could take a whole other article. Since we must maintain some state between page requests, so that we can accurately reference information in memory, this discussion is necessary. I'm just going to hit the top-level pros and cons here. I'm using Session variables in this example, but any of these are available.
Session variables time out. They rely on a CFID and CFTOKEN (autogenerated by ColdFusion Server) combination to uniquely identify a Session. They're really convenient. ColdFusion 4.0+ requires a lock on accessing these variables if your application runs in a multi-threaded environment. This incurs a small performance hit.
Like Session variables, Client variables rely on a CFID and CFTOKEN combination to identify a Client. On the plus side, you can store them in cookies, in a database, or (forbid!) the system registry. They don't time out, unless the system wipes them clean after being in disuse for a while.
Passing information along in a series of hidden form fields can be good at times, but they increase the download time for a page. They are hard to maintain and are useless without a form submission. This is one of the last options I would ever consider using.
As a developer, I get excited about clean, efficient code. When I look at a URL that is choked with encoded variables, it turns me off. Here's an example of one that makes my hair stand on end:
Yikes. Since I am an aesthetic programmer, if I were ever to use this method, I would hide the URL from the user with a frameset and use the pages in the frame to accept the egregious URL. The user, meanwhile, sees only the www.yourdomain.com in the address bar.
It's conceivable that a programmer could store and retrieve information from a database with every form submission. It is persistent and remains safe in the warm arms of relational tables. Still, when I use a database connection to pump that information back and forth, I get worried. What if, for some reason, the database is being hit really hard? I don't know, I've got reservations about it. It remains an option, but my preference is to store stuff in Session or Client variables, if robustness (or whatever) is critical.
One of the most interesting ColdFusion developers I know uses a file to store a WDDX packet of serialized ColdFusion structures. I've done it, too, and it works well. Unfortunately, this option draws the ire of UNIX administrators when these files begin clogging their directories. On a Windows PC, I have less faith in the file access, not from any proof other than the number of corrupted files I've come across on my NT boxes. Again, I prefer to stick with Session or Client variables stored in a database.
I firmly believe that a ColdFusion developer should never choke the Application.cfm with a bunch of silly stuff. Normally I would consider form handling to be a silly thing. In the case of wizards, however, our application must be ready to receive information between any two points of the process. The most logical place, then, is to store the data in a local Application.cfm.
For the people asking themselves, "What is this Application.cfm that he keeps mumbling about?" please allow me to explain.
ColdFusion has two templates that run every time a ColdFusion page is requested. They are Application.cfm and OnRequest-End.cfm. Application.cfm runs before a ColdFusion page and OnRequestEnd.cfm runs after a ColdFusion page. Most people know about Application.cfm. If you never heard of it, don't admit to it. It's one of those pieces of information that if you don't have, Allaire will strip you of your ColdFusion T-shirt. A lot of people don't know about OnRequestEnd.cfm. I like to put Easter eggs in mine. It enlivens the application with a little chance.
Since the Application.cfm is always willing to run before any page in a wizard, it's the perfect place to put form-handling code. The application captures form information as it inputs. This is important because it allows the form to save information in the event the user hits the "Back" or "Previous" button.
Handling the Form Collection
There are two types of fields in a form submittal - fields that always appear for your form-handling functions and fields that sometimes show up for those functions. Text areas and boxes, selects, and hidden fields show up for the functions to handle all the time. If these fields are in a form and you hit the "Submit" button, the information will show up for the functions to handle, even if they contain empty strings.
Regardless of whether the fields appear in the posted page, I perform some server-side validation. If there are any field values that do not comply with my rules, I append that field name to a list of bad field names, reload the appropriate form, and mark the erred entries, usually by changing the attributes of the form field's label. For example, if there is incorrect or incompatible information entered into a field named Age, the following code would change the text label for that field to show that there was an error - by making the word all capital letters, red, and bold.
<cfif IsDefined( "lBadFields" )
AND ListFindNoCase( lBadFields, "Age" )>
Checkboxes and radio buttons, on the other hand, don't show up reliably if they're not checked. This can complicate an application. Sometimes, form-field functions overlap. I use the following strategies for handling form data in a wizard.
Checkboxes and Radio Buttons
Since checkboxes and radio buttons must be handled independently of their existence in a submitted form, I use the CurrentStep value to help my application create the appropriate values.
On the first step the Daypart, Food type, and Public are checkboxes. I am expecting three form fields: Form[ "Daypart" ], Form[ "FoodType" ] and Form[ "Public" ]. If someone chooses not to check any of those boxes, none of these form fields appear in the posted page. So, for every checkbox and radio button in the step, I'll write a special handler for it. The code in Listing 1 would handle the Daypart group.
Text and Select Fields
All the remaining fields can be garnered by having a little fun with the <cfloop> tag. Here's an example of capturing the information from Step 3 in Table 1, the Ingredient item, amount, and unit of measurement. This will add an ingredient to the end of the list. Passed with the form is a hidden field (Form[ "Procedure-Number" ]) that tells stores the Procedure number (see Listing 2).
Now, to whatever page the form is submitted, the structure will be able to get all the required information that it needs.
Mutually Exclusive Fields
Multidirectional Wizard Navigation
A wizard should allow the user to backtrack without losing any information. The code techniques developed above allow form information to be captured within the wizard. Now, how do we get that information to the server, regardless of the user's traveling direction?
Remember the variable CurrentStep? The form validation can result in two states: "everything's okay" and "there's something wrong." If there's something wrong, we don't want to move away from the current step of the wizard. If everything is okay and we're going forward, then we add 1 to CurrentStep. If everything is okay and we're going backward, then we subtract 1 from CurrentStep. If we adopt the page design shown in Figure 1, then we can always include the correct page.
Somewhere at the top of RecipeWizard.cfm, I stick the following code:
<cflock type="READONLY" scope="SESSION" timeout="30">
<cfset stRecipe = StructCopy( Session.stRecipe )>
This creates a local copy of the recipe structure that my application can use without incurring the performance hit of a lock on the Session scope every time I need to reference a value.
Netscape Navigator 2+ and Microsoft Internet Explorer 4+ allow developers to read and write to the action property of a form. I usually set the form's action property to <cfoutput> #CGI.SCRIPT_NAME#</cfoutput> so that the form just submits back to the same page. (In case you don't know about the CGI variables, look into them. They are amazing, at times.) Now, when the forward button is hit, the default action is produced. If a user clicks the "Go Back a Step" button, then we have to do something to allow the application to know where the user wants to go.
I solve this by appending a URL variable to the action before submission. The button shown in Listing 4 would do the job.
All that's left is the code in Application.cfm to figure out where to go. That code could look something like Listing 5.
Combining this with the page design and the <cfinclude template="RWiz_Step#NextStep#.cfm">, we are assured that the user can navigate easily through the wizard while we continue to collect all the required information.
We've spent all this time collecting information intelligently from forms and allowing our user to traipse all over the wizard willy-nilly. The last action is to display any information that we may have stored. This prevents users from being confused by empty fields that they thought they had filled out. Luckily, we already have the information stored in an intuitive and easily accessible manner.
If the user comes back to the first page sometime during his or her experience, we can populate the fields of the first page with something like:
<input type="text" name="Name" size="30" maxlength="128"
<cfif StructKeyExists( stRecipe, "Name" )>
value="<cfoutput>#stRecipe[ "Name" ]#</cfoutput>"
If the structure has a Name stored in it, then it will populate the field with that value.
The Final Step
The last thing to do is put all this information somewhere. Usually it's headed for a database. This structure allows you to do looping over arrays and nested structures to perform the insertion in a mechanical mode.
The concept of a wizard is easy: provide questions in a logical order and in intuitive groupings. This coding technique makes it extremely simple for a programmer to do just that. Next time a client asks you to collect 37 pieces of information from a user, or your application demands that you collect structured information, think about using a wizard. You and your user will be much happier with the result.
|Old 08/14/01 05:34:00 PM EDT|
- Where Are RIA Technologies Headed in 2008?
- The Next Programming Models, RIAs and Composite Applications
- AJAX World RIA Conference & Expo Kicks Off in New York City
- Constructing an Application with Flash Forms from the Ground Up
- Building a Zip Code Proximity Search with ColdFusion
- Personal Branding Checklist
- CFEclipse: The Developer's IDE, Eclipse For ColdFusion
- Has the Technology Bounceback Begun?
- Adobe Flex 2: Advanced DataGrid
- i-Technology Viewpoint: We Need Not More Frameworks, But Better Programmers
- Web Services Using ColdFusion and Apache CXF
- Passing Parameters to Flex That Works