Invoke ActionListener of Backing Component in Comp

2019-06-09 01:29发布

try to write a composite component that allows mutltiple text inputs. I read that it is possible to define a backing component for a composite component, so I don't have to write a renderer nor a handler. What I couldn't figure out is how to delegate actions declared in composite's xhtml to the backing component. I guess i did not yet quite understand the concept of this. Does anybody has an Idea?

I am using Tomcat 7, EL 2.2, Spring 3, Mojarra 2.1.7

This is the way i'd like to use the component:

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

Where the BackingBean.java holds a list of objects:

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

The composite component multiInput.xhtml looks like this:

<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>

And here the backing component 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);
    }

    ...
}

Now - removeRow and addEmptyRow is never called on MultiInput. An ajax request is triggered but it gets lost somewhere. Why?

3条回答
smile是对你的礼貌
2楼-- · 2019-06-09 02:19

I think the method signature for ajax listener methods should include the AjaxBehaviorEvent (unverified):

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

and the f:ajax tag should just look like (without parentheses):

<f:ajax execute=":#{cc.clientId}" render=":#{cc.clientId}" listener="#{cc.addEmptyRow}" />
查看更多
做自己的国王
3楼-- · 2019-06-09 02:26

I'm struggling with the same problem here: using <f:ajax>, action listener methods in the composite component backing component are not executed.

It works partially when using Primefaces <p:commandButton>: the action listener method is correctly called in this case. However, the value of the 'process' attribute seems to be ignored in this case: All form fields are submitted, which causes validation failure in my case. If this is not a problem for you, you could try this.

I have created some test classes that reproduce the problem:

The composite component file 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>

The backing component class:

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; }
}

And the using page 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>

When clicking the first button and filling in the two operand fields, the result is correctly calculated. However, when a non-numeric value is entered in field1, there is a failed verification.

When using the second button, the action listener method is never calculated. However, the complete form is always submitted, so entering a non-numeric value in field1 triggers the error too.

I also tried p:ajax, which behaved the same as f:ajax.

I really have no idea what is happening here. Hopefully someone with more JSF wisdom can help out.

查看更多
手持菜刀,她持情操
4楼-- · 2019-06-09 02:26

Although I don't understand everything in detail, I found a way to make it work. Since on each request a new instance of the backing component MultiInput is created, I had to save the state by overwriting saveState and restoreState. This way I could keep the property rows as a simple property. I also removed the encodeBegin method and overwrote getSubmittedValue.

At least this way it is working in Mojarra. When using MyFaces with default settings, I got some serialization exceptions, but I did not get deepter into that since we will stick on Mojarra. Also MyFaces seemed to be more stricked with ajax event listeners. It required "AjaxBehaviorEvent" parameters in listener methods.

Here the complete backing component 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;
        }
    }
}
查看更多
登录 后发表回答