web-dev-qa-db-ja.com

JavaFxでMVCを適用する

GUIワールド/ OOデザインパターンは初めてで、GUIアプリケーションにMVCパターンを使用します。MVCパターンに関する小さなチュートリアルを読みました。モデルにはデータが含まれ、ビューには視覚要素が含まれ、コントローラーは、ビューとモデルを結び付けます。

ListViewノードを含むViewがあり、ListViewにはPersonクラス(モデル)からの名前が入力されます。しかし、私は一つのことについて少し混乱しています。

私が知りたいのは、ファイルからデータをロードすることがコントローラーまたはモデルの責任であるかどうかです??そして、名前のObservableList:コントローラーまたはモデルに保存する必要がありますか?

25
karim

このパターンにはさまざまなバリエーションがあります。特に、Webアプリケーションのコンテキストでの「MVC」は、シッククライアント(デスクトップなど)アプリケーションのコンテキストでの「MVC」とは多少異なって解釈されます(Webアプリケーションは、要求と応答のサイクルの上に置かれる必要があるため)。これは、JavaFXを使用してシッククライアントアプリケーションのコンテキストでMVCを実装するための1つのアプローチにすぎません。

Personクラスは、非常に単純なアプリケーションがない限り、実際にはモデルではありません。これは通常、ドメインオブジェクトと呼ばれるものであり、モデルには他のデータとともにその参照が含まれます。 ListViewについてjust考えているような狭いコンテキストでは、Personをデータモデル(it ListView)の各要素のデータをモデル化しますが、アプリケーションのより広いコンテキストでは、考慮するデータと状態が多くあります。

ListView<Person>を表示している場合、最低限必要なデータはObservableList<Person>です。リストで選択されたアイテムを表すcurrentPersonなどのプロパティも必要になる場合があります。

持っているonlyビューがListViewである場合、これを保存するための別のクラスを作成するのはやり過ぎですが、実際のアプリケーションは通常複数のビューになります。この時点で、モデルでデータを共有することは、異なるコントローラーが互いに通信するための非常に便利な方法になります。

したがって、たとえば、次のようなものがあります。

public class DataModel {

    private final ObservableList<Person> personList = FXCollections.observableArrayList();

    private final ObjectProperty<Person> currentPerson = new SimpleObjectPropery<>(null);

    public ObjectProperty<Person> currentPersonProperty() {
        return currentPerson ;
    }

    public final Person getCurrentPerson() {
        return currentPerson().get();
    }

    public final void setCurrentPerson(Person person) {
        currentPerson().set(person);
    }

    public ObservableList<Person> getPersonList() {
        return personList ;
    }
}

これで、ListViewディスプレイ用のコントローラーが次のようになります。

public class ListController {

    @FXML
    private ListView<Person> listView ;

    private DataModel model ;

    public void initModel(DataModel model) {
        // ensure model is only set once:
        if (this.model != null) {
            throw new IllegalStateException("Model can only be initialized once");
        }

        this.model = model ;
        listView.setItems(model.getPersonList());

        listView.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> 
            model.setCurrentPerson(newSelection));

        model.currentPersonProperty().addListener((obs, oldPerson, newPerson) -> {
            if (newPerson == null) {
                listView.getSelectionModel().clearSelection();
            } else {
                listView.getSelectionModel().select(newPerson);
            }
        });
    }
}

このコントローラーは、基本的にリストに表示されているデータをモデルのデータにバインドし、モデルのcurrentPersonが常にリストビューで選択されたアイテムであることを保証します。

これで、人のfirstNamelastName、およびemailプロパティ用の3つのテキストフィールドがある別のビュー、たとえばエディタが表示される場合があります。コントローラは次のようになります。

public class EditorController {

    @FXML
    private TextField firstNameField ;
    @FXML
    private TextField lastNameField ;
    @FXML
    private TextField emailField ;

    private DataModel model ;

    public void initModel(DataModel model) {
        if (this.model != null) {
            throw new IllegalStateException("Model can only be initialized once");
        }
        this.model = model ;
        model.currentPersonProperty().addListener((obs, oldPerson, newPerson) -> {
            if (oldPerson != null) {
                firstNameField.textProperty().unbindBidirectional(oldPerson.firstNameProperty());
                lastNameField.textProperty().unbindBidirectional(oldPerson.lastNameProperty());
                emailField.textProperty().unbindBidirectional(oldPerson.emailProperty());
            }
            if (newPerson == null) {
                firstNameField.setText("");
                lastNameField.setText("");
                emailField.setText("");
            } else {
                firstNameField.textProperty().bindBidirectional(newPerson.firstNameProperty());
                lastNameField.textProperty().bindBidirectional(newPerson.lastNameProperty());
                emailField.textProperty().bindBidirectional(newPerson.emailProperty());
            }
        });
    }
}

これらのコントローラーが同じモデルを共有するように設定すると、エディターはリストで現在選択されているアイテムを編集します。

データのロードと保存は、モデルを介して実行する必要があります。これを、モデルが参照する別のクラスに含めることもあります(たとえば、ファイルベースのデータローダーとデータベースデータローダー、またはWebサービスにアクセスする実装を簡単に切り替えることができます)。簡単なケースでは、あなたがするかもしれません

public class DataModel {

    // other code as before...

    public void loadData(File file) throws IOException {

        // load data from file and store in personList...

    }

    public void saveData(File file) throws IOException {

        // save contents of personList to file ...
    }
}

次に、この機能へのアクセスを提供するコントローラーがある場合があります。

public class MenuController {

    private DataModel model ;

    @FXML
    private MenuBar menuBar ;

    public void initModel(DataModel model) {
        if (this.model != null) {
            throw new IllegalStateException("Model can only be initialized once");
        }
        this.model = model ;
    }

    @FXML
    public void load() {
        FileChooser chooser = new FileChooser();
        File file = chooser.showOpenDialog(menuBar.getScene().getWindow());
        if (file != null) {
            try {
                model.loadData(file);
            } catch (IOException exc) {
                // handle exception...
            }
        }
    }

    @FXML
    public void save() {

        // similar to load...

    }
}

これで、アプリケーションを簡単に組み立てることができます。

public class ContactApp extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {

        BorderPane root = new BorderPane();
        FXMLLoader listLoader = new FXMLLoader(getClass().getResource("list.fxml"));
        root.setCenter(listLoader.load());
        ListController listController = listLoader.getController();

        FXMLLoader editorLoader = new FXMLLoader(getClass().getResource("editor.fxml"));
        root.setRight(editorLoader.load());
        EditorController editorController = editorLoader.getController();

        FXMLLoader menuLoader = new FXMLLoader(getClass().getResource("menu.fxml"));
        root.setTop(menuLoader.load());
        MenuController menuController = menuLoader.getController();

        DataModel model = new DataModel();
        listController.initModel(model);
        editorController.initModel(model);
        menuController.initModel(model);

        Scene scene = new Scene(root, 800, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

先ほど言ったように、このパターンには多くのバリエーションがあります(そして、これはおそらくモデルビュープレゼンター、または「パッシブビュー」バリエーションです)。コンストラクターを介してコントローラーにモデルを提供する方が少し自然ですが、fx:controller属性を使用してコントローラークラスを定義するのは非常に困難です。このパターンは、依存性注入フレームワークにも強く役立ちます。

Update:この例の完全なコードは here です。

67
James_D

私が知りたいのは、ファイルからデータをロードするのがコントローラーまたはモデルの責任である場合ですか?

私にとって、このモデルは、アプリケーションのビジネスロジックを表す必要なデータ構造をもたらすことのみを担当しています。

任意のソースからそのデータをロードするアクションは、コントローラーレイヤーで実行する必要があります。 リポジトリパターン を使用することもできます。これは、ビューからデータにアクセスするときにソースのタイプから抽象化するのに役立ちます。これを実装すると、リポジトリ実装がファイル、sql、nosql、webserviceからデータをロードしているかどうか気にする必要はありません...

そして、名前のObservableListはコントローラーまたはモデルに保存されますか?

私にとって、ObservableListはビューの一部です。これは、javafxコントロールにバインドできるデータ構造の一種です。そのため、たとえば、ObservableListにはモデルの文字列を入力できますが、ObservableList参照はViewのクラスの属性でなければなりません。 Javafxでは、モデルからのドメインオブジェクトによって裏付けられたObservable Propertiesにjavafxコントロールをバインドすることは非常に喜ばしいことです。

viewmodel concept をご覧になることもできます。私にとって、POJOに裏打ちされたJavaFx Beanは、ビューモデルと見なすことができ、ビューに表示する準備ができたモデルオブジェクトとして見ることができます。したがって、たとえば、ビューに2つのモデル属性から計算された合計値を表示する必要がある場合、この合計値はビューモデルの属性になります。この属性は永続化されず、ビューを表示するたびに計算されます。

2
alvaro