Передать / привязать коллекцию к пользовательскому компоненту (расширенному из vbox) в F XML через параметры - PullRequest
0 голосов
/ 02 марта 2020

В моем приложении я объявил пользовательский компонент, подобный этому:

@DefaultProperty("todoItems")
public class TodoItemsVBox extends VBox {
    private ObservableList<TodoItem> todoItems;

    // Setter/Getter omitted
}

и теперь где-то в f xml Я хочу использовать TodoItemsVBox такой компонент:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>

<BorderPane prefHeight="600" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1" 
xmlns:fx="http://javafx.com/fxml/1" 
fx:controller="com.todolist.controller.TodoListController"
        stylesheets="@../css/app.css">
<top>
    <HBox spacing="10.0">
        <TextField fx:id="input" layoutX="35.0" layoutY="64.0" prefWidth="431.0" promptText="Enter todo task" HBox.hgrow="ALWAYS" onAction="#addTask"/>
        <Button layoutX="216.0" layoutY="107.0" mnemonicParsing="false" onAction="#addTask" prefHeight="27.0" prefWidth="70.0" text="Add" HBox.hgrow="ALWAYS" />
        <padding>
            <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
        </padding>
    </HBox>
</top>
<center>
    <ScrollPane fitToHeight="true" fitToWidth="true" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
        <TodoItemsVBox fx:id="todoItemsVBox" todoItems="${todoTasks}"/>
    </ScrollPane>
</center>

... так что, как мы видим, у f xml есть контроллер TodoListController

public class TodoListController implements {
    private final ObservableList<TodoItem> todoTasks = FXCollections.observableArrayList(/*Fill in the collection somehow - for now doesn't matter*/);

    @FXML
    private TodoItemsVBox todoItemsVBox;

    // Setter/Getter omitted
}

Итак, вот что я хочу сделать: передать todoTasks в TodoItemsVBox , определенный в F XML, с помощью такой конструкции: todoItems = "$ {todoTasks}" ---- к сожалению, это не работает так, как я ожидал, потому что f xml файлы загружаются до инициализации контроллеров, поэтому todoTasks всегда null . Я также попробовал @NamedArg с одним конструктором arg в TodoItemsVBox - он даже не работает, за исключением: «Невозможно привязать к нетипизированному объекту». .

Может кто-нибудь предложить решение, как передать коллекцию объектов, определенных в контроллере, в пользовательский компонент через его параметры?

1 Ответ

1 голос
/ 03 марта 2020

В коде есть две проблемы, которые у вас есть:

  1. Для привязки выражений F XML вам необходимо предоставить свойства вашего класса, а не только сами значения. Это относится как к ObservableList s, так и к обычным значениям. Таким образом, ваш TodoItemsVBox класс должен предоставить ListProperty todoItemsProperty()
  2. F XML привязок выражений (т.е. ${todoTasks}), ссылающихся на FXMLLoader s namespace, а не на контроллер. Контроллер автоматически вводится в пространство имен (с ключом "controller"), поэтому, учитывая, что список задач хранится в вашем контроллере (что не обязательно является хорошей идеей), вы можете использовать ${controller.todoTasks} здесь.

Вот минимальная, полная версия вашего приложения, которая работает.

Основа c TodoItem. java:

public class TodoItem {

    private final String name ;
    public TodoItem(String name) {
        this.name = name ;
    }
    public String getName() {
        return name ;
    }
}

A TodoItemsVBox, которая предоставляет список как свойство:

import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;

public class TodoItemsVBox extends VBox {

    private ListProperty<TodoItem> todoItems = new SimpleListProperty<>();

    public TodoItemsVBox() {
        // not efficient but works for demo:
        todoItems.addListener((Change<? extends TodoItem> c) -> rebuildView());
    }

    private void rebuildView() {
        getChildren().clear();
        todoItems.stream()
            .map(TodoItem::getName)
            .map(Label::new)
            .forEach(getChildren()::add);
    }

    public ListProperty<TodoItem> todoItemsProperty() {
        return todoItems ;
    }

    public ObservableList<TodoItem> getTodoItems() {
        return todoItemsProperty().get() ;
    }

}

Простой контроллер:

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;

public class TodoListController  {
    private ObservableList<TodoItem> todoTasks = FXCollections.observableArrayList();

    // not actually needed...
    @FXML
    private TodoItemsVBox todoItemsVBox;

    @FXML
    private TextField input ;


    public ObservableList<TodoItem> getTodoTasks() {
        return todoTasks;
    }


    @FXML
    private void addTask() {
        todoTasks.add(new TodoItem(input.getText()));
    }
}

Файл F XML (TodoList.f xml):

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>

<?import org.jamesd.examples.TodoItemsVBox ?>

<BorderPane prefHeight="600" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1" 
xmlns:fx="http://javafx.com/fxml/1" 
fx:controller="com.todolist.controller.TodoListController"
>

<top>
    <HBox spacing="10.0">
        <TextField fx:id="input" promptText="Enter todo task" HBox.hgrow="ALWAYS" onAction="#addTask"/>
        <Button onAction="#addTask" text="Add" HBox.hgrow="ALWAYS" />
        <padding>
            <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
        </padding>
    </HBox>
</top>
<center>
    <ScrollPane fitToWidth="true" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
        <TodoItemsVBox fx:id="todoItemsVBox" todoItems="${controller.todoTasks}"/>
    </ScrollPane>
</center>
</BorderPane>

И, наконец, класс приложения:

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

public class TodoApp extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("TodoList.fxml"));
        Scene scene = new Scene(loader.load());
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

}

Действительно, контроллеру не место для хранения данных; для этого у вас должен быть отдельный класс модели, который используется совместно с контроллером и представлением. Это достаточно просто сделать здесь; вам просто нужно немного поработать с FXMLLoader (а именно поместить модель в пространство имен и вручную создать и настроить контроллер).

Например:

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

public class TodoModel {

    private ObservableList<TodoItem> todoTasks = FXCollections.observableArrayList();

    public ObservableList<TodoItem> getTodoTasks() {
        return todoTasks;
    }
}

Тогда ваш контроллер становится:

import javafx.fxml.FXML;
import javafx.scene.control.TextField;

public class TodoListController  {

    // not actually needed...
    @FXML
    private TodoItemsVBox todoItemsVBox;

    @FXML
    private TextField input ;

    private TodoModel model ;

    public TodoModel getModel() {
        return model;
    }

    public void setModel(TodoModel model) {
        this.model = model;
    }

    @FXML
    private void addTask() {
        model.getTodoTasks().add(new TodoItem(input.getText()));
    }
}

Измените F XML для использования

<TodoItemsVBox fx:id="todoItemsVBox" todoItems="${model.todoTasks}"/>

И, наконец, соберите приложение с

public void start(Stage primaryStage) throws Exception {

    TodoModel model = new TodoModel();

    FXMLLoader loader = new FXMLLoader(getClass().getResource("TodoList.fxml"));
    loader.getNamespace().put("model", model);
    Scene scene = new Scene(loader.load());

    TodoListController controller = loader.getController();
    controller.setModel(model);

    primaryStage.setScene(scene);
    primaryStage.show();
}

Преимущество этого Подход заключается в том, что ваши данные теперь отделены от пользовательского интерфейса (как вида, так и контроллера), что становится необходимым, если вы хотите получить доступ к тем же данным в другой части пользовательского интерфейса (который будет использовать другой F XML и другой контроллер).

...