Sunday, October 11, 2009

Model-Glue: Event result handlers

One feature of Model-Glue that seems to frequently trip newcomers is the <result> tag. I tend to call these result handlers, to distinguish them from results issued by controller methods.

A result handler is used in a event handler to specify which event(s) should be processed after the current event. A named result handler is fired only when a controller issues a result of the same name during the event, while an unnamed or default event handler is "always" fired and does not require an explicit result from a controller (except under certain situations that I will explain in this post).

However, a Model-Glue result handler has two quite distinct behaviours depending on the value of the redirect attribute:

  • When redirect="true", processing of the current event is stopped and the event queue is discarded. The framework then issues a to the target event. By default the event state is preserved and is available in the target event, but this can be overridden by adding preserveState="false".
  • When redirect="false" or is unset, the target event is added to the event queue. Because an event handler may have more than one <result> tag, an event handler may add more than event to the queue. After all matching <result> tags have been processed, the framework invokes the handler for the next event in the queue.
The basics of the <result> tag is explained in the Section 5 of the Model-Glue Quickstart.

A Model-Glue newcomer is likely to first think of <result> as a kind of GOTO statement and may not realize that multiple events can get queued by an event handler. In a recent post to the Model-Glue mailing list, a developer wondered why the following event handler did not work as expected when an error result was issued:

<event-handler name="signup">
<broadcasts>
<message name="doSignupForm" />
</broadcasts>
<results>
<result do="page.index" />
<result name="error" do="signupform" />
</results>
</event-handler>

What was happening of course is that both "page.index" and "signupform" were being added to the event queue. In this case the developer was seeing the view rendered by page.index, but the final rendering would depend on how the two target event handlers were written.

There are two solutions to this. One solution suggested by another member of the mailing list is to have the controller method issue a success result when there is no error:

<event-handler name="signup">
<broadcasts>
<message name="doSignupForm" />
</broadcasts>
<results>
<result name="success" do="page.index" />
<result name="error" do="signupform" />
</results>
</event-handler>



Another solution is to use redirect="true" to imbue the error result with the expected GOTO-like behaviour:

<event-handler name="signup">
<broadcasts>
<message name="doSignupForm" />
</broadcasts>
<results>
<result do="page.index" />
<result name="error" do="signupform" redirect="true" />
</results>
</event-handler>

One interesting feature of redirect result handlers that I've discovered is that they fire immediately upon a controller method issuing them. This means that any controllers that are waiting to process the current or subsequent message broadcasts in the current event will not be executed.

For example, let's say you wanted to use a message broadcast to log successful signups. One approach would be to add a second broadcast to your signup event:

<event-handler name="signup">
<broadcasts>
<message name="doSignupForm" />
<message name="log"> <argument name="message" value="Signup success" /> </message>
/broadcasts>
<results>
<result do="page.index" />
<result name="error" do="signupform" redirect="true" />
</results>
</event-handler>

If the error result uses redirect="true", this will work as intended because an error result would interrupt the event processing so that the log message would not be broadcast. If the redirect="true" is removed, the log message would be broadcast even on error results.

To get the correct behavior for a non-redirect result you must put the log broadcast in the target event. You may need to define an intermediate event for this. Here is an example:

<event-handler name="signup">
<broadcasts>
<message name="doSignupForm" />
</broadcasts>
<results>
<result name="success" do="signupSuccess" />
<result name="error" do="signupform" />
</results>
</event-handler>

<event-handler name="signupSuccess" access="private">
<broadcasts>
<message name="log">
<argument name="message" value="Signup success" />
</message>
</broadcasts>
<results>
<result do="page.index" />
</results>
</event-handler>
In this case I had to use a named result handler for success so that it would not be fired on error. I also declared the target event handler as private: this is to prevent anyone from triggering the event externally.

For the signupSuccess event handler I would make one more change. If a user signs up successfully and then triggers a browser reload, I do not want to process the signup event again. To prevent this, I could change the final result handler to a redirect:

<event-handler name="signupSuccess" access="private">
<broadcasts>
<message name="log">
<argument name="message" value="Signup success" />
</message>
</broadcasts>
<results>
<result do="page.index" redirect="true" />
</results>
</event-handler>
With this change, a user who successfully signs up would end up at index.cfm?event=page.index instead of index.cfm?event=signup. A browser reload at that point would simply reload the main application page instead of repeating the signup processsing.

No comments:

Post a Comment