【问题标题】:Invoke ActionListener of Backing Component in Composite Component在复合组件中调用Backing Component的ActionListener
【发布时间】:2012-04-12 09:59:02
【问题描述】:

尝试编写一个允许多个文本输入的复合组件。我读到可以为复合组件定义支持组件,因此我不必编写渲染器或处理程序。我想不通的是如何将复合 xhtml 中声明的操作委托给支持组件。我想我还不太明白这个概念。有人有想法吗?

我正在使用 Tomcat 7、EL 2.2、Spring 3、Mojarra 2.1.7

这是我想使用该组件的方式:

<custom:multiInput value="#{backingBean.inputList}"/>

BackingBean.java 包含一个对象列表:

@Component
@Scope(value="view")
public class BackingBean {
    ...
    private List<Foo> inputList;
    ....
}

复合组件 multiInput.xhtml 如下所示:

<cc:interface componentType="MultiInput">
    <cc:attribute name="value" required="true" type="java.util.List" />
</cc:interface>

<cc:implementation>    
    <div id="#{cc.clientId}">
        <h:dataTable value="#{cc.attrs.rows}" var="row">
            <h:column>
                <!-- here will be a selector component in order to select a foo object -->
            </h:column>
            <h:column>
               <h:commandButton value="Remove Row">
                    <f:ajax execute=":#{cc.clientId}" render=":#{cc.clientId}" listener="#{cc.removeRow(row)}" />
                </h:commandButton>
            </h:column>
            <h:column>
                <h:commandButton value="Add Row" rendered="#{cc.lastRow}">
                    <f:ajax execute=":#{cc.clientId}" render=":#{cc.clientId}" listener="#{cc.addEmptyRow()}" />
                </h:commandButton>
            </h:column>
        </h:dataTable>
    </div>    
</cc:implementation>

这里是支持组件MultiInput.java

@FacesComponent(value="MultiInput")
public class MultiInput extends UIInput implements NamingContainer, Serializable{

    ...

    @Override
    public String getFamily() {
        return "javax.faces.NamingContainer";
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        initRowsFromValueAttribute();
        super.encodeBegin(context);
    }

    public void removeRow(MultiInputRow row) {
        // why is this method is never reached when clicking remove button?
    }

    public void addEmptyRow() {
        // why is this method is never reached when clicking add button?
    }

    public ListDataModel<MultiSelectRow> getRows() {
        return (ListDataModel<MultiSelectRow>) getStateHelper().eval(PropertyKeys.rows, null);
    }

    private void setRows(ListDataModel<MultiSelectRow> rows) {
        getStateHelper().put(PropertyKeys.rows, rows);
    }

    ...
}

现在 - removeRowaddEmptyRow 永远不会在 MultiInput 上调用。触发了 ajax 请求,但它在某处丢失了。为什么?

【问题讨论】:

  • 复合材料或其任何父项上是否有 rendered 属性?如果是这样,您是否 100% 在表单提交期间评估 true?另请参阅stackoverflow.com/questions/2118656/… 顺便说一句,您在代码中已经有很多红鲱鱼了。简化/重命名时请小心。
  • thx @BalusC,我更新了样本,因此它的“红鲱鱼”更少(希望如此)。是的,我验证了所有父组件rendered 属性都被评估为true。但让我想知道的是stackoverflow.com/questions/2118656/… 中的第 4 点。似乎没有保留支持组件。每次我单击删除或添加按钮CompositeComponentTagHandler.createComponent 时,都会创建一个支持组件MultiInput 的新实例。但为什么呢?
  • 我以前创建过类似的组件,它们工作正常。我复制粘贴了您的确切代码(为简单起见,我只用Object 替换了FooMultiSelectRow)并且效果很好。您的具体问题是在其他地方引起的,到目前为止发布的代码中没有显示。也许是嵌套形式。也许是一个评估falserendered 属性。谁知道。唯一的区别是我不使用 Spring,因此只在 bean 上使用标准 JSF 注释。

标签: jsf jsf-2 facelets composite-component


【解决方案1】:

虽然我没有详细了解所有内容,但我找到了一种方法让它发挥作用。由于每个请求都会创建支持组件MultiInput 的新实例,因此我必须通过覆盖saveStaterestoreState 来保存状态。这样我就可以将属性 rows 保留为一个简单的属性。我还删除了encodeBegin 方法并覆盖了getSubmittedValue

至少这种方式在 Mojarra 中是有效的。在使用默认设置的 MyFaces 时,我遇到了一些序列化异常,但我没有深入研究,因为我们将坚持使用 Mojarra。 MyFaces 似乎对 ajax 事件监听器更感兴趣。它需要侦听器方法中的“AjaxBehaviorEvent”参数。

这里是完整的支持组件MultInput

@FacesComponent(value = "MultiInput")
public class MultiInput extends UIInput implements NamingContainer, Serializable {

    ListDataModel<MultiInputRow> rows;

    @Override
    public String getFamily() {
        return "javax.faces.NamingContainer";
    }

    @Override
    public Object getSubmittedValue() {
        List<Object> values = new ArrayList<Object>();
        List<MultiInputRow> wrappedData = (List<MultiInputRow>) getRows().getWrappedData();
        for (MultiInputRow row : wrappedData) {
            if (row.getValue() != null) { // only if a valid value was selected
                values.add(row.getValue());
            }
        }
        return values;
    }

    public boolean isLastRow() {
        int row = getRows().getRowIndex();
        int count = getRows().getRowCount();
        return (row + 1) == count;
    }

    public boolean isFirstRow() {
        int row = getRows().getRowIndex();
        return 0 == row;
    }

    public void removeRow(AjaxBehaviorEvent e) {
        List<MultiInputRow> wrappedData = (List<MultiInputRow>) getRows().getWrappedData();
        wrappedData.remove(rows.getRowIndex());
        addRowIfEmptyList();
    }

    public void addEmptyRow(AjaxBehaviorEvent e) {
        List<MultiInputRow> wrappedData = (List<MultiInputRow>) getRows().getWrappedData();
        wrappedData.add(new MultiInputRow(null));
    }

    public ListDataModel<MultiInputRow> getRows() {
        if (rows == null) {
            rows = createRows();
            addRowIfEmptyList();
        }
        return rows;
    }

    public List<Object> getValues() {
        return (List<Object>) super.getValue();
    }

    private ListDataModel<MultiInputRow> createRows() {
        List<MultiInputRow> wrappedData = new ArrayList<MultiInputRow>();
        List<Object> values = getValues();
        if (values != null) {
            for (Object value : values) {
                wrappedData.add(new MultiInputRow(value));
            }
        }
        return new ListDataModel<MultiInputRow>(wrappedData);
    }

    private void addRowIfEmptyList() {
        List<MultiInputRow> wrappedData = (List<MultiInputRow>) rows.getWrappedData();
        if (wrappedData.size() == 0) {
            wrappedData.add(new MultiInputRow(null));
        }
    }

    @Override
    public Object saveState(FacesContext context) {
        if (context == null) {
            throw new NullPointerException();
        }
        Object[] values = new Object[2];
        values[0] = super.saveState(context);
        values[1] = rows != null ? rows.getWrappedData() : null;
        return (values);
    }

    @Override
    public void restoreState(FacesContext context, Object state) {
        if (context == null) {
            throw new NullPointerException();
        }

        if (state == null) {
            return;
        }
        Object[] values = (Object[]) state;
        super.restoreState(context, values[0]);
        rows = values[1] != null ? new ListDataModel<MultiInputRow>((List<MultiInputRow>) values[1]) : null;
    }

    /**
     * Represents an editable row that holds a value that can be edited.
     */
    public class MultiInputRow {

        private Object value;

        MultiInputRow(Object value) {
            this.value = value;
        }

        public Object getValue() {
            return value;
        }

        public void setValue(Object value) {
            this.value = value;
        }
    }
}

【讨论】:

    【解决方案2】:

    我在这里遇到同样的问题:使用&lt;f:ajax&gt;,复合组件支持组件中的动作侦听器方法不会执行。

    它在使用 Primefaces &lt;p:commandButton&gt; 时部分工作:在这种情况下正确调用了动作侦听器方法。但是,在这种情况下,“流程”属性的值似乎被忽略了:所有表单字段都已提交,这在我的情况下会导致验证失败。如果这对你来说不是问题,你可以试试这个。

    我创建了一些重现问题的测试类:

    复合组件文件testComponent.xhtml:

    <html xmlns="http://www.w3c.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
        xmlns:h="http://java.sun.com/jsf/html" 
        xmlns:p="http://primefaces.org/xmlns:ui="http://java.sun.com/jsf/facelets" 
        xmlns:composite="http://java.sun.com/jsf/composite">
    
    <composite:interface componentType="testComponent">
    </composite:interface>
    
    <composite:implementation>
        <div id="#{cc.clientId}">
            <h:panelGroup id="addPanel">
                <h:inputText id="operand1" value="#{cc.operand1}"/>
                <h:outputText value=" + " />
                <h:inputText id="operand2" value="#{cc.operand2}"/>
                <h:outputText value=" = " />
                <h:outputText id="result" value="#{cc.result}" />
                <br />
                <p:commandButton id="testButton1" value="Primefaces CommandButton"
                    actionListener="#{cc.add()}" process="addPanel" update="addPanel"/>
                <h:commandButton id="testButton2" value="f:ajax CommandButton">
                    <f:ajax execute="addPanel" render="addPanel" listener="#{cc.add()}" />
                </h:commandButton>
            </h:panelGroup>
        </div>
    </composite:implementation>
    </html>
    

    支持组件类:

    package be.solidfrog.pngwin;
    
    import javax.faces.component.FacesComponent;
    import javax.faces.component.UINamingContainer;
    import javax.faces.event.ActionEvent;
    
    @FacesComponent("testComponent")
    public class TestComponent extends UINamingContainer {
    
        private Integer operand1, operand2, result;
    
        public void add() {
            System.err.println("Adding " + operand1 + " and " + operand2);
            result = operand1 + operand2;
        }
    
        public Integer getOperand1() { return operand1; }
        public void setOperand1(Integer operand1) { this.operand1 = operand1; }
        public Integer getOperand2() { return operand2; }
        public void setOperand2(Integer operand2) { this.operand2 = operand2; }
        public Integer getResult() { return result; }
        public void setResult(Integer result) { this.result = result; }
    }
    

    以及使用页面test.xhtml:

    <!DOCTYPE html>
    <html xmlns="http://www.w3c.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
        xmlns:h="http://java.sun.com/jsf/html" xmlns:p="http://primefaces.org/ui"
        xmlns:ui="http://java.sun.com/jsf/facelets"
        xmlns:sf="http://java.sun.com/jsf/composite/solidfrog">
    <h:body>
        <h:messages />
        <h:form id="testForm">
            <h:outputLabel for="field1" value="Integer field: "/>
            <h:inputText id="field1" value="#{testBean.field1}" />
            <hr/>
            <sf:testComponent id="testComponent" />
        </h:form>
    </h:body>
    </html>
    

    单击第一个按钮并填写两个操作数字段时,结果计算正确。但是,在 field1 中输入非数字值时,验证失败。

    使用第二个按钮时,永远不会计算动作监听器方法。但是,总是提交完整的表单,因此在 field1 中输入非数字值也会触发错误。

    我还尝试了p:ajax,其行为与f:ajax 相同。

    我真的不知道这里发生了什么。希望有更多JSF智慧的人可以提供帮助。

    【讨论】:

    • 谢谢@Davy。实际上,在我的情况下,它可以同时使用 primefaces 和标准 jsf 按钮。也许您可以尝试以这种方式调用侦听器&lt;f:ajax execute=":#{cc.clientId}" render=":#{cc.clientId}" listener="#{cc.add}" /&gt; + 覆盖saveStaterestoreState。但这只是猜测……
    【解决方案3】:

    我认为 ajax 侦听器方法的方法签名应该包括 AjaxBehaviorEvent(未验证):

    public void addEmptyRow(AjaxBehaviorEvent event) { ... }
    

    f:ajax 标签应该看起来像(不带括号):

    <f:ajax execute=":#{cc.clientId}" render=":#{cc.clientId}" listener="#{cc.addEmptyRow}" />
    

    【讨论】:

    • @fischermatte 只是一些想法 ;-)
    • 实际上在使用 myfaces 而不是 mojarra 时,它似乎是必需的。看我的回答。
    猜你喜欢
    • 2016-09-29
    • 2023-04-01
    • 2012-10-30
    • 1970-01-01
    • 2013-10-22
    • 2013-02-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多