JSF复合组件目标的行动失败内c:forEach标记
我们使用commandButtons
一个c:forEach
标签,其中按钮的action
接收forEach
var
属性作为这样的参数内:JSF复合组件目标的行动失败内c:forEach标记
<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
,它不会被正确评估。 动物可以请解释,为什么会发生这种情况,以及如何解决这个问题?
我们在mojarra 2.2.8,el-api 2.2.5,tomcat 8.0上。
这是由Mojarra中的错误引起的,也可能是JSF规范中关于复合组件重定向方法表达式的疏忽。
解决方法是在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;
}
}
}
在faces-config.xml
如下安装它:
<factory>
<view-declaration-language-factory>com.example.FaceletViewHandlingStrategyPatch</view-declaration-language-factory>
</factory>
我怎么确定下来的呢?
我们已经确认问题在于,当在复合组件中调用操作时,方法表达式参数变为null
,而操作本身在另一个组合组件中声明。
<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>
我所做的就是定位谁负责背后#{bean.action(item)}
创建MethodExpression
实例的代码的第一件事。我知道它通常是通过ExpressionFactory#createMethodExpression()
创建的。我也知道所有EL上下文变量通常通过ELContext#getVariableMapper()
提供。所以我在createMethodExpression()
中放置了一个调试断点。
在调用栈,我们可以检查ELContext#getVariableMapper()
,也谁负责创建MethodExpression
。在又一个复合组件嵌套一个常规命令按钮和一个复合命令按钮测试页,我们可以看到在ELContext
以下区别:
我们可以看到,经常按钮使用DefaultFaceletContext
作为ELContext
,并且VariableMapper
包含右侧item
变量。
复合键:
我们可以看出,复合按钮使用标准ELContextImpl
为ELContext
,而且VariableMapper
不包含正确item
变量。因此,我们需要在调用堆栈中返回一些步骤,以查看此标准ELContextImpl
的来源以及为什么使用它而不是DefaultFaceletContext
。
一旦发现创建特定ELContext
实现中,我们可以发现,它是从FacesContext#getElContext()
获得。但是这并不代表复合组件的EL背景!这由当前的FaceletContext
表示。所以我们需要再回头去找出为什么FaceletContext
没有被传递。
我们可以在这里看到CompositeComponentTagHandler#applyNextHander()
没有经过FaceletContext
但FacesContext
来代替。这是JSF规范中可能忽略的确切部分。 ViewDeclarationLanguage#retargetMethodExpressions()
本应要求提供另一个参数,代表涉及的实际ELContext
。
但它是什么。我们现在无法立即改变规范。最好我们能做的就是向他们报告一个问题。
上面所示的FaceletViewHandlingStrategyPatch
作品最终重写FacesContext#getELContext()
如下如下:
@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();
}
你看,它会检查,如果JSF目前正在建设的看法,如果有一个FaceletContext
存在。如果是,则返回FaceletContext
而不是标准ELContext
执行(注意FaceletContext
just extends ELContext
)。这样,MethodExpression
将创建右边ELContext
举行权利VariableMapper
。
情况与我们的情况并不完全相同:我们没有在复合组件中使用'
我无法重现与Mojarra 2.2.14和Tomcat 8.5.16的问题。我只是没有在项目中手动包含EL,因为Tomcat已经有了自己的EL版本。 – BalusC
当给定的c:forEach代码片段又被封装在另一个复合组件中时,我再现了你的问题。 – BalusC