4
votes

In JavaFX8 there is a UI Controls Architecture that is used for make custom controls. Basically is based in:

  • Control.
  • Skin.
  • CSS.

Also, there is a basic structure of an FXML project that is used to make GUI too. Basically:

  • Control.
  • FXML file.
  • CSS.

I would like to use FXML with the UI Controls Architecture, so my question is:

Who is the controller for the FXML file? The Skin?

I have to do something like this code below?:

public class MySkin extends SkinBase<MyControl> {
public GaugeSkin(MyControl control) {
    super(control);
    FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("MyView.fxml"));
    fxmlLoader.setRoot(control);
    fxmlLoader.setController(control);

    try {
        fxmlLoader.load();
    } catch (IOException exception) {
        throw new RuntimeException(exception);
    }
}
2

2 Answers

4
votes

I think you're on the right track with the Skin class being the controller for the loaded FXML file because it is the Skin that is responsible for defining the nodes that comprise the 'look' of a particular control.

The Control class itself should really only define properties that hold the state of the control and should not care how the Skin actually creates the view hierarchy (that is, it should only care about it's state, not how it looks).

One difference I would make is to change fxmlloader.setController(control); to fxmlloader.setController(this); so that the Skin class becomes the controller rather than the control itself.

Another thing you could do is to move the FXMLLoader logic into a base class so that you don't have to duplicate it every time you want to create a Skin, something like this:

public abstract class FXMLSkin<C extends Control> extends SkinBase<C>{

    public FXMLSkin(C control) {
        super(control);
        this.load();
    }

    private void load() {
        FXMLLoader loader = new FXMLLoader(getFXML());
        loader.setController(this);

        try {
            Node root = loader.load();
            this.getChildren().add(root);
        } catch (IOException ex) {
            Logger.getLogger(FXMLSkin.class.getName()).log(Level.SEVERE, null, ex);
        }   
    }

    protected abstract URL getFXML();
}

I have a JavaFX UserControl on my Github page which does something very similar to the FXMLSkinBase class above. It uses a convention to load an FXML file with the same name as the derived class so that the FXML file name does not need to be specified each time. I.E. if your derived skin is called FooControlSkin, the control will automatically load an FXML file called FooControlSkin.fxml.

The class is very simple and the code could very easily be refactored into a fully featured FXMLSkinBase class that would meet your requirements.

1
votes

My better way:

  • removing need for <fx:root> as root element of .fxml file.
  • removing need to call fxmlLoader.setRoot(control);.
  • removing need to call fxmlLoader.setController(control);.
  • allowing a Skin to be a controller automatically via fx:controller="{controller class name}" in FXML root element.
  • allowing an IDE to highlight bogus @FXML references to FXML in ControlNameSkin class.

A proper Control class.

public static class ControlName extends Control {
    @Override
    protected Skin<?> createDefaultSkin() {
        ControlNameSkin.Factory factory = new ControlNameSkin.Factory(this);
        FXMLLoader fxmlLoader = new FXMLLoader(getClass()
                 .getResource("ControlName.fxml"));
        fxmlLoader.setControllerFactory(factory);
        try {
            Node root = fxmlLoader.load();
            ControlNameSkin skin = fxmlLoader.getController();
            skin.construct(root);
            return skin;
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}

A combined Skin and Controller class.

public static class ControlNameSkin extends SkinBase<ControlName> {
    public ControlNameSkin(Factory factory) {
        super(factory.control);
        // any setup NOT using FXML references
    }

    public void construct(Node root) {
        ControlName control = getSkinnable();
        // any setup using FXML references
        getChildren().add(root);
    }

    public static class Factory implements Callback<Class<?>, Object> {
        public final ControlName control;

        public Factory(ControlName control) {
            this.control = control;
        }

        @Override
        public Object call(Class<?> cls) {
            try {
                return cls.getConstructor(Factory.class).newInstance(this);
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        }
    }
}