View Parameters and View Actions

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11), Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, with Java 17 or later. Faces 4.0 has the same tags and attributes; nothing in the 4.0 or 4.1 change lists is specific to view parameters or view actions (see What Changed), but nothing below was run on Faces 4.0. The examples were compiled and deployed in a scratch Bookshelf WAR on Tomcat 11.0.26 with Weld 6.0.4.Final, and the status codes, headers and messages quoted here were captured with curl against Mojarra 4.1.17 and, except where the text says that only one implementation was run, repeated on MyFaces 4.1.4 (checked on 2026-10-08). The OmniFaces material (OmniFaces 5.5.5) comes from its documentation and was not run.

Faces was designed around the postback: a page is shown, the user fills a form, the same view is submitted with POST, and the response is rendered from the saved state. A public catalogue needs the opposite. Somebody follows a link, a search engine crawls an address, a user bookmarks a book or pastes a URL into a chat, and the server must produce the right page from the address alone: /detail.xhtml?isbn=9780441013593. Two tags in the view metadata make that possible:

  • f:viewParam binds a request parameter to a model property, with conversion and validation, so that the address carries the state of the page.

  • f:viewAction runs a method when the view is entered, usually to load the thing the parameters identify, and may navigate away, for example to a 404 page or to the new address of a page that moved.

Together with h:link and h:button (which build the matching addresses), the POST-Redirect-GET pattern and the HTTP status codes, they turn a postback-oriented framework into one that behaves well on the open web. This page covers all of it for the Bookshelf book-detail page.

The Big Picture

View parameters and view actions are ordinary parts of the request processing lifecycle (see the six phases). The part that surprises people is the first one. On an initial (non-postback) request Faces normally skips straight to Render Response, because there is no submitted data to process. The Jakarta Faces specification (section 2.2.1, Restore View) makes one exception: if the view declares metadata with children (view parameters, view actions or f:event listeners), Faces builds a view that contains only the metadata, and the remaining phases run over it. Without metadata the request goes straight to Render Response.

flowchart TD req["GET /detail.xhtml?isbn=9780441013593"] --> rv["1 Restore View: view has metadata, so the full lifecycle runs"] rv --> arv["2 Apply Request Values: viewParam reads the parameter, viewActions with immediate or an early phase run"] arv --> pv["3 Process Validations: convert, required, validators"] pv -->|"conversion or validation failed"| rr pv --> umv["4 Update Model Values: setIsbn is called"] umv --> ia["5 Invoke Application: viewAction load runs"] ia -->|"outcome for another view"| red["302 redirect to that view"] ia -->|"responseSendError and responseComplete"| err["404 sent, lifecycle ends"] ia --> rr["6 Render Response: the view is built, preRenderView listeners run, HTML is sent"] rr -->|"validation failed and the listener sends 400"| bad["400 sent"]

The diagram is the route of the Bookshelf detail page developed below. Three consequences follow, and each is demonstrated in the sections of this page:

  • The model is not updated until phase 4. A view action that runs earlier (immediate="true" or an early phase) sees null in the bean.

  • When conversion or validation of a view parameter fails, phases 4 and 5 are skipped: the default view action does not run, and the page is rendered (with status 200) unless you do something about it.

  • The view itself is built late, when Render Response starts. A view action runs before the page’s components exist.

Where They Live: f:metadata

Both tags are children of f:metadata, which must be a direct child of f:view (in a view without an explicit f:view the implicit one is enough) and must be in the page itself or in a template client, never in the template. The placement rules, the template slot that lets every page declare its own metadata, and the silent failure when the block sits in a template are explained in the core tags page; they are not repeated here. If a parameter or an action seems to be ignored, check that first. One portability rule matters when the page uses a template: the block must go in a slot that the template places directly inside its f:view (the ui:insert name="metadata" pattern of that page), not in the content slot. MyFaces 4.1.4 rejected an f:metadata placed inside the content ui:define of a template client with the error Parent UIComponent ... should be instance of UIViewRoot, while Mojarra 4.1.17 accepted it; the examples on this page are standalone views, where the problem does not arise.

A Complete Example: the Book Detail Page

The page below is the one the rest of this section dissects. A book is addressed by its ISBN, the parameter must be present and must have 13 digits, the book is loaded by a view action, an unknown ISBN produces a real 404 and a malformed address a real 400.

src/main/webapp/detail.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<f:metadata>
  <f:viewParam name="isbn" value="#{bookDetailBean.isbn}" required="true"
               requiredMessage="An ISBN is required">
    <f:validateRegex pattern="\d{13}"/>
  </f:viewParam>
  <f:viewAction action="#{bookDetailBean.load}"/>
  <f:event type="preRenderView" listener="#{bookDetailBean.rejectInvalid}"/>
</f:metadata>
<h:head>
  <title>#{bookDetailBean.book.title}</title>
</h:head>
<h:body>
  <h1>#{bookDetailBean.book.title}</h1>
  <p>#{bookDetailBean.book.author}, #{bookDetailBean.book.year}</p>
  <h:messages id="flashMessages" infoClass="notice"/>
</h:body>
</html>
BookDetailBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.IOException;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;

@Named
@RequestScoped
public class BookDetailBean {

    @Inject
    private BookService books;

    @Inject
    private FacesContext facesContext;

    @Inject
    private ExternalContext externalContext;

    private String isbn;
    private Book book;

    public void load() throws IOException {
        book = books.findByIsbn(isbn);
        if (book == null) {
            externalContext.responseSendError(404, "No such book");
            facesContext.responseComplete();
        }
    }

    public void rejectInvalid() throws IOException {
        if (facesContext.isValidationFailed()) {
            externalContext.responseSendError(400, "Invalid view parameters");
            facesContext.responseComplete();
        }
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public Book getBook() {
        return book;
    }
}

FacesContext and ExternalContext are injected with @Inject (supported since Faces 2.3). The two error pages are ordinary Servlet error pages declared in web.xml; they can be Facelets views:

WEB-INF/web.xml (excerpt)
<error-page>
  <error-code>404</error-code>
  <location>/error/not-found.xhtml</location>
</error-page>
<error-page>
  <error-code>400</error-code>
  <location>/error/bad-request.xhtml</location>
</error-page>
error/not-found.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="jakarta.faces.html">
<h:head><title>Not found</title></h:head>
<h:body><h1>Book not found</h1><p>The address you followed does not match a book.</p></h:body>
</html>

(error/bad-request.xhtml has the same shape.) Requesting the page with curl -i gave the following, identically on Mojarra 4.1.17 and MyFaces 4.1.4:

Request Status Why

detail.xhtml?isbn=9780441013593

200

Parameter valid, view action found the book

detail.xhtml?isbn=9780000000000

404

Valid shape, but load() found no book and sent the error

detail.xhtml

400

required failed; the view action did not run; the preRenderView listener sent 400

detail.xhtml?isbn=abc

400

f:validateRegex failed, same route

detail.xhtml?isbn=

400

An empty value is treated as missing by required

The 404 page came back with status 404 and the Facelets content of not-found.xhtml, so a Faces view works as a Servlet error page (error pages in general: the exception handling page). The sections that follow explain each ingredient: f:viewParam, f:viewAction, Missing Entities: Sending a Real 404 and Malformed Addresses: a Real 400.

f:viewParam

f:viewParam creates a UIViewParameter in the view’s metadata. UIViewParameter extends UIInput, so everything a text field can have (a converter, validators, required, messages, value change listeners) is valid on it. What makes it different is where the value comes from: the request parameter whose name is given by name, on the query string of a GET or in the body of a POST. It is "bi-directional": it reads the parameter into the model on a request and writes the model value back into links when the page is rendered (Bookmarkable Links: h:link, h:button and includeViewParams).

Attributes

Attribute Required Meaning

name

yes

The request parameter to read. Only the request parameter map is consulted: path segments are not

value

no

A value expression the converted value is written to. Without it, see A View Parameter Without a Value Expression

required, requiredMessage

no

The parameter must be present and not empty; custom text for the failure

converter, converterMessage

no

A converter id or instance; custom text for a conversion failure

validator, validatorMessage

no

A validator method; custom text that replaces every validator message of this parameter

valueChangeListener

no

Called when the stored value changes

id, binding

no

The component id and a binding to the component, as on every component

for, maxlength

no

Listed in the VDL; for is documented as referring to an attached object of a composite component

Nested content is the same as on an input: f:convertNumber, f:convertDateTime, f:converter, f:validateLength, f:validateLongRange, f:validateRegex, f:validateBean and the rest of the standard validators. There is no label attribute (see Messages), and immediate and rendered are not on the tag: the Javadoc of UIViewParameter.isImmediate() states that the setting is not relevant for view parameters and is always false.

Conversion and Validation

A view parameter is converted and validated like any input, in the Process Validations phase, and a failure queues a message and skips Update Model Values and Invoke Application. The conversion turns the string into the type of the bound property: a Long, a LocalDate, an enum, or a whole entity. The first example binds a String and checks its shape with a standard validator. The second binds the Book itself with a converter, which is attractive because the bean then receives the entity:

BookConverter.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.Dependent;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.convert.Converter;
import jakarta.faces.convert.FacesConverter;
import jakarta.inject.Inject;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;

@Dependent
@FacesConverter(value = "bookshelf.book", managed = true)
public class BookConverter implements Converter<Book> {

    @Inject
    private BookService books;

    @Override
    public Book getAsObject(FacesContext context, UIComponent component, String value) {
        if (value == null || value.isBlank()) {
            return null;
        }
        return books.findByIsbn(value.trim());
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Book book) {
        return book == null ? "" : book.isbn();
    }
}
detail-converter.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<f:metadata>
  <f:viewParam id="book" name="isbn" value="#{bookViewBean.book}" converter="bookshelf.book"/>
  <f:viewAction action="#{bookViewBean.requireBook}"/>
</f:metadata>
<h:head><title>Detail</title></h:head>
<h:body>
  <h1 id="title">#{bookViewBean.book.title}</h1>
</h:body>
</html>
BookViewBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.IOException;
import org.example.bookshelf.model.Book;

@Named
@RequestScoped
public class BookViewBean {

    @Inject
    private FacesContext facesContext;

    @Inject
    private ExternalContext externalContext;

    private Book book;

    public void requireBook() throws IOException {
        if (book == null) {
            externalContext.responseSendError(404, "No such book");
            facesContext.responseComplete();
        }
    }

    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }
}

The converter above returns null for an unknown ISBN, and the view action turns that null into a 404: detail-converter.xhtml?isbn=1 and a request without the parameter both answered 404 and ?isbn=9780441013593 answered 200. The choice of what the converter does with an unknown value matters:

  • A converter that throws a ConverterException (the usual way to report a bad value) produced status 200, the page rendered with the converter’s message, and the view action never ran (a trace bean in the action stayed empty). For a public page that is the wrong answer to a crawler. Return null and decide in the action, or keep the parameter a String and look the entity up in the action, as the first example does.

  • With required="true" and a converter that returns null, the required check fails (it looks at the converted value), so an unknown ISBN produced the requiredMessage and status 200 rather than a 404.

  • The converters page covers writing converters in general; the point here is only that a view parameter converter runs on every request that carries the parameter.

The String version keeps the lookup in one place (the action) and the shape check in the page, which is the combination used by the detail page. The entity version saves the lookup code in the bean at the price of the three details above.

Messages

A failed conversion, required or validator queues a FacesMessage as for any input, and h:messages shows it. A view parameter has no label attribute, so the standard messages that mention the component are prefixed with its generated client id (the validators page and converters page describe the messages themselves). Requesting the scratch page noval.xhtml?n=abc&m=9, where the parameters had no custom messages, rendered:

j_idt1: Validation Error: Value is required.
j_idt2: 'abc' must be a number consisting of one or more digits.
j_idt3: Validation Error: Specified attribute is not between the expected values of 1 and 5.

on Mojarra, and the same texts with the ids j_id__md_1 to j_id__md_3 on MyFaces. Always set requiredMessage, converterMessage and validatorMessage on a view parameter whose messages a user can ever see (the 404/400 route above means this is rare for public detail pages, but common for search and report pages that render with a message). The fuller treatment of messages, bundles and FacesMessage is on the messages page.

Missing and Empty Parameters

Three cases behave differently, and a trace bean that logged every setter and validator call showed them (both implementations agreed):

Request What happened

?isbn=ok

validator called in Process Validations, setIsbn("ok") called in Update Model Values

?isbn= (empty value)

the validator method was not called, and setIsbn("") was called: the bean receives an empty string

no isbn at all

neither the validator nor the setter was called; the bean property keeps its initial value

So an empty parameter is not "absent": the model gets "" unless the context parameter jakarta.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL is true (on Mojarra the setter then received null; the parameter was not tried on MyFaces). A validator, even one that would reject null, is not asked about a parameter that is not there: only required catches it, which is why the detail page has required="true". With the @NotNull of a Bean Validation constraint on the model property the same reasoning applies, but that combination was not run for this page; the Faces 2.3 change list contains an item titled "@NotNull and f:viewParam" (What Changed).

A View Parameter Without a Value Expression

value is optional. Without it the converted value is the component’s local value and, when it is valid, Faces also stores it in request scope under the parameter’s name (the Javadoc of UIViewParameter.updateModel()), so #{isbn} works anywhere in the page. A page with <f:viewParam name="isbn" required="true"/> printed isbn=x for ?isbn=x on both implementations. It is handy for a throw-away page and bad design for anything bigger: the value is untyped (a String unless there is a converter), cannot be used by a view action without going through EL, and includeViewParams links can only reproduce it from the component. MyFaces 4.1.4 also failed with a ClassCastException in UIViewParameter.getStringValue when a converter such as jakarta.faces.Integer produced a non-String for a parameter without value (Mojarra rendered it). Give the parameter a value.

View Parameters on a Postback

A view parameter is part of the view, and so it takes part in every request to the view, including the POST that a form on the page triggers. The form’s action URL does not carry the query string (the rendered form tag had action="/bookshelf/trace.xhtml", with a ;jsessionid path parameter on the first response), so on a postback the parameter is not in the request. What happens next differs between the implementations:

  • Mojarra kept the value. UIViewParameter.encodeAll() stores the string value as the submitted value while the page is rendered, so it is saved in the view state. On the postback the validator and the setter ran again with the stored value (the trace showed validator @PROCESS_VALIDATIONS value=ok and setIsbn(ok) @UPDATE_MODEL_VALUES on a request that contained only the form fields), and a request-scoped bean received the ISBN again.

  • MyFaces 4.1.4 did not. On the same postback neither the validator nor the setter ran, and a request-scoped bean’s isbn stayed null. Worse, a required="true" view parameter failed with "Value is required" on the postback, even when a @ViewScoped bean still held the value. I did not investigate why.

The portable rules are therefore:

  1. Hold the value in a @ViewScoped bean (see the scopes page) (or re-read it from somewhere you control) if the page has a form. A request-scoped bean loses the parameter on MyFaces.

  2. Make required apply only to the initial request: required="#{not facesContext.postback}". required accepts a value expression that is evaluated for each request. With that, the @ViewScoped page below worked on both implementations; with plain required="true" it failed on MyFaces.

postback-fixed.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<f:metadata>
  <f:viewParam name="isbn" value="#{postbackBean.isbn}" required="#{not facesContext.postback}" requiredMessage="ISBN missing"/>
</f:metadata>
<h:head><title>Postback</title></h:head>
<h:body>
  <h:messages/>
  <h:form id="f">
    <h:commandButton id="go" value="Save" action="#{postbackBean.save}"/>
  </h:form>
  <p id="state">isbn=#{postbackBean.isbn} saves=#{postbackBean.saves}</p>
</h:body>
</html>
PostbackBean.java
package org.example.bookshelf.web;

import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import java.io.Serializable;

@Named
@ViewScoped
public class PostbackBean implements Serializable {

    private String isbn;
    private int saves;

    public void save() {
        saves++;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public int getSaves() {
        return saves;
    }
}

A GET with ?isbn=ok printed isbn=ok saves=0, a GET without the parameter printed the "ISBN missing" message, and the postback printed isbn=ok saves=1 on both. (On the same page with required="true", MyFaces answered the postback with "ISBN missing" and saves=0.)

Two more facts about postbacks:

  • A parameter that is present on the postback URL overrides the stored value. Posting to trace.xhtml?isbn=other made the validator and the setter run with other on both implementations (the VDL describes name as the parameter read "on an initial request or to override the stored value on a postback").

  • In a stateless view (<f:view transient="true">) there is no stored value on any implementation: the rendered view state is the literal stateless. A required view parameter then failed on the postback with the requiredMessage on both. Posting to a URL that repeats the query string fixed it, and so did a plain HTML hidden input named like the parameter, which a browser submits with the form:

review-stateless-hidden.xhtml (excerpt)
<h:form id="f">
  <h:inputTextarea id="text" value="#{reviewFormBean.text}"/>
  <input type="hidden" name="isbn" value="#{reviewFormBean.isbn}"/>
  <h:commandButton id="go" value="Save" action="#{reviewFormBean.saveNoRedirect}"/>
</h:form>

Source: Jakarta Faces 4.1 API: UIViewParameter (encodeAll, decode, processValidators, updateModel) and VDL: f:viewParam

Do Not Initialise the Bean in @PostConstruct

CDI creates the bean when the first expression touches it, which for a view parameter is the setter call in Update Model Values. The @PostConstruct method therefore runs before the setter and sees null. A bean with @PostConstruct void init() { seen = String.valueOf(isbn); } showed seen=null and isbn=42 for ?isbn=42 on both implementations. Load data in a view action, which runs after the parameters are in the model.

f:viewAction

f:viewAction creates a UIViewAction, a component that implements ActionSource. It is "a light-weight front-controller": a method that runs when a view is requested, with the view parameters already applied, and whose return value goes to the navigation system just like the outcome of a command button. It exists so that you do not need a preRenderView listener (View Action, preRenderView or @PostConstruct?) to initialise a page.

Attributes

Attribute Required Meaning

action

yes

A method expression Object action(); the toString() of the result is the logical outcome given to the NavigationHandler

actionListener

no

A method that takes an ActionEvent, or no arguments

phase

no

APPLY_REQUEST_VALUES, PROCESS_VALIDATIONS, UPDATE_MODEL_VALUES or INVOKE_APPLICATION (default); case does not matter; takes precedence over immediate

immediate

no

true runs the action in Apply Request Values instead of Invoke Application

onPostback

no

false by default: the action does not run on a postback. true allows it

if

no

Like rendered on a visual component: when it evaluates to false the view action does nothing

An invalid phase makes setPhase throw a FacesException (Javadoc). The action returns a String in nearly all code, but the expression may return any Object. Because UIViewAction is an ActionSource, the listener attributes of a command exist on it (see the core tags page for the listener tags); the examples on this page use only action, and listeners on a view action were not exercised.

In Which Phase and in Which Order

The following page puts one view action on each phase option and logs the phase in which every call happens, together with the validator, the setter and a preRenderView listener.

trace.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets">
<f:metadata>
  <f:viewParam name="isbn" value="#{traceBean.isbn}" validator="#{traceBean.validate}"/>
  <f:viewAction action="#{traceBean.act('default')}"/>
  <f:viewAction phase="APPLY_REQUEST_VALUES" action="#{traceBean.act('apply')}"/>
  <f:viewAction phase="PROCESS_VALIDATIONS" action="#{traceBean.act('validations')}"/>
  <f:viewAction phase="UPDATE_MODEL_VALUES" action="#{traceBean.act('update')}"/>
  <f:viewAction immediate="true" action="#{traceBean.act('immediate')}"/>
  <f:viewAction onPostback="true" action="#{traceBean.act('onPostback')}"/>
  <f:event type="preRenderView" listener="#{traceBean.pre}"/>
</f:metadata>
<h:head><title>Trace</title></h:head>
<h:body>
  <h:messages/>
  <h:form id="f">
    <h:commandButton id="go" value="Go" action="#{traceBean.save}"/>
  </h:form>
  <ol id="log"><ui:repeat value="#{traceBean.log}" var="l"><li>#{l}</li></ui:repeat></ol>
</h:body>
</html>
TraceBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.ComponentSystemEvent;
import jakarta.faces.validator.ValidatorException;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.ArrayList;
import java.util.List;

@Named
@RequestScoped
public class TraceBean {

    @Inject
    private FacesContext facesContext;

    private String isbn;
    private final List<String> log = new ArrayList<>();

    public String act(String name) {
        log.add(name + " action @" + facesContext.getCurrentPhaseId() + " isbn=" + isbn
                + " postback=" + facesContext.isPostback());
        return null;
    }

    public void validate(FacesContext ctx, UIComponent c, Object value) {
        log.add("validator @" + ctx.getCurrentPhaseId() + " value=" + value);
        if ("bad".equals(value)) {
            throw new ValidatorException(new FacesMessage("bad isbn"));
        }
    }

    public void pre(ComponentSystemEvent e) {
        log.add("preRenderView validationFailed=" + facesContext.isValidationFailed()
                + " postback=" + facesContext.isPostback());
    }

    public void save() {
        log.add("save action @" + facesContext.getCurrentPhaseId());
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        log.add("setIsbn(" + isbn + ") @" + facesContext.getCurrentPhaseId());
        this.isbn = isbn;
    }

    public List<String> getLog() {
        return log;
    }
}

trace.xhtml?isbn=ok produced this log, identical on Mojarra and MyFaces apart from the formatting of the phase (APPLY_REQUEST_VALUES 2 against APPLY_REQUEST_VALUES(2)):

apply action @APPLY_REQUEST_VALUES 2 isbn=null postback=false
immediate action @APPLY_REQUEST_VALUES 2 isbn=null postback=false
validator @PROCESS_VALIDATIONS 3 value=ok
validations action @PROCESS_VALIDATIONS 3 isbn=null postback=false
setIsbn(ok) @UPDATE_MODEL_VALUES 4
update action @UPDATE_MODEL_VALUES 4 isbn=ok postback=false
default action @INVOKE_APPLICATION 5 isbn=ok postback=false
onPostback action @INVOKE_APPLICATION 5 isbn=ok postback=false
preRenderView validationFailed=false postback=false
Setting Runs in What the bean can see

immediate="true"

Apply Request Values

isbn is null: nothing has been converted yet

phase="APPLY_REQUEST_VALUES"

Apply Request Values

isbn is null

phase="PROCESS_VALIDATIONS"

Process Validations

isbn is still null: the validator ran, the model is not updated yet

phase="UPDATE_MODEL_VALUES"

Update Model Values

isbn is set, because the view parameter is declared first

default (INVOKE_APPLICATION)

Invoke Application

isbn is set; this is the right place for almost everything

The default phase is what you want when the action depends on the parameters. The early phases exist for cases in which the action must run before conversion or validation, for example to redirect a request that must never be converted (an old address, a banned parameter) or to check a precondition with #{param.isbn}.

When the view parameter was invalid (?isbn=bad, rejected by the validator) the log lost every entry after the validator: isbn=null for the validations action, no setIsbn, no update and no default or onPostback entry, and preRenderView printed validationFailed=true. So a view action in the default phase does not run for a request whose parameters failed to convert or validate. This is the reason the detail page checks isValidationFailed() in a preRenderView listener (Malformed Addresses: a Real 400). The preRenderView listener always runs because it belongs to Render Response.

The if Attribute

if works like rendered: if it is false at the moment the action event is queued, nothing happens. That moment is Apply Request Values, before any view parameter was converted. A page with

<f:viewParam name="isbn" value="#{traceBean.isbn}"/>
<f:viewAction if="#{traceBean.isbn ne null}" action="#{traceBean.act('if-model')}"/>
<f:viewAction if="#{not empty param.isbn}" action="#{traceBean.act('if-param')}"/>
<f:viewAction if="#{param.debug eq '1'}" action="#{traceBean.act('if-debug')}"/>

and ?isbn=ok&debug=1 logged only the second and third actions on both implementations: the condition on the model property was false because the property had not been set yet, while the conditions on param could be evaluated. Base if on request data, or decide inside the action method. (The OmniFaces o:viewAction evaluates if after the model is updated; see [omnifaces].)

onPostback

By default a view action ignores postbacks: the view action of a page does not run when the user submits a form on it. This is intended; the action initialises the page. With onPostback="true" it runs on postbacks as well, and, as the log above shows, onPostback="true" does not mean "only on postbacks": the onPostback action also ran on the initial GET (at the same position as the default action). A postback of trace.xhtml logged, on Mojarra:

validator @PROCESS_VALIDATIONS 3 value=ok
setIsbn(ok) @UPDATE_MODEL_VALUES 4
onPostback action @INVOKE_APPLICATION 5 isbn=ok postback=true
save action @INVOKE_APPLICATION 5
preRenderView validationFailed=false postback=true

with the default view action absent. (On MyFaces the same postback logged isbn=null for the onPostback action and no validator or setter call, for the reason given in View Parameters on a Postback.)

Ajax requests are postbacks. With onPostback="true" the action ran on an Ajax request whose execute was only the button (f:ajax default): on Mojarra the log contained viewAction on ajax action @INVOKE_APPLICATION 5 isbn=null postback=true, and on MyFaces the log stayed empty. Mojarra seems to process the metadata on every Ajax request, MyFaces only when the execute list reaches it; I did not investigate further, and I did not test other execute values reliably. Avoid onPostback="true" on pages with Ajax unless the action is idempotent and you have run it on the implementation you deploy.

The outcome of the action goes to the application’s NavigationHandler, exactly as for a command. The Javadoc adds one rule: if the outcome leads to a different view id, the runtime forces a redirect to it, whether or not the matched navigation case asks for one. A page old-book.xhtml that maps an old address to the new one demonstrates it:

old-book.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<f:metadata>
  <f:viewParam name="id" value="#{oldBookBean.id}"/>
  <f:viewAction action="#{oldBookBean.forward}"/>
</f:metadata>
<h:head><title>Old address</title></h:head>
<h:body>Moved</h:body>
</html>
OldBookBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.context.ExternalContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;

@Named
@RequestScoped
public class OldBookBean {

    @Inject
    private ExternalContext externalContext;

    private String id;

    public String forward() {
        return "detail?isbn=" + id;
    }

    public String forwardWithSession() {
        externalContext.getSession(true);
        return "detail?isbn=" + id;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

old-book.xhtml?id=9780441013593 answered 302 with Location: /bookshelf/detail.xhtml?isbn=9780441013593 on Mojarra even though the outcome had no faces-redirect=true. An outcome that resolves to the same view id (including null) just continues with Render Response. The specification adds that, when a view action causes the navigation, the messages queued so far are kept in the flash so that they survive the redirect.

MyFaces 4.1.4 has a gap here: for a request that arrived without a session cookie (a first visit from a bookmark, which is the usual case) the same redirect ended in status 500 although the Location header was set. The log shows an IllegalStateException ("Cannot create a session after the response has been committed") thrown by FlashImpl while saving the messages after the redirect. A request with an existing session answered 302, and so did a request whose action created the session first. That is what forwardWithSession() does, and old-book-session.xhtml answered 302 on both implementations. If you deploy on MyFaces and redirect from a view action, create the session before returning the outcome. Mojarra needs no such workaround.

On Mojarra an outcome with a literal faces-redirect=false (detail?faces-redirect=false&isbn=...) was copied into the redirect URL as an ordinary query parameter; omit the parameter instead.

View Action, preRenderView or @PostConstruct?

All three are used to "load the page’s data". They are not equivalent:

f:viewAction f:event type="preRenderView" @PostConstruct

Runs

Once per initial request, after parameters are in the model

On every request that renders the view, postbacks and Ajax renders included

When the bean is created, before the parameters are set (see Do Not Initialise the Bean in @PostConstruct)

Runs when parameters are invalid

No (default phase)

Yes

Yes

Can navigate

Yes, through the outcome

Only programmatically

No

Can run on postbacks

With onPostback="true"

Always

Per bean instance

Typical use

Load the entity, redirect, send 404

Per-render work, check isValidationFailed()

Initialise state that does not depend on parameters

A preRenderView listener on the detail page, as in the core tags page, loads the book again on every postback that renders the view (the core tags page printed postback=true for it). Use f:viewAction for the initial load.

Bookmarkable Links: h:link, h:button and includeViewParams

The command components submit forms. A bookmarkable link is a plain GET link whose address is computed when the page is rendered (the specification calls it preemptive navigation). h:link renders an a element and h:button a button that sets window.location. Both take an outcome, which is resolved with the navigation system, and parameters from nested f:param. The catalogue page links to the detail page:

catalogue.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets">
<h:head>
  <title>Catalogue</title>
</h:head>
<h:body>
  <ul>
    <ui:repeat value="#{catalogueBean.books}" var="b">
      <li>
        <h:link value="#{b.title}" outcome="detail">
          <f:param name="isbn" value="#{b.isbn}"/>
        </h:link>
        <h:button value="Open" outcome="detail">
          <f:param name="isbn" value="#{b.isbn}"/>
        </h:button>
      </li>
    </ui:repeat>
  </ul>
</h:body>
</html>

The rendered markup, on both implementations:

<a href="/bookshelf/detail.xhtml?isbn=9780441013593">Dune</a>
<input type="button" onclick="window.location.href='/bookshelf/detail.xhtml?isbn=9780441013593'; return false;" value="Open" />

(MyFaces adds ;jsessionid=... to the path when the first request has no session cookie.) Parameter values are URL-encoded: a f:param value a b&c rendered as isbn=a+b%26c, and the fragment attribute appends #top. An outcome that matches nothing (outcome="nosuchview") rendered no link: Mojarra, in every project stage except Production, printed the text with the message "This link is disabled because a navigation case could not be matched" and logged JSF1090, MyFaces printed the text only (see the project stage effects).

includeViewParams="true" adds the target view’s own view parameters to the link, using the values the current model holds. It removes the repetition of f:param for "stay on this page but change one thing" links. The detail page declares an isbn and a lang parameter, and links to itself:

detail-links.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<f:metadata>
  <f:viewParam name="isbn" value="#{bookDetailBean.isbn}"/>
  <f:viewParam name="lang" value="#{langBean.lang}"/>
  <f:viewAction action="#{bookDetailBean.load}"/>
</f:metadata>
<h:head><title>Links</title></h:head>
<h:body>
  <h:link id="self" value="Spanish" outcome="detail-links" includeViewParams="true">
    <f:param name="lang" value="es"/>
  </h:link>
  <h:link id="self2" value="Same" outcome="detail-links" includeViewParams="true"/>
  <h:link id="other" value="Detail" outcome="detail" includeViewParams="true"/>
  <h:link id="plain" value="Plain" outcome="detail-links"/>
  <h:link id="nested" value="Nested" outcome="detail-links?x=1" includeViewParams="true"/>
  <h1>#{bookDetailBean.book.title}</h1>
</h:body>
</html>
LangBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;

@Named
@RequestScoped
public class LangBean {

    private String lang;

    public String getLang() {
        return lang;
    }

    public void setLang(String lang) {
        this.lang = lang;
    }
}

For detail-links.xhtml?isbn=9780441013593&lang=en Mojarra rendered:

Link href

self

/bookshelf/detail-links.xhtml?lang=es&isbn=9780441013593 (the explicit f:param for lang wins over the model value)

self2

/bookshelf/detail-links.xhtml?isbn=9780441013593&lang=en

other

/bookshelf/detail.xhtml?isbn=9780441013593 (only the view parameters of the target view are included)

plain

/bookshelf/detail-links.xhtml (no includeViewParams: nothing is carried over)

nested

/bookshelf/detail-links.xhtml?x=1&isbn=9780441013593&lang=en (a query string in the outcome is merged with the rest)

MyFaces produced the same parameters, with a different parameter order in some links. The rules from the specification: explicit f:param values win; a view parameter with a value expression contributes the value of that expression, converted back to a string with the converter’s getAsString, which is why the converter shown earlier has a real getAsString; a view parameter without a value expression contributes its local value (its own, or that of the same-named view parameter of the current view). Two related warnings:

  • A link is GET. It must never change state. A "delete" or "borrow" that is a h:link can be fired by a crawler, a prefetching browser or a mail scanner. Use a command button inside a form (which carries the view state and, with it, CSRF protection) for anything that changes data; the security page covers that.

  • The address is built at render time. A link that depends on a bean property is only correct if that property has its final value when Render Response starts, so compute the value in the view action, not in a later event.

The rules for outcomes themselves (implicit navigation, navigation cases, faces-redirect) are the subject of the navigation page; its GET navigation section covers h:link and h:button from the navigation side.

POST-Redirect-GET

A form that changes data should end with a redirect to a GET address, not render a page directly. If the POST renders the page, the browser’s address bar shows the form’s URL, reloading the page asks the user whether to resubmit the form (and, if the user agrees, saves the review twice), and the bookmarkable address of the result is lost. The standard fix is POST-Redirect-GET (PRG): the POST does its work, answers 302 with the address of the result, and the browser follows it with a GET. View parameters and view actions are what makes the GET half work.

sequenceDiagram participant B as Browser participant S as Server B->>S: GET /review.xhtml?isbn=9780441013593 S-->>B: 200 form (view state) B->>S: POST /review.xhtml (text, view state) Note over S: Invoke Application: ReviewBean.save() stores the review and returns detail?faces-redirect=true&isbn=9780441013593 S-->>B: 302 Location /bookshelf/detail.xhtml?isbn=9780441013593 B->>S: GET /detail.xhtml?isbn=9780441013593 Note over S: viewParam, viewAction load(), flash message shown once S-->>B: 200 detail page B->>S: reload (F5) repeats only the GET

The review form is a page of its own, addressed by the book it is about. It uses the portable view parameter described earlier (@ViewScoped bean, required only on the initial request):

review.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<f:metadata>
  <f:viewParam name="isbn" value="#{reviewBean.isbn}" required="#{not facesContext.postback}"
               requiredMessage="Which book?"/>
</f:metadata>
<h:head><title>Review</title></h:head>
<h:body>
  <h:messages/>
  <h:form id="f">
    <h:inputTextarea id="text" value="#{reviewBean.text}"/>
    <h:commandButton id="go" value="Save" action="#{reviewBean.save}"/>
  </h:form>
</h:body>
</html>
ReviewBean.java
package org.example.bookshelf.web;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

@Named
@ViewScoped
public class ReviewBean implements Serializable {

    @Inject
    private FacesContext facesContext;

    private String isbn;
    private String text;

    public String save() {
        // persist the review here
        facesContext.getExternalContext().getFlash().setKeepMessages(true);
        facesContext.addMessage(null, new FacesMessage("Thanks, your review was saved."));
        return "detail?faces-redirect=true&isbn=" + URLEncoder.encode(isbn, StandardCharsets.UTF_8);
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }
}

Submitting the form answered 302 Location: /bookshelf/detail.xhtml?isbn=9780441013593 on both implementations. The following GET (with the same session cookie) showed the h:messages text "Thanks, your review was saved." above the book, and a second GET did not: the flash keeps the message for exactly one redirect. setKeepMessages(true) is the call that makes this work; without it the message is lost with the POST request. The URLEncoder.encode of the ISBN keeps an unexpected character from breaking the outcome.

What the redirect buys is visible when it is omitted. A variant whose action returned "detail?isbn=" + isbn (no faces-redirect) answered the POST with status 200 and rendered the detail view inside the POST response, but the title and author were empty: the view action and the view parameter of the target view do not run for a navigation inside a postback, so the book was never loaded. Do not forward to a view that depends on view parameters; redirect. The flash and the other PRG details (how long messages survive, flash scope beans) are the subject of the flash and PRG page.

Never build a redirect target from request data without checking it. A return value or a externalContext.redirect(url) whose URL comes from a parameter (?next=...) is an open redirect: restrict it to your own view ids, as the example does by fixing the path and encoding the single parameter.

Missing Entities: Sending a Real 404

A page for ?isbn=9780000000000 that renders an empty template with status 200 is a "soft 404": crawlers index it, monitoring sees healthy requests and users see a broken page. The view action is the right place to detect it, because it runs after the parameters are in the model and before anything is rendered. ExternalContext has two methods for it:

  • responseSendError(int, String) calls HttpServletResponse.sendError, so the container selects the error-page of web.xml (or its own error page). The Javadoc does not say that it ends the lifecycle, unlike redirect(String), which does call responseComplete().

  • setResponseStatus(int) only sets the status code; the view is then rendered normally.

BookDetailBean.load() above uses the first and calls facesContext.responseComplete() immediately after it. The call is not optional:

Variant Mojarra 4.1.17 MyFaces 4.1.4

responseSendError + responseComplete()

404, error page

404, error page

responseSendError without responseComplete()

404, error page; the log shows a warning "Skipping attempt to set buffer size on a committed response"

500 for a request without a session cookie (IllegalStateException "Cannot create a session after the response has been committed" in Render Response); 404 when the request already had a session

So the lifecycle must be told that the response is finished. A real-world detail page with a database lookup does exactly this once.

The second option renders the Faces view with status 404 and so lets the page explain what happened and offer a way forward (a search box, a link to the catalogue) with the full template:

detail4.xhtml (excerpt of the body)
<h:body>
  <p id="gone"><h:outputText value="Sorry, that book does not exist." rendered="#{not experimentBean.found}"/></p>
  <h1 id="title">#{experimentBean.book.title}</h1>
</h:body>
ExperimentBean.java (excerpt)
public void loadStatusOnly() {
    book = books.findByIsbn(isbn);
    if (book == null) {
        externalContext.setResponseStatus(404);
    }
}

public boolean isFound() {
    return book != null;
}

That variant answered status 404 with the page’s own content on both implementations. Choose by who reads the page: the container error page is simplest and consistent across the application; a status plus your own content is friendlier but must be written for each page. Either way the status is what a crawler and a monitoring system read.

Three cautions:

  • Do not put request data into the error message (responseSendError(404, "No book with ISBN " + isbn)). Error pages of some servers show the message, and the value is attacker-controlled. The detail page uses fixed text.

  • A 404 for "exists but you may not see it" tells an attacker the resource exists; answer 404 in both cases when that matters, and do the authorisation in a security constraint or Jakarta Security annotation rather than only in a view action. A view action is application code that runs after the Faces servlet was reached; the security page covers authorisation.

  • The ISBN is user input. Show it with h:outputText or #{...} (both escape), never with escape="false".

Malformed Addresses: a Real 400

A parameter that fails conversion or validation is a client error, but because the view action does not run for it, the lifecycle falls through to Render Response and the page is rendered with status 200 and, at best, a message. To answer with the right status, check FacesContext.isValidationFailed() where code still runs: a preRenderView listener, which belongs to Render Response and so runs even when phases 4 and 5 were skipped. BookDetailBean.rejectInvalid() above does it, and the table in A Complete Example: the Book Detail Page shows the result: 400 for a missing, empty or malformed ISBN on both implementations.

isValidationFailed() is true after any conversion or validation failure in the request, which on a page with only metadata means a view parameter. If the page also has a form, restrict the listener to the initial request with !facesContext.isPostback() or give it an if-like guard in code, otherwise a failed form validation would turn into a 400.

For pages where the user can fix the problem (a search page with a typed-in filter), do not send an error: render the page with the messages, and make sure the messages are readable (Messages). The decision is between "this address is wrong" (4xx) and "the input is wrong" (a form message).

Redirecting Old Addresses

The old-book.xhtml example in Navigation From a View Action is the whole technique: a view with a f:viewParam for the old parameter and a view action that returns the new address as an outcome, answered with a redirect. Typical uses are renamed pages, merged pages and addresses that carried an internal id before the application switched to ISBNs. The redirect is a temporary one (302 in the run); a permanent redirect (301) is a web server or filter concern. Remember the MyFaces session detail described in that section, and give the old view a trivial body: the user never sees it.

Alternatives and Complements

Read the parameter without view parameters. A CDI bean can inject the request parameter with @ManagedProperty (the Faces annotation, with @Inject) or the whole map with @RequestParameterMap:

ParamBean.java
package org.example.bookshelf.web;

import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.annotation.ManagedProperty;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;

@Named
@RequestScoped
public class ParamBean {

    @Inject
    @ManagedProperty("#{param.isbn}")
    private String isbn;

    @Inject
    private BookService books;

    private Book book;

    @PostConstruct
    void init() {
        book = books.findByIsbn(isbn);
    }

    public String getIsbn() {
        return isbn;
    }

    public Book getBook() {
        return book;
    }
}

mp.xhtml?isbn=9780441013593, a page with no metadata at all, printed isbn=9780441013593 title=Dune on both implementations, and @PostConstruct saw the parameter, unlike with f:viewParam. The price is that there is no conversion, no validation, no messages, no includeViewParams and no page metadata, so Faces does not run the lifecycle for the request (it goes straight to Render Response); use it for simple read-only pages, and keep f:viewParam where you need the features. Sending a 404 from @PostConstruct is awkward, because the bean is created in the middle of expression evaluation.

OmniFaces. The OmniFaces library (see the OmniFaces page) has several tools in the same area. From its documentation (OmniFaces 5.5.5; the following was not run for this page):

  • @Param is a CDI qualifier that injects, converts and validates a request parameter directly in a bean, for example @Inject @Param(name = "isbn") private String isbn;. Unlike f:viewParam the value is already available in @PostConstruct, so no view action or preRenderView listener is needed; it also supports multi-valued parameters, a converter and validator attribute, and path parameters (pathIndex).

  • o:viewParam extends f:viewParam with a stateless mode (it does not convert, validate or update the model again on postbacks) and fixes the "null becomes empty string in the query string" and "validation never triggered when the parameter is absent" problems of the standard tag, which addresses the postback behaviour discussed above. Its documentation also describes a design flaw of the standard tag: with required="true" and an absent parameter, the PreValidateEvent and PostValidateEvent listeners are never invoked. I did not verify that claim.

  • o:viewAction evaluates if in the Invoke Application phase instead of Apply Request Values, so if can read converted values, and it adds a message attribute that shows a flash message after the redirect.

  • o:viewParamValidationFailed answers a failed view parameter validation with a redirect or an HTTP error status (sendError), which replaces the preRenderView listener of Malformed Addresses: a Real 400.

Documented OmniFaces usage (not run; the o prefix must be bound to the OmniFaces namespace of your version)
<f:metadata>
  <o:viewParam name="isbn" value="#{bookDetailBean.isbn}" required="true"/>
  <o:viewParamValidationFailed sendError="400"/>
  <o:viewAction action="#{bookDetailBean.load}"/>
</f:metadata>

The snippet combines the sendError example of the o:viewParamValidationFailed showcase page with the tags above; the tag also has sendRedirect and message attributes in the OmniFaces VDL. Declare the o prefix as in the OmniFaces documentation for your version.

Path-style addresses. f:viewParam reads request parameters only: /book/9780441013593 carries no parameter. Such addresses are usually produced with a URL-rewriting filter or a server rewrite rule in front of the FacesServlet; the extensionless mapping covered by the project setup page only removes the .xhtml suffix. OmniFaces @Param(pathIndex = ...) and FacesViews read path segments (its documentation); this page only used query parameters.

Mojarra and MyFaces

The examples of this page were run on both implementations in the same scratch application, except where the text names one. Differences found:

Behaviour Mojarra 4.1.17 MyFaces 4.1.4

View parameter on a postback without the parameter (View Parameters on a Postback)

stored value re-applied: validator and setter run

not re-applied; required="true" fails

responseSendError without responseComplete() (Missing Entities: Sending a Real 404)

404 plus a warning in the log

500 for a request without session

Redirect from a view action on a request without a session (Navigation From a View Action)

302

500 (flash saves messages after the response was committed); fine with a session

onPostback="true" view action on an Ajax request (onPostback)

ran

did not run (default execute)

View parameter without value and with a converter producing a non-String (A View Parameter Without a Value Expression)

worked

ClassCastException while rendering

Default messages of a view parameter

prefixed with j_idt<n>

prefixed with j_id__md_<n>

h:link with an unresolved outcome

Any stage except Production: text plus explanation, JSF1090 log

the text only

Order of parameters in includeViewParams links

isbn before lang in some links

lang before isbn in some links

HTTP statuses, isValidationFailed(), phases of the view actions, if, redirect for a different view id

same

same

These are the findings of one run on one machine with one scratch application; the causes of the MyFaces differences were not investigated and some may be bugs rather than design. If portability matters, use a @ViewScoped bean, relax required on postbacks, always call responseComplete(), and create the session before redirecting from a view action.

What Changed

Version Change

Faces 2.0

f:viewParam and UIViewParameter, f:metadata and ViewMetadata, and the bookmarkable components h:link and h:button (UIOutcomeTarget) with includeViewParams. The Javadoc of the four classes says Since: 2.0. ExternalContext.responseSendError and setResponseStatus are also Since: 2.0.

Faces 2.2

f:viewAction and UIViewAction (Since: 2.2): the specification’s change log says the class "came over from the JBoss Seam Faces Module". The same change list adds PhaseId.getName() and PhaseId.phaseIdValueOf(String) (used by setPhase) and a change of the default NavigationHandler algorithm "to account for UIViewAction".

Faces 2.3

The change list names two related items: "@NotNull and f:viewParam" and "Allow entry into flow via f:viewAction". Injection of FacesContext and ExternalContext with @Inject (used in the examples) is also new in 2.3. What exactly the first item changed is not described in the part of the specification I read.

Faces 3.0 (Jakarta EE 9)

The packages become jakarta.faces.*; the tags are unchanged.

Faces 4.0 (Jakarta EE 10)

The tag libraries use the jakarta.faces.core and jakarta.faces.html namespace URNs, as in the examples. No change specific to view parameters or view actions.

Faces 4.1 (Jakarta EE 11)

No entry in the change list concerns them. The VDL pages for the tags are the 4.1.0 documentation.

Faces 5.0 is under development (milestone 5.0.0-M9 at the time of writing) and is not described here.

Where the Books Are Outdated

The books of the section’s bibliography that discuss view parameters and bookmarkable URLs (the JSF 2.0 books by Burns and Schalk and by Leonard) were written before Faces 2.2:

  • No f:viewAction. In 2.0 and 2.1 the only hook after the view parameters was a preRenderView listener or a managed bean constructor, which is why older recipes load data in preRenderView and have to guard against postbacks themselves. Today f:viewAction is the tool for the initial load (View Action, preRenderView or @PostConstruct?).

  • Legacy beans and namespaces. The recipes use @ManagedBean @RequestScoped beans, javax.faces imports and xmlns:f="http://java.sun.com/jsf/core". The current names are CDI @Named beans, jakarta.faces.* and the jakarta.faces.core namespace URN.

  • Pretty URLs through add-ons. Path-style addresses were provided by libraries such as PrettyFaces, which is no longer maintained; use a rewrite rule or filter, OmniFaces FacesViews, or the extensionless mapping of Faces 4.0, as described above.

  • Errors as 200. Older examples rarely discuss HTTP status codes; a missing entity usually renders an empty page. Current practice, and search engine behaviour, expects the status to be right.

  • Redirects built by hand. Early material calls ExternalContext.redirect(String) with a concatenated URL, where the outcome with faces-redirect=true and the view action navigation shown here is shorter and consistent with the rest of navigation.

These points rest on the dates and scope of the books in the concept table of the section’s source specification; I did not re-read the books for this page.

Common Mistakes

  • Putting f:metadata in the template. The parameters are silently never read. Put it in the page, or in a dedicated ui:define slot that the template inserts directly inside its f:view (see the core tags page). Do not put it in the content slot: MyFaces 4.1.4 rejects that with Parent UIComponent ... should be instance of UIViewRoot although Mojarra accepts it.

  • Expecting the view action to run for an invalid parameter. It does not; send the 400 from a preRenderView listener or validate in the action.

  • Loading data in @PostConstruct or in a getter. The bean exists before the parameters; a getter runs several times per request. Use f:viewAction.

  • A converter that throws for "not found". Status 200, a message and no action; return null or look up in the action and send the 404 yourself.

  • Forgetting responseComplete() after responseSendError. A 500 on MyFaces, a warning on Mojarra.

  • Using required="true" on a page with a form and a request-scoped bean. It works on Mojarra and fails on MyFaces. Use a @ViewScoped bean and required="#{not facesContext.postback}".

  • Relying on if to read the model. It is evaluated before the model is updated; use param or decide in the method.

  • onPostback="true" on an action that is not idempotent. It runs on every postback, and on an Ajax request in some implementations.

  • Forwarding to a parameterised view from a POST. The target’s parameters and view action do not run; redirect.

  • State-changing links. h:link, h:button and a view action are GET: a crawler can trigger them. Changes belong in a POST with view state.

  • Trusting the parameter. Convert and validate it, do not echo it unescaped, do not build a redirect target from it, and check that the current user may see the entity it identifies (a missing authorisation check on ?id= is the classic insecure direct object reference).

  • Stateless pages with a form. A transient view has no stored view parameter value; repeat the parameter in the form or avoid required on the postback.