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

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

ButtonPanel.fxml

  

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,我用于两个单独的场景,可能同时在屏幕上激活或不激活。 一个实际的例子就是将内容停靠在侧面板上,再加上一个按钮,可以在一个可以拖动/resize/等的单独窗口中打开相同的内容。

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

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

我很清楚,尝试将两个视图链接到一个控制器是不好的MVC实践,但是我想避免必须实现几乎完全相同的单独的FXML和控制器。

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

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

提前致谢!

史蒂夫

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

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

创建一个存储数据的模型类。 在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不会更改:

          

您的控制器具有对模型的引用。 它可以使用模型属性上的绑定或侦听器来更新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, 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); } }