问题
Below is a small Application that illustrates the problem:
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);
}
}
Basically, I have a single FXML that I am using for two separate scenes that may or may not be active on the screen at the same time. A practical example of this would be having content docked to a side panel plus a button that opens the same content in a separate window that can be dragged/resized/etc.
The goal I am trying to achieve is to keep the views in sync (changes to one of the views affects the other one).
I am able to point both views to the same controller via a callback however the issue I am running into now is that the UI changes are only reflected on the 2nd scene. Both views talk to the controller but the controller only talks back to the 2nd scene. I'm assuming something with JavaFX's implementation of MVC or IOC is linking the controller to the view in some 1:1 relationship when it is loaded via the FXMLLoader.
I am well aware that trying to link two views to 1 controller is bad MVC practice, however I would like to avoid having to implement a separate FXML and Controller that are practically identical.
Is it possible to achieve this kind of synchronization that I listed above?
If I need to create a separate Controller, what's the best way to ensure that both UI's are in sync (even down to sidebar movements)?
Thanks in Advance!
-Steve
回答1:
The reason your code doesn't work is that the FXMLLoader
injects references to elements in the FXML with fx:id
attributes into fields in the controller with matching names. So when you load the FXML file the first time, the FXMLLoader
sets the field myButton
to be a reference to the button it creates when it loads the FXML. Since you use the exact same controller instance the second time you load the FXML, the FXMLLoader
now sets that same field (in the same controller instance) to be a reference to the button it creates when the FXML file is loaded again. In other words, buttonController.myButton
now refers to the second button created, not the first. So when you call myButton.setStyle(...)
it updates the style of the second button.
Basically, you always want one controller instance per view instance. What you need is for both controllers to access the same shared state.
Create a model class that stores the data. In a MVC architecture, the View observes the model and updates when the data in the model changes. The controller reacts to user interaction with the view and updates the model.
(Arguably, FXML gives you more of a MVP architecture, which is similar. There are variants of this too, but generally the presenter will observe the model and update the view when data in the model changes, as well as update the model in response to user interaction.)
So your model might look like:
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() );
}
}
Your ButtonPanel.fxml doesn't change:
<?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>
Your controller has a reference to the model. It can use bindings or listeners on the model properties to update the UI, and the handler methods just update the model:
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();
}
}
Finally, you keep everything synchronized because the views are views of the same model. In other words you just create one model and hand its reference to both controllers. Since I made the model a constructor parameter in the controller (which is nice, because you know you have a model as soon as the instance is created), we need a controller factory to create the controller instances:
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);
}
}
来源:https://stackoverflow.com/questions/35805710/javafx-sync-duplicate-views-to-the-same-controller-fxml-mvc