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:viewParambinds a request parameter to a model property, with conversion and validation, so that the address carries the state of the page. -
f:viewActionruns 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.
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 earlyphase) seesnullin 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.
<!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>
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:
<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>
<!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 |
|---|---|---|
|
200 |
Parameter valid, view action found the book |
|
404 |
Valid shape, but |
|
400 |
|
|
400 |
|
|
400 |
An empty value is treated as missing by |
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 |
|---|---|---|
|
yes |
The request parameter to read. Only the request parameter map is consulted: path segments are not |
|
no |
A value expression the converted value is written to. Without it, see A View Parameter Without a Value Expression |
|
no |
The parameter must be present and not empty; custom text for the failure |
|
no |
A converter id or instance; custom text for a conversion failure |
|
no |
A validator method; custom text that replaces every validator message of this parameter |
|
no |
Called when the stored value changes |
|
no |
The component id and a binding to the component, as on every component |
|
no |
Listed in the VDL; |
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:
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();
}
}
<!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>
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;
}
}
Source: Jakarta Faces 4.1 API: UIViewParameter and Jakarta EE Tutorial: Creating and Using a Custom Converter
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. Returnnulland decide in the action, or keep the parameter aStringand look the entity up in the action, as the first example does. -
With
required="true"and a converter that returnsnull, the required check fails (it looks at the converted value), so an unknown ISBN produced therequiredMessageand 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 |
|---|---|
|
validator called in Process Validations, |
|
the validator method was not called, and |
no |
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 showedvalidator @PROCESS_VALIDATIONS value=okandsetIsbn(ok) @UPDATE_MODEL_VALUESon 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
isbnstayednull. Worse, arequired="true"view parameter failed with "Value is required" on the postback, even when a@ViewScopedbean still held the value. I did not investigate why.
The portable rules are therefore:
-
Hold the value in a
@ViewScopedbean (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. -
Make
requiredapply only to the initial request:required="#{not facesContext.postback}".requiredaccepts a value expression that is evaluated for each request. With that, the@ViewScopedpage below worked on both implementations; with plainrequired="true"it failed on MyFaces.
<!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>
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=othermade the validator and the setter run withotheron both implementations (the VDL describesnameas 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 literalstateless. Arequiredview parameter then failed on the postback with therequiredMessageon 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:
<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 |
|---|---|---|
|
yes |
A method expression |
|
no |
A method that takes an |
|
no |
|
|
no |
|
|
no |
|
|
no |
Like |
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.
<!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>
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 |
|---|---|---|
|
Apply Request Values |
|
|
Apply Request Values |
|
|
Process Validations |
|
|
Update Model Values |
|
default ( |
Invoke Application |
|
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.
Source: Jakarta Faces 4.1 VDL: f:viewAction and API: UIViewAction
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].)
Source: Jakarta Faces 4.1 VDL: f:viewAction and API: UIViewAction
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.
Navigation From a View Action
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:
<!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>
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 |
Always |
Per bean instance |
Typical use |
Load the entity, redirect, send 404 |
Per-render work, check |
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:
<!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:
<!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>
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
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 ah:linkcan 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.
Source: Jakarta Faces 4.1 VDL: h:link, VDL: h:button and Jakarta Faces 4.1 specification, 2.5.6 Bookmarkability
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.
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):
<!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>
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)callsHttpServletResponse.sendError, so the container selects theerror-pageofweb.xml(or its own error page). The Javadoc does not say that it ends the lifecycle, unlikeredirect(String), which does callresponseComplete(). -
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 |
|---|---|---|
|
404, error page |
404, error page |
|
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 ( |
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:
<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>
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 Securityannotation 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:outputTextor#{...}(both escape), never withescape="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:
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):
-
@Paramis a CDI qualifier that injects, converts and validates a request parameter directly in a bean, for example@Inject @Param(name = "isbn") private String isbn;. Unlikef:viewParamthe value is already available in@PostConstruct, so no view action orpreRenderViewlistener is needed; it also supports multi-valued parameters, aconverterandvalidatorattribute, and path parameters (pathIndex). -
o:viewParamextendsf:viewParamwith 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: withrequired="true"and an absent parameter, thePreValidateEventandPostValidateEventlisteners are never invoked. I did not verify that claim. -
o:viewActionevaluatesifin the Invoke Application phase instead of Apply Request Values, soifcan read converted values, and it adds amessageattribute that shows a flash message after the redirect. -
o:viewParamValidationFailedanswers a failed view parameter validation with a redirect or an HTTP error status (sendError), which replaces thepreRenderViewlistener of Malformed Addresses: a Real 400.
<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; |
|
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 |
|
ran |
did not run (default |
View parameter without |
worked |
|
Default messages of a view parameter |
prefixed with |
prefixed with |
|
Any stage except Production: text plus explanation, |
the text only |
Order of parameters in |
|
|
HTTP statuses, |
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 |
|
Faces 2.2 |
|
Faces 2.3 |
The change list names two related items: "@NotNull and f:viewParam" and "Allow entry into flow via f:viewAction". Injection of |
Faces 3.0 (Jakarta EE 9) |
The packages become |
Faces 4.0 (Jakarta EE 10) |
The tag libraries use the |
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 apreRenderViewlistener or a managed bean constructor, which is why older recipes load data inpreRenderViewand have to guard against postbacks themselves. Todayf:viewActionis the tool for the initial load (View Action, preRenderView or @PostConstruct?). -
Legacy beans and namespaces. The recipes use
@ManagedBean @RequestScopedbeans,javax.facesimports andxmlns:f="http://java.sun.com/jsf/core". The current names are CDI@Namedbeans,jakarta.faces.*and thejakarta.faces.corenamespace 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 withfaces-redirect=trueand 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:metadatain the template. The parameters are silently never read. Put it in the page, or in a dedicatedui:defineslot that the template inserts directly inside itsf:view(see the core tags page). Do not put it in the content slot: MyFaces 4.1.4 rejects that withParent UIComponent ... should be instance of UIViewRootalthough Mojarra accepts it. -
Expecting the view action to run for an invalid parameter. It does not; send the 400 from a
preRenderViewlistener or validate in the action. -
Loading data in
@PostConstructor in a getter. The bean exists before the parameters; a getter runs several times per request. Usef:viewAction. -
A converter that throws for "not found". Status 200, a message and no action; return
nullor look up in the action and send the 404 yourself. -
Forgetting
responseComplete()afterresponseSendError. 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@ViewScopedbean andrequired="#{not facesContext.postback}". -
Relying on
ifto read the model. It is evaluated before the model is updated; useparamor 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:buttonand 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
requiredon the postback.