Compile and run the following code. It creates a simple table with 1 column and 50 rows. Each of the rows is assigned an integer from 1 to 50, and the column has a custom value factory that also assigns those row values to each cell, such that:
cell.getItem() == cell.getTableRow().getItem()
*******************************************
import javafx.application.*;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.value.ObservableIntegerValue;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.*;
public class TableRowBug extends Application {
public static void main(String[] args) {
launch(args);
}
@Override public void start(Stage primaryStage) {
primaryStage.setTitle("getTableRow() Bug");
StackPane root = new StackPane();
root.getChildren().add( buildTable() );
primaryStage.setScene(new Scene(root, 500, 250));
primaryStage.show();
}
private TableView buildTable() {
// create a table with one column that displays TableRow.getItem() * 2
final TableView<Integer> table = new TableView<>();
TableColumn<Integer, Integer> col = new TableColumn<>(" getItem() ");
setupValueFactory( col );
setupCellFactory( col );
col.setPrefWidth( 100 );
table.getColumns().add( col );
// add integers from 1 to 50 as row items in the table
for ( int i = 1; i <= 50; i++ ) {
table.getItems().add(i);
}
return table;
}
// set up a value factory that use the row's value as each cell's value.
private void setupValueFactory(TableColumn col) {
col.setCellValueFactory(
new Callback<TableColumn.CellDataFeatures<Integer, Integer>,
ObservableIntegerValue>() {
public ObservableIntegerValue call(TableColumn.CellDataFeatures<Integer, Integer> p) {
return new ReadOnlyIntegerWrapper(p.getValue());
}
});
}
// display each cell value using a slightly customized cell.
private void setupCellFactory(TableColumn col) {
col.setCellFactory( new Callback<TableColumn<Integer,Integer>, TableCell<Integer,Integer>>() {
public TableCell<Integer, Integer> call( TableColumn<Integer, Integer> c ) {
return new TableCell() {
@Override protected void updateItem( Object item, boolean empty ) {
super.updateItem( item, empty );
setText( empty ? "" : item.toString() );
// this section shows the bug; namely that the tablerow's
// "item" property has not yet been updated, even though
// the cell's "item" property has. shouldn't the row
// always be updated first?
if ( !isEmpty() && !getTableRow().isEmpty() ) {
int expectedRowItem = ((Integer)getItem());
int rowItem = (Integer)getTableRow().getItem();
if ( expectedRowItem != rowItem ) {
System.out.println("Wrong row item: expected " +
expectedRowItem + ", instead was " + rowItem );
}
}
}
};
}
});
}
}
********************************************************
Now that you're running my demo app, start scrolling up and down through the rows. Right away, you'll see some system.out.printlns declaring "Wrong row item, etc, etc".
This is the bug. But what does this mean? Take a look at the setupCellFactory() method, where the error message originates. The TableView uses a custom TableCell that displays the cell's value as you'd expect, but also checks the cell's TableRow to see if its value is "correct", too. Recall that each cell's value is supposed to be the same as its row's value.
When these two values are different, it's because the TableCell's itemProperty() has been updated, but the cell's TableRow's itemProperty() has not. This happens all the time when you scroll through the table, but not during initial layout. Also, if you run this demo on (significantly) older builds of Java 8, the problem won't occur at all.
The problem is a timing issue; if you change the timing by wrapping the section that checks for the bug in a Platform.runLater(), then the row has a chance to be updated and the problem goes away.
I'm reporting this as a bug because I think it's natural to assume that the row value is always updated first, followed by each of its cells. The fact that it can happen in either order is a "gotcha" that pretty much anyone who writes a custom TableCell is going to run into sooner or later.
cell.getItem() == cell.getTableRow().getItem()
*******************************************
import javafx.application.*;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.value.ObservableIntegerValue;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.*;
public class TableRowBug extends Application {
public static void main(String[] args) {
launch(args);
}
@Override public void start(Stage primaryStage) {
primaryStage.setTitle("getTableRow() Bug");
StackPane root = new StackPane();
root.getChildren().add( buildTable() );
primaryStage.setScene(new Scene(root, 500, 250));
primaryStage.show();
}
private TableView buildTable() {
// create a table with one column that displays TableRow.getItem() * 2
final TableView<Integer> table = new TableView<>();
TableColumn<Integer, Integer> col = new TableColumn<>(" getItem() ");
setupValueFactory( col );
setupCellFactory( col );
col.setPrefWidth( 100 );
table.getColumns().add( col );
// add integers from 1 to 50 as row items in the table
for ( int i = 1; i <= 50; i++ ) {
table.getItems().add(i);
}
return table;
}
// set up a value factory that use the row's value as each cell's value.
private void setupValueFactory(TableColumn col) {
col.setCellValueFactory(
new Callback<TableColumn.CellDataFeatures<Integer, Integer>,
ObservableIntegerValue>() {
public ObservableIntegerValue call(TableColumn.CellDataFeatures<Integer, Integer> p) {
return new ReadOnlyIntegerWrapper(p.getValue());
}
});
}
// display each cell value using a slightly customized cell.
private void setupCellFactory(TableColumn col) {
col.setCellFactory( new Callback<TableColumn<Integer,Integer>, TableCell<Integer,Integer>>() {
public TableCell<Integer, Integer> call( TableColumn<Integer, Integer> c ) {
return new TableCell() {
@Override protected void updateItem( Object item, boolean empty ) {
super.updateItem( item, empty );
setText( empty ? "" : item.toString() );
// this section shows the bug; namely that the tablerow's
// "item" property has not yet been updated, even though
// the cell's "item" property has. shouldn't the row
// always be updated first?
if ( !isEmpty() && !getTableRow().isEmpty() ) {
int expectedRowItem = ((Integer)getItem());
int rowItem = (Integer)getTableRow().getItem();
if ( expectedRowItem != rowItem ) {
System.out.println("Wrong row item: expected " +
expectedRowItem + ", instead was " + rowItem );
}
}
}
};
}
});
}
}
********************************************************
Now that you're running my demo app, start scrolling up and down through the rows. Right away, you'll see some system.out.printlns declaring "Wrong row item, etc, etc".
This is the bug. But what does this mean? Take a look at the setupCellFactory() method, where the error message originates. The TableView uses a custom TableCell that displays the cell's value as you'd expect, but also checks the cell's TableRow to see if its value is "correct", too. Recall that each cell's value is supposed to be the same as its row's value.
When these two values are different, it's because the TableCell's itemProperty() has been updated, but the cell's TableRow's itemProperty() has not. This happens all the time when you scroll through the table, but not during initial layout. Also, if you run this demo on (significantly) older builds of Java 8, the problem won't occur at all.
The problem is a timing issue; if you change the timing by wrapping the section that checks for the bug in a Platform.runLater(), then the row has a chance to be updated and the problem goes away.
I'm reporting this as a bug because I think it's natural to assume that the row value is always updated first, followed by each of its cells. The fact that it can happen in either order is a "gotcha" that pretty much anyone who writes a custom TableCell is going to run into sooner or later.
- relates to
-
JDK-8251483 TableCell: NPE on modifying item's list
-
- Resolved
-
-
JDK-8344067 TableCell indices may not match the TableRow index
-
- Resolved
-