Search code examples
javajavafxjavafx-8fxml

How to properly inherit an FXML annotated attribute for having it available in a subclass?


Problem

I'm trying to implement the transmission of messages over a Controller Area Network, where the messages are built from user's inputs through a GUI created with JavaFX.

I have a MainController class which is linked to a Main.fxml. In the MainController I have defined an FXML annotated TextField attribute in_desiredVelocity which is correctly linked to its fx:id in Main.fxml.

Then I have defined an abstract class canMessage which defines the backbone of a message which has to be sent over the network.

Now the class PcToVcuMessage implements a particular type of canMessage. In order to be able to access the FXML attribute (defined in MainController) I've decided that the abstract class canMessage extends MainController, and PcToVcuMessage extends canMessage.

The application compiles correctly but when I enter, in the GUI, the TextField in_desiredVelocity a NullPointerException is launched.

Question

Despite the above FXML attribure is inherited by PcToVcuMessage (which inherits from the abstract class canMessage and this one extends the MainController), how can I use it in this class to achieve my goal?

Main:

package canbusgui;

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

import java.io.IOException;

public class MainApplication extends Application {
    public static Stage stage = null;

    @Override
    public void start(Stage stage) throws IOException {
        
        stage.setOnCloseRequest(event -> {
           System.exit(0);
        });

        MainApplication.stage = stage;

        // Create a FXMLLoader to load the FXML file that defines the user interface
        FXMLLoader fxmlLoader = new FXMLLoader(MainApplication.class.getResource("MainView.fxml"));

        Scene scene = new Scene(fxmlLoader.load());

        stage.setTitle("CANbus GUI");
        stage.setScene(scene);
        stage.setResizable(false);
        stage.setMinHeight(768);
        stage.setMinWidth(1366);
        stage.show();
    }

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

MainController:

package canbusgui;

import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;


public class MainViewController {
  @FXML
  protected TextField in_desiredVelocity;

  @FXML
  public void initialize (){

        in_desiredVelocity.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
                if (event.getCode() == KeyCode.ENTER) {
                    try {
                        sendMessage(new PcToVcuMessage("222"));
                    }
                    catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
        });
  }
  

  public void sendMessage(canMessage message) throws Exception {
        
        message.constructData();
       
        message.validateInputs();
        
        byte[] data = message.getData();

        // Send the data array to the CAN bus
        canBusController.sendCommand(HexFormat.fromHexDigits(message.getId()), data);
  }

}

canMessage.java (it contains an abstract class canMessage and PcToVcuMessage extends it):

package canbusgui;

import static java.lang.Integer.parseInt;

public abstract class canMessage extends MainViewController{

    // Declare common attributes
    protected String id;
    protected byte[] data;

    public canMessage(String id) {
        this.id = id;
        // Initialize an empty byte array for data
        this.data = new byte[8];
    }


    // Define an abstract method to construct the data array
    public abstract void constructData();

    // Define an abstract method to validate the inputs
    public abstract void validateInputs() throws Exception;

    // Define a getter method for the data array
    public byte[] getData() {
        return this.data;
    }

    public String getId() {
        return this.id;
    }

}


// Define a subclass for PC_to_VCUMessage message
class PcToVcuMessage extends canMessage {


    public PcToVcuMessage(String id) {
        // Call the superclass constructor
        super(id);
    }


    // Override the constructData method
    @Override
    public void constructData() {


        data[0] = (byte) 0;
        data[1] = (byte) 0;
        data[2] = (byte) 0;
        data[3] = (byte) parseInt(in_desiredVelocity.getText()); //HERE in_desiredVelocity is null and a NillPointerException is launched
        data[4] = (byte) 0;
        data[5] = (byte) 0;
        data[6] = (byte) 0;
        data[7] = (byte) 0;
    }

    public  void validateInputs() throws Exception{}


}

EDIT

The format of the CAN message is the following: ID(hex), data0, data1, data2, data3, ......, data7. So, when I call the PcuToVcuMessage's constructor in the controller I'm passing the ID of the message 222 (which, btw, is specified in the data sheet of the device)

In PcuToVcuMessage I need to acess to the FXML attribute in_desiredVelocity which has been set by the user by typing the value in the TextField of the GUI: in this way it is possible to retrieve the value typed by the user in order to build the message.

EDIT2

Since there can be multiple messages with different IDs, I have thought to use polymorphism in the sendMessage method in the controller. Furthermore there can be messages which needs to access more than one FXML attribute from controller class.


Solution

  • This simply isn't what inheritance does. Inheritance is a relationship between classes, not between objects.

    When you do

    public class MainViewController {
        // ...
        protected TextField inDesiredVelocity;
        // ...
    }
    

    it means that every instance of MainViewController will have a field called inDesiredVelocity of type TextField.

    When you do

    public abstract class CanMessage extends MainViewController {
        // ...
    }
    

    it means that every instance of CanMessage is also an instance of MainViewController.

    When you load the FXML, the FXMLLoader creates an instance of MainViewController (because you have fx:controller="canbusgui.MainViewController" in the FXML) and initializes the inDesiredVelocity field in that instance with a reference to the text field declared in the FXML.

    Later in your controller, you do

    new PcToVcuMessage("222")
    

    which, of course, creates a new instance of PcToVcuMessage with id "222". Since PcToVcuMessage inherits from CanMessage, that new instance is also an instance of CanMessage, and since CanMessage inherits from MainViewController, that instance is also an instance of MainViewController, and since every instance of MainViewController has a field inDesiredVelocity, this new instance of PcToVcuMessage has a field called inDesiredVelocity of type TextField.

    However, at no point do you initialize that field (and there's no sensible way to do so), so the inDesiredVelocity field in your PcToVcuMessage is null.


    It doesn't make any sense to do any of this. I don't really understand what your domain model is (and I probably don't need to to answer this question), but it doesn't make any sense for a TextField to be part of an object whose type is some kind of message.

    Instead, it probably makes sense for the data that is being sent by this message to be part of the PcToVcuMessage. I.e. you can do

    class PcToVcuMessage extends CanMessage {
    
        private int desiredVelocity ;
    
    
        public PcToVcuMessage(String id, int desiredVelocity) {
            // Call the superclass constructor
            super(id);
            this.desiredVelocity = desiredVelocity;
        }
    
    
        // Override the constructData method
        @Override
        public void constructData() {
    
    
            data[0] = (byte) 0;
            data[1] = (byte) 0;
            data[2] = (byte) 0;
            data[3] = (byte) desiredVelocity;
            data[4] = (byte) 0;
            data[5] = (byte) 0;
            data[6] = (byte) 0;
            data[7] = (byte) 0;
        }
    
        public  void validateInputs() throws Exception{}
    
    
    }
    

    and in the controller replace new PcToVcuMessage("222") with

    new PcToVcuMessage("222", Integer.parseInt(inDesiredVelocity.getText()))
    

    Then just remove the extends MainViewController from the CanMessage class. This obviously makes no sense at all (a message is not a thing that controls a UI).

    Some unrelated issues with your code:

    1. Don't register key handlers with text fields. To handle the user pressing Enter, use an action handler in the usual way.
    2. Name classes and variables appropriately. Class names should be nouns (they represent things). CanMessage is a verb (or verbal phrase). Probably Message is more appropriate, but again I don't really understand what you are modeling here.