JavaFX controller to controller - access to UI Con

2019-07-19 22:39发布

问题:

After research on my MVC javaFX - Problem without any solution for more than 2 days, I decided now to post it:

I've one application Window divided by different SplitPanes and each of this Panes has its own .fxml-form controlled by its own controller.

My Question is: How is it possible to get access to external UI-Controlls? For Example: Clicking on a TableView-row should effect to fill Textfields in an other Form.

My current (not working) Solution is as follows:

1st Controller: Providing an instance

public static SpielerController instance;
    public SpielerController() {};
    public static SpielerController getInstance()
    {   
        if(SpielerController.instance==null)
        {
            synchronized (SpielerController.class)
            {
                if(SpielerController.instance == null)
                {
                    SpielerController.instance = new SpielerController();
                }
            }
        }

        return SpielerController.instance;

    }

2nd Controller: Getting an instance and invoking a Method

SpielerController.getInstance().setPID(Integer.toString(pid));

The result is:

  • it is possible to pass the value pid to the invoked method and print it out (System.out.println(pid);)

  • it is not possible to set a value e.g. TextField1.setText(pid);

Is it possible to set values in this way and when yes - how?

Is there maybe an other (better) way to meet this demand?

回答1:

Don't expose the UI controls or maintain references to other controllers. Instead, expose some observable data in the controllers, and use bindings to bind everything together.

For example, suppose you have a Table.fxml file which displays a TableView<Person> (Person is just the example data class) and has a corresponding controller:

public class TableController {

    @FXML
    private TableView<Person> table ;

    private final ReadOnlyObjectWrapper<Person> selectedPerson = new ReadOnlyObjectWrapper<>();
    public ReadOnlyObjectProperty<Person> selectedPersonProperty() {
        return selectedPerson.getReadOnlyProperty() ;
    }
    public final Person getSelectedPerson() {
        return selectedPersonProperty().get();
    }

    public void initialize() {
        selectedPerson.bind(table.getSelectionModel().selectedItemProperty());
    }
}

And a second FXML (perhaps containing text fields for editing the data associated with the selected item in the table) Editor.fxml with a corresponding EditorController class:

public class EditorController {

    @FXML
    private TextField nameTextField ;

    private ObjectProperty<Person> person = new SimpleObjectProperty<>();
    public ObjectProperty<Person> personProperty() {
        return person ;
    }
    public final Person getPerson() {
        return personProperty().get();
    }
    public final void setPerson(Person person) {
        personProperty().set(person);
    }

    public void initialize() {

        // update text field bindings when person changes:
        personProperty().addListener((obs, oldPerson, newPerson) -> {
            if (oldPerson != null) {
                oldPerson.nameProperty().unbindBidirectional(nameTextField.textProperty());
            }
            if (newPerson != null) {
                newPerson.nameProperty().bindBidirectional(nameTextField.textProperty());
            }
        }
    }
}

Now when you load the FXML files you just need to bind the two exposed properties:

FXMLLoader tableViewLoader = new FXMLLoader(getClass().getResource("Table.fxml"));
Parent tableView = tableViewLoader.load();
TableController tableController = tableViewLoader.getController();

FXMLLoader editorLoader = new FXMLLoader(getClass().getResource("Editor.fxml"));
Parent editorView = editorLoader.load();
EditorController editorController = editorLoader.getController();

// assemble views...

// bind properties from controllers:
editorController.personProperty().bind(tableController.selectedPersonProperty());

There are various ways to manage the details here, e.g. you can use listeners instead of bindings to gain more control as to when values are updated, etc. But the basic idea is to expose the data necessary from the controllers and observe it, instead of tightly coupling the different controllers together.

If there is sufficient data that needs to be shared between two controllers that this gets unwieldy, then you can bundle those data into a Model class and use exactly the same technique to share the model between the controllers. If you are willing to forego setting the controller in the FXML with a fx:controller attribute, you can have the controller accept a Model reference in its constructor; for example

public class TableController {
    @FXML
    private TableView<Person> table ;
    private Model model ;
    public TableController(Model model) {
        this.model = model ;
    }

    public void initialize() {
        table.getSelectionModel().selectedItemProperty().addListener((obs, oldPerson, newPerson) 
            -> model.setSelectedPerson(newPerson));
    }
}

and the editor just observes the property in the model:

public class EditorController {
    private Model model ;
    @FXML
    private TextField nameTextField ;

    public EditorController(Model model) {
        this.model = model ;
    }

    public void initialize() {
        model.selectedPersonProperty().addListener((obs, oldPerson, newPerson) 
            -> nameTextField.setText(newPerson.getName()));
    }
}

Then the assembly code looks like

Model model = new Model();
TableController tableController = new TableController(model);
FXMLLoader tableLoader = new FXMLLoader(getClass().getResource("Table.fxml"));
tableLoader.setController(tableController);
Parent tableView = tableLoader.load();

EditorController editorController = new EditorController(model);
FXMLLoader editorLoader = new FXMLLoader(getClass().getResource("Editor.fxml"));
editorLoader.setController(editorController);
Parent editorView = editorLoader.load();

// assemble views...

In this version, the FXML files cannot have fx:controller attributes.

Another variant can set the controller factory on the FXMLLoader, so that it loads classes defined by the fx:controller attribute, but you control how it loads them (i.e. it can pass a model to the constructor).

Finally, you might consider a dedicated dependency injection framework for injecting a model into the controllers. afterburner.fx is excellent for this.



回答2:

There are many different variants of the MVC pattern. For the one I use, the view reflects the model. The controller is the thing in the middle that fascilitates this. Therefore, controllers should not now about each other or influence each other directly.

Say string1 is what's in the TextField in form2. It can change depending on what the user does. Therefore it is my belief its value should be stored in the model layer. Form2 can listen for change and update its TextField accordingly. When the user clicks the TableView-Row in form1, the controller for form1 updates string1 in the model layer, (which it does have access to). Form2 then does the rest.

Form1 now doesn't know anything about the structure of form2. It just knows about String1. This reflects the ideals of MVC much better.

If you need a code example, please let me know and I'll whip one up for you. EDIT: I added a code example here. Note that this probably can be improved upon still and is by no means the one final definitive way to do it.

public class JavaFXApplication23 extends Application {

    @Override
    public void start(Stage stage1) throws IOException {
        final SomeDataObject data = new SomeDataObject();
        final Stage stage2 = new Stage();

        Parent form1 = load(data, "FXMLDocument_1.fxml");
        Parent form2 = load(data, "FXMLDocument_2.fxml");

        Scene scene1 = new Scene(form1);
        Scene scene2 = new Scene(form2);

        stage1.setScene(scene1);
        stage1.show();

        stage2.setScene(scene2);
        stage2.show();
    }

    private Parent load(SomeDataObject data, String resource) throws IOException {
        final FXMLLoader loader = new FXMLLoader(getClass().getResource(resource));
        final Parent parent = loader.load();
        final Controller controller = loader.getController();
        controller.setData(data);
        return parent;
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

}

public class SomeDataObject {

    private final StringProperty stringProp = new SimpleStringProperty("");

    public StringProperty getStringProp() {
        return stringProp;
    }

}

public interface Controller {

    void setData(SomeDataObject data);

}

public class Form1Controller implements Controller {

    private SomeDataObject data;

    @FXML
    private void handleButtonAction(ActionEvent event) {
        data.getStringProp().set(data.getStringProp().get() + "Merry Christmas!\n");
    }

    @Override
    public void setData(SomeDataObject data) {
        this.data = data;
    }

}

public class Form2Controller implements Controller {

    @FXML
    private Label label;

    private SomeDataObject data;

    @Override
    public void setData(SomeDataObject data) {
        this.data = data;
        label.setText(data.getStringProp().get());
        data.getStringProp()
                .addListener((ObservableValue<? extends String> observable, 
                        String oldValue, String newValue) -> {
            label.setText(newValue);
        });
    }

}