Search code examples
jsfwebsocketajax-update

How JSF 2.3 websocket work with Ajax with f:ajax actionlisenter?


I have tried to taste the new features of JSF 2.3, one attractive is the websocket.

I have read some sample codes from mojarra tests and JSF 2.3 specific @Push javadoc.

And encountered some issues when used f:websocket and f:ajax.

The facelets template is:

   <h:panelGroup id="messagePanel" layout="block">
        <ul>
            <ui:repeat value="#{ajaxBean.messages}" var="m">
                <li>#{m}</li>
            </ui:repeat>
        </ul>
    </h:panelGroup>

    <h:form id="form">
        <h:commandButton 
            id="sendMessage" 
            action="#{ajaxBean.sendMessage()}" 
            value="Send Ajax Message">
            <f:ajax/>
        </h:commandButton>
    </h:form>
    <h:form>
        <f:websocket channel="ajaxChannel" scope="view">
            <f:ajax event="ajaxEvent" render=":messagePanel" />
        </f:websocket>
    </h:form>
    <h:form>
        <f:websocket channel="ajaxListenerChannel" scope="view">
            <f:ajax event="ajaxListenerEvent" listener="#{ajaxBean.ajaxPushed}" render=":messagePanel" />
        </f:websocket>
    </h:form>

    <f:websocket channel="commandScriptChannel" scope="view" onmessage="onCommandScript"/>
    <h:form>
        <h:commandScript name="onCommandScript" action="#{ajaxBean.commandScriptExecuted()}" render=":messagePanel"/>
    </h:form>

And backend bean is:

@ViewScoped
@Named("ajaxBean")
public class AjaxBean implements Serializable {

    private static final Logger LOG = Logger.getLogger(AjaxBean.class.getName());

    @Inject
    @Push
    PushContext ajaxChannel;

    @Inject
    @Push
    PushContext ajaxListenerChannel;

    @Inject
    @Push
    PushContext commandScriptChannel;

    private List<String> messages = new ArrayList<>();

    public void ajaxPushed(AjaxBehaviorEvent e) throws AbortProcessingException{
        LOG.log(Level.INFO, "ajax pushed: " + e.toString());

        messages.add("ajaxListenerEvent is sent at: " + LocalDateTime.now());

        ajaxListenerChannel.send("ajaxListenerEvent");
    }

    public void commandScriptExecuted() {
        LOG.log(Level.INFO, "commandScriptExecuted pushed.");

        messages.add("commandScriptExecuted message is sent at: " + LocalDateTime.now());

        commandScriptChannel.send("onCommandScript");
    }

    public void sendMessage() {
//        LOG.log(Level.INFO, "ajax pushed by button: " + e.toString());

        messages.add("ajaxEvent is sent at: " + LocalDateTime.now());

        ajaxChannel.send("ajaxEvent");
    }

    public List<String> getMessages() {
        return messages;
    }

    public void setMessages(List<String> messages) {
        this.messages = messages;
    }

}

The result is the first button can trigger the ajax output as needed. But f:ajax with a listener does not work, and h:commandScript also does not work here.

How to correct these?


Solution

  • Your concrete problem is caused because you're nowhere in your code explicitly sending a push message to those websockets. If your attempt were possible in some way, the websocket would keep sending a push message to itself in an infinite loop. This doesn't make sense.

    In order to explicitly send a push message, you have to let your code explicitly call push.send(message), exactly as you already did with that <h:commandButton>.

    <h:form>
        <h:commandButton value="Send push message" action="#{bean.sendPushMessage}">
            <f:ajax />
        </h:commandButton>
    </h:form>
    <h:form>
        <f:websocket channel="pushWithAjaxUpdate" scope="view">
            <f:ajax event="updateMessages" listener="#{bean.updateMessages}" render=":messages" />
        </f:websocket>
    </h:form>
    <h:panelGroup id="messages" layout="block">
        <ul>
            <ui:repeat value="#{bean.messages}" var="message">
                <li>#{message}</li>
            </ui:repeat>
        </ul>
    </h:panelGroup>
    

    @Inject @Push
    private PushContext pushWithAjaxUpdate;
    
    public void sendPushMessage() {
        messages.add("Push message is sent at: " + LocalDateTime.now());
        pushWithAjaxUpdate.send("updateMessages");
    }
    
    public void updateMessages() {
        messages.add("Ajax event is received at: " + LocalDateTime.now());
    }
    

    I understand that you're just experimenting, but it should be said that above approach wouldn't make sense in real world code either. The <f:websocket> is superfluous in this specific use case and it would suffice to just use <h:commandButton><f:ajax listener="...">. It's in real world code expected that the push is not synchronously invoked by some command button in the very same page, but that it is asynchronously invoked by some business event. Otherwise you could as good just return the result in the ajax response itself. This saves an additional round-trip to the server.