JSF 复合组件目标操作在 c:forEach 标记内失败
JSF Composite Component target actions fail within the c:forEach Tag
我们在 c:forEach
标签内使用 commandButtons
,其中按钮的 action
接收 forEach
var
属性作为参数,如下所示:
<c:forEach var="myItem" items="#{myModel}">
<h:commandButton
action="#{myController.process(myItem)}"
value="#{myItem.name}" />
</c:forEach>
这很好用。但是,如果我们将 commandButton
包装在复合组件中,它就不再起作用了:控制器被调用,但参数始终是 null
。
下面是一个包含按钮和包装按钮的复合组件的 c:forEach
标记示例。第一个有效,第二个无效。
<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>
使用以下 my:mybutton
实现:
<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>
请注意,按钮的 value
属性(也绑定到 c:ForEach
var
)工作正常。只有通过复合组件的 targets
机制传播的 action
没有得到正确的评估。
Anoymone 能否解释一下,为什么会发生这种情况以及如何解决这个问题?
我们在 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
:
Regular button:
的
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
.
我们在 c:forEach
标签内使用 commandButtons
,其中按钮的 action
接收 forEach
var
属性作为参数,如下所示:
<c:forEach var="myItem" items="#{myModel}">
<h:commandButton
action="#{myController.process(myItem)}"
value="#{myItem.name}" />
</c:forEach>
这很好用。但是,如果我们将 commandButton
包装在复合组件中,它就不再起作用了:控制器被调用,但参数始终是 null
。
下面是一个包含按钮和包装按钮的复合组件的 c:forEach
标记示例。第一个有效,第二个无效。
<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>
使用以下 my:mybutton
实现:
<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>
请注意,按钮的 value
属性(也绑定到 c:ForEach
var
)工作正常。只有通过复合组件的 targets
机制传播的 action
没有得到正确的评估。
Anoymone 能否解释一下,为什么会发生这种情况以及如何解决这个问题?
我们在 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()
.
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
:
Regular button:
的
We can see that the regular button uses DefaultFaceletContext
as ELContext
and that the VariableMapper
contains the right item
variable.
Composite button:
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
.