如何识别JavaFX属性更改的来源?

时间:2018-02-24 23:06:21

标签: javafx

如何识别JavaFX属性更改的来源,即更改了属性?

-

示例:"选择" CheckBox的状态必须与另一个对象状态同步,而另一个对象不支持Properties或Bindings。显而易见的方法是在CheckBox的selectedProperty上注册ChangeHandler以更新其他对象状态。在相反的方向,另一个通知工具用于调用CheckBox的setSelected()selectedProperty.set()。这导致了一个问题:注册的ChangeHandler不仅在用户单击UI中的CheckBox时被调用,而且在另一个对象更改其状态时被调用。

在后一种情况下,我们当然不希望将更改传播回对象。令人惊讶的是,似乎没有任何东西可以让ChangeHandler决定是由UI控件本身还是从外部更改属性。处理函数的第一个参数仅引用更改的Property / Observabe ,而不是更改它。 oldValuenewValue参数可用于打破无限通知周期,但它们不足以阻止第一个不必要的,可能是有害的通知。

-

上述说明应该足够了,但如果您希望以最小工作示例的形式提出此问题,则以下代码演示了此问题:

"其他对象"一个布尔标志为" state" (现实世界中的数据库):

package sample;

public class SomeDatabaseEntry {

    private boolean someFlag = false;

    public boolean getSomeFlag()
    {
        return someFlag;
    }

    public void setSomeFlag(boolean state)
    {
        someFlag = state;
    }

    public void toggleSomeFlag()
    {
        someFlag = !someFlag;
    }

}

主:

package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

    /*
     * dummy object representing the "other side", e.g. a database
     */
    public final SomeDatabaseEntry _someEntry = new SomeDatabaseEntry();

    private Controller _controller;

    @Override
    public void start(Stage primaryStage) throws Exception{
        FXMLLoader fxmlLoader = new FXMLLoader();
        Parent root = fxmlLoader.load(getClass().getResource("sample.fxml").openStream());

        primaryStage.setTitle("Hello World");
        primaryStage.setScene(new Scene(root, 300, 275));
        primaryStage.show();

        _controller = fxmlLoader.getController();
        _controller.init(_someEntry);

        startSomeDummyDatabaseUpdater();
    }


    /*
     * dummy logic that emulates external changes (e.g. database updates)
     * in the real world there would be a function that is called by the
     * database with a description of the changes that occured.
     * as said: this part is not under my control
     */
    public void startSomeDummyDatabaseUpdater()
    {
        new Thread(() -> {
            while (true)
            {
                _someEntry.toggleSomeFlag();
                _controller.updateUIFromDatabase();

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

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

控制器处理用户输入和其他对象状态(数据库):

package sample;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;

public class Controller {

    private SomeDatabaseEntry _someEntry;

    @FXML
    private CheckBox myCheckBox;

    public void init(SomeDatabaseEntry entry)
    {
        _someEntry = entry;

        myCheckBox.selectedProperty().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
                // If the user toggled the CheckBox in the UI, update the state of the database
                // PROBLEM: this handler is also called in the context of a database update (function below)
                //          in this case the database must not be updated (e. g. because the update is expensive)
                _someEntry.setSomeFlag(newValue);
                System.out.println("Database was updated: " + newValue);
            }
        });
    }

    public void updateUIFromDatabase()
    {
        myCheckBox.selectedProperty().setValue(_someEntry.getSomeFlag());
    }

}

FXML:

<?import javafx.scene.layout.GridPane?>

<?import javafx.scene.control.CheckBox?>
<GridPane fx:id="root" fx:controller="sample.Controller"
          xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
    <CheckBox fx:id="myCheckBox" text="someFlag"></CheckBox>
</GridPane>

1 个答案:

答案 0 :(得分:1)

只需介绍一个额外的财产。通常这将是模型的一部分。

public class UIModel {

    private final BooleanProperty value = new SimpleBooleanProperty();

    public BooleanProperty valueProperty() {
        return value ;
    }

    public final boolean getValue() {
        return valueProperty().get();
    }

    public final void setValue(boolean value) {
        valueProperty().set(value);
    }

    // other properties, etc...
}

现在您创建模型的实例并与感兴趣的各方共享该实例,例如使用依赖注入等

您的控制器应该执行类似

的操作
public class Controller {

    @Inject // or inject it by hand, just imagining DI here for simplicity
    private UIModel model ;

    @Inject
    private DAO dao ;

    @FXML
    private CheckBox myCheckBox ;

    public void initialize() {

        model.valueProperty().addListener((obs, oldValue, newValue) -> 
            myCheckBox.setSelected(newValue));
        myCheckBox.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
            if (model.getValue() != isNowSelected) {
                updateDatabase(isNowSelected);
                model.setValue(isNowSelected);
            }
        });

    }

    private void updateDatabase(boolean value) {
        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() throws Exception {
                dao.update(value);
                return null ;
            }
        };
        task.setOnFailed(e -> { /* handle errors */ });
        new Thread(task).start() ; // IRL hand to executor, etc.
    }
}

现在您的“数据库更新”(我假设它代表已经发生在外部数据库中的更改)看起来像

UIModel model = ... ; // from DI or wherever.
while (! Thread.currentThread().isInterrupted()) {
    Platform.runLater(() -> model.setValue(! model.getValue()));
    try {
        Thread.sleep(2000);
    } catch (InterruptedException exc) {
        Thread.currentThread().interrupt();
    }
}

这里的要点是您更新模型,使其与外部资源保持同步。模型上的监听器更新复选框,因此现在这些复选框是一致的。这会在复选框的选定状态下触发侦听器,但如果模型和复选框已经同步,则该侦听器不会更新数据库,如果由于模型更改而更改了复选框,则会出现这种情况。 / p>

另一方面,如果用户选中复选框,则模型将不会同步,因此侦听器会更新数据库,然后使模型与UI和数据库同步。

在任何实际应用程序中,无论如何都需要定义UI模型(可能不止一个类),因为您希望将UI状态与视图和控制器分开。如何管理模型到后端服务的确切接口可能会有所不同,您可能需要修改与该接口相关的位置,但这种分离应提供执行所需操作的方法,如本示例所示。