Takeshi Tokugawa YD
Takeshi Tokugawa YD

Reputation: 979

Dynamic attribute values in FXML?

In this JavaFX tutorial, address book application creation suggested. Registered person could be deleted, however before deleting, person must be selected in table view.

There will be an ArrayIndexOutOfBoundsException because it could not remove a person item at index -1. The index -1 was returned by getSelectedIndex() - which means that there was no selection.

To ignore such an error is not very nice, of course. We should let the user know that he/she must select a person before deleting. (Even better would be if we disabled the button so that the user doesn’t even have the chance to do something wrong.)

Author is totally correct about "Even better would be if we disabled the button ...", however he selected the first way. I suppose, manipulation of the button state is the essential skill for JavaFX application development, so I tried to implement the better solution.

Of course, we can do it by way like this:

peopleTable.getSelectionModel().selectedItemProperty().addListener(
  (observable, oldValue, newValue) -> {

    showPersonDetails(newValue);

    boolean somebodySelected = peopleTable.getSelectionModel().getSelectedIndex() >= 0;
    button.setDisable(!somebodySelected);
  }
);

How ever I am interested for the other way: using dynamic attribute value for button:

<Button
  mnemonicParsing="false"
  onAction="#handleDeletePerson"
  text="Delete"
  disable="disableDeleteButtonFlag"
/>

If dynamic attribute value is possible, there is no need explicitly invoke button.setDisable() anymore. However, below code does not work.

@FXML
private boolean disableDeleteButtonFlag = true;

// ...


@FXML
private void initialize() {


  // ...

  peopleTable.getSelectionModel().selectedItemProperty().addListener(
      (observable, oldValue, newValue) -> {
        showPersonDetails(newValue);
        disableDeleteButtonFlag = peopleTable.getSelectionModel().getSelectedIndex() < 0;
      }
  );
}

Upvotes: 0

Views: 703

Answers (1)

Slaw
Slaw

Reputation: 46116

First off, I'm not sure if referencing a boolean field that way would even let the FXML file be loaded (you don't specific exactly how it "doesn't work"). But ignoring that, fields are not observable in Java which means updating the field will not automagically cause the disable property of the Button to be updated. This is why JavaFX has the [ReadOnly]Property interfaces and introduces the "JavaFX Bean" (an extension to Java Bean that adds a "property getter"); it allows observers to see changes in an object's properties. Note you can bind a writable property to an ObservableValue so it always has the same value.

Now, I would have expected an expression binding would be what you're looking for, but the following:

<ListView fx:id="list"/>
<Button disable="${list.selectionModel.selectedIndex == -1}"/>

Doesn't appear to work—I get an exception (using JavaFX 12.0.1):

Caused by: java.lang.ClassCastException: class java.lang.Long cannot be cast to class java.lang.Integer (java.lang.Long and java.lang.Integer are in module java.base of loader 'bootstrap')
    at java.base/java.lang.Integer.compareTo(Integer.java:64)
    at javafx.fxml/com.sun.javafx.fxml.expression.Expression.lambda$equalTo$5(Expression.java:1105)
    at javafx.fxml/com.sun.javafx.fxml.expression.BinaryExpression.evaluate(BinaryExpression.java:55)
    at javafx.fxml/com.sun.javafx.fxml.expression.ExpressionValue.getValue(ExpressionValue.java:192)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper.addListener(ExpressionHelper.java:53)
    at javafx.base/javafx.beans.value.ObservableValueBase.addListener(ObservableValueBase.java:55)
    at javafx.fxml/com.sun.javafx.fxml.expression.ExpressionValue.addListener(ExpressionValue.java:201)
    at javafx.base/javafx.beans.binding.BooleanBinding.bind(BooleanBinding.java:106)
    at javafx.base/javafx.beans.property.BooleanPropertyBase$ValueWrapper.<init>(BooleanPropertyBase.java:254)
    at javafx.base/javafx.beans.property.BooleanPropertyBase.bind(BooleanPropertyBase.java:168)
    at javafx.fxml/javafx.fxml.FXMLLoader$Element.processPropertyAttribute(FXMLLoader.java:326)
    at javafx.fxml/javafx.fxml.FXMLLoader$Element.processInstancePropertyAttributes(FXMLLoader.java:242)
    at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processEndElement(FXMLLoader.java:775)
    at javafx.fxml/javafx.fxml.FXMLLoader.processEndElement(FXMLLoader.java:2838)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2557)
    ... 17 more

Using the selectedItem property instead:

<Button disable="${list.selectionModel.selectedItem == null}"/>

Gives a different exception:

Caused by: java.lang.NullPointerException
    at javafx.fxml/com.sun.javafx.fxml.expression.Expression.lambda$equalTo$5(Expression.java:1105)
    at javafx.fxml/com.sun.javafx.fxml.expression.BinaryExpression.evaluate(BinaryExpression.java:55)
    at javafx.fxml/com.sun.javafx.fxml.expression.ExpressionValue.getValue(ExpressionValue.java:192)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper.addListener(ExpressionHelper.java:53)
    at javafx.base/javafx.beans.value.ObservableValueBase.addListener(ObservableValueBase.java:55)
    at javafx.fxml/com.sun.javafx.fxml.expression.ExpressionValue.addListener(ExpressionValue.java:201)
    at javafx.base/javafx.beans.binding.BooleanBinding.bind(BooleanBinding.java:106)
    at javafx.base/javafx.beans.property.BooleanPropertyBase$ValueWrapper.<init>(BooleanPropertyBase.java:254)
    at javafx.base/javafx.beans.property.BooleanPropertyBase.bind(BooleanPropertyBase.java:168)
    at javafx.fxml/javafx.fxml.FXMLLoader$Element.processPropertyAttribute(FXMLLoader.java:326)
    at javafx.fxml/javafx.fxml.FXMLLoader$Element.processInstancePropertyAttributes(FXMLLoader.java:242)
    at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processEndElement(FXMLLoader.java:775)
    at javafx.fxml/javafx.fxml.FXMLLoader.processEndElement(FXMLLoader.java:2838)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2557)
    ... 17 more

As both of these exceptions are emanating from JavaFX code (i.e. not our code) this behavior is probably a bug. That, or I'm using expression bindings incorrectly—in which case I hope someone corrects me. Since attempting to bind the property in FXML is not working, the workaround is to bind the property in code.

<VBox xmlns="http://javafx.com/" xmlns:fx="http://javafx.com/fxml" fx:controller="Controller">
    <ListView fx:id="list"/>
    <Button fx:id="button"/>
</VBox>
public class Controller {

    @FXML private ListView<YourType> list;
    @FXML private Button button;

    @FXML
    private void initialize() {
        button.disableProperty()
                .bind(list.getSelectionModel().selectedIndexProperty().isEqualTo(-1));
    }

}

The isEqualTo method will return a BooleanBinding whose value is true whenever the selectedIndex property holds -1. The disable property is then bound to this BooleanBinding so it always has the same value. You can read Using JavaFX Properties and Binding for more information.

Upvotes: 2

Related Questions