The following example and explanations are quite long, so here is the gist of my question: how to deal with scalac's name-mangling of private fields when using a framework which insists on performing field injection (on fields which really should stay private)?
I am writing an application in Scala, using ScalaFX/JavaFX and FXML. When you use FXML to define your views in JavaFX, objects defined in FXML (such as buttons and text fields) are injected into the controller by :
- adding an
fx:idproperty to the FXML elements - adding (usually private) fields to the controller, with the
@FXMLannotation and with field names matching the values of thefx:idproperties defined in the FXML - when the
FXMLoaderinstantiates the controller, it automatically injects thefx:idannotated elements into the matching@FXMLannotated fields of the controller through reflexion
I'm not a big fan of field injection, but that's how FXML works. However, I've run into unexpected complications in Scala, due to field name mangling performed by the compiler in some circumstances...
Here is an example application :
test/TestApp.scala (nothing interesting, just needed to run the example)
package test
import javafx.application.Application
import javafx.fxml.FXMLLoader
import javafx.scene.{Scene, Parent}
import javafx.stage.Stage
object TestApp {
def main(args: Array[String]) {
Application.launch(classOf[TestApp], args: _*)
}
}
class TestApp extends Application {
override def start(primaryStage: Stage): Unit = {
val root: Parent = FXMLLoader.load(getClass.getResource("/test.fxml"))
val scene: Scene = new Scene(root, 200, 200)
primaryStage.setTitle("Test")
primaryStage.setScene(scene)
primaryStage.show()
}
}
test.fxml (the view)
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0"
prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="test.TestController">
<children>
<CheckBox fx:id="testCheckBox" mnemonicParsing="false" text="CheckBox"/>
<Button fx:id="testButton" mnemonicParsing="false" text="Button"/>
</children>
</VBox>
test/TestController.scala (the controller for the test.fxml view)
package test
import javafx.fxml.FXML
import javafx.scene.{control => jfxsc}
import scalafx.Includes._
class TestController {
@FXML private var testCheckBox: jfxsc.CheckBox = _
@FXML private var testButton: jfxsc.Button = _
def initialize(): Unit = {
println(s"testCheckBox=$testCheckBox")
println(s"testButton=$testButton")
testCheckBox.selected.onChange {
testButton.text = "changed"
}
}
}
When running the application, the println statements show that testCheckBox gets injected properly, but testButton is null. If I click on the checkbox, there is, as expected, a NullPointerException when calling testButton.text_=.
The reason is quite obvious when looking at the compiled classes :
- There is a
TestController$$anonfun$initialize$1class, for the anonymous function passed totestCheckBox.selected.onChange()in theinitialize()method - In the
TestControllerclass, there are two private fields :testCheckBox(as expected) andtest$TestController$$testButton(rather than justtestButton), and the accessor/mutator methods. Of those, only the accessor method fortest$TestController$$testButtonis public.
Clearly, the Scala compiler mangled the name of the testButton field because it had to make its accessor method public (to access it from TestController$$anonfun$initialize$1) and because the field and the accesor/mutator methods should keep the same name.
Now, finally, here is my question: is there a reasonable solution to deal with this situation? Right now, what I have done is make the fields public: since the compiler doesn't need to change their visibility, it won't mangle their name. However, those fields really have no business being public.
Note: Another solution would be to use the scala-fxml library, which completely hides the field injection, but I'd rather use bog-standard FXML loading for other reasons.