How to attach event handlers to controller in MVC | JavaFX - javafx

I am having trouble understanding how to apply the mvc pattern with JavaFX.
Here are my questions with respect to the code below, since I need to follow the pattern given in the code:
a) How can I attach an event handler of the button which is present in my ViewA to the code in my ControllerA (specifically, attachEventHandlers() method). For example, I want my button to populate the comboBox in ViewA with the results of getModelItems() method from controller.
Note that the method getModelItems() is private.
b) I would have multiple buttons and event handlers in my view. How will I bind each one of them uniquely to the controller?
c) I want to invoke setName(String name) on my model in the controller, and the parameter I want to pass is the selected value on the comboBox in viewA. How can I achieve this?
Thank you so much for any help!
Below is the code referred in the description.
Controller:
import model.ModelA;
import view.ViewA;
public class ControllerA {
private ViewA view;
private ModelA model;
public ControllerA(ViewA view, ModelA model) {
//initialise model and view fields
this.model = model;
this.view = view;
//populate combobox in ViewB, e.g. if viewB represented your ViewB you could invoke the line below
//viewB.populateComboBoxWithCourses(setupAndRetrieveCourses());
this.attachEventHandlers();
}
private void attachEventHandlers() {
}
private String[] getModelItems() {
String[] it = new String[2];
it[0] = "0";
it[1] = "1";
return it;
}
}
Model:
public class ModelA {
private String name;
public Name() {
name = "";
}
public void setName(String name) {
this.name = name;
}
#Override
public String toString() {
return "Name = " + name;
}
}
View:
import javafx.scene.layout.BorderPane;
//You may change this class to extend another type if you wish
public class ViewA extends BorderPane {
public BorderPane bp;
public ViewA(){
this.bp = new BorderPane();
ComboBox comboBox = new ComboBox();
Button button1 = new Button("Populate");
bp.setTop(button1);
bp.setBottom(comboBox);
}
}
Loader:
public class ApplicationLoader extends Application {
private ViewA view;
#Override
public void init() {
//create model and view and pass their references to the controller
ModelA model = new ModelA();
view = new ViewA();
new ControllerA(view, model);
}
#Override
public void start(Stage stage) throws Exception {
//whilst you can set a min width and height (example shown below) for the stage window,
//you should not set a max width or height and the application should
//be able to be maximised to fill the screen and ideally behave sensibly when resized
stage.setMinWidth(530);
stage.setMinHeight(500);
stage.setTitle("Final Year Module Chooser Tool");
stage.setScene(new Scene(view));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}

You add delegates to your ViewA to allow for access:
public class ViewA extends BorderPane {
ComboBox comboBox;
Button button1;
public ViewA(){
comboBox = new ComboBox();
button1 = new Button("Populate");
setTop(button1);
setBottom(comboBox);
}
// Add delegates for all functionality you want to make available through ViewA
public ObservableList<String> getItems() { return comboBox.getItems(); }
public void setOnButton1Action(...) { ... }
public void setOnButton2Action(...) { ... }
...
}
You can go as broad or as narrow as you like, based on how much you want to manage through ViewA.

Related

JavaFX static ObservableList not refreshing ComboBox

What I'm trying to do is have a single class that maintains a static ObservableList of countries. I want to display these countries in a ComboBox. I've got this part working fine. Now, I also want to enable the user to add new countries to the list. So, there is a button beside the combo box that will show another dialog allowing entry of another country name. After the user enters the country name and clicks save, I would like the single static ObservableList to be updated with the new country and then it show up in the ComboBox. This part is not happening.
I'll show what DOES work, and what does not.
Saving a reference to the static list and updating that works. Like so:
public class CustomerController implements Initializable {
private ObservableList<Country> countryList;
#Override
public void initialize(URL url, ResourceBundle rb) {
countryList = Country.getCountryList();
comboCountry.setItems(countryList);
}
...
// Fired when clicking the "new country" button
#FXML
void handleNewCountry(ActionEvent event) {
Country country = new Country();
country.setCountry("Austria");
countryList.add(country);
}
}
This is what I would like to do, however it does not work:
public class CustomerController implements Initializable {
#FXML
private ComboBox<Country> comboCountry;
#Override
public void initialize(URL url, ResourceBundle rb) {
comboCountry.setItems(Country.getCountryList());
}
#FXML
void handleNewCountry(ActionEvent event) {
showScene("Country.fxml", "dialog.newCountry");
}
private void showScene(String sceneResource, String titleResource) {
try {
FXMLLoader loader = new FXMLLoader(
getClass().getResource(sceneResource),
resourceBundle
);
Scene scene = new Scene(loader.load());
getNewStage(resourceBundle.getString(titleResource), scene).showAndWait();
} catch (IOException e) {
e.printStackTrace();
}
}
private Stage getNewStage(String title, Scene scene) {
Stage stage = new Stage();
stage.setTitle(title);
stage.setResizable(false);
stage.setScene(scene);
stage.initOwner(rootPane.getScene().getWindow());
stage.initModality(Modality.APPLICATION_MODAL);
return stage;
}
}
The Country class:
public class Country extends BaseModel {
private int countryID;
private StringProperty country;
private static ObservableList<Country> countryList; // The static observable list
public Country() {
countryList = FXCollections.observableArrayList();
country = new SimpleStringProperty();
}
public int getCountryID() {
return countryID;
}
public void setCountryID(int countryID) {
this.countryID = countryID;
}
public StringProperty countryProperty() {
return this.country;
}
public String getCountry() {
return this.country.get();
}
public void setCountry(String country) {
this.country.set(country);
}
public boolean equals(Country country) {
if (this.getCountry().compareToIgnoreCase(country.getCountry()) != 0) {
return false;
}
return true;
}
public static ObservableList<Country> getCountryList() {
if (countryList.size() < 1) {
updateCountryList();
}
return countryList;
}
public static void updateCountryList() {
countryList.clear();
ArrayList<Country> daoList = CountryDao.listCountries();
for (Country country : daoList) {
countryList.add(country);
}
}
#Override
public String toString() {
return this.getCountry();
}
}
And the dialog for entering a new country:
public class CountryController implements Initializable {
#FXML
private TextField textCountry;
#Override
public void initialize(URL url, ResourceBundle rb) {
}
#FXML
void handleSave(ActionEvent event) {
Country country = new Country();
country.setCountry(textCountry.getText().trim());
CountryDao.insert(country); // Insert the country into the database
Country.updateCountryList(); // Update the static ObservableList
close();
}
#FXML
void handleCancel() {
close();
}
void close() {
final Stage stage = (Stage) textCountry.getScene().getWindow();
stage.close();
}
}
So, my theory is that somehow the ComboBox is creating a new instance of the ObservableList when setItems is called. I'm really not sure though. A static object should only have one instance, so updating it from anywhere should update that ComboBox. Anyone know what's up with this?
You're creating a new ObservableList instance every time the Country constructor is invoked. This way a list different to the one used with the ComboBox is modified.
If you really need to keep the list of countries in a static field (this is considered bad practice), you should make sure to only create a single ObservableList:
private static final ObservableList<Country> countryList = FXCollections.observableArrayList();
(Remove the assignment of this field from the constructor too.)

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

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.

Binding an enum's toString() to a Label

I have set up my application to change its function based on an enum. The value of a variable linked to this enum will determine how the program interprets certain actions like mouse clicks and so on. I would like a Label (perhaps in the status area in the bottom left) to reflect what the current "mode" the application is in, and display a readable message for the user to see.
Here's my enum:
enum Mode {
defaultMode, // Example states that will determine
alternativeMode; // how the program interprets mouse clicks
// My attempt at making a property that a label could bind to
private SimpleStringProperty property = new SimpleStringProperty(this, "myEnumProp", "Initial Text");
public SimpleStringProperty getProperty() {return property;}
// Override of the toString() method to display prettier text
#Override
public String toString()
{
switch(this) {
case defaultMode:
return "Default mode";
default:
return "Alternative mode";
}
}
}
From what I've gathered, what I'm looking for is a way to bind an enum's toString() property (which I overrode into more digestable form) to this label. The binding would be so that whenever I set something like
applicationState = Mode.alternativeMode;
the label will display the toString() results automatically, without me needing to place a leftStatus.setText(applicationState.toString()) every time I do that.
Here's what I've tried: (in my main controller class):
leftStatus.textProperty().bind(applicationState.getProperty());
That sets the label to the initial text, but won't update when I update the applicationState enum.
What am I doing wrong?
Instead of adding a property to the enum class, why not use a ObjectProperty for the application state? Have a look at this MCVE:
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class Example extends Application {
private ObjectProperty<Mode> appState = new SimpleObjectProperty<>(Mode.DEFAULT);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
Button btn = new Button("Toggle mode");
btn.setOnMouseClicked((event) -> appState.setValue(appState.get() == Mode.DEFAULT ? Mode.ALTERNATIVE : Mode.DEFAULT));
Label lbl = new Label();
lbl.textProperty().bind(appState.asString());
FlowPane pane = new FlowPane();
pane.getChildren().addAll(btn, lbl);
primaryStage.setScene(new Scene(pane));
primaryStage.show();
}
public enum Mode {
DEFAULT("Default mode"),
ALTERNATIVE("Alternative mode");
private String description;
private Mode(String description) {
this.description = description;
}
#Override
public String toString() {
return description;
}
}
}
Use asString to get a StringBinding from a Property<Mode> containing the value of the property converted to String using the object's toString method.
Example:
#Override
public void start(Stage primaryStage) {
ComboBox<Mode> combo = new ComboBox<>();
combo.getItems().setAll(Mode.values());
Label label = new Label();
// use "state" property from combo box
// (you could replace combo.valueProperty() with your own property)
label.textProperty().bind(combo.valueProperty().asString());
Scene scene = new Scene(new VBox(combo, label), 200, 200);
primaryStage.setScene(scene);
primaryStage.show();
}
Otherwise, if you want the property value contained in the enum, you could use Bindings.selectString, provided you rename the getProperty() method to propertyProperty() to adhere the naming conventions:
enum Mode {
...
public StringProperty propertyProperty() {return property;}
...
}
private final Random random = new Random();
#Override
public void start(Stage primaryStage) {
ComboBox<Mode> combo = new ComboBox<>();
combo.getItems().setAll(Mode.values());
Label label = new Label();
// use "state" property from combo box
// (you could replace combo.valueProperty() with your own property)
label.textProperty().bind(Bindings.selectString(combo.valueProperty(), "property"));
Scene scene = new Scene(new VBox(combo, label), 200, 200);
scene.setOnMouseClicked(evt -> {
// change property values at random
Mode.defaultMode.propertyProperty().set(random.nextBoolean() ? "a" : "b");
Mode.alternativeMode.propertyProperty().set(random.nextBoolean() ? "c" : "d");
});
primaryStage.setScene(scene);
primaryStage.show();
}

Updating after data binding does not update content in UI

Sorry, but I must have a mental lapsus right now, because I don't see where the problem is, and should be trivial. I've prepared a simple scenario where I bind a field to a bean property using the BeanFieldGroup, and when I click the Change and Reset buttons, the model is set with the correct values, but the textfield in the UI is not being updated.
I'm using Vaadin4Spring, but should not be the issue.
import com.vaadin.data.fieldgroup.BeanFieldGroup;
import com.vaadin.navigator.View;
import com.vaadin.navigator.ViewChangeListener;
import com.vaadin.spring.annotation.SpringView;
import com.vaadin.ui.Button;
import com.vaadin.ui.Notification;
import com.vaadin.ui.TextField;
import com.vaadin.ui.VerticalLayout;
import java.io.Serializable;
#SpringView(name = "test")
public class TestView extends VerticalLayout implements View {
private TextField txtTest = new TextField("Test");
private Button btnChange = new Button("Click!");
private Button btnReset = new Button("Reset");
private TestBean testBean = new TestBean();
public TestView() {
txtTest.setImmediate(true);
addComponent(txtTest);
addComponent(btnChange);
addComponent(btnReset);
BeanFieldGroup<TestBean> binder = new BeanFieldGroup<>(TestBean.class);
binder.setItemDataSource(testBean);
binder.setBuffered(false);
binder.bind(txtTest, "text");
initComponents();
}
private void initComponents() {
btnChange.addClickListener(new Button.ClickListener() {
#Override
public void buttonClick(Button.ClickEvent event) {
testBean.setText("Hello world!");
}
});
btnReset.addClickListener(new Button.ClickListener() {
#Override
public void buttonClick(Button.ClickEvent event) {
testBean.setText("");
}
});
}
#Override
public void enter(ViewChangeListener.ViewChangeEvent event) {
Notification.show("Test");
}
public class TestBean implements Serializable {
private String text;
public TestBean() {
text = "";
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
}
The closest thing I have found is binder.discard(), which forces all bound fields to re-read its value from the bean. Yes, it still has to be called manually, but is still far less painful than getItemDataSource().getItemProperty(...).setValue(...). If there are any concerns with this brute-force approach then of course one can call Field.discard() directly on the fields that should be affected.
You are calling a bean setter directly and because Java doesn't provide any way to listen that kind of changes, the Vaadin property (or a TextField) doesn't know that the value has been changed. If you change the value through a Vaadin property by saying
binder.getItemDataSource().getItemProperty("text").setValue("new value");
then you see "new value" on the TextField, and because buffering is disabled, testBean.getText() also returns "new value".

Resources