Home > other >  How to manually instantiated FXML controller with FXrouter, or another way?
How to manually instantiated FXML controller with FXrouter, or another way?

Time:12-28

Good afternoon all. I am trying to initiate a controller to an FXML page, but not sure how. I am currently using the FXRouter GitHub API.

I would like to bind controller pages to FXML pages OUTSIDE of the FXML file. I have a "LoadNewRoute" Method below that currently does it, but I would like to improve this, so it will assign the Controllers to FXML pages outside. What is the best way to do this?

Method to Load the page

private static void loadNewRoute(RouteScene route) throws IOException {
        // get Main Class package name to get correct files path

        String pathRef = mainRef.getClass().getPackage().getName();


        // set FXRouter current route reference
        currentRoute = route;

        // create correct file path.  "/" doesn't affect any OS
        String scenePath = "/"   pathRef   "/"   route.scenePath;


        // load .fxml resource
        FXMLLoader loader = new FXMLLoader(new Object() { }.getClass().getResource(scenePath));
        Parent resource = loader.load();




      //  resource.setController(BuyController);

        // set window title from route settings or default setting
        window.setTitle(route.windowTitle);
        // set new route scene
        window.setScene(new Scene(resource, route.sceneWidth, route.sceneHeight));
        // show the window
        window.show();

        // set scene animation
        routeAnimation(resource);
    }

FXML page-

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.text.Font?>

<AnchorPane xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.gradle.Controllers.BuyController" >

    <MenuBar layoutX="676.0" layoutY="2.0">
        <Menu mnemonicParsing="false" text="Pages">
            <MenuItem mnemonicParsing="false" onAction="#SwitchToMainScene" text="Home" />
            <MenuItem mnemonicParsing="false" onAction="#SwitchToBuyScene" text="Buy" />
            <MenuItem mnemonicParsing="false" onAction="#SwitchToSellScene" text="Sell" />
        </Menu>
        <Menu mnemonicParsing="false" text="Options">
            <MenuItem mnemonicParsing="false" onAction="#CloseApp" text="Exit" />
            <MenuItem mnemonicParsing="false" onAction="#SwitchToProfileScene" text="Profile" />
        </Menu>
    </MenuBar>
    <Pane layoutX="14.0" layoutY="67.0" prefHeight="200.0" prefWidth="200.0">
        <CheckBox layoutX="7.0" layoutY="116.0" mnemonicParsing="false" prefHeight="17.0" prefWidth="82.0" text="DogeCoin" />
        <CheckBox layoutX="7.0" layoutY="153.0" mnemonicParsing="false" prefHeight="17.0" prefWidth="72.0" text="Bitcoin" />
        <CheckBox layoutX="7.0" layoutY="83.0" mnemonicParsing="false" text="Etherium" />
        <Label layoutX="27.0" layoutY="26.0" text="Select your CryptoCurrency" />
    </Pane>
    <Pane layoutX="528.0" layoutY="-1.0" prefHeight="37.0" prefWidth="272.0">
        <Button layoutX="222.0" layoutY="-1.0" mnemonicParsing="false" onAction="#CloseApp" prefHeight="30.0" prefWidth="50.0" text="Exit" />
        <Button layoutX="166.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToProfileScene" prefHeight="30.0" prefWidth="56.0" text="Profile" />
        <Button layoutX="118.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToSellScene" prefHeight="30.0" prefWidth="48.0" text="Sell" />
        <Button layoutX="70.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToBuyScene" prefHeight="30.0" prefWidth="48.0" text="Buy" />
        <Button layoutX="14.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToMainScene" prefHeight="30.0" prefWidth="56.0" text="Home" />
    </Pane>
    <Pane layoutX="300.0" layoutY="88.0" prefHeight="200.0" prefWidth="200.0">
        <RadioButton layoutX="13.0" layoutY="38.0" mnemonicParsing="false" text="Amount in coin" />
        <RadioButton layoutX="13.0" layoutY="67.0" mnemonicParsing="false" text="Amount in cash" />
        <TextField layoutX="13.0" layoutY="139.0" />
        <Label layoutX="-1.0" prefHeight="34.0" prefWidth="200.0" text="Choose type of buying, and enter amount" wrapText="true" />
    </Pane>
    <Button layoutY="-1.0" mnemonicParsing="false" onAction="#BackButton" prefHeight="30.0" prefWidth="64.0" text="Back" />
    <Button layoutX="237.0" layoutY="447.0" mnemonicParsing="false" onAction="#BuyButton" prefHeight="82.0" prefWidth="278.0" text="Buy">
        <font>
          <Font size="31.0" />
        </font>
     </Button>
</AnchorPane>

Controller-

   package com.gradle.Controllers;
   import com.gradle.Controller;
  import com.gradle.FXRouter;
  import javafx.event.ActionEvent;
  import javafx.fxml.FXMLLoader;

  import java.io.IOException;

  public class BuyController extends com.gradle.Controller {

  public void example () {

  }

    

Home Controller

public class HomeController extends com.gradle.Controller {


    public void SwitchToMainScene (ActionEvent event) throws IOException {
        FXRouter.goTo("Home", new HomeController());}

    public void SwitchToBuyScene (ActionEvent event) throws IOException {
        FXRouter.goTo("Buy", new BuyController());}

    public void SwitchToSellScene (ActionEvent event) throws IOException {
        FXRouter.goTo("Sell", new SellController());}

    public void SwitchToProfileScene (ActionEvent event) throws IOException {
        FXRouter.goTo("Profile", new ProfileController());}

    @Override
    public void initilize(URL url, ResourceBundle rb) {

    }
}

call-

 FXRouter.goTo("Home", new HomeController());

FXML-

  <Button fx:id="exit" layoutX="222.0" layoutY="-1.0" mnemonicParsing="false" onAction="#CloseApp" prefHeight="30.0" prefWidth="50.0" text="Exit" />
        <Button layoutX="166.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToProfileScene" prefHeight="30.0" prefWidth="56.0" text="Profile" />
        <Button layoutX="118.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToSellScene" prefHeight="30.0" prefWidth="48.0" text="Sell" />
        <Button layoutX="70.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToBuyScene" prefHeight="30.0" prefWidth="48.0" text="Buy" />
        <Button layoutX="14.0" layoutY="-1.0" mnemonicParsing="false" onAction="#SwitchToMainScene" prefHeight="30.0" prefWidth="56.0" text="Home" />
    </Pane>
   <Button layoutY="-1.0" mnemonicParsing="false" onAction="#BackButton" prefHeight="30.0" prefWidth="64.0" text="Back" />
</AnchorPane>

All of the onAction are giving me errors that it cannot find the calls

CodePudding user response:

Without FXMLRouter

A standard way to manually configure a controller for an FXML (without the FXMLRouter library) is demonstrated in the answer to this question:

Note that if you set the controller manually, you should not also provide a fx:controller attribute in your fxml file.

If you are not using the FXMLRouter library, then just follow the example in the answer to the linked question and ignore the rest of this answer.


With FXMLRouter

I looked at the code, FXMLRouter can't do what you want.

It uses a static load function. If you want to set a controller you created, then you must use a loader instance as demonstrated in the prior referenced answer.

The whole router thing is just one class.

If you want to use it:

  1. Copy the source code into your project.

  2. Modify the loadNewRoute method (and the methods calling it) to pass in a controller you created.

    • If you want to retain the current public interfaces, you could do that and also add new overloaded interfaces which also accept your locally constructed controller class.
  3. If manually setting a controller, change the implementation to create a new FXMLLoader and set the controller on the new loader instance rather than using the static load method.

The required modifications to the loading implementation are demonstrated in the previously referenced answer.

Or, you can modify the FXMLLoader code to use a custom FXML controller factory method as discussed elsewhere in this answer.

Example FXMLRouter Implementation

This is not my code, I have modified it though as per previous modification suggestions in this answer, original code and the license info are at:

I have not tested these modifications at all, and provide no guarantee that they would work. The code is just provided as an example.

import java.io.IOException;
import java.util.AbstractMap;
import java.util.HashMap;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.Parent;
import javafx.util.Duration;
import javafx.animation.FadeTransition;

/**
 * FXRouter allows to manage scenes switching on JavaFX Application with an easy API
 * Inspired by Angular JS $routeProvider
 * @author Marco Trombino
 * @version 1.0.0
 */
public final class FXRouter {
    private static final String WINDOW_TITLE = "";
    private static final Double WINDOW_WIDTH = 800.0;
    private static final Double WINDOW_HEIGHT = 600.0;
    private static final Double FADE_ANIMATION_DURATION = 800.0;

    // FXRouter Singleton
    private static FXRouter router;
    // FXRouter Main Class reference to get main package
    private static Object mainRef;
    // FXRouter Application Stage reference to set scenes
    private static Stage window;

    // Application Stage title
    private static String windowTitle;
    // Application Stage size
    private static Double windowWidth;
    private static Double windowHeight;

    // routes switching animation
    private static String animationType;
    private static Double animationDuration;

    // FXRouter routes map
    private static AbstractMap<String, RouteScene> routes = new HashMap<>();
    // FXRouter current route
    private static RouteScene currentRoute;

    /**
     * FXRouter Inner Class used into routes map
     */
    private static class RouteScene {
        // route .fxml Scene path
        private String scenePath;
        // Scene (Stage) title
        private String windowTitle;
        private double sceneWidth;
        private double sceneHeight;
        // route data passed from goTo()
        private Object data;

        private RouteScene(String scenePath) {
            this(scenePath, getWindowTitle(), getWindowWidth(), getWindowHeight());
        }

        private RouteScene(String scenePath, String windowTitle) {
            this(scenePath, windowTitle, getWindowWidth(), getWindowHeight());
        }

        private RouteScene(String scenePath, double sceneWidth, double sceneHeight) {
            this(scenePath, getWindowTitle(), sceneWidth, sceneHeight);
        }

        /** Route scene constructor
         * @param scenePath: .FXML scene file
         * @param windowTitle: Scene (Stage) title
         * @param sceneWidth: Scene Width
         * @param sceneHeight: Scene Height
         */
        private RouteScene(String scenePath, String windowTitle, double sceneWidth, double sceneHeight) {
            this.scenePath = scenePath;
            this.windowTitle = windowTitle;
            this.sceneWidth = sceneWidth;
            this.sceneHeight = sceneHeight;
        }

        private static String getWindowTitle() {
            return FXRouter.windowTitle != null ? FXRouter.windowTitle : WINDOW_TITLE;
        }

        private static double getWindowWidth() {
            return FXRouter.windowWidth != null ? FXRouter.windowWidth : WINDOW_WIDTH;
        }

        private static double getWindowHeight() {
            return FXRouter.windowHeight != null ? FXRouter.windowHeight : WINDOW_HEIGHT;
        }
    }

    /**
     * FXRouter constructor kept private to apply Singleton pattern
     */
    private FXRouter() {}

    public static void bind(Object ref, Stage win) {
        checkInstances(ref, win);
    }

    public static void bind(Object ref, Stage win, String winTitle) {
        checkInstances(ref, win);
        windowTitle = winTitle;
    }

    public static void bind(Object ref, Stage win, double winWidth, double winHeight) {
        checkInstances(ref, win);
        windowWidth = winWidth;
        windowHeight = winHeight;
    }

    /** FXRouter binder with Application Stage and main package
     * @param ref: Main Class reference
     * @param win: Application Stage
     * @param winTitle: Application Stage title
     * @param winWidth: Application Stage width
     * @param winHeight: Application Stage height
     */
    public static void bind(Object ref, Stage win, String winTitle, double winWidth, double winHeight) {
        checkInstances(ref, win);
        windowTitle = winTitle;
        windowWidth = winWidth;
        windowHeight = winHeight;
    }

    /** set FXRouter references only if they are not set yet
     * @param ref: Main Class reference
     * @param win: Application Stage
     */
    private static void checkInstances(Object ref, Stage win) {
        if(mainRef == null)
            mainRef = ref;
        if(router == null)
            router = new FXRouter();
        if(window == null)
            window = win;
    }

    public static void when(String routeLabel, String scenePath) {
        RouteScene routeScene = new RouteScene(scenePath);
        routes.put(routeLabel, routeScene);
    }

    public static void when(String routeLabel, String scenePath, String winTitle) {
        RouteScene routeScene = new RouteScene(scenePath, winTitle);
        routes.put(routeLabel, routeScene);
    }

    public static void when(String routeLabel, String scenePath, double sceneWidth, double sceneHeight) {
        RouteScene routeScene = new RouteScene(scenePath, sceneWidth, sceneHeight);
        routes.put(routeLabel, routeScene);
    }

    /** Define a FXRouter route
     * @param routeLabel: Route label identifier
     * @param scenePath: .FXML scene file
     * @param winTitle: Scene (Stage) title
     * @param sceneWidth: Scene Width
     * @param sceneHeight: Scene Height
     */
    public static void when(String routeLabel, String scenePath, String winTitle, double sceneWidth, double sceneHeight) {
        RouteScene routeScene = new RouteScene(scenePath, winTitle, sceneWidth, sceneHeight);
        routes.put(routeLabel, routeScene);
    }

    public static void goTo(String routeLabel) throws IOException {
        goTo(routeLabel, null, null);
    }

    public static void goTo(String routeLabel, Object data) throws IOException {
        goTo(routeLabel, data, null);
    }

    /** Switch between FXRouter route and show corresponding scenes
     * @param routeLabel: Route label identifier
     * @param data: Data passed to route
     * @oaram controller: Controller instance to use.
     * @throws Exception: throw FXMLLoader exception if file is not loaded correctly
     */
    public static void goTo(String routeLabel, Object data, Object controller) throws IOException {
        // get corresponding route
        RouteScene route = routes.get(routeLabel);
        // set route data
        route.data = data;
        loadNewRoute(route, controller);
    }

    /** Helper method of goTo() which load and show new scene
     * @throws Exception: throw FXMLLoader exception if file is not loaded correctly
     */
    private static void loadNewRoute(RouteScene route, Object controller) throws IOException {
        // get Main Class package name to get correct files path
        String pathRef = mainRef.getClass().getPackage().getName();

        // set FXRouter current route reference
        currentRoute = route;

        // create correct file path.  "/" doesn't affect any OS
        String scenePath = "/"   pathRef   "/"   route.scenePath;

        // load .fxml resource
        FXMLLoader loader = new FXMLLoader();
        loader.setController(controller);
        Parent resource = loader.load(new Object() { }.getClass().getResource(scenePath));

        // set window title from route settings or default setting
        window.setTitle(route.windowTitle);
        // set new route scene
        window.setScene(new Scene(resource, route.sceneWidth, route.sceneHeight));
        // show the window
        window.show();

        // set scene animation
        routeAnimation(resource);
    }

    /* Syntactic sugar for goTo() method when FXRouter get set */
    public static void startFrom(String routeLabel) throws Exception {
        goTo(routeLabel);
    }

    public static void startFrom(String routeLabel, Object data) throws Exception {
        goTo(routeLabel, data);
    }

    public static void startFrom(String routeLabel, Object data, Object controller) throws Exception {
        goTo(routeLabel, data, controller);
    }

    /** set FXRouter switching animation
     * @param anType: Animation type
     */
    public static void setAnimationType(String anType) {
        animationType = anType;
    }

    /** set FXRouter switching animation
     * @param anType: Animation type
     * @param anDuration: Animation duration
     */
    public static void setAnimationType(String anType, double anDuration) {
        animationType = anType;
        animationDuration = anDuration;
    }

    /** Animate routes switching based on animation type
     */
    private static void routeAnimation(Parent node) {
        String anType = animationType != null ? animationType.toLowerCase() : "";
        switch(anType) {
            case "fade":
                Double fd = animationDuration != null ? animationDuration : FADE_ANIMATION_DURATION;
                FadeTransition ftCurrent = new FadeTransition(Duration.millis(fd), node);
                ftCurrent.setFromValue(0.0);
                ftCurrent.setToValue(1.0);
                ftCurrent.play();
                break;
            default:
                break;
        }
    }

    /** Get current route data
     */
    public static Object getData() {
        return currentRoute.data;
    }

}

Alternate Implementation

Ignore this section if you don't need it. The FXMLRouter is pretty simple and if the modifications meet your requirements, then great.

There are alternate libraries available that accomplish similar things, I won't recommend any here, but I advise you review this tutorial by eden coding:

There is a box in the Eden tutorial titled "Dependency Injector – Full Code", after you have read the tutorial, expand the box and study the code to see if you can understand it, if you can understand it, then you understand advanced usage of the FXMLLoader well, if not, that is OK.

You don't need to use the concepts from it if you don't want to or if what you currently have already suits your purposes. I just provide the tutorial link as a reference as it helps explain some complex topics quite well.

More sophisticated again is to integrate JavaFX with SpringBoot, but that is more complex, so I won't recommend that here unless it was something you really wanted to do to take advantage of other features of SpringBoot.

FAQ

Do you think it would be possible to take out the need for a Controller to be passed, if you used a switch?

If you look at the example code I provided, I already did that.

A switch is not needed, you can just use overloaded methods (which is the way I did it).

If you don't want to pass the controller, then don't use the method which passes the controller.

For example, if you don't want to pass a controller, call goTo(routeLabel) or goTo(routeLabel, data) rather than goTo(routeLabel, data, controller).

The new code is backward compatible with the existing implementation, so if you don't add the extra parameter in the call, no controller will be set (just like how the original code worked).

But if you don't pass a controller, you should specify the controller in the FXML in an fx:controller attribute.

How would you then create the object from the controller classes? > I currently have

public void SwitchToMainScene (ActionEvent event) throws IOException {FXRouter.goTo("Home");}

Would I just add in my BuyController name at the end?

There are two ways you can do this.

The simplest way would be to manually create and pass the controller for each call:

public void switchToMainScene(ActionEvent event) throws IOException {
    FXRouter.goTo(
        "Home", 
        null, 
        new BuyController(
            <whatever parameters you want to pass to your new instance>
        )
    );
}

Note: I changed the case for the method example, always write code that follows the Java naming conventions. If you don't want to pass any parameters to the controller constructor, then don't, in such a case the behavior will be the same as if the FXMLLoader constructed the controller from the fx:controller attribute in the FXML.

The alternate method is to use a controller factory. This is discussed in the eden coding dependency injection tutorial I referred you to previously.

To use a factory with the FXMLRouter, modify the FXMLRouter implementation to:

  1. Create a factory that maps your FXML files to controller instance (either via a stored hash map or a switch statement or using a dependency injection framework).
  2. Set the factory on the FXMLRouter instance (could be statically set for the entire application if you wanted).
  3. Set the controller factory on the loader instance rather than setting the controller instance on the loader instance.

I won't provide code for this here at this time, perhaps you could work out what to do from the Javadoc, the eden tutorial, and the prior example for setting the constructor manually.

Whenever I remove the fx:Controller from my .fxml, all of my onAction refuses to work, even though I have them assigned in the controller.

When you remove the fx:controller reference, the Idea IDE can't work out what controller is associated with an FXML file, and will highlight errors for references to methods it can't resolve for intelligent editing and navigation.

However, at runtime the controller is set and resolves and so do the method references (as long as you have correctly named them, not forgetting the names are case-sensitive), so it is not an actual error, and you can ignore it, though you do lose some of the benefits of Idea's intelligent editing.

Alternately, to allow you to both specify an fx:controller value in the fxml (which retains intelligent editing) and manually configure a controller, you can use a controller factory. There is an example implementation in the answer to:

  • Related