Friday 2 November 2012

Disabled selection button for PrimeFaces data table

Introduction

When you use the disabled attribute of a button, you can have some troubles executing the action and actionListener bound targets. The same goes for an input component where you can loose the user input.
But there are various use cases where an initial disabled button could be vary handy. The easiest solution could be that you change the scope of the managed bean the property is located that determines the value of the disabled attribute. But in this post I’ll describe an alternative and why it is maybe the best solution.

Disabled button problem

In the Apply request values phase of the JSF life cycle, you can find various resources on the internet that explain this important concept of JSF, there is a check if the post values contains the id of the button.
In that case, the specific button is clicked and an event is generated that the logic attached to the button needs to be executed in the Invoke application phase.
This kind of check is required because a form can contain multiple buttons and thus we need to know which functionality needs to be executed.  But the check that the button must be enabled can give us sometimes a problem.
The EL expression specified in the disabled attribute is evaluated in the Apply request values phase and when it evaluates to true, the event isn't posted.
So even when the button is enabled on the screen, during the Apply request values phase
of the JSF lifecycle, the EL expression can return a disabled true value and thus the click on the button has no effect.

Selection button for PrimeFaces data table

So, let us try with the PrimeFaces data table. You have various row selection options but I want to use the one where a button is placed in a footer of the table.
Suppose you have a JSF managed bean called personListView on a view scope that contains the list of records that you need to show in the page.
The view scope will be used in a lot of cases for the backing of a data table as you don't need to read the data in the database every time you change the sorting or filtering. This of course if the list of records you want to show isn't too long and read them all at once in memory.
The second managed bean, personView called, is on request scope and will be used to edit the selected record. It is on request scope because it only need to capture the data entered by the user and save it to the database.
The JSF page contains a section like this:
<p:dataTable value="#{personListView.personList}" var="person" selectionMode="single"
                     selection="#{personView.selectedPerson}" rowKey="#{person.id}">
   <p:column headerText="first name">
      ...
   </p:column>
   <f:facet name="footer">
     <p:commandButton ajax="false" value="Edit" action="person.jsf"
                      actionListener="#{personView.checkPerson}"/>
   </f:facet>
</p:dataTable>

In the footer, the button is shown to navigate to the second page, person.jsf, and calls here for demonstration purposes an actionListener method that just add an information message.

The idea is that the edit button stays disabled as long as you haven't selected a record in the data table.

So we change the markup to use the disabled attribute of the commandButton and an ajax call that listens to a row selection so that we can ask for a re-rendering of the button.
<p:dataTable value="#{personListView.personList}" var="person" selectionMode="single"
                     selection="#{personView.selectedPerson}" rowKey="#{person.id}">
  <p:ajax event="rowSelect" update="editBtn" />
  <p:column headerText="first name">
                ...
  </p:column>
  <f:facet name="footer">
    <p:commandButton ajax="false" value="Edit" action="person.jsf"
                     actionListener="#{personView.checkPerson}" id="editBtn"
                     disabled="#{empty personView.selectedPerson}"/>
  </f:facet>
</p:dataTable>    

Now the button is disabled as long as we don’t click on a row in the data table.  If we do so, the ajax event is fired, it sets the selected record to the selectedPerson property of the personView managed bean. The re-rendering of the button enables it so that we can click on it.

But if we do click on it, nothing happens. The first thing you should check with a PrimeFaces commandButton that is supposed to do navigation, is that we aren't using ajax.  But in this case, we haven't made any mistake. (ajax=”false” attribute on the commandButton)

The reason why the click on the button has no effect is because of the disabled attribute.  As described above, during Apply request values phase, the EL expression points to a new instance of the personView managed bean, it is at request scope, and thus the selectedPerson property is still null. It will be filled during the Apply model values phase, later on in the life cycle.

Scope change as a solution?


The easiest solution is to change the scope of the personView managed bean to view scope. The bean is first initialized when the ajax call is executing for the row selection. And the bean is kept alive for the click on the button and thus the selectedPerson value has still a value and the button is processed as we expect.

But view scope is a kind of session scope. Surely it has his advantages and usages but a bean with a view scope is stored in the session of the user.  Additional processing is required to remove the bean from the session storage when the user navigates to another view.  So in terms of processing time and server memory consumption, a view scoped bean is worse then a session bean.

So my first reflex is always to use beans on a request scope.  When the situations justifies to use another, longer scope, I will use it of course. So can we fix the above described problem by keeping the personView on request scope?  Yes we can.

The binding attribute.


The solution lies in the binding attribute that each JSF component has. Using this attribute can lead also to some problems (maybe this can be the inspiration for another blog item) but here it makes the selection button responsive again.

The binding attribute gives you to opportunity to have a reference in your backing bean to the java object instance that is used on the component tree representation of your view. It allows programmatic access and changes to the component. So JSF view fragment becomes this:
<p:dataTable value="#{personListView.personList}" var="person" selectionMode="single"
                     selection="#{personView.selectedPerson}" rowKey="#{person.id}">
   <p:ajax event="rowSelect" update="editBtn" listener="#{personView.enableEditBtn}"/>
   <p:column headerText="first name">
      ...
   </p:column>
   <f:facet name="footer">
      <p:commandButton ajax="false" value="Edit" action="person.jsf" binding="#{personView.button}"
                       actionListener="#{personView.checkPerson}" id="editBtn"
                       disabled="#{empty personView.selectedPerson}"/>
   </f:facet>
</p:dataTable>

It has 2 additions; the binding attribute on the commandButton and the listener attribute on the ajax rowSelect tag.

The binding refers to a property in the request scopes personView bean of type HtmlCommanfButton. During the first phase of the life cycle, Restore view Phase, it gets the reference to the java object instance on the component tree for the selection button.
 
private HtmlCommandButton button;

// Setter and getter


The listener attribute of the ajax component is bound to a method in the personView managed bean that manipulates the button programmatically.
 
public void enableEditBtn() {
   button.setDisabled(false);
}
 

When we now click on a record in the data table, the enableEditBtn method is also executed. It 'overrides' the EL expression #{empty personView.seelctedPerson} with the constant value false. It makes the button enabled on the screen when it gets re-rendered but the most important thing of all, the actionListener and navigation are executed since the value of the disabled property is no longer true during the Apply request values phase.

Conclusion

When a JSF commandButton is initially disabled, there are situations that it doesn't respond to a user click when it is re-enabled. Changing the scope of the bean in the EL expression of the disabled button can solve it. But view scope isn't good for the performance of your application. The use of the binding attribute can be the solution.

1 comment: