Help

Built with Seam

You can find the full source code for this website in the Seam package in the directory /examples/wiki. It is licensed under the LGPL.

JSF 2.0 introduces view parameters, which provide a declarative value binding between query string parameters and model properties. View parameters are most relevant on a non-faces (GET) request, but also get applied to a faces (POST) request. They can also be written into the outbound URL of the new bookmarkable component tags (<h:link> and <h:button>). As such, view parameters go a long way towards accommodating the action-oriented scenario in JSF.

However, a critical piece of this scenario is the ability to execute a method before the view is rendered, optionally navigating to an alternative view afterward (whether because of a constraint violation, a security restriction, or because the request was for a pseudo-view). That's the purpose of a view action.

Origin

View actions originated in Seam as page actions. One of the biggest criticism of JSF 1.2 and earlier is that the NonFaces request lacks a front controller. The problems caused by direct requests for JSP pages that Struts solved 8 years ago still plague JSF. This need is partly addressed by the view parameters proposal, which handles the mapping between request parameters and model properties via conversion, validation, and assignment. But the followup is missing, the ability to execute a method to make use of these bindings. Seam solved that problem with page actions.

An early edition

As JSF 2.0 was being developed, the idea was to shoehorn the page action functionality into the new, extended event system introduced in JSF 2.0 (i.e., SystemEvent) using <f:event type="preRenderView">. But at the same time, a new NonFaces request life cycle emerged in the view parameters proposal that arguably suits the page action functionality better.

Building on the non-faces life cycle

In this addendum proposal to view parameters, I'd like to extend this new life cycle execution to include view actions. There are three reasons why view actions are significant:

  1. View parameters are tightly coupled with view actions and lacking one would tremendously reduce the value of the other
  2. The view parameters proposal introduced the groundwork to support view actions
  3. Invoking view actions would complete the JSF life cycle on a non-faces request (i.e., by introducing the Invoke Application phase)

The view parameter proposal introduced the view metadata life cycle, which is really just the normal JSF life cycle applied only to the javax_faces_metadata facet of UIViewRoot. This partial tree traversal executes on both a non-faces and faces request. It's exactly the same as the new partial tree traversal in JSF 2.0 that is invoked via Ajax. On a non-faces request, the javax_faces_metadata facet of UIViewRoot is built in the Restore View phase and taken through the full JSF life cycle. The remainder of the component tree is built during the Render Response phase and encoded.

Here's what happens during that life cycle on a non-faces request:

  1. Create the UIViewRoot and build only the javax_faces_metafacet facet of UIViewRoot
  2. Collect the UIViewParameters
  3. Decode each UIViewParameter (capture the request parameter as the submitted value on the component)
  4. Convert and validate the submitted value on each UIViewParameter
  5. Apply the converted value on each UIViewParameter to the value ValueExpression

The JSF request then switches to the Render Response phase and the remainder of the view is built (if non-faces request) and rendered. UIViewParameters are encoded into the tree during the Render Response phase of a non-faces request.

What's clearly missing from the view metadata life cycle is the Invoke Application step, followed by navigation if a case is matched. Better yet, everything is already setup for it! We have view metadata and we have a life cycle for it. We just need one additional step. And this solves two important problems:

  1. We want to be able to navigate away from a page if view parameters don't validate or a pre-render event listener determines that this request is invalid
  2. We have a way to tie into the declarative navigation system, which system events do not accommodate

Here's how a view action would be defined:

<f:view>
    <f:metadata>
        <f:viewParam name="id" value="#{blog.entryId}"/>
        <f:viewAction execute="#{blog.loadEntry}" onPostback="true"/>
    </f:metadata>
</f:view>

UIViewAction is similar to UICommand.

Implementation details

Here's one way to process view actions. We add a method to UIViewRoot called processViewActions(FacesContext). This is fired in the Render Response phase one line before the view is rendered.

UIViewRoot viewRoot = facesContext.getViewRoot();
viewRoot.processViewActions(facesContext);
if (!facesContext.getResponseComplete()) {
	facesContext.getApplication().getViewHandler().renderView(facesContext, viewRoot);
}

Below is the UIViewRoot#processViewActions(FacesContext) method. It's almost identical to ActionListener#processAction(FacesContext)

You'll notice that a UIViewAction extends UICommand by introducing the onPostback property. This property indicates whether a view action should always be executed before a view is rendered (e.g., security check) or if it should only be used on a NonFaces request (e.g., lookup data). The default is false. Also notice that if a navigation case is matched (the view root changes) then all remaining page actions are skipped.

NavigationHandler navHandler = context.getApplication().getNavigationHandler();
boolean postback = context.isPostback();

if (!postback && context.getValidationFailed()) {
    navHandler.handleNavigation(context, null, null);
    return;
}

List<UIViewAction> actions = getViewActions();
for (UIViewAction action : actions) {
    if (postback && !action.isOnPostback()) {
        continue;
    }
    String outcome = null;
    String fromAction = null;

    MethodBinding binding = action.getAction();
    if (binding != null) {
        try {
            Object returnVal = binding.invoke(context, null);
            outcome = (returnVal != null ? returnVal.toString() : null);
            fromAction = binding.getExpressionString();
        } catch (MethodNotFoundException e) {
            throw new FacesException(binding.getExpressionString() + ": " + e.getMessage(), e);
        } catch (EvaluationException e) {
            throw new FacesException(binding.getExpressionString() + ": " + e.getMessage(), e);
        }
    }

    if (navHandler == null) {
        navHandler = context.getApplication().getNavigationHandler();
    }
    navHandler.handleNavigation(context, fromAction, outcome);
    // if a navigation took and it changed the view root, short-circuit remaining actions
    if (context.getResponseComplete() || context.getViewRoot() != this) {
        break;
    }
}

As a result of all this, we have a life cycle that proceeds a NonFaces request that matches the life cycle on a postback. Here's how they differ in purpose:

  • The purpose of a view action is to set the stage for rendering and navigate on a failure
  • The purpose of a postback action is to perform a CRUD operation and navigate on success

Unfortunately, view actions need to be executed differently then postback actions. That's because any view action can be followed by a navigation. Additionally, if a navigation case is matched, all remaining view actions should be short-circuited. On a postback, only the primary action can trigger navigation. So it's not enough just so add UIViewAction components to the metadata facet and run the normal JSF life cycle. Perhaps we can resolve this discrepancy so that the view meta data life cycle is nothing more than the regular JSF life cycle using a partial tree traversal.

Vamp

There is one additional bit of functionality that can be considered as an optional clause. If a view action results in navigation, it's only natural that the view metadata life cycle be executed for that new view. Thus, the logic in the UIViewRoot#processViewActions(FacesContext) would be recursive (keep in mind this is just a sketch):

if (context.getResponseComplete() || context.getViewRoot() != this) {
	context.getViewRoot().processViewParameters(context);
	context.getViewRoot().processViewActions(context);
    break;
}

Prior art