Bassinator
Bassinator

Reputation: 1724

NullPointerException when using TextArea.append()

I realize there is an nearly identical question in title. The question does not seem to be relevant to my particular issues.

I'm using the JavaFX scene builder to create my UI, (which includes the TextArea in question). All I want to do is take the message I get from the server and post it into the text area. Through various println statements I have narrowed the problem to this. I have tried various solutions (for hours); coming here was a last resort.

The only other possible cause I can think of would be something going wrong with the multithreading, but can even begin to think of what.

public class IncomingReader implements Runnable
{
    @Override
    public void run()
    {
        String message;

        try
        {
            while((message = Connection.connection.reader.readLine()) != null)
            {
                System.out.println("read" + message); //for debug, prints fine
                FXMLDocumentController.controller.chatBox
                        .appendText(message + "\n");  /////////PROBLEM HERE//////
            }
        }
        catch(IOException e)
        {
            System.out.println("Problem!"); // TODO: Catch better.
        }
    }
}

FXML controller class (relevant line only):

@FXML protected TextArea chatBox;

public class JavaChat extends Application 
{   
    @Override
    public void start(Stage stage) throws Exception {
        // Create and set up scene...
        Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));
        Scene scene = new Scene(root);
        // Establish connection to server...
        Connection.createNewConnection();
        // Create new reader thread...
        Thread readerThread = new Thread(new IncomingReader());
        readerThread.start();
        // When all done...
        stage.setScene(scene);
        stage.show();
    }
    public static void main(String[] args) {
        launch(args);
    }
}

The line of FXML that defines the chatBox:

<TextArea id="ChatBox" fx:id="chatBox" focusTraversable="false" mouseTransparent="true" prefHeight="400.0" prefWidth="200.0" promptText="Chat:" wrapText="true" BorderPane.alignment="CENTER">

The resulting exception:

Exception in thread "Thread-4" java.lang.NullPointerException
    at javachat.IncomingReader.run(IncomingReader.java:28)
    at java.lang.Thread.run(Thread.java:745)

Upvotes: 1

Views: 2235

Answers (1)

James_D
James_D

Reputation: 209358

Note: this is really covered fully in Passing Parameters JavaFX FXML. However, the code you posted is so far from being structured properly to use the approaches there, that you probably need to see it specifically for your example. I would strongly recommend you read through that post and understand it, though.

Why you are seeing a NullPointerException:

FXMLDocumentController.controller refers to an instance of FXMLDocumentController which is not the same instance that was created for you by the FXMLLoader. Consequently, the chatBox field in FXMLDocumentController.controller was not initialized by the FXMLLoader.

What FXMLLoader does:

When you call one of the FXMLLoader.load(...) methods, it basically does the following:

  1. Parses the FXML file (or stream)
  2. If the root element of the FXML contains an fx:controller attribute, and no controller has been set by other means, it creates an instance of the class specified in that attribute
  3. Creates the object hierarchy described by the FXML. If any of the objects defined in FXML have fx:id attributes, and a controller is associated with the FXMLLoader, it initializes @FXML annotated fields of the controller with the corresponding objects
  4. Associates event handlers with the nodes, where defined
  5. Returns the root object of the FXML object hierarchy

How to access the controller after loading the FXML

To access the controller, you must create an FXMLLoader instance instead of relying on the (evil) static FXMLLoader.load(URL) method. You can either pass the URL of the FXML resource into the FXMLLoader constructor, or call setLocation(...) on the FXMLLoader. Then, to load the FXML file, just call the no-argument load() method on the FXMLLoader. Once that is complete, you can access the controller by calling getController() on the FXMLLoader instance.

Other issues in your code

You cannot update the UI from a background thread: you must update it from the JavaFX Application Thread. As it stands (if you fix your NullPointerException issue), your code would throw an IllegalArgumentException in Java 8. In JavaFX 2.2 and earlier you would have to live with the possibility of bugs showing up at arbitrary times in the future. You can schedule code to be executed on the FX Application Thread by wrapping that code in a call to Platform.runLater().

Not quite so bad, but imo a bad design, is that you are exposing UI elements (the TextArea) outside of your FXML-controller pair. This becomes a real issue when your boss comes into your office and tells you he doesn't want the messages displayed in a TextArea any more, he wants them in a ListView. Since the TextArea is exposed, you have to trawl through all your code looking for any references to it. A better approach is to define a appendMessage(String) method in your controller, and access it. You might even want to factor all the data out into a separate model class, and pass references an instance of that class to both the controller and the reader class, but that is beyond the scope of this question.

I will refrain from complaining about your overuse of static stuff.. ;).

Here's the skeleton of one way to fix this code:

public class IncomingReader implements Runnable
{

    private final FXMLDocumentController controller ;

    public IncomingReader(FXMLDocumentController controller) {
        this.controller = controller ;
    }

    @Override
    public void run()
    {
        String message;

        try
        {
            while((message = Connection.connection.reader.readLine()) != null)
            {
                System.out.println("read" + message); //for debug, prints fine
                final String msg = message ;
                Platform.runLater(new Runnable() {
                    @Override
                    public void run() {
                        controller.appendMessage(msg + "\n");  
                    }
                });
            }
        }
        catch(IOException e)
        {
            System.out.println("Problem!"); // TODO: Catch better.
        }
    }
}

Controller:

public class FXMLDocumentController { 
    @FXML
    private TextArea chatBox ;

    public void appendMessage(String message) {
        chatBox.appendText(message);
    }

    // other stuff as before...
}

Application:

public class JavaChat extends Application 
{   
    @Override
    public void start(Stage stage) throws Exception {
        // Create and set up scene...
        FMXLLoader loader = new FXMLLoader(getClass().getResource("FXMLDocument.fxml"));
        Parent root = loader.load();
        FXMLDocumentController controller = (FXMLDocumentController) loader.getController();
        Scene scene = new Scene(root);
        // Establish connection to server...
        Connection.createNewConnection();
        // Create new reader thread...
        Thread readerThread = new Thread(new IncomingReader(controller));
        readerThread.start();
        // When all done...
        stage.setScene(scene);
        stage.show();
    }
    public static void main(String[] args) {
        launch(args);
    }
}

Upvotes: 4

Related Questions