Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does a TableView's change listener give different results for ObjectProperty<T> vs TProperty columns in JavaFX8?

Tags:

A relative Java newbie question.

I have a TableView with extractors and a ListChangeListener added to the underlying ObservableList.

If I have a StringProperty column in the data model, the change listener doesn't detect changes if I double-click the cell and then hit ENTER without making any changes. That's good.

However, if I define the column as ObjectProperty<String> and double-click and then hit ENTER, the change listener always detects changes even when none have been made.

Why does that happen? What's the difference between ObjectProperty<String> and StringProperty from a change listener's point of view?

I've read Difference between SimpleStringProperty and StringProperty and JavaFX SimpleObjectProperty<T> vs SimpleTProperty and think I understand the differences. But I don't understand why the change listener is giving different results for TProperty/SimpleTProperty and ObjectProperty<T>.

If it helps, here is a MVCE for my somewhat nonsensical case. I'm actually trying to get a change listener working for BigDecimal and LocalDate columns and have been stuck on it for 5 days. If I can understand why the change listener is giving different results, I might be able to get my code working.

I'm using JavaFX8 (JDK1.8.0_181), NetBeans 8.2 and Scene Builder 8.3.

package test17;

import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.converter.DefaultStringConverter;

public class Test17 extends Application {

    private Parent createContent() {

        ObservableList<TestModel> olTestModel = FXCollections.observableArrayList(testmodel -> new Observable[] {
                testmodel.strProperty(),
                testmodel.strObjectProperty()
        });

        olTestModel.add(new TestModel("A", "a"));
        olTestModel.add(new TestModel("B", "b"));

        olTestModel.addListener((ListChangeListener.Change<? extends TestModel > c) -> {
            while (c.next()) {
                if (c.wasUpdated()) {
                    System.out.println("===> wasUpdated() triggered");
                }
            }
        });

        TableView<TestModel> table = new TableView<>();

        TableColumn<TestModel, String> strCol = new TableColumn<>("strCol");
        strCol.setCellValueFactory(cellData -> cellData.getValue().strProperty());
        strCol.setCellFactory(TextFieldTableCell.forTableColumn(new DefaultStringConverter()));
        strCol.setEditable(true);
        strCol.setPrefWidth(100);
        strCol.setOnEditCommit((CellEditEvent<TestModel, String> t) -> {
                ((TestModel) t.getTableView().getItems().get(
                        t.getTablePosition().getRow())
                        ).setStr(t.getNewValue());
        });

        TableColumn<TestModel, String> strObjectCol = new TableColumn<>("strObjectCol");
        strObjectCol.setCellValueFactory(cellData -> cellData.getValue().strObjectProperty());
        strObjectCol.setCellFactory(TextFieldTableCell.forTableColumn(new DefaultStringConverter()));
        strObjectCol.setEditable(true);
        strObjectCol.setPrefWidth(100);
        strObjectCol.setOnEditCommit((CellEditEvent<TestModel, String> t) -> {
            ((TestModel) t.getTableView().getItems().get(
                    t.getTablePosition().getRow())
                    ).setStrObject(t.getNewValue());
        });

        table.getColumns().addAll(strCol, strObjectCol);
        table.setItems(olTestModel);
        table.getSelectionModel().setCellSelectionEnabled(true);
        table.setEditable(true);

        BorderPane content = new BorderPane(table);
        return content;
    }

    public class TestModel {

        private StringProperty str;
        private ObjectProperty<String> strObject;

        public TestModel(
            String str,
            String strObject
        ) {
            this.str = new SimpleStringProperty(str);
            this.strObject = new SimpleObjectProperty(strObject);
        }

        public String getStr() {
            return this.str.get();
        }

        public void setStr(String str) {
            this.str.set(str);
        }

        public StringProperty strProperty() {
            return this.str;
        }

        public String getStrObject() {
            return this.strObject.get();
        }

        public void setStrObject(String strObject) {
            this.strObject.set(strObject);
        }

        public ObjectProperty<String> strObjectProperty() {
            return this.strObject;
        }

    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.setTitle("Test");
        stage.setWidth(350);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}
like image 422
GreenZebra Avatar asked Sep 19 '18 08:09

GreenZebra


People also ask

What does setCellValueFactory do?

setCellValueFactory. Sets the value of the property cellValueFactory.

What is TableView in JavaFX?

The TableView class provides built-in capabilities to sort data in columns. Users can alter the order of data by clicking column headers. The first click enables the ascending sorting order, the second click enables descending sorting order, and the third click disables sorting. By default, no sorting is applied.

What is PropertyValueFactory in JavaFX?

public PropertyValueFactory(String property) Creates a default PropertyValueFactory to extract the value from a given TableView row item reflectively, using the given property name. Parameters: property - The name of the property with which to attempt to reflectively extract a corresponding value for in a given object.


1 Answers

The difference can be seen by looking at the source code of StringPropertyBase and ObjectPropertyBase—specfically, their set methods.

StringPropertyBase

@Override
public void set(String newValue) {
    if (isBound()) {
        throw new java.lang.RuntimeException((getBean() != null && getName() != null ?
                getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set.");
    }
    if ((value == null)? newValue != null : !value.equals(newValue)) {
        value = newValue;
        markInvalid();
    }
}

ObjectPropertyBase

@Override
public void set(T newValue) {
    if (isBound()) {
        throw new java.lang.RuntimeException((getBean() != null && getName() != null ?
                getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set.");
    }
    if (value != newValue) {
        value = newValue;
        markInvalid();
    }
}

Notice the difference in how they check if the new value is equal to the old value? The StringPropertyBase class checks by using Object.equals whereas the ObjectPropertyBase class uses reference equality (==/!=).

I can't answer for certain why this difference exists, but I can hazard a guess: An ObjectProperty can hold anything and therefore there's the potential for Object.equals to be expensive; such as when using a List or Set. When coding StringPropertyBase I guess they decided that potential wasn't there, that the semantics of String equality was more important, or both. There may be more/better reasons for why they did what they did, but as I was not involved in development I'm not aware of them.


Interestingly, if you look at how they handle listeners (com.sun.javafx.binding.ExpressionHelper) you'll see that they check for equality using Object.equals. This equality check only occurs if there are currently ChangeListeners registered—probably to support lazy evaluation when there are no ChangeListeners.

If the new and old values are equals the ChangeListeners are not notified. This doesn't stop the InvalidationListeners from being notified, however. Thus, your ObservableList will fire an update change because that mechanism is based on InvalidationListeners and not ChangeListeners.

Here's the relevant source code:

ExpressionHelper$Generic.fireValueChangedEvent

@Override
protected void fireValueChangedEvent() {
    final InvalidationListener[] curInvalidationList = invalidationListeners;
    final int curInvalidationSize = invalidationSize;
    final ChangeListener<? super T>[] curChangeList = changeListeners;
    final int curChangeSize = changeSize;

    try {
        locked = true;
        for (int i = 0; i < curInvalidationSize; i++) {
            try {
                curInvalidationList[i].invalidated(observable);
            } catch (Exception e) {
                Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
            }
        }
        if (curChangeSize > 0) {
            final T oldValue = currentValue;
            currentValue = observable.getValue();
            final boolean changed = (currentValue == null)? (oldValue != null) : !currentValue.equals(oldValue);
            if (changed) {
                for (int i = 0; i < curChangeSize; i++) {
                    try {
                        curChangeList[i].changed(observable, oldValue, currentValue);
                    } catch (Exception e) {
                        Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
                    }
                }
            }
        }
    } finally {
        locked = false;
    }
}

And you can see this behavior in the following code:

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;

public class Main {

  public static void main(String[] args) {
    ObjectProperty<String> property = new SimpleObjectProperty<>("Hello, World!");
    property.addListener(obs -> System.out.printf("Property invalidated: %s%n", property.get()));
    property.addListener((obs, ov, nv) -> System.out.printf("Property changed: %s -> %s%n", ov, nv));
    property.get(); // ensure valid

    property.set(new String("Hello, World!")); // must not use interned String
    property.set("Goodbye, World!");
  }

}

Output:

Property invalidated: Hello, World!
Property invalidated: Goodbye, World!
Property changed: Hello, World! -> Goodbye, World!
like image 129
Slaw Avatar answered Nov 26 '22 18:11

Slaw