使用“<fx:include>”时如何将参数传递给控制器​​的构造函数?

How to pass parameters to a controller's constructor when using `<fx:include>`?

我正在学习 JavaFX,我遇到了一个我似乎无法解决的涉及控制器实例化的问题。本质上,我想知道是否可以执行以下操作之一:

  1. <fx:include> 包含 FXML 时将参数传递给控制器​​的构造函数;或
  2. 指定包含 FXML 文件时使用的自定义控制器实例 <fx:include>

请注意,这些问题是相关的。事实上,我问选项 (2) 的原因是因为它可以解决选项 (1)。


我的设置


我有以下 "main" FXML 文件:

<!-- XML declaration, imports, etc. removed for brevity -->
<BorderPane xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml">
    <!-- ... -->
    <center>
        <!-- Note that PageSwitcher is a custom control that is capable of switching between pages — you should be able to ignore it here. -->
        <PageSwitcher fx:id="mainPageSwitcher" currentPageIndex="0">
            <!-- ... -->
            <fx:include source="dashboard.fxml" fx:id="dashboard" />
        </PageSwitcher>
    </center>
</BorderPane>

它有一个关联的控制器,MainPaneController。我不会在这里显示它,但如果需要,我可以。

您可能已经注意到我的主 FXML 文件在其 BorderPane 上没有 fx:controller 属性,尽管我说它有一个关联的控制器。这是因为,在加载我的主应用程序 class 中的主 FXML 页面(即扩展应用程序的 class),以创建我自己的 MainPaneController class 实例。可以看到我主应用的start()方法class:

@Override
public void start(Stage primaryStage) throws IOException {
    FXMLLoader mainPaneLoader;
    MainPaneController mainPaneController;
    Parent mainPane;

    // Initialize the project manager.
    projectManager = new ProjectManager(primaryStage);

    // Initialize the main pane loader.
    mainPaneLoader = new FXMLLoader();

    // Initialize the main pane controller.
    mainPaneController = new MainPaneController(projectManager);

    // Load the main pane.
    mainPaneLoader.setController(mainPaneController);
    mainPaneLoader.setLocation(getClass().getResource(MAIN_PANE_FXML_PATH));
    mainPane = mainPaneLoader.load();

    Scene mainScene;

    // Create the main scene and add it to the primary scene.
    mainScene = new Scene(mainPane);
    primaryStage.setScene(mainScene);

    // Initialize the primary stage.
    primaryStage.setTitle(APPLICATION_TITLE);

    // Show the primary stage.
    primaryStage.show();
}

请注意,上面定义并传递到主窗格控制器的构造函数中的 "project manager" 对象实际上是整个问题背后的主要动机;它是(除了传递给主控制器之外)我需要传递给我使用 <fx:include>.

包含在主 FXML 文件中的 FXML 文件的控制器的对象

现在,这种创建我自己的控制器实例并将其提供给 FXMLLoader 的方法对我来说非常有效。它使我能够轻松地将参数传递给控制器​​的构造函数,而无需进行任何混乱的反射。但是,它仅在我有一个 FXMLoader 对象将控制器实例提供给时才有效。

other 的情况下,我使用 <fx:include> 从主 FXML 文件中包含一个 FXML 文件,JavaFX 为我创建了控制器,没有办法让我 (1) 将参数传递给控制器​​的构造函数,或 (2) 使用我自己的控制器实例。


我试过的


在研究这个问题时,我偶然发现 this Whosebug question 这似乎与这个问题至少有一定的关系。从中我了解到FXMLLoader.setControllerFactory(),乍一看似乎可以解决这个问题。然而,为了使用它,我不得不使用一些相当混乱的反射来检查类型的构造函数是否可以接受我的对象,然后使用 more 反射来创建控制器,所有同时希望不会因为我的代码中的漏洞而抛出任何错误。我被迫承认这行不通。

我也尝试过,不是将我的对象传递给控制器​​的构造函数,而是在控制器初始化后 上设置对象。然而,这并没有很好地工作,因为我需要在我的控制器的 initialize() 方法中使用该对象,该方法称为 before 我将在控制器上设置该对象。这可以通过添加 另一个 初始化方法来解决,在该方法中可以找到需要该对象的任何功能,可能称为 objectInitialized();但是我必须将这个方法添加到每个需要这个功能的控制器中,而且我必须记住在某个时候调用所有这些方法。另外,我希望对象成为控制器 class 中的 final 字段;显然,如果需要外部设置就不能是final了。

最后,我还考虑了以下选项:对于我需要包含到主 FXML 文件中的每个 FXML 文件,我可以从 Java 控制器中执行此操作,而不是将其包含在 FMXL 中。这样我就可以创建自己的 FXMLLoader,在其上设置自己的控制器实例,从而解决问题。但是,如果可能的话,我更愿意将所有 UI 代码保留在 FXML 文件中。


总结


总而言之,我需要一种在使用 <fx:include> 时将参数传递给控制器​​构造函数的方法。

我知道这是一个很长的问题,而且有点复杂,所以非常感谢您提供的任何帮助。另外,如果我需要澄清任何内容或 post 附加代码,请在评论中告诉我。

谢谢大家的帮助!
—雅各布

经过深思熟虑,我决定在"What I've Tried"下的问题中简单地执行选项(3)。基本上,我决定不使用 <fx:include>,而是从我的 Java 代码中包含任何需要参数 directly/manually 的控制器,这样会更容易、更灵活、更面向未来。


为什么?


当我对我的问题进行更多思考时,我意识到我绝对没有办法将参数传递给我的控制器 "from my FXML" 的构造函数。毕竟,由于我试图传递的对象是在我的 Java 代码(主应用程序 class)中定义的,因此无法在 FXML 中使用它。因此,我对这个问题的整个看法都改变了。

选项 1

在我重新评估了我的选择之后,我最终考虑只使用 FXMLLoader.setControllerFactor(); James_D 在他针对我的问题发表的评论中证实了这种方法:由于 all 控制器实例化是通过反射发生的,因此不一定使用反射将我的对象传递给控制器和我想的一样糟糕。事实上,它可能会很好用。

然而,虽然如果我需要做的只是将一个 单个 对象传递给控制器​​,这会很好地工作,但是如果对于我的某些控制器,我想传递多个 对象(可能是不同类型的)?然后控制器工厂会变得相当笨拙,因为我必须检查 多个 可能不同类型的参数,然后传递正确的参数。

此外,如果我想将一个对象传递给控制器​​,而不是从定义控制器工厂的应用程序class,而是从某个地方传递对象怎么办?别的?例如,假设我有一个应用程序 class、MyApplication.java、一个 "main" 控制器 MainController.java 和一个控制器 "under" 主控制器 NavigationBarController.java?也许我在某些时候想要传递一个对象,不是从 MyApplicationNavigationBarController,而是从 MainController NavigationBarController。如果是这种情况(很有可能),那么我的控制器工厂将不再工作,因为它无法访问必要的对象。因此,我得出结论,setControllerFactory() 对我不起作用,至少不是很好。

选项 2(我使用的)

另一个选项(也是我最终使用的选项)是直接使用 FXMLLoader 从我的 Java 代码中包含我的控制器;并且,而不是包括主应用程序 class 中的所有控制器,我可以将每个控制器都包括在它 "resided" 中的 controller 中。

例如,使用上面选项 (1) 中提供的示例,而不是在 FXML 文件中使用控制器工厂和 fx:controller 属性,我将从任何文件中删除 fx:controller 属性需要访问我的对象的 FXML file/controller 对。

相反,在 MyApplication 中,我会直接使用 FXMLLoader 来加载、初始化和添加 MainController。然后,在 MainController 中,我将使用 FXMLLoader 来包含 NavigationBarController

现在,这种方法最大的潜在弱点是,每次我想包含 FXML file/controller 对时,我基本上都需要重复加载代码。为了解决这个问题,我创建了一种名为 FXMLQuickLoader 的 "utility class",它包含可以轻松快速地加载 FXML file/controller 对的方法,但带有自定义控制器对象。如果您有兴趣查看代码,我创建了一个包含 FXMLQuickLoader.java.

GitHub Gist

选项 3(未测试)

现在,James_D 指出我可能会使用一种叫做 "dependency injection factory" 的东西。如果我对他的理解正确,它会自动初始化我的控制器字段,而不是我需要将对象传递给构造函数。该解决方案似乎可以完美运行;但是,因为这是我的第一个 JavaFX 项目,并且由于我的时间有限,我认为学习它可能需要很长时间。

但是,对于我的下一个 JavaFX 项目,我可能会研究它并(强烈)考虑使用它。如果我这样做,我会尝试稍后更新这个答案。


总结


总而言之,对我来说,我确定最好使用 FXMLLoader 手动包含我的控制器,而不是尝试使用其中一种替代方法。但是,我只是在学习 JavaFX,我对这方面的知识并不十分了解,所以您应该对这个答案持保留态度。不过,它可能会帮助其他出现的用户,并且至少概述一些 潜在 解决方案。我也仍然愿意接受有关此问题的进一步建议。

这可以实现一个单例(或 Spring 中的 Bean),然后每个想要访问“共享”数据的控制器只需要使用 getInstance 方法访问单例,然后恢复它。

如果使用 Map 属性(而不是 User)实现并使用 put("key", objectValue) 方法放置数据,然后使用 get("key") ,则更加灵活;

基于此post

//Bean or Singleton
public final class UserHolder {

private User user;
private final static UserHolder INSTANCE = new UserHolder();

private UserHolder() {}

public static getInstance() {
  return INSTANCE;
}

public void setUser(User u) {
  this.user = u;
}

public User getUser() {
  return this.user;
}
}

//put data in Controller 1
@FXML
private void sendData(MouseEvent event) {
// Step 1
User u = new User();
Node node = (Node) event.getSource();
Stage stage = (Stage) node.getScene().getWindow();
stage.close();
try {
  Parent root =     
  FXMLLoader.load(getClass().getClassLoader().getResource("fxml/SceneB.fxml"));
  // Step 2
  UserHolder holder = UserHolder.getInstance();
  // Step 3
  holder.setUser(u);
  // Step 4
  Scene scene = new Scene(root);
  stage.setScene(scene);
  stage.show();
} catch (IOException e) {
  System.err.println(String.format("Error: %s", e.getMessage()));
}
}

//recover data in Controller 2
@FXML
private void receiveData(MouseEvent event) {
  UserHolder holder = UserHolder.getInstance();
  User u = holder.getUser();
  String name = u.getName();
  String email = u.getEmail();
}