2016-04-11 42 views
6

Ho un problema in cui un JavaFX modificabile 8 Spinner causa uno NullPointerException non catturato se si cancella il testo dell'editor e si impegna e quindi fa clic sul pulsante di incremento o decremento. Questo è j8u60 j8u77. Con un po 'di fortuna il pulsante di incremento/decremento si bloccherà in stato di depressione e gli NPE continueranno a scorrere bloccando l'applicazione.JavaFX Spinner testo vuoto nullpointerexception

Il seguente codice riproduce il problema per 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, cancellare il testo, premere invio (NullPointerException), facendo clic su di incremento o decremento pulsante sarà ora causare anche NPE.

Qualcuno può confermare che si tratta di un bug JavaFX e suggerire una soluzione alternativa?

Edit: L'analisi dello stack eccezione

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) 
+0

Non è un bug. È uno spinner intero ed è un valore non intero che è stato inserito al suo interno. Può solo ruotare i valori se sono interi validi. Quindi, questo sarebbe un comportamento previsto. Avrai bisogno di gestire questa potenziale eccezione nel tuo codice. Basta impostare Modificabile su Falso e rimuove la possibilità di cambiare quel valore. – ManoDestra

+2

@ManoDestra Dai un'occhiata alla traccia dello stack. Non ha senso da dove prenderlo. È completamente interno a JavaFX. – VGR

+0

Perché il codice precedente non fa nulla per gestire gli eventi del controllo. Eppure è ancora impostato per essere modificabile. Se non si desidera che si verifichi l'eccezione, impostare modificabile su falso o gestire gli eventi di controllo, se si consente di modificarlo. Semplice :) – ManoDestra

risposta

3

ho avuto un rovistare tra la sorgente JDK.

La NPE è gettato dal if (newValue < getMin()) { nel lambda ascoltatore qui:

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); 
    } 

presumibilmente newValue è null e l'unboxing automatica di null lancia NPE. Poiché l'input proviene dall'editor, sospetto che lo IntegerStringConverter sia il convertitore predefinito.

Guardando l'attuazione qui:

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())); 
    } 
} 

Vediamo che sarà volentieri null per la stringa vuota, che è una specie di ragionevole dato che non esiste un valore valido per l'input.

Tracciare lo stack di chiamate trovo dove il valore è venuta da:

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); 
      } 
     } 
    }); 

Il valore è impostato con il valore ottenuto dalla convertitore T value = converter.fromString(text); che presumibilmente è nullo.A questo punto credo che la classe spinner dovrebbe verificare che value non sia null e se si ripristini il valore precedente per l'editor.

Ora sono abbastanza sicuro che questo è un bug. Inoltre non penso che un lavoro con un convertitore che non restituisce mai null funzionerà correttamente in quanto maschererà solo il problema e quale valore dovrebbe essere restituito quando il valore non può essere convertito?

Edit: Soluzione

Sostituzione della onAction dell'editor filatore di rifiutare input non valido con un "ritorno alla valida" la politica di risolvere il problema:

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(); 
    }); 
} 

Al contrario di un ascoltatore sulla valueProperty questo evita di attivare altri listener con dati non validi. Tuttavia questo evidenzia un altro problema nella classe spinner. Mentre il precedente risolve il problema tornando ad un valore valido premendo Invio. Cancellare l'input senza commetterlo (premere invio) e quindi premere incremento o decremento causerà lo stesso NPE ma con stack di chiamate leggermente diverso.

Causa:

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); 
} 

decremento è simile, sia chiamata in commitEditorText di seguito:

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); 
     } 
    } 
} 

Avviso il copia-incolla dal onAction nel costruttore:

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 ritenere che commitEditorText debba essere modificato per attivare onAction sull'editor invece in questo modo:

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

allora il comportamento sarebbe stato coerente e dare l'editor la possibilità di gestire l'ingresso prima che vada alla fabbrica di valore.

1

Secondo me è un errore: il IntegerSpinnerValueFactory dovrebbe gestire correttamente questo caso.

Una soluzione è quella di fornire un converter alla fabbrica valore di filatore che restituisce un valore predefinito se il valore di testo non è valido:

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(); 
    } 
} 
+0

Sebbene funzionante, ci sono casi in cui un valore predefinito non è applicabile (o forse nemmeno nel dominio dei valori per lo spinner). –

+0

In tal caso è possibile fornire un'implementazione del valore di fabbrica ... Basta vedere se riesco a farlo funzionare –

3

Questo è il comportamento corretto e previsto per un controllo Spinner basato su un numero intero.

È necessario impostare la proprietà Editable su false, se non si desidera che gli utenti modifichino i valori impostati tramite Factory.

O dovresti gestire l'evento generato dalla proprietà del valore dello spinner.

Ecco un semplice esempio di come farlo:

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()); 
     } 
    } 
} 

This può anche aiutare, se si desidera utilizzare una classe convertitore per aiutare nella gestione dei cambiamenti più completo.

Vedere anche la documentazione ufficiale su setEditable method;

+0

In realtà interpreto la documentazione che hai collegato in modo un po 'diverso; in particolare che il valore factory dovrebbe porre il veto al cambiamento se non è valido. Il problema con soluzioni che utilizzano i listener per ripristinare una modifica a un valore non valido è che gli altri listener del valore osserveranno la modifica al valore non valido e quindi la modifica indietro. Ciò interrompe la semantica del controllo e quegli ascoltatori devono essere scritti per gestire (probabilmente ignorare) quel caso. –

+0

Forse. Sono d'accordo, ma è così che funziona il controllo. Non è necessario ripristinare il valore, ovviamente. Questo era solo un esempio per evidenziare che l'eccezione causata dall'input intero non valido può essere gestita e puoi quindi scegliere come gestirlo. Il ritorno del valore è puramente a scopo dimostrativo qui. Non è necessariamente l'approccio ideale, solo un esempio. L'OP ha chiesto una "soluzione alternativa". Questo è uno di questi esempi :) – ManoDestra

+1

Sì, detto questo, sembra che "IntegerSpinnerValueFactory" gestisca i valori "fuori dall'intervallo" usando una strategia "ripristina la validità", cosa che a me non piace molto. –