FxControllerBridge – Micro Cross-Controller @FXML Injection Helper

Sometimes one likes to build complex JavaFX screen without decomposing them into multiple FXML files. This is especially true when using the SceneBuilder tool. The problem in this case is however that you are only allowed to have one single JavaFX controller declared, which will get the components injected and event handler invoked as they are defined in the FXML file.

In this case, this FXML controller injection bridge may come handy. This class is basically a micro FXML injection framework. It allows “to bridge” the injection from the main controller into sub-controllers without the need to pass chains of component references around. When it comes to event handlers, the bridge also allows to forward the invocation.

Before you shout STOOOOOP – Yes, the choice of being able to inject components of one FXML file into only one controller is made by design by the JavaFX team. The reason is that developers are encouraged to define a controller per “component” (in the larger sense). The FXML files should be modular, and composed for instance using “fx:include“. While this is definitively the right thing to do, sometimes you just don’t want to :)

So, this’s how it’s used:

import ....FxControllerBridge;

/**
 * Main FX Controller
 */
public class MainController {

    @FXML private MenuItem openMenu;
    @FXML private TreeView<MyTreeNode> sourceTree;
    @FXML private Button sourceDownloadButton;

    // The scope of the injections is the 'interlacing' with the instance
    private final FxControllerBridge fxControllerBridge=new FxControllerBridge();

    /**
     * JavaFX callback to initialise the controller class.
     */
    public void initialize() {

        FxControllerBridge.debug=false;
        fxControllerBridge.interlace(this);
        // |
        // |___ All @FXML members are detected

        statusSubController=new StatusPanesSubController(fxControllerBridge);
        statusSubController.initialize();

        sourcesSubController=new SourcesSubController(fxControllerBridge);
        sourcesSubController.initialize();
    }

    @FXML 
    void onMenuOpen(ActionEvent event) { 
         fxControllerBridge.event(); 
         // |
         // |___ Forwards the event to a @BridgedFXML annotated method
         //      that has the same name, in whatever controller interlaced
         //      with the above FxControllerBridge instance.
    }
    
    @FXML void onSourceDownloadButton(ActionEvent event) { 
         fxControllerBridge.event(); 
    }

}

The DI is made based on the control member name or event handler method name (<3 Angular ^^), so these choices have to be unique in the group of classes the bridge bridges. The target injection points are annotated with @BridgedFXML (instead if @FXML). This is not strictly needed as the name would be enough to identify the injection point, but the annotation improves the readability in terms of recognising “what is injected where”.

Setting the static debug flag to TRUE makes the class write all detections, injections and forwards to stdout.

import ....FxControllerBridge;
import ....FxControllerBridge.BridgedFXML;

 /**
  * A sub-controller...
  */
 public class SourcesSubController  {
  
     @BridgedFXML private MenuItem openMenu;
     @BridgedFXML private TreeView<TreeNode> sourceTree;
     @BridgedFXML private Button sourceDownloadButton;
     
     public SourcesSubController(FxControllerBridge fxControllerBridge) {
 
         fxControllerBridge.interlace(this);
         // |
         // |___ All @BridgedFXML members are injected based on name.
         //      The scope is the classes interlaced with the received
         //      FxControllerBridge instance.
     }
 
     public void initialize() {
     }
     
     @BridgedFXML
     private void onSourceDownloadButton()  {
         // Handle
         // The 
         // Event
     }

Now the code of the FxControllerBridge. I have removed all debug output statements to make the code more compact. You can download the fully operational class from https://github.com/imifos/FxControllerBridge.

package pro.carl.fxcontrollerbridge;

import javafx.fxml.FXML;
import javafx.util.Pair;

import java.lang.annotation.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;


/**
 * JavaFX Controller Bridge
 * Smart helper to deal with JavaFX sub-controllers.
 *
 * By @imifos / https://github.com/imifos/FxControllerBridge
 *
 * License:
 * The author accepts no liability for damage caused by this code. If you do
 * not accept this condition then you are prohibited from using this tool.
 *
 * In all other respects the Apache License version 2 applies.
 * *
 * This code is free software; you can redistribute it and/or modify
 * it under the terms of the Apache License version 2 as
 * published by the Apache Foundation.
 *
 * This code is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 */
public class FxControllerBridge {

    public static boolean debug=X; // Stripped - see github for complete class

    // The BridgedFXML annotation
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.METHOD})
    public @interface BridgedFXML {
    }


    // Caches @FXML annotated member <name,value> (value = reference of the FX control instance)
    private final Map<String,Object> fxmlMembers=new HashMap<>();

    // Caches the @BridgeFXML annotated event handler names, and the controller where found
    private final List<Pair<String,Object>> fxmlMethods=new ArrayList<>();


    /**
     * Does the magic...
     */
    public void interlace(Object fxController) {
        scanForFXMLMembers(fxController);
        injectIntoBridgedFXMLMembers(fxController);
        scanForBridgeFXMLEventHandlers(fxController);
    }



    /**
     * Must be called when the original FXML event handler is invoked.
     * Searches all registered bridge-event handlers and invokes all sub-controller handlers based on the method name.
     */
    public void event() {

        try {
            // Obtain the name of the caller which should be the original event handler
            StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
            String callerMethod = stackTrace[2].getMethodName();

            // Invoke all known instances, annotated with @BridgedFXML
            for (Pair<String, Object> eventHandler : fxmlMethods) {

                if (eventHandler.getKey().equals(callerMethod)) {

                    Method method=eventHandler.getValue().getClass().getDeclaredMethod(eventHandler.getKey());
                    method.setAccessible(true);

                    method.invoke(eventHandler.getValue());
                }
            }
        }
        catch(Exception e) {
            throw new RuntimeException(e);
        }
    }



    /**
     * Loads all class meta information of event handlers annotated with 'BridgeFXML' into the internal map.
     */
    private void scanForBridgeFXMLEventHandlers(Object fxController) {

        try {
            for (Method method : fxController.getClass().getDeclaredMethods()) {
                if (method.getAnnotation(BridgedFXML.class)!=null) {
                    fxmlMethods.add(new Pair(method.getName(),fxController));
                }
            }
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }




    /**
     * Loads all values of class members annotated with 'FXML' injection into the internal map.
     */
    private void scanForFXMLMembers(Object fxController) {

        try {
            for (Field field : fxController.getClass().getDeclaredFields()) {

                field.setAccessible(true); // overwrite 'privacy'...

                if (fxmlMembers.containsKey(field.getName()) && field.getAnnotation(BridgedFXML.class)==null) {
                    // Most common reason is a @FXML tag in a sub-controller instead the @BridgedFXML annotation
                    // Warning: Conflict / Field already loaded from another controller - see github
                }

                Object value = field.get(fxController);
                if (value!=null && field.getAnnotation(FXML.class)!=null) {
                    fxmlMembers.put(field.getName(), value);
                }
            }
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }



    /**
     * Injects FXML member values into 'interlaced' class members that are annotated with 'BridgedFXML'.
     */
    private void injectIntoBridgedFXMLMembers(Object fxController) {

        try {
            for (Field field : fxController.getClass().getDeclaredFields()) {

                Object value=fxmlMembers.get(field.getName());
                if (value!=null) {

                    if (field.getAnnotation(BridgedFXML.class)!=null) {
                        field.setAccessible(true);
                        field.set(fxController, value);
                    }
                    else {
                        // Warning: Identified possible injection point without @BridgedFXML annotation
                    }

                } // value!=null
            } // for field
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *