Skip to content
Advertisement

Bind StringProperty to Label from a Singleton

I have a singleton called MenuText. It is responsible for displaying the correct text in the menu. It gets updated dynamically.

public class MenuText implements LanguageObserver {

    private MenuText() { //I want this to be private, only one instance should exist
        Config.getInstance().subscribe(this);
    }

    private static class MenuTextHolder {
        private static final MenuText INSTANCE = new MenuText();
    }

    public static MenuText getInstance() {
        return MenuTextHolder.INSTANCE;
    }

    @Override
    public void update(Language language) {
        System.out.println("Updating...");
        switch (language) {
            case ENGLISH -> {
                setText("Play");
            }
            case GERMAN -> {
                setText("Spielen");
            }
        }
    }

    private final StringProperty text = new SimpleStringProperty("Play");

    public StringProperty textProperty() {
        return text;
    }

    public String getText() {
        return text.get();
    }

    private void setText(String text) {
        this.text.set(text);
    }
}

I have a fxml file, but the MenuText can’t have a reference to it. (This would contradict the MVVM architectural style)

<?import tiles.text.MenuText?>
<VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
      prefHeight="1080.0" prefWidth="1920.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="tiles.viewModel.GameMenuViewModel">

<!--here I want to bind to the text property-->
<Button text="???"/>

</VBox>

Initially I used <fx:define> to setup a reference to the MenuText from the fxml file, but this doesn’t allow private constructors. It shouldn’t be that difficult, because MenuText is static, but I’m unable to make a static reference to it’s singleton.

I tried <Button text="${MenuText.getInstance().text}">


Update

As mentioned in this answer, I shouldn’t use the Singleton Pattern. Based on this I added an ApplicationFactory:

//Creation of items with application lifetime
public class ApplicationFactory {

    private Config config;

    public void build() {
        config = new Config();
    }

    public Config getConfig() {
        return config;
    }
}

Is this the correct approach? I now have a MenuFactory, which gets also created in the JavaFX start() method. It sets the parent of the scene.

public class MenuFactory {
    public Parent getMenu(Config config, String fxmlLocation) {
        MenuText menuText = new MenuText(config);
        MenuViewModel menuViewModel = new MenuViewModel(config);

        try {
            FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(getClass().getResource(fxmlLocation)));
            loader.getNamespace().put("menuText", menuText);
            return loader.load();
        } catch (IOException e) {
            //...
        }
    }
}

The start() mehtod looks like this:

    @Override
    public void start(Stage primaryStage) {
        ApplicationFactory applicationFactory = new ApplicationFactory();
        applicationFactory.build();

        MenuFactory menuFactory = new MenuFactory();

        Parent root = menuFactory.getMenu(applicationFactory.getConfig(), MENU);
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();

    }

This makes it way more complicated and I’m not sure if this is correct. Furthermore, I still don’t know how I set the MenuText in the fxml file. I tried, but I think this isn’t the correct way to set a namespace in fxml.

    <fx:define>
        <MenuText fx:id="menuText"/>
    </fx:define>

I read these documentations but don’t understand how I can set this custom namespace.

Advertisement

Answer

Be aware that the singleton pattern is widely considered to be an anti-pattern. However, if you really want to do this:

Initially I used <fx:define> to setup a reference to the MenuText from the fxml file

This is the correct approach. You can combine it with fx:factory to get a reference to an instance of a class that does not have a (public) default constructor:

<fx:define>
    <MenuText fx:factory="getInstance" fx:id="menuText" />
</fx:define>

And then do

<Button text="${menuText.text}" />

Another solution is to “manually” insert the MenuText instance into the FXML namespace:

MenuText menuText = ... ;
FXMLLoader loader = new FXMLLoader(getClass().getResource(...));
loader.getNamespace().put("menuText", menuText);
Parent root = loader.load();

And then

<Button text="${menuText.text}" />

should work with no additional FXML code. This approach allows you to avoid using the singleton pattern (you’re basically injecting the dependency into the FXML, so you could combine it easily with a DI framework).

Advertisement