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