Search code examples
javafxtableviewcoercion

Why can't I specify NamedArgs and item elements on cellFactory?


I have the following tableView in fxml

<TableView fx:id="tableView" prefHeight="525.0" prefWidth="814.0">
  <columns>
    <TableColumn prefWidth="75.0">
      <graphic><ToggleButton fx:id="mainToggleButton" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" onAction="#onMainToggleButtonAction" text="Start all" /></graphic>
      <cellFactory><ToggleButtonTableCellFactory maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" activatedText="Started" deactivatedText="Stopped"/></cellFactory>
      <cellValueFactory><PropertyValueFactory property="state"/></cellValueFactory>
    </TableColumn>
    <TableColumn prefWidth="75.0" text="Side">
      <cellValueFactory><PropertyValueFactory property="side"/></cellValueFactory>
    </TableColumn>
    <TableColumn prefWidth="75.0" text="Source">
      <cellValueFactory><PropertyValueFactory property="sourceContract"/></cellValueFactory>
    </TableColumn>
    <TableColumn prefWidth="75.0" text="Reference" editable="true">
      <cellFactory>
        <ChoiceBoxTableCellFactory maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308">
          <items>
            <FXCollections fx:factory="observableArrayList">
              <String fx:value="r01" />
              <String fx:value="r02" />
            </FXCollections>
          </items>
        </ChoiceBoxTableCellFactory>
      </cellFactory>
      <cellValueFactory><PropertyValueFactory property="referenceContract"/></cellValueFactory>
    </TableColumn>
    <TableColumn prefWidth="75.0" text="Destination">
      <cellValueFactory><PropertyValueFactory property="destinationContract"/></cellValueFactory>
    </TableColumn>
    <TableColumn prefWidth="75.0" text="Margin" editable="true">
      <cellFactory><SpinnerTableCellFactory maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" min="0" max="1" initialValue="0" amountToStepBy="0.025" decimalFormat="0.000"/></cellFactory>
      <cellValueFactory><PropertyValueFactory property="margin"/></cellValueFactory>
    </TableColumn>
    <TableColumn prefWidth="75.0" text="Bot">
      <cellValueFactory><PropertyValueFactory property="bot"/></cellValueFactory>
    </TableColumn>
    <TableColumn prefWidth="75.0" text="Price">
      <cellValueFactory><PropertyValueFactory property="price"/></cellValueFactory>
    </TableColumn>
    <TableColumn prefWidth="75.0" text="Volume">
      <cellValueFactory><PropertyValueFactory property="volume"/></cellValueFactory>
    </TableColumn>
  </columns>
  <items>
    <FXCollections fx:factory="observableArrayList">
      <GridRowModel state="false" side="BID" sourceContract="s01" referenceContract="r01" destinationContract="d01" margin="0" bot="MinMax" price="15.125" volume="0" />
      <GridRowModel state="false" side="ASK" sourceContract="s02" referenceContract="r01" destinationContract="d02" margin="0" bot="MinMax" price="15.125" volume="0" />
    </FXCollections>
  </items>
</TableView>

The ChoiceBoxTableCellFactory has a constructor which takes both named arguments from the fxml and a setter for the items element.

public class ChoiceBoxTableCellFactory<S, T> implements Callback<TableColumn<S, String>, TableCell<S, String>> {

  private ObservableList<String> items;
  private double maxHeight;
  private double maxWidth;

  public ChoiceBoxTableCellFactory() {
  }

  public ChoiceBoxTableCellFactory(
    @NamedArg("maxHeight") double maxHeight,
    @NamedArg("maxWidth") double maxWidth) {
    this.maxHeight = maxHeight;
    this.maxWidth = maxWidth;
  }

  @Override
  public TableCell<S, String> call(TableColumn<S, String> param) {
    return new TableCell<S, String>() {

      ChoiceBox<String> choiceBox = new ChoiceBox<>(getItems());

      {
        choiceBox.setMaxHeight(maxHeight);
        choiceBox.setMaxWidth(maxWidth);
        choiceBox.valueProperty().addListener((obs, oldValue, newValue) -> {
          ObservableValue<String> value = getTableColumn().getCellObservableValue(getIndex());
          if (value instanceof WritableValue) {
            ((WritableValue<String>) value).setValue(newValue);
          }
        });
      }

      @Override
      protected void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);
        if (empty) {
          setText(null);
          setGraphic(null);
        } else {
          if (isEditing()) {
            setText(null);
            setGraphic(null);
          } else {
            choiceBox.setValue(item);
            setText(null);
            setGraphic(choiceBox);
          }
        }
      }
    };
  }

  public ObservableList<String> getItems() {
    return items;
  }

  public void setItems(ObservableList<String> items) {
    this.items = items;
  }
}

But that throws this exception

Caused by: java.lang.IllegalArgumentException: Unable to coerce [[r01, r02]] to interface javafx.collections.ObservableList.
    at com.sun.javafx.fxml.BeanAdapter.coerce(BeanAdapter.java:496)
    at com.sun.javafx.fxml.builder.ProxyBuilder$Setter.invoke(ProxyBuilder.java:533)
    at com.sun.javafx.fxml.builder.ProxyBuilder.createObjectFromDefaultConstructor(ProxyBuilder.java:338)
    ... 144 more

When I remove the maxHeight and maxWidth attributes the items field is set correctly through the setter. With these attributes the items value is wrapped in an additional array. How can I achieve the desired result?


Solution

  • That looks like it should work. I tried a simpler test and the following workarounds seemed to work there:

    If you don't need the setItems() method anywhere else in your code, remove it and use the read only list properties approach. I.e. remove the setItems(...) method entirely, and in the FXML do

        <ChoiceBoxTableCellFactory maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308">
          <items>
            <String fx:value="r01" />
            <String fx:value="r02" />
          </items>
        </ChoiceBoxTableCellFactory>
    

    The other way around seems to be to use an <fx:define> block to define the items, and initialize it using an attribute:

    <fx:define>
        <FXCollections fx:factory="observableArrayList" fx:id="items">
            <String fx:value="r01"/>
            <String fx:value="r02"/>
        </FXCollections>
    <fx:define>
    <ChoiceBoxTableCellFactory maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308"
        items="$items" />