JavaFX Spinner empty text nullpointerexception

I have a problem when an editable JavaFX 8 Spinner raises an uncaught NullPointerException if it removes the editor text and commits, and then clicks the increase or decrease button. This is j8u60 j8u77. With some luck, the increment / decment button will get stuck in the pressed state, and NPE will continue to block the application.

The following code reproduces the problem for me:

 import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.Spinner; import javafx.scene.control.SpinnerValueFactory; import javafx.scene.control.SpinnerValueFactory.IntegerSpinnerValueFactory; import javafx.stage.Stage; public class Test extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage aPrimaryStage) throws Exception { IntegerSpinnerValueFactory valueFactory = new IntegerSpinnerValueFactory(0, 10); Spinner<Integer> spinner = new Spinner<>(valueFactory); spinner.setEditable(true); aPrimaryStage.setScene(new Scene(spinner)); aPrimaryStage.show(); } } 

Run it, clear the text, press Enter ( NullPointerException ), pressing the increase or decrease button will now also cause NPE.

Can I confirm that this is a JavaFX bug and suggest a workaround?

Edit: exception stack trace

 Exception in thread "JavaFX Application Thread" java.lang.NullPointerException at javafx.scene.control.SpinnerValueFactory$IntegerSpinnerValueFactory.lambda$new$215(SpinnerValueFactory.java:475) at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361) at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81) at javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:105) at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112) at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146) at javafx.scene.control.SpinnerValueFactory.setValue(SpinnerValueFactory.java:150) at javafx.scene.control.Spinner.lambda$new$210(Spinner.java:139) at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86) at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238) at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191) at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59) at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58) at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74) at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49) at javafx.event.Event.fireEvent(Event.java:198) at javafx.scene.Node.fireEvent(Node.java:8411) at com.sun.javafx.scene.control.behavior.TextFieldBehavior.fire(TextFieldBehavior.java:179) at com.sun.javafx.scene.control.behavior.TextInputControlBehavior.callAction(TextInputControlBehavior.java:178) at com.sun.javafx.scene.control.behavior.BehaviorBase.callActionForEvent(BehaviorBase.java:218) at com.sun.javafx.scene.control.behavior.TextInputControlBehavior.callActionForEvent(TextInputControlBehavior.java:127) at com.sun.javafx.scene.control.behavior.BehaviorBase.lambda$new$74(BehaviorBase.java:135) at com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218) at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80) at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238) at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191) at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59) at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58) at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74) at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49) at javafx.event.Event.fireEvent(Event.java:198) at javafx.scene.Node.fireEvent(Node.java:8411) at com.sun.javafx.scene.control.skin.SpinnerSkin.lambda$new$473(SpinnerSkin.java:151) at com.sun.javafx.event.CompositeEventHandler$NormalEventFilterRecord.handleCapturingEvent(CompositeEventHandler.java:282) at com.sun.javafx.event.CompositeEventHandler.dispatchCapturingEvent(CompositeEventHandler.java:98) at com.sun.javafx.event.EventHandlerManager.dispatchCapturingEvent(EventHandlerManager.java:223) at com.sun.javafx.event.EventHandlerManager.dispatchCapturingEvent(EventHandlerManager.java:180) at com.sun.javafx.event.CompositeEventDispatcher.dispatchCapturingEvent(CompositeEventDispatcher.java:43) at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:52) at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74) at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54) at javafx.event.Event.fireEvent(Event.java:198) at javafx.scene.Scene$KeyHandler.process(Scene.java:3964) at javafx.scene.Scene$KeyHandler.access$1800(Scene.java:3910) at javafx.scene.Scene.impl_processKeyEvent(Scene.java:2040) at javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2501) at com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:197) at com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:147) at java.security.AccessController.doPrivileged(Native Method) at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleKeyEvent$353(GlassViewEventHandler.java:228) at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389) at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleKeyEvent(GlassViewEventHandler.java:227) at com.sun.glass.ui.View.handleKeyEvent(View.java:546) at com.sun.glass.ui.View.notifyKey(View.java:966) at com.sun.glass.ui.win.WinApplication._runLoop(Native Method) at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191) at java.lang.Thread.run(Thread.java:745) 
+8
source share
4 answers

I had a break through the JDK source.

The NPE is thrown from if (newValue < getMin()) { in the listener's lambda here:

javafx.scene.control.SpinnerValueFactory.java

  public IntegerSpinnerValueFactory(@NamedArg("min") int min, @NamedArg("max") int max, @NamedArg("initialValue") int initialValue, @NamedArg("amountToStepBy") int amountToStepBy) { setMin(min); setMax(max); setAmountToStepBy(amountToStepBy); setConverter(new IntegerStringConverter()); valueProperty().addListener((o, oldValue, newValue) -> { // when the value is set, we need to react to ensure it is a // valid value (and if not, blow up appropriately) if (newValue < getMin()) { setValue(getMin()); } else if (newValue > getMax()) { setValue(getMax()); } }); setValue(initialValue >= min && initialValue <= max ? initialValue : min); } 

supposedly newValue is null , and automatically unlocking null causes NPE. Since the input comes from the editor, I suspect that IntegerStringConverter is the default converter.

Take a look at the implementation here:

javafx.util.converter.IntegerStringConverter

 public class IntegerStringConverter extends StringConverter<Integer> { /** {@inheritDoc} */ @Override public Integer fromString(String value) { // If the specified value is null or zero-length, return null if (value == null) { return null; } value = value.trim(); if (value.length() < 1) { return null; } return Integer.valueOf(value); } /** {@inheritDoc} */ @Override public String toString(Integer value) { // If the specified value is null, return a zero-length String if (value == null) { return ""; } return (Integer.toString(((Integer)value).intValue())); } } 

We see that it will happily return null for an empty string, which is reasonable given that there is no valid value for input.

By tracking the call stack, I find where this value comes from:

javafx.scene.control.Spinner

 public Spinner() { getStyleClass().add(DEFAULT_STYLE_CLASS); setAccessibleRole(AccessibleRole.SPINNER); getEditor().setOnAction(action -> { String text = getEditor().getText(); SpinnerValueFactory<T> valueFactory = getValueFactory(); if (valueFactory != null) { StringConverter<T> converter = valueFactory.getConverter(); if (converter != null) { T value = converter.fromString(text); valueFactory.setValue(value); } } }); 

The value is set with the value obtained from the converter T value = converter.fromString(text); which is supposedly null. At this point, I believe that the spinner class should check that value not null , and if it restores the previous value in the editor.

Now I'm sure this is a mistake. Moreover, I don’t think that working with a converter that never returns null will work correctly, as this will only mask the problem and what value should be returned when the value cannot be converted?

Edit: workaround

Replacing the onAction counter editor to reject invalid input using the back to action policy fixes the problem:

 public static <T> void fixSpinner2(Spinner<T> aSpinner) { aSpinner.getEditor().setOnAction(action -> { String text = aSpinner.getEditor().getText(); SpinnerValueFactory<T> factory = aSpinner.getValueFactory(); if (factory != null) { StringConverter<T> converter = factory.getConverter(); if (converter != null) { T value = converter.fromString(text); if (null != value) { factory.setValue(value); } else { aSpinner.getEditor().setText(converter.toString(factory.getValue())); } } } action.consume(); }); } 

Unlike the listener on valueProperty , this allows you to run other listeners with invalid data. However, this highlights another problem in the spinner class. While the above fixes the problem by returning a valid value when pressing enter. Erasing the input without committing (pressing enter) and then incrementing or decreasing will result in the same NPE, but with a slightly different call stack.

Cause:

 public void increment(int steps) { SpinnerValueFactory<T> valueFactory = getValueFactory(); if (valueFactory == null) { throw new IllegalStateException("Can't increment Spinner with a null SpinnerValueFactory"); } commitEditorText(); valueFactory.increment(steps); } 

The reduction seems to be both invoked in commitEditorText below:

 private void commitEditorText() { if (!isEditable()) return; String text = getEditor().getText(); SpinnerValueFactory<T> valueFactory = getValueFactory(); if (valueFactory != null) { StringConverter<T> converter = valueFactory.getConverter(); if (converter != null) { T value = converter.fromString(text); valueFactory.setValue(value); } } } 

Note the copy-paste from onAction in the constructor:

  getEditor().setOnAction(action -> { String text = getEditor().getText(); SpinnerValueFactory<T> valueFactory = getValueFactory(); if (valueFactory != null) { StringConverter<T> converter = valueFactory.getConverter(); if (converter != null) { T value = converter.fromString(text); valueFactory.setValue(value); } } }); 

I believe that commitEditorText should be changed to call onAction in the editor, and not like this:

 private void commitEditorText() { if (!isEditable()) return; getEditor().getOnAction().handle(new ActionEvent(this, this)); } 

then the behavior will be consistent and will give the editor the ability to process the input data before it moves to the factory value.

+3
source

This is the correct and expected behavior for an Integer-based Spinner control.

You must either set the Editable property to false if you do not want users to edit the values ​​set using Factory.

Or you should handle the event raised by the value of the spinner property.

Here is a simple example of how to do this:

 import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.Spinner; import javafx.scene.control.SpinnerValueFactory; import javafx.scene.control.SpinnerValueFactory.IntegerSpinnerValueFactory; import javafx.stage.Stage; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; public class Spin extends Application { Spinner<Integer> spinner; public static void main(String[] args) { launch(args); } @Override public void start(Stage aPrimaryStage) throws Exception { IntegerSpinnerValueFactory valueFactory = new IntegerSpinnerValueFactory(0, 10); spinner = new Spinner<>(valueFactory); spinner.setEditable(true); spinner.valueProperty().addListener((observableValue, oldValue, newValue) -> handleSpin(observableValue, oldValue, newValue)); aPrimaryStage.setScene(new Scene(spinner)); aPrimaryStage.show(); } private void handleSpin(ObservableValue<?> observableValue, Number oldValue, Number newValue) { try { if (newValue == null) { spinner.getValueFactory().setValue((int)oldValue); } } catch (Exception e) { System.out.println(e.getMessage()); } } } 

It can also help you if you want to use the converter class to help process changes in more detail.

See also the official documentation for the setEditable method ;

+4
source

I would consider this a mistake: IntegerSpinnerValueFactory should handle this case correctly.

One solution is to provide converter spinner factory value that evaluates to the default value if the text value is invalid:

 import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.Spinner; import javafx.scene.control.SpinnerValueFactory.IntegerSpinnerValueFactory; import javafx.stage.Stage; import javafx.util.StringConverter; public class Test extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage aPrimaryStage) throws Exception { IntegerSpinnerValueFactory valueFactory = new IntegerSpinnerValueFactory(0, 10); valueFactory.setConverter(new StringConverter<Integer>() { @Override public String toString(Integer object) { return object.toString() ; } @Override public Integer fromString(String string) { if (string.matches("-?\\d+")) { return new Integer(string); } // default to 0: return 0 ; } }); Spinner<Integer> spinner = new Spinner<>(valueFactory); spinner.setEditable(true); aPrimaryStage.setScene(new Scene(spinner)); aPrimaryStage.show(); } } 
+1
source

This is a known bug fixed in Java 9 - see https://bugs.openjdk.java.net/browse/JDK-8150962

+1
source

All Articles