I have a JSF page with some common elements and then 4 parts that are dynamically loaded and included based on various user actions. I have beans which encapsulate the functionality of the various includes, and the structure is something like this:
mainview.xhtml
--backed by--> mainViewBean (request scope)
--with managed property--> mainViewView (view scope)
include1.xhtml
--backed by--> include1Bean (request scope)
--with managed property--> mainViewBean
include2.xhtml
--backed by--> include2Bean (request scope)
--with managed property--> mainViewBean
include3.xhtml
--backed by (and with component bindings to)--> include3Bean (request scope)
--with managed property--> mainViewBean
include4.xhtml
--backed by--> mainViewBean
On the main view page, each include is included via a series of conditionally rendered h:panelGroups as described by BalusC in these questions:
https://stackoverflow.com/a/9897016/945403
https://stackoverflow.com/a/7113961/945403
The view scoped bean contains some various view information concerning the current state of the user's activities, what items they are currently viewing and so on.
Each of the included views perform various tasks via both ajax and non ajax posts. My problem is that certain actions in certain panels seem to cause the view scope to be destroyed, and there does not seem to be a rhyme or reason to it. I apologize in advance for the convoluted work flow outlined below, but I'll try to be as clear as I can.
If I perform the ajax actions in include3.xhtml, they render components only within the include and I can do them all day long and the view scope remains. If I perform the non ajax posts, the refresh will update (and potentially add a previously non-rendered) include4.xhtml. I can then subsequently perform an action in any of the other includes.
If I perform an ajax action in include1.xhtml which only calls methods in its own backing bean, and only updates its own components, the view scope remains. If I perform an ajax action which calls a method in include3Bean and updates the div containing include2.xhtml and include3.xhtml, the view scope remains and as long as I continue to perform actions in include1.xhtml, the view scope will remain. As soon as I try to perform another action in one of the other includes, the view scope is destroyed.
As this point, I thought to myself that the problem must be in that I'm updating different includes than the action is being called from. But this does not appear to be the problem (or at least the only problem) as I will now explain.
include2.xhtml has an ajax action which when fired, calls a method in mainViewBacking and updates the div containing include2.xhtml and include3.xhtml. If I continue to perform this action, or take any other action in include2.xhtml (including non ajax actions) everything works as expected and the view scope remains. However, if I subsequently perform an action in include3.xhtml, the view scope is destroyed. Where this gets weird is that I can perform actions in include1.xhtml and they will continue to have access to the view scope for as long as I want, but if I try to go back and perform an action in include2.xhtml or include3.xhtml, the view scope is lost again.
I'm a bit stuck at this point, and I'm not even sure how to go about figuring out what's wrong. I'm assuming something about be refreshing (and removing or adding) the includes is causing the view scope to be lost, but from the previously linked questions, it appears this shouldn't be a problem, as the actual src values for the ui:includes aren't being dynamically generated. And I do have partial state saving for mainview.xhtml turned off.
Is there something about these dynamically displaying ui:includes that breaks view scope?
While looking at the latest omnifaces showcase, I noticed that BalusC has implemented a script which should solve this same issue, and probably do it in a much nicer way than my code below.
http://showcase.omnifaces.org/scripts/FixViewState
It turns out this was a bug in JSF as described by BalusC here: http://balusc.blogspot.com/2011/09/communication-in-jsf-20.html#AjaxRenderingOfContentWhichContainsAnotherForm
Unfortunately, his solution did not work consistently for me for some reason. As a result, I wrote my own (slightly more annoying) work around, which I'll provide here for others if they need it.
I added the following javascript to the mainview.xhtml page:
var lastForm = null;
function getViewStateFromLastForm(callingElement){
var currentForm = $(callingElement).closest("form");
var formId = currentForm.attr("id");
if(lastForm != null ){
var viewState = $("#" + lastForm).children("[name='javax.faces.ViewState']").val();
currentForm.children("[name='javax.faces.ViewState']").remove();
$('<input/>').attr({
type: 'hidden',
id: 'javax.faces.ViewState',
name: 'javax.faces.ViewState',
autocomplete: 'off',
value: viewState
}).appendTo(currentForm);
}
lastForm = formId;
}
Then every button on the various included pages which submit to the server have a call getViewStateFromLastForm(this);
in their onclick attributes.