I am trying to have a property total which is obtained by multiplying two properties together, namely currentPrice and volumeHeld, where currentPrice is actually obtained by downloading google finance stock price every 10 secs. And it automatically updates every 10 seconds.
Now the getCurrentPrice() is initialized at 0, as shown in the code. 10 seconds later, it picks up a new value and this all works fine.
But in the binding method below, the total is not automatically updated when the currentPrice property has changed.
totalBinding = Bindings.createDoubleBinding(() -> {
System.out.println("current price: " + getCurrentPrice() + "vol held: " + getVolumeHeld());
return getCurrentPrice() * getVolumeHeld();
});
total.bind(totalBinding);
Question: I discovered that within the createDoubleBinding statement above, the getCurrentPrice() has a value of 0 (as mentioned above) and when its value is changed, the change is NOT propagated in the total property. By that I mean the total property is not able to pick up the new value from getCurrentPrice() even when the current price has changed.
So the problem is two-fold but I am guessing the solutions for both of my questions below will be similar if not exactly the same:
How can I fix the problem mentioned above?
Later on, I will be binding this total property to another property to work out the total of the total property for all Trade objects). This fails miserably and it is always equal to 0. This method is written in a different class, i.e. not in the Trade class.
UPDATE:
Code shown below:
class SummaryofTrade{
...
sumOfTotals = new ReadOnlyDoubleWrapper();
sumOfTotalsBinding = Bindings.createDoubleBinding(() -> {
double sum = 0;
for(Trade t : this.observableListOfTrades){
sum += t.getTotal();
}
return sum;
}, total); // I cannot put "total" as a second parameter, as it is a property that resides in the Trade class , not this class.
sumOfTotals.bind(sumOfTotalsBinding);
...
}
The error log message:
Caused by: java.lang.Error: Unresolved compilation problem:
total cannot be resolved to a variable
Please note that the sumOfTotalsBinding and sumOfTotals live in another class.
Code for Trade object below:
class Trade{
...
private final ReadOnlyDoubleWrapper total;
private final ReadOnlyDoubleWrapper currentPrice;
private DoubleProperty volumeHeld;
public DoubleBinding totalBinding;
private final ScheduledService<Number> priceService = new ScheduledService<Number>() {
#Override
public Task<Number> createTask(){
return new Task<Number>() {
#Override
public Number call() throws InterruptedException, IOException {
return getCurrentPriceFromGoogle();
}
};
}
};
public Trade(){
...
priceService.setPeriod(Duration.seconds(10));
priceService.setOnFailed(e -> priceService.getException().printStackTrace());
this.currentPrice = new ReadOnlyDoubleWrapper(0);
this.currentPrice.bind(priceService.lastValueProperty());
startMonitoring();
this.total = new ReadOnlyDoubleWrapper();
DoubleBinding totalBinding = Bindings.createDoubleBinding(() ->
getCurrentPrice() * getVolumeHeld(),
currentPriceProperty(), volumeHeldProperty());
total.bind(totalBinding);
}
// volume held
public double getVolumeHeld(){
return this.volumeHeld.get();
}
public DoubleProperty volumeHeldProperty(){
return this.volumeHeld;
}
public void setVolumeHeld(double volumeHeld){
this.volumeHeld.set(volumeHeld);
}
// multi-threading
public final void startMonitoring() {
priceService.restart();
}
public final void stopMonitoring() {
priceService.cancel();
}
public ReadOnlyDoubleProperty currentPriceProperty(){
return this.currentPrice.getReadOnlyProperty();
}
public final double getCurrentPrice(){
return currentPriceProperty().get();
}
// total
public final Double getTotal(){
return totalProperty().getValue();
}
public ReadOnlyDoubleProperty totalProperty(){
return this.total;
}
}
UPDATE 9/15/2015:
I am trying to elaborate my problem in a logical way here. Let me know if this does not make sense. Thanks.
First, in the Trade class above (please note the code above has been updated and specified the property dependency), each Trade object contains a total property, which is the product of currentPrice and VolumeHeld. If the user manually edit the values of current price and volume held. The total property will be updated automatically.
Now, I have an ObservableList of Trade objects, each of them has a total property. My goal is to sum up the the total property of each Trade object in the observable list and bind the sum to a variable called sumOfTotals. This is done in a class called SummaryOfTrade. And whenever the total property of any one of the Trades in the Observable list changes, the sumOfTotals property should also change automatically.
class SummaryofTrade{
...
// within constructor, we have
sumOfTotals = new ReadOnlyDoubleWrapper();
sumOfTotalsBinding = Bindings.createDoubleBinding(() -> {
double sum = 0;
for(Trade t : this.observableListOfTrades){
sum += t.getTotal();
}
return sum;
}, totalProperty());
sumOfTotals.bind(sumOfTotalsBinding);
...
}
This is where the problem comes in. Eclipse is saying that it does not recognise the Trade object's property,totalProperty. Error message shown below.
The error log message:
Caused by: java.lang.Error: Unresolved compilation problem:
The method totalProperty() is undefined for the type SummaryOfTrade
I have specified the property dependency already yet Eclipse is throwing an error. How should I resolve this?
Since both current price and volume held are properties, you could just bind them directly:
total.bind(currentPriceProperty().multiply(volumeHeldProperty()));
If you absolutely need to use a custom double binding, you first need to provide dependencies so that the computation is performed once the dependencies become invalidated as per documentation:
DoubleBinding totalBinding = new DoubleBinding() {
{
super.bind(currentPrice, volumeHeld);
}
#Override
protected double computeValue() {
return currentPrice.get() * volumeHeld.get();
}
};
The following helper function provided by Bindings should also work:
DoubleBinding totalBinding = Bindings.createDoubleBinding(() ->
currentPrice.get() * volumeHeld.get(),
currentPrice, volumeHeld);
You have an ObservableList<Trade>, where each Trade object has an observable totalProperty(). Your sumOfTotals need to be updated when either the content of that list change, or when any of the individual totalProperty()s belonging to any of the elements change.
You can do this by hand:
DoubleBinding sumOfTotalsBinding = new DoubleBinding() {
{
bind(observableListOfTrades);
observableListOfTrades.forEach(trade -> bind(trade.totalProperty());
observableListOfTrades.addListener((Change<? extends Trade> change) -> {
while (change.next()) {
if (change.wasAdded()) {
change.getAddedSubList().forEach(trade -> bind(trade.totalProperty()));
}
if (change.wasRemoved()) {
change.getRemoved().forEach(trade -> unbind(trade.totalProperty()));
}
}
});
}
#Override
public double computeValue() {
return observableListOfTrades.stream().collect(Collectors.summingDouble(Trade::getTotal));
}
};
Or, you can create your list with an extractor. This will cause the list to fire update notifications (thereby labeling it as invalid) when any of the specified properties belonging to the elements change:
ObservableList<Trade> observableListOfTrades =
FXCollections.observableArrayList(trade -> new Observable[] { trade.totalProperty() });
and then you can just do
sumOfTotals.bind(Bindings.createDoubleBinding(() ->
observableListOfTrades.stream().collect(Collectors.summingDouble(Trade::getTotal)),
observableListOfTrades);
since now binding just to the observableListOfTrades will cause recomputation when any of the individual totals change.
Here's an SSCCE:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.Collectors;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.HPos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.Stage;
import javafx.util.converter.DoubleStringConverter;
import javafx.util.converter.IntegerStringConverter;
public class TradeTableExample extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Trade> table = new TableView<>();
table.setEditable(true);
TableColumn<Trade, String> nameCol = column("Name", trade -> new ReadOnlyStringWrapper(trade.getName()));
TableColumn<Trade, Integer> volumeCol = column("Volume", t -> t.volumeProperty().asObject());
TableColumn<Trade, Double> priceCol = column("Price", t -> t.priceProperty().asObject());
TableColumn<Trade, Number> totalCol = column("Total", Trade::totalProperty);
volumeCol.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
priceCol.setCellFactory(TextFieldTableCell.forTableColumn(new DoubleStringConverter()));
table.getColumns().addAll(Arrays.asList(nameCol, volumeCol, priceCol, totalCol));
ObservableList<Trade> data = FXCollections.observableArrayList(trade -> new Observable[] {trade.totalProperty()});
DoubleBinding grandTotal = Bindings.createDoubleBinding(() ->
data.stream().collect(Collectors.summingDouble(Trade::getTotal)),
data);
data.addAll(createData());
table.setItems(data);
Label totalLabel = new Label();
totalLabel.textProperty().bind(grandTotal.asString("Total: %,.2f"));
TextField nameField = new TextField();
TextField volumeField = new TextField("0");
TextField priceField = new TextField("0.00");
Button add = new Button("Add");
add.setOnAction(e -> {
data.add(
new Trade(nameField.getText(),
Integer.parseInt(volumeField.getText()),
Double.parseDouble(priceField.getText())));
nameField.setText("");
volumeField.setText("0");
priceField.setText("0.00");
});
Button delete = new Button("Delete");
delete.setOnAction(e -> data.remove(table.getSelectionModel().getSelectedIndex()));
delete.disableProperty().bind(table.getSelectionModel().selectedItemProperty().isNull());
HBox buttons = new HBox(5, add, delete);
GridPane controls = new GridPane();
controls.addRow(0, new Label("Name:"), nameField);
controls.addRow(1, new Label("Volume:"), volumeField);
controls.addRow(2, new Label("Price:"), priceField);
controls.add(buttons, 0, 3, 2, 1);
controls.add(totalLabel, 0, 4, 2, 1);
ColumnConstraints leftCol = new ColumnConstraints();
leftCol.setHalignment(HPos.RIGHT);
ColumnConstraints rightCol = new ColumnConstraints();
rightCol.setHgrow(Priority.ALWAYS);
controls.getColumnConstraints().addAll(leftCol, rightCol);
GridPane.setHalignment(controls, HPos.LEFT);
GridPane.setHalignment(totalLabel, HPos.LEFT);
controls.setHgap(5);
controls.setVgap(5);
BorderPane root = new BorderPane(table, null, null, controls, null);
Scene scene = new Scene(root, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private List<Trade> createData() {
Random rng = new Random();
List<Trade> trades = new ArrayList<>();
for (int i=0; i<10; i++) {
StringBuilder name = new StringBuilder();
for (int c = 0; c < 3; c++) {
name.append(Character.toString((char)(rng.nextInt(26)+'A')));
}
double price = rng.nextInt(100000)/100.0 ;
int volume = rng.nextInt(10000);
trades.add(new Trade(name.toString(), volume, price));
}
return trades ;
}
private <S,T> TableColumn<S,T> column(String text, Function<S, ObservableValue<T>> property) {
TableColumn<S,T> col = new TableColumn<>(text);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
return col ;
}
public static class Trade {
private final String name ;
private final IntegerProperty volume = new SimpleIntegerProperty();
private final DoubleProperty price = new SimpleDoubleProperty();
private final ReadOnlyDoubleWrapper total = new ReadOnlyDoubleWrapper();
public Trade(String name, int volume, double price) {
this.name = name ;
setPrice(price);
setVolume(volume);
total.bind(priceProperty().multiply(volumeProperty()));
}
public final String getName() {
return name ;
}
public final IntegerProperty volumeProperty() {
return this.volume;
}
public final int getVolume() {
return this.volumeProperty().get();
}
public final void setVolume(final int volume) {
this.volumeProperty().set(volume);
}
public final DoubleProperty priceProperty() {
return this.price;
}
public final double getPrice() {
return this.priceProperty().get();
}
public final void setPrice(final double price) {
this.priceProperty().set(price);
}
public final ReadOnlyDoubleProperty totalProperty() {
return this.total.getReadOnlyProperty();
}
public final double getTotal() {
return this.totalProperty().get();
}
}
public static void main(String[] args) {
launch(args);
}
}
Related
I have a TableView which uses a RowFactory to style rows depending on a specific property of the item of the row. The RowFactory uses worker threads to check the validity of this specific property against a call to the database. The problem is that correct rows are sometimes marked as incorrect (red through a PseudoClass) and incorrect rows not marked. I have created a Minimal Reproducible Example below. This example should mark only rows that are even...but it also marks other rows.
Test Entity
public class TestEntity
{
public TestEntity(String firstName, String lastName, int c)
{
setFirstName(firstName);
setLastName(lastName);
setC(c);
}
private StringProperty firstName = new SimpleStringProperty();
private StringProperty lastName = new SimpleStringProperty();
private IntegerProperty c = new SimpleIntegerProperty();
public int getC()
{
return c.get();
}
public IntegerProperty cProperty()
{
return c;
}
public void setC(int c)
{
this.c.set(c);
}
public String getFirstName()
{
return firstName.get();
}
public StringProperty firstNameProperty()
{
return firstName;
}
public void setFirstName(String firstName)
{
this.firstName.set(firstName);
}
public String getLastName()
{
return lastName.get();
}
public StringProperty lastNameProperty()
{
return lastName;
}
public void setLastName(String lastName)
{
this.lastName.set(lastName);
}
}
Main
public class TableViewProblemMain extends Application
{
public static void main(String[] args)
{
launch(args);
AppThreadPool.shutdown();
}
#Override
public void start(Stage stage)
{
TableView<TestEntity> tableView = new TableView();
TableColumn<TestEntity, String> column1 = new TableColumn<>("First Name");
column1.setCellValueFactory(new PropertyValueFactory<>("firstName"));
TableColumn<TestEntity, String> column2 = new TableColumn<>("Last Name");
column2.setCellValueFactory(new PropertyValueFactory<>("lastName"));
TableColumn<TestEntity, String> column3 = new TableColumn<>("C");
column3.setCellValueFactory(new PropertyValueFactory<>("c"));
tableView.getColumns().addAll(column1, column2, column3);
tableView.setRowFactory(new TestRowFactory());
for (int i = 0; i < 300; i++)
{
tableView.getItems().add(new TestEntity("Fname" + i, "Lname" + i, i));
}
VBox vbox = new VBox(tableView);
Scene scene = new Scene(vbox);
scene.getStylesheets().add(this.getClass().getResource("/style.css").toExternalForm());
// Css has only these lines:
/*
.table-row-cell:invalid {
-fx-background-color: rgba(240, 116, 116, 0.18);
}
* */
stage.setScene(scene);
stage.show();
}
}
Row Factory
public class TestRowFactory implements Callback<TableView<TestEntity>, TableRow<TestEntity>>
{
private final PseudoClass INVALID_PCLASS = PseudoClass.getPseudoClass("invalid");
#Override
public TableRow<TestEntity> call(TableView param)
{
TableRow<TestEntity> row = new TableRow();
Thread validationThread = new Thread(() ->
{
try
{
if(row.getItem() != null)
{
Thread.sleep(500); // perform validation and stuff...
if(row.getItem().getC() % 2 == 0)
{
Tooltip t = new Tooltip("I am a new tooltip that should be shown only on red rows");
row.setTooltip(t);
row.pseudoClassStateChanged(INVALID_PCLASS, true);
}
}
} catch (InterruptedException e)
{
e.printStackTrace();
}
});
ChangeListener changeListener = (obs, old, current) ->
{
row.setTooltip(null);
AppThreadPool.perform(validationThread);
};
row.itemProperty().addListener((observable, oldValue, newValue) ->
{
row.setTooltip(null);
if (oldValue != null)
{
oldValue.firstNameProperty().removeListener(changeListener);
}
if (newValue != null)
{
newValue.firstNameProperty().removeListener(changeListener);
AppThreadPool.perform(validationThread);
}
else
{
row.pseudoClassStateChanged(INVALID_PCLASS, false);
}
});
row.focusedProperty().addListener(changeListener);
return row;
}
}
AppThreadPool
public class AppThreadPool
{
private static final int threadCount = Runtime.getRuntime().availableProcessors();
private static final ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadCount * 2 + 1);
public static <R extends Runnable> void perform(R runnable)
{
executorService.submit(runnable);
}
public static void shutdown()
{
executorService.shutdown();
}
}
Screenshot
There are several misunderstandings in your code. The first is about cells and reuse (a TableRow is a Cell). Cells can be reused arbitrarily, and potentially frequently (especially during user scrolling) to stop displaying one item and display a different one.
In your code, if the row is used to display an entity that is invalid, the listener on the row's itemProperty will trigger the runnable on the background thread, which will at some point set the pseudoclass state to true.
If the cell is subsequently reused to display a valid item, however, the next runnable that is executed does not change the pseudoclass state. So that state remains true and the row color remains red.
Consequently, a row is red if it ever displayed an invalid item at some point. (Not if it is currently displaying an invalid item.) If you scroll enough, eventually all cells will be red.
Secondly, you must not update any UI that is part of the scene graph from any thread other than the FX Application Thread. Additionally, some other operations, such as creating Window instances (Tooltip is a subclass of Window) must be performed on the FX Application Thread. Note that this includes modifying model properties which are bound to the UI, including properties used in the table columns. You violate this in your validationThread, where you create a Tooltip, set it on the row, and change the pseudoclass state, all in a background thread.
A good approach here is to use the JavaFX concurrency API. Use Tasks which, as far as possible, use only immutable data and return an immutable value. If you do need to update properties which are displayed in the UI, use Platform.runLater(...) to schedule those updates on the FX Application Thread.
In terms of MVC design, it is a good practice for your Model class(es) to store all the data needed by the View. Your design runs into trouble because there is no real place where the validation status is stored. Moreover, the validation status is really more than just "valid" or "invalid"; there is a phase while the thread is running but not completed where the validation status is unknown.
Here's my solution, which addresses these issues. I am assuming:
Your entity has a notion of validity.
Establishing the validity of an entity is a long-running process
Validity depends on one or more properties which may change while the UI is displayed
The validity should be "lazily" established, on an as-need basis.
The UI prefers not to display "unknown" validity, and if an entity is displayed whose validity is unknown, it should be established and redisplayed.
I created an enum for ValidationStatus, which has four values:
public enum ValidationStatus {
VALID, INVALID, UNKNOWN, PENDING ;
}
UNKNOWN indicates the validity is not known and validation has not been requested; PENDING indicates that validation has been requested but is not yet complete.
Then I have a wrapper for your entity which adds the validation status as an observable property. If the property on which validation depends in the underlying entity changes, the validation is reset to UNKNOWN.
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
public class ValidatingTestEntity {
private final TestEntity entity ;
private final ObjectProperty<ValidationStatus> validationStatus = new SimpleObjectProperty<>(ValidationStatus.UNKNOWN);
public ValidatingTestEntity(TestEntity entity) {
this.entity = entity;
entity.firstNameProperty().addListener((obs, oldName, newName) -> setValidationStatus(ValidationStatus.UNKNOWN));
}
public TestEntity getEntity() {
return entity;
}
public ValidationStatus getValidationStatus() {
return validationStatus.get();
}
public ObjectProperty<ValidationStatus> validationStatusProperty() {
return validationStatus;
}
public void setValidationStatus(ValidationStatus validationStatus) {
this.validationStatus.set(validationStatus);
}
}
The ValidationService provides a service to validate entities on a background thread, updating the appropriate properties with the result. This is managed through a thread pool and with JavaFX Tasks. This just mimics a database call by sleeping for a random amount of time and then returning alternating results.
When the task changes state (i.e. as it progresses though its lifecycle), the validation property of the entity is updated: UNKNOWN if the task fails to complete normally, PENDING if the task is in an incomplete state, and either VALID or INVALID, depending on the result of the task, if the task succeeds.
import javafx.application.Platform;
import javafx.concurrent.Task;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
public class ValidationService {
private final Executor exec = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2 + 1,
r -> {
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
}
);
public Task<Boolean> validateEntity(ValidatingTestEntity entity) {
// task runs on a background thread and should not access mutable data,
// so make final copies of anything needed here:
final String firstName = entity.getEntity().getFirstName();
final int code =entity.getEntity().getC();
Task<Boolean> task = new Task<Boolean>() {
#Override
protected Boolean call() throws Exception {
try {
Thread.sleep(500 + ThreadLocalRandom.current().nextInt(500));
} catch (InterruptedException exc) {
// if interrupted other than being cancelled, reset thread's interrupt status:
if (! isCancelled()) {
Thread.currentThread().interrupt();
}
}
boolean result = code % 2 == 0;
return result;
}
};
task.stateProperty().addListener((obs, oldState, newState) ->
entity.setValidationStatus(
switch(newState) {
case CANCELLED, FAILED -> ValidationStatus.UNKNOWN;
case READY, RUNNING, SCHEDULED -> ValidationStatus.PENDING ;
case SUCCEEDED ->
task.getValue() ? ValidationStatus.VALID : ValidationStatus.INVALID ;
}
)
);
exec.execute(task);
return task ;
}
}
This is the TableRow implementation. It has a listener which observes the validation status of the current item, if there is one. If the item changes, the listener is removed from the old item (if there is one), and attached to the new item (if there is one). If either the item changes, or the validation state of the current item changes, the row is updated. If the new validation status is UNKNOWN, a request is sent to the service to validate the current item. There are two pseudoclass states: invalid (red) and unknown (orange), which are updated any time the item or its validation status change. The tooltip is set if the item is invalid, and set to null otherwise.
import javafx.beans.value.ChangeListener;
import javafx.css.PseudoClass;
import javafx.scene.control.TableRow;
import javafx.scene.control.Tooltip;
public class ValidatingTableRow extends TableRow<ValidatingTestEntity> {
private final ValidationService validationService ;
private final PseudoClass pending = PseudoClass.getPseudoClass("pending");
private final PseudoClass invalid = PseudoClass.getPseudoClass("invalid");
private final Tooltip tooltip = new Tooltip();
private final ChangeListener<ValidationStatus> listener = (obs, oldStatus, newStatus) -> {
updateValidationStatus();
};
public ValidatingTableRow(ValidationService validationService){
this.validationService = validationService ;
itemProperty().addListener((obs, oldItem, newItem) -> {
setTooltip(null);
if (oldItem != null) {
oldItem.validationStatusProperty().removeListener(listener);
}
if (newItem != null) {
newItem.validationStatusProperty().addListener(listener);
}
updateValidationStatus();
});
}
private void updateValidationStatus() {
if (getItem() == null) {
pseudoClassStateChanged(pending, false);
pseudoClassStateChanged(invalid, false);
setTooltip(null);
return ;
}
ValidationStatus validationStatus = getItem().getValidationStatus();
if( validationStatus == ValidationStatus.UNKNOWN) {
validationService.validateEntity(getItem());
}
if (validationStatus == ValidationStatus.INVALID) {
tooltip.setText("Invalid entity: "+getItem().getEntity().getFirstName() + " " +getItem().getEntity().getC());
setTooltip(tooltip);
} else {
setTooltip(null);
}
pseudoClassStateChanged(pending, validationStatus == ValidationStatus.PENDING);
pseudoClassStateChanged(invalid, validationStatus == ValidationStatus.INVALID);
}
}
Here's the Entity, which is the same as in the question:
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class TestEntity
{
public TestEntity(String firstName, String lastName, int c)
{
setFirstName(firstName);
setLastName(lastName);
setC(c);
}
private StringProperty firstName = new SimpleStringProperty();
private StringProperty lastName = new SimpleStringProperty();
private IntegerProperty c = new SimpleIntegerProperty();
public String getFirstName() {
return firstName.get();
}
public StringProperty firstNameProperty() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName.set(firstName);
}
public String getLastName() {
return lastName.get();
}
public StringProperty lastNameProperty() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName.set(lastName);
}
public int getC() {
return c.get();
}
public IntegerProperty cProperty() {
return c;
}
public void setC(int c) {
this.c.set(c);
}
}
And here's the application class. I added the ability to edit the first name, which lets you see an item reverting to unknown and then re-establishing its validity (you need to change selection quickly after committing the edit).
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TableViewProblemMain extends Application
{
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage stage)
{
TableView<ValidatingTestEntity> tableView = new TableView();
tableView.setEditable(true);
TableColumn<ValidatingTestEntity, String> column1 = new TableColumn<>("First Name");
column1.setCellValueFactory(cellData -> cellData.getValue().getEntity().firstNameProperty());
column1.setEditable(true);
column1.setCellFactory(TextFieldTableCell.forTableColumn());
TableColumn<ValidatingTestEntity, String> column2 = new TableColumn<>("Last Name");
column2.setCellValueFactory(cellData -> cellData.getValue().getEntity().lastNameProperty());
TableColumn<ValidatingTestEntity, Number> column3 = new TableColumn<>("C");
column3.setCellValueFactory(cellData -> cellData.getValue().getEntity().cProperty());
tableView.getColumns().addAll(column1, column2, column3);
ValidationService service = new ValidationService();
tableView.setRowFactory(tv -> new ValidatingTableRow(service));
for (int i = 0; i < 300; i++)
{
tableView.getItems().add(new ValidatingTestEntity(
new TestEntity("Fname" + i, "Lname" + i, i)));
}
VBox vbox = new VBox(tableView);
Scene scene = new Scene(vbox);
scene.getStylesheets().add(this.getClass().getResource("/style.css").toExternalForm());
stage.setScene(scene);
stage.show();
}
}
Finally, for completeness, the stylesheet:
.table-row-cell:invalid {
-fx-background-color: rgba(240, 116, 116, 0.18);
}
.table-row-cell:pending {
-fx-background-color: rgba(240, 120, 0, 0.18);
}
this is my first question here so bear with me please.
I'm trying to use the Oracle Java docs, but when I try to compile example 6 on this page it doesn't work. I've tried everything I can think of. It's an old example, so that may be the issue, but it's disheartening that I can't figure out what should be a minor problem.
Example 6 Using an InvalidationListener
package bindingdemo;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.binding.NumberBinding;
import javafx.beans.binding.Bindings;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
class Bill {
// Define the property
private DoubleProperty amountDue = new SimpleDoubleProperty();
// Define a getter for the property's value
public final double getAmountDue(){return amountDue.get();}
// Define a setter for the property's value
public final void setAmountDue(double value){amountDue.set(value);}
// Define a getter for the property itself
public DoubleProperty amountDueProperty() {return amountDue;}
}
public class Main {
public static void main(String[] args) {
Bill bill1 = new Bill();
Bill bill2 = new Bill();
Bill bill3 = new Bill();
NumberBinding total = Bindings.add(bill1.amountDueProperty().add(bill2.amountDueProperty()),
bill3.amountDueProperty());
total.addListener(new InvalidationListener() {
#Override public void invalidated(Observable o) {
System.out.println("The binding is now invalid.");
}
});
// First call makes the binding invalid
bill1.setAmountDue(200.00);
// The binding is now invalid
bill2.setAmountDue(100.00);
bill3.setAmountDue(75.00);
// Make the binding valid...
System.out.println(total.getValue());
// Make invalid...
bill3.setAmountDue(150.00);
// Make valid...
System.out.println(total.getValue());
}
}
Thanks
I have a custom object FermentableInRecipe, which populates a TableView. In order to respond to changes to items in the list, as well as the list itself, I have decided to employ an extractor. Here is my declaration and instantiation of my ObservableList:
private ObservableList<FermentableInRecipe> fermentablesInRecipe =
FXCollections.observableArrayList(item -> new Observable[]{item.WeightProperty()});
Here are the relevant segments of my custom class:
public class FermentableInRecipe {
private DoubleProperty weight;
...
public Double getWeight() {
return this.weight.getValue();
}
public void setWeight(Double value) {
this.weight.setValue(value);
}
public DoubleProperty WeightProperty() {
if (weight == null) {
weight = new SimpleDoubleProperty(0.0);
}
return weight;
}
...
}
In the links I've provided below, this approach worked. But Netbeans is telling me "DoubleProperty cannot be converted to Observable". I can see why this is the case, but I cannot understand why it worked in the links below and not for me, and how I should create extractor and link it to the weightProperty() function if this approach doesn't work.
Links:
JavaFX 2.0 Choice Box Issue. How to update a choiceBox, which represents a list of objects, when an object is updated?
JavaFX, ObservableList: How to fire an InvalidationListener whenever an object of the list gets modified?
Thanks in advance. Let me know if I've missed any crucial information.
There's nothing wrong with your code as written, this compiles just fine for me:
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.stage.Stage;
public class JavaFXApplication1 extends Application {
class FermentableInRecipe {
private DoubleProperty weight;
public Double getWeight() {
return this.weight.getValue();
}
public void setWeight(Double value) {
this.weight.setValue(value);
}
public DoubleProperty WeightProperty() {
if (weight == null) {
weight = new SimpleDoubleProperty(0.0);
}
return weight;
}
}
private ObservableList<FermentableInRecipe> fermentablesInRecipe = FXCollections.observableArrayList(item -> new Observable[]{item.WeightProperty()});
#Override
public void start(Stage primaryStage) throws Exception {
}
}
I'd suggest double checking imports, and make sure you haven't imported java.util.Observable or similar by mistake.
Problem overview:
I have two models OrderModel and OrderStatusModel. The OrderStatusModel items are put into a combobox that contains choices for the user to update a foreign key field in an Order table called orderStatusId. When I initialize the data, I want to be able to pull from OrderModel and retrieve the current orderStatusId and then set the combobox (that holds all the user choices) to match the orderStatusId located in the OrderModel(which holds the FK located in the Order table). The combobox needs to be able to update the OrderModel when a new selection is made. While getting data from a database to populate the OrderModel, I pull my FK orderStatusId. From this point I use case statements to select the appropriate item in OrderStatusModel and select the choice on initialization. I know this can't be the greatest method because if I were to add a new row in the backend database for a new type of status (for example: order cancelled) then I need to go and update all my switch statements to allow for that change.
Example Code
Controller
package test.pkg2;
import java.awt.Label;
import java.beans.Statement;
import java.net.URL;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ComboBox;
public class Test2Controller implements Initializable{
#FXML
private Label customerName;
#FXML
private Label orderNumber;
#FXML
private Label orderStatusId;
#FXML
private ComboBox<OrderStatusModel> orderStatusCmb;
private ObservableList<OrderStatusModel> orderStatusModel;
private ObservableList<OrderModel> orderModel;
int statusId;
#Override
public void initialize(URL location, ResourceBundle resources) {
this.orderStatusCmb.getSelectionModel().selectedItemProperty().addListener((obs, newValue, oldValue) ->{
if(newValue != null && newValue != oldValue){
this.orderStatusCmb.getSelectionModel().getSelectedItem().getOrderStatusId();
//TODO UPDATE Order table with new orderStatusId from above code ^^^^^^
}
});
populateOrderStatusCombobox();
getCurrentOrder();
}
private void populateOrderStatusCombobox(){
String SQL = "SELECT Status.statusId, Status.statusName \n"
+"FROM Status;";
orderStatusModel = FXCollections.observableArrayList();
//Status table:
// +-----------+-------------+------+-----+---------+
// | Field | Type | Null | Key | Default |
// +-----------+-------------+------+-----+---------+
// | statusId | int | YES | PK | NULL |
// | statusName| varchar(20) | YES | | NULL |
// +-----------+-------------+------+-----+---------+
//example data: statusId = 1 statusName = Order Taken
//example data: statusId = 2 statusName = Order Processing
//example data: statusId = 3 statusName = Shipped
//Above items are populated into the combobox for selection
try(Connection connection = DBConnection.getConnection()){
PreparedStatement statement = connection.prepareStatement(SQL);
ResultSet resultSet = statement.executeQuery();
while(resultSet.next()){
this.orderStatusModel.add(new OrderStatusModel(resultSet.getInt("statusId"),
resultSet.getString("statusName")));
}
} catch(SQLException e){
}
this.orderStatusCmb.setItems(orderStatusModel);
}
private void getCurrentOrder(){
String SQL = "SELECT Order.orderNumber, Order.customerName, Order.orderStatusId \n"
+"FROM Order"
+"WHERE orderNumber = 123;";
orderModel = FXCollections.observableArrayList();
//Order table:
// +--------------+-------------+------+-----+---------+
// | Field | Type | Null | Key | Default |
// +--------------+-------------+------+-----+---------+
// | orderNumber | int | YES | PK | NULL |
// | customerName | varchar(20) | YES | | NULL |
// | orderStatusId| int | YES | FK | NULL |
// +--------------+-------------+------+-----+---------+
//example data: orderNumber = 123
// customerName = SomeCompany
// orderStatusId = 1
//I usually set the combobox here using the below method:
try(Connection connection = DBConnection.getConnection()){
PreparedStatement statement = connection.prepareStatement(SQL);
ResultSet resultSet = statement.executeQuery();
while(resultSet.next()){
this.orderModel.add(new OrderModel(resultSet.getInt("orderNumber"),
resultSet.getString("customerName"),
resultSet.getInt("orderStatusId")));
statusId = resultSet.getInt("orderStatusId");
}
//HERE is where I initially set the orderStatus to match the orderModel
//I am guessing I need some kind of binding here?
switch(statusId){
case 1: this.orderStatusCmb.getSelectionModel().select(0);
break;
case 2: this.orderStatusCmb.getSelectionModel().select(1);
break;
case 3: this.orderStatusCmb.getSelectionModel().select(3);
break;
}
} catch(SQLException e){
}
}
}
OrderStatusModel
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class OrderStatusModel {
private final IntegerProperty orderStatusId;
private final StringProperty orderStatusName;
public OrderStatusModel(int orderStatusId, String orderStatusName){
this.orderStatusId = new SimpleIntegerProperty(orderStatusId);
this.orderStatusName = new SimpleStringProperty(orderStatusName);
}
public IntegerProperty getOrderStatusId() {
return orderStatusId;
}
public StringProperty getOrderStatusName() {
return orderStatusName;
}
}
OrderModel
package test.pkg2;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class OrderModel {
private final IntegerProperty orderNumber;
private final StringProperty customerName;
private final IntegerProperty orderStatusId;
public OrderModel(int orderNumber, String customerName, int orderStatusId){
this.orderNumber = new SimpleIntegerProperty(orderNumber);
this.customerName = new SimpleStringProperty(customerName);
this.orderStatusId = new SimpleIntegerProperty(orderStatusId);
}
public IntegerProperty getOrderNumber() {
return orderNumber;
}
public StringProperty getCustomerName() {
return customerName;
}
public IntegerProperty getOrderStatusId() {
return orderStatusId;
}
}
You don't need to use any explicit binding to achieve a link between choices and IDs. Instead, backing the combobox by your OrderStatusModel object, which already includes both the ID and friendly string name, then applying an appropriate StringConverter to the combobox will achieve a nice display of the combobox. The id -> friendly name link is still retained in the model class, which can be used in reverse in subsequent database updates as needed.
Sample
What this sample does is a few things:
Extracts database interaction to an interface.
Provides an example in-memory implementation of the database interface (replace this with an implementation that performs your actual database access code).
Defines the database access in a separate class, which moves its logic out of your UI code so that you can more easily independently develop it, test it and swap out its implementation logic (which is all pretty much preferable, even for a small app).
Defines the UI layout in code rather than FXML (using FXML is preferable, but creating it in the code was just for this sample to keep the code relatively short).
Updates the naming convention for the accessors on the model classes to xxxProperty() rather than getXXX(), which is what you were using. Anything which returns a property should have a name suffix of Property by standard JavaFX naming convention. getXXX() methods are for retrieving the actual values of properties, not the properties themselves.
In the OrderModel, the order status is recorded as ObjectProperty, this makes the OrderModel easier to use as it includes within it all of the relevant status information, without requiring joins and queries to get additional info. I know it's not represented that way in your database system, but having the database interface class do the transform and return the all of the relevant info is usually a better way to go for a case like this.
When working with the ComboBox, uses the valueProperty rather than the selectedItemProperty, as valueProperty is a more direct representation of the value of the ComboBox (doesn't matter much in this case where the user always chooses from a select drop-down and can not text edit the value portion, but still, it is a little more direct conceptually to just work with the value).
A StringConverter is defined for the combobox which will translate the OrderStatusModel values to their friendly String names for display in the combobox.
Defines equals and hashcode for the OrderStatusModel so that it can be properly compared and inserted into sets with the Java collections APIs using appropriate matching and duplicate logic (which, otherwise, they may not act as you expect).
Sample Output
ComboApp.java
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.util.Optional;
public class ComboApp extends Application {
private Label orderNumber = new Label();
private Label customerName = new Label();
private ComboBox<OrderStatusModel> orderStatus = new ComboBox<>();
private Database database = new InMemoryTestDatabase();
#Override
public void start(Stage stage) throws Exception {
HBox layout = new HBox(
10,
orderNumber,
customerName,
orderStatus
);
layout.setPadding(new Insets(10));
layout.setAlignment(Pos.BASELINE_LEFT);
populateOrderStatusCombobox();
showOrder(123);
updateOrderWhenStatusChanged();
stage.setScene(new Scene(layout));
stage.show();
}
private void updateOrderWhenStatusChanged() {
orderStatus.valueProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null && !newValue.equals(oldValue)) {
database.updateOrder(
new OrderModel(
Integer.parseInt(orderNumber.getText()),
customerName.getText(),
orderStatus.getValue()
)
);
System.out.println(
"Updated order status of order "
+ orderNumber.getText()
+ " to "
+ orderStatus.getValue()
.orderStatusNameProperty()
.getValue()
);
}
});
}
private void populateOrderStatusCombobox(){
orderStatus.setPlaceholder(new Label("None Selected"));
orderStatus.getItems().setAll(database.listAllOrderStatuses());
orderStatus.setConverter(new StringConverter<OrderStatusModel>() {
#Override
public String toString(OrderStatusModel orderStatusModel) {
return orderStatusModel.orderStatusNameProperty().get();
}
#Override
public OrderStatusModel fromString(String orderStatusName) {
Optional<OrderStatusModel> result = database.findOrderStatusWithName(
orderStatusName
);
return result.orElse(null);
}
});
}
private void showOrder(int orderId){
Optional<OrderModel> result = database.findOrderById(orderId);
if (!result.isPresent()) {
orderNumber.setText("");
customerName.setText("");
orderStatus.setValue(null);
return;
}
OrderModel order = result.get();
orderNumber.setText("" + order.orderNumberProperty().get());
customerName.setText(order.customerNameProperty().get());
orderStatus.setValue(order.orderStatusProperty().get());
}
public static void main(String[] args) {
launch(args);
}
}
Database.java
import java.util.List;
import java.util.Optional;
public interface Database {
List<OrderStatusModel> listAllOrderStatuses();
Optional<OrderStatusModel> findOrderStatusWithName(String orderStatusName);
Optional<OrderStatusModel> findOrderStatusById(int orderStatusId);
Optional<OrderModel> findOrderById(int orderId);
void updateOrder(OrderModel order);
}
InMemoryTestDatabase.java
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class InMemoryTestDatabase implements Database {
List<OrderStatusModel> orderStatuses = new ArrayList<>();
List<OrderModel> orders = new ArrayList<>();
public InMemoryTestDatabase() {
// populate with dummy sample data.
orderStatuses.add(new OrderStatusModel(1, "Order Taken"));
orderStatuses.add(new OrderStatusModel(2, "Order Processing"));
orderStatuses.add(new OrderStatusModel(3, "Shipped"));
orders.add(
new OrderModel(
123,
"XYZZY",
findOrderStatusById(1)
.orElse(null)
)
);
}
public List<OrderStatusModel> listAllOrderStatuses() {
return orderStatuses;
}
#Override
public Optional<OrderStatusModel> findOrderStatusWithName(String orderStatusName) {
return orderStatuses.stream()
.filter(orderStatus -> orderStatusName.equals(orderStatus.orderStatusNameProperty().get()))
.findFirst();
}
#Override
public Optional<OrderStatusModel> findOrderStatusById(int orderStatusId) {
return orderStatuses.stream()
.filter(orderStatus -> orderStatusId == orderStatus.orderStatusIdProperty().get())
.findFirst();
}
public Optional<OrderModel> findOrderById(int orderId) {
return orders.stream()
.filter(order -> orderId == order.orderNumberProperty().get())
.findFirst();
}
public void updateOrder(OrderModel order) {
for (int i = 0; i < orders.size(); i++) {
if (orders.get(i).orderNumberProperty().get() == order.orderNumberProperty().get()) {
orders.set(i, order);
break;
}
}
}
}
OrderModel.java
import javafx.beans.property.*;
public class OrderModel {
private final IntegerProperty orderNumber;
private final StringProperty customerName;
private final ObjectProperty<OrderStatusModel> orderStatus;
public OrderModel(int orderNumber, String customerName, OrderStatusModel orderStatus){
this.orderNumber = new SimpleIntegerProperty(orderNumber);
this.customerName = new SimpleStringProperty(customerName);
this.orderStatus = new SimpleObjectProperty<>(orderStatus);
}
public IntegerProperty orderNumberProperty() {
return orderNumber;
}
public StringProperty customerNameProperty() {
return customerName;
}
public ObjectProperty<OrderStatusModel> orderStatusProperty() {
return orderStatus;
}
}
OrderStatusModel.java
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import java.util.Objects;
public class OrderStatusModel {
private final IntegerProperty orderStatusId;
private final StringProperty orderStatusName;
public OrderStatusModel(int orderStatusId, String orderStatusName){
this.orderStatusId = new SimpleIntegerProperty(orderStatusId);
this.orderStatusName = new SimpleStringProperty(orderStatusName);
}
public IntegerProperty orderStatusIdProperty() {
return orderStatusId;
}
public StringProperty orderStatusNameProperty() {
return orderStatusName;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OrderStatusModel that = (OrderStatusModel) o;
return Objects.equals(orderStatusId.get(), that.orderStatusId.get()) &&
Objects.equals(orderStatusName.get(), that.orderStatusName.get());
}
#Override
public int hashCode() {
return Objects.hash(orderStatusId.get(), orderStatusName.get());
}
}
Background
This answer was loosely based on a similar question and answer from a JavaFX forum post, which related to ChoiceBoxes rather than ComboBoxes:
ChoiceBox with Database IDs forum post.
ChoiceBox with Database IDs sample code.
However, this answer is updated to work directly with ComboBoxes rather than ChoiceBoxes.
Unrelated Advice
A couple of unrelated bits of advice about your posted code:
Never catch an exception without rethrowing it, taking action on it or logging it. Even if it is just for example code posted on the internet, don't do it.
You import java.awt.Label which you then try to inject using FXML: that's not right, you should use javafx.scene.control.Label. Don't mix awt code and JavaFX code.
so i have a table view with 3 columns and one of them is a column of comboboxes, the way i create the column of combobox is as so
Source = new TableColumn<>("Configure Interface as..");
Source.setCellValueFactory(i -> {
final StringProperty value = i.getValue().optionProperty();
// binding to constant value
return Bindings.createObjectBinding(() -> value);
});
Source.setCellFactory(col -> {
TableCell<TableViewTest, StringProperty> c = new TableCell<>();
ComboBox<String> comboBox = new ComboBox<>(options);
c.itemProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
comboBox.valueProperty().unbindBidirectional(oldValue);
}
if (newValue != null) {
comboBox.valueProperty().bindBidirectional(newValue);
}
});
c.graphicProperty().bind(Bindings.when(c.emptyProperty()).then((Node) null).otherwise(comboBox));
return c;
});
the column gets its values from the getter method optionProperty() which resides within my TableViewTest class.
So the problem i'm having is I have another combobox (comboBoxA) that is above my tableview table in my gui, and when ever i change the value of comboBoxA i want to change the values of the comboboxes with the column.
I can do this by calling the following code within the method that is listening for the selection change of comboboxA
Source.setCellValueFactory(i -> {
final StringProperty value = i.getValue().optionTwoProperty();
// binding to constant value
return Bindings.createObjectBinding(() -> value);
});
but the values don't change unless is start scrolling down to near the bottom of the table. is there a way to force the comboboxes to change to the new values within the getter method optionTwoProperty() without me having to scroll down?.
EDIT
Okay so the line
final StringProperty value = i.getValue().optionTwoProperty();
doesnt actaully get called until i start scrolling down.
So, with help from fabian, I think I understand that you want the combo box above the table to change the property in your model class that is represented in the cells in the table column.
One way to do this is to make the type of the combo box function that maps the model class to a property, and populate it with functions mapping to each of the properties you want.
Then you can represent the cell value factory for the table column with a binding that observes all the possible properties that could be represented, along with the selected value in the combo box, and returns the value computed by applying the function from the combo box to the model instance (and retrieving its wrapped value).
For the cell factory for the column, you can observe the selected value in the cell's combo box. When it changes, use the selected item in the combo box above the table to figure out which property to update.
Here's a SSCCE:
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TableWithSetAllComboBox extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> itemCol = new TableColumn<>("Item");
itemCol.setCellValueFactory(cellData -> Bindings.createStringBinding(() -> cellData.getValue().getName()));
table.getColumns().add(itemCol);
TableColumn<Item, String> choiceCol = new TableColumn<>("Choice");
ComboBox<Function<Item, StringProperty>> option = new ComboBox<>();
option.getItems().add(Item::choiceProperty);
option.getItems().add(Item::choice2Property);
option.setCellFactory(lv -> createListCell());
option.setButtonCell(createListCell());
option.getSelectionModel().select(0);
ObservableList<String> choices = FXCollections.observableArrayList("First choice", "Second choice", "Third choice");
choiceCol.setCellFactory(col -> {
TableCell<Item, String> cell = new TableCell<>();
ComboBox<String> combo = new ComboBox<>(choices);
cell.graphicProperty().bind(Bindings.when(cell.emptyProperty()).then((Node)null).otherwise(combo));
combo.valueProperty().addListener((obs, oldValue, newValue) -> {
if (! cell.isEmpty() && newValue != null) {
Item item = table.getItems().get(cell.getIndex()) ;
StringProperty property = option.getValue().apply(item);
property.set(newValue);
}
});
cell.itemProperty().addListener((obs, oldItem, newItem) -> combo.setValue(newItem));
return cell ;
});
choiceCol.setPrefWidth(150);
table.getColumns().add(choiceCol);
choiceCol.setCellValueFactory(cellData -> Bindings.createStringBinding(
() -> option.getValue().apply(cellData.getValue()).get(),
cellData.getValue().choiceProperty(),
cellData.getValue().choice2Property(),
option.valueProperty()));
choiceCol.setGraphic(option);
choiceCol.setPrefWidth(200);
for (int i = 1; i <= 30 ; i++) table.getItems().add(new Item("Item "+i ,choices.get(0)));
Button debug = new Button("Debug");
debug.setOnAction(e -> table.getItems().stream().
map(item -> String.format("%s (%s, %s)", item.getName(), item.getChoice(), item.getChoice2())).
forEach(System.out::println));
BorderPane root = new BorderPane(table);
BorderPane.setMargin(debug, new Insets(5));
root.setBottom(debug);
primaryStage.setScene(new Scene(root, 600, 600));
primaryStage.show();
}
private ListCell<Function<Item, StringProperty>> createListCell() {
return new ListCell<Function<Item, StringProperty>>() {
#Override
public void updateItem(Function<Item, StringProperty> item, boolean empty) {
super.updateItem(item, empty);
setText(empty ? null : item.apply(new Item("", "")).getName());
}
};
}
public static class Item {
private final String name ;
private final StringProperty choice ;
private final StringProperty choice2 ;
public Item(String name, String choice) {
this.choice = new SimpleStringProperty(this, "Choice", choice);
this.choice2 = new SimpleStringProperty(this, "Choice 2", "Second choice");
this.name = name ;
}
public final StringProperty choiceProperty() {
return this.choice;
}
public final java.lang.String getChoice() {
return this.choiceProperty().get();
}
public final void setChoice(final java.lang.String choice) {
this.choiceProperty().set(choice);
}
public String getName() {
return name;
}
public final StringProperty choice2Property() {
return this.choice2;
}
public final java.lang.String getChoice2() {
return this.choice2Property().get();
}
public final void setChoice2(final java.lang.String choice2) {
this.choice2Property().set(choice2);
}
}
public static void main(String[] args) {
launch(args);
}
}
The issue is the TableView not listening to modifications of the cellValueFactory property of the elements of it's columns. Therefore the TableView doesn't know it should redraw it's cells. In JavaFX 8u60 the refresh() method was added for this purpose (for some reason I can't find it in the online javadoc though), which allows you to change the code of your method changing the cellValueFactory like this:
Source.setCellValueFactory(i -> {
final StringProperty value = i.getValue().optionTwoProperty();
// binding to constant value
return Bindings.createObjectBinding(() -> value);
});
tableview.refresh();
In older versions you have to use the workaround of setting the column value to trigger a change in the list:
List<TableColumn<TableViewTest, ?>> columns = tableview.getColumns();
columns.set(columns.indexOf(Source), Source);
But this workaround could cease to work in future versions, since the list is not actually modified with this operation and triggering a list change event is not required by the contract of ObservableList (but replacing the TableColumn with a new instance (and copying the properties) should always work).
Hard to say given the code snippets. Maybe you're not on the javaFX thread when doing the update? In that case use Platform.runLater(...), or share some minimal amout of code to reproduce the problem.