1

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)
Bassinator
  • 1,682
  • 3
  • 23
  • 50
  • Has chat box been initialised? – MadProgrammer Aug 12 '14 at 20:51
  • Is `connection` or `connection.reader` null? – Elliott Frisch Aug 12 '14 at 20:55
  • When do you start the thread? Btw.: Don't modify the UI from another thread than the FX Application Thread! – isnot2bad Aug 12 '14 at 20:58
  • Which one is line 28? – Tomas Mikula Aug 12 '14 at 20:58
  • @MadProgrammer Shouldn't the FXML generated by the Scene builder handle that? Although this was one of the first solutions I tried. I changed my code to `@FXML protected TextArea chatBox = new TextArea();`. When I did this, the exception stopped, but the text was not appended. – Bassinator Aug 12 '14 at 21:00
  • @Tomas Line 28 is the one indicated with `////Problem here/////` – Bassinator Aug 12 '14 at 21:01
  • @isnot2bad The thread is started from another class. – Bassinator Aug 12 '14 at 21:02
  • @ElliotFrisch It can't be; the print statement is printing correctly. – Bassinator Aug 12 '14 at 21:03
  • 1
    1. You cannot update the UI from the background thread. This could conceivably be the cause of the issue, but probably isn't: you need to fix it anyway. 2. What is `FXMLDocumentController.controller`? Presumably it is an instance of your controller class, but is it the same instance created by the `FXMLLoader`? You probably need to show the code relevant to that. 3. Are you sure you have the `fx:id` attribute set correctly so that the `TextArea` is injected into the controller. Show the relevant FXML code. – James_D Aug 12 '14 at 21:11
  • @James_D Edited question with more relevant code. In response to: 1.) What thread _should_ I do it in? 2.) You are correct, and I don't think so. Would this be the root variable declared in the main class? 3.) Positive. – Bassinator Aug 12 '14 at 21:24
  • You didn't post any controller code as far as I can see. I still can't tell what `FXMLDocumentController.controller` is. But if it is not the controller created by the `FXMLLoader`, how would you expect its `chatBox` field to have been initialized? – James_D Aug 12 '14 at 21:27
  • Oh, and you should update the UI on the FX Application Thread. Use `Platform.runLater()`, or (probably better) re-design the whole think to use [`Task`s](http://docs.oracle.com/javase/8/javafx/api/javafx/concurrent/Task.html) – James_D Aug 12 '14 at 21:28
  • @James_D There is no more relevant controller code. Everything else within the class relates to other stuff. And how would I get the controller created by the FXMLLoader? – Bassinator Aug 12 '14 at 21:36
  • Well, somewhere you either do `FXMLDocumentController.controller = ...;` (or within the `FXMLDocumentController` class you do `controller = ...;`). Since the problem seems to be that that instance of `FXMLDocumentController` is not the same one as the instance that was created for you by the `FXMLLoader`, I'd argue that code (and the declaration of `controller`) is pretty relevant. – James_D Aug 12 '14 at 21:39
  • @ James_D What I am asking is; how do I make is so that I am using the same instance of FXMLDocumentController created by the FXML? – Bassinator Aug 12 '14 at 21:46
  • Pass a reference to it to the `IncomingReader`. I will put together an answer, though I think this is very thoroughly covered in other questions. – James_D Aug 12 '14 at 21:51
  • @James_D slightly edited main class above. – Bassinator Aug 12 '14 at 21:51
  • @James_D What I mean is that, I don't know how to get at the one created by the FXMLLoader to create a reference. Sorry haha, I realize that was a little unclear. – Bassinator Aug 12 '14 at 21:53

1 Answers1

4

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);
    }
}
Community
  • 1
  • 1
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thank you sir, you are a gentleman and a scholar! And I was always planning to refactor, I was just trying to get a working version up first. – Bassinator Aug 12 '14 at 23:53