【问题标题】:JavaFX getUserData() leads to NullPointerExceptionJavaFX getUserData() 导致 NullPointerException
【发布时间】:2017-02-28 22:37:48
【问题描述】:

我目前正在研究将我的 SQL 连接保存在我的 JavaFX 项目中的最简单方法,以便在我的所有控制器中使用它。 由于我在 SideBar FXML 文件中创建控制器,因此无法将对象从一个控制器传递到另一个控制器。

因此,我想使用 Node.setUserData() 方法并将连接对象保存到根节点。不幸的是,当我想调用它时,我得到了 NullPointers。

保存效果很好:

myStage.getScene().getRoot().setUserData(con);

从同一个阶段变量调用它也可以正常工作:

... = (Connection) myStage.getScene().getRoot().getUserData();

但我正在通过我的 Sidebar.fxml 访问舞台

Stage stage = (Stage) myButton.getScene().getWindow();

在通过访问 UserData 时导致 NullPointers 的原因

stage.getScene().getRoot().getUserData();

我知道这是因为它不是“完全相同”的阶段变量。但它必须是同一个阶段(当我在那里显示一个新视图时,它会显示在与以前相同的阶段)。

如何找到与我之前保存过的 UserData 完全相同的节点?或者有没有办法从我没有舞台的另一个上下文访问同一个节点?

编辑:我在这里放了一个 MCVE 来显示我的问题是什么:https://github.com/lud-hu/myJavaFxMcve/ 编辑:代码现在在 Github 中运行,我将在此处发布带有初始问题的代码:

MyMcveStarter.java

    package myMcve;

import myMcve.controller.LoginController;
import javafx.application.Application;
import javafx.stage.Stage;

public class MyMcveStarter extends Application {

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

    @Override
    public void start(Stage primaryStage) throws Exception {
        LoginController controller = new LoginController(primaryStage);
        controller.displaySceneOn(primaryStage);
    }
}

LoginController.java

    package myMcve.controller;

import myMcve.view.LoginSceneView;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;


public class LoginController {

    private LoginSceneView view;
    private Parent scene;
    Stage myStage;

    String defaultUrl;
    String defaultName;
    String defaultPassword;



    public LoginController(Stage stage) {

            defaultUrl = "jdbc:mysql://localhost:3306/db";
            defaultName = "root";
            defaultPassword = "localhost";

        myStage = stage;

        FXMLLoader loader = new FXMLLoader();
        loader.setLocation(getClass().getResource("../view/LoginScene.fxml"));
        try {
            scene = loader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }
        view = loader.getController();
    }

    public void displaySceneOn(Stage stage) {
        stage.setTitle("login");
        Scene myScene = new Scene(scene, 1250, 650);

        stage.setScene(myScene);
        stage.show();

        try {
            initializeDbConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private void initializeDbConnection() throws SQLException {

            try {
                DriverManager.setLoginTimeout(15);
                Connection con = DriverManager.getConnection(defaultUrl, defaultName, defaultPassword);

                UserManagementController controller = new UserManagementController(myStage, con);
                controller.displaySceneOn(myStage);

            } catch (Exception e) {
            }
        }


}

SideBarController.java

package myMcve.controller;

import myMcve.controller.LevelManagementController;
import myMcve.controller.LoginController;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.stage.Stage;

public class SideBarController{
    @FXML
    private Button levelManBtn;

    public Button getLevelManBtn() {
        return levelManBtn;
    }


    @FXML
    private void levelMan(ActionEvent event){
        //start other Controller from here (SideBar)
        //how do I access the DB Connection here?
        Stage stage = (Stage) levelManBtn.getScene().getWindow();
        //LevelManagementController controller = new LevelManagementController(stage, con);
        //controller.displaySceneOn(stage);
    }


}

UserManagementController.java

package myMcve.controller;

import com.sun.prism.impl.Disposer;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.stage.Stage;
import javafx.util.Callback;
import myMcve.view.UserManagementView;

import java.io.IOException;
import java.sql.*;

public class UserManagementController{

    private UserManagementView view;
    private Parent scene;
    Stage myStage;
    Connection con;

    public UserManagementController(Stage stage, Connection con){

        myStage = stage;
        this.con = con;

        FXMLLoader loader = new FXMLLoader();
        loader.setLocation(getClass().getResource("../view/UserManagementScene.fxml"));

        try {
            scene = loader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }
        view = loader.getController();

    }

    public void displaySceneOn(Stage stage){
        stage.setTitle("user management");
        Scene myScene = new Scene(scene, 1250, 650);

        stage.setScene(myScene);

        stage.show();
    }

}

LoginSceneView.java

package myMcve.view;

import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

public class LoginSceneView {
    @FXML
    private Label label;

    public Label getLabel() {
        return label;
    }

}

LoginScene.fxml

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

<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.AnchorPane?>

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="600.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.112-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="myMcve.view.LoginSceneView">

    <Label fx:id="label" text="login Buttons etc..." />

</AnchorPane>

SideBar.fxml

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

<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.VBox?>

<VBox fx:id="sidebar" prefHeight="650.0" prefWidth="250.0" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1" fx:controller="myMcve.controller.SideBarController">
    <children>
        <Button fx:id="levelManBtn" layoutX="10.0" layoutY="123.0" prefHeight="50.0" prefWidth="350.0" text="Level Management" onAction="#levelMan"/>
    </children>
</VBox>

UserManagementView.java

package myMcve.view;

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

public class UserManagementView {

    @FXML
    private Label label;

    public Label getLabel() {
        return label;
    }
}

UserManagementScene.fxml

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

<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>


<BorderPane xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1" fx:controller="myMcve.view.UserManagementView">
    <left>
        <!-- SideBar import -->
        <fx:include fx:id="sidebar" source="SideBar.fxml" />
    </left>
    <center>
        <Label fx:id="label" text="user managemnt tableview and Buttons etc..." />
    </center>
</BorderPane>

【问题讨论】:

  • "无法将对象从一个控制器传递到另一个控制器。"为什么不呢?
  • 另外,如果你真的想通过将其设置为根节点上的用户数据来做到这一点,那应该可以工作(尽管从设计角度来看,它非常糟糕)。什么实际上是空的?为什么它是空的?
  • 使用 userData 似乎不是推荐的方法。相关:Passing Parameters JavaFX FXMLWhat is the main way to connect a view and a model in JavaFX?
  • @James_D 这是不可能的,因为我必须从无法将连接传递到的侧边栏中实例化新的控制器。至少我试了好几个小时还是没用。
  • @James_D 当我从上面提到的侧边栏控制器调用时,我从 getUserData() 获得的对象为空。我知道它应该工作,但不幸的是我没有。 :(

标签: javafx


【解决方案1】:

我尝试使用与您正在使用的设计类似的设计,其中您通过使用 FXML/JavaFX 控制器对作为 MVC 视图来实现传统的 MVC 架构,并创建一个单独的 MVC 控制器。最后我认为它太复杂了。

FXML 控制器设计中隐含的架构实际上是一种称为 MVP(“Model-View-Presenter”)的 MVC 变体,如果您阅读有关 UI 架构的 Martin Fowler article,它接近于他称之为“被动”的变体看法”。在这种架构中,FXML 文件代表视图,它本质上是完全被动的,只是定义了布局。 JavaFX Controller 代表“Presenter”,它观察和更新模型,并通过修改视图来响应模型中的变化。我的主要建议是重构您的设计,使其符合这一点:因此完全删除“控制器”层之一,并考虑“JavaFX 控制器”在 MVC 变体之一中履行控制器/演示者的一般角色。 (我对此here 发布了更完整的答案。)

当您使用&lt;fx:include&gt; 时,您的设计尤其困难。问题是您的设计是“以控制器为中心的”,从某种意义上说,您喜欢创建控制器并让控制器创建视图。使用&lt;fx:include&gt; 基本上会为您创建一个新视图(通过加载另一个 FXML 文件),然后为您创建控制器。所以这更“以视图为中心”。这两者之间的冲突使得在控制器之间共享模型(或服务)变得很棘手。

实现此功能的一种方法是在FXMLLoader 上设置控制器工厂。控制器工厂是将类(由 FXML 文件中的 fx:controller 属性定义)映射到 JavaFX 控制器实例的函数。因此,您可以使用该工厂创建控制器,并在控制器中初始化连接。 (顺便说一句,连接在您的应用程序中扮演着 MVC 模型的角色:您可能希望在稍后阶段将其重构为更健壮的东西。您至少应该将所有特定于数据库的代码分解为数据访问对象,并且共享该对象而不是原始连接。)

首先,在SideBarController 中定义一个接受连接的构造函数:

public class SideBarController{
    @FXML
    private Button levelManBtn;

    private final Connection con ;

    public SideBarController(Connection con) {
        this.con = con ;
    }

    // existing code (which obviously can now access the connection)...

}

现在,当您加载 UserManagementView 时,指定一个控制器工厂,该工厂调用构造函数并采用 Connection(如果存在),否则调用无参数构造函数。此处显示的方法使用反射,并且基本上类似于依赖注入(这实际上是您在此处尝试执行的操作)框架的实现方式。还有其他可能性。

public UserManagementController(Stage stage, Connection con){

    myStage = stage;
    this.con = con;

    FXMLLoader loader = new FXMLLoader();

    // note that this resource name will likely not work if you bundle the app as a jar....        
    loader.setLocation(getClass().getResource("../view/UserManagementScene.fxml"));

    loader.setControllerFactory((Class<?> controllerType) -> {

        try {
            // check constructors of controllerType to see if one takes a Connection:
            for (Constructor<?> c : controllerType.getConstructors()) {
                if (c.getParameterCount() == 1 && c.getParameterTypes()[0].equals(Connection.class)) {
                    // found matching constructor, invoke it with the connection as parameter:
                    return c.newInstance(con);
                }
            }

            // no matching constructor, just invoke default constructor:
            return controllerType.newInstance();
        } catch (Exception e) {
            // fatal...
            throw new RuntimeException(e);
        }
    });

    try {
        scene = loader.load();
    } catch (IOException e) {
        e.printStackTrace();
    }
    view = loader.getController();

}

控制器工厂被传播到&lt;fx:include&gt;d FXML 文件的加载过程。所以当UserManagementScene.fxml被加载时,指定的控制器是UseManagementView。没有构造函数采用Connection,因此将调用默认构造函数。当遇到SideBar.fxml&lt;fx:include&gt; 时,它会指定SideBarController 的控制器,确实(现在)有一个构造函数采用Connection,因此调用该构造函数。 p>

请注意,侧边栏的设计与应用程序其他部分的设计略有不同,因为在其他组件中,您的视图是带有单独控制器类的 FXML 控制器对。对于侧栏,您使用 JavaFX 方法,其中视图是 FXML 文件,控制器指定为 fx:controller。同样,我可能会重构应用程序,以便一切都遵循第二种设计,而不是第一种。

【讨论】:

  • 非常感谢!这似乎工作得很好,而且比我想象的要容易! :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-07
  • 2017-05-16
  • 1970-01-01
  • 2014-06-09
  • 2015-11-07
  • 1970-01-01
相关资源
最近更新 更多