It is often necessary to split the manipulation of an object into several cohesive steps. Usually with web applications this becomes a series of separate forms (aka a wizard). Implementing multi-step forms can be difficult because of the web's statelessness. I'll attempt to explain, with code, one possible to way to make multistep forms easier to build in the context of the mach-ii framework.

Don't forget your bean!

I'm a pack-rat... you should see my garage. I figure, why throw away a perfectly good object (bean) at the end of a request, especially if you are going to create an exact clone of it *and* its state during a user's subsequent request(s). Thus, my first and most important recommendation when doing multi-step forms is to keep the bean in memory longer than a single request. Coldfusion's session scope is a prime candidate for a storage facility, because:

  • it's partitioned for each individual user of your application, thus you don't have to worry about a user accidentally modifiying someone else's object
  • it's guaranteed to (eventually) clean up any mess, so you don't have to worry about permanent memory leaks
  • it doesn't require a lot of code to implement

I've written a miniscule mach-ii app that demonstrates this technique, which you can download above (under "Example Code"). I'll explain what each file is and does so you don't have to dig around:

  • /MultiStep/controller/
    • MultiStep-Config.xml - the standard mach-ii config file
    • AppPlugin.cfc - a simple plugin... all it does is create a sessionFacade and place it in mach-ii's property manager (so that other components can get access to it)
    • WidgetListener.cfc - this listener contains all the handling code for our bean (yep it's your standard "example application" widget that we'll be working with)
  • MultiStep/model/
    • Widget.cfc - a very simple bean with 6 properties. I split the properies up into three separate form steps.
  • MultiStep/util/
    • SessionFacade.cfc - your plain vanilla session facade (yet another).
      It exposes the following methods:
      • get(string key)
      • put(string key, any value)
      • delete(string key)
  • MultiStep/view/ - just a start page, 3 forms for each step, and a finish page

My multi-step form implementation assumes that each user will start the form steps at the same point. Once they start, they can move back and forth between the forms, and once they finish, they must start over. I think this makes sense for most users, as they are used to Window's (usually awful) wizards. It would be trivial to support unlimited entry points, you would just check for the existance of the object first and create it if it did not (use a filter perhaps?).

So, the first event that you will see is "showStart", which just renders the start page. From there, the user will trigger the "createNewWidget" event, which is probably the most important piece of this app:

<event-handler event="createNewWidget" access="public">
   <notify listener="widgetListener" method="createNewWidget"/>
   <announce event="showStepOne"/>
</event-handler>

It creates the bean and stores it via the SessionFacade. Look at the body of createNewWidget method in WidgetListener.cfc that is notified by the above event handler:

   <!--- create a new instance of Widget --->
   <cfset var currentWidget = createObject("component","multiStep.model.Widget").init()/>

  
   <!--- store new widget in session facade --->
   <cfset variables.sessionFacade.put("currentWidget",currentWidget)/>

Yep, that's all we do. Create the object then store it. Note that variables.sessionFacade is our session facade, created by our plugin, and then pulled in during the WidgetListener's configure() method.

The next thing that the "createNewWidget" event does is announce the "showStepOne" event.

<event-handler event="showStepOne" access="public">
   <notify listener="widgetListener" method="getCurrentWidget" resultKey="request.currentWidget"/>
   <event-arg name="currentWidget" variable="request.currentWidget"/>
   <view-page name="stepOne"/>
</event-handler>

"showStepOne" actually asks the WidgetListener to return the "currentWidget", which is kind of redundant - we just created it why not return it then? The reason is that I wanted the "showStepOne" event to be generic enough so that it could easily handle users coming in both directions, both starting the process and navigating in from another step. By asking the WidgetListener (who asks the SessionFacade) for the "currentWidget" we assure we are always working with the right object:

<cfreturn variables.sessionFacade.get("currentWidget")/>

Each form submits to a processStepN event, which takes the event args from the form submission and calls the appropriate setter methods on the in-memory bean:

<event-handler event="processStepOne" access="public">
   <notify listener="widgetListener" method="processStepOne"/>
   <announce event="showStepTwo"/>
</event-handler>

Each time we announce the next showStepN event, which pulls the in-memory bean so that the form can display any existing values it is interested in.

The nice thing is now I was able to just add "Go Back" buttons on each form step, which trigger the showStepN-1 event (it has no idea whether the user is moving forward or back). I purposely prevented the data on the current step/form from being "saved" when the user goes back, but that would be another trivial change (have the "Go Back" button actually submit the form and the listener dynamically announce the shopStepN event based on which button they clicked).

That's pretty much it... the last event will announce a "showFinish" event, which dumps the bean's values and removes it from the session. Usually this would be the place where you perform some validation and most likely store the bean in a more persistant place (like a database). The example app has some redundant peices of code... i'm sure that a little refactoring could reduce the number of event handlers needed, however I wanted it to be simple enough to understand without studying the code for too long.

If you have questions about this technique or code, please post them below.


Post a comment:

(required, will not be displayed)
 


   You will be sent an email asking you to validate your comment.



Driven by Farcry Open Source CMS. Dressed in Aura.
Powered by ColdFusion MX.