Search code examples
javajavafxfxml

JavaFX: Custom setter name for FXML attribute?


I have the following class that uses fluent setters in order to allow concatenation:

/**
 * Data that affects the way an animation is played.
 */
public class AnimationSettings {

    private Duration duration = Duration.seconds(1);
    private Curve curve = Curve.LINEAR;

    /**
     * @param duration duration of the animation to set
     * @return this for concatenation
     */
    public AnimationSettings withDuration(Duration duration) {
        this.duration = duration;
        return this;
    }

    /**
     * @return duration of the animation
     */
    public Duration getDuration() {
        return duration;
    }

    /**
     * @param curve curve of the animation to set
     * @return this for concatenation
     */
    public AnimationSettings withCurve(Curve curve) {
        this.curve = curve;
        return this;
    }

    /**
     * @return curve of the animation
     */
    public Curve getCurve() {
        return curve;
    }
}

What I would like to achieve is creating a new instance and set those values from an FXML file, like this:

<AnimatedNode>
    <settings>
        <AnimationSettings duration="900ms" curve="LINEAR"/>
    </settings>
</AnimatedNode>

However, an error is given as the attributes duration and curve can't be found.
After some investigation, I realized an FXML attribute should be linked to both a getter and setter, strictly named getX and setX, so my withX is not seen as a setter and the attribute cannot be set.

I was wondering if there is some annotation I am not aware of that tells JavaFX that a method should be treated as a setter, or some other way, as I think renaming those methods to setX would lose the 'fluent' feel.


Solution

  • I made this work with a custom Builder. It's probably more effort than it is worth. I would recommend just implementing both withCurve() and setCurve() in your AnimationSettings class.

    However, for demo purposes I added

    package org.jamesd.examples.buildertest;
    
    public enum Curve {
        LINEAR
    }
    

    Left AnimationSettings as-is:

    package org.jamesd.examples.buildertest;
    
    public class AnimationSettings {
    
        private Duration duration = Duration.seconds(1);
        private Curve curve = Curve.LINEAR;
    
        /**
         * @param duration duration of the animation to set
         * @return this for concatenation
         */
        public AnimationSettings withDuration(Duration duration) {
            this.duration = duration;
            return this;
        }
    
        /**
         * @return duration of the animation
         */
        public Duration getDuration() {
            return duration;
        }
    
        /**
         * @param curve curve of the animation to set
         * @return this for concatenation
         */
        public AnimationSettings withCurve(Curve curve) {
            this.curve = curve;
            return this;
        }
    
        /**
         * @return curve of the animation
         */
        public Curve getCurve() {
            return curve;
        }
    }
    

    Then I created a builder for AnimationSettings. This is based on the current implementation of JavaFXFontBuilder. This implementation makes it a Map<String, Object> implementation, with the put(...) method accepting the name of a property and its value. You can also implement this using Java Bean style setCurve(...) and setDuration(...) methods.

    package org.jamesd.examples.buildertest;
    
    import javafx.util.Builder;
    import javafx.util.Duration;
    
    import java.util.AbstractMap;
    import java.util.Set;
    
    public class AnimationSettingsBuilder extends AbstractMap<String, Object> implements Builder<AnimationSettings> {
    
        private Duration duration = Duration.seconds(1);
    
        private Curve curve = Curve.LINEAR;
    
        @Override
        public AnimationSettings build() {
            return new AnimationSettings()
                    .withCurve(curve)
                    .withDuration(duration);
        }
    
        @Override
        public Object put(String key, Object value) {
            if ("duration".equals(key)) {
                // can parse units here if you want more flexibility, but for simplicity:
                duration = Duration.seconds(Double.parseDouble((String)value));
            } else if ("curve".equals(key)) {
                curve = Curve.valueOf((String)value);
            }
            return null;
        }
    
        @Override
        public boolean containsKey(Object key) {
            return false; // False in this context means that the property is NOT read only
        }
    
        @Override
        public Object get(Object key) {
            return null; // In certain cases, get is also required to return null for read-write "properties"
        }
    
        @Override
        public Set<Entry<String, Object>> entrySet() {
            return null;
        }
    
    }
    

    And then finally a builder factory. The builder factory has to implement the getBuilder(...) method. This just wraps a default implementation, returns the builder defined above to build AnimationSettings instances, and delegates to the default builder factory for other classes.

    package org.jamesd.examples.buildertest;
    
    import javafx.fxml.JavaFXBuilderFactory;
    import javafx.util.Builder;
    import javafx.util.BuilderFactory;
    
    public class AnimationSettingsBuilderFactory implements BuilderFactory {
    
        private final BuilderFactory defaultBuilderFactory = new JavaFXBuilderFactory();
        @Override
        public Builder<?> getBuilder(Class<?> type) {
            if (type == AnimationSettings.class) {
                return new AnimationSettingsBuilder();
            }
            return defaultBuilderFactory.getBuilder(type);
        }
    }
    

    For simple testing, here is Test.fxml:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import org.jamesd.examples.buildertest.AnimationSettings?>
    <AnimationSettings xmlns="http://javafx.com/javafx"
                       xmlns:fx="http://javafx.com/fxml"
                       duration="2.0" curve="LINEAR">
    
    </AnimationSettings>
    

    and Test.java:

    package org.jamesd.examples.buildertest;
    
    import javafx.fxml.FXMLLoader;
    
    import java.io.IOException;
    
    public class Test {
        public static void main(String[] args) throws IOException {
            FXMLLoader loader = new FXMLLoader(Test.class.getResource("Test.fxml"));
            loader.setBuilderFactory(new AnimationSettingsBuilderFactory());
            AnimationSettings settings = loader.load();
            System.out.println("Duration: "+settings.getDuration());
            System.out.println("Curve: "+settings.getCurve());
        }
    }