在Composite Component中调用Backing Component的ActionListener

时间:2012-03-28 18:46:17

标签: jsf jsf-2 facelets composite-component

尝试编写一个允许多个文本输入的复合组件。我读到可以为复合组件定义支持组件,因此我不必编写渲染器或处理程序。我无法弄清楚的是如何将复合的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);
    }

    ...
}

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

3 个答案:

答案 0 :(得分:1)

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

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

和f:ajax标签应该看起来像(没有括号):

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

答案 1 :(得分:0)

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

使用Primefaces <p:commandButton>时,它可以部分工作:在这种情况下,正确调用了动作侦听器方法。但是,在这种情况下,似乎忽略了'process'属性的值:提交所有表单字段,这导致我的情况下验证失败。如果这不是你的问题,你可以试试这个。

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

复合组件文件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智慧的人可以提供帮助。

答案 2 :(得分:0)

虽然我不太了解所有细节,但我找到了一种方法让它发挥作用。由于在每个请求上都创建了支持组件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;
        }
    }
}