【问题标题】:JavaFX TableView doesn't update outside of initialize methodJavaFX TableView 不会在初始化方法之外更新
【发布时间】:2015-08-19 15:26:12
【问题描述】:

我有一个应用程序(就这个问题而言)有 3 个组件。

  1. 包含 TableView 的主视图(带有控制器)
  2. 此视图的表项 (TableMessage) 的模型
  3. 线程侦听器侦听要添加到表中的消息

当项目添加到连接的 ObservableList 时,我遇到了 TableView 不更新的问题。如果我在控制器的 initialize 方法中添加示例数据,则数据显示正常。但是,当我从程序的其他地方(本例中的侦听器)调用相同的方法来添加时,TableView 不会更新。在调试时,我可以看到数据被添加到连接列表中(并且样本数据在那里,所以我知道它是正确的对象)。

控制器:

@FXML
private TableView<TableMessage> messageTable;
@FXML
private TableColumn<TableMessage, String> messageIDColumn;
@FXML
private TableColumn<TableMessage, String> timestampColumn;
@FXML
private TableColumn<TableMessage, String> reportTypeColumn;
@FXML
private TableColumn<TableMessage, String> tNumberColumn;

private ObservableList<TableMessage> tableContent = FXCollections.observableArrayList();

@FXML
public void initialize() {

    linkColumns();

    // this works
    addRow(new TableMessage("001", "today", "1", "10"));

}

private void linkColumns() {
    messageIDColumn.setCellValueFactory(new PropertyValueFactory<TableMessage, String>("messageID"));
    timestampColumn.setCellValueFactory(new PropertyValueFactory<TableMessage, String>("timestamp"));
    reportTypeColumn.setCellValueFactory(new PropertyValueFactory<TableMessage, String>("reportType"));
    tNumberColumn.setCellValueFactory(new PropertyValueFactory<TableMessage, String>("tNumber"));
    messageTable.setItems(tableContent);
}

public void addRow(TableMessage row) {
    tableContent.add(row);
}

型号:

public class TableMessage{
    private SimpleStringProperty messageID = new SimpleStringProperty ("");
    private SimpleStringProperty timestamp = new SimpleStringProperty ("");
    private SimpleStringProperty reportType = new SimpleStringProperty ("");
    private SimpleStringProperty tNumber = new SimpleStringProperty ("");

// all my constructors, getters, setters below
...
}

听众:

// same sample code as before, doesn't work here (reference to myController is set separately)
myController.addRow(new TableMessage("001", "today", "1", "10"));

我不明白为什么 TableView 在初始化后停止观看。如前所述,我确认正在更新正确的 tableContent 引用。

谢谢

编辑 1:

根据下面的问题,我上面的视图的父级(我们称之为 MainController)通过以下方式获取对上述控制器的引用:

FXMLLoader loader = new FXMLLoader(getClass().getResource("MessageView.fxml"));
        loader.load();
        messageViewController= (MessageViewController) loader.getController();

然后将SpringContext(用于侦听器)向下传递到messageViewController,这会从SpringContext 创建侦听器。

然后给听众参考我的messageViewController我打电话

myListener.setReferenceToController(this);  

看起来像这样

public void setReferenceToController(MessageController ref) {
    this.messageController = ref;
}

作为一个侧面的想法,如果在对控制器的错误引用中出现错误,为什么我在跟踪时会在可观察列表中看到示例数据(回想一下它是在 initialize 中调用的)听者的来电?

【问题讨论】:

  • 1.你能展示你是如何将控制器的引用放入 Listener 类的吗?您确定它与加载 FXML 时为您创建的控制器实例相同吗? 2.你在FX应用线程上调用addRow(...)吗?
  • 幽默我。 (您发布的代码没有任何问题,因此错误在其他地方。获得正确的引用是最明显的候选者。)将System.out.println("In initialize: "+this); 放入控制器的initialize() 方法中,将System.out.println("In Listener: "+myController); 放入@ 987654338@(就在您显示的代码行之前);验证变量的输出是否相同。
  • 以上更新。此外,幽默被授予。 Initialize 在应用程序启动时被调用两次(因此创建了两个控制器)。侦听器引用的控制器是创建的第二个控制器。我愿意打赌这就是我的问题所在。我在调试时看到 ArrayList 中的示例数据的原因是它仍然被初始化调用,但它不是活动视图。
  • 在您的编辑中,您调用loader.load(),但您放弃了结果。其结果是包含 TableView 的 UI 节点。由于您使用的控制器连接到未显示的TableView,因此当您修改TableView 时看不到任何结果也就不足为奇了。相反,由于您清楚地看到了TableView,因此您必须在其他地方使用相同的 FXML 调用 load(...) 并显示结果。您需要对由 that 调用创建的控制器的引用 load()(而不是对它的其他任意调用)。
  • 默认情况下,它总是生成一个新的控制器。有一些可能的解决方法:我真的没有足够的信息来知道哪种方法适合您的情况。最好的选择可能是您可以将fx:included FXML 文件的控制器注入到“包含”FXML 文件的控制器中。将fx:id 添加到fx:include,然后在注入时将"Controller" 附加到该ID。见here

标签: java javafx


【解决方案1】:

FXMLLoader 在 FXML 文件的根元素中遇到 fx:controller 属性时的默认行为是通过调用其 no-参数构造函数,并将其用作该 FXML 定义的视图的控制器。

所以当你通过代码获得对控制器的引用时

FXMLLoader loader = new FXMLLoader(getClass().getResource("MessageView.fxml"));
loader.load();
messageViewController= (MessageViewController) loader.getController();

FXMLLoader 创建MessageViewController 的新实例并将其与MessageView.fxml 定义的视图的新实例相关联。由于您丢弃了该视图(您不对返回值 loader.load() 执行任何操作),因此您引用的控制器与未显示的视图相关联。

(请注意,FXMLLoader 仍会在该控制器实例上调用initialize(...),因此initialize() 方法的任何效果都将在您获得的引用中可见。)

在您的 cmets 中,您实际显示的视图是通过在另一个 FXML 文件中包含 MessageView.fxml 来创建的。当FXMLLoader 使用Nested Controllers 技术加载包含的FXML 文件时,可以注入对创建的控制器的引用。简而言之,将fx:id 添加到&lt;fx:include&gt; 元素。通过将"Controller" 附加到@FXML 注释字段的名称中的fx:id 属性的值,可以将包含文件中的控制器从包括 FXML 文件注入到控制器中。例如:

MainView.fxml:

<!-- xml headers and imports etc -->
<BorderPane fx:controller="com.example.MainController" ... >

    <!-- ... -->

    <fx:include source="MessageView.fxml" fx:id="messageView"/>

    <!-- ... -->

</BorderPane>

MainController.java:

public class MainController {

    @FXML
    private MessageViewController messageViewController ;

    public void initialize() {
        // messageViewController will be initialized and be a reference to the controller
        // for the included messageView

        // ...
    }
}

这对于您的用例可能已经足够了。

还有其他几种方法可以修改创建控制器的默认机制。最直接的(在&lt;fx:include&gt;s 的情况下实际上没有帮助)是从FXML 文件中删除fx:controller 属性并直接在FXMLLoader 上设置控制器:

FXMLLoader loader = new FXMLLoader(getClass().getResource("MessageView.fxml"));
MessageViewController myController = new MessageViewController();
loader.setController(myController);
// calling load will now inject @FXML-annotated fields and call initialize() on myController
Parent view = loader.load();

这个的主要用例是使用需要将参数传递给构造函数的控制器。您可以使用这种技术重复使用单个控制器实例来多次加载 FXML 文件:我不建议这样做,因为如果您真的希望视图的两个实例处于活动状态,那么很快就会出错。

请注意,如果您设置了一个控制器,然后加载一个设置了fx:controller 属性的FXML 文件,则会出现运行时异常并且加载会失败。

另一种机制是在加载器上设置controllerFactory。控制器工厂本质上是一个将Class&lt;?&gt; 映射到控制器实例(可能属于该类,但没有强制执行)的函数。这里要注意的一个重要特征是controllerFactory 向下传播到&lt;fx:include&gt;s;换句话说,当加载 FXML 并包含 &lt;fx:include&gt; 标签时,加载包含的 FXML 时使用的控制器工厂与加载周围 FXML 时使用的控制器工厂相同。

我经常使用控制器工厂来实例化具有共享模型实例的控制器。 IE。给定一个模型类:

public class Model { 
    private ObservableList<TableMessage> messages = FXCollections.observableArrayList();

    public ObservableList<TableMessage> getMessages() {
        return messages ;
    }
}

我愿意

Model model = new Model() ;
Callback<Class<?>, Object> controllerFactory = clazz -> {
    try {
        // see if controller class has a constructor taking a Model:
        for (Constructor<?> constructor : class.getConstructors()) {
            if (constructor.getParameterCount() == 1 
               && constructor.getParameterTypes()[0] == Model.class) {
                return constructor.newInstance(model);
            }
        }
        // no suitable constructor, just invoke no-arg constructor:
        return clazz.newInstance();
    } catch (RuntimeException exc) {
        throw exc ;
    } catch (Exception exc) {
        throw new RuntimeException(exc);
    }
};
FXMLLoader loader = new FXMLLoader(...);
loader.setControllerFactory(controllerFactory);
Parent mainView = loader.load();

共享模型实例通常可以避免传递控制器引用的任何需要,因为控制器可以只更新共享数据模型:

public class MainController {
    private final Model model ;

    @FXML
    private TableView<TableMessage> messageTable ;

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

    public void initialize() {
        messageTable.setItems(model.getMessages());
        // ...
    }
}

public class MessageViewController {
    private final Model model ;

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

    @FXML
    public void addMessage() {
        model.getMessages().add(...);
    }
}

(与您的应用程序的结构不同,但您明白了)。

控制器工厂机制非常强大。例如,afterburner.fx 是一个非常轻量级的框架,它使用控制器工厂允许在 FX 控制器类中使用 @Inject,因此您可以只注入共享模型实例。

既然你提到你使用 Spring,你可以考虑将你的控制器定义为 Spring 管理的 bean。然后就可以了

ApplicationContext applicationContext = ... ;
FXMLLoader loader = new FXMLLoader(...);
loader.setControllerFactory(applicationContent::getBean);
Parent view = loader.load();

现在FXMLLoader 将通过调用applicationContext.getBean(Class&lt;?&gt;) 获取控制器实例,传递fx:controller 属性指定的类。这样,您可以使用弹簧注入将模型实例(或您需要的任何东西)注入控制器。您可以在 fx:controller 属性中使用接口名称,并让您的 spring 配置选择接口的实现。由于上面提到的原因,强烈建议给控制器 bean prototype 范围(尽管注入的模型 bean 可能是 singleton 范围)。只是一些想法......

【讨论】:

    猜你喜欢
    • 2017-04-10
    • 2015-07-20
    • 2015-04-11
    • 2016-12-08
    • 2013-02-24
    • 1970-01-01
    • 2016-12-22
    • 1970-01-01
    相关资源
    最近更新 更多