javajavafxdesign-patternsfxml

How to Implement Decorator Design Pattern in JavaFX Controllers?


I'm working on a JavaFX project and would like to apply the decorator design pattern to my controllers to enhance their functionality. Specifically, I want to add additional behavior to existing controllers without modifying their code directly.

What would be the best approach to implement the decorator pattern in JavaFX controllers? Are there any specific considerations or best practices when applying this design pattern in the context of JavaFX? Can you provide a simple example or code snippet demonstrating how to create a decorator for a JavaFX controller?

I have different FXML files with Controller with common parts. The Common Parts:

@FXML
public Text Text1;
@FXML
public Text Text2;
@FXML
public TextField ID_IN;//Common ID id input field
@FXML
public Button Save; //Edit and Add Save button
@FXML
public Button Close;


@Override
public void showOutsideElements() {
    ID_IN.setVisible(true);
    Close.setVisible(true);
    Save.setVisible(true);
    Text1.setVisible(true);
    Text2.setVisible(true);
}
@Override
public void hideOutsideElements() {
    ID_IN.setVisible(false);
    Close.setVisible(false);
    Save.setVisible(false);
    Text1.setVisible(false);
    Text2.setVisible(false);
}

I would like to hide and show these elements with the decorated version.

@FXML
public void showOutsideElements() {
    super.showOutsideElements();
    SearchFile.setVisible(true);
    AppRe_IN.setVisible(true);
}
@FXML
public void hideOutsideElements() {
    super.hideOutsideElements();
    SearchFile.setVisible(false);
    AppRe_IN.setVisible(false);
}

My code would be more structured with this design pattern, but it seems like the FXML elements can only be accede by the controller.

Here is the actual code:

This is my Base controller:

package main.tooldatabase.controller;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.text.Text;

import java.sql.SQLException;
import java.util.Optional;


public abstract class  BaseController<T> {
    @FXML
    protected TextField filterField;    //Search field
    //Input field titles
    @FXML
    protected Text Text1;
    @FXML
    protected Text Text2;
    @FXML
    protected Text Text3;
    @FXML
    protected Text Text4;
    @FXML
    protected Text Text5;
    @FXML
    protected Text Text6;
    @FXML
    protected Text Text7;


    //Save button
    @FXML
    protected Button Save; //Edit and Add Save button
    @FXML
    protected Button Close;
    //Tabels
    @FXML
    protected TableView<T> output_table;
    @FXML
    protected TableColumn<T, String> Actions;

    
    protected boolean statusCode;
    protected ObservableList<T> listData;
    protected FilteredList<T> filteredData;
    public BaseController() {
        listData = FXCollections.observableArrayList();
        Actions = new TableColumn<>("Actions");
        Actions.setSortable(false);
        //crudData = data;
    }
    public boolean isStatusCode() {
        return statusCode;
    }

    public void setStatusCode(boolean statusCode) {
        this.statusCode = statusCode;
    }

    protected void showData1(){
        output_table.setItems(filteredData);
    }


    protected void showSaveConfirmation() {
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setTitle("Save Confirmation");
        alert.setHeaderText(null);
        alert.setContentText("Save operation completed successfully!");

        alert.showAndWait();
    }

    protected boolean confirmDelete(T selectedItem) throws SQLException {
        Alert confirmation = new Alert(Alert.AlertType.CONFIRMATION);
        confirmation.setTitle("Delete Confirmation");
        confirmation.setHeaderText("Confirm Deletion");
        confirmation.setContentText("Are you sure you want to delete?");

        Optional<ButtonType> result = confirmation.showAndWait();
        if (result.isPresent() && result.get() == ButtonType.OK) {
            return true;
        }else{
            return false;
        }
    }

    // Abstract method for deletion
    protected abstract void deleteSelectedItem(T selectedItem, String successMessage) throws SQLException;

    // Method to show a success message after an action (deletion, saving, etc.)
    protected void showSuccessMessage(String message) {
        Alert successAlert = new Alert(Alert.AlertType.INFORMATION);
        successAlert.setTitle("Success");
        successAlert.setHeaderText(null);
        successAlert.setContentText(message);

        successAlert.showAndWait();
    }
}

Here is Member Controller:

package main.tooldatabase.controller;

import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.HBox;
import main.tooldatabase.database.implementations.MainTables.implMemberDatab;
import main.tooldatabase.database.implementations.MainTables.implProjectDatab;
import main.tooldatabase.database.implementations.SwitchTables.impM_AssignDatab;
import main.tooldatabase.database.models.MainTables.MemberModel;
import main.tooldatabase.database.models.SwitchTables.M_AssignModel;

import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.sql.SQLException;
import java.util.ResourceBundle;

public class MemberController extends BaseController<MemberModel> implements Initializable {
    @FXML
    private TextField Name_IN;

    @FXML
    private TextField ID_IN;

    @FXML
    private TableColumn<MemberModel, String> ID_OUT;

    @FXML
    private TableColumn<MemberModel, String> Name_OUT;

    @FXML
    private ComboBox<String> Position_IN;

    @FXML
    private TableColumn<MemberModel, String> Position_OUT;
    @FXML
    private ComboBox<String> Project_IN;


    private final implMemberDatab crudData;

    private final implProjectDatab ProjectData;

    private final impM_AssignDatab attach;

    ObservableList<String> PositionList;
    public MemberController() {
        super();
        crudData = new implMemberDatab();
        ProjectData = new implProjectDatab();
        attach = new impM_AssignDatab();

        ID_OUT=new TableColumn<>();
        Name_OUT=new TableColumn<>();
        Position_OUT=new TableColumn<>();
        PositionList= FXCollections.observableArrayList("Position1","Position2","Position3");
    }

    @Override
    protected void deleteSelectedItem(MemberModel selectedItem, String successMessage) throws SQLException {

    }

    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {
        ID_OUT.setCellValueFactory((TableColumn.CellDataFeatures<MemberModel, String> cellData)
                ->cellData.getValue().member_idProperty());
        Name_OUT.setCellValueFactory((TableColumn.CellDataFeatures<MemberModel, String> cellData)
                -> cellData.getValue().nameProperty());

        Position_OUT.setCellValueFactory((TableColumn.CellDataFeatures<MemberModel, String> cellData)
                -> cellData.getValue().positionProperty());

        try {
            filteredData = new FilteredList<>(crudData.getAll(), b -> true);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }

        TableColumn<MemberModel, Object> newColumn = new TableColumn<>("Project");
        newColumn.setCellValueFactory(new PropertyValueFactory<>("propertyName"));

        output_table.getColumns().add(newColumn);
        try {
            ObservableList<String> teszt =ProjectData.getNames();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }


        Actions.setCellValueFactory(new PropertyValueFactory<>("member_id"));

        Actions.setCellFactory(param -> new TableCell<MemberModel, String>() {
            final Button deleteButton = new Button("Delete");
            final Button editButton = new Button("Edit"); // Hozzáadott Edit gomb


            @Override
            protected void updateItem(String item, boolean empty) {
                super.updateItem(item, empty);

                if (empty) {
                    setGraphic(null);
                    setText(null);
                } else {
                    // Ha nem üres a sor, akkor adj hozzá a gombot
                    deleteButton.setOnAction(event -> {
                        MemberModel selectedSample = getTableView().getItems().get(getIndex());
                        try {
                            onDeleteButtonClick(selectedSample);
                        } catch (SQLException e) {
                            throw new RuntimeException(e);
                        }
                    });
                    editButton.setOnAction(event -> {
                        MemberModel selectedSample = getTableView().getItems().get(getIndex());
                        onEditButtonClick(selectedSample); // Kérdéses az onEditButtonClick neve
                    });
                    HBox buttonsContainer = new HBox(deleteButton, editButton);
                    buttonsContainer.setSpacing(10); // Gombok közötti tér
                    setGraphic(buttonsContainer);
                    setText(null);
                }
            }
        });


        // Set the filter Predicate whenever the filter changes.
        filterField.textProperty().addListener((observable, oldValue, newValue) -> {
            filteredData.setPredicate(sample -> {
                if (newValue == null || newValue.isEmpty()) {
                    return true;
                }
                String lowerCaseFilter = newValue.toLowerCase();
                String first = "";
                String second = "";
                String third = "";


                if(sample.getMember_id()!=null) {
                    first = sample.getMember_id().toLowerCase();
                }
                if(sample.getName()!=null) {
                    second = sample.getName().toLowerCase();
                }
                if(sample.getPosition()!=null) {
                    third = sample.getPosition().toLowerCase();
                }
                return first.contains(lowerCaseFilter) || second.contains(lowerCaseFilter)|| third.toLowerCase().contains(lowerCaseFilter);
            });
        });
        output_table.getColumns().add(Actions);
        statusCode = false;
        try {
            Project_IN.setItems(ProjectData.getNames());
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        showData1();

        newColumn.setCellValueFactory(cellData -> {
            try {
                return new ReadOnlyObjectWrapper<>(
                        ProjectData.getProject(cellData.getValue().getMember_id())
                        );
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        });
        output_table.getSelectionModel().clearSelection();
        Position_IN.setItems(PositionList);
        hideOutsideElements();

    }
    private void autoId() throws SQLException {
        MemberModel m = new MemberModel();
        crudData.autoId(m);
        ID_IN.setText(m.getMember_id());
    }
    @FXML
    void AddItem(ActionEvent event) throws SQLException {
        showOutsideElements();
        statusCode = true;
        autoId();
        filterField.setText("");
    }

    @FXML
    void OnClose(ActionEvent event) {
        hideOutsideElements();
    }

    @FXML
    void OnSave(ActionEvent event) throws SQLException, InvocationTargetException, IllegalAccessException {
        MemberModel ToSave = new MemberModel();
        M_AssignModel ToAssign = new M_AssignModel();
        ToSave.setMember_id(ID_IN.getText());
        ToSave.setName(Name_IN.getText());
        ToSave.setPosition(Position_IN.getValue());
        ToAssign.setMember_id(ID_IN.getText());
        ToAssign.setProject_id(ProjectData.getProjectID(Project_IN.getValue()));

        if(statusCode){
            crudData.insert(ToSave);
            attach.insert(ToAssign);

        }else {
            crudData.update(ToSave);
            if(attach.IsThereRecord(ToAssign)){
                attach.update(ToAssign);
            }else {
                attach.insert(ToAssign);
            }
        }
        hideOutsideElements();
        output_table.setItems(crudData.getAll());
        filterField.setText("");
    }

    private void onEditButtonClick(MemberModel selectedSample) {
        showOutsideElements();
        //Project_IN.setItems(ProjData.getProjectNames());
        ID_IN.setText(selectedSample.getMember_id());
        Name_IN.setText(selectedSample.getName());
        Position_IN.setItems(PositionList);
        Position_IN.setValue(selectedSample.getPosition());
        statusCode = false;
    }

    private void onDeleteButtonClick(MemberModel selectedSample) throws SQLException {
        M_AssignModel connect = new M_AssignModel();
        connect.setMember_id(selectedSample.getMember_id());
        attach.delete(connect);
        crudData.delete(selectedSample);
        filterField.setText("");
        hideOutsideElements();
        output_table.setItems(crudData.getAll());
    }

    @FXML
    void hideOutsideElements() {
        Name_IN.setVisible(false);
        Position_IN.setVisible(false);
        ID_IN.setVisible(false);
        Project_IN.setVisible(false);
        Close.setVisible(false);
        Save.setVisible(false);
        Text1.setVisible(false);
        Text2.setVisible(false);
        Text3.setVisible(false);
        Text6.setVisible(false);
    }
    @FXML
    void showOutsideElements() {
        Name_IN.setVisible(true);
        Position_IN.setVisible(true);
        ID_IN.setVisible(true);
        ID_IN.setDisable(true);
        Project_IN.setVisible(true);
        Close.setVisible(true);
        Save.setVisible(true);
        Text1.setVisible(true);
        Text2.setVisible(true);
        Text3.setVisible(true);
        Text6.setVisible(true);
    }

}

Assembly Controller:

package main.tooldatabase.controller;

import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.stage.FileChooser;
import main.tooldatabase.database.implementations.MainTables.implAssemblyDatab;
import main.tooldatabase.database.models.MainTables.AssemblyModel;
import main.tooldatabase.interfaces.interCRUD;

import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.sql.SQLException;
import java.util.ResourceBundle;

    public class AssemblyController extends BaseController<AssemblyModel> implements Initializable {

        @FXML
        private TextField AppRe_IN;

        @FXML
        private TableColumn<AssemblyModel, String> AppRe_OUT;

        @FXML
        private Button Close;

        @FXML
        private Button SearchFile;

        @FXML
        private TextField ID_IN;

        @FXML
        private TableColumn<AssemblyModel, String> ID_OUT;


        //@FXML
       // private ProgressBar progressBar; // Make sure to link ProgressBar in your FXML file

        ObservableList<String> PhaseList;
        private final interCRUD crudData = new implAssemblyDatab();
        public AssemblyController() {
            super();
            ID_OUT=new TableColumn<>();
            AppRe_OUT=new TableColumn<>();
            FileChooser fileChooser = new FileChooser();
        }

        @Override
        public void initialize(URL url, ResourceBundle resourceBundle) {
            ID_OUT.setCellValueFactory((TableColumn.CellDataFeatures<AssemblyModel, String> cellData)
                    ->cellData.getValue().assembly_idProperty());
            AppRe_OUT.setCellValueFactory((TableColumn.CellDataFeatures<AssemblyModel, String> cellData)
                    -> cellData.getValue().application_releaseProperty());

            try {
                filteredData = new FilteredList<>(crudData.getAll(), b -> true);
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }

            Actions.setCellValueFactory(new PropertyValueFactory<>("assembly_id"));

            Actions.setCellFactory(param -> new TableCell<AssemblyModel, String>() {
                final Button deleteButton = new Button("Delete");
                final Button editButton = new Button("Edit"); // Hozzáadott Edit gomb
                final Button openButton = new Button("Open");

                @Override
                protected void updateItem(String item, boolean empty) {
                    super.updateItem(item, empty);

                    if (empty) {
                        setGraphic(null);
                        setText(null);
                    } else {
                        // Ha nem üres a sor, akkor adj hozzá a gombot
                        deleteButton.setOnAction(event -> {
                            AssemblyModel selectedSample = getTableView().getItems().get(getIndex());
                            try {
                                deleteSelectedItem(selectedSample,"Delete succes!");
                            } catch (SQLException e) {
                                throw new RuntimeException(e);
                            }
                        });
                        editButton.setOnAction(event -> {
                            AssemblyModel selectedSample = getTableView().getItems().get(getIndex());
                            onEditButtonClick(selectedSample); // Kérdéses az onEditButtonClick neve
                        });
                        openButton.setOnAction(event -> {
                            AssemblyModel selectedSample = getTableView().getItems().get(getIndex());
                            File pdfFile = new File(selectedSample.getApplication_release());
                            if (pdfFile != null) {
                                try {
                                    Desktop.getDesktop().open(pdfFile);
                                } catch (IOException e) {
                                    e.printStackTrace();
                                    // Handle the exception (e.g., show an alert to the user)
                                }
                            } else {
                                System.out.println("No PDF file selected.");
                                // Optionally, show an alert or message to the user
                            }
                        });
                        HBox buttonsContainer = new HBox(openButton,deleteButton, editButton);
                        buttonsContainer.setSpacing(10); // Gombok közötti tér
                        setGraphic(buttonsContainer);
                        setText(null);
                    }
                }

            });

            // Set the filter Predicate whenever the filter changes.
            filterField.textProperty().addListener((observable, oldValue, newValue) -> {
                filteredData.setPredicate(sample -> {
                    if (newValue == null || newValue.isEmpty()) {
                        return true;
                    }
                    String lowerCaseFilter = newValue.toLowerCase();
                    String first = sample.getAssembly_id().toLowerCase();
                    String second = sample.getApplication_release().toLowerCase();

                    return first.contains(lowerCaseFilter) || second.contains(lowerCaseFilter);
                });
            });
            output_table.getColumns().add(Actions);
            statusCode = false;
            showData1();
            output_table.getSelectionModel().clearSelection();
            hideOutsideElements();
        }

        private void autoId() throws SQLException {
            AssemblyModel m = new AssemblyModel();
            crudData.autoId(m);
            ID_IN.setText(m.getAssembly_id());
        }

        @FXML
        void AddItem(ActionEvent event) throws SQLException {
            showOutsideElements();
            //Phase_IN.setItems(PhaseList);
            //Project_IN.setItems(ProjData.getProjectNames());
            statusCode = true;
            autoId();
            filterField.setText("");
        }
        @FXML
        void OnClose(ActionEvent event) {
            hideOutsideElements();
        }

        @FXML
        void OnSave(ActionEvent event) throws SQLException, InvocationTargetException, IllegalAccessException {
            AssemblyModel ToSave = new AssemblyModel();
            ToSave.setAssembly_id(ID_IN.getText());
            ToSave.setApplication_release(AppRe_IN.getText());
            if(statusCode){
                crudData.insert(ToSave);
            }else {
                crudData.update(ToSave);
            }
            hideOutsideElements();
            output_table.setItems(crudData.getAll());
            filterField.setText("");
            showSaveConfirmation();
        }
       
        @FXML
        void ChooseFile(ActionEvent event) {
            FileChooser fileChooser = new FileChooser();
            fileChooser.setTitle("Choose a PDF File");

            // Állítsd be a megengedett fájltípusokat, ha szükséges
            fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("PDF Files", "*.pdf"));

            File selectedFile = fileChooser.showOpenDialog(null);


            if (selectedFile != null) {
                // Itt megteheted, amit szeretnél a kiválasztott fájllal
                System.out.println("Selected File: " + selectedFile.getAbsolutePath());
                AppRe_IN.setText(selectedFile.getAbsolutePath());
                // További logika a kiválasztott fájllal való munkához
            }
        }

        private void onEditButtonClick(AssemblyModel selectedSample) {
            showOutsideElements();
            //Project_IN.setItems(ProjData.getProjectNames());
            ID_IN.setText(selectedSample.getAssembly_id());
            AppRe_IN.setText(selectedSample.getApplication_release());
            statusCode = false;
        }

        private void onDeleteButtonClick(AssemblyModel selectedSample) throws SQLException {
            crudData.delete(selectedSample);
            filterField.setText("");
            hideOutsideElements();
            output_table.setItems(crudData.getAll());
        }

        @Override
        protected void deleteSelectedItem(AssemblyModel selectedItem, String successMessage) throws SQLException {
            // Perform the deletion
            if(confirmDelete(selectedItem)) {
                crudData.delete(selectedItem);

                // Show success message after deletion
                showSuccessMessage(successMessage);
            }
            // Refresh the table or update data after deletion if needed
            output_table.setItems(crudData.getAll());
        }
        @FXML
        void hideOutsideElements() {
            ID_IN.setVisible(false);
            AppRe_IN.setVisible(false);
            Close.setVisible(false);
            Save.setVisible(false);
            SearchFile.setVisible(false);
            Text1.setVisible(false);
            Text6.setVisible(false);
        }
        @FXML
        void showOutsideElements() {
            ID_IN.setVisible(true);
            AppRe_IN.setVisible(true);
            AppRe_IN.setDisable(true);
            Close.setVisible(true);
            Save.setVisible(true);
            SearchFile.setVisible(true);
            Text1.setVisible(true);
            Text6.setVisible(true);
        }
    }

I would like to create different decorator for different controller functionalites like Choose File and extend the show and hide method with controller specific elements.

I've researched the decorator pattern in general, but I'm looking for guidance on how to apply it effectively in the context of JavaFX. Any insights or code examples would be greatly appreciated!


Solution

  • FWIW, here is an example of applying a Decorator pattern to decorate text in JavaFX.

    The purpose of the example is to demonstrate how to apply the Decorator pattern, not to demonstrate the best way to style text in JavaFX (that would be with style classes and CSS rules defined in a stylesheet).

    pattern

    In the example code provided below, the name mapping is:

    Component -> Message
    ConcreteComponent -> SimpleMessage
    Decorator -> MessageDecorator
    ConcreteDecorator -> BoldMessageDecorator
    ConcreteDecorator -> ItalicMessageDecorator
    
    operation() -> String getCSSRules();
    operation() -> void applyStyle(String cssRules);
    operation() -> StringProperty textProperty();
    operation() -> Node getNode();
    

    Perhaps if you look at the example it might provide you with some insight on if, when, and how you might wish to apply the Decorator pattern to future projects.

    I'll provide the code without a lot of commentary. If you wish to learn more about the decorator pattern then there are many resources on the web you can learn from. The code itself is a straight implementation of the structure and hierarchy outlined in the linked Wikipedia article on the topic. The code also applies some JavaFX capabilities (e.g. CSS, scene graph rendering, and properties).

    styled text

    The app applies various decorators to a text label node to achieve different style combinations based on user interaction with the style toolbar in the app.

    Message.java

    import javafx.beans.property.StringProperty;
    import javafx.scene.Node;
    
    public interface Message {
        String getCSSRules();
        void applyStyle(String cssRules);
        StringProperty textProperty();
        Node getNode();
    }
    

    SimpleMessage.java

    import javafx.beans.property.StringProperty;
    import javafx.scene.Node;
    import javafx.scene.control.Label;
    
    public class SimpleMessage implements Message {
        private static final String DEFAULT_STYLE = """
                -fx-font-family: "sans-serif";
                -fx-font-size: 25px;
                -fx-text-alignment: center;
                -fx-wrap-text: true;
                -fx-text-fill: crimson;
                -fx-background-color: lavenderblush;
                """;
    
        private static final String CSS_DATA_URL = "data:text/css,";
        private static final String CSS_TEMPLATE = CSS_DATA_URL + // language=CSS
                """
                .message {
                    %s
                }
                """;
    
        private final Label label = new Label();
    
        public SimpleMessage(String text) {
            label.setText(text);
            label.getStyleClass().add("message");
        }
    
        @Override
        public String getCSSRules() {
            return DEFAULT_STYLE;
        }
    
        @Override
        public void applyStyle(String cssRules) {
            System.out.println("Applying\n" + cssRules);
            label.getStylesheets().setAll(
                    CSS_TEMPLATE.formatted(
                            cssRules
                    )
            );
        }
    
        @Override
        public StringProperty textProperty() {
            return label.textProperty();
        }
    
        @Override
        public Node getNode() {
            return label;
        }
    }
    

    MessageDecorator.java

    import javafx.beans.property.StringProperty;
    import javafx.scene.Node;
    
    abstract public class MessageDecorator implements Message {
        private final Message messageToBeDecorated;
    
        public MessageDecorator(Message messageToBeDecorated) {
            this.messageToBeDecorated = messageToBeDecorated;
        }
    
        @Override
        public void applyStyle(String cssRules) {
            messageToBeDecorated.applyStyle(cssRules);
        }
    
        @Override
        public StringProperty textProperty() {
            return messageToBeDecorated.textProperty();
        }
    
        @Override
        public String getCSSRules() {
            return messageToBeDecorated.getCSSRules();
        }
    
        @Override
        public Node getNode() {
            return messageToBeDecorated.getNode();
        }
    }
    

    BoldMessageDecorator.java

    public class BoldMessageDecorator extends MessageDecorator {
        private static final String BOLD_STYLE = "-fx-font-weight: bold;\n";
    
        public BoldMessageDecorator(Message messageToBeDecorated) {
            super(messageToBeDecorated);
        }
    
        @Override
        public String getCSSRules() {
            return super.getCSSRules() + BOLD_STYLE;
        }
    }
    

    ItalicMessageDecorator.java

    public class ItalicMessageDecorator extends MessageDecorator {
        private static final String ITALIC_STYLE = "-fx-font-style: italic;\n";
    
        public ItalicMessageDecorator(Message messageToBeDecorated) {
            super(messageToBeDecorated);
        }
    
        @Override
        public String getCSSRules() {
            return super.getCSSRules() + ITALIC_STYLE;
        }
    }
    

    DecoratedApp

    import javafx.application.Application;
    import javafx.beans.property.ObjectProperty;
    import javafx.beans.property.SimpleObjectProperty;
    import javafx.geometry.Insets;
    import javafx.scene.Scene;
    import javafx.scene.control.Toggle;
    import javafx.scene.control.ToggleButton;
    import javafx.scene.control.ToolBar;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.StackPane;
    import javafx.stage.Stage;
    
    import java.util.List;
    
    public class DecoratorApp extends Application {
        private static final String SAMPLE_TEXT =
                "Lasciate ogne speranza, voi ch'intrate";
    
        private static final String CSS_DATA_URL = "data:text/css,";
        private static final String CSS = CSS_DATA_URL + // language=CSS
                """
                .root {
                    -fx-font-size: 20px;
                }
                
                .bold {
                    -fx-font-weight: bold;
                    -fx-font-family: "sans-serif";
                }
                .italic {
                    -fx-font-style: italic;
                    -fx-font-family: "sans-serif";
                }
                """;
    
        private final Message sampleMessage = new SimpleMessage(
                SAMPLE_TEXT
        );
        private final ObjectProperty<Message> message = new SimpleObjectProperty<>(
                sampleMessage
        );
    
        private final ToggleButton bold = new ToggleButton("B");
        private final ToggleButton italic = new ToggleButton("i");
    
        private final List<Toggle> toggles = List.of(bold, italic);
    
        @Override
        public void start(Stage stage) {
            bold.getStyleClass().add("bold");
            italic.getStyleClass().add("italic");
    
            bold.selectedProperty().addListener(o ->
                applyStyles()
            );
    
            italic.selectedProperty().addListener(o ->
                applyStyles()
            );
    
            ToolBar toolBar = new ToolBar(
                    bold,
                    italic
            );
    
            StackPane content = new StackPane(
                    message.get().getNode()
            );
            message.addListener(o ->
                    content.getChildren().setAll(
                            message.get().getNode()
                    )
            );
    
            content.setPadding(
                    new Insets(10)
            );
            content.setPrefSize(
                    300, 100
            );
    
            BorderPane layout = new BorderPane();
            layout.setTop(toolBar);
            layout.setCenter(content);
    
            applyStyles();
    
            Scene scene = new Scene(layout);
            scene.getStylesheets().add(CSS);
            stage.setScene(scene);
            stage.show();
        }
    
        private void applyStyles() {
            List<Toggle> selectedToggles = toggles.stream()
                    .filter(Toggle::isSelected)
                    .toList();
    
            System.out.println(
                    selectedToggles.stream()
                            .map(toggle ->
                                    ((ToggleButton) toggle).getText()
                            ).toList()
            );
    
            Message styledMessage = sampleMessage;
            if (selectedToggles.contains(bold)) {
                styledMessage = new BoldMessageDecorator(styledMessage);
            }
            if (selectedToggles.contains(italic)) {
                styledMessage = new ItalicMessageDecorator(styledMessage);
            }
            styledMessage.applyStyle(styledMessage.getCSSRules());
    
            message.set(styledMessage);
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    On Application to FXML and FXML controllers

    I won't even try to extend this to work with FXML and controllers (I am not sure that would be such a good idea in any case) or to adapt the sample code from your question. You can review the comments on the question for some thoughts on applying the Decorator pattern with FXML and controllers.