-
Available since OmniFaces 1.6
The CDI view scope annotation, with more optimal handling of bean destroy as compared to standard Faces one.
In standard Faces, the @
PreDestroy
annotated method on a view scoped bean is only invoked when the session expires. However, there may be cases when it's desirable to immediately destroy a view scoped bean as well when the browser unload
event is invoked. I.e. when the user navigates away by GET, or closes the browser tab/window. The standard Faces view scope does not support this. Since OmniFaces 2.2, this CDI view scope annotation will guarantee that the @PreDestroy
annotated method is also invoked on browser unload. This trick is done by navigator.sendBeacon
. For browsers not supporting navigator.sendBeacon
, it will fallback to a synchronous XHR request.
Since OmniFaces 2.3, the unload has been further improved to also physically remove the associated Faces view state from Faces implementation's internal LRU map in case of server side state saving, hereby further decreasing the risk at ViewExpiredException
on the other views which were created/opened earlier. As side effect of this change, the @PreDestroy
annotated method of any standard Faces view scoped beans referenced in the same view as the OmniFaces CDI view scoped bean will also guaranteed be invoked on browser unload.
Since OmniFaces 2.6, this annotation got a new attribute: saveInViewState
. When using client side state saving, this attribute can be set to true
in order to force Faces to store whole view scoped bean instances annotated with this annotation in the Faces view state instead of in the HTTP session. For more detail, see the saveInViewState()
.
In a nutshell: if you want the @PreDestroy
to be invoked on browser unload too, then use OmniFaces 2.2+ with this view scope annotation. Or, if you want to store whole view scoped beans in the Faces view state when using client side state saving, then use OmniFaces 2.6+ with this view scope annotation and the saveInViewState
attribute set to true
.
Related Faces issues:
Usage
Just use it the usual way as all other CDI scopes. Watch out with IDE autocomplete on import that you don't accidentally import standard Faces own one.
import jakarta.inject.Named;
import org.omnifaces.cdi.ViewScoped;
@Named
@ViewScoped
public class OmniCDIViewScopedBean implements Serializable {}
Please note that the bean must implement Serializable
, otherwise the CDI implementation will throw an exception about the bean not being passivation capable.
Under the covers, CDI managed beans with this scope are via ViewScopeManager
by default stored in the session scope by an UUID
based key which is referenced in Faces own view map as available by UIViewRoot.getViewMap()
. They are not stored in the Faces view state itself as that would be rather expensive in case of client side state saving.
In case you are using client side state saving by having the jakarta.faces.STATE_SAVING_METHOD
context parameter set to true
along with a valid jsf/ClientSideSecretKey
in web.xml
as below,
<context-param>
<param-name>jakarta.faces.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
<env-entry>
<env-entry-name>jsf/ClientSideSecretKey</env-entry-name>
<env-entry-type>java.lang.String</env-entry-type>
<env-entry-value><!-- See https://stackoverflow.com/q/35102645/157882 --></env-entry-value>
</env-entry>
And you explicitly want to store the whole view scoped bean instance in the Faces view state, then set the annotation's saveInViewState
attribute to true
.
import jakarta.inject.Named;
import org.omnifaces.cdi.ViewScoped;
@Named
@ViewScoped(saveInViewState=true)
public class OmniCDIViewScopedBean implements Serializable {}
It's very important that you understand that this setting has potentially a major impact in the size of the Faces view state, certainly when the view scoped bean instance holds "too much" data, such as a collection of entities for a data table, and that such beans will in fact never expire as they are stored entirely in the jakarta.faces.ViewState
hidden input field in the HTML page. Moreover, the @
PreDestroy
annotated method on such bean will explicitly never be invoked, even not on an unload as it's quite possible to save or cache the page source and re-execute it at a (much) later moment.
It's therefore strongly recommended to use this setting only on a view scoped bean instance which is exclusively used to keep track of the dynamically controlled form state, such as disabled
, readonly
and rendered
attributes which are controlled by ajax events.
This setting is NOT recommended when using server side state saving. It has basically no effect and it only adds unnecessary serialization overhead. The system will therefore throw an IllegalStateException
on such condition.
Configuration
By default, the maximum number of active view scopes is hold in a LRU map in HTTP session with a default size equal to the first non-null value of the following context parameters:
- "org.omnifaces.VIEW_SCOPE_MANAGER_MAX_ACTIVE_VIEW_SCOPES" (OmniFaces)
- "com.sun.faces.numberOfLogicalViews" (Mojarra-specific)
- "org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION" (MyFaces-specific)
If none of those context parameters are present, then a default size of 20 will be used. When a view scoped bean is evicted from the LRU map, then its @PreDestroy
will also guaranteed to be invoked.
This setting has no effect when saveInViewState
attribute is set to true
.
Using window.onbeforeunload
If you have a custom onbeforeunload
handler, then it's strongly recommended to use plain vanilla JS window.onbeforeunload = function
instead of e.g. jQuery $(window).on("beforeunload", function)
or DOM window.addEventListener("beforeunload", function)
for this. This way the @ViewScoped
unload can detect it and take it into account and continue to work properly. Otherwise the view scoped bean will still be destroyed in background even when the user cancels and decides to stay in the same page.
Below is a kickoff example how to properly register it, assuming jQuery is available, and that "stateless" forms and inputs (for which you don't want to trigger the unsaved data warning) have the class stateless
set:
$(document).on("change", "form:not(.stateless) :input:not(.stateless)", function() {
$("body").data("unsavedchanges", true);
});
OmniFaces.Util.addSubmitListener(function() { // This hooks on Mojarra/MyFaces/PrimeFaces ajax submit events too.
$("body").data("unsavedchanges", false);
});
window.onbeforeunload = function() {
return $("body").data("unsavedchanges") ? "You have unsaved data. Are you sure you wish to leave this page?" : null;
};
Using download links
If you have a synchronous download link as in <a href="/path/to/file.ext">
, then the unload will also be triggered. For HTML5-capable browsers it's sufficient to add the download
attribute representing the file name you'd like to use in the client specific "Save As" dialogue.
<a href="/path/to/file.ext" download="file.ext">download</a>
When this attribute is present, then the browser won't anymore trigger the unload event. In case your target browser does not support it, then you'd need to explicitly disable the OmniFaces unload event as follows:
<a href="/path/to/file.ext" onclick="OmniFaces.Unload.disable();">download</a>
An alternative is to explicitly open the download in a new tab/window. Decent browsers, even these not supporting the download
attribute, will usually automatically close the newly opened tab/window when a response with Content-Disposition: attachment
is received.
<a href="/path/to/file.ext" target="_blank">download</a>
Detecting unload requests
When the unload request has hit your servlet filter or authentication mechanism or whatever global listener/observer, and you would like to be able to detect them, so that you can exclude them from the logic, then you can use ViewScopeManager.isUnloadRequest(jakarta.servlet.http.HttpServletRequest)
or ViewScopeManager.isUnloadRequest(FacesContext)
, depending on whether the FacesContext
is available in the current context. You should always ensure that the flow just continues for them, else the unload requests won't be able to do their work of explicitly destroying the bean and state.
Here is an example assuming that you're in a servlet filter:
if (!ViewScopeManager.isUnloadRequest(request)) {
// Do actual job here.
}
chain.doFilter(request, response); // Ensure that this just continues!
CDI issues in EAR
Note that CDI has known issues when the same web fragment library is bundled in multiple WARs in a single EAR and the CDI feature is based on an Extension
.
It's important to understand that those issues are not related to OmniFaces, but to the CDI spec.
For an overview of those issues, please refer Known issues of OmniFaces CDI features in combination with specific application servers.
Needless to say is that EAR is a dead end since introduction of the cloud and has no value in the microservices world.
In other words, if you want to use CDI (or microservices), then you shouldn't be using an EAR in first place.
CDI view scoped bean
Faces view scoped bean
<h3>CDI view scoped bean</h3>
<h:form id="cdiViewScopedForm">
<p>Status:</p>
<ul>
<li>It's now: #{now}</li>
<li>Session ID: #{session.id}</li>
<li>CDI view scoped bean: #{cdiViewScopedBean}</li>
</ul>
<p>
<h:commandButton value="submit form without ajax" action="#{cdiViewScopedBean.submit}" />
<f:ajax execute="@form" render="@form :facesViewScopedForm">
<h:commandButton value="submit form with ajax" action="#{cdiViewScopedBean.submit}" />
<h:commandButton value="rebuild view" action="#{cdiViewScopedBean.rebuildView}" />
<h:commandButton value="navigate on POST" action="#{cdiViewScopedBean.navigate}" />
</f:ajax>
<h:button value="refresh page" />
</p>
<p>Messages from CDI view scoped bean:</p>
<h:messages for="cdiViewScopedForm" />
</h:form>
<hr />
<h3>Faces view scoped bean</h3>
<h:form id="facesViewScopedForm">
<p>Status:</p>
<ul>
<li>It's now: #{now}</li>
<li>Session ID: #{session.id}</li>
<li>Faces view scoped bean: #{facesViewScopedBean}</li>
</ul>
<p>
<h:commandButton value="submit form without ajax" action="#{facesViewScopedBean.submit}" />
<f:ajax execute="@form" render="@form :cdiViewScopedForm">
<h:commandButton value="submit form with ajax" action="#{facesViewScopedBean.submit}" />
<h:commandButton value="rebuild view" action="#{facesViewScopedBean.rebuildView}" />
<h:commandButton value="navigate on POST" action="#{facesViewScopedBean.navigate}" />
</f:ajax>
<h:button value="refresh page" />
</p>
<p>Messages from Faces view scoped bean:</p>
<h:messages for="facesViewScopedForm" />
</h:form>
package org.omnifaces.showcase.cdi;
import java.io.Serializable;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.faces.application.FacesMessage;
import jakarta.inject.Named;
import org.omnifaces.cdi.ViewScoped;
import org.omnifaces.cdi.viewscope.ViewScopeManager;
import org.omnifaces.util.Faces;
import org.omnifaces.util.Messages;
@Named
@ViewScoped
public class CdiViewScopedBean implements Serializable {
private static final long serialVersionUID = 1L;
@PostConstruct
public void postConstruct() {
FacesMessage unloadMessage = Faces.removeSessionAttribute("unloadMessage");
if (unloadMessage != null) {
Messages.add("cdiViewScopedForm", unloadMessage);
}
Messages.addInfo("cdiViewScopedForm", "PostConstruct invoked: {0}", this);
}
public void submit() {
Messages.addInfo("cdiViewScopedForm", "Submit invoked: {0}", this);
}
public String navigate() {
Messages.addInfo("cdiViewScopedForm", "Navigate on POST invoked: {0}", this);
return Faces.getViewId();
}
public void rebuildView() {
Messages.addInfo("cdiViewScopedForm", "Rebuild view invoked: {0}", this);
Faces.setViewRoot(Faces.getViewId());
}
@PreDestroy
public void preDestroy() {
if (Faces.getContext() != null) { // It can be null during session invalidate!
if (ViewScopeManager.isUnloadRequest(Faces.getContext())) {
Faces.setSessionAttribute("unloadMessage", Messages.createInfo("PreDestroy invoked during unload: {0}", this));
}
else {
Messages.addInfo("cdiViewScopedForm", "PreDestroy invoked during postback: {0}", this);
}
}
}
}
package org.omnifaces.showcase.cdi;
import java.io.Serializable;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import org.omnifaces.util.Faces;
import org.omnifaces.util.Messages;
@Named
@ViewScoped
public class FacesViewScopedBean implements Serializable {
private static final long serialVersionUID = 1L;
@PostConstruct
public void postConstruct() {
Messages.addInfo("facesViewScopedForm", "PostConstruct invoked: {0}", this);
}
public void submit() {
Messages.addInfo("facesViewScopedForm", "Submit invoked: {0}", this);
}
public String navigate() {
Messages.addInfo("facesViewScopedForm", "Navigate on POST invoked: {0}", this);
return Faces.getViewId();
}
public void rebuildView() {
Messages.addInfo("facesViewScopedForm", "Rebuild view invoked: {0}", this);
Faces.setViewRoot(Faces.getViewId());
}
@PreDestroy
public void preDestroy() {
if (Faces.getContext() != null) { // It can be null during session invalidate!
Messages.addInfo("facesViewScopedForm", "PreDestroy invoked: {0}", this);
}
}
}
API documentation
Java source code
org.omnifaces.cdi.viewscope.ViewScopeStorageInSession
org.omnifaces.cdi.viewscope.ViewScopeManager
org.omnifaces.cdi.viewscope.ViewScopeExtension
org.omnifaces.cdi.BeanStorage
org.omnifaces.viewhandler.OmniViewHandler
org.omnifaces.cdi.viewscope.ViewScopeContext
org.omnifaces.cdi.viewscope.ViewScopeStorageInViewState
org.omnifaces.util.cache.LruCache
org.omnifaces.context.OmniExternalContextFactory
org.omnifaces.context.OmniExternalContext
org.omnifaces.cdi.ViewScoped
org.omnifaces.cdi.viewscope.ViewScopeStorage
org.omnifaces.cdi.viewscope.ViewScopeEventListener