Previous Section  < Day Day Up >  Next Section

6.3 Conditionally Render Components

So far, the components we've created are always shown to the user, but sometimes that's not what we want. The menu area in the sample application, for instance, contains buttons for accepting or rejecting a report only if the current user is a manager. Component properties may also depend on runtime conditions, e.g., the way the menu area buttons are enabled or disabled depending on the current report's status in the sample application.

Figure 6-6 shows the menu area as it appears to a regular user for a brand new report without entries, i.e., without the buttons available only to managers and with all buttons disabled.

Figure 6-6. Menu area
figs/Jsf_0606.gif

If you come from a JSP background and have used JSTL a bit, your first impulse is likely to use the JSTL <c:if> or <c:choose> actions for conditional processing along these lines:

<%@ page contentType="text/html" %>

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>



<f:view>

  <h:form>

    <table cellpadding="0" cellspacing="0" width="100%">

      <tr>

        <td>

          <h:commandButton value="New" 

            disabled="#{reportHandler.currReport.status == 0}" />

          <h:commandButton value="Delete" 

            disabled="#{reportHandler.currReport.status == 0 ||

              reportHandler.currReport.status == 2 || 

              reportHandler.currReport.status == 3}" />

          <h:commandButton value="Submit" 

            disabled="#{reportHandler.currReport.status == 0 ||

              reportHandler.currReport.status == 2 || 

              reportHandler.currReport.status == 3}" />

          <c:if test="${reportHandler.manager}">

            <h:commandButton id="acceptButton" value="Accept" 

              disabled="#{reportHandler.currReport.status == 0 || 

                ! reportHandler.currReport.status == 2}" />

            <h:commandButton id="rejectButton" value="Reject" 

              disabled="#{reportHandler.currReport.status == 0 || 

                !reportHandler.currReport.status == 2}" />

          </c:if>

        </td>

        <td align="right">

          You're logged in as "${pageContext.request.remoteUser}"

          [<h:outputLink value="../../logout.jsp" />

             <h:outputText value="Logout" />

           </h:outputLink>]

          [<h:outputLink value="prefUser.faces" />

             <h:outputText value="Preferences" />

           </h:outputLink>]

        </td>

      </tr>

    </table>

  </h:form>

</f:view>

The <c:if> action in this example includes or excludes the manager buttons depending on a property of the reportHandler that tells whether the current user is a manager, and the components are disabled based on JSF EL expressions checking the current report's status.

I recommend that you fight the urge to implement the logic this way when you use JSF. There are a number of reasons:

  • Using JSTL actions and JSF actions means using both JSP EL expressions and JSF EL expressions. Because of the subtle differences between these two expression types, it's not always obvious what's going on. For instance, all variables in a JSP EL expression must refer to existing beans, while beans for variables in a JSF value binding expression are created automatically if they don't exist, as I described earlier. Another difference is that a JSF EL expression can't access page scope variables.

  • When you use <c:if> to conditionally include or exclude a JSF action, the corresponding component is physically added or removed from the view's component tree. This can lead to strange effects because the component loses its state when its removed from the view.

  • The <c:if> action and the type of conditions used in the EL expressions represent application logic, and it's better kept out of the template altogether.

I suggest that you look at the JSP page as just a way to layout JSF components and static text on the screen, and forget that it can actually contain conditional code like the JSTL <c:if> and <c:choose> elements.

If you can't fight the urge to use JSTL actions to conditionally include or exclude JSF component action elements, you must use the id attribute to give these JSF components explicit IDs, as in the previous example. If you don't, all kinds of strange things can happen.


6.3.1 Conditionally Disable Components Using Bean Properties

A better approach to conditionally include and disable components is to externalize all details for these decisions to a Java class. Example 6-6 shows this approach.

Example 6-6. Menu area using value expressions for conditional rendering (expense/stage1/menuArea.jsp)
<%@ page contentType="text/html" %>

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>



<f:view>

  <h:form>

    <table cellpadding="0" cellspacing="0" width="100%">

      <tr>

        <td>

          <h:commandButton value="New" 

            disabled="#{reportHandler.newDisabled}" />

          <h:commandButton value="Delete" 

            disabled="#{reportHandler.deleteDisabled}" />

          <h:commandButton value="Submit" 

            disabled="#{reportHandler.submitDisabled}" />

          <h:commandButton value="Accept" 

            rendered="#{reportHandler.acceptRendered}"

            disabled="#{reportHandler.acceptDisabled}" />

          <h:commandButton value="Reject" 

            rendered="#{reportHandler.rejectRendered}"

            disabled="#{reportHandler.rejectDisabled}" />

        </td>

        <td align="right">

          You're logged in as "${pageContext.request.remoteUser}"

          [<h:outputLink value="../../logout.jsp" />

             <h:outputText value="Logout" />

           </h:outputLink>]

          [<h:outputLink value="prefUser.faces" />

             <h:outputText value="Preferences" />

           </h:outputLink>]

        </td>

      </tr>

    </table>

  </h:form>

</f:view>

All HTML form elements have an attribute named disabled. When set to true, the browser disables the corresponding user interface widget: a button can't be clicked, an input field doesn't accept input, and so on.

All JSF components that are rendered as HTML form elements also support the disabled attribute and, in Example 6-6, I use value binding expressions that point to reportHandler properties to calculate the appropriate value at runtime. Example 6-7 shows how the property getter methods are implemented in the ReportHandler class.

Example 6-7. ReportHandler methods for conditional rendering and disabling
package com.mycompany.expense;



...

public class ReportHandler {

    ...

    private Rules rules;

    private Report currentReport;

    private String currentUser;

    private boolean isManager;

    ...



    public boolean isNewDisabled( ) {

        return isReportNew( );

    }



    public boolean isDeleteDisabled( ) {

        return isReportNew( ) || 

            !rules.canDelete(currentUser, isManager, currentReport);

    }



    public boolean isSubmitDisabled( ) {

        return isReportNew( ) || 

            !rules.canSubmit(currentUser, isManager, currentReport);

    }



    public boolean isAcceptDisabled( ) {

        return isReportNew( ) || 

            !rules.canAccept(currentUser, isManager, currentReport);

    }



    public boolean isRejectDisabled( ) {

        return isReportNew( ) || 

            !rules.canReject(currentUser, isManager, currentReport);

    }



    ...

    private boolean isReportNew( ) {

        return currentReport.getStatus( ) == Report.STATUS_NEW;

    }

    ...

}

The methods simply return a Boolean value that reflects how the corresponding button should be handled in the page.

Most of these methods first check if the report has status New (i.e., it doesn't have any entries yet), and if not, call a method in a separate Rules class to see if the report can be deleted, submitted, and so on. These rules are used for other parts of the application that we haven't looked at yet, so it makes sense to put them in a separate class. Having them in a separate class makes it easy to change the rules, if necessary.

The Rules class is shown in Example 6-8.

Example 6-8. Report processing rules in the Rules class
package com.mycompany.expense;



public class Rules {

    public boolean canEdit(String user, boolean isManager, Report report) {

            return report.getOwner( ).equals(user) && !isLocked(report);

    }



    public boolean canDelete(String user, boolean isManager, Report report) {

            return report.getOwner( ).equals(user) && !isLocked(report);

    }



    public boolean canSubmit(String user, boolean isManager, Report report) {

            return report.getOwner( ).equals(user) && !isLocked(report);

    }



    public boolean canAccept(String user, boolean isManager, Report report) {

            return isManager && report.getStatus( ) == Report.STATUS_SUBMITTED;

    }



    public boolean canReject(String user, boolean isManager, Report report) {

            return isManager && report.getStatus( ) == Report.STATUS_SUBMITTED;

    }



    public boolean canView(String user, boolean isManager, Report report) {

            return isManager || report.getOwner( ).equals(user);

    }



    public boolean isLocked(Report report) {

            return report.getStatus( ) == Report.STATUS_SUBMITTED ||

              report.getStatus( ) == Report.STATUS_ACCEPTED;

    }

}

All Rules methods consider both the type of user and the report's status when deciding whether the operation can be performed.

An instance of the Rules class is created in the ReportHandler constructor, where the instance variables for the current user and the current report are also initialized, as shown in Example 6-9.

Example 6-9. Initialization of ReportHandler instance variables
public class ReportHandler {

    ...

    private Rules rules;

    private Report currentReport;

    private String currentUser;

    private boolean isManager;

    ...

    public ReportHandler( ) {

        rules = new Rules( );

        currentReport = getCurrentReport( );

        currentUser = getCurrentUser( );

        isManager = isManager( );

    }

    ...

    public Report getCurrentReport( ) {

        if (currentReport == null) {

            currentReport = createNewReport( );

        }

        return currentReport; 

    }



    public boolean isManager( ) {

        FacesContext context = FacesContext.getCurrentInstance( );

        ExternalContext ec = context.getExternalContext( );

        return ec.isUserInRole("manager");

    }

    ...



    private String getCurrentUser( ) {

        FacesContext context = FacesContext.getCurrentInstance( );

        ExternalContext ec = context.getExternalContext( );

        return ec.getRemoteUser( );

    }



    private Report createNewReport( ) {

        Report report = new Report( );

        report.setOwner(getCurrentUser( ));

        ...

        return report;

    }

    ...

}

The isManager() method deserves some explanation. As you may recall from Chapter 4, container-based security works in terms of roles assigned to real users when the web application is deployed. To test if the current user is playing a certain role, the servlet HttpServletRequest class has a method called isUserInRole( ). That's the method used by the isManager() method, indirectly, through a JSF class called javax.faces.context.ExternalContext.

The isManager() method first gets a reference to the current FacesContext instance through the static getCurrentInstance() method. Methods about the runtime context that are not related directly to JSF are delegated to the ExternalContext class, available through the getExternalContext() method. The isUserInRole() is one such method, and the isManager() method calls it with the role name "manager" and returns the result.

The getCurrentUser() method uses the same technique to get a reference to the ExternalContext, and then uses the getRemoteUser() method to get the username of the authenticated user making the request.

6.3.2 Conditionally Include Components Using Bean Properties

All JSF action elements also have a rendered property. If you set it to false, the component (and all its children) remains in the view but it's never asked to render itself. In Example 6-6, the rendered attributes for the Accept and Reject buttons are set to value binding expressions that delegate the rendering decision to the reportHandler bean. Here's how the ReportHandler class implements the corresponding getter methods:

    public boolean isAcceptRendered( ) {

        return isManager( );

    }



    public boolean isRejectRendered( ) {

        return isManager( );

    }

The JSF <h:commandButton> action elements in Example 6-6 use value binding expressions that get the appropriate values from the reportHandler bean for the disabled and rendered attribute values, pushing all the detailed decisions to the bean. Placing this type of logic in a bean instead of in the page has clear maintenance advantages, so I recommend that you follow this pattern for your own applications.

In this chapter, we looked at how JSF components are created and how they generate the HTML elements for the browser in some detail. In the next chapter, we'll look at what happens when the user fills out a form and submits it to JSF for processing.

    Previous Section  < Day Day Up >  Next Section