Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ValueChangeListener for BigDecimal values not called if only scale changed

Tags:

jsf

I have a JSF 2.2 application where the user has to input BigDecimal values in an <h:inputText>. For such an input-field, a valueChangeListener is configured to be called on input changes. Here is the XHTML code:

<h:form id="theForm">
  <h:inputText id="bd" value="#{bean.bd}"
               valueChangeListener="#{bean.bdChangedListener()}"/>
  <h:commandButton id="submit" value="Submit" />
</h:form>

This works fine in most cases. The bdChangedListener() method is called, when the value changed and the submit-button is pressed. The value is correctly committed to the model.

However, if I entered 1.1 and change it to 1.10, the new value is committed to the model, but the valueChangeListener is never called! Debuging showed, that the reason for this is in javax.faces.component.UIInput#compareValues(). The JavaDoc from this method says:

Return true if the new value is different from the previous value. First compare the two values by passing value to the equals method on argument previous. If that method returns true, return true. If that method returns false, and both arguments implement java.lang.Comparable, compare the two values by passing value to the compareTo method on argument previous. Return true if this method returns 0, false otherwise.

So it seems to me, this is intentionally. But why?

The user-input changed, and there are applications, where the scale of a BigDecimal is relevant. So JSF should not just ignore the changed input but notify me! It updates the model with the new value, why would it skip the valueChangeListener-method?

How could I avoid this behavior and get notified in a clean way? (I know I could hook into the setter, but that's not what I call a clean way!)

Any ideas?

Further reading and comments

In addition to the above I want to mention, that I've already read questions like BigDecimal equals() versus compareTo(). I do understand why BigDecimal's equals() and compareTo() behave like they do and I'd say it is correct. The behavior of BigDecimal is not the problem, the UIInput.compareValues() is the problem.

Also a converter (as suggested in comments or already deleted answers) won't save my day. The user input is correctly converted and I need the BidDecimal including the exact scale in my application. Any converter returning a BigDecimal won't change the observed behavior.

A wrapper class around BigDecimal could possibly solve my problem, but is not what I consider a clean solution. I really want to know why UIInput behaves the way it does.

like image 730
Martin Höller Avatar asked Apr 12 '17 08:04

Martin Höller


2 Answers

It's indeed behaving as specified. It is what it is. You have very correctly nailed down the root cause to javax.faces.component.UIInput#compareValues(). The solution would be to let it skip the additional Comparable#compareTo() check.

Your best bet is to extend the HtmlInputText and override the compareValues() accordingly.

@FacesComponent(createTag=true)
public class InputBigDecimal extends HtmlInputText {

    @Override
    protected boolean compareValues(Object previous, Object value) {
        return !Objects.equals(previous, value);
    }

}

Then just replace <h:inputText> by <x:inputBigDecimal> as below.

<... xmlns:x="http://xmlns.jcp.org/jsf/component">
...
<h:form id="theForm">
    <x:inputBigDecimal id="bd" value="#{bean.bd}"
                       valueChangeListener="#{bean.bdChangedListener()}" />
    <h:commandButton id="submit" value="Submit" />
</h:form>

Note: code is complete as-is. No additional XML config files are necessary thanks to JSF 2.2's createTag=true. You'll only miss IDE's XML intellisense on this, but that's a different problem.

like image 107
BalusC Avatar answered Oct 20 '22 23:10

BalusC


BalusC's answer describes a generic approach to fix the problem in a clean way without changing application behavior. Antoher possible workaround that works, but slightly changes your application behavior, is to use an AJAX-listener instead of the valueChangeListener:

<h:form id="theForm">
  <h:inputText id="bd" value="#{bean.bd}">
    <f:ajax listener="#{bean.ajaxListener}"/>
  </h:inputText>
  <h:commandButton id="submit" value="Submit" />
</h:form>

The listener method must have a different signature:

public void ajaxListener(AjaxBehaviorEvent event)
{
    // Do whatever you like here.
    // The new value is already in the model.
}

Note, that the AJAX-listener is called as soon as the DOM change event fires (not when the sumbit button is pressed) and does not check if the actual value changed, as valueChangeListener does.

Details about differences in the two approaches can be found in BalusC's excellent answer to When to use valueChangeListener or f:ajax listener?.

like image 23
Martin Höller Avatar answered Oct 20 '22 21:10

Martin Höller