Search code examples
javajavafxfxml

Custom action emitter property for custom component JavaFX - How?


In the midst of developing a JavaFX application project, this is my first real stumper and I don't even know how to properly say what I'm trying to do because I don't know the names of the techniques I'm trying to utilize.

In my application, I have multiple custom components so I can build a more complex scene. This problem involves 3 control components; a PrimaryControl component (SplitPane) which contains a MenuComponent and an ActivityComponent (both custom) which changes based on the selection of radio buttons set up in the MenuComponent.

How can I get the MenuControl class to "emit" the performed action when a radio button in the ToggleGroup (from that component) is changed which will then be handled by a function in the parent component PrimaryControl so that it may change which ActivityComponent is visible/active? Bear with me.

primary_control.fxml

<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml">
    <MenuControl onToggleGroupButtonSelectionChanged="#onChange"/>
    <ActivityOneControl fx:id="one" isVisible="true"/>
    <ActivityTwoControl fx:id="two" isVisible="false"/>
    ...
</fx:root>

menu_control.fxml

<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/10.0.2-internal">

    <GridPane alignment="CENTER" hgap="10" vgap="10">
        <padding><Insets bottom="25" left="25" right="25" top="15" /></padding>
        <Label text="Select A Task" GridPane.columnIndex="0" GridPane.rowIndex="0">
            <font><Font name="System Bold" size="13.0" /></font>
        </Label>

        <fx:define>
            <ToggleGroup fx:id="menuSelection"/>
        </fx:define>

        <RadioButton fx:id="one" text="one" GridPane.columnIndex="0" GridPane.rowIndex="1" toggleGroup="$menuSelection">
        </RadioButton>
        <RadioButton fx:id="two" text="two" GridPane.columnIndex="0" GridPane.rowIndex="2" toggleGroup="$menuSelection">
        </RadioButton>
        <RadioButton fx:id="three" text="three" GridPane.columnIndex="0" GridPane.rowIndex="3" toggleGroup="$menuSelection">

        </RadioButton>
    </GridPane>

</fx:root>

How do I create a listener in my MenuControl java class that will be able to detect the ToggleGroup selection change, then emit that change to onToggleGroupButtonSelectionChanged="#onChange" so I can handle the button selection from the PrimaryControl java class? I remember doing this type of thing in Angular and it was pretty easy but for some reason, I can't find any information on how to do this is Java. Is this what's known as dependency injection?

I found this link and tried to implement for my problem but unsuccessful as nothing was explained and a different event was being listened for JavaFX 2.0 - create action handler for custom component in FXML.

PrimaryController.java (same as MenuControl.java)

public class PrimaryControl extends VBox {

    public DashboardControl() {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("primary_control.fxml"));
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);

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

    @FXML
    protected void onChange() {
        System.out.println("change activity");
    }
}

Thank you in advance to anyone helping me with this.


Solution

    • First, create an enum that represents the possible selections:
    public enum SelectionMode {
        ONE, TWO, THREE
    }
    
    • Now, add a property to your Menu control that gives other controls the ability to know what is selected without exposing the logic behind it or making it changeable from other controls (using ReadOnlyObjectProperty) :
    private ReadOnlyObjectWrapper<SelectionMode> selectionMode
        = new ReadOnlyObjectWrapper<>(this, "selectionMode");
    
    public final ReadOnlyObjectProperty<SelectionMode> selectionModeProperty() {
        return selectionMode.getReadOnlyProperty();
    }
    
    private final SelectionMode getSelectionMode() {
        return selectionMode.get();
    }
    
    • Bind this property to the selected radio button in the Menu controller class's initialize method. Note that I am using setUserData and getUserData methods, otherwise, you need to need to have an if clause for each radio button you have:
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        one.setUserData(SelectionMode.ONE);
        two.setUserData(SelectionMode.TWO);
        three.setUserData(SelectionMode.THREE);
    
        selectionMode.bind(Bindings.createObjectBinding(() -> {
            Toggle selectedToggle = menuSelection.getSelectedToggle();
            return (SelectionMode) selectedToggle.getUserData();
        }, menuSelection.selectedToggleProperty()));
    }
    
    • Your Primary controller class can access its Menu controller by setting an id in <MenuControl fx:id="menu"/>. Then you can just assign a ChangeListener and get notified every time the selection is changed:
    menu.selectionModeProperty().addListener((observable, oldValue, newValue) -> {
        // write your code here
    });
    

    Note: you can initialize user data in your fxml like this (instead of doing it in initialize method):

    <RadioButton fx:id="one" text="one" toggleGroup="$menuSelection">
        <userData>
            <SelectionMode fx:constant="ONE"/>
        </userData>
    </RadioButton>
    <RadioButton fx:id="two" text="two" toggleGroup="$menuSelection">
        <userData>
            <SelectionMode fx:constant="TWO"/>
        </userData>
    </RadioButton>
    <RadioButton fx:id="three" text="three" toggleGroup="$menuSelection">
        <userData>
            <SelectionMode fx:constant="THREE"/>
        </userData>
    </RadioButton>