简体   繁体   中英

Custom JavaFX Component with Constructor That Takes a java.time.Duration

I am attempting to create a Spinner<Duration> object within FXML. I have created a DurationSpinnerValueFactory that extends SpinnerValueFactory<Duration> . I have defined a default constructor for DurationSpinnerValueFactory , as well as a constructor that takes a maximum allowed value. The constructors are defined as follows:

import java.time.Duration;
...

public class DurationSpinnerValueFactory extends SpinnerValueFactory<Duration> {

    public DurationSpinnerValueFactory() {
        this(null);
    }

    public DurationSpinnerValueFactory(@NamedArg("max") final Duration max) {
        ...
    }

}

Within the FXML, the following is working as expected (calling the default constructor):

...
<?import myNamespace.DurationSpinnerValueFactory?>
...
<Spinner fx:id="mySpinner">
  <valueFactory>
    <DurationSpinnerValueFactory />
  </valueFactory>
</Spinner>
...

However, when I try to add a value for the max property, thus changing the called constructor, I get an error. The following illustrates the FXML change:

<DurationSpinnerValueFactory max="PT10M" />

The error I am getting is:

javafx.fxml.LoadException: 
unknown path:23

    at javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2601)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2579)
    at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2425)
    ...
    at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$162(LauncherImpl.java:863)
    at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$175(PlatformImpl.java:326)
    at com.sun.javafx.application.PlatformImpl.lambda$null$173(PlatformImpl.java:295)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater$174(PlatformImpl.java:294)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    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)
Caused by: java.lang.RuntimeException: java.lang.IllegalArgumentException: Unable to coerce PT10M to class java.time.Duration.
    at com.sun.javafx.fxml.builder.ProxyBuilder.createObjectFromDefaultConstructor(ProxyBuilder.java:340)
    at com.sun.javafx.fxml.builder.ProxyBuilder.build(ProxyBuilder.java:223)
    at javafx.fxml.FXMLLoader$ValueElement.processEndElement(FXMLLoader.java:763)
    at javafx.fxml.FXMLLoader.processEndElement(FXMLLoader.java:2823)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2532)
    ... 18 more
Caused by: java.lang.IllegalArgumentException: Unable to coerce PT10M to class java.time.Duration.
    at com.sun.javafx.fxml.BeanAdapter.coerce(BeanAdapter.java:496)
    at com.sun.javafx.fxml.builder.ProxyBuilder$Setter.invoke(ProxyBuilder.java:533)
    at com.sun.javafx.fxml.builder.ProxyBuilder.createObjectFromDefaultConstructor(ProxyBuilder.java:338)
    ... 22 more

I know that I could change the type of max in the constructor to a String , then parse the value within the constructor. However, I would prefer to avoid doing that since I know the type of max should be Duration . Is there a way to get FXMLLoader.load(...) to be able to parse Duration objects from within the FXML?

Solution

Try using "10m" instead of "PT10M" as the string representation of your Duration and consistently using javafx.util.Duration to represent all of the duration related types that are associated with code primarily targeted to service a JavaFX GUI.

Explanatory Background Info

You are getting confused about your Duration types and formats.

The "PT10M" string that you supply is a string representation of the following type:

java.time.Duration

However, the Duration used in your DurationSpinnerValueFactory should probably be the following type:

javafx.util.Duration

What the FXMLLoader is doing, when you look at its code, is trying to invoke a static valueOf function on Duration . For example, for javafx.util.Duration :

javafx.util.Duration.valueOf

When you look at the documentation for that function, it states that the valid input should be:

The syntax is "[number][ms|s|m|h]".

Which is clearly, not what you are using. Instead, you are using a java.time.Duration.parse() format.

Coding up the following statement and running it throws an exception:

javafx.util.Duration.valueOf("PT10M");

Exception in thread "main" java.lang.NumberFormatException: empty String
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at javafx.util.Duration.valueOf(Duration.java:85)

However, if you just code it as below, it executes fine:

javafx.util.Duration.valueOf("10m");

I would think there would be some way to tell an FXMLLoader how to parse custom types from text within the FXML.

Yes, you can do that, it's not officially documented anyway though and I haven't tried it myself. From looking at how javafx.util.Duration is handled, you could supply a wrapper type or subclass of java.time.Duration that implements a static valueOf function.

As another option, I think a builder class can be associated with certain class types to allow them to be resolved and parsed within the context of an FXMLLoader. I haven't looked into the builder type solution, but it probably relies either on reflection or some APIs exposed in the FXMLLoader. See, for example, the FXMLLoader APIs that take builder factories as parameters . There is absolutely no documentation of how this approach works as far as I know, so you will probably need to rely on examining the FXMLLoader code and some example builders that may ship with the JavaFX runtime to support use of the in-built JavaFX controls within FXML.

My recommendation to you is to change your custom component to work with javafx.util.Duration rather than java.time.Duration, then you don't have to start writing additional code to retrofit different duration types and additionally, you don't end up confusing the heck out of yourself and anybody else trying to use the control.

Thanks to @jewelsea for pointing me to the FXMLLoader code. After reviewing the code, it became apparent that there is no way to add custom parsers to the FXML load process. However, the default action for any unknown type T is to reflectively call a static method with the signature public static T valueOf(String value) . Therefore, I simply created the following wrapper class for java.time.Duration :

public class DurationWrapper {

    private final Duration myValue;

    private DurationWrapper(final Duration value) {
        myValue = value;
    }

    public static DurationWrapper valueOf(final String value) {
        final Duration duration = Duration.parse(value);
        final DurationWrapper result = new DurationWrapper(duration);
        return result;
    }

    public Duration getValue() {
        return myValue;
    }

}

Then, I modified my DurationSpinnerValueFactory constructors:

public class DurationSpinnerValueFactory extends SpinnerValueFactory<Duration> {

    public DurationSpinnerValueFactory(
            @NamedArg("max") final DurationWrapper max) {
        this(max.getValue());
    }

    public DurationSpinnerValueFactory(final Duration max) {
        ...
    }

}

I still need to verify that FXMLLoader.load(...) is guaranteed to select the constructor that takes a DurationWrapper over the one that takes a Duration , but it is working on Windows 7 Enterprise SP1 x64 running JRE 8u77 x64. If not, then I will just make the constructor that takes a Duration object non-public.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM