Scala name mangling of private fields and JavaFX FXML injection
Asked Answered
C

2

6

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:id property to the FXML elements
  • adding (usually private) fields to the controller, with the @FXML annotation and with field names matching the values of the fx:id properties defined in the FXML
  • when the FXMLoader instantiates the controller, it automatically injects the fx:id annotated elements into the matching @FXML annotated 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$1 class, for the anonymous function passed to testCheckBox.selected.onChange() in the initialize() method
  • In the TestController class, there are two private fields : testCheckBox (as expected) and test$TestController$$testButton (rather than just testButton), and the accessor/mutator methods. Of those, only the accessor method for test$TestController$$testButton is 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.

Cougar answered 31/7, 2015 at 19:46 Comment(2)
I couldn't find any other solution to this than writing the scalafxml library :/ By the way I'm curious why you prefer not using it.Well
I actually quite like the idea of scalafxml, but when I tried it, I had issues trying to get custom controls or especially controllers injections from <fx:includes> to work... And since I'm still quite new to JavaFX/ScalaFX (and the Scala language itself), I didn't want to spend time on that sort of thing, especially as I didn't know whether I would encounter other features breaking with scalafxml, and I didn't feel confident enough to try to contribute my own solutions to them. I'm pretty sure I'll give scalafxml another try at some point in the future, though.Stephanistephania
R
6

This is how I declare my injected fields:

@FXML
var popoutButton: Button = _

IOW, leave off the private and things work fine. Feels a bit dirty if you're coming from the Java world, but workarounds take a lot more code.

Controllers implied by fx:include also work fine:

FXML:

<fx:include fx:id="paramTable" source="ParameterTable.fxml" />

Scala:

@FXML
var paramTable: TableView[(ModelParameter, ParameterValue)] = _
@FXML
var paramTableController: ParameterTableController = _
Resistance answered 29/10, 2015 at 15:7 Comment(4)
@FXML var popoutButton: Button = _ I tried this same thing before I found this answer and didn't have any luck. Did this work for anyone?Cenacle
It still works for us fine. What's the error, and just to confirm, your field is in a class and not an object?Resistance
I was getting an NPE when trying to access a Stage from a controller class with a similar declaration. I think I can work around by setting to and getting from an object that is outside of both classes. Thanks for getting back.Cenacle
My experience has been that if you're depending on a Stage during the initialization phase, there's another way to think about it. I usually grab the Stage at the point where I have to have it for construction of another object, on demand. This can mean requiring a separate FXML file to keep things modular.Resistance
M
1

You can annotate your fields with @BeanProperty to stop Scala from rewriting the field name. The only drawback seems to be that the field now has to be at least protected, or the Scala compiler complains.

Maleeny answered 26/8, 2015 at 15:2 Comment(1)
I don't think this is entirely true. I've found that it is the private visibility specifier that opens up to name rewriting.Resistance

© 2022 - 2024 — McMap. All rights reserved.