java - 与嵌套 Controller 共享模型

标签 java javafx

我正在尝试使用 SceneBuilder 使用 JavaFX 构建一个简单的 GUI,我在其中使用 MenuItem(在 Main.fxml 中)来选择根文件夹。然后文件夹的内容列在一个 TextArea 中,该 TextArea 再次包裹在一个 TabPane 中(FileListTab.fxml,包含在 Main.fxml 中的嵌套 FXML)。

我用了this post作为习惯 MVC 的起点。不幸的是,我不知道如何让我的嵌套 FXML 监听或绑定(bind)到外部 FXML,因为我没有明确调用它。现在我只能在标签中显示我选择的文件夹。

我现在的最小工作代码如下所示:

Main.fxml

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

<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="MainController">
   <top>
      <MenuBar BorderPane.alignment="CENTER">
        <menus>
          <Menu mnemonicParsing="false" text="File">
            <items>
                  <MenuItem mnemonicParsing="false" onAction="#browseInputFolder" text="Open folder" />
            </items>
          </Menu>
        </menus>
      </MenuBar>
   </top>
   <center>
      <TabPane prefHeight="200.0" prefWidth="200.0" tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
        <tabs>
          <Tab text="File listing">
            <content>
                <fx:include fx:id="analysisTab" source="FileListTab.fxml" />
            </content>
          </Tab>
        </tabs>
      </TabPane>
   </center>
</BorderPane>

FileListTab.fxml

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" spacing="15.0" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="FileListController">
   <children>
      <HBox spacing="10.0">
         <children>
            <Label minWidth="100.0" text="Root folder:" />
            <Label fx:id="label_rootFolder" />
         </children>
      </HBox>
      <TextArea prefHeight="200.0" prefWidth="200.0" />
      <HBox spacing="10.0">
         <children>
            <Label minWidth="100.0" text="Found files:" />
            <Label fx:id="label_filesFound" />
         </children>
      </HBox>
   </children>
   <padding>
      <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
   </padding>
</VBox>

Model.java(应该是 Controller 之间的共享模型)

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Model {
    private StringProperty rootFolder;

    public String getRootFolder() {
        return rootFolderProperty().get();
    }

    public StringProperty rootFolderProperty() {
        if (rootFolder == null)
            rootFolder = new SimpleStringProperty();
        return rootFolder;
    }

    public void setRootFolder(String rootFolder) {
        this.rootFolderProperty().set(rootFolder);
    }
}

NestedGUI.java(主类)

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

import java.io.IOException;

public class NestedGUI extends Application {
    Model model = new Model();

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        Parent root = null;
        try {
            FXMLLoader fxmlLoader = new FXMLLoader();
            fxmlLoader.setLocation(getClass().getClassLoader().getResource("Main.fxml"));
            root = (BorderPane) fxmlLoader.load();
            MainController controller = fxmlLoader.getController();
            controller.setModel(model);

         // This openes another window with the tab's content that is actually displaying the selected root folder
/*            FXMLLoader fxmlLoader2 = new FXMLLoader();
            fxmlLoader2.setLocation(getClass().getClassLoader().getResource("FileListTab.fxml"));
            VBox vBox = (VBox) fxmlLoader2.load();
            FileListController listController = fxmlLoader2.getController();
            listController.setModel(model);

            Scene scene = new Scene(vBox);
            Stage stage = new Stage();
            stage.setScene(scene);
            stage.show();*/

        } catch (IOException e) {
            e.printStackTrace();
        }

        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }
}

主 Controller .java

import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;

import java.io.File;

public class MainController {
    Model model;

    public void setModel(Model model) {
        this.model = model;
    }

    public void browseInputFolder() {
        DirectoryChooser chooser = new DirectoryChooser();
        chooser.setTitle("Select folder");
        File folder = chooser.showDialog(new Stage());
        if (folder == null)
            return;

        String inputFolderPath = folder.getAbsolutePath() + File.separator;
        model.setRootFolder(inputFolderPath);
        System.out.print(inputFolderPath);
    }
}

FileListController.java

import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class FileListController {
    Model model;

    @FXML
    Label label_rootFolder;

    public void setModel(Model model) {
        label_rootFolder.textProperty().unbind();
        this.model = model;
        label_rootFolder.textProperty().bind(model.rootFolderProperty());
    }
}

我在 SO 上浏览了各种帖子,但要么我不理解答案,要么其他人有不同的问题。 有人可以给我一些指示吗? (解决这个问题的提示、代码片段、链接...)它看起来像是一个非常基本的 FXML 问题,但我就是不明白。

最佳答案

简单的解决方案

一种选择是将“嵌套 Controller ”注入(inject)主 Controller ,如 FXML documentation 中所述。 .

规则是 Controller 的字段名称应该是fx:id。对于 <fx:include>使用字符串 "Controller"附加。所以在你的情况下,fx:id="analysisTab"所以该字段将是 FileListController analysisTabController .完成后,您可以将模型传递给在主 Controller 中设置的嵌套 Controller :

public class MainController {
    Model model;

    @FXML
    private FileListController analysisTabController ;

    public void setModel(Model model) {
        this.model = model;
        analysisTabController.setModel(model);
    }

    // ...
}

高级解决方案

上述简单解决方案的一个缺点是您必须手动将模型传播到所有嵌套 Controller ,这可能会变得难以维护(特别是如果您有多个级别的 <fx:include> s)。另一个缺点是您是在创建和初始化 Controller 之后设置模型(因此,例如,模型在 initialize() 方法中不可用,这是您最自然希望使用它的地方)。

更高级的方法是设置一个 controllerFactoryFXMLLoader 上. controllerFactory是一个将 Controller 类(由 fxml 文件中的 fx:controller 属性指定)映射到将用作 Controller 的对象(几乎总是该类的实例)的函数。默认 Controller 工厂只调用类上的无参数构造函数。您可以使用它来调用采用模型的构造函数,因此模型在 Controller 实例化后立即可用。

如果您设置 Controller 工厂,则同一 Controller 工厂将用于所有包含的 fxml 文件。

所以你可以重写你的 Controller 让构造函数接受一个模型实例:

public class MainController {
    private final Model model;

    public MainController(Model model) {
        this.model = model;
    }

    public void browseInputFolder() {
        DirectoryChooser chooser = new DirectoryChooser();
        chooser.setTitle("Select folder");
        File folder = chooser.showDialog(new Stage());
        if (folder == null)
            return;

        String inputFolderPath = folder.getAbsolutePath() + File.separator;
        model.setRootFolder(inputFolderPath);
        System.out.print(inputFolderPath);
    }
}

并且在 FileListController这意味着您现在可以直接在 initialize() 中访问模型方法:

public class FileListController {
    private final Model model;

    @FXML
    Label label_rootFolder;

    public FileListController(Model model) {
        this.model = model ;
    }

    public void initialize() {
        label_rootFolder.textProperty().bind(model.rootFolderProperty());
    }
}

现在您的应用程序类需要创建一个调用这些构造函数的 Controller 工厂。这是棘手的部分:您可能想在这里使用一些反射并实现以下形式的逻辑:“如果 Controller 类具有采用模型的构造函数,则使用(共享)模型实例调用它;否则调用默认构造函数”。这看起来像:

public class NestedGUI extends Application {
    Model model = new Model();

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        Parent root = null;
        try {
            FXMLLoader fxmlLoader = new FXMLLoader();
            fxmlLoader.setLocation(getClass().getClassLoader().getResource("Main.fxml"));

            fxmlLoader.setControllerFactory((Class<?> type) -> {
                try {
                    for (Constructor<?> c : type.getConstructors()) {
                        if (c.getParameterCount() == 1 && c.getParameterTypes()[0] == Model.class) {
                            return c.newInstance(model);
                        }
                    }
                    // default behavior: invoke no-arg constructor:
                    return type.newInstance();
                } catch (Exception exc) {
                    throw new RuntimeException(exc);
                }
            });

            root = (BorderPane) fxmlLoader.load();


        } catch (IOException e) {
            e.printStackTrace();
        }

        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }
}

此时,您基本上是创建依赖注入(inject)框架的一步(您正在使用工厂类将模型注入(inject) Controller ......)!因此,您可能只考虑使用一个而不是从头开始创建一个。 afterburner.fx是一个流行的JavaFX依赖注入(inject)框架,实现的核心本质上就是上面代码中的思想。

关于java - 与嵌套 Controller 共享模型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37068243/

相关文章:

java - 如何将需要登录的 URL 加载到变量中?

javafx - 如何在滚动条中放置标记

从字节数组构造的 Java String 长度错误

java - "com.mysql.jdbc.MysqlDataTruncation: Data truncation: Out of range value"与按位 'OR'

JavaFX 在线程任务完成后显示对话

java - 老虎机时间线动画不起作用

java - e(fx)clipse javafx的导入无法解析

javafx - 使用 SplitPanes 构建 BorderPane 并最小化隐藏节点的区域

java - MongoDB 主机名/URI 配置

java - 使用 JAAS LdapLoginModule 向 ActiveDirectory 进行身份验证时遇到 FailedLoginException