10

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.

Community
  • 1
  • 1
Martin Höller
  • 2,714
  • 26
  • 44
  • 3
    Sorry but 1.1 and 1.10 are equals. It's only a format change not the value. – Rene M. Apr 12 '17 at 08:32
  • The numbers might be equal, but the BigDecimal objects are not. Why would equals() otherwise return false? And there are situations, where the scale of an actual BigDecimal matters. It's not only about formatting but about calculation, which might give different results depending on the actual scale. – Martin Höller Apr 12 '17 at 09:09
  • @MartinHöller But the *values* are equal, and it is a *`ValueChangeListener`*. – user207421 Apr 12 '17 at 10:18
  • @EJP Now we are getting philosophical ;-) What is _the value_ of the ``? The BigDecimal object or the number represented by it? But I got your point and that might indeed be the reason for the observed behavior. Which I still find very unintuitive. – Martin Höller Apr 12 '17 at 10:49
  • @Downvoter Please let me know why you downvoted this question. Otherwise I can't learn for the future. – Martin Höller Apr 12 '17 at 10:53
  • I'd like to know the rationale for including a natural order check as well. They're never equal, neither prior to conversion as Strings nor after conversion as BigDecimals. It seems like this really breaks the contract of `ValueChangeEvent` – axemoi Apr 12 '17 at 15:17
  • @Kukeltje: the **correct** value is applied to the model (as stated in the question). What should using another converter change here? And BTW, I am using a converter, configured via `faces-config.xml`. – Martin Höller May 04 '17 at 09:46
  • On the right I see a 'linked' to http://stackoverflow.com/questions/6787142/bigdecimal-equals-versus-compareto but I do not see it used in the question or comments here. It does however seem to be what clarifies the behaviour you see. Maybe write a 'wrapper' class that implements a different 'compareTo' – Kukeltje May 04 '17 at 09:55
  • After reading everything about it that I can (including http://stackoverflow.com/questions/14102083/why-is-bigdecimal-equals-specified-to-compare-both-value-and-scale-individually) , I'd say It all behaves 'correctly' in the way that it 100% does what the UIInput and the BigDecimal docs state. It just does not what you want it to do and effectively you do not agree what the BigDecimal does (since you most likely disagree with the small example in the question I linke to in my comment above). So creating a wrapper is the way to solve this and it is not a JSF problem. – Kukeltje May 04 '17 at 12:31
  • I never said it behaves incorrectly. According to the docs it behaves correctly. But for me this behavior is just unintuitive and I want to understand what are the reasons behind this. And I don't mean `BigDecimal`. `BigDecimal` behaves exactly as I want it to. JSFs `UIInput` is the problem: the user did **change** the input, which now has a **different meaning** in the application. The new value **is applied** by JSF to the model correclty. But why would JSF **not call the valueChangeListener** in this case? – Martin Höller May 04 '17 at 12:39
  • If it has a meaning in the application, `BigDecimal` does **not** behave correctly, since the compareTo returns true and you want it to return false. So `BigDecimal` is not the right Class for you to use and a wrapper is the solution... And JSF always applies the value to the model. It does not not (yes double negation) call the setter of the model if the value is unchanged. And JSF does not call the valueChangeListener since the BigDecimal says it is not changed!... – Kukeltje May 04 '17 at 13:06
  • The meaning of the scale in the application is not related to `compareTo()`. I need the scale for further calculation and I use `BigDecimal` because it supports exactly this. And yes, you are right regarding call of setters: JSF always calls the setter. But still, the user-input and the object representing it (`BigDecimal`) changed and I would expect JSF to call the listener. – Martin Höller May 04 '17 at 14:25
  • Martin, for JSF the model is leading... The model is a BigDecimal and it did **not** change according to the model... JSF should not make its own assumptions and have knowledge about what the model is and how it should be interpreted. The **only** solution is to create a MyBigDecimal and override the compareTo and use this MyBigDecimal in your model. I cannot see any other way no matter how many bounty points you give it. I can write this as an answer, but I doubt you want to hear this. – Kukeltje May 09 '17 at 12:23
  • @Kukeltje, thanks for your comments. However, the BigDecimal **did** change, otherwise `equals()` wouldn't return false and calculations with different objects wouldn't result in different results! I completely agree that JSF shouldn't have knowledge about how to interpret the model, but IMHO it does so by calling `compareTo()`. If I'd write a MyBigDecimal class, I'd have to return +1/-1 on calls to `compareTo()` for 1.1 and 1.10 to see changed behavior, which both is not, what my model would suggest! – Martin Höller May 09 '17 at 13:27
  • _"However, the BigDecimal did change, otherwise equals() wouldn't return false and calculations with different objects wouldn't result in different results!"_ object wise yes (equals) , value wise no... That is the difference. So it is in fact **correct** in not only using 'equals' – Kukeltje May 09 '17 at 13:35
  • According to the JavaDoc of `Comparable` and `compareTo()`, absolutely nothing is said about the _value_ of the objects. `compareTo()` only defines the ordering of objects, which is totally irrelevant for mathematical operations. – Martin Höller May 09 '17 at 13:45

2 Answers2

4

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.

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • I understand this solution and yes it wil work, but why this instead of creating a custom MyBigDecimal? This way it seems the UI is adapting to the behaviour of the model while the model should be 'corrected'? – Kukeltje May 09 '17 at 13:33
  • @Kukeltje Because I agree the ValueChangeListener behaving unintuitive here and a wrapper wouldn't be a *clean* solution. – BalusC May 09 '17 at 13:43
  • Thanks BalusC! This is at least a very clean workaround for my problem. – Martin Höller May 09 '17 at 14:10
1

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?.

Community
  • 1
  • 1
Martin Höller
  • 2,714
  • 26
  • 44