We use commandButtons
inside a c:forEach
tag, where the button's action
receives the forEach
var
attribute as a parameter like this:
<c:forEach var="myItem" items="#{myModel}">
<h:commandButton
action="#{myController.process(myItem)}"
value="#{myItem.name}" />
</c:forEach>
This works just fine. If we wrap the commandButton
in a composite component, it does not work anymore, though: The controller gets called, but parameter is always null
.
Here is an example of a c:forEach
Tag containing a button and composite component wrapping a button. The first one works, the second one does not.
<c:forEach var="myItem" items="#{myModel}">
<h:commandButton
action="#{myController.process(myItem)}"
value="#{myItem.name}" />
<my:mybutton
action="#{myController.process(myItem)}"
value="#{myItem.name}" />
</c:forEach>
with the following my:mybutton
implementation:
<composite:interface>
<composite:attribute name="action" required="true" targets="button" />
<composite:attribute name="value" required="true" />
</composite:interface>
<composite:implementation>
<h:commandButton id="button"
value="#{cc.attrs.value}">
</h:commandButton>
</composite:implementation>
Please Note, that the button's value
attribute, which is also bound to the c:ForEach
var
, works just fine. It is only the action
, propagated through the composite component's targets
mechanism, that does not get evaluated correctly.
Can anoymone please explain, why this happens and how to fix this?
We are on mojarra 2.2.8, el-api 2.2.5, tomcat 8.0.
This is caused by a bug in Mojarra or perhaps an oversight in the JSF specification with regard to retargeting method expressions for composite components.
The work around is the below ViewDeclarationLanguage
.
public class FaceletViewHandlingStrategyPatch extends ViewDeclarationLanguageFactory {
private ViewDeclarationLanguageFactory wrapped;
public FaceletViewHandlingStrategyPatch(ViewDeclarationLanguageFactory wrapped) {
this.wrapped = wrapped;
}
@Override
public ViewDeclarationLanguage getViewDeclarationLanguage(String viewId) {
return new FaceletViewHandlingStrategyWithRetargetMethodExpressionsPatch(getWrapped().getViewDeclarationLanguage(viewId));
}
@Override
public ViewDeclarationLanguageFactory getWrapped() {
return wrapped;
}
private class FaceletViewHandlingStrategyWithRetargetMethodExpressionsPatch extends ViewDeclarationLanguageWrapper {
private ViewDeclarationLanguage wrapped;
public FaceletViewHandlingStrategyWithRetargetMethodExpressionsPatch(ViewDeclarationLanguage wrapped) {
this.wrapped = wrapped;
}
@Override
public void retargetMethodExpressions(FacesContext context, UIComponent topLevelComponent) {
super.retargetMethodExpressions(new FacesContextWithFaceletContextAsELContext(context), topLevelComponent);
}
@Override
public ViewDeclarationLanguage getWrapped() {
return wrapped;
}
}
private class FacesContextWithFaceletContextAsELContext extends FacesContextWrapper {
private FacesContext wrapped;
public FacesContextWithFaceletContextAsELContext(FacesContext wrapped) {
this.wrapped = wrapped;
}
@Override
public ELContext getELContext() {
boolean isViewBuildTime = TRUE.equals(getWrapped().getAttributes().get(IS_BUILDING_INITIAL_STATE));
FaceletContext faceletContext = (FaceletContext) getWrapped().getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);
return (isViewBuildTime && faceletContext != null) ? faceletContext : super.getELContext();
}
@Override
public FacesContext getWrapped() {
return wrapped;
}
}
}
Install it as below in faces-config.xml
:
<factory>
<view-declaration-language-factory>com.example.FaceletViewHandlingStrategyPatch</view-declaration-language-factory>
</factory>
How I nailed down it?
We have confirmed that the problem is that the method expression argument became null
when the action is invoked in a composite component while the action itself is declared in another composite component.
<h:form>
<my:forEachComposite items="#{['one', 'two', 'three']}" />
</h:form>
<cc:interface>
<cc:attribute name="items" required="true" />
</cc:interface>
<cc:implementation>
<c:forEach items="#{cc.attrs.items}" var="item">
<h:commandButton id="regularButton" value="regular button" action="#{bean.action(item)}" />
<my:buttonComposite value="cc button" action="#{bean.action(item)}" />
</c:forEach>
</cc:implementation>
<cc:interface>
<cc:attribute name="action" required="true" targets="compositeButton" />
<cc:actionSource name=""></cc:actionSource>
<cc:attribute name="value" required="true" />
</cc:interface>
<cc:implementation>
<h:commandButton id="compositeButton" value="#{cc.attrs.value}" />
</cc:implementation>
First thing I did was locating the code who's responsible for creating the MethodExpression
instance behind #{bean.action(item)}
. I know it's normally created via ExpressionFactory#createMethodExpression()
. I also know that all EL context variables are normally provided via ELContext#getVariableMapper()
. So I placed a debug breakpoint in createMethodExpression()
.
In the call stack we can inspect the
ELContext#getVariableMapper()
and also who's responsible for creating the MethodExpression
. In a test page with a composite component in turn nesting one regular command button and one composite command button we can see the following difference in ELContext
:
We can see that the regular button uses DefaultFaceletContext
as ELContext
and that the VariableMapper
contains the right item
variable.
Composite button:
We can see that the composite button uses standard
ELContextImpl
as ELContext
and that the VariableMapper
doesn't contain the right item
variable. So we need to go some steps back in the call stack to see where this standard ELContextImpl
is coming from and why it's being used instead of DefaultFaceletContext
.
Once found where the particular ELContext
implementation is created, we can find that it's obtained from FacesContext#getElContext()
. But this does not represent the EL context of the composite component! This is represented by the current FaceletContext
. So we need to go some more steps back to figure out why the FaceletContext
isn't being passed down.
We can see here that CompositeComponentTagHandler#applyNextHander()
doesn't pass through the FaceletContext
but the FacesContext
instead. This is the exact part which might have been an oversight in the JSF specification. The ViewDeclarationLanguage#retargetMethodExpressions()
should have asked for another argument, representing the actual ELContext
involved.
But it is what it is. We can't change the spec on the fly right now. Best what we can do is to report an issue to them.
The above shown FaceletViewHandlingStrategyPatch
works as follows by ultimately overriding the FacesContext#getELContext()
as below:
@Override
public ELContext getELContext() {
boolean isViewBuildTime = TRUE.equals(getWrapped().getAttributes().get(IS_BUILDING_INITIAL_STATE));
FaceletContext faceletContext = (FaceletContext) getWrapped().getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);
return (isViewBuildTime && faceletContext != null) ? faceletContext : super.getELContext();
}
You see, it checks if JSF is currently building the view and if there's a FaceletContext
present. If so, then return the FaceletContext
instead of the standard ELContext
implementation (note that FaceletContext
just extends ELContext
). This way the MethodExpression
will be created with the right ELContext
holding the right VariableMapper
.