Почему слушатель изменений TableView дает разные результаты для столбцов ObjectProperty <T>и TProperty в JavaFX8? - PullRequest
0 голосов
/ 19 сентября 2018

Относительный вопрос новичка в Java.

У меня есть TableView с экстракторами и ListChangeListener, добавленный в базовый ObservableList.

Если у меня есть столбец StringProperty в модели данныхпрослушиватель изменений не обнаруживает изменения, если дважды щелкнуть ячейку и нажать клавишу ВВОД, не внося никаких изменений.Это хорошо.

Однако, если я определяю столбец как ObjectProperty<String>, дважды щелкаю, а затем нажимаю клавишу ВВОД, прослушиватель изменений всегда обнаруживает изменения, даже если ничего не было сделано .

Почему это происходит?В чем разница между ObjectProperty<String> и StringProperty с точки зрения слушателя изменений?

Я прочитал Разница между SimpleStringProperty и StringProperty и JavaFX SimpleObjectProperty противSimpleTProperty и, думаю, я понимаю разницу.Но я не понимаю, почему слушатель изменений дает разные результаты для TProperty / SimpleTProperty и ObjectProperty<T>.

Если это поможет, вот MVCE для моего несколько бессмысленного случая.Я на самом деле пытаюсь заставить слушателя изменений работать для столбцов BigDecimal и LocalDate и застрял на нем в течение 5 дней.Если я пойму, почему слушатель изменений дает разные результаты, я смогу заставить мой код работать.

Я использую JavaFX8 (JDK1.8.0_181), NetBeans 8.2 и 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);
    }

}

1 Ответ

0 голосов
/ 19 сентября 2018

Разницу можно увидеть, посмотрев на исходный код StringPropertyBase и ObjectPropertyBase - в частности, их set методов.

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();
    }
}

Обратите внимание на разницу в том, как они проверяют, равно ли новое значение старому значению?Класс StringPropertyBase проверяет, используя Object.equals, тогда как класс ObjectPropertyBase использует равенство ссылок (== / !=).

Я не могу точно ответить , почему эта разница существует, но я могу рискнуть предположить: ObjectProperty может содержать что угодно , и, следовательно, есть потенциал дляObject.equals быть дорогим;например, при использовании List или Set.При кодировании StringPropertyBase я полагаю, что они решили, что потенциал отсутствует, что семантика String равенства важнее, или и то, и другое.Может быть больше / больше причин, почему они сделали то, что сделали, но, поскольку я не участвовал в разработке, я не знаю о них.


Интересно, если вы посмотрите, как они обращаются со слушателями(com.sun.javafx.binding.ExpressionHelper) вы увидите, что они проверяют равенство, используя Object.equals.Эта проверка на равенство выполняется только в том случае, если в настоящее время зарегистрировано ChangeListener с, вероятно, для поддержки отложенной оценки при отсутствии ChangeListener с.

Если новые и старые значения равны equals, то ChangeListener с не уведомляются.Однако это не мешает уведомлять InvalidationListener s .Таким образом, ваш ObservableList сгенерирует изменение обновления, потому что этот механизм основан на InvalidationListener с, а не ChangeListener с.

Вот соответствующий исходный код:

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;
    }
}

И вы можете увидеть это поведение в следующем коде:

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!");
  }

}

Вывод:

Property invalidated: Hello, World!
Property invalidated: Goodbye, World!
Property changed: Hello, World! -> Goodbye, World!
...