Blog-Archiv

Samstag, 14. September 2019

JSF AJAX Test Page

To get used to the JSF AJAX facilities you should play around with it for some time. This Blog provides a basic implementation with different AJAX and non-AJAX callbacks to start with.

The execute attribute of an <f:ajax> element enumerates the ids of fields to take to the server when called, the render attribute enumerates ids of elements to update when the asynchronous response comes back from the server. Optionally the event attribute enumerates events (e.g. "click") that should trigger the AJAX call.

Remarkable:

  • From the execute and render attributes of an <f:ajax> element, you can not easily refer to element-ids being in another form (in case several forms are on a page)
  • Fields that are not in a form are not taken to the server, even when referenced explicitly
  • The viewId, as retrieved from FacesContext.getCurrentInstance().getViewRoot().getViewId(), is the same on every browser tab, and in any browser. Also the clientId is the same everywhere, additionally it is identical to the id. None of them identifies the JSF user interface instance (browser tab/window).

Contents

  1. Application Screenshots
  2. Full Source Code

Application Screenshots

In the following you see screenshots of the user interface, and explanations about what has been clicked and typed on the UI, and how this was implemented in the application.

Mind that I changed to a new browser-tab several times during these screenshots, so the shown identities and times are not consistent.

User Interactions

  1. Initial State
  2. Typing "Name"
  3. Selecting "Language"
  4. Clicking "AJAX commandButton"
  5. Clicking "Listener commandButton"
  6. Clicking "Action commandButton"
  7. "Action commandButton" without POST-Redirect-GET

The test page contains two forms and some command-buttons. It displays its own log messages on bottom. Following is the look when loading it into a browser:

The blue area on top is the observed and logged input form. Below are some informations about the HTTP session and the JSF view. On bottom are the log messages, generated by the Java controller when some callback is invoked. It logs the time, phase and message-text of any server-side event.

Initially just the controller constructor logged its creation in RENDER_RESPONSE phase. In field "HTTP Method" we see that a GET was used to read the JSF page.

    public PersonController() {
        log("PersonController() constructor");
        creationTime = logging().getCurrentTime();
    }

After entering some text in "Name" field and clicking elsewhere into the page (to make the browser trigger the change-event) it looks like this:

We see that, in PROCESS_VALIDATIONS phase, a valueChange callback was called, and consequently in UPDATE_MODEL_VALUES the new value was set into the bean. Then, in INVOKE_APPLICATION phase, an AJAX callback was triggered.

These three things were caused by a valueChangeListener attribute on the text field, the data-binding in value attribute, and an AJAX listener in a nested <f:ajax> element. The render-id "fieldsToUpdateByAjax" is the information- and logging-area below the blue panel, which should be updated with server-information on any event.

                    <h:inputText value="#{personController.name}" id="textInputId"
                            valueChangeListener="#{personController.textValueChangeCallback}">
                        
                        <f:ajax render="fieldsToUpdateByAjax"
                                listener="#{personController.textAjaxCallback}"/>
                    </h:inputText>

Mind that we see the old field value (null) just in the valueChange callback, but no more in the AJAX callback. There is a workaround to queue a valueChange event to the INVOKE_APPLICATION phase, so that the business logic can access also the old value, by calling event.setPhaseId(PhaseId.INVOKE_APPLICATION); event.queue().

In field "HTTP Method" we see that a POST was used to perform the AJAX call.

When we now clear the messages, and then select "de" as "Language":

The same happened for the language selection-menu: a valueChange callback, then the bean setter, then an AJAX callback.

                    <h:selectOneMenu value="#{personController.language}"  id="selectInputId"
                            valueChangeListener="#{personController.selectValueChangeCallback}">
                            
                        <f:selectItems value="#{personController.languages}"/>
                        
                        <f:ajax render="fieldsToUpdateByAjax"
                                listener="#{personController.selectAjaxCallback}"/>
                    </h:selectOneMenu>

Clear messages, then press "AJAX commandButton". Looks like this:

The "AJAX commandButton" has been programmed to take the "Name" text field to the server (see execute=...), but not the "Language" selection field, thus we see the name again being put into the bean, but not the language. Then an AJAX callback is invoked.

                    <h:commandButton value="AJAX commandButton"> <!-- Doesn't take selectInputId to server! -->
                        <f:ajax execute="textInputId"
                                render="fieldsToUpdateByAjax" 
                                listener="#{personController.commandAjaxCallback}"/>
                    </h:commandButton>

Two more buttons to study. The "Listener commandButton" actually is an actionListener. When clearing messages and pressing that button, following happens:

We see that this button submitted the full form ("Name" and "Language" fields), but it did not reset the form for new input. The listener method was finally executed in INVOKE_APPLICATION phase.

                    <h:commandButton value="Listener commandButton"
                            actionListener="#{personController.commandListenerCallback}"/>

Now lets clear the messages and press the "Action commandButton", which is the normal JSF form submitter.

The "Action commandButton" button did the same as the "Listener commandButton", it took all fields in the form to the server, but after that it reloaded the page to get a new empty input form. We can see that the PersonController constructor has been called again.

                    <h:commandButton value="Action commandButton"
                            action="#{personController.commandActionCallback}"/>

This is the Java controller part to do the reload:

    public String commandActionCallback()   {
        log("commandActionCallback(), postRedirectGet="+postRedirectGet);
        if (postRedirectGet == false)
            return null;    // stay on same page
        
        String viewId = FacesContext.getCurrentInstance().getViewRoot().getViewId();
        return viewId+"?faces-redirect=true";    // reload page
    }

We see that there is a switch that lets us choose whether the button reloads the page or just saves the properties to the bean. Now lets deactivate this by clicking onto the "POST-Redirect-GET" checkbox, and then press "Action commandButton" again.

We see that the fields were not cleared, and the PersonController constructor has not been called. Anyway the input data have been saved to the bean. So this seems to be the same as "Listener commandButton".


Full Source Code

There are no special configurations, no JPA involved, stick to web.xml, faces-config.xml, beans.xml, ApplicationFacesConfig.java as I showed them in my recent Blog about setup of a JSF project in Eclipse. I called the project jsfAjax, put this as artifactId into the Maven pom.xml.

Project Structure

Files

  1. index.xhtml
  2. Person.java
  3. PersonController.java
  4. Logging.java

index.xhtml

Remarkable: in JSF, you can avoid the browser-specific default action when the user presses the ENTER key by adding following attribute to any input field (except textarea): onkeypress="if (event.keyCode === 13) return false". See <h:inputText> with id="textInputId". When you remove this and press ENTER while standing in the "Name" text field, the first found submit-button would get triggered by the browser; in this case it would be the "AJAX commandButton", which may not be what we want.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
        xmlns:h="http://xmlns.jcp.org/jsf/html"
        xmlns:f="http://xmlns.jcp.org/jsf/core"
>
    <h:head><title>JSF AJAX Example</title></h:head>
    
    <h:body>
        <h2>JSF AJAX Test Form</h2>
        
        <h:form style="background-color: lightBlue;">
            <h:panelGrid columns="2">
                <h:panelGrid columns="2">
                    <h:outputLabel>Name</h:outputLabel>
                    <h:inputText value="#{personController.name}" id="textInputId"
                            valueChangeListener="#{personController.textValueChangeCallback}"
                            onkeypress="if (event.keyCode === 13) return false;">
                            <!-- Avoid browser default which would trigger first submit-button on page -->
                        
                        <f:ajax render="fieldsToUpdateByAjax"
                                listener="#{personController.textAjaxCallback}"/>
                    </h:inputText>
                    
                    <h:outputLabel>Language</h:outputLabel>
                    <h:selectOneMenu value="#{personController.language}"  id="selectInputId"
                            valueChangeListener="#{personController.selectValueChangeCallback}">
                            
                        <f:selectItems value="#{personController.languages}"/>
                        
                        <f:ajax render="fieldsToUpdateByAjax"
                                listener="#{personController.selectAjaxCallback}"/>
                    </h:selectOneMenu>
                </h:panelGrid>
                
                <h:panelGrid columns="1">
                    <h:commandButton value="AJAX commandButton"> <!-- Doesn't take selectInputId to server! -->
                        <f:ajax execute="textInputId"
                                render="fieldsToUpdateByAjax" 
                                listener="#{personController.commandAjaxCallback}"/>
                    </h:commandButton>
                    
                    <h:commandButton value="Listener commandButton"
                            actionListener="#{personController.commandListenerCallback}"/>
                    
                    <h:commandButton value="Action commandButton"
                            action="#{personController.commandActionCallback}"/>
                            
                </h:panelGrid>
            </h:panelGrid>
        </h:form>
        
        <hr/>
        
        <h:form id="fieldsToUpdateByAjax">
        <!-- AJAX-fields in other form can not see fields inside here, but they can see this form -->
            <h:selectBooleanCheckbox value="#{personController.postRedirectGet}" id="postRedirectGetId">
                <f:ajax render="fieldsToUpdateByAjax"/>
            </h:selectBooleanCheckbox>
            <h:outputLabel for="postRedirectGetId">Make "Action commandButton" perform POST-Redirect-GET</h:outputLabel>
                 
            <h:panelGrid columns="2">
                <h:outputLabel value="viewId:"/>#{view.viewId}
                <h:outputLabel value="id, clientId:" />#{view.id}, #{view.clientId}
                <h:outputLabel value="Session Start:"/> #{logging.creationTime}
                <h:outputLabel value="View Creation:"/> #{personController.creationTime}
                <h:outputLabel value="Latest Request:"/> #{personController.requestTime}
                <h:outputLabel value="HTTP Method:"/>#{request.method}
                <h:outputLabel value="Bean-Identity:"/>#{personController.beanIdentity}
                <h:outputLabel value="ViewRoot-Identity:"/>#{personController.viewRootIdentity}
            </h:panelGrid>
            
            <hr/>

            <h:outputLabel><b>Log</b> (session-scoped)</h:outputLabel>
            
            <h:commandButton value="Clear">
                <f:ajax render="fieldsToUpdateByAjax" listener="#{personController.clearLog}"/>
            </h:commandButton>
    
            <h:selectBooleanCheckbox value="#{logging.newestOnTop}" id="newestOnTopId">
                <f:ajax render="fieldsToUpdateByAjax"/>
            </h:selectBooleanCheckbox>
            <h:outputLabel for="newestOnTopId">Newest on Top</h:outputLabel>
                 
            <h:dataTable value="#{logging.messages}" var="message" style="width: 100%;" border="1" cellpadding="3">
                <h:column><f:facet name="header">Time</f:facet>#{message.time}</h:column>
                <h:column><f:facet name="header">Phase</f:facet>#{message.phase}</h:column>
                <h:column><f:facet name="header">Message</f:facet>#{message.text}</h:column>
            </h:dataTable>
        
        </h:form>
        
    </h:body>
</html>

Person.java

This normally would be an @Entity stored in some database.

package fri.jsf;

import java.io.Serializable;

public class Person implements Serializable
{
    private String name;
    private String language;
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    
    public String getLanguage() {
        return language;
    }
    public void setLanguage(String language) {
        this.language = language;
    }
}

PersonController.java

This "backing bean" serves both the blue input-form and the information- and logging-area below. Any logging message is generated here.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package fri.jsf;

import java.io.Serializable;
import java.util.Arrays;
import java.util.List;

import javax.faces.context.FacesContext;
import javax.faces.event.ActionEvent;
import javax.faces.event.AjaxBehaviorEvent;
import javax.faces.event.ValueChangeEvent;
import javax.faces.view.ViewScoped;
import javax.inject.Named;

@Named
@ViewScoped
public class PersonController implements Serializable
{
    private Person person = new Person();
    private List<String> languages = Arrays.asList(new String[] { "en", "fr", "de" });
    
    private boolean postRedirectGet = true;
    private final String creationTime;
    private String requestTime;

    public PersonController() {
        log("PersonController() constructor");
        creationTime = logging().getCurrentTime();
    }
    
    // form properties
    
    public String getName() {
        return person.getName();
    }
    public void setName(String name) {
        log("setName("+logging().formatString(name)+")");
        person.setName(name);
    }
    
    public String getLanguage() {
        return person.getLanguage();
    }
    public void setLanguage(String language) {
        log("setLanguage("+logging().formatString(language)+")");
        person.setLanguage(language);
    }
    
    public List<String> getLanguages() {
        return languages;
    }
    
    // text field callbacks
    
    public void textValueChangeCallback(ValueChangeEvent changeEvent)   {
        log("textValueChangeCallback("+logging().formatChangeEvent(changeEvent)+")");
    }

    public void textAjaxCallback(AjaxBehaviorEvent behaviorEvent)   {
        log("textAjaxCallback()");
    }
    
    // select field callbacks
    
    public void selectValueChangeCallback(ValueChangeEvent changeEvent)   {
        log("selectValueChangeCallback("+logging().formatChangeEvent(changeEvent)+")");
    }

    public void selectAjaxCallback(AjaxBehaviorEvent behaviorEvent)   {
        log("selectAjaxCallback()");
    }
    
    // button callbacks
    
    public String commandActionCallback()   {
        log("commandActionCallback(), postRedirectGet="+postRedirectGet);
        if (postRedirectGet == false)
            return null;    // stay on same page
        
        String viewId = FacesContext.getCurrentInstance().getViewRoot().getViewId();
        return viewId+"?faces-redirect=true";    // reload page
    }

    public void commandListenerCallback(ActionEvent actionEvent)   {
        log("commandListenerCallback()");
    }
    
    public void commandAjaxCallback(AjaxBehaviorEvent behaviorEvent)   {
        log("commandAjaxCallback()");
    }
    
    // logging area
    
    public boolean isPostRedirectGet() {
        return postRedirectGet;
    }
    public void setPostRedirectGet(boolean postRedirectGet) {
        System.out.println("setPostRedirectGet("+postRedirectGet+")");
        this.postRedirectGet = postRedirectGet;
    }
    
    public String getCreationTime() {
        return creationTime;
    }
    
    public String getRequestTime() {
        return requestTime;
    }
    
    public String getBeanIdentity() {
        return String.valueOf(System.identityHashCode(this));
    }
    
    public String getViewRootIdentity() {
        return String.valueOf(System.identityHashCode(FacesContext.getCurrentInstance().getViewRoot()));
    }
    
    public void clearLog(AjaxBehaviorEvent behaviorEvent)   {
        logging().clear(null);
        System.out.println("clearLog()");
    }
    
    private void log(String message)  {
        requestTime = logging().getCurrentTime();
        logging().addMessage(message);
    }
    private Logging logging()  {
        return javax.enterprise.inject.spi.CDI.current().select(Logging.class).get();
    }
}

Logging.java

This holds the list of logging messages. The PersonController was bound to view-scope, but to be able to observe everything at any time, this is bound to session-scope. It provides presentation logic for the area below the blue input-form.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package fri.jsf;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.*;
import javax.enterprise.context.SessionScoped;
import javax.faces.context.FacesContext;
import javax.faces.event.AjaxBehaviorEvent;
import javax.faces.event.ValueChangeEvent;
import javax.inject.Named;

@Named
@SessionScoped
public class Logging implements Serializable
{
    public static class Message implements Serializable
    {
        // it is not possible with JSF to have this public final without getters!
        private final String time;
        private final String phase;
        private final String text;
        
        public Message(String time, String phase, String text) {
            this.time = time;
            this.phase = phase;
            this.text = text;
        }
        public String getTime() {
            return time;
        }
        public String getPhase() {
            return phase;
        }
        public String getText() {
            return text;
        }
        @Override
        public String toString() {
            return time+" | "+phase+" | "+text;
        }
    }

    
    private String creationTime;
    private List<Message> messages = new ArrayList<>();
    private boolean newestOnTop = false;

    public String getCreationTime() {
        if (creationTime == null)
            creationTime = getCurrentTime();
        return creationTime;
    }
    
    public List<Message> getMessages() {
        return messages;
    }
    
    public void clear(AjaxBehaviorEvent behaviorEvent) {
        messages.clear();
    }
    
    public boolean isNewestOnTop() {
        return newestOnTop;
    }
    public void setNewestOnTop(boolean newestOnTop) {
        this.newestOnTop = newestOnTop;
        Collections.reverse(messages);
    }
    
    public void addMessage(String message)  {
        String time = getCurrentTime();
        String phase = ""+FacesContext.getCurrentInstance().getCurrentPhaseId();
        
        Message m = new Message(time, phase, message);
        if (newestOnTop)
            messages.add(0, m);
        else
            messages.add(m);
        
        System.out.println(m);
    }
    
    public String formatChangeEvent(ValueChangeEvent event) {
        return formatString(event.getOldValue())+" -> "+formatString(event.getNewValue());
    }
    
    public String formatString(Object s) {
        return (s == null) ? "null" : "\""+s+"\"";
    }
    
    public String getCurrentTime() {
        return new SimpleDateFormat("HH:mm:ss:SSS").format(new Date());
    }
}


Conclusion

JSF Blogs are as long as the learning curve is steep. Hope this article helps :-!




Keine Kommentare: