Wednesday, June 27, 2007

Event Listener Oddities



I have a pretty straight forward paradigm:

- two groups of 3 radio buttons followed by a property selection.
- an async form with no listener whatsoever
- a submit button with an action listener (called saveTime)

The idea is that choosing a radio button narrows down the list in the property selection.

I chose to add eventlisteners to the onclicks of the radiobuttons, and update the property selection in response. The eventlisteners submit the above form, which is why there is no listener set in the form binding. That is the gist of the problem.

Here is my impl:

<html jwcid="@Shell" title="EventListener">
<body jwcid="@Body">

<div id="main">

<span jwcid="response@Any">
<span jwcid="message"/>
</span>

<form jwcid="addEventForm">
<div>
<label>Category</label>
<div jwcid="category" id="categoryRadioGroup">
<input type="radio" jwcid="@Radio" value="ognl:@...Category@VIDEO" onclick="document.getElementById('categoryRadioGroup').clickRadio(this);"/> Video
<input type="radio" jwcid="@Radio" value="ognl:@...Category@PROJECT" onclick="document.getElementById('categoryRadioGroup').clickRadio(this);"/> Project
<input type="radio" jwcid="@Radio" value="ognl:@...Category@OTHER" onclick="document.getElementById('categoryRadioGroup').clickRadio(this);"/> Other
</div>
</div>

<div>
<label>Stage</label>
<div jwcid="stage" id="stageRadioGroup">
<input type="radio" jwcid="@Radio" value="ognl:@...Stage@PreProduction" onclick="document.getElementById('stageRadioGroup').clickRadio(this);"/> Pre-
<input type="radio" jwcid="@Radio" value="ognl:@...Stage@Production" onclick="document.getElementById('stageRadioGroup').clickRadio(this);"/> Production
<input type="radio" jwcid="@Radio" value="ognl:@...Stage@PostProduction" onclick="document.getElementById('stageRadioGroup').clickRadio(this);"/> Post-
</div>
</div>

<div>
<label>Phase</label>
<select jwcid="phase">
<option>A-Roll</option>
<option>Programming</option>
</select>
</div>

<input jwcid="addEventButton" type="submit" value="Save Event"/>

</form>

</div>

</body>
</html>


<page-specification class="ca.ucalgary.tlc.eventlistener.presentation.Home">

<component id="addEventForm" type="Form">
<binding name="clientValidationEnabled" value="true"/>
<binding name="async" value="ognl:true"/>
</component>

<component id="addEventButton" type="Submit">
<binding name="action" value="listener:addEvent"/>
<binding name="async" value="ognl:true"/>
</component>

<component id="category" type="RadioGroup">
<binding name="displayName" value="literal:Category"/>
<binding name="selected" value="ognl:category"/>
<binding name="validators" value="validators:required"/>
</component>

<component id="stage" type="RadioGroup">
<binding name="displayName" value="literal:Stage"/>
<binding name="selected" value="ognl:stage"/>
<binding name="validators" value="validators:required"/>
</component>

<component id="phase" type="PropertySelection">
<binding name="displayName" value="literal:Phase"/>
<binding name="value" value="ognl:phase"/>
<binding name="model" value="ognl:phaseSelectionModel"/>
<binding name="validators" value="validators:required"/>
</component>

<component id="message" type="Insert">
<binding name="value" value="ognl:message"/>
</component>

</page-specification>


public abstract class Home extends BasePage {

private Log log = LogFactory.getLog(Home.class);

@InitialValue("ognl:null")
public abstract void setCategory(Category category);
public abstract Category getCategory();

@InitialValue("ognl:null")
public abstract void setStage(Stage stage);
public abstract Stage getStage();

@InitialValue("ognl:null")
public abstract void setPhase(Phase phase);
public abstract Phase getPhase();

@InitialValue("ognl:null")
public abstract String getMessage();
public abstract void setMessage(String message);

public IPropertySelectionModel getPhaseSelectionModel() {
List phases = PhaseRule.getRelevantPhases(PhaseRule.getAllPhases(), getCategory(), getStage());
return new PhaseSelectionModel(phases);
}

@EventListener(events = "clickRadio", elements = "categoryRadioGroup", async = true, submitForm = "addEventForm", validateForm = false)
public void categorySelected(IRequestCycle cycle) {
log.info("Chose a category: " + getCategory());
cycle.getResponseBuilder().updateComponent("phase");
}

@EventListener(events = "clickRadio", elements = "stageRadioGroup", async = true, submitForm = "addEventForm", validateForm = false)
public void stageSelected(IRequestCycle cycle) {
log.info("Chose a stage: " + getStage());
cycle.getResponseBuilder().updateComponent("phase");
}

public void addEvent(IRequestCycle cycle)
{
log.info("Adding event.");
setMessage("Added event with phase = " + getPhase().shortString());

cycle.getResponseBuilder().updateComponent("response");
}

}



Now, this all works perfectly the first time through - click a radio button, watch the property selection values change, submit the form. Of particular interest, note that saveEvent is not called when you click a radio button - only categorySelected.

The second time around, however, the eventlisteners behave slightly differently. This time, when you click a radio button, the form is submitted, categorySelected is invoked _and_ saveEvent is called (which is a problem).

The primary difference between the two posts (at the request level) is the inclusion of the name of said button, which I believe is how Tapestry determines what listener to call (ie if &addEventButton=Save%20Event is included, then saveEvent is called - correct me if I'm wrong).

Note also the trick used to get the radio buttons to send an event, by adding the onclick handler.

You can download a sample application here: eventlistener.zip. It's mavenized, and antized, so it's pretty easy to get a working, editable app and running in a few minutes.