In my current JavaFX app, there can be different users which have different data relating to them. In different parts of the app labels, tables, graphs etc are bound to observable properties in the user class.
The problem comes when changing users. The bindings are still bound to the previous user. Is there a better way to update this other than rebinding all the parts of the UI on a user change?
The user data is stored in a DataManager class which is passed to all controllers, so they have access to the same data.
DataManager example:
public class DataManager {
private ObservableList<User> userList = FXCollections.observableArrayList();
private User currentUser;
public void addUser(String name, int age, double height, double weight) {
User newUser = new User(name, age, height, weight);
try {
DatabaseWriter.createDatabase();
newUser.setId(UserDBOperations.insertNewUser(newUser));
} catch (SQLException e) {
e.printStackTrace();
}
userList.add(newUser);
}
public void deleteUser(User user) {
try {
DatabaseWriter.createDatabase();
UserDBOperations.deleteExistingUser(user.getId());
} catch (SQLException e) {
e.printStackTrace();
}
userList.remove(user);
}
public updateCurrentUser();
public changeUser();
}
Example User class:
public class User {
private int id;
private StringProperty name;
private IntegerProperty age;
private DoubleProperty height;
private DoubleProperty weight;
private DoubleProperty bmi;
private DoubleProperty totalDistance;
private ObservableList<Event> eventList = FXCollections.observableArrayList();
Is there a better way to use model data that would work better in this situation? I can provide more code/app context if it would help in answering.
Thanks
EDIT 1:
public class ParentController {
private DataManager dataManager = new DataManager();
private ChildController childController1;
private ChildController childController2;
public void initializeChild() {
childController1.setDataManager(dataManager);
childController2.setDataManager(dataManager);
// Controllers for different FXML Files
//
}
}
public class ChildController {
/* Child controller has elements which need data
from the DataManager
*/
private DataManager dataManager;
public void setDataManager(DataManager dataManager) {
this.dataManager = dataManager;
}
}
The main problem I'm trying to overcome is how to keep the database, the model stored in memory, and all the different pages in sync with the same data model. Any suggestions or resources that could point me in the right direction would be great.
EDIT 2: Added more context about database connection
Simply put, you must rebind the controls to display the new data.
When you bind a TextProperty to a user.NameProperty, for example, that binding is specific for that one user object. Even if you change the user, the binding is still pointing back to the original user.
One possible and simple solution is to use a Singleton class to store your selected User. This will allow the selected user to be visible to your entire application:
User.java
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class User {
private StringProperty username = new SimpleStringProperty();
public User(String username) {
this.username.set(username);
}
public String getUsername() {
return username.get();
}
public StringProperty usernameProperty() {
return username;
}
}
GlobalData.java
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
public class GlobalData {
// Global property to hold the currently-selected user
private ObjectProperty<User> selectedUser = new SimpleObjectProperty<>();
private static GlobalData ourInstance = new GlobalData();
public static GlobalData getInstance() {
return ourInstance;
}
private GlobalData() {
}
public User getSelectedUser() {
return selectedUser.get();
}
public ObjectProperty<User> selectedUserProperty() {
return selectedUser;
}
public void setSelectedUser(User selectedUser) {
this.selectedUser.set(selectedUser);
}
}
Now, in your UI controllers, you would just need to create a listener to watch for changes to the selectedUser. When the user changes, just rebind the UI elements in each controller.
Here is a simple MCVE to demonstrate:
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.Random;
public class Main extends Application {
// The Label which holds the username value
private Label lblUsername = new Label();
// Grab a reference to the GlobalData singleton
private GlobalData globalData = GlobalData.getInstance();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
// Create the initial sample User
globalData.setSelectedUser(new User("User #1"));
// Simple interface
VBox root = new VBox(10);
root.setPadding(new Insets(10));
root.setAlignment(Pos.CENTER);
// HBox to hold the username display
HBox hBox = new HBox(5);
hBox.setAlignment(Pos.CENTER);
// Add the username labels to the HBox
hBox.getChildren().addAll(
new Label("Username:"),
lblUsername
);
// Bind the Label to display the current selected user's username
lblUsername.textProperty().bind(globalData.getSelectedUser().usernameProperty());
// Here we use a listener on the the selectedUser property. When it changes, we
// call the rebindUser() method to update the UI
globalData.selectedUserProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
rebindUser(newValue);
}
});
// Add a button that just changes the selectedUser
Button button = new Button("Change User");
// Set the action for the button to change users
button.setOnAction(this::changeUser);
// Add the HBox and button to the root layout
root.getChildren().addAll(hBox, button);
// Show the Stage
primaryStage.setWidth(400);
primaryStage.setHeight(200);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
private void changeUser(ActionEvent event) {
// Create a new user with a random # at the end
Random rnd = new Random();
int num = rnd.nextInt(50 + 2);
globalData.setSelectedUser(new User("User #" + num));
}
// This method accepts a new User object and updates all the displayed bindings
private void rebindUser(User newUser) {
lblUsername.textProperty().unbind();
lblUsername.textProperty().bind(newUser.usernameProperty());
}
}
This produces the following output:
Clicking the button will create a random new user, update the selectedUser property of the GlobalData singleton, and the Label gets updated through the rebindUser() method.
Related
I've been searching for a while, but all I found seems very old and can't get it to work and I'm very confused.
I have a tableview with a checkbox in a column header (select all) and another checkbox for each row (select row). What I am trying to achieve is to get all the rows whose checkboxes are checked to perform an action.
Here's what it looks like:
And here's the code in my controller:
package com.comparador.controller;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.comparador.ComparadorPreciosApplication;
import com.comparador.entity.Commerce;
import com.comparador.entity.Items;
import com.comparador.entity.ShoppingListPrices;
import com.comparador.repository.CommerceRepository;
import com.comparador.repository.ProductRepository;
import com.comparador.service.ShoppingService;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.stage.Stage;
import javafx.util.converter.IntegerStringConverter;
#Component
public class ShoppingController implements Initializable {
// #Autowired
// #Qualifier("lblTitulo")
private String titulo = "Productos";
#Autowired
private ProductRepository productRepository;
#Autowired
private CommerceRepository commerceRepository;
#Autowired
private ShoppingService shoppingService;
#FXML
private Label lblTitulo;
#FXML
private Button btBack;
#FXML
private TableView<Items> tvProducts;
#FXML
private TableColumn<Items, CheckBox> colSelected; //THE CHECKBOX COLUMN
#FXML
private TableColumn<Items, String> colName;
#FXML
private TableColumn<Items, Integer> colAmount;
#FXML
private TableView<ShoppingListPrices> tvTotalPrices;
#FXML
private TableColumn<ShoppingListPrices, String> colCommerce;
#FXML
private TableColumn<ShoppingListPrices, Double> colTotal;
private CheckBox selectAll;
List<ShoppingListPrices> shoppingList = new ArrayList<>();
#Override
public void initialize(URL location, ResourceBundle resources) {
colName.setCellValueFactory(new PropertyValueFactory<>("name"));
colAmount.setCellValueFactory(new PropertyValueFactory<>("amount"));
colAmount.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
// colSelected.setCellFactory(CheckBoxTableCell.forTableColumn(colSelected));
// colSelected.setCellValueFactory(cellData -> new ReadOnlyBooleanWrapper(cellData.getValue().getChecked()));
colSelected.setCellValueFactory(new PropertyValueFactory<>("selected"));
colCommerce.setCellValueFactory(new PropertyValueFactory<>("commerceName"));
colTotal.setCellValueFactory(new PropertyValueFactory<>("total"));
lblTitulo.setText(titulo);
tvProducts.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
reloadTableViewProducts();
selectAll = new CheckBox();
selectAll.setOnAction(event -> {
event.consume();
tvProducts.getItems().forEach(item -> {
item.getSelected().setSelected(selectAll.isSelected());
});
});
setShoppingList();
colSelected.setGraphic(selectAll);
}
#FXML
public void editAmount(CellEditEvent<Items, Integer> event) {
Items item = event.getRowValue();
if(event.getTableColumn().getText().equals("Cantidad")) {
item.setAmount(event.getNewValue());
}
setShoppingList();
}
/*
* CLICKING ON A CHECKBOX SHOULD CALL THIS METHOD AND ADD THE ROW TO "selectedItems"
*/
#FXML
public void setShoppingList() {
List<Items> selectedItems = new ArrayList<>();
//Before trying this I was selecting each row by Ctrl + Clicking on it
// List<Items> selectedItems = tvProducts.getSelectionModel().getSelectedItems();
//This didn't seem to work
// List<ShoppingListItems> selectedItems = tvProducts.getItems().filtered(x->x.getSelected() == true);
List<Commerce> commerces = commerceRepository.findByNameContaining("");
ShoppingListPrices pricesMixingCommerces = shoppingService.getCheapestShoppingList(commerces, selectedItems);
List<ShoppingListPrices> pricesByCommerce = shoppingService.getShoppingListsPerCommerce(commerces, selectedItems);
shoppingList = new ArrayList<>();
shoppingList.add(pricesMixingCommerces);
shoppingList.addAll(pricesByCommerce);
ObservableList<ShoppingListPrices> resultOL = FXCollections.observableArrayList();
resultOL.addAll(shoppingList);
tvTotalPrices.setItems(resultOL);
}
#FXML
public void openShoppingList() throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/shoppingList.fxml"));
ShoppingListController shoppingListController = new ShoppingListController();
loader.setControllerFactory(ComparadorPreciosApplication.applicationContext::getBean);
loader.setController(shoppingListController);
shoppingListController.setup(tvTotalPrices.getSelectionModel().getSelectedItem());
try {
Scene scene = new Scene(loader.load(), 800, 400, true, SceneAntialiasing.BALANCED);
Stage stage = new Stage();//(Stage) btBack.getScene().getWindow();
stage.setUserData(tvTotalPrices.getSelectionModel().getSelectedItem());
stage.setScene(scene);
stage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
#FXML
public void goBack() {
FXMLLoader loader = new FXMLLoader(ComparadorPreciosApplication.class.getResource("/index.fxml"));
loader.setControllerFactory(ComparadorPreciosApplication.applicationContext::getBean);
try {
Scene scene = new Scene(loader.load(), 800, 800, false, SceneAntialiasing.BALANCED);
Stage stage = (Stage) btBack.getScene().getWindow();
stage.setScene(scene);
stage.show();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void reloadTableViewProducts() {
List<String> productNames = productRepository.findOnProductPerName("");
List<Items> items = new ArrayList<>();
for(String name : productNames) {
//items.add(new Items(new SimpleBooleanProperty(false), name, 1));
Items item = new Items((CheckBox) new CheckBox(), name, 1);
item.getSelected().setSelected(false);
items.add(item);
}
ObservableList<Items> itemsOL = FXCollections.observableArrayList();
itemsOL.addAll(items);
tvProducts.setItems(itemsOL);
}
}
Your Items class should not reference any UI objects, including CheckBox. The model should ideally not even know the view exists. If you plan on having Items track if it's selected itself, then it should expose a BooleanProperty representing this state. With a properly configured table and column, the check box associated with an item and the item's selected property will remain synchronized. And since the items of the table keep track of their own selected state, getting all the selected items is relatively straightforward. Simply iterate/stream the items and grab all the selected ones.
Here's an example using CheckBoxTableCell:
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
var table = new TableView<Item>();
table.setEditable(true);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
for (int i = 0; i < 50; i++) {
table.getItems().add(new Item("Item #" + (i + 1)));
}
var selectedCol = new TableColumn<Item, Boolean>("Selected");
// configure cell factory to use a cell implementation that displays a CheckBox
selectedCol.setCellFactory(CheckBoxTableCell.forTableColumn(selectedCol));
// link CheckBox and model selected property
selectedCol.setCellValueFactory(data -> data.getValue().selectedProperty());
table.getColumns().add(selectedCol);
var nameCol = new TableColumn<Item, String>("Name");
nameCol.setCellValueFactory(data -> data.getValue().nameProperty());
table.getColumns().add(nameCol);
var button = new Button("Print checked items");
button.setOnAction(e -> {
// filter for selected items and collect into a list
var checkedItems = table.getItems().stream().filter(Item::isSelected).toList();
// log selected items
System.out.printf("There are %,d checked items:%n", checkedItems.size());
for (var item : checkedItems) {
System.out.println(" " + item);
}
});
var root = new BorderPane();
root.setTop(button);
root.setCenter(table);
root.setPadding(new Insets(10));
BorderPane.setMargin(button, new Insets(0, 0, 10, 0));
BorderPane.setAlignment(button, Pos.CENTER_RIGHT);
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
}
public static class Item {
private final StringProperty name = new SimpleStringProperty(this, "name");
public final void setName(String name) { this.name.set(name); }
public final String getName() { return name.get(); }
public final StringProperty nameProperty() { return name; }
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
public final void setSelected(boolean selected) { this.selected.set(selected); }
public final boolean isSelected() { return selected.get(); }
public final BooleanProperty selectedProperty() { return selected; }
public Item() {}
public Item(String name) {
setName(name);
}
#Override
public String toString() {
return String.format("Item(name=%s, selected=%s)", getName(), isSelected());
}
}
}
Note that TableView has a selection model. That is not the same thing. It's used for the selection of rows or cells of the table (and thus works best on a per-table basis). You, however, want to be able to "check" items, and that requires keeping track of that state differently--an item's row could be selected while the item is not checked, and vice versa.
And note I recommend that any model class used with TableView expose JavaFX properties (like the Item class in the example above). It makes it much easier to work with TableView. But that could interfere with other parts of your code (e.g., Spring). In that case, you could do one of three things:
Create a simple adapter class that holds a reference to the "real" object and provides a BooleanProperty. This adapter class would only be used for the TableView.
Create a more complex adapter class that mirrors the "real" class in content, but exposes the properties as JavaFX properties (e.g., BooleanProperty, StringProperty, etc.). Map between them as you cross layer boundaries in your application.
In the controller, or wherever you have the TableView, keep the selected state external to the model class. For instance, you could use a Map<Item, BooleanProperty>.
I probably would only use this approach as a last resort, if ever.
I have a class tasks, which handles multiple tasks using a menu layout, class tasks check the application flow by setting up the stage, with the menu scene to list all individual task. I want to run some task from the available list using there own classes, something like this:
Tasks.java:
package tasks;
import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.scene.control.Button;
public class Tasks extends Application
{
private Stage window;
private Scene menuScene;
private Task1 task1;
public Tasks()
{
this.window=null;
this.menuScene=null;
this.task1=null;
}
private void setMenu()
{
VBox menu=new VBox();
Button newTask1Button=new Button("New Task 1");
newTask1Button.setOnAction(clickEvent -> this.startNewTask1());
menu.getChildren().add(newTask1Button);
//More buttons
this.menuScene=new Scene(menu,400,600);
this.window.setScene(this.menuScene);
}
private void startNewTask1()
{
this.task1=new Task1(this.window);
this.launchTask1();
}
private void launchTask1()
{
if(this.task1!=null)
{
int task1State=1;
//while(task1State==1) //To re-run for pause state
//{
task1State=this.task1.runTask1();
System.out.println("Task1 is in state "+task1State); //In no way part of program, just for debugging. Always give state=-1
//If 1-Paused, then display pause Menu for task1, by calling this.task1.paused(); and then again based on user input re-run runTask1
//If 0-Exit, then change the scene back to menuScene and quit the function
//}
}
}
#Override
public void start(Stage primaryStage)
{
this.window=primaryStage;
this.window.setTitle("Tasks");
this.setMenu();
this.window.show();
}
public static void main(String[] args)
{
Application.launch(args);
}
}
Task1.java:
package tasks;
import javafx.stage.Stage;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.control.Button;
class Task1
{
private Stage window;
private Scene task1Scene;
private boolean intialised;
private int state;
public Task1()
{
}
public Task1(Stage _window)
{
this.window=_window;
this.task1Scene=null; //Will be set later
this.intialised=false;
this.state=-1;
}
private Scene createScene()
{
//Creates some GUI to interact
//Buttons in End, to control exit
HBox menu=new HBox();
Button pauseButton=new Button("Pause");
pauseButton.setOnAction(clickEvent -> this.state=1);
menu.getChildren().add(pauseButton);
Button exitButton=new Button("Exit");
exitButton.setOnAction(clickEvent -> this.state=0);
menu.getChildren().add(exitButton);
Scene scene=new Scene(menu,400,600);
return scene;
}
private void setupControls()
{
//To assign event handlers to interact with GUI
}
public int runTask1()
{
if(!this.intialised)
this.task1Scene=this.createScene();
this.window.setScene(this.task1Scene);
this.setupControls();
//while(this.state==-1);
return this.state;
}
}
The problem with this I face is, function runTask1() is always instantly returning, even though operation assigned using event handlers for Task1 are still running and no event for exit has been generated.
I tried to solve this by setting an instance variable named state and setting it to -1, and putting a while loop till this state variable is not modified. But that completely stops the GUI.
I realised its reason later by Googling, but couldn't determine which way to solve this.
At places, it was suggested to use Threads (not sure how, I don't want multiple processes running in the program) and at places, it was also suggested to set another Event Handler (but, they were running the different process in the start() function (inherited from Application) itself and it was more of transferring the flow rather than returning backwards).
How should I code to keep running only runTask1() till it is not finished, and return to launchTask1() sequentially?
The infinite while loop in method runTask1() in class Task1 is freezing the JavaFX application thread. Just remove it.
Basically your Task1 class is another Scene so when you click on button newTask1Button in class Tasks you simply want to set a new Scene.
Here is class Task1 with the required change.
package tasks;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.control.Button;
public class Task1 {
private Stage window;
private Scene task1Scene;
private boolean intialised;
private int state;
public Task1() {
}
public Task1(Stage _window) {
this.window = _window;
this.task1Scene = null; // Will be set later
this.intialised = false;
this.state = -1;
}
private Scene createScene() {
// Creates some GUI to interact
// Buttons in End, to control exit
HBox menu = new HBox();
Button pauseButton = new Button("Pause");
pauseButton.setOnAction(clickEvent -> this.state = 1);
menu.getChildren().add(pauseButton);
Button exitButton = new Button("Exit");
exitButton.setOnAction(clickEvent -> this.state = 0);
menu.getChildren().add(exitButton);
Scene scene = new Scene(menu, 400, 600);
return scene;
}
private void setupControls() {
// To assign event handlers to interact with GUI
}
public int runTask1() {
if (!this.intialised)
this.task1Scene = this.createScene();
this.window.setScene(this.task1Scene);
this.setupControls();
// while (this.state == -1)
// ;
return this.state;
}
}
As you can see, I simply commented out the while loop. The JavaFX application thread contains a loop that waits for user actions to occur such as moving the mouse or typing a key on the keyboard. You don't have to handle that in your code.
EDIT
As a result of the typo in the code in your question, that you mentioned in your comment to my answer and that you corrected in your question in a subsequent edit, I am editing my answer.
The way a JavaFX application works is that it reacts to user actions. You want class Tasks to be notified when the "state", in class Task1, is changed and the "state" is changed when the user clicks on either pauseButton or exitButton in class Task1. According to the code you posted, you could callback to class Tasks from the event handler of pauseButton.
Class Task1.
(Note comment CHANGE HERE and extra parameter in constructor.)
package tasks;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.control.Button;
public class Task1 {
private Tasks tasks;
private Stage window;
private Scene task1Scene;
private boolean intialised;
private int state;
public Task1(Stage _window, Tasks tasks) {
this.tasks = tasks;
this.window = _window;
this.task1Scene = null; // Will be set later
this.intialised = false;
this.state = -1;
}
private Scene createScene() {
HBox menu = new HBox();
Button pauseButton = new Button("Pause");
pauseButton.setOnAction(clickEvent -> tasks.setState(this.state = 1)); // CHANGE HERE
menu.getChildren().add(pauseButton);
Button exitButton = new Button("Exit");
exitButton.setOnAction(clickEvent -> this.state = 0);
menu.getChildren().add(exitButton);
Scene scene = new Scene(menu, 400, 600);
return scene;
}
private void setupControls() {
// To assign event handlers to interact with GUI
}
public int runTask1() {
if (!this.intialised) {
this.task1Scene = this.createScene();
}
this.window.setScene(this.task1Scene);
this.setupControls();
return this.state;
}
}
Class Tasks
(Added method setState(int).)
package tasks;
import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.scene.control.Button;
public class Tasks extends Application {
private Stage window;
private Scene menuScene;
private Task1 task1;
public Tasks() {
this.window = null;
this.menuScene = null;
this.task1 = null;
}
private void setMenu() {
VBox menu = new VBox();
Button newTask1Button = new Button("New Task 1");
newTask1Button.setOnAction(clickEvent -> this.startNewTask1());
menu.getChildren().add(newTask1Button);
this.menuScene = new Scene(menu, 400, 600);
this.window.setScene(this.menuScene);
}
private void startNewTask1() {
this.task1 = new Task1(this.window, this);
this.launchTask1();
}
private void launchTask1() {
if (this.task1 != null) {
this.task1.runTask1();
}
}
#Override
public void start(Stage primaryStage) {
this.window = primaryStage;
this.window.setTitle("Tasks");
this.setMenu();
this.window.show();
}
public static void main(String[] args) {
Application.launch(args);
}
public void setState(int task1State) {
System.out.println("Task1 is in state " + task1State); // In no way part of program,
// just for debugging.
}
}
I have a pet project which is a 2D game engine.
After creating all the backend functionality, I want to implement a UI for it.
My current plan is to do this via MVC because it strikes me as the most feasible way of doing this (logic first, then UI).
Now, I am unsure how to design/implement this with either Swing or JavaFX, as I do not yet fully understand what the underlying concepts of either are.
Can someone describe to me how to implement MVC with Swing or JavaFX?
You can have a model that is a general one can be used by different views.
To demonstrate it let's first introduce a interface that can be used to listen to such model:
//Interface implemented by SwingView and used by Model
interface Observer {
void observableChanged();
}
Consider a very simple model, with one attribute only: an integer value between 0 and a certain max:
//Generic model. Not dependent on the GUI tool kit. Use by Swing as well as JAvaFX
class Model {
private int value;
private static final int MAX_VALUE = 100;
private Observer observer;
int getValue(){
return value;
}
void setValue(int value){
this.value = Math.min(MAX_VALUE, Math.abs(value));
notifyObserver();
}
int getMaxValue() {
return MAX_VALUE;
}
//-- handle observers
void setObserver(Observer observer) {
this.observer = observer;
}
private void notifyObserver() {
if(observer != null) {
observer.observableChanged();
}
}
}
Note that the model calls observer.observableChanged() when value changes it.
Now let's use this model with a Swing gui that displays a random number :
import java.awt.*;
import java.util.Random;
import javax.swing.*;
//Swing app, using a generic model
public class SwingMVC {
public static void main(String[] args) {
new SwingController();
}
}
//SwingController of the MVC pattern."wires" model and view (and in this case also worker)
class SwingController{
public SwingController() {
Model model = new Model();
SwingView swingView = new SwingView(model);
model.setObserver(swingView); //register view as an observer to model
update(model);
}
//change model
private void update(Model model) {
Random rnd = new Random();
//use swing timer so the change is performed on the Event Dispatch Thread
new Timer(1000,(e)-> model.setValue(1+rnd.nextInt(model.getMaxValue()))).start();
}
}
//view of the MVC pattern. Implements observer to respond to model changes
class SwingView implements Observer{
private final Model model;
private final JLabel label;
public SwingView(Model model) {
this.model = model;
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationByPlatform(true);
frame.setLayout(new GridBagLayout());
label = new JLabel(" - ");
label.setFont(new Font(label.getFont().getName(), Font.PLAIN, 48));
label.setHorizontalTextPosition(SwingConstants.CENTER);
frame.add(label);
frame.pack();
frame.setVisible(true);
}
#Override
public void observableChanged() {
//update text in response to change in model
label.setText(String.format("%d",model.getValue()));
}
}
We can use the very same model and achieve the same functionality with a JavaFx gui:
import java.util.Random;
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.util.Duration;
//JavaFa app, using a generic model
public class FxMVC extends Application{
#Override
public void start(Stage primaryStage) throws Exception {
FxController fxController = new FxController();
Scene scene = new Scene(fxController.getParent());
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(null);
}
}
class FxController{
private final FxView view;
FxController() {
Model model = new Model();
view = new FxView(model);
model.setObserver(view); //register fxView as an observer to model
update(model);
}
//change model
private void update(Model model) {
Random rnd = new Random();
//use javafx animation tools so the change is performed on the JvaxFx application thread
PauseTransition pt = new PauseTransition(Duration.seconds(1));
pt.play();
pt.setOnFinished(e->{
model.setValue(1+rnd.nextInt(model.getMaxValue()));
pt.play();
});
}
Parent getParent(){
return view;
}
}
//View of the MVC pattern. Implements observer to respond to model changes
class FxView extends StackPane implements Observer{
private final Model model;
private final Label label;
public FxView(Model model) {
this.model = model;
label = new Label(" - ");
label.setFont(new Font(label.getFont().getName(), 48));
getChildren().add(label);
}
#Override
public void observableChanged() { //update text in response to change in model
//update text in response to change in model
label.setText(String.format("%d",model.getValue()));
}
}
On the other hand, you can have a model that is more specific, intended to be used with a specific tool-kit, and gain some advantages by using some tool of that tool kit.
For example a model made using JavaFx properties, in this example SimpleIntegerProperty, which simplifies the listening to model changes (does not make use of the Observer interface):
import java.util.Random;
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.scene.*;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.util.Duration;
//JavaFa app, using a JavaFx model
public class FxApp extends Application{
#Override
public void start(Stage primaryStage) throws Exception {
FxController fxController = new FxController();
Scene scene = new Scene(fxController.getParent());
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(null);
}
}
class FxAppController{
private final FxAppView view;
FxAppController() {
FxAppModel model = new FxAppModel();
view = new FxAppView(model);
update(model);
}
//change model
private void update(FxAppModel model) {
Random rnd = new Random();
//use javafx animation tools so the change is performed on the JvaxFx application thread
PauseTransition pt = new PauseTransition(Duration.seconds(1));
pt.play();
pt.setOnFinished(e->{
model.setValue(1+rnd.nextInt(model.getMaxValue()));
pt.play();
});
}
Parent getParent(){
return view;
}
}
//View does not need to implement listener
class FxAppView extends StackPane{
public FxAppView(FxAppModel model) {
Label label = new Label(" - ");
label.setFont(new Font(label.getFont().getName(), 48));
getChildren().add(label);
model.getValue().addListener((ChangeListener<Number>) (obs, oldV, newV) -> label.setText(String.format("%d",model.getValue())));
}
}
//Model that uses JavaFx tools
class FxAppModel {
private SimpleIntegerProperty valueProperty;
private static final int MAX_VALUE = 100;
SimpleIntegerProperty getValue(){
return valueProperty;
}
void setValue(int value){
valueProperty.set(value);
}
int getMaxValue() {
return MAX_VALUE;
}
}
A Swing gui and a JavaFx gui, each uses a different instance of the same Model:
Very new to JavaFX and lacking a bit of knowledge in the way controllers work but here it goes.
My problem is easy. I need to update a Label on the screen during runtime.
This problem has been addressed on this site before:
Java FX change Label text
Java FX change Label text 2
Passing Parameters
Also, are these links describing the same thing but done differently?
But my program is a little different.
The flow of the program is as follows:
The Main Stage has several Objects that extends Pane with a Label inside. These Objects can be right clicked which opens a context menu. An option in the context menu opens a new window with RadioButtons.
The idea is to select one of the RadioButtons and use that string to rewrite the Label back on the Main Stage.
However my code only works once, the first time. All subsequent changes are not shown on the screen. I can even output the Label that was changed to the Console and it shows the correct value, but never updates the Label on the Stage.
Class that has the Label on the screen:
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
public class CoursePane extends Pane {
private Label courseID;
public CoursePane(Label courseID) {
this.courseID = courseID;
}
public String getCourseID() {
return courseID.getText();
}
public Label getCourseLabel() {
return courseID;
}
public void setCourseID(String ID) {
courseID.setText(ID);
}
}
The Context Menu Class that invokes the menu:
public class CourseContext {
static String fxmlfile;
private static Object paneSrc; //the CoursePane that was clicked on
public static void start(CoursePane pane, String courseSrc) {
//Context Menu
ContextMenu contextMenu = new ContextMenu();
//MenuItems
MenuItem item4 = new MenuItem("option");
//add items to context menu
contextMenu.getItems().addAll(item4);
pane.addEventHandler(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.isSecondaryButtonDown()) {
//the coursePane that was right clicked on
paneSrc = event.getSource().toString();
contextMenu.show(pane, event.getScreenX(), event.getScreenY());
item4.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getClassLoader().getResource("my fxml file for the radio Buttons"));
Parent root= loader.load();
ElectiveController electiveController = loader.getController();
electiveController.start( "pass the coursePane that was right clicked on" );
Scene scene = new Scene(root);
Stage stage = new Stage();
stage.setScene(scene);
stage.setTitle("Set Elective");
stage.show();
}
catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
});
}
}
And finally, the class that has the value that Label is supposed to be set to:
public class ElectiveController {
#FXML
private Button setButton;
private RadioButton chk;
//the pane that was right clicked on
private static String courseSource;
public void start(Course courseSrc) { //courseSrc: the Pane you right clicked on
courseSource = courseSrc.getCoursenamenumber().getValue();
}//end start
//sets the course pane with the selected elective radio button
#FXML
private void setElective() {
chk = (RadioButton)humElectiveGroup.getSelectedToggle();
//This is supposed to set the value for the coursePane Object to show on the screen!
MainStage.getCoursePanes().get(courseSource).setCourseID(chk.getText());
Stage stage = (Stage) setButton.getScene().getWindow();
stage.close();
}
}
I have looked into dependency injection, tried binding and passing parameters but getting the same results. I know this is straight forward, any help is appreciated! Thanks.
Here is an mcve of how you could wire up the different parts.
- It can be copy pasted into a single file and invoked.
- Note that it is not meant to represent or mock your application. It is meant to demonstrate a (very basic and simplistic) solution for the issue
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
//main class
public class UpdateViewByMenu extends Application {
private Controller controller;
#Override
public void start(Stage stage) throws Exception {
BorderPane root = new BorderPane();
controller = new Controller();
root.setTop(controller.getMenu());
root.setBottom(controller.getView());
Scene scene = new Scene(root, 350,200);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) { launch(args);}
}
//controller which "wires" view to model
class Controller {
private Model model;
private View view;
private TopMenu menu;
public Controller() {
model = new Model();
view = new View();
menu = new TopMenu();
//wire up menu to model : menu changes update model
menu.getMenuTextProperty().addListener(
e-> model.setCourseID(menu.getMenuTextProperty().get()));
//wire model to view: change in model update view
view. geLabelTextProerty().bind(model.getCourseIDProperty());
//set initial value to show
menu.getMenuTextProperty().set("Not set");
}
Model getModel() {return model;}
Pane getView() { return view;}
MenuBar getMenu() { return menu; }
}
//model which represent the data, in this case label info
class Model{
SimpleStringProperty courseIdProperty;
Model(){
courseIdProperty = new SimpleStringProperty();
}
StringProperty getCourseIDProperty() {
return courseIdProperty;
}
void setCourseID(String id) {
courseIdProperty.set(id);
}
}
//represents main view, in this case a container for a label
class View extends HBox {
private Label courseID;
View() {
courseID = new Label();
getChildren().add(courseID);
}
StringProperty geLabelTextProerty() {
return courseID.textProperty();
}
}
//menu
class TopMenu extends MenuBar{
SimpleStringProperty menuTextProperty;
TopMenu() {
menuTextProperty = new SimpleStringProperty();
Menu menu = new Menu("Select id");
MenuItem item1 = getMenuItem("10021");
MenuItem item2 = getMenuItem("10022");
MenuItem item3 = getMenuItem("10023");
MenuItem item4 = getMenuItem("10024");
menu.getItems().addAll(item1, item2, item3, item4);
getMenus().add(menu);
}
MenuItem getMenuItem(String text) {
MenuItem item = new MenuItem(text);
item.setOnAction(e -> menuTextProperty.set(item.textProperty().get()));
return item;
}
StringProperty getMenuTextProperty() {
return menuTextProperty;
}
}
Do not hesitate to ask for clarifications as needed.
I'm using GXT 3 Grid with InlineEdit mode following (more or less) the example code on their site. I don't think there is a way to get the check box cell to fire the 'EditComplete' event and if so, I'm not sure how I would, upon receiving it, disable the date cell on that same row. Just look for the comment: "// not firing for checkbox:" in the code below.
The following code works in an Eclipse web application project - you just need to use it in your 'onModuleLoad' method as demonstrated here:
public void onModuleLoad() {
GridInlineEditingTest j = new GridInlineEditingTest();
}
Here's the code:
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import com.google.gwt.cell.client.DateCell;
import com.google.gwt.core.client.GWT;
import com.google.gwt.editor.client.Editor.Path;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
import com.sencha.gxt.cell.core.client.form.CheckBoxCell;
import com.sencha.gxt.core.client.ValueProvider;
import com.sencha.gxt.data.shared.ListStore;
import com.sencha.gxt.data.shared.ModelKeyProvider;
import com.sencha.gxt.data.shared.PropertyAccess;
import com.sencha.gxt.data.shared.Store;
import com.sencha.gxt.widget.core.client.FramedPanel;
import com.sencha.gxt.widget.core.client.button.TextButton;
import com.sencha.gxt.widget.core.client.container.BoxLayoutContainer.BoxLayoutPack;
import com.sencha.gxt.widget.core.client.container.VerticalLayoutContainer;
import com.sencha.gxt.widget.core.client.container.VerticalLayoutContainer.VerticalLayoutData;
import com.sencha.gxt.widget.core.client.container.Viewport;
import com.sencha.gxt.widget.core.client.event.CompleteEditEvent;
import com.sencha.gxt.widget.core.client.event.CompleteEditEvent.CompleteEditHandler;
import com.sencha.gxt.widget.core.client.event.SelectEvent;
import com.sencha.gxt.widget.core.client.event.SelectEvent.SelectHandler;
import com.sencha.gxt.widget.core.client.form.CheckBox;
import com.sencha.gxt.widget.core.client.form.DateField;
import com.sencha.gxt.widget.core.client.form.DateTimePropertyEditor;
import com.sencha.gxt.widget.core.client.grid.ColumnConfig;
import com.sencha.gxt.widget.core.client.grid.ColumnModel;
import com.sencha.gxt.widget.core.client.grid.Grid;
import com.sencha.gxt.widget.core.client.grid.Grid.GridCell;
import com.sencha.gxt.widget.core.client.grid.GridView;
import com.sencha.gxt.widget.core.client.grid.editing.GridEditing;
import com.sencha.gxt.widget.core.client.grid.editing.GridInlineEditing;
public class GridInlineEditingTest {
public GridInlineEditingTest() {
VerticalLayoutContainer vlc = new VerticalLayoutContainer();
vlc.add(createGrid(), new VerticalLayoutData(1, 1));
Viewport vp = new Viewport();
vp.add(vlc);
RootPanel.get().add(vp);
}
interface PlaceProperties extends PropertyAccess<Plant> {
ValueProvider<Plant, Date> available();
#Path("id")
ModelKeyProvider<Plant> key();
ValueProvider<Plant, String> name();
ValueProvider<Plant, Boolean> indoor();
}
private static final PlaceProperties properties = GWT.create(PlaceProperties.class);
protected Grid<Plant> grid;
private FramedPanel panel;
private ListStore<Plant> store;
private DateField dateField;
public Widget createGrid() {
if (panel == null) {
ColumnConfig<Plant, String> nameCol = new ColumnConfig<Plant, String>( properties.name(), 220, "Name" );
ColumnConfig<Plant, Date> dateCol = new ColumnConfig<Plant, Date>( properties.available(), 95, "Date" );
ColumnConfig<Plant, Boolean> indorCol = new ColumnConfig<Plant, Boolean>( properties.indoor(), 55, "Indoor");
// display formatting
DateCell dateCell = new DateCell(DateTimeFormat.getFormat(PredefinedFormat.DATE_SHORT));
dateCol.setCell(dateCell);
// display a checkbox in the gridview
indorCol.setCell(new CheckBoxCell());
List<ColumnConfig<Plant, ?>> l = new ArrayList<ColumnConfig<Plant, ?>>();
l.add(nameCol);
l.add(dateCol);
l.add(indorCol);
ColumnModel<Plant> columns = new ColumnModel<Plant>(l);
store = new ListStore<Plant>(properties.key());
store.setAutoCommit(false);
store.addAll(getPlants());
GridView<Plant> gridView = new GridView<Plant>();
grid = new Grid<Plant>(store, columns, gridView);
grid.getView().setAutoExpandColumn(nameCol);
// EDITING//
final GridEditing<Plant> editing = new GridInlineEditing<Plant>(grid);
dateField = new DateField(new DateTimePropertyEditor(DateTimeFormat.getFormat(PredefinedFormat.DATE_SHORT)));
dateField.setClearValueOnParseError(false);
editing.addEditor(dateCol, dateField);
CheckBox checkField = new CheckBox();
editing.addEditor(indorCol, checkField);
editing.addCompleteEditHandler( new CompleteEditHandler<Plant>(){
// not firing for checkbox:
#Override
public void onCompleteEdit(CompleteEditEvent<Plant> event) {
GridCell cell = event.getEditCell();
int row = cell.getRow();
int col = cell.getCol();
System.out.println("got here. row "+row+", col "+col);
}
});
panel = new FramedPanel();
panel.setHeadingText("Editable Grid Example");
panel.setPixelSize(600, 400);
panel.addStyleName("margin-10");
VerticalLayoutContainer con = new VerticalLayoutContainer();
con.setBorders(true);
con.add(grid, new VerticalLayoutData(1, 1));
panel.setWidget(con);
panel.setButtonAlign(BoxLayoutPack.CENTER);
panel.addButton(new TextButton("Reset", new SelectHandler() {
#Override
public void onSelect(SelectEvent event) {
store.rejectChanges();
}
}));
panel.addButton(new TextButton("Save", new SelectHandler() {
#Override
public void onSelect(SelectEvent event) {
store.commitChanges();
}
}));
}
return panel;
}
private static int AUTO_ID = 0;
public class Plant {
private DateTimeFormat df = DateTimeFormat.getFormat("MM/dd/y");
private int id;
private String name;
private String light;
private double price;
private Date available;
private boolean indoor;
private String color;
private int difficulty;
private double progress;
public Plant() {
id = AUTO_ID++;
difficulty = (int) (Math.random() * 100);
progress = Math.random();
}
public Plant(String name, String light, double price, String available, boolean indoor) {
this();
setName(name);
setLight(light);
setPrice(price);
setAvailable(df.parse(available));
setIndoor(indoor);
}
public int getId() { return id; }
public double getProgress() { return progress; }
public String getColor() { return color; }
public int getDifficulty() { return difficulty; }
public Date getAvailable() { return available; }
public String getLight() { return light; }
public String getName() { return name; }
public double getPrice() { return price; }
public boolean isIndoor() { return indoor; }
public void setId(int id) { this.id = id; }
public void setProgress(double progress) { this.progress = progress; }
public void setAvailable(Date available) { this.available = available; }
public void setDifficulty(int difficulty) { this.difficulty = difficulty; }
public void setColor(String color) { this.color = color; }
public void setIndoor(boolean indoor) { this.indoor = indoor; }
public void setLight(String light) { this.light = light; }
public void setName(String name) { this.name = name; }
public void setPrice(double price) { this.price = price; }
#Override
public String toString() {
return name != null ? name : super.toString();
}
}
public List<Plant> getPlants() {
List<Plant> plants = new ArrayList<Plant>();
plants.add(new Plant("Bloodroot", "Mostly Shady", 2.44, "03/15/2006", true));
plants.add(new Plant("Columbine", "Shade", 9.37, "03/15/2006", true));
plants.add(new Plant("Marsh Marigold", "Mostly Sunny", 6.81, "05/17/2006", false));
plants.add(new Plant("Cowslip", "Mostly Shady", 9.90, "03/06/2006", true));
plants.add(new Plant("Dutchman's-Breeches", "Mostly Shady", 6.44, "01/20/2006", true));
plants.add(new Plant("Ginger, Wild", "Mostly Shady", 9.03, "04/18/2006", true));
return plants;
}
}
thanks. and have a great day!!
You are setting a checkbox cell in the column, and then also attaching a field as an inline editor for the column. So if the user clicks the checkbox (cell), you are expecting that click to be ignored, but instead a checkbox (field) to show up over it, which the user may then click?
Instead what is happening is that the checkbox (cell) is reporting that it is using that click event to do something useful - it is changing its value. As a result, the grid editing mechanism ignores the click, so the checkbox (field) never goes into edit mode, and so of course it doesn't complete edit mode.
What are you trying to achieve by making it the purpose of two different checkboxes to be drawn in the same place, and function differently? If you are trying to use the CheckBoxCell instance as a way to always draw the checkbox symbol in the grid cell, there are two main choices:
Skip the CheckBox field in the inline editing, and just let the cell take care of it. It will not fire the editing events, but it will still directly interact with the store. You can listen to the cell's events if you need to, or just to the record change events from the store, or you can subclass the cell to modify behavior.
Removing the event handing guts of the CheckBoxCell to prevent it from handling the event - this may be as simple as overriding onBrowserEvent to do nothing, though I suspect that you actually will want to prevent its check changing behavior entirely so that the Inline Editing version takes care of it
Finally, remember that the purpose of inline editing is to keep the grid from being a mass of fields, and to make it only draw those fields when the user actually interacts with it. This means that the user must first click a field to get something like a checkbox to show up, then interface with the field to change it. Looking one more time at the CheckBox field in an inline editable grid (though this time with a custom cell) at http://www.sencha.com/examples/#ExamplePlace:inlineeditablegrid you'll see that this means two clicks to change a value and get the CompleteEditing event (as well as the various other field change events) that you are after - is this really what you have in mind?
As per the Source Code of CheckBoxCell#isEditing() that says:
A checkbox is never in "edit mode". There is no intermediate state between checked and unchecked.
Find the alternate solution here How to get the row index of selected checkbox on grid GXT.
Please have a look at GXT checkbox in grid