Friday, September 7, 2007

Using the a4j:region tag

The a4j:region tag is a JSF tag provided by the ajax4jsf framework, which is now owned by JBoss. The documentation for this tag is somewhat lacking, but I have figured out a usage of it that helps improve performance of JSF applications using Ajax4JSF and Richfaces.

The key realization came from studying the Ajax4JSF codebase. It is clear that in the code when an Ajax request is received, Ajax4JSF checks to see if the request is coming from a particular region, or from the entire page. If there is no region specified then it has to reprocess the entire page. If an Ajax request is generated from within a particular region, then only that region is reprocessed on the server.

Also - if you look at the Ajax request that is generated by the browser, you can see that the very first part of the request is specifying the region that the request is coming from

You will either see something like this:

AJAXREQUEST=_viewRoot...


Or you will see something like this:


AJAXREQUEST=fooRegion...


The request itself is specifying the region that is being updated so that the server can be smart about how to respond to that request. How can you take advantage of this in your Ajax4JSF applications?

A very simple example is a complicated page with lots of content - from which you want to pop up various dialogs and wizards. These wizards and dialogs have content that needs to be regenerated from the server - but you don't want to regenerate the whole page on the server because the operations for generating the page are expensive.

Lets say you have a page to manage users and groups. So your main page looks something like this (before applying regions to it):


<rich:datatable id="userTable" value="#{allUsers.list}">
...
</rich:datatable>

<rich:datatable id="groupTable" value="#{allGroups.list}">
...
</rich:datatable>

<rich:modalPanel id="addUserPanel">
<h:panelGroup id="addUserWizard">
<a4j:include id="addUser" viewId="addUserPanelOne.xhtml" />
</h:panelGroup>
</rich:modalPanel>

If your wizard uses any a4j:commandButtons for next, back, or finish - you will notice that the the "getList" methods for each of the tables is called each time you click one of those buttons. Even though the buttons may only be reRendering the wizard panel itself, and the wizard panel is the only panel being sent back to the client, on the server the whole page is being processed.

The way to prevent the tables from being reprocessed on the server is to wrap the wizard in a region tag.


<a4j:region id="addUserWizardRegion" renderRegionOnly="true">
<rich:modalPanel id="addUserPanel">
<h:panelGroup id="addUserWizard">
<a4j:include id="addUser" viewId="addUserPanelOne.xhtml" />
</h:panelGroup>
</rich:modalPanel>
</a4j:region>


Now what will happen is that any a4j:commandButtons inside the wizard will include in their request the name of the region - letting the server know that for this Ajax request, the whole page does not need to be reprocessed - just the areas inside the region. Neither of the list methods will be called. Note that renderRegionOnly must be set to true - by default it is false, which means the entire page will be reRendered instead of just the region.

There is one final problem. What if on the last page of the wizard, when the user hits finish - you actually do want to update one of the tables on the original page, which is now outside the wizard region.

The solution is to use another Ajax4JSF tag - the a4j:jsFunction tag. This tag allows you to define a javascript function that can be invoked from within a wizard panel that will issue an Ajax request to refresh the areas of the page that are outside the region. The a4j:jsFunction tag must be defined at the page level . If it is defined inside the wizard region it will not be able to re-render parts of the page outside that region.

Now your page looks something like this:


<rich:datatable id="userTable" value="#{allUsers.list}">
...
</rich:datatable>

<rich:datatable id="groupTable" value="#{allGroups.list}">
...
</rich:datatable>

<a4j:region id="addUserWizardRegion" renderRegionOnly="true">
<rich:modalPanel id="addUserPanel">
<h:panelGroup id="addUserWizard">
<a4j:include id="addUser" viewId="addUserPanelOne.xhtml" />
</h:panelGroup>
</rich:modalPanel>
</a4j:region>

<a4j:jsFunction name="rerenderUserTable"
actionListener="#{allUsers.dummyCall}"
reRender="userTable" >
</a4j:jsFunction>


And in the last page of your wizard, you will have a finish button that looks like this:


<a4j:commandButton value="Finish"
action="#{userBean.saveUser}"
oncomplete="rerenderUserTable" />

3 comments:

Sergey said...

As I see, your understanding still need to be polished.

I do not see the reason why to use jsFunction here. You need to be more flixible with the way how and why you use renderRegionOnly attribute.

It is important to understanding what is the default JSF lifecycle. You talk about the specific of Ajax4jsf for the issues that are belonged to the core JSF

In general, thank you for your attempt to express your visual.

I will continue at:
the forum post

Keith said...

The reason for the jsFunction is you need an ajax request to originate from outside the region that is wrapped around the wizard. When I first tried reRendering the table from inside the wizard, it did not get reRendered.

This combination of regions and jsFunction is the only way I have managed avoid the getters on the tables being called when ajax requests are issued inside the wizard. I'm not claiming it is the only way - but it does work.

Thanks for your follow up comment in the post - it is very helpful!

Sergey said...

Do not forget that renderRegionOnly might not be hardcodded with true or false only. You can use EL expression there. Like
renderRegionOnly=
"#{allUsers.disableTableUpdate}"

You can have this flag equals to true by default. This will be the same that you have now. Set it to false in the #{userBean.saveUser} action. It will be the 5th phase, so, on the 6th phase during the rendering, it will work like you have no renderRegionOnly restriction and you can point to the table with reRender right from the commandButton without using jsFunction.
I did not test something like that yet, but it should work based on the common logic.