javafx - TableView 嵌套 UI 元素在动态添加行时丢失引用数据对象
javafx - TableView Nested UI elements looses reference data object on dynamic addition of row
我正在尝试创建一个 API,它可以在 table 视图的每个单元格中使用不同的嵌套 UI 组件动态填充 table 视图。我能够使用 table 将数据集绑定到模型对象。
问题是当我尝试动态添加并尝试启用编辑时,对象引用似乎搞砸了。
供参考:
如您所见,我的最后一栏有 4 个按钮,分别是添加、编辑、删除、重置功能。单击添加后 - 它会克隆当前行,单击编辑 - 它会启用 Coulmn 类别的 ComboBox
,单击删除 - 它会删除当前行。
我遇到的是,在添加多个条目时,我确实动态添加了行,但随后单击第一行编辑按钮 - 然后启用了多个 ComboBox
,这不是预期用途。用例是当前行的 ComboBox
必须只启用。
实施:我编写了扩展 TableView<S>
的自定义 API。
以下代码段可能有所帮助:
//column category
final ClumpElement< ConstraintsDataModel, String > categoryElement =
new ClumpElement<>( ClumpType.COMBOBOX, true, getCategoryData() );
categoryElement.setClumpTableCellValue( data -> data.categoryProperty() );
categoryElement.setClumpTableNodeAction( ( control, data ) -> {
final ComboBox< String > comboBox = (ComboBox< String >)control;
comboBox.disableProperty().bind( data.disableProperty() );
} );
clumpTableView.addNewColumn( "Category", categoryElement );
// column Action
final ClumpElement< ConstraintsDataModel, String > buttonsElement =
new ClumpElement<>( ClumpType.GROUP_BUTTONS, 4, "+", "✎", "X", "↻" );
buttonsElement.setClumpTableNodeAction( ( control, data ) -> {
final Button button = (Button)control;
switch( button.getText() ) {
case "+":
final ConstraintsDataModel ref =
clumpTableView.getItems().get( clumpTableView.getItems().size() - 1 );
if( ConstraintsDataModel.isValidModel( ref ) )
clumpTableView.getItems().add( new ConstraintsDataModel( data ) );
else
System.out.println( "ERROR: Finish previous constraints" );
break;
case "✎":
data.setDisableValue( false );
button.setText( "✔" );
break;
case "✔":
data.setDisableValue( true );
button.setText( "✎" );
break;
default:
//NOTHING
break;
}
} );
clumpTableView.addNewColumn( "Action", buttonsElement );
clumpTableView.setItems( getData() );
这是我的 CustomTableView
class:
public < T > void addNewColumn( final String columnName, final ClumpElement< S, T > element ) {
final TableColumn< S, T > column = new TableColumn<>( columnName );
getColumns().add( column );
if( element.getClumpTableCellValue() != null ) {
column.setCellValueFactory( param -> element.getClumpTableCellValue()
.act( param.getValue() ) );
}
clumpCellCall( columnName, element, column );
}
private < T > void clumpCellCall( final String colName, final ClumpElement< S, T > element,
final TableColumn< S, T > column ) {
switch( element.getUiNode() ) {
case COMBOBOX:
if( element.getItems() != null && !element.getItems().isEmpty() ) {
column.setCellFactory( param -> {
final ClumpComboBoxTableCell< S, T > clumpComboBoxTableCell =
new ClumpComboBoxTableCell<>( element.isDisable(), element.getItems() );
clumpComboBoxTableCell.prefWidthProperty().bind( column.widthProperty() );
clumpComboBoxTableCell.selectionListener( element );
return clumpComboBoxTableCell;
} );
}
break;
case GROUP_BUTTONS:
column.setCellFactory( param -> {
final ClumpButtonsTableCell< S, T > clumpButtonsTableCell =
new ClumpButtonsTableCell<>( element.getNoOfElements() );
clumpButtonsTableCell.prefWidthProperty().bind( column.widthProperty() );
IntStream.range( 0, element.getNoOfElements() ).forEach( item -> {
final Button button = clumpButtonsTableCell.getButtons().get( item );
button.setText( element.getNames().get( item ) );
button.setOnAction( event -> {
if( element.getClumpTableNodeAction() != null
&& clumpButtonsTableCell.getIndex() < getItems().size() ) {
element.getClumpTableNodeAction()
.act( button, getItems().get( clumpButtonsTableCell.getIndex() ) );
}
} );
} );
return clumpButtonsTableCell;
} );
break;
default:
column.setCellFactory( params -> {
final TextFieldTableCell< S, T > textFieldTableCell = new TextFieldTableCell<>();
textFieldTableCell.setConverter( new StringConverter< T >() {
@Override
public String toString( final T object ) {
return (String)object;
}
@Override
public T fromString( final String string ) {
return (T)string;
}
} );
return textFieldTableCell;
} );
break;
}
}
在我的自定义 API 中,它将调用自定义 TableCell<S,T>
,根据文档,它具有 ComboBox<T>
非常标准的实现。这里它在一个选择监听器中,因为我发现当单元格呈现时,只有这个选择监听器被调用。
public abstract class AbstractClumpTableCell< S, T > extends TableCell< S, T > {
public AbstractClumpTableCell() {
setContentDisplay( ContentDisplay.GRAPHIC_ONLY );
setAlignment(Pos.CENTER);
}
public abstract void renewItem( T item );
@Override
protected void updateItem( T item, boolean empty ) {
super.updateItem( item, empty );
if( empty ) {
setGraphic( null );
} else {
renewItem( item );
}
}
}
public class ClumpComboBoxTableCell< S, T > extends AbstractClumpTableCell< S, T > {
private final ComboBox< T > comboBox;
@SuppressWarnings( "unchecked" )
public ClumpComboBoxTableCell( final boolean isDisable, final ObservableList< T > item ) {
super();
this.comboBox = new ComboBox<>( item );
this.comboBox.setDisable( isDisable );
this.comboBox.valueProperty().addListener( ( obs, oVal, nVal ) -> {
ObservableValue< T > property = getTableColumn().getCellObservableValue( getIndex() );
if( property instanceof WritableValue ) {
((WritableValue< T >)property).setValue( nVal );
}
} );
}
@Override
public void renewItem( T item ) {
comboBox.setValue( item );
setGraphic( comboBox );
}
public ComboBox< T > getComboBox() {
return comboBox;
}
protected void selectionListener( final ClumpElement< S, T > element ) {
this.comboBox.getSelectionModel().selectedItemProperty().addListener( ( obs, oVal, nVal ) -> {
if( element.getClumpTableNodeAction() != null
&& getIndex() < getTableView().getItems().size() ) {
element.getClumpTableNodeAction().act( this.comboBox,
getTableView().getItems().get( getIndex() ) );
}
} );
}
}
我的数据模型有一个 SimpleStringProperty
相应地绑定到该列。
那么,如何在 TableView<S>
中正确绑定嵌套 UI 元素?我的方法正确还是有其他选择?
我会尝试回答,但正如我所说,代码对我来说很难理解(尤其是因为它是部分的,所以有些方法我只能假设其目的)。
如评论中所述,问题是 TableView
中的节点虚拟化。你不能绕过它,你真的不想 - 这是一种大大提高性能的方法,因为你不需要成百上千的 UI 节点(它们是 "heavy"并降低性能),但仅足以填充 table 的显示部分,从而支持更大的数据集。
问题,据我所知,你有一些 属性 的 行 (当前是否是 editable ) 您需要在某些 列 中反映出来。更具体地说,您希望组合框的 disable
属性 始终反映它所属行的 disable
属性,因此在 updateItem
中您必须做这样的事情:
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(T, empty);
if (empty) {
setGraphic(null);
} else {
renewItem(item);
// since the disable property if given by the row value, not only the column value
// we need to get the row value. The cast is needed due to a design oversight
// in JavaFX 8, which is fixed in newer versions. See https://bugs.openjdk.java.net/browse/JDK-8144088
ConstraintsDataModel data = ((TableRow<ConstraintsDataModel>)getTableRow())
.getItem();
combobox.disableProperty().unbind();
combobox.disableProperty().bind(data.disableProperty());
}
}
这是假设您的行数据类型确实是 ConstaintDataModel
,我无法完全理解。
另一个可能更优雅的选项是使用行的 editing
属性 - 将组合框的 disable
属性 绑定到 editing
属性行,开始和结束编辑时使用startEdit
和cancelEdit
/commitEdit
。这样您就不必重新绑定组合框的禁用 属性,因为它总是指向正确的行。
我正在尝试创建一个 API,它可以在 table 视图的每个单元格中使用不同的嵌套 UI 组件动态填充 table 视图。我能够使用 table 将数据集绑定到模型对象。
问题是当我尝试动态添加并尝试启用编辑时,对象引用似乎搞砸了。
供参考:
如您所见,我的最后一栏有 4 个按钮,分别是添加、编辑、删除、重置功能。单击添加后 - 它会克隆当前行,单击编辑 - 它会启用 Coulmn 类别的 ComboBox
,单击删除 - 它会删除当前行。
我遇到的是,在添加多个条目时,我确实动态添加了行,但随后单击第一行编辑按钮 - 然后启用了多个 ComboBox
,这不是预期用途。用例是当前行的 ComboBox
必须只启用。
实施:我编写了扩展 TableView<S>
的自定义 API。
以下代码段可能有所帮助:
//column category
final ClumpElement< ConstraintsDataModel, String > categoryElement =
new ClumpElement<>( ClumpType.COMBOBOX, true, getCategoryData() );
categoryElement.setClumpTableCellValue( data -> data.categoryProperty() );
categoryElement.setClumpTableNodeAction( ( control, data ) -> {
final ComboBox< String > comboBox = (ComboBox< String >)control;
comboBox.disableProperty().bind( data.disableProperty() );
} );
clumpTableView.addNewColumn( "Category", categoryElement );
// column Action
final ClumpElement< ConstraintsDataModel, String > buttonsElement =
new ClumpElement<>( ClumpType.GROUP_BUTTONS, 4, "+", "✎", "X", "↻" );
buttonsElement.setClumpTableNodeAction( ( control, data ) -> {
final Button button = (Button)control;
switch( button.getText() ) {
case "+":
final ConstraintsDataModel ref =
clumpTableView.getItems().get( clumpTableView.getItems().size() - 1 );
if( ConstraintsDataModel.isValidModel( ref ) )
clumpTableView.getItems().add( new ConstraintsDataModel( data ) );
else
System.out.println( "ERROR: Finish previous constraints" );
break;
case "✎":
data.setDisableValue( false );
button.setText( "✔" );
break;
case "✔":
data.setDisableValue( true );
button.setText( "✎" );
break;
default:
//NOTHING
break;
}
} );
clumpTableView.addNewColumn( "Action", buttonsElement );
clumpTableView.setItems( getData() );
这是我的 CustomTableView
class:
public < T > void addNewColumn( final String columnName, final ClumpElement< S, T > element ) {
final TableColumn< S, T > column = new TableColumn<>( columnName );
getColumns().add( column );
if( element.getClumpTableCellValue() != null ) {
column.setCellValueFactory( param -> element.getClumpTableCellValue()
.act( param.getValue() ) );
}
clumpCellCall( columnName, element, column );
}
private < T > void clumpCellCall( final String colName, final ClumpElement< S, T > element,
final TableColumn< S, T > column ) {
switch( element.getUiNode() ) {
case COMBOBOX:
if( element.getItems() != null && !element.getItems().isEmpty() ) {
column.setCellFactory( param -> {
final ClumpComboBoxTableCell< S, T > clumpComboBoxTableCell =
new ClumpComboBoxTableCell<>( element.isDisable(), element.getItems() );
clumpComboBoxTableCell.prefWidthProperty().bind( column.widthProperty() );
clumpComboBoxTableCell.selectionListener( element );
return clumpComboBoxTableCell;
} );
}
break;
case GROUP_BUTTONS:
column.setCellFactory( param -> {
final ClumpButtonsTableCell< S, T > clumpButtonsTableCell =
new ClumpButtonsTableCell<>( element.getNoOfElements() );
clumpButtonsTableCell.prefWidthProperty().bind( column.widthProperty() );
IntStream.range( 0, element.getNoOfElements() ).forEach( item -> {
final Button button = clumpButtonsTableCell.getButtons().get( item );
button.setText( element.getNames().get( item ) );
button.setOnAction( event -> {
if( element.getClumpTableNodeAction() != null
&& clumpButtonsTableCell.getIndex() < getItems().size() ) {
element.getClumpTableNodeAction()
.act( button, getItems().get( clumpButtonsTableCell.getIndex() ) );
}
} );
} );
return clumpButtonsTableCell;
} );
break;
default:
column.setCellFactory( params -> {
final TextFieldTableCell< S, T > textFieldTableCell = new TextFieldTableCell<>();
textFieldTableCell.setConverter( new StringConverter< T >() {
@Override
public String toString( final T object ) {
return (String)object;
}
@Override
public T fromString( final String string ) {
return (T)string;
}
} );
return textFieldTableCell;
} );
break;
}
}
在我的自定义 API 中,它将调用自定义 TableCell<S,T>
,根据文档,它具有 ComboBox<T>
非常标准的实现。这里它在一个选择监听器中,因为我发现当单元格呈现时,只有这个选择监听器被调用。
public abstract class AbstractClumpTableCell< S, T > extends TableCell< S, T > {
public AbstractClumpTableCell() {
setContentDisplay( ContentDisplay.GRAPHIC_ONLY );
setAlignment(Pos.CENTER);
}
public abstract void renewItem( T item );
@Override
protected void updateItem( T item, boolean empty ) {
super.updateItem( item, empty );
if( empty ) {
setGraphic( null );
} else {
renewItem( item );
}
}
}
public class ClumpComboBoxTableCell< S, T > extends AbstractClumpTableCell< S, T > {
private final ComboBox< T > comboBox;
@SuppressWarnings( "unchecked" )
public ClumpComboBoxTableCell( final boolean isDisable, final ObservableList< T > item ) {
super();
this.comboBox = new ComboBox<>( item );
this.comboBox.setDisable( isDisable );
this.comboBox.valueProperty().addListener( ( obs, oVal, nVal ) -> {
ObservableValue< T > property = getTableColumn().getCellObservableValue( getIndex() );
if( property instanceof WritableValue ) {
((WritableValue< T >)property).setValue( nVal );
}
} );
}
@Override
public void renewItem( T item ) {
comboBox.setValue( item );
setGraphic( comboBox );
}
public ComboBox< T > getComboBox() {
return comboBox;
}
protected void selectionListener( final ClumpElement< S, T > element ) {
this.comboBox.getSelectionModel().selectedItemProperty().addListener( ( obs, oVal, nVal ) -> {
if( element.getClumpTableNodeAction() != null
&& getIndex() < getTableView().getItems().size() ) {
element.getClumpTableNodeAction().act( this.comboBox,
getTableView().getItems().get( getIndex() ) );
}
} );
}
}
我的数据模型有一个 SimpleStringProperty
相应地绑定到该列。
那么,如何在 TableView<S>
中正确绑定嵌套 UI 元素?我的方法正确还是有其他选择?
我会尝试回答,但正如我所说,代码对我来说很难理解(尤其是因为它是部分的,所以有些方法我只能假设其目的)。
如评论中所述,问题是 TableView
中的节点虚拟化。你不能绕过它,你真的不想 - 这是一种大大提高性能的方法,因为你不需要成百上千的 UI 节点(它们是 "heavy"并降低性能),但仅足以填充 table 的显示部分,从而支持更大的数据集。
问题,据我所知,你有一些 属性 的 行 (当前是否是 editable ) 您需要在某些 列 中反映出来。更具体地说,您希望组合框的 disable
属性 始终反映它所属行的 disable
属性,因此在 updateItem
中您必须做这样的事情:
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(T, empty);
if (empty) {
setGraphic(null);
} else {
renewItem(item);
// since the disable property if given by the row value, not only the column value
// we need to get the row value. The cast is needed due to a design oversight
// in JavaFX 8, which is fixed in newer versions. See https://bugs.openjdk.java.net/browse/JDK-8144088
ConstraintsDataModel data = ((TableRow<ConstraintsDataModel>)getTableRow())
.getItem();
combobox.disableProperty().unbind();
combobox.disableProperty().bind(data.disableProperty());
}
}
这是假设您的行数据类型确实是 ConstaintDataModel
,我无法完全理解。
另一个可能更优雅的选项是使用行的 editing
属性 - 将组合框的 disable
属性 绑定到 editing
属性行,开始和结束编辑时使用startEdit
和cancelEdit
/commitEdit
。这样您就不必重新绑定组合框的禁用 属性,因为它总是指向正确的行。