How to update view via model in JavaFX MVC? - java

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

Related

Bind StringProperty to Label from a Singleton

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).

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 - Access parent fx:id from child

Let's say I have a button in a nested (child) fxml file, and in the child's controller I have created an action event that fires on button click. From that method I want to disable or enable certain controls (for instance some tabs in a tabpane) in my main (parent) fxml.
How can I achieve this?
This is the closest thread I found, which discussed how to do it the other way around: JavaFX - Access fx:id from nested FXML
Any help is greatly appreciated!
Define an observable property in the nested controller, and observe it from the surrounding controller:
public class ChildController {
private final BooleanProperty stuffShouldBeDisabled = new SimpleBooleanProperty();
public BooleanProperty stuffShouldBeDisabledProperty() {
return stuffShouldBeDisabled ;
}
public final boolean getStuffShouldBeDisabled() {
return stuffShouldBeDisabledProperty().get();
}
#FXML
private void handleButtonClick(ActionEvent event) {
stuffShouldBeDisabled.set( ! stufShouldBeDisabled.get() );
}
// ...
}
and then in the "surrounding" (Parent) controller (i.e. the controller for the FXML file with the <fx:include> tag):
public class MainController {
#FXML
private ChildController childController ; // injected via <fx:include fx:id="child" ... />
#FXML
private Tab someTab ;
public void initialize() {
childController.stuffShouldBeDisabledProperty().addListener((obs, wasDisabled, isNowDisabled) -> {
someTab.setDisable(isNowDisabled);
}
}
// ...
}

JavaFX bind a hyperlink to a label

I am using MVP in my JavaFX application.
Resources:
public class InfoStageResources{
StringProperty lblBlogText;
Hyperlink linkBlog;
InfoStageResources() {
this.lblBlogText = new SimpleStringProperty("link");
this.linkBlog = new Hyperlink("link");
}
}
Controller:
public class InfoStageController{
private InfoStageView view;
private InfoStageResources res;
public void initView(){
this.res = new InfoStageResources();
this.view = new InfoStageView(this.res);
this.initViewBindings();
}
private void initViewBindings(){
this.view.lblBlog.textProperty().bind(this.res.lblBlogText);
//this will not work
this.view.lblBlog.textProperty().bind(this.res.linkBlog);
}
}
View
In my InfoStageView in just init my labels and style my view.
How can bind my Hyperlink to my label. I tried some things but without success.
My StringProperty lblBlogText isn't clickable but easy to bind.
My goal: I want to open the browser with the link.
I think you are looking for
this.view.lblBlog.textProperty().bind(this.res.linkBlog.textProperty());

Javafx how to access controls of second controller from first controller

I have one question regarding Javafx controller.
Lets say, I have multiple fxml files that are bind together in a main app. Then I have separate controllers for every fxml files. Lets see the following structure
com.par.app
- MainApp.java -> This is the main Application
- FirstController.java
- SecondController.java
com.par.app.view
- First.fxml
- Second.fxml
com.par.app.model
- MyModel -> This has some getter and setter methods.
Now as per above structure, I have a checkbox in First.fxml and a label in Second.fxml.
My Question : How can i set the label text in Second.FXML by checking and unchecking the checkbox in First.FXML , I have tried like this:
// In FirstController.Java
1) Initialize the SecondController
2) Get checkbox from FXMl as , priate CheckBox box1;
3) On initialize(....) method, I have set the event handler, as box1.setOnAction(enableHandle)
4) Finally the event Handler as,
EventHandler<ActionEvent> enableHandle = new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
if (box1.isSelected()) {
secondController.setLabelText("PoP");
} else {
secondController.setText("Gone Wrong");
}
}
};
Similarly, On my second controller I have declared FXML control as,
#FXML
private Label lblTes;
// the method
public void setLabelText(String a)
{
this.lblTes.settest(a);
}
-> The above wont work as it returns Unknown Source.
The next way I tried is by using the MyModel , and using getter and setter methods, But unsuccessful.
I'm sorry my question is really long. I have tried but not succeeded.
What can I do to solve this?
Thanks in advance.
// my model looks like
public class MyModel {
private String btnname;
public String getBtnname() {
return btnname;
}
public void setBtnname(String btnname) {
this.btnname = btnname;
}
}
When you check the check box then in the controller of the FirstView (where you implement an event handler for the check box click) change the label text in your model.
Your model should be bound to your views therefore the label text in your SecondView should be updated.
If you did not bind the model to your views you may use an Observer pattern.
1.Change your model and extend java.util.Observable
public class MyModel extends Observable {
private String btnname;
public String getBtnname() {
return btnname;
}
public void setBtnname(String btnname) {
this.btnname = btnname;
pingObservers()
}
private void pingObservers() {
setChanged();
notifyObservers();
}
}
Register your SecondController as an Observer of the model. When you set the model to the controller add a line similar to this:
model.addObserver(this);
SecondController must implement java.util.Observer.update(...)
void update(Observable o, Object o1) {
// Set the label text with model value
}
In you event handler in the FirstController when you call the setBtnname() method on your model the update() method in the SecondController will be called. There up to you to add the code to change your label text. Since the label is in the view controlled by SecondController you just need to inject a reference of the label in the controller with #FXML annotation.

Categories