TLDR
Is an adaption of an extractor callback approach a good solution if I want to cause a TableView to refresh (without calling the refresh() method) when some arbitrary ObservableValue has changed but the underlying TableView data has not changed?
Here is an example of an implementation that uses the TableView refresh() method.
package com.example.rtv1;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
public class App extends Application {
/**
* A data class exemplar.
*/
public static class Planet {
private final StringProperty name;
private final DoubleProperty mass;
private final DoubleProperty uncertainty;
public Planet(String name, double mass, double uncertainty) {
this.name = new SimpleStringProperty(name);
this.mass = new SimpleDoubleProperty(mass);
this.uncertainty = new SimpleDoubleProperty(uncertainty);
}
public StringProperty getNameProperty() {
return name;
}
public DoubleProperty getMassProperty() {
return mass;
}
public DoubleProperty getUncertaintyProperty() {
return uncertainty;
}
}
/**
* Provides a CellFactory and CellValueFactory that use an external
* (to the TableView) property to control how data is represented.
*/
public class DerivedCell<S extends Planet> {
private final ObservableValue<Boolean> watch;
private final NumberFormat valueFormatter;
private final NumberFormat percentFormatter;
private final DoubleProperty value;
public DerivedCell(ObservableValue<Boolean> watch) {
this.watch = watch;
valueFormatter = new DecimalFormat("0.000E0");
percentFormatter = new DecimalFormat("0.0000");
value = new SimpleDoubleProperty(Double.NaN);
// I could put a listener here to invalidate value (perhaps set
// it to NAN) when the boolean property is toggled, but my concern
// is that could fire many listeners. Is that less overhead than
// calling TableView.refresh()?
}
/**
* Provides a CellFactory that will change formatting based on
* an external property.
*/
public Callback<TableColumn<S, Double>, TableCell<S, Double>> forCell() {
return list -> new TableCell<S, Double>() {
@Override
public void updateItem(Double item, boolean empty
) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
setText(watch.getValue()
? percentFormatter.format(item)
: valueFormatter.format(item));
}
}
};
}
/**
* Provides a CellValueFactory that will change the representation of
* the data based on an external property.
*/
public Callback<TableColumn.CellDataFeatures<S, Double>, ObservableValue<Double>> getValue() {
return r -> {
var u = r.getValue().getUncertaintyProperty().get();
if (watch.getValue()) {
var v = r.getValue().getMassProperty().get();
value.setValue(100.0 * u / v);
} else {
value.setValue(u);
}
return value.asObject();
};
}
}
@Override
public void start(Stage stage) throws Exception {
var planets = FXCollections.observableArrayList(
// From: https://en.wikipedia.org/wiki/List_of_Solar_System_objects_by_size
new Planet("Mercury", 330.11E21, 0.02E21),
new Planet("Venus", 4867.5E21, 0.2E21),
new Planet("Earth", 5972.4E21, 0.3E21),
new Planet("Mars", 641.71E21, 0.03E21),
new Planet("Jupiter", 1898187E21, 88E21),
new Planet("Saturn", 568317E21, 13E21),
new Planet("Uranus", 86813E21, 4E21),
new Planet("Neptune", 102413E21, 5E21),
new Planet("Pluto", 13.03E21, 0.03E21)
);
var layout = new VBox();
var toggle = new CheckBox("Uncertainty as %");
toggle.selectedProperty().setValue(true);
var table = new TableView<Planet>();
var nameCol = new TableColumn<Planet, String>("Name");
var massCol = new TableColumn<Planet, Double>("Mass");
var uncCol = new TableColumn<Planet, Double>("Uncertainty");
var derived = new DerivedCell<Planet>(toggle.selectedProperty());
nameCol.setCellValueFactory(
r -> r.getValue().getNameProperty());
massCol.setCellValueFactory(
r -> r.getValue().getMassProperty().asObject());
uncCol.setCellValueFactory(derived.getValue());
uncCol.setCellFactory(derived.forCell());
table.getColumns().addAll(nameCol, massCol, uncCol);
table.setItems(planets);
// Call the maligned refresh() TableView method when the CheckBox
// changes state. It would be fantastic if there was a way to
// call the fireChanged() method in the observable list...
toggle.selectedProperty().addListener(
(var ov, var t, var t1) -> {
table.refresh();
});
layout.getChildren().addAll(toggle, table);
var scene = new Scene(layout);
stage.setTitle("Refreshable TableView");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
The Details
When a similar question was asked previously, @kleopatra stated "no, never-ever use refresh."
I have a case where the underlying data is not changing, only how it is represented in the TableView. Specifically, I have two columns, a measurement and an uncertainty. The user is able to control the measurement units (e.g. kg) and whether the uncertainty should be displayed in the units of the measurement or as a percentage.
The cell factory and the cell value factory use properties that control how to represent the value and numerical formatting. When one of those properties change, a change listener is fired and the TableView is refreshed.
Considered Solutions
Simple Solutions
- Toggle the visibility off and on via setVisible();
- Create a derived ObservableList from the original that is in the desired representation and use setItems();
- Futz with the size of the Node to trigger a refresh;
- Do a getItems(), clear the TableView, and do a setItems(); and
- The refresh() method.
I've tried #2 and #5 and both obviously work. Options 1, 3, and 4 seem like kludge. I'm not a fan of #2 because it is wasteful of memory and I personally don't think the underlying data should be changed to reflect how it is represented to the user.
Other refresh() free solutions
fireChanged()
I looked at the fireChanged() method in ObservableList, however, it is protected and not public. Philosophically, it isn't the data that has changed, just the representation--so I am not a fan of this solution.
Extractor Callback
I think this approach can be adapted to solve this problem, but I have not implemented a solution yet (and is the rationale for this question).
Multiple TableColumns
I can create TableColumns for the different representations and change the visiblity to reflect the desired configuration, but that seems wasteful (but it might be better than refresh()--I'm not sure if refresh() causes all the cells to be created each time it is called).
Style Manipulation
I am thinking that doing something with the style might trigger a refresh, but I have not explored this in any detail.
The End
Of the above solutions, I think the extractor callback is the way to proceed. I do think I am missing the obvious solution, so any insight is greatly appreciated.