JSF复合组件目标操作在c:forEach标记内失败

我们在c:forEach标记内使用commandButtons,其中按钮的动作接收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>

以下我的: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)可以正常工作.只有通过复合组件的目标机制传播的动作才能正确评估.
可以解释anoymone,为什么会发生这种情况以及如何解决这个问题?

我们在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()中放置了一个调试断点.

《JSF复合组件目标操作在c:forEach标记内失败》

在调用堆栈中,我们可以检查ELContext#getVariableMapper()以及谁负责创建MethodExpression.在具有复合组件的测试页面中依次嵌套一个常规命令按钮和一个复合命令按钮,我们可以在ELContext中看到以下区别:

常规按钮:

《JSF复合组件目标操作在c:forEach标记内失败》

我们可以看到常规按钮使用DefaultFaceletContext作为ELContext,并且VariableMapper包含正确的项目变量.

复合按钮:

《JSF复合组件目标操作在c:forEach标记内失败》

我们可以看到复合按钮使用标准ELContextImpl作为ELContext,并且VariableMapper不包含正确的项变量.因此,我们需要在调用堆栈中返回一些步骤,以查看此标准ELContextImpl的来源以及为何使用它而不是DefaultFaceletContext.

《JSF复合组件目标操作在c:forEach标记内失败》

一旦找到创建特定ELContext实现的位置,我们就可以发现它是从FacesContext#getElContext()获得的.但这并不代表复合组件的EL上下文!这由当前的FaceletContext表示.所以我们需要再回过头来弄清楚FaceletContext没有被传递下去的原因.

《JSF复合组件目标操作在c:forEach标记内失败》

我们可以在这里看到,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.

点赞