The AjaxComponent strategy for JSF: The best of both worlds

Ajax-enable your components with a phase listener

1 2 3 4 5 6 7 Page 6
Page 6 of 7

The PhaseListener

Remember that you've also declared a phase listener in the lifecycle element. That phase listener is the key to the whole process. Listing 11 includes the whole class.

Listing 11. The PhaseListener class (AjaxListener)

package tutorial.jsf.ajax;

// Imports exluded ...

public class AjaxListener implements PhaseListener {
  private static final transient Log log = LogFactory.getLog(tutorial.jsf.ajax.AjaxListener.class);
  // These constants are used to get values from the request params
  // Determining if the request is an AjaxComponent and what the client id of the component
  // making the request is, respectively.
  private static final String AJAX_PARAM_KEY = "tutorial.jsf.ajax.AJAX_REQUEST";
  private static final String AJAX_CLIENT_ID_KEY = "tutorial.jsf.ajax.AJAX_CLIENT_ID";

  public AjaxListener() {
  }

  /**
   * Handling the any potential Ajax component requests after the Restore View phase makes the restored view
   * available to us.  Therefore, we can get the component (which made the request) from the view,
   * and let it respond to the request.
   */
  public void afterPhase(PhaseEvent event) {
    if (log.isInfoEnabled()) { log.info("BEGIN afterPhase()"); }
    FacesContext context = event.getFacesContext().getCurrentInstance();

    HttpServletRequest request = (HttpServletRequest)context.getExternalContext().getRequest();
    String ajaxParam = request.getParameter(AJAX_PARAM_KEY);

    // Check for the existence of the Ajax param
    if (ajaxParam != null && ajaxParam.equals("true")){  // 1
      if (log.isInfoEnabled()) { log.info("This is an ajax request."); }
      context.responseComplete(); // Let JSF know to skip the rest of the lifecycle

      String componentId = request.getParameter(AJAX_CLIENT_ID_KEY);  // Get the Ajax component ID
      if (componentId == null){ // 2
        if (log.isWarnEnabled()) { log.warn("No Client ID found under key: " + componentId); }
      } else {
        handleAjaxRequest(context, componentId);
      }

      // Save the state of the page
      context.getApplication().getStateManager().saveSerializedView(context);
    }
  }

  protected void handleAjaxRequest(FacesContext context, String ajaxClientId) {
    UIViewRoot viewRoot = context.getViewRoot();

    AjaxInterface ajaxComponent = null;
    try {
      ajaxComponent = (AjaxInterface)viewRoot.findComponent(ajaxClientId); // 3
    } catch (ClassCastException cce){
      throw new IllegalArgumentException("Component found under Ajax key was not of expected type.");
    }
    if (ajaxComponent == null){
      throw new NullPointerException("No component found under specified client id: " + ajaxClientId);
    }

    ajaxComponent.handleAjaxRequest(context); // 4
  }

  public void beforePhase(PhaseEvent arg0) {
    // We do nothing in the before phase.
  }

  public PhaseId getPhaseId() {
    return PhaseId.RESTORE_VIEW;
  }
}


Implementing PhaseListener requires only three methods. The first thing to decide is what phase the listener will affect. For the AjaxComponent, you want the RESTORE_VIEW phase. After that, you need to decide whether to execute before or after the phase (or both before and after, if need be).

The reason you choose RESTORE_VIEW is that it's the first phase; because you're handling an Ajax request, you want to avoid the rest of the JSF processing lifecycle. You use the afterPhase() method because you need the view to be restored, and thus make the component available to use, to handle the request.

Take a look at Listing 11 in more detail. I've annotated the code with some commented numbers that I'll use to direct your attention to particularly interesting sections.

In line 1, you check for the request parameter denoting this as an Ajax request. Remember back in the JavaScript, when you added that to the parameters of the Ajax call? If that parameter is not found, you immediately leave the phase listener and allow the regular lifecycle to proceed.

If, on the other hand, you do find the Ajax request parameter, you first call FacesContext.requestComplete(). That means that as soon as this phase is done, Ajax will skip any more processing.

Next, you get the clientId from the request. That was also sent with the Ajax call. In line 2, you check to make sure that it was found. If not, that means the request was set up wrong, and was flagged as an Ajax call, but no component clientId was sent. On the other hand, if the clientId is found, you call handleAjaxRequest().

Line 3 shows how you can recover the AjaxComponent from the view. JSF maintains each view as a hierarchical model, and every component can find its children via the findComponent() method. So you get the root of the view, and use find component to get the component that made the request.

Next, you check to make sure the component was found. If not, something has gone awry. Perhaps the clientId was not correct, or maybe you somehow haven't gotten the right view back. That can happen if you sent the request to a different URL than the one that set it up. In that case, JSF will create a new view tree -- one that doesn't have your component in it.

At line 4, once the component is found, you execute the handleAjaxRequest(). That brings you back to where you began. You've already seen how the component delegates to the renderer, and the renderer then returns an XML response.

You've also seen how the JavaScript onButtonClick() method used dojo.io.bind() to set the AjaxComponent.onAjaxResponse() function to handle the response. You can see how the onAjaxResponse() actually does this in Listing 12.

Listing 12. Handling the XML response back from the server: AjaxComponent.onAjaxResponse()

AjaxComponent.onAjaxResponse = function(responseXml){
    var status = responseXml.getElementsByTagName("status")[0].childNodes[0].nodeValue;
    var msg = responseXml.getElementsByTagName("message")[0].childNodes[0].nodeValue;

    var style = "";
    if (status != "OK"){
      style = "color: red;";
    }

    var msgNode = document.createTextNode(msg);

    var para = document.createElement("p");
    para.setAttribute("style", style);
    para.appendChild(msgNode);

    AjaxComponent.messageDiv.appendChild(para);
}

Parsing XML in JavaScript can be a little finicky. Still, you can see what's going on here. You get what was sent in the <status> element, and create a style based on that. Then you create a paragraph element, give it the style, and append the text from the <message> tag. Finally, using the domNode you saved when you rendered the component, you append the new paragraph element to the message <div>.

1 2 3 4 5 6 7 Page 6
Page 6 of 7