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 属性行,开始和结束编辑时使用startEditcancelEdit/commitEdit。这样您就不必重新绑定组合框的禁用 属性,因为它总是指向正确的行。