JavaFX 将重复视图同步到同一控制器(FXML 和 MVC)

JavaFX Sync Duplicate Views to the Same Controller (FXML & MVC)

下面是一个说明问题的小应用程序:

ButtonPanel.fxml

<ScrollPane fx:controller="ButtonPanelController">
    <VBox>
        <Button fx:id="myButton" text="Click Me" onAction="#buttonClickedAction" />
    </VBox>
</ScrollPane>

ButtonPanelController.java

public class ButtonPanelController {

    @FXML
    Button myButton;

    boolean isRed = false;

    public void buttonClickedAction(ActionEvent event) {
        if(isRed) {
            myButton.setStyle("");
        } else {
            myButton.setStyle("-fx-background-color: red");
        }
        isRed = !isRed;
    }
}

TestApp.java

public class TestApp extends Application {

    ButtonPanelController buttonController;

    @Override
    public void start(Stage stage) throws Exception {
        // 1st Stage
        stage.setTitle("1st Stage");
        stage.setWidth(200);
        stage.setHeight(200);
        stage.setResizable(false);

        // Load FXML
        FXMLLoader loader = new FXMLLoader(
                ButtonPanelController.class.getResource("ButtonPanel.fxml"));
        Parent root = (Parent) loader.load();

        // Grab the instance of ButtonPanelController
        buttonController = loader.getController();

        // Show 1st Scene
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();

        // 2nd Stage
        Stage stage2 = new Stage();
        stage2.setTitle("2nd Stage");
        stage2.setWidth(200);
        stage2.setHeight(200);
        stage2.setResizable(false);

        /* Override the ControllerFactory callback to use 
         * the stored instance of ButtonPanelController
         * instead of creating a new one.
         */
        Callback<Class<?>, Object> controllerFactory = type -> {
            if(type == ButtonPanelController.class) {
                return buttonController;
            } else {
                try {
                    return type.newInstance();
                } catch(Exception e) {
                    throw new RuntimeException(e);
                }
            }
        };

        // Load FXML
        FXMLLoader loader2 = new FXMLLoader(
                ButtonPanelController.class.getResource("ButtonPanel.fxml"));
        // Set the ControllerFactory before the load takes place
        loader2.setControllerFactory(controllerFactory);
        Parent root2 = (Parent) loader2.load();

        // Show 2nd Scene
        Scene scene2 = new Scene(root2);
        stage2.setScene(scene2);
        stage2.show();
    }

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

基本上,我有一个 FXML 用于两个单独的场景,这两个场景可能同时在屏幕上处于活动状态,也可能不处于活动状态。一个实际的例子是将内容停靠到侧面板加上一个按钮,该按钮可以在单独的 window 中打开相同的内容,可以是 dragged/resized/etc.

我试图实现的目标是保持视图同步(更改其中一个视图会影响另一个视图)。

我可以通过回调将两个视图指向同一个控制器,但是我 运行 现在遇到的问题是 UI 更改仅反映在第二个场景中。两个视图都与控制器对话,但控制器只与第二个场景对话。我假设在通过 FXMLLoader 加载时,JavaFX 的 MVC 或 IOC 实现 link 将控制器以某种 1:1 关系连接到视图。

我很清楚尝试 link 对 1 个控制器的两个视图是错误的 MVC 实践,但是我想避免必须实现一个单独的 FXML 和控制器,它们实际上是相同的。

是否可以实现我上面列出的这种同步?

如果我需要创建一个单独的控制器,确保两个 UI 同步(甚至连边栏移动)的最佳方法是什么?

提前致谢!

-史蒂夫

您的代码不起作用的原因是 FXMLLoader 将对 FXML 中具有 fx:id 属性的元素的引用注入到具有匹配名称的控制器中的字段中。因此,当您第一次加载 FXML 文件时,FXMLLoader 将字段 myButton 设置为对它在加载 FXML 时创建的按钮的引用。由于您在第二次加载 FXML 时使用完全相同的控制器实例,因此 FXMLLoader 现在将相同的字段(在同一控制器实例中)设置为对再次加载 FXML 文件时创建的按钮的引用.换句话说,buttonController.myButton 现在指的是创建的第二个按钮,而不是第一个。因此,当您调用 myButton.setStyle(...) 时,它会更新第二个按钮的样式。

基本上,您总是希望每个视图实例有一个控制器实例。您需要的是两个控制器访问相同的共享状态。

创建一个存储数据的模型class。在 MVC 架构中,View 观察模型并在模型中的数据发生变化时进行更新。控制器对用户与视图的交互做出反应并更新模型。

(可以说,FXML 给你更多的是 MVP 架构,这是相似的。这也有变体,但一般情况下,演示者会观察模型并在模型中的数据发生变化时更新视图,以及更新模型以响应用户交互。)

因此您的模型可能如下所示:

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;

public class Model {

    private final BooleanProperty red = new SimpleBooleanProperty();

    public final BooleanProperty redProperty() {
        return this.red;
    }


    public final boolean isRed() {
        return this.redProperty().get();
    }


    public final void setRed(final boolean red) {
        this.redProperty().set(red);
    }


    public void toggleRed() {
        setRed(! isRed() );
    }
}

你的ButtonPanel.fxml没有改变:

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

<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>

<ScrollPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="ButtonPanelController">
    <VBox >
        <Button fx:id="myButton" text="Click Me" onAction="#buttonClickedAction" />
    </VBox>
</ScrollPane>

您的控制器引用了该模型。它可以在模型属性上使用绑定或侦听器来更新 UI,并且处理程序方法只更新模型:

import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;

public class ButtonPanelController {

    @FXML
    Button myButton;

    boolean isRed = false;

    private Model model ;

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

    public void initialize() {

        myButton.styleProperty().bind(Bindings.
                when(model.redProperty()).
                then("-fx-background-color: red;").
                otherwise("")
        );
    }

    public void buttonClickedAction(ActionEvent event) {
        model.toggleRed();
    }
}

最后,您保持所有内容同步,因为视图是同一模型的视图。换句话说,您只需创建一个模型并将其引用传递给两个控制器。由于我将模型作为控制器中的构造函数参数(这很好,因为您知道实例一创建就拥有一个模型),我们需要一个控制器工厂来创建控制器实例:

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.util.Callback;

public class TestApp extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        // 1st Stage
        stage.setTitle("1st Stage");
        stage.setWidth(200);
        stage.setHeight(200);
        stage.setResizable(false);

        // The one and only model we will use for both views and controllers:
        Model model = new Model();

        /* Override the ControllerFactory callback to create 
         * the controller using the model:
         */
        Callback<Class<?>, Object> controllerFactory = type -> {
            if(type == ButtonPanelController.class) {
                return new ButtonPanelController(model);
            } else {
                try {
                    return type.newInstance();
                } catch(Exception e) {
                    throw new RuntimeException(e);
                }
            }
        };

        // Load FXML
        FXMLLoader loader = new FXMLLoader(
                ButtonPanelController.class.getResource("ButtonPanel.fxml"));
        loader.setControllerFactory(controllerFactory);

        Parent root = (Parent) loader.load();

        // Show 1st Scene
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();

        // 2nd Stage
        Stage stage2 = new Stage();
        stage2.setTitle("2nd Stage");
        stage2.setWidth(200);
        stage2.setHeight(200);
        stage2.setResizable(false);

        // Load FXML
        FXMLLoader loader2 = new FXMLLoader(
                ButtonPanelController.class.getResource("ButtonPanel.fxml"));
        // Set the ControllerFactory before the load takes place
        loader2.setControllerFactory(controllerFactory);
        Parent root2 = (Parent) loader2.load();

        // Show 2nd Scene
        Scene scene2 = new Scene(root2);
        stage2.setScene(scene2);
        stage2.show();
    }

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