Search code examples
spring-bootjavafxfxmlcode-reuse

JavaFX and Spring - Reusable FXML component


I am building a GUI with Scene Builder in JavaFX and Spring Boot. Several of my scenes have one element in common, which is a simple KPI indicator, called PerformanceBeacon. Depending on the configuration, done by the controller class, the beacon can be used e.g. for project parameters like budget, time and so one. Ideally, even a single scene may have multiple of this elements.

The issue is, having multiple KPI beacons in a single scene does somehow not work, since all embedded controllers are pointing to the same memory and hence, I can only manipulate one - which is the last - KPI beacon.

The snipped from the fxml file looks like this:

...
<VBox layoutX="301.0" layoutY="325.0" spacing="6.0" AnchorPane.leftAnchor="2.0" AnchorPane.topAnchor="24.0">
      <children>
               <fx:include source="PerformanceBeacon.fxml" fx:id="embeddedTimeKPI" />
               <fx:include source="PerformanceBeacon.fxml" fx:id="embeddedBudgetKPI"/>
               <fx:include source="PerformanceBeacon.fxml" fx:id="embeddedResourcesKPI"/>
               <fx:include source="PerformanceBeacon.fxml" fx:id="embeddedScopeKPI"/>
               <fx:include source="PerformanceBeacon.fxml" fx:id="embeddedEffortKPI"/>
               <fx:include source="PerformanceBeacon.fxml" fx:id="embeddedQualityKPI"/>
      </children>
</VBox>
...

The embedded controllers are defined in the controller class of the enclosing UI and the code should "only" set the label of each of the Hyperlinks in each PerformanceBeacon.

@Controller
public class ProductManagerCtrl implements Initializable {
 ...
 @FXML protected PerformanceBeaconCtrl embeddedTimeKPIController;       
 @FXML protected PerformanceBeaconCtrl embeddedBudgetKPIController; 
 @FXML protected PerformanceBeaconCtrl embeddedResourcesKPIController;  
 @FXML protected PerformanceBeaconCtrl embeddedScopeKPIController;      
 @FXML protected PerformanceBeaconCtrl embeddedEffortKPIController;     
 @FXML protected PerformanceBeaconCtrl embeddedQualityKPIController;

 ...

    @Override
    public void initialize(URL arg0, ResourceBundle arg1) {

        ...
        initializePerformanceIndicator();
        ...
     }

    protected void initializePerformanceIndicator() {
        embeddedBudgetKPIController.setHyperlink("Budget", null);
        embeddedScopeKPIController.setHyperlink("Scope", null);
        embeddedTimeKPIController.setHyperlink("Time", null);
        embeddedQualityKPIController.setHyperlink("Quality", null);
        embeddedResourcesKPIController.setHyperlink("Resources", null);
        embeddedEffortKPIController.setHyperlink("Effort estimates", null);
    }

When I debug the code, each of these embedded controllers points to the same memory. So effectively I have one controller and can only manipulate the last element. In the end the last beacon has the "Effort estimates" label and all others are unchanged.

The PerformanceBeacon.fxml is rather simple. I copy it, in case you want to try yourself

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.Double?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Hyperlink?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.shape.Circle?>
<?import javafx.scene.shape.Polygon?>

<GridPane styleClass="pane" stylesheets="@../stylesheets/PerformanceBeacon.css" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.agiletunes.productmanager.controllers.PerformanceBeaconCtrl">
   <children>
      <Circle fx:id="beacon" fill="#1fff4d" radius="10.0" stroke="BLACK" strokeType="INSIDE" GridPane.columnIndex="1" GridPane.valignment="CENTER">
         <GridPane.margin>
            <Insets bottom="12.0" left="12.0" right="6.0" top="12.0" />
         </GridPane.margin>
      </Circle>
      <Polygon fx:id="trendArrow" fill="#11ff1c" rotate="45.0" stroke="#2e229a" strokeType="INSIDE" GridPane.columnIndex="2" GridPane.valignment="CENTER">
        <points>
          <Double fx:value="-10.0" />
          <Double fx:value="10.0" />
          <Double fx:value="-5.0" />
          <Double fx:value="10.0" />
          <Double fx:value="-5.0" />
          <Double fx:value="15.0" />
          <Double fx:value="5.0" />
          <Double fx:value="15.0" />
          <Double fx:value="5.0" />
          <Double fx:value="10.0" />
          <Double fx:value="10.0" />
          <Double fx:value="10.0" />
          <Double fx:value="0.0" />
          <Double fx:value="-10.0" />
        </points>
         <GridPane.margin>
            <Insets bottom="12.0" left="12.0" right="12.0" top="12.0" />
         </GridPane.margin>
      </Polygon>
      <Hyperlink fx:id="hyperlink" onAction="#showDetails" text="Subject">
         <GridPane.margin>
            <Insets left="24.0" />
         </GridPane.margin>
      </Hyperlink>
   </children>
   <columnConstraints>
      <ColumnConstraints hgrow="ALWAYS" maxWidth="1.7976931348623157E308" minWidth="170.0" />
      <ColumnConstraints minWidth="10.0" />
      <ColumnConstraints minWidth="10.0" />
   </columnConstraints>
   <rowConstraints>
      <RowConstraints />
   </rowConstraints>
</GridPane>

And last but not least the Controller of the PerformanceBeacon

@Controller
public class PerformanceBeaconCtrl {

    @FXML   protected Circle beacon;
    @FXML   private Polygon trendArrow;
    @FXML   private Hyperlink hyperlink;

    @FXML
    public void showDetails(ActionEvent event) {
        // TODO
    }

    public void setHyperlink(String label, URL url) {
        Platform.runLater(() -> {
            hyperlink.setText(label);
        });
    }

    public void setBeaconColor(Color color) {
        Platform.runLater(() -> {
            beacon.setFill(color);
        });
    }

    public void setTrend(Double angle) {
        Platform.runLater(() -> {
            trendArrow.rotateProperty().set(angle);
        });
    }

}

I am successfully using embedded UI elements / controller also elsewhere in my code. But never the same element multiple times.

I am not sure whether this is a Spring side effect?

What am I doing wrong? Thank you in advance.


Solution

  • The described observation suggests that the issue was related to a singleton somewhere in the design. My suspicion regarding the Spring influence was right. The ProductManagerCtrl controller class gets created using

    fxmlLoader.setControllerFactory(c -> ProdMgrApp.springContext.getBean(c));
    

    and as it turns out, this approach creates singletons by default. Therefore all embedded controller are also singletons. The way to fix it is to annotate the PerformanceBeaconCtrl controller class also with

    @Scope("prototype")