Sunday, June 18, 2006

Running actions despite validation errors

My blog has moved, please update your links. You can find this article here

Recently, I was developing a page that had 3 data tables. Each table had an 'add' button and each row in the table had 'delete' buttons. The problem was that I ended up fighting the JSF specification trying to accomplish this simple task.

The functionality that I wanted for my users was to add or remove rows from these tables regardless of if the data on the rest of the page was valid.

Table of contents:
  1. Immediate
  2. Action validation
  3. Tomahawk sandbox subForm
  4. Optional validator framework
  5. New component (solution)

First I tried the immediate attribute on my command links. Although this technically worked, I lost all my data in the data table input components. Due to the way the UIData component works, the submitted values will not be correctly processed unless the validation phase is run.

After that, I looked into using only action validation (no validators or required flags). Once again I didn't get the functionality that I desired. It was not possible, while maintaining a clean Object-Oriented design to be able to correctly assign component IDs to the validation messages for the components in the tables (I would have to hard-code the indexes into the messages as well as the view IDs - not an elegant solution).

The third attempt led me to the subForm Tomahawk component from MyFaces. First problem was that the all the components in the form had to be within a subForm. Then I had to create a dummy sub-form that I would submit:

<f:form> <s:subForm id="mainForm"> <-- Other input componenets here --> <t:commandLink action="#{userBean.addEmail}" actionFor="dummyForm" /> </subForm> <s:subForm id="dummyForm" />

Once again this worked only half-way. My action was run, but like with the immediate attribute, the values were lost in the data tables.

Forth attempt was a look at the Optional validator framework from http://jsf-comp.sf.net. This would have worked but it didn't have the functionality I needed. For one, only validators with IDs were supported (no attributes could be passed, so I would not be able to use validators like validate length which takes a maximum and minimum value. It is also not able to handle required validators on child components of data tables.

Seeing as how my problem again and again was the phase in which my action was executed, I need a different solution in terms of JSF phase. If immediate was set to true, the action would run but the validation phase would be skipped. The validation phase is needed for the submitted values of data tables though. Having the action fire during model update meant that the validation phase had to be skipped or there had to be a way to skip validation errors.

My fifth and final attempt ended up being my solution. It is perhaps not too pretty, but it got the job done. I needed to get the action event to fire during the process validators phase (in between when immediate fires it and non-immediate). I couldn't just extend command link though because UICommand always sets the phase ID of action events during queueEvent. I could have made a special action event that just disabled the setPhaseId funciton (by doing nothing in the method body), but I thought that more of a hack than my final solution.

My solution was to create a component thats sole purpose in life would be to alter the phase ID of an action event. It would need no renderer either. In the queueEvent I would simple set any ActionEvent's phase IDs to process validation. Then, placing this new component as the parent of an action component (or components), I would make sure my action would run during validation.

Using facelets, I needed no tag, so I just wrote the component and gave it an active attribute in case I ever want to disable the behavior using EL:

import javax.faces.component.UIComponentBase; import javax.faces.context.FacesContext; import javax.faces.el.ValueBinding; import javax.faces.event.ActionEvent; import javax.faces.event.FacesEvent; import javax.faces.event.PhaseId; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * * @author Andrew Robinson (andrew) */ public class UIValidationActionPhase extends UIComponentBase { private static final Log log = LogFactory.getLog( UIValidationActionPhase.class); public static final String COMPONENT_FAMILY = "-- set family here --"; public static final String COMPONENT_TYPE = "-- set component type here --"; private Boolean active; /** * @return Returns the active. */ public boolean isActive() { if (active != null) return this.active; ValueBinding vb = getValueBinding("active"); return (vb == null) || (Boolean)vb.getValue(getFacesContext()); } /** * @param active The active to set. */ public void setActive(boolean active) { this.active = active; } /** * @see javax.faces.component.UIComponent#getFamily() */ @Override public String getFamily() { return COMPONENT_FAMILY; } /** * @see javax.faces.component.UIComponentBase#getRendererType() */ @Override public String getRendererType() { return null; } /** * @see javax.faces.component.UIComponentBase#queueEvent(javax.faces.event.FacesEvent) */ @Override public void queueEvent(FacesEvent event) { if (event instanceof ActionEvent && isActive()) { log.debug("Changing Phase ID of action event " + event); event.setPhaseId(PhaseId.PROCESS_VALIDATIONS); } super.queueEvent(event); } /** * @see javax.faces.component.UIComponentBase#saveState(javax.faces.context.FacesContext) */ @Override public Object saveState(FacesContext context) { return new Object[] { super.saveState(context), active, }; } /** * @see javax.faces.component.UIComponentBase#restoreState(javax.faces.context.FacesContext, java.lang.Object) */ @Override public void restoreState(FacesContext context, Object state) { Object[] array = (Object[])state; int index = -1; super.restoreState(context, array[++index]); active = (Boolean)array[++index]; } }
The usage in my XHTML file was simple:
<f:form> <my:validationAction> <t:commandLink action="#{userBean.addEmail}" /> </my:validationAction> </f:form>

Now the user is able to see validation errors and get feedback on bad input data and my action was still able to run with the validation errors. Luckily my actions (add and delete) did not depend on the model being updated. This solution will not work for those that would need data from the user to be applied to the model.

© Copyright 2006 - Andrew Robinson.
Please feel free to use in your applications under the LGPL license
(http://www.gnu.org/licenses/lgpl.html).
Updates/Change log
Date Description
2007.03.07 Fixed some of the code in the component that was a result of pasting the code into the blog incorrectly.
2007.04.22 Reformatting of HTML in blog.

12 comments:

Martin Grotzke said...
This comment has been removed by a blog administrator.
Martin Grotzke said...

Hi, i had a similar problem that i could solve with your solution. Thanx a lot for this posting!

bfayle said...

This could be what I'm looking for however, the code you've posted has duplicate method names and can't compile. What should the correct code be?

atzbert said...

You definitely get a digg for that, I am sure I will need that one day, although it doesn't quite fit my current issue..

Thanks for posting this!

tross said...

Could you please post a war file with your entire project? Using this code snippet, I can't get the action & actionListener methods on the UICommand component (HTMLCommandButton in my case) to fire when wrapped with the custom validationAction component, even though the debugger stops in the validationAction component's queueEvent method, and the HTMLCommandButton is the event.getSource(). It's like the validationAction component is masking out the action/actionListener methods of the child component. I did have to code a tld file for the validationAction component (I don't know how to make Facelets "see" a "naked" component named my:validationAction without some tag information). Your war file could point me in the right direction. Thanks for pioneering an outstanding technique to help manage the (somewhat unwieldy) JSF lifecycle ??

Andrew said...

What you see is what you get. I didn't end up using this technique, so I don't have any more code to share than is already in this blog. I'm not sure why you are not seeing it working. The code doesn't do anything special, so what happens is all JSF core: (1) queueEvent is called during processDecodes of the HtmlCommandButton by either the button or its renderer. (2) My code changes the phase ID of the event. (3) UIViewRoot calls broadcast to the component of the event (the command button).

Now the only way for the event to not fire is if the events get cleared before the right phase. That would mean it was aborted.

I would recommend using a debugger to debug into UIViewRoot and step through the event broadcasting (the lifecycle too)

Romain said...

Hi,

Thanks for this tip.

However, there is a problem here:
With your component, my action on the table is indeed executed, but also all other validations of the form.
Thus, if I have a mandatory field that is empty, when I click on "Delete a row" button, the row will be removed, but a message will be displayed (in case I use <h:messages/> component, of course).

Another solution is to use Ajax4JSF framework:
On my table, I create this button:
<a4j:commandButton bypassUpdates="true" reRender="myDataTableId" .../>

With this code, the action on the table will be executed, and my table will be updated. In fact, it is not perfect as the validation of fields will also be performed, but the messages will not be displayed to the user as only the datatable is rerendered by Ajax4JSF.

Thanks anyway for your tip.

jollyy said...

Hi,

Your solution saved me from throwing my PC through the windows ;-)

Thanks a lot!

Andrew said...

Glad to help. FYI, it is always good to keep "Windows" far away from computers.

Steve K said...

Hi Andrew,

This is something very similar to what I've been struggling with. Thanks for the idea. But, because I'm fairly new to JSF, I've got a question about the 'my:validateAction' tag. How did you link the 'my' namespace to your UIValidationActionPhase class?

Andrew said...

@Steve - I would recommend a JSF tutorial on creating components.

Facelets documentation has information on creating taglib.xml files. For JSP, you need to create a tag class and a TLD file.

You can try MyFaces code (see the tomahawk demo for example), or use a demo app like faces goodies:
http://code.google.com/p/facesgoodies/

Steve K said...

Hi Andrew,

Thanks for the hints. It's working now and seems to be a fix for our issue. This has been a good learning experience.

Steve