Refreshing a column of comboboxes in a table view, javafx - java

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.

Related

JavaFX ChoiceBox - How can you update the text of the popup items?

I have a ChoiceBox where I can select the language for my program. When I select another language, the label gets translated as desired (because it is recomputed using ChoiceBoxSkin#getDisplayText and my StringConverter takes the language into account), but the elements in the popup list stay the same.
Now, I could do something like
public void updateStrings() {
var converter = getConverter();
setConverter(null);
setConverter(converter);
var selected = valueProperty().getValue();
valueProperty().setValue(null);
valueProperty().setValue(selected);
}
in my ChoiceBox-subclass. This will re-populate the popup list with the correctly translated texts. Setting the value again is necessary beacause ChoiceBoxSkin#updatePopupItems (which is triggered when changing the converter) also resets the toggleGroup. That means that the selected item would no longer be marked as selected in the popup list.
Despite being kind of ugly, this actually works for my current use case. However, it breaks if any listener of the valueProperty does something problematic on either setting it to null or selecting the desired item a second time.
Am I missing a cleaner or just all-around better way to achieve this?
Another approach might be to use a custom ChoiceBoxSkin. Extending that, I'd have access to ChoiceBoxSkin#getChoiceBoxPopup (although that is commented with "Test only purpose") and could actually bind the text properties of the RadioMenuItems to the corresponding translated StringProperty. But that breaks as soon as ChoiceBoxSkin#updatePopupItems is triggered from anywhere else...
A MRP should be:
import javafx.scene.control.ChoiceBox;
import javafx.util.StringConverter;
public class LabelChangeChoiceBox extends ChoiceBox<String> {
private boolean duringUpdate = false;
public LabelChangeChoiceBox() {
getItems().addAll("A", "B", "C");
setConverter(new StringConverter<>() {
#Override
public String toString(String item) {
return item + " selected:" + valueProperty().getValue();
}
#Override
public String fromString(String unused) {
throw new UnsupportedOperationException();
}
});
valueProperty().addListener((observable, oldValue, newValue) -> {
if(duringUpdate) {
return;
}
duringUpdate = true;
updateStrings();
duringUpdate = false;
});
}
public void updateStrings() {
var converter = getConverter();
setConverter(null);
setConverter(converter);
var selected = valueProperty().getValue();
valueProperty().setValue(null);
valueProperty().setValue(selected);
}
}
And an Application-class like
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import ui.LabelChangeChoiceBox;
public class Launcher extends Application {
#Override
public void start(Stage stage) {
Scene scene = new Scene(new LabelChangeChoiceBox());
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
This works but needs the duringUpdate variable and can break if there is another change listener.
I’m not sure if this meets your needs, as your description of the problem is unclear in a few places.
Here’s a ChoiceBox which updates its converter using its own chosen language, and also retains its value when that change occurs:
import java.util.Locale;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.layout.BorderPane;
import javafx.util.StringConverter;
public class FXLocaleSelector
extends Application {
#Override
public void start(Stage stage) {
ChoiceBox<Locale> choiceBox = new ChoiceBox<>();
choiceBox.getItems().addAll(
Locale.ENGLISH,
Locale.FRENCH,
Locale.GERMAN,
Locale.ITALIAN,
Locale.CHINESE,
Locale.JAPANESE,
Locale.KOREAN
);
choiceBox.converterProperty().bind(
Bindings.createObjectBinding(
() -> createConverter(choiceBox.getValue()),
choiceBox.valueProperty()));
BorderPane pane = new BorderPane(choiceBox);
pane.setPadding(new Insets(40));
stage.setScene(new Scene(pane));
stage.setTitle("Locale Selector");
stage.show();
}
private StringConverter<Locale> createConverter(Locale locale) {
Locale conversionLocale =
(locale != null ? locale : Locale.getDefault());
return new StringConverter<Locale>() {
#Override
public String toString(Locale value) {
if (value != null) {
return value.getDisplayName(conversionLocale);
} else {
return "";
}
}
#Override
public Locale fromString(String s) {
return null;
}
};
}
public static void main(String[] args) {
launch(FXLocaleSelector.class, args);
}
}
Not entirely certain whether or not I understand your requirement correctly, my assumptions:
there's a ChoiceBox which contains the "language" for your ui, including the itself: lets say it contains the items Locale.ENGLISH and Locale.GERMAN, the visual representation of its items should be "English", "German" if its value is Locale.ENGLISH and "Englisch", "Deutsch" if its value is Locale.GERMAN
the visual representation is done by a StringConverter configurable with the value
If so, the solution is in separating out concerns - actually, it's not: the problem described (and hacked!) in the question is JDK-8088507: setting the converter doesn't update the selection of the menu items in the drop down. One hack is as bad or good as another, my personal preferenced would go for a custom skin which
adds a change listener to the converter property
reflectively calls updateSelection
Something like:
public static class MyChoiceBoxSkin<T> extends ChoiceBoxSkin<T> {
public MyChoiceBoxSkin(ChoiceBox<T> control) {
super(control);
registerChangeListener(control.converterProperty(), e -> {
// my local reflection helper, use your own
FXUtils.invokeMethod(ChoiceBoxSkin.class, this, "updateSelection");
});
}
}
Note: the hacks - this nor the OP's solution - do not solve the missing offset of the popup on first opening (initially or after selecting an item in the popup).
Not a solution to the question, just one way to have a value-dependent converter ;)
have a StringConverter with a fixed value (for simplicity) for conversion
have a converter controller having that a property with that value and a second property with a converter configured with the value: make sure the converter is replaced on change of the value
bind the controller's value to the box' value and the box' converter to the controller's converter
In (very raw) code:
public static class LanguageConverter<T> extends StringConverter<T> {
private T currentLanguage;
public LanguageConverter(T language) {
currentLanguage = language;
}
#Override
public String toString(T object) {
Object value = currentLanguage;
return "" + object + (value != null ? value : "");
}
#Override
public T fromString(String string) {
return null;
}
}
public static class LanguageController<T> {
private ObjectProperty<StringConverter<T>> currentConverter = new SimpleObjectProperty<>();
private ObjectProperty<T> currentValue = new SimpleObjectProperty<>() {
#Override
protected void invalidated() {
currentConverter.set(new LanguageConverter<>(get()));
}
};
}
Usage:
ChoiceBox<String> box = new ChoiceBox<>();
box.getItems().addAll("A", "B", "C");
box.getSelectionModel().selectFirst();
LanguageController<String> controller = new LanguageController<>();
controller.currentValue.bind(box.valueProperty());
box.converterProperty().bind(controller.currentConverter);

Java 8 method references with javafx

I just developed a JavaFX applications this twenty different pages. Each page has a table and I wanted to place a context menu on each table.
Basically its always the same code for placing the context menu to the table but I am hoping that method references can help here a little bit.
This is the actual code snippet:
resultTable.setRowFactory(new Callback<TableView<InterfaceModel>, TableRow<InterfaceModel>>() {
#Override
public TableRow<InterfaceModel> call(TableView<InterfaceModel> tableView) {
final TableRow<InterfaceModel> row = new TableRow<InterfaceModel>();
final ContextMenu rowMenu = new ContextMenu();
MenuItem editItem = new MenuItem("EDIT");
editItem.setOnAction(event -> {
// action if edit was selected
});
And I want something like that:
ContextMenuHelper helper = new ContextMenuHelper(resultTable);
helper.addItem("Edit", [referenceToAMethod]);
helper.addItem("Item 2", [referenceToADifferentMethod]);
What I mean is that this helper creates the context menu. All this helper needs is the label for the entry and a method to call after selection of this entry.
Is that possible with the method-refereces from java 8?
Thanks,
Hauke
If you just want to define a method for creating a MenuItem, then it's easy enough: you just need to decide on the functional interface you will need for the parameter that takes the method reference (or lambda, etc). E.g. if the method signature takes no parameters and has void return type, you could use Runnable:
public MenuItem createItem(String text, Runnable handler) {
MenuItem item = new MenuItem(text);
item.setOnAction(e -> handler.run());
}
You probably want the menu item event handler to have access to the table item in the row, in which case it would need a reference to the row:
public <T> MenuItem createItem(String text, TableRow<T> row, Consumer<T> handler) {
MenuItem item = new MenuItem(text);
item.setOnAction(e -> handler.accept(row.getItem()));
}
Then you can do
TableView<InterfaceModel> table = new TableView<>();
ContextMenuHelper helper = new ContextMenuHelper();
table.setRowFactory(t -> {
TableRow<InterfaceModel> row = new TableRow<>();
ContextMenu menu = new ContextMenu();
row.setContextMenu(menu);
menu.getItems().addItem(helper.createItem("Edit", row, this::edit));
// etc...
});
with
private void edit(InterfaceModel model) {
// ...
}
What you didn't actually ask, but I'm sort of guessing you really want, is for the "helper" class to actually set the row factory and create all the menus, etc. This is a bit harder to structure, because you need to entirely build the context menu inside the row factory, so you need to know all the menu items before you can actually set the row factory. For this, you probably want to consider a builder pattern:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.util.Callback;
public class TableRowContextMenuBuilder<T> {
private final List<MenuItemConfig<T>> items ;
private boolean built ;
public TableRowContextMenuBuilder() {
this.items = new ArrayList<>();
}
public static <T> TableRowContextMenuBuilder<T> create(Class<T> type) {
return new TableRowContextMenuBuilder<>();
}
public TableRowContextMenuBuilder<T> addItem(String text, Consumer<T> handler) {
if (built) {
throw new IllegalStateException("Row factory is already built: cannot add new items");
}
items.add(new MenuItemConfig<T>(text, handler));
return this ;
}
public TableRowContextMenuBuilder<T> addItem(String text, Runnable handler) {
return addItem(text, t -> handler.run());
}
public Callback<TableView<T>, TableRow<T>> build() {
if (built) {
throw new IllegalStateException("Cannot build row factory more than once");
}
built = true ;
return t -> {
TableRow<T> row = new TableRow<>();
ContextMenu menu = new ContextMenu();
row.setContextMenu(menu);
items.stream()
.map(config -> config.asMenuItem(row))
.forEach(menu.getItems()::add);
return row ;
};
}
public void buildForTable(TableView<T> table) {
table.setRowFactory(build());
}
private static class MenuItemConfig<T> {
private final String text ;
private final Consumer<T> handler ;
MenuItemConfig(String text, Consumer<T> handler) {
this.text = text;
this.handler = handler;
}
MenuItem asMenuItem(TableRow<T> row) {
MenuItem item = new MenuItem(text);
item.setOnAction(e -> handler.accept(row.getItem()));
return item ;
}
}
}
And now you can do
TableView<InterfaceModel> table = new TableView<>();
TableViewContextMenuBuilder.create(InterfaceModel.class)
.menuBuilder.addItem("Edit", this::edit);
.menuBuilder.addItem("Item 2", this::handleOtherItem);
// ...
.buildForTable(table);
with the appropriate methods defined:
private void edit(InterfaceModel model) { /* ... */}
private void handleOtherItem(InterfaceModel model) { /* ... */}

javafx: Bindings not working as expected

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

CheckBoxTreeItems always independent, not rendered properly. Why?

I am new to JavaFX and I am trying to create a simple TreeTableView, containing a
single Boolean column and is rendered with a CheckBoxTreeTableCell.
The problem I am having is that the two CheckBoxTreeItems seem independent (selecting
the root doesn't select the child and the other way around). I even try setting the
independancy manually (see commented code) but it makes no difference.
The documentation for CheckBoxTreeItems says that "By default, CheckBoxTreeItem instances are dependent", which doesn't seem to work for me.
Also, I am expecting toString() value of the Model class to be shown as checkboxes'
texts but no text is drawn, only empty checkboxes. Why is this?
And finally, it is possible to set a graphic node for a CheckBoxTreeItem, and this
node is then shown to the left of the CheckBoxTreeItem. Would it be possible to have
it drawn between the checkbox and the checkbox text instead? Something like:
[x][graphic_node]A simple checkbox text
I am using JDK 1.8.0_40
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.scene.Scene;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.cell.CheckBoxTreeTableCell;
import javafx.stage.Stage;
public final class CheckBoxTreeItemTest extends Application {
private Stage stage;
public static void main(String[] args) {
launch(args);
}
#Override
public final void start(final Stage stage) throws Exception {
this.stage = stage;
final CheckBoxTreeItem<Model> root = new CheckBoxTreeItem<>(new Model("Root"));
final CheckBoxTreeItem<Model> parent = new CheckBoxTreeItem<>(new Model("Parent"));
final CheckBoxTreeItem<Model> child = new CheckBoxTreeItem<>(new Model("Child"));
//Manually setting independence makes no difference
/*parent.setIndependent(false);
child.setIndependent(false);
root.setIndependent(false);*/
parent.getChildren().add(child);
root.getChildren().add(parent);
final TreeTableColumn<Model, Boolean> selectedColumn =
new TreeTableColumn<>("Selection");
selectedColumn.setEditable(true);
selectedColumn.setCellValueFactory(param -> param.getValue().getValue().selectedProperty());
selectedColumn.setCellFactory(CheckBoxTreeTableCell.<Model>forTreeTableColumn(selectedColumn));
final TreeTableView<Model> table = new TreeTableView<>(root);
table.setShowRoot(false);
table.setEditable(true);
table.getColumns().add(selectedColumn);
final Scene scene = new Scene(table, 500, 350);
stage.setScene(scene);
stage.show();
}
private class Model {
private final BooleanProperty selected;
private final StringProperty name;
public Model(final String name) {
this.selected = new SimpleBooleanProperty(false);
this.name = new SimpleStringProperty(name);
}
public final void setSelected(final boolean selected) {
this.selected.set(selected);
}
public final boolean isSelected() {
return selected.get();
}
public final BooleanProperty selectedProperty() {
return selected;
}
public final StringProperty nameProperty() {
return name;
}
#Override
public String toString() {
return "Model [selected=" + selected + ", name=" + name + "]";
}
}
}
A CheckBoxTreeItem provides a selected property. It is this property that respects the independent state of the CheckBoxTreeItem (i.e. if the parent CheckBoxTreeItem is selected, then this CheckBoxTreeItem is automatically selected, etc).
However, in your application, the CheckBoxTreeItem's selected property is not the property represented by the item, because you set the cell value factory to map to the selected property of the Model instance represented by the item. So checking the check box sets Model.selected to true, but of course there is no logic managing that property in terms of parent and/or child selected properties.
Typically when you have your own boolean property representing the state of the checkbox, you would not use a CheckBoxTreeItem. However, if you want the functionality of the non-independent properties, you would have to implement that yourself. Since that logic is actually quite complicated, if you want your own Model class, I would just bidirectionally bind the property of interest to the CheckBoxTreeItem's selectedProperty:
Model rootModel = new Model("Root");
final CheckBoxTreeItem<Model> root = new CheckBoxTreeItem<>(rootModel);
root.selectedProperty().bindBidirectional(rootModel.selectedProperty());
Model parentModel = new Model("Parent");
final CheckBoxTreeItem<Model> parent = new CheckBoxTreeItem<>( parentModel);
parent.selectedProperty().bindBidirectional(parentModel.selectedProperty());
Model childModel = new Model("Child");
final CheckBoxTreeItem<Model> child = new CheckBoxTreeItem<>(childModel);
child.selectedProperty().bindBidirectional(childModel.selectedProperty());

Are bindings automatically removed in a TreeCell

I have TreeView that has a cell factory set on it. The TreeCells I'm returning are displayed below:
import javafx.beans.binding.StringBinding;
import javafx.collections.ObservableMap;
import javafx.scene.control.TreeCell;
public class TreeCellTest extends TreeCell<String> {
private ObservableMap<String, StringBinding> lookup;
public TreeCellTest(ObservableMap<String, StringBinding> lookup) {
this.lookup = lookup;
}
#Override
protected void updateItem(String id, boolean empty) {
super.updateItem(id, empty);
if (empty) {
setText(null);
} else {
StringBinding stringBinding = lookup.get(id);
textProperty().bind(stringBinding);
}
}
}
Notice that I'm not setting the text but I'm binding the textProperty to a StringBinding. This works fine in normal situations but I'm wondering if it is OK to use it inside a TreeCell.
The TreeCell gets recycled as and when needed so I would like to know whether when this happens the binding gets automatically removed or whether I need to remove it manually?
I don't want the case where each TreeCell has 100's of bindings attached to it.
While it's not documented, it appears that calling bind(...) will remove any existing bindings before creating the new binding.
For example:
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class RebindingTest {
public static void main(String[] args) {
StringProperty text = new SimpleStringProperty();
StringProperty value1 = new SimpleStringProperty();
StringProperty value2 = new SimpleStringProperty();
text.addListener((obs, oldValue, newValue) -> System.out.printf("text changed from %s to %s%n", oldValue, newValue));
text.bind(value1);
value1.set("Set value 1");
text.bind(value2);
value2.set("Set value 2");
value1.set("Reset value 1");
}
}
So I think all you need to do to make your code work correctly is add
textProperty().unbind();
to the if (empty) { ... } block.
Of course, calling that unconditionally in your updateItem(...) method would mean you're not relying on undocumented behavior, and any loss of efficiency is probably minimal.

Categories