与嵌套控制器共享模型

fuk*_*uri 1 java javafx

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

我使用这篇文章作为开始习惯 MVC 的起点。不幸的是,我不知道如何让我的嵌套 FXML 侦听或绑定到外部 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>
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

Model.java(应该是控制器之间的共享模型)

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

主控制器.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);
    }
}
Run Code Online (Sandbox Code Playgroud)

文件列表控制器.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());
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Jam*_*s_D 5

简单的解决方案

一种选择是将“嵌套控制器”注入主控制器,如FXML 文档中所述

规则是控制器的字段名称应该是带有附加字符串的fx:idfor 。因此,在您的情况下,该字段将是. 完成后,您可以在主控制器中设置模型时将模型传递给嵌套控制器:<fx:include>"Controller"fx:id="analysisTab"FileListController analysisTabController

public class MainController {
    Model model;

    @FXML
    private FileListController analysisTabController ;

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

    // ...
}
Run Code Online (Sandbox Code Playgroud)

先进的解决方案

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

更高级的方法是controllerFactoryFXMLLoader. 该controllerFactory函数将控制器类(由fx:controllerfxml 文件中的属性指定)映射到将用作控制器的对象(几乎总是该类的实例)。默认控制器工厂只调用类上的无参数构造函数。您可以使用它来调用采用模型的构造函数,因此一旦控制器被实例化,模型就可用。

如果您设置控制器工厂,则相同的控制器工厂将用于任何包含的 fxml 文件。

因此,您可以重写控制器以让构造函数采用模型实例:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

而在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());
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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