Bind StringProperty to Label from a Singleton - java

I have a singleton called MenuText. It is responsible for displaying the correct text in the menu. It gets updated dynamically.
public class MenuText implements LanguageObserver {
private MenuText() { //I want this to be private, only one instance should exist
Config.getInstance().subscribe(this);
}
private static class MenuTextHolder {
private static final MenuText INSTANCE = new MenuText();
}
public static MenuText getInstance() {
return MenuTextHolder.INSTANCE;
}
#Override
public void update(Language language) {
System.out.println("Updating...");
switch (language) {
case ENGLISH -> {
setText("Play");
}
case GERMAN -> {
setText("Spielen");
}
}
}
private final StringProperty text = new SimpleStringProperty("Play");
public StringProperty textProperty() {
return text;
}
public String getText() {
return text.get();
}
private void setText(String text) {
this.text.set(text);
}
}
I have a fxml file, but the MenuText can't have a reference to it. (This would contradict the MVVM architectural style)
<?import tiles.text.MenuText?>
<VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
prefHeight="1080.0" prefWidth="1920.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="tiles.viewModel.GameMenuViewModel">
<!--here I want to bind to the text property-->
<Button text="???"/>
</VBox>
Initially I used <fx:define> to setup a reference to the MenuText from the fxml file, but this doesn't allow private constructors. It shouldn't be that difficult, because MenuText is static, but I'm unable to make a static reference to it's singleton.
I tried <Button text="${MenuText.getInstance().text}">
Update
As mentioned in this answer, I shouldn't use the Singleton Pattern.
Based on this I added an ApplicationFactory:
//Creation of items with application lifetime
public class ApplicationFactory {
private Config config;
public void build() {
config = new Config();
}
public Config getConfig() {
return config;
}
}
Is this the correct approach? I now have a MenuFactory, which gets also created in the JavaFX start() method. It sets the parent of the scene.
public class MenuFactory {
public Parent getMenu(Config config, String fxmlLocation) {
MenuText menuText = new MenuText(config);
MenuViewModel menuViewModel = new MenuViewModel(config);
try {
FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(getClass().getResource(fxmlLocation)));
loader.getNamespace().put("menuText", menuText);
return loader.load();
} catch (IOException e) {
//...
}
}
}
The start() mehtod looks like this:
#Override
public void start(Stage primaryStage) {
ApplicationFactory applicationFactory = new ApplicationFactory();
applicationFactory.build();
MenuFactory menuFactory = new MenuFactory();
Parent root = menuFactory.getMenu(applicationFactory.getConfig(), MENU);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
This makes it way more complicated and I'm not sure if this is correct. Furthermore, I still don't know how I set the MenuText in the fxml file. I tried, but I think this isn't the correct way to set a namespace in fxml.
<fx:define>
<MenuText fx:id="menuText"/>
</fx:define>
I read these documentations but don't understand how I can set this custom namespace.

Be aware that the singleton pattern is widely considered to be an anti-pattern. However, if you really want to do this:
Initially I used <fx:define> to setup a reference to the MenuText from the fxml file
This is the correct approach. You can combine it with fx:factory to get a reference to an instance of a class that does not have a (public) default constructor:
<fx:define>
<MenuText fx:factory="getInstance" fx:id="menuText" />
</fx:define>
And then do
<Button text="${menuText.text}" />
Another solution is to "manually" insert the MenuText instance into the FXML namespace:
MenuText menuText = ... ;
FXMLLoader loader = new FXMLLoader(getClass().getResource(...));
loader.getNamespace().put("menuText", menuText);
Parent root = loader.load();
And then
<Button text="${menuText.text}" />
should work with no additional FXML code. This approach allows you to avoid using the singleton pattern (you're basically injecting the dependency into the FXML, so you could combine it easily with a DI framework).

Related

How to update view via model in JavaFX MVC?

I have FXML application with model, view and controller. My view is in .fxml file and I have Text there like this
<Text fx:id="position" text="None" GridPane.columnIndex="1" GridPane.rowIndex="3">
<font>
<Font name="System Bold" size="18.0" />
</font>
</Text>
My Controller look like this
public class Controller{
#FXML
private Text position;
public void updatePosition(String text){
position.setText(text);
}
}
In my Model, there I have String variable, which is changing throughout all my project. Model look like this
public class Model {
public String position = "None";
public String getPosition() {
return tactic;
}
public void setPosition(String position) {
this.position = position;
}
}
There are another classes in my project, which call setPositon method and update variable position. Is there a way how to change Text in my view, when someone change position variable in Model class?
You need to observe the model. The easiest way to do this in JavaFX is to use JavaFX properties to represent the data.
public class Model {
private final StringProperty position = new SimpleStringProperty("None");
public StringProperty positionProperty() {
return position ;
}
public final String getPosition() {
return positionProperty().get();
}
public final void setPosition(String position) {
positionProperty().set(position);
}
}
Now you can simply bind the text's textProperty() to the model's positionProperty(), and if the model's position is changed, the text will automatically update:
public class Controller{
#FXML
private Text position;
private Model model ;
public void initialize() {
position.textProperty().bind(model.positionProperty());
}
// ...
}
The only tricky part now is to make sure the controller has a reference to the correct model instance. The best way to do this is to provide a constructor for the controller taking a model:
public class Controller {
#FXML
private Text position;
private final Model model ;
public Controller(Model model) {
this.model = model ;
}
public void initialize() {
position.textProperty().bind(model.positionProperty());
}
// ...
}
The problem now is that the default mechanism for creating controllers via the FXMLLoader relies on a zero-argument constructor, so it won't work. So you need to remove the fx:controller attribute from the FXML file and set the controller "by hand":
Model model = new Model(); // or just reference to existing model...
FXMLLoader loader = new FXMLLoader(getClass().getResource("View.fxml"));
loader.setController(new Controller(model));
Parent root = loader.load();
// ...
model.setPosition("Left"); // will update text in view
See Passing Parameters JavaFX FXML for more ways to pass the model (or other data) to the controller.
Also see the related question Applying MVC With JavaFx

Javafx - how to acces FXML "objects"

I have some JavaFX components declared in my fxml file.
and how do I get the values of the fields (Username, Password) when the button is pressed? (To perform the login).
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
stage.setTitle("Login");
stage.setScene(new Scene(root,400,300));
stage.show();
or is this the complete wrong way to do it?
My Questions:
Is it a good idea to declare all fields in the fxml file?
How do I get the objects to acces the Values?
If its the complete wrong way, how can it be done? (I want to use scenebuilder)
EDIT :
https://hastebin.com/qexipogoma.xml <- My FXML fie and my controller
Scene scene = stage.getScene();
Button btn = (Button) scene.lookup("#myBtnID");
TextField txt = (TextField ) scene.lookup("#myTxtID");
you're looking for:
txt.getText();
and
btn.setOnAction( lambda here );
Documentation:
Button
TextField
EDIT:
declare ids this way
<TextField fx:id="myTxtID" ... />
Is it a good idea to declare all fields in the fxml file?
This depends on what you need. In this case you do not need to add/remove any parts of the scene dynamically. You'll probably replace the window/scene on a successful login. There should not be any issue with creating this scene via fxml.
How do I get the objects to acces the values?
Use a controller with the fxml and access the values via this controller.
<AnchorPane fx:controller="mypackage.LoginController" ...>
<children>
...
<TextField fx:id="username" ... />
...
<PasswordField fx:id="password" ... />
...
<Button onAction="#login" ... />
</children>
</AnchorPane>
package mypackage;
...
public class LoginController {
private boolean login = false;
#FXML
private TextField username;
#FXML
private PasswordField password;
#FXML
private void login() {
// regular close the login window
login = true;
password.getScene().getWindow().hide();
}
public String getUsername() {
return username.getText();
}
public String getPassword() {
return password.getText();
}
public boolean isLogin() {
return login;
}
public void resetLogin() {
// allow reuse of scene for invalid login data
login = false;
}
}
FXMLLoader loader = new FXMLLoader(getClass().getResource("sample.fxml"));
Stage stage = new Stage(new Scene(loader.load()));
LoginController controller = loader.getController();
boolean loginSuccess = false;
stage.showAndWait();
if (controller.isLogin()) {
if (checkLogin(controller.getUsername(), controller.getPassword())) {
// handle login success
} else {
// handle invalid login
}
}
Try using SceneBuilder. It will produce an FXML file and a compatible controller. This will give you a good start.

How to specialize a components of JavaFX and hide its API to only show mine

As said in the title I want to specialize the usage of a TableView that I will reuse many time, that speciazation contains :
Columns shown
Filtering of what is added according to a default duplicate filter and some addiotional (either based on boolean values or callbacks).
I use raw FXML files and Controller, no UI drag and drop building.
In order to keep the usage of my component the easiest possible I would like to hide the component part of JavaFX and only allow my methods, how to do it ?
Create a class extending Control. Add the methods you want the user to access to this class.
Create a skin for this class and implement the behavior you don't want the user to access there.
Benefits:
Hides the implementation details from user.
Allows the user to replace the "private" behavior, if neccessary.
Allows you to access the "public" behavior form the node directly.
Example
Control
public class MyControl extends Control {
#Override
protected Skin<?> createDefaultSkin() {
return new MyControlSkin(this);
}
private final StringProperty text = new SimpleStringProperty();
public final String getText() {
return this.text.get();
}
public final void setText(String value) {
this.text.set(value);
}
public final StringProperty textProperty() {
return this.text;
}
}
Skin
public class MyControlSkin extends SkinBase<MyControl> {
public MyControlSkin(MyControl control) {
super(control);
Text text = new Text();
text.textProperty().bind(control.textProperty());
getChildren().setAll(text);
}
}
Use
#Override
public void start(Stage primaryStage) {
final MyControl control = new MyControl();
Button btn = new Button("Say 'Hello World'");
btn.setOnAction((ActionEvent event) -> {
control.setText("Hello World!");
});
Scene scene = new Scene(new VBox(10, btn, control));
primaryStage.setScene(scene);
primaryStage.show();
}
Note that it does not matter how you create the UI. It could be directly created from java or loaded from fxml - this does not matter. You could e.g. use the skin as fxml controller and root:
<fx:root type="javafx.scene.control.SkinBase" xmlns:fx="http://javafx.com/fxml">
<children>
<Text fx:id="text"/>
</children>
</fx:root>
#FXML
private Text text;
public MyControlSkin(MyControl control) throws IOException {
super(control);
getChildren().clear();
FXMLLoader loader = new FXMLLoader(someUrl);
loader.setRoot(this);
loader.setController(this);
loader.load();
text.textProperty().bind(control.textProperty());
}
BTW: The duplicate filtering would IMHO be better of in a seperate class, like a TransformationList. This way you could reuse the behavior independent of the UI which would allow easier reuse (e.g. For use with a ListView).
Here is the current way I found to do it use an interface an use that interface when using #FXML injection.
My specialized components :
public class EquipmentTableView extends AnchorPane implements IEquipmentTableView{[...]}
Where AnchorPane is from JavaFX and the interface from me.
The FXML of the component :
<fx:root type="javafx.scene.layout.AnchorPane" xmlns:fx="http://javafx.com/fxml">
<TableView fx:id="tableView"
AnchorPane.topAnchor="0.0" AnchorPane.bottomAnchor="0.0"
AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0">
</TableView>
</fx:root>
The FXML to include the component :
<EquipmentTableView fx:id="tableView"/>
The injection in the controller
public class ReadController {
// use the interface here
#FXML private IEquipmentTableView tableView;
public static String FXML="read.fxml";
public static String TITLE_KEY="read.title";
#FXML
void initialize(){
tableView.addAll(Arrays.asList(
new Equipment("name1", "partNumber1", "MFC1", "00000000001", "3400000000000001"),
new Equipment("name2", "partNumber2", "MFC2", "00000000002", "3400000000000002"),
new Equipment("name3", "partNumber3", "MFC3", "00000000003", "3400000000000003")));
}
}
To sum it up :
By using the TableView inside the components instead of inheriting it I don't allow to manipulate the TableView. I inherit AnchorPane instead because I want my table view to auto-resize, you can inherits from Pane otherwise.
I am allowed to manipulate it using only the interface methods since I cannot access the TableView.
Using only the interface disallow myself to manipulate the AnchorPane for other purposes or having to search through the methods that are mines.

JavaFX: Adding a new tab from a tab controller

I am trying to make it such that I can create a new tab for my TabPane from within another tab but I am having some difficulty. Currently I have the TabPane set up in the "main-window.fxml" with the corresponding MainWindowController. I have a tab within this TabPane which, via fx:include, displays "mainTab.fxml" to the scene graph, controlled by MainTabController. Now from within the "mainTab" I want a button to be able to add an additional tab to the TabPane, but since this is requires a reference to the TabPane in "main-window", I have created a static method in "main-window". When the run the code below I get a NullPointerException on this line in the MainWindowController:
mainTabPane.getTabs().add(new Tab(team.getTeamName()));
Could someone please tell me as to why it is giving this exception and how I can begin to work around it?
main-window.fxml:
<TabPane fx:id="mainTabPane">
<tabs>
<Tab fx:id="mainTab" text="Main" closable="false">
<fx:include source="mainTab.fxml" fx:id="mainWindowTab" alignment="CENTER"/>
</Tab>
</tabs>
</TabPane>
mainTab.fxml (the event handler for the button):
#FXML
public void handleSubmit() {
String teamName = teamNameTextField.getText();
Roster roster = rosterComboBox.getValue();
int startWeek = spinner.getValue();
Team newTeam = new Team(teamName, startWeek, roster);
TeamData.addTeam(newTeam);
MainWindowController controller = new MainWindowController();
controller.createTeamTab(newTeam);
}
MainWindowController:
public class MainWindowController {
#FXML
private TabPane mainTabPane;
public void createTeamTab(Team team) {
mainTabPane.getTabs().add(new Tab(team.getTeamName()));
}
}
Your code doesn't work because you are not calling createTeamTab(...) on the controller: you are calling it on another instance of MainWindowController that you created. (The fields annotated #FXML are initialized in the controller instance by the FXMLLoader when the FXML is loaded: for fairly obvious reasons they will not be set to the same values in arbitrary other instances of the same class.) You need to get a reference to the controller you are using for the main tab, and pass it a reference to the main controller.
You didn't tell us the class name for the controller of mainTab.fxml: I will assume it is MainTabController (so just change it to whatever class name you actually use).
In MainWindowController, do:
public class MainWindowController {
#FXML
private TabPane mainTabPane;
#FXML
// fx:id of the fx:include with "Controller" appended
private MainTabController mainWindowTabController ;
public void initialize() {
mainWindowTabController.setMainWindowController(this);
}
public void createTeamTab(Team team) {
mainTabPane.getTabs().add(new Tab(team.getTeamName()));
}
}
and then in MainTabController do
public class MainWindowController {
private MainWindowController mainWindowController ;
public void setMainWindowController(MainWindowController mainWindowController) {
this.mainWindowController = mainWindowController ;
}
#FXML
public void handleSubmit() {
String teamName = teamNameTextField.getText();
Roster roster = rosterComboBox.getValue();
int startWeek = spinner.getValue();
Team newTeam = new Team(teamName, startWeek, roster);
TeamData.addTeam(newTeam);
mainWindowController.createTeamTab(newTeam);
}
}

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

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
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);
}
}

Categories