2016-04-11 54 views
6

Mam problem, w którym edytowalna JavaFX 8 Spinner powoduje, że niezatłoczony NullPointerException usuwa tekst edytora i zatwierdza, a następnie klika przycisk zwiększania lub zmniejszania. To jest j8u60 j8u77. Przy odrobinie szczęścia przycisk inkrementacji/dekrementacji utknie w stanie wciśniętym, a NPE będzie dalej płynąć, blokując aplikację.JavaFX Spinner pusty tekst punkt zerowy punktu zerowego

Poniższy kod reprodukuje problem dla mnie:

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

uruchomić go, wyczyść tekst, naciśnij ENTER (NullPointerException), klikając opcję przyrost lub przycisk Decrement będzie teraz również powodować NPE.

Czy ktoś może potwierdzić, że jest to błąd JavaFX i zaproponować obejście tego problemu?

Edycja: Wyjątek ślad stosu

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

To nie jest błąd. Jest to spinner Integer i jest to wartość niecałkowita, która została w nim umieszczona. Może obracać wartości tylko wtedy, gdy są poprawnymi liczbami całkowitymi. Tak więc byłoby to oczekiwane zachowanie. Będziesz musiał obsłużyć ten potencjalny wyjątek w swoim kodzie. Po prostu ustaw Edycja na wartość fałsz i usunie możliwość zmiany tej wartości. – ManoDestra

+2

@ManoDestra Spójrz na ślad stosu. Nie ma sensu, aby go złapać. Jest całkowicie wewnętrzna w stosunku do JavaFX. – VGR

+0

Ponieważ powyższy kod nie robi nic, aby obsłużyć zdarzenia kontrolne. A mimo to nadal można ustawić jako edytowalny. Jeśli nie chcesz, aby wystąpił wyjątek, ustaw opcję editable na false lub obsługuj zdarzenia sterujące, jeśli zezwolisz na edycję. Prosty :) – ManoDestra

Odpowiedz

3

miałem grzebać źródła JDK.

NPE jest wyrzucany z if (newValue < getMin()) { w lambda słuchacza tutaj:

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

przypuszczalnie newValue jest null i auto unboxing z null wyrzuca NPE. Ponieważ dane wejściowe pochodzą od edytora, podejrzewam, że IntegerStringConverter jest domyślnym konwerterem.

Patrząc na realizację tutaj:

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

Widzimy, że będzie szczęśliwie powrócić null na pusty ciąg, który jest swego rodzaju rozsądny biorąc pod uwagę, że nie ma żadnej prawidłowej wartości dla danych wejściowych.

Kalka górę stosu wywołań znajdę gdzie wartość pochodzi z:

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

wartość jest ustawiona z wartością uzyskaną z konwerter T value = converter.fromString(text); który prawdopodobnie jest pusty.W tym momencie uważam, że klasa spinner powinna sprawdzić, czy value nie jest null i jeśli przywraca poprzednią wartość edytorowi.

Jestem teraz dość pewien, że to błąd. Co więcej nie sądzę, że praca z konwerterem, który nigdy nie zwróci wartości null, będzie działała poprawnie, ponieważ będzie maskować tylko problem i jaką wartość należy zwrócić, gdy nie można przekonwertować wartości?

Edit: Obejście

Wymiana onAction edytora wirowania odrzucić nieprawidłowe dane z „powrót do ważnej” polityki rozwiązuje 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(); 
    }); 
} 

W przeciwieństwie do słuchacza na valueProperty to unika wyzwalania innych słuchaczy z niepoprawnymi danymi. To jednak podkreśla kolejny problem w klasie spinner. Chociaż powyższe rozwiązuje problem przez powrót do prawidłowej wartości po naciśnięciu klawisza Enter. Usunięcie wejścia bez zatwierdzenia (naciśnięcie klawisza enter), a następnie naciśnięcie inkrementacji lub zmniejszenia spowoduje ten sam NPE, ale z nieco innym stosem wywołań.

Przyczyna:

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

Decrement jest podobna, zarówno wezwanie do commitEditorText poniżej:

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

zauważy copy-paste z onAction w konstruktorze:

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 uważamy, że commitEditorText należy zmienić, aby wywołać onAction na edytorze zamiast tak:

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

wówczas zachowanie byłoby zgodne i dać redaktorowi okazję obsługiwać wejście, zanim przejdzie do fabryki wartości.

1

uznałbym to błąd: the IntegerSpinnerValueFactory należy prawidłowo obsługiwać tę sprawę.

Jeden obejście jest dostarczenie converter do fabryki wartości spinner, który ocenia na wartość domyślną, jeśli wartość tekstu nie jest ważny:

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

Chociaż działa, istnieją przypadki, w których wartość domyślna nie ma zastosowania (lub prawdopodobnie nawet w domenie wartości dla przędzarki). –

+0

W takim przypadku możesz podać implementację fabryki wartości ... Po prostu sprawdzam, czy mogę wykonać tę pracę. –

3

Jest to poprawne i oczekiwane zachowanie dla sterowania Spinner opartego na liczbach całkowitych.

Powinieneś ustawić jego właściwość Editable na false, jeśli nie chcesz, aby użytkownicy edytowali wartości ustawione fabrycznie.

Albo powinieneś obsługiwać wydarzenie podniesione przez właściwość wartości spinnera.

Oto prosty przykład jak to zrobić:

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 może również pomóc, jeśli chcesz użyć klasy konwerter pomóc w prowadzeniu zmiany bardziej kompleksowo.

Zobacz także oficjalną dokumentację na temat setEditable method;

+0

Rzeczywiście interpretuję dokumentację, którą łączyłeś nieco inaczej; w szczególności, że fabryka wartości powinna zawetować zmianę, jeśli jest nieważna. Problem z rozwiązaniami, które używają detektorów do przywracania zmiany do nieprawidłowej wartości, polega na tym, że inni słuchacze tej wartości będą obserwować zmianę nieprawidłowej wartości, a następnie zmianę z powrotem. Łamie to semantykę kontroli, a ci słuchacze muszą być napisani, aby obsłużyć (prawdopodobnie zignorować) tę sprawę. –

+0

Prawdopodobnie. Zgadzam się, ale tak działa kontrola. Oczywiście nie musisz odwracać wartości. To był tylko przykład do podkreślenia, że ​​wyjątek spowodowany nieprawidłowym wprowadzeniem liczb całkowitych może zostać obsłużony i możesz wtedy wybrać sposób obsługi. Odwrócenie wartości jest tutaj wyłącznie w celach demonstracyjnych. To niekoniecznie idealne podejście, tylko przykład. PO zwrócił się o "obejście". To jeden z takich przykładów :) – ManoDestra

+1

Tak, to powiedziawszy, wygląda na to, że 'IntegerSpinnerValueFactory' obsługuje wartości" poza zakresem ", używając strategii" powrócić do poprawnej ", której tak naprawdę nie lubię. –