JavaFX - Update a text field through observer pattern - javafx

I am trying to update a text field through observer pattern. The update function in the observer (FXML controller) is called after clicking on a listItem in another controller class. And that works fine. The only problem is that my textfield won't update.
Here is my update function in the observer.
#Override
public void update(Observable o, final Object arg) {
System.out.println("test"); // works
firstNameTextField.setText("test"); // doesn't work (text field is still empty)
System.out.println(firstNameTextField.getText()); //works and shows me the word "test" on my console
}
The funny thing is, if I print the text from the text field on my console it's printing the word "test" on the console. It seems like the text field value is updated but it doesn't show up on the ui.
EDIT:
This is my MainController
public class MainController extends Observable implements Initializable {
private ObservableList<String> items = FXCollections.observableArrayList("item1", "item2");
private List<UserProfile> userProfiles = new ArrayList<UserProfile>();
private String[] tabTitles = { "Profile"};
#FXML
private TabPane tabPane;
#FXML
ListView<String> listView;
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
for (String tabTitle : tabTitles) {
Tab tab = new Tab(tabTitle);
tab.setClosable(false);
tabPane.getTabs().add(tab);
}
tabPane.getSelectionModel().clearSelection();
for (Tab tab : tabPane.getTabs()) {
try {
String newStringValue = tab.getText();
Parent root = FXMLLoader.load(getClass().getResource("profile.fxml"));
tab.setContent(root);
FXMLLoader fxmlLoader = new FXMLLoader();
Object p = fxmlLoader.load(getClass().getResource("profile.fxml").openStream());
if (fxmlLoader.getController() instanceof ProfileController) {
ProfileController profileController = (ProfileController) fxmlLoader.getController();
this.addObserver(profileController);
}
} catch (IOException e) {
e.printStackTrace();
}
}
tabPane.getSelectionModel().selectFirst();
listView.setItems(items);
listView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
setChanged();
notifyObservers();
}
});
}
}
ProfileController
public class ProfileController implements Initializable, Observer {
#FXML
TextField firstNameTextField;
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
}
#Override
public void update(Observable o, final Object arg) {
System.out.println("test"); // works
firstNameTextField.setText("test"); // doesn't work (text field is still empty)
System.out.println(firstNameTextField.getText()); //works and shows me the word "test" on my console
}
}
Can anybody help me out with this?
Thanks!

When you execute
Object p = fxmlLoader.load(getClass().getResource("profile.fxml").openStream());
if (fxmlLoader.getController() instanceof ProfileController) {
ProfileController profileController = (ProfileController) fxmlLoader.getController();
this.addObserver(profileController);
}
you load the structure represented by profile.fxml, and place that hierarchy (including firstNameTextField) in the object you called p. When you invoke update(...) on profileController, it changes the text in the text field that is part of the hierarchy of p. However, you never do anything with p: you don't display it in your UI. So when you change the text of the text field, the changes are of course invisible (because you are changing a text field that isn't displayed).
Presumably, since you said you have the text field displayed, somewhere in the code you couldn't be bothered to include you are loading profile.fxml and displaying the content in the UI. You need to get the reference to that controller, and register it as an observer. Registering an arbitrary instance of the same class will not have the desired effect.

Related

Updating TextField - JavaFx

I have 1 "ViewElements"-Class, 1 Controller and 1 FXML-file.
The ViewElements-Class contains the elements of the FXML like Buttons and textfields.
The Controller-Class contains the Business logic.
I try to update the TextField "textfieldDateiAuswaehlen", I want to set the path of the File into the TextField but my method does not work.
ViewElements:
public class ViewElements {
#FXML private TextField textfieldDateiAuswaehlen;
#FXML private TextArea textareaXmlContent;
#FXML private Button buttonXmlBearbeiten;
#FXML private Button buttonXmlLaden;
#FXML private Button buttonXmlOeffnen;
public ViewElements() {
this.textfieldDateiAuswaehlen= new TextField();
this.textareaXmlContent = new TextArea();
this.buttonXmlBearbeiten = new Button();
this.buttonXmlLaden = new Button();
this.buttonXmlOeffnen = new Button();
}
public TextField getTextfieldDateiAuswaehlen() {
return textfieldDateiAuswaehlen;
}
public void setTextfieldDateiAuswaehlenText(String text) {
this.textfieldDateiAuswaehlen.setText(text);
}
public String getTextfieldDateiAuswaehlenContent() {
return this.textfieldDateiAuswaehlen.getText();
}
public TextArea getTextareaXmlContent() {
return textareaXmlContent;
}
public void setTextareaXmlText(String text) {
this.textareaXmlContent.setText(text);
}
public Button getButtonXmlBearbeiten() {
return buttonXmlBearbeiten;
}
public Button getButtonXmlLaden() {
return buttonXmlLaden;
}
public Button getButtonXmlOeffnen() {
return buttonXmlOeffnen;
}}
Controller:
public class SampleController implements Initializable{
ViewElements viewElems= new ViewElements();
#FXML
private void handleButtonLaden(ActionEvent event){
System.out.println("Klicked");
}
#FXML
private void handleButtonXmlOeffnen(ActionEvent event){
FileChooser filechooser = new FileChooser();
File file = filechooser.showOpenDialog(null);
//Falls eine Datei ausgewaehlt ist
if(file != null){
//Falls TextField leer ist
if(viewElems.getTextfieldDateiAuswaehlenContent().isEmpty()) {
System.out.println(file.getAbsolutePath().toString());
viewElems.getTextfieldDateiAuswaehlen().clear();
String verzeichnis = file.getAbsolutePath().toString();
viewElems.setTextfieldDateiAuswaehlenText(verzeichnis);
Service<Void> service = new Service<Void>() {
#Override
protected Task<Void> createTask() {
return new Task<Void>() {
#Override
protected Void call() throws Exception {
Platform.runLater(() -> viewElems.setTextfieldDateiAuswaehlenText(verzeichnis));
return null;
}
};
}
};
service.start();
System.out.println("PRINT: " + viewElems.getTextfieldDateiAuswaehlenContent());
}
}
}
#Override
public void initialize(URL location, ResourceBundle resources) {
}}
In the screenshot you see that the path is passed to TextField but the TextField in the UI does not update.
Where is my mistake?
When you load the FXML file the FXMLLoader creates the UI nodes corresponding to the elements in the FXML.
If you declare a controller, give the elements fx:id attributes, and declare #FXML-annotated fields in the controller, the FXMLLoader will set those fields in the controller to the UI nodes created from the FXML.
In your code, your controller contains no #FXML-annotated fields. You create an instance of your ViewElements class, which creates some new instances of TextField and Button:
public ViewElements() {
this.textfieldDateiAuswaehlen= new TextField();
this.textareaXmlContent = new TextArea();
this.buttonXmlBearbeiten = new Button();
this.buttonXmlLaden = new Button();
this.buttonXmlOeffnen = new Button();
}
Obviously these are not the same text fields and buttons created by the FXMLLoader.
Presumably, somewhere, you load the FXML and display the UI created by the FXMLLoader; but you don't display the UI nodes created in your ViewElements instance. So when you modify the nodes in your ViewElements instance, you are not modifying the UI you have displayed, and consequently you don't see anything.
You need to place the UI elements directly in the controller (which is perhaps better thought of as a presenter). The only way the FXMLLoader can assign the objects it creates to fields is if those fields are in the controller, because that is the only other object the controller "knows about".
If you want to separate the logic into a different class from the class that contains the UI elements, then make the "controller" the class that has the UI elements, and create a different class containing the implementation of the logic. Then in the "controller" class, just delegate the user event handling to your new class.
I.e. change the fx:controller attribute to point to ViewElements, and refactor as
public class ViewElements {
#FXML private TextField textfieldDateiAuswaehlen;
#FXML private TextArea textareaXmlContent;
#FXML private Button buttonXmlBearbeiten;
#FXML private Button buttonXmlLaden;
#FXML private Button buttonXmlOeffnen;
private SampleController controller ;
public void initialize() {
controller = new SampleController(this);
}
#FXML
private void handleButtonXmlOeffnen(ActionEvent event){
controller.handleButtonXmlOeffnen();
}
public TextField getTextfieldDateiAuswaehlen() {
return textfieldDateiAuswaehlen;
}
public void setTextfieldDateiAuswaehlenText(String text) {
this.textfieldDateiAuswaehlen.setText(text);
}
public String getTextfieldDateiAuswaehlenContent() {
return this.textfieldDateiAuswaehlen.getText();
}
public TextArea getTextareaXmlContent() {
return textareaXmlContent;
}
public void setTextareaXmlText(String text) {
this.textareaXmlContent.setText(text);
}
public Button getButtonXmlBearbeiten() {
return buttonXmlBearbeiten;
}
public Button getButtonXmlLaden() {
return buttonXmlLaden;
}
public Button getButtonXmlOeffnen() {
return buttonXmlOeffnen;
}
}
public class SampleController {
private final ViewElements viewElems ;
public SampleController(ViewElements viewElems) {
this.viewElems = viewElems ;
}
public void handleButtonXmlOeffnen() {
FileChooser filechooser = new FileChooser();
File file = filechooser.showOpenDialog(null);
//Falls eine Datei ausgewaehlt ist
if(file != null){
//Falls TextField leer ist
if(viewElems.getTextfieldDateiAuswaehlenContent().isEmpty()) {
System.out.println(file.getAbsolutePath().toString());
viewElems.getTextfieldDateiAuswaehlen().clear();
String verzeichnis = file.getAbsolutePath().toString();
viewElems.setTextfieldDateiAuswaehlenText(verzeichnis);
Service<Void> service = new Service<Void>() {
#Override
protected Task<Void> createTask() {
return new Task<Void>() {
#Override
protected Void call() throws Exception {
Platform.runLater(() -> viewElems.setTextfieldDateiAuswaehlenText(verzeichnis));
return null;
}
};
}
};
service.start();
System.out.println("PRINT: " + viewElems.getTextfieldDateiAuswaehlenContent());
}
}
}
}

How to prepare FXML Controller to be GC

I have an issue in my application. Any Controllers are garbage collected.
I was prepared one, pretty simple with clear function, but it is no removed from memory.
#Log4j2
public class DialogOpenProjectController extends DialogPane implements Initializable, FXMLController, FXMLPane, FXMLDialogController {
#FXML
private ObjectProperty<ResourceBundle> languageBundle = new SimpleObjectProperty<>();
#FXML
private JFXTabPane TAB_PANE_OPEN_PROJECT;
#FXML
private JFXButton BUTTON_CONFIRM;
private Tab tabOpenNewProject;
private Tab tabOpenExistingProject;
private Stage stage;
private ChangeListener<? super ResourceBundle> languageListener = this::languageChange;
private ChangeListener<? super Tab> selectedTabListener = this::selectedTabChanged;
{
tabOpenExistingProject = new Tab();
tabOpenNewProject = new Tab();
}
/**
* Initializes the controller class.
*/
#Override
public void initialize(URL url, ResourceBundle rb) {
log.trace(LogMessages.MSG_CTRL_INITIALIZATION);
this.languageBundle.setValue(rb);
initTabs();
addSelectedTabListener();
addDisableButtonListener();
addLanguageListener();
log.trace(LogMessages.MSG_CTRL_INITIALIZED);
}
#FXML
public void cancel() {
this.stage.close();
clean();
}
#FXML
public void confirm() {
if (TAB_PANE_OPEN_PROJECT.getSelectionModel().getSelectedItem().equals(tabOpenNewProject)) {
actionNewProject();
} else if (TAB_PANE_OPEN_PROJECT.getSelectionModel().getSelectedItem().equals(tabOpenExistingProject)) {
actionOpenProject();
} else {
//To to show error
}
this.stage.close();
clean();
}
private void initTabs() {
TAB_PANE_OPEN_PROJECT.getSelectionModel().select(tabOpenNewProject);
}
private void addSelectedTabListener() {
TAB_PANE_OPEN_PROJECT.getSelectionModel().selectedItemProperty().addListener(selectedTabListener);
}
private void addDisableButtonListener() {
//nothing to do temporary
}
private void clean() {
this.languageBundle.removeListener(languageListener);
languageBundle.unbind();
languageBundle.setValue(null);
TAB_PANE_OPEN_PROJECT.getSelectionModel().selectedItemProperty().removeListener(selectedTabListener);
TAB_PANE_OPEN_PROJECT.getSelectionModel().clearSelection();
TAB_PANE_OPEN_PROJECT.getTabs().clear();
BUTTON_CONFIRM.disableProperty().unbind();
selectedTabListener = null;
languageListener = null;
tabOpenNewProject = null;
tabOpenExistingProject = null;
stage = null;
getChildren().clear();
}
private void addLanguageListener() {
this.languageBundle.addListener(languageListener);
}
private void languageChange(ObservableValue<? extends ResourceBundle> observable, ResourceBundle oldValue, ResourceBundle newValue) {
reloadElements();
}
private String getValueFromKey(String key) {
return this.languageBundle.getValue().getString(key);
}
private void reloadElements() {
// Nothing to do
}
public void setStage(Stage stage) {
this.stage = stage;
}
private void selectedTabChanged(ObservableValue<? extends Tab> observable, Tab oldValue, Tab newValue) {
if (newValue.equals(tabOpenNewProject)) {
BUTTON_CONFIRM.setText(getValueFromKey(Keys.CREATE));
} else if (newValue.equals(tabOpenExistingProject)) {
BUTTON_CONFIRM.setText(getValueFromKey(Keys.OPEN));
}
}
}
For loading FXML files I use Singleton Class ScreenManager. This method is called to load this Dialog :
public void showNewDialog( FilesFXML fxml) {
FXMLLoader loader = new FXMLLoader(getClass().getResource(fxml.toString()), context.getBundleValue());
try {
Stage dialogStage = new Stage();
AnchorPane dialogwindow = (AnchorPane) loader.load();
FXMLDialogController controller = loader.getController();
dialogStage.initModality(Modality.WINDOW_MODAL);
dialogStage.initOwner(this.getStage());
Scene scene = new Scene(dialogwindow);
dialogStage.setScene(scene);
dialogStage.setResizable(false);
controller.setStage(dialogStage);
dialogStage.showAndWait();
} catch (Exception ex) {
log.error(ex.getMessage());
log.error(ex.getCause());
ex.printStackTrace();
}
}
I was checked it in VisualVM and I see one 1 of this controller and 2 lambdas ( I suppose it consiste initialization of 2 listener)
But event clear function is called this dialog is still in memory and cannot be garbage collected. I have no more idea how to force removing it. It is reallegrave, because it consist all of my Controllers.
I'm pretty confused what you are asking and what you are talking about. Nevertheless, if that dialog window is open, there is no way it can be garbage collected.
The JavaFX main API is holding a strong reference of the stage (maybe the scene too).
The stage/scene is holding strong reference of all the nodes specified in your FXML file.
Some of those nodes are holding strong reference of the controller because you have specified action event handlers in the controller.
In order to make the controller eligible for garbage collection, you need to break this chain somewhere.
The easiest way is to close the dialog window. When the window is closed, because the JavaFX engine does not need to manage render pulses anymore, it will release its reference to the stage. At that point, the whole thing under that stage is eligible for garbage collection, if nothing else (that itself is not eligible for garbage collection) is holding strong references of them.

JavaFX: Retrieve a Node

I have two FXML documents each one represents a view, let's call them View1 and View2, and I have placed each one in a separate Tab, (Tab1 and Tab2) inside a TabPane.
Now in the Controller1 of View1 I have an event that will switch the selectedItem of my TabPane from Tab1 to Tab2. My question is how can I access my TabPane from Controller1
In general. How do we retrieve a certain Node in Javafx.
Edit
View1
<VBox fx:controller="controllers.Controller1">
<Button onAction="#openView2"/>
</VBox>
Controller1
public class Controller1{
public void openView2(){
//What should I do here
}
}
MainView
<TabPane fx:id="tabPane" fx:controller="controllers.MainController"/>
MainController
public class MainController implements Initializable {
#FXML
public TabPane tabPane;
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
try {
tabPane.getTabs().add(createView1Tab());
tabPane.getTabs().add(createView2Tab());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
protected Tab createView1Tab() throws IOException {
Tab tab = new Tab();
tab.setContent(FXMLLoader.load(getClass().getResource("/views/View1.fxml")));
return tab;
}
protected Tab createView2Tab() throws IOException {
Tab tab = new Tab();
tab.setContent(FXMLLoader.load(getClass().getResource("/views/View2.fxml")));
return tab;
}
}
You should create a "view model" which encapsulates the current state of the view, and share it with each of the controllers. Then observe it from your main controller and respond accordingly.
For example:
public class ApplicationState {
private final StringProperty currentViewName = new SimpleStringProperty();
public StringProperty currentViewNameProperty() {
return currentViewName ;
}
public final String getCurrentViewName() {
return currentViewNameProperty().get();
}
public final void setCurrentViewName(String viewName) {
currentViewNameProperty().set(viewName);
}
}
Now you can do (note I also removed your redundant repetitive code here):
public class MainController implements Initializable {
#FXML
public TabPane tabPane;
private final ApplicationState appState = new ApplicationState();
private final Map<String, Tab> views = new HashMap<>();
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
try {
tabPane.getTabs().add(createViewTab("View1", new Controller1(appState)));
tabPane.getTabs().add(createViewTab("View2", new Controller2(appState)));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
appState.currentViewNameProperty().addListener((obs, oldView, newView) ->
tabPane.getSelectionModel().select(views.get(newView)));
appState.setCurrentViewName("View1");
}
protected Tab createViewTab(String viewName, Object controller) throws IOException {
Tab tab = new Tab();
FXMLLoader loader = new FXMLLoader(getClass().getResource("/views/"+viewName+".fxml"));
loader.setController(controller);
tab.setContent(loader.load());
views.put(viewName, tab);
return tab;
}
}
Now your controllers just have to do:
public class Controller1{
private final ApplicationState appState ;
public Controller1(ApplicationState appState) {
this.appState = appState ;
}
public void openView2(){
appState.setCurrentViewName("View2");
}
}
Note that since the controllers don't have no-arg constructors, I am setting them in code with loader.setController(...). This means you have to remove the fx:controller attribute from the fxml files for view1 and view2, e.g. your View1.fxml becomes:
<VBox xmlns="..."> <!-- no fx:controller here -->
<Button onAction="#openView2"/>
</VBox>
The advantage of this design is that when your boss walks into your office in 8 months and says "The customer doesn't like the tab pane, replace it with something that only shows one screen at a time", it's very easy to make changes like that as everything is properly decoupled. (You would only have to change the main view and its controller, none of the other views or controllers would change at all.) If you exposed the tab pane to all the other controllers, you would have to find all the places you had accessed it to make changes like that.

JavaFX Change label text from another class with controller

I want to change the text of a Label with the controller from another class. I have made a method in the FXMLDocumentController, which sets the text to the label:
public void setLabelText(String text)
{
lbZeit.setText(text);
}
Now I want to change this text from another class like my SerialHandlerClass. First, I need the controller, am I right? So I did this:
FXMLLoader loader = new FXMLLoader(FXMLDocumentController.class.getResource("FXMLDocument.fxml"));
loader.load();
controller = (FXMLDocumentController) loader.getController();
Now I run the "setLabelText" method....
controller.setLabelText("asd");
... and nothing happens...
It's very funny, because when I add System.out.println(text); to the "setLabelText(String text)" method, the program writes the correct text to the console.
But, why?
Sorry for my bad english, it's not my native language :)
Thanks,
Julian
You are not updating the label because you are creating another instance of FXMLDocumentController when you use the FXMLoader.
You should set the controller instance, that contains the label, as a parameter to the other Class.
Below you have the code that could solve your need. Here I set the Controller instance to the Connector class, so you can call the setLabelText method from the other class:
public class Connector {
public static void Connecting(FXMLDocumentController controller) {
try {
System.out.println("Connector.Connecting(): Called");
controller.setLabelText("Bye World");
} catch (IOException ex) {
Logger.getLogger(Connector.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
public class FXMLDocumentController implements Initializable {
#FXML
private Label label;
#FXML
private void handleButtonAction(ActionEvent event) {
System.out.println("FXMLDocumentController.#handleButtonAction");
label.setText("Hello World!");
Connector.Connecting(this);
}
#Override
public void initialize(URL url, ResourceBundle rb) {
// TODO
}
public void setLabelText(String text)
{
System.out.println("FXMLDocumentController.setLabelText(): Called");
label.setText(text);
}
}
Note:
If your routine is going to take longer to execute whatever it needs to, you might want to use a Task, so you don't freeze your UI. To update the Label, you have to bind the text property and then update the Text value using the updateMessage() method.
public class FXMLDocumentController implements Initializable {
#FXML
private Label label;
#FXML
private void handleButtonAction(ActionEvent event) {
System.out.println("FXMLDocumentController.#handleButtonAction");
label.setText("Hello World!");
Task<Boolean> connectorTask = new ConnectorTask();
label.textProperty().bind(connectorTask.messageProperty());
connectorTask.setOnSucceeded(e -> {
// this is going to be called if the task ends up without error
label.textProperty().unbind();
});
new Thread(connectorTask).start();
}
#Override
public void initialize(URL url, ResourceBundle rb) {
// TODO
}
//public void setLabelText(String text)
//{
// System.out.println("FXMLDocumentController.setLabelText(): Called");
// label.setText(text);
//}
public class ConnectorTask extends Task<Boolean> {
#Override
protected Boolean call() throws Exception {
// ... do whatever you need here
// then you call this method to update the TextProperty from the Label that was bound.
updateMessage("Bye World");
return Boolean.TRUE;
}
}
}
NOTE:
There is a possible duplicate question for this, please see my answer for this question here!

Using a button in a tableColumn to add a song to a playlist javafx

I was want have a table column with a button labeled "Add to Playlist". The row in the column represents a song. I have the following class:
private class ButtonCell extends TableCell<Record, Boolean> {
final Button cellButton = new Button("Add to PlayList");
ButtonCell(){
cellButton.setOnAction(new EventHandler<ActionEvent>(){
#Override
public void handle(ActionEvent t) {
// do something when button clicked
//playList.add(this.getTableRow().getItem());
}
});
}
//Display button if the row is not empty
#Override
protected void updateItem(Boolean t, boolean empty) {
super.updateItem(t, empty);
if(!empty){
setGraphic(cellButton);
}
}
}
EventHandler<ActionEvent> btnNewHandler =
new EventHandler<ActionEvent>(){
#Override
public void handle(ActionEvent t) {
}
};
And I want to be able to do this 'playList.add(this.getTableRow().getItem());'
Is there a way to do this?
The gist of the question is how do I get the information of a cell and add that information to an observable list?
Thanks
For some reason, getTableRow() returns a raw TableRow object, instead of a generically typed one. In other words the method signature is
public class TableCell<S,T> {
public TableRow getTableRow();
}
when I think it should be
public class TableCell<S,T> {
public TableRow<S> getTableRow();
}
Because of this, the expresison getTableRow().getItem() has a compile-time type of Object, instead of the same type as the table (Record in your example), so you need a downcast. This should work (assuming you have everything else set up as I expect you do):
playList.add((Record) this.getTableRow().getItem() );
A different technique entirely is to make the column type Record, so you can just call getItem() directly on the cell. The answer to this question uses this approach.

Resources