0

Edit: I was lucky enough that James_D quickly identified and explained this phenomenon in his comments below. Having now found the alternative to the use of PropertyValueFactory (for Groovy users) I thought I'd add my own answer to help anyone unlucky enough to be flummoxed by this in future.


Specs Java: 11. OS: Linux Mint 18.3. All this app and testing code is written in Groovy (2.5.10), but this shouldn't be particularly relevant (i.e. the same phenomenon should occur with an equivalent project written in Java).

I have a method called from App.start() where App extends javafx.application.Application (so it is running in the JAT/JavaFX Application Thread):

class GraphBuilder {
...
def attachAndShow(Stage stage, Node rootNode) {
    try {
        App.instance.fxmlController.treeTableView.root = rootTreeItem
        App.instance.fxmlController.treeTableView.showRoot = false
        stage.scene = new Scene(rootNode, 1200, 800)
        stage.show()
    }catch( Throwable t ){
        log.error( "got throwable in attachAndShow... $t.message", t )
    }
}

I am TDD'ing my way forward on the model of a "spike" (untested code where I just raced ahead) where the TreeTableView gets populated. I emphasise that there is no sign any anything being amiss with the spike.

Intrinsic to doing this is to set the TreeTableView's columns "cell value factories", which stipulate the name of a field of the class instances of which will be set as the value of the TreeItems.

The value class is called Task... and its first field is String title. I've added a first factory for this. But (because I'm proceeding by TDD) I have added the factory for the second column, property dueDate, but I haven't yet included this field in Task. Naturally the test to make sure that a factory has been set, with the right property name, is failing. But I'm more interested in what happens when I do a Gradle installDist (i.e. distribute a self-contained executable) at this point.

This runs: the window is displayed. I can see that the TreeItems have been given the right titles. In the Terminal I see a stack trace caused by my failure to include a required field (dueDate) in the Task instances:

Mar 23, 2020 6:16:25 PM javafx.scene.control.cell.TreeItemPropertyValueFactory getCellDataReflectively
WARNING: Can not retrieve property 'dueDate' in TreeItemPropertyValueFactory: javafx.scene.control.cell.TreeItemPropertyValueFactory@2d1e69d0 with provided class type: class core.Task
java.lang.IllegalStateException: Cannot get property dueDate
    at com.sun.javafx.property.PropertyReference.getProperty(PropertyReference.java:194)
    at javafx.scene.control.cell.TreeItemPropertyValueFactory.getCellDataReflectively(TreeItemPropertyValueFactory.java:185)
    at javafx.scene.control.cell.TreeItemPropertyValueFactory.call(TreeItemPropertyValueFactory.java:158)
    at javafx.scene.control.cell.TreeItemPropertyValueFactory.call(TreeItemPropertyValueFactory.java:136)
    at javafx.scene.control.TreeTableColumn.getCellObservableValue(TreeTableColumn.java:576)
    at javafx.scene.control.TreeTableColumn.getCellObservableValue(TreeTableColumn.java:561)
    at javafx.scene.control.TreeTableCell.updateItem(TreeTableCell.java:632)
    at javafx.scene.control.TreeTableCell.indexChanged(TreeTableCell.java:457)
    at javafx.scene.control.IndexedCell.updateIndex(IndexedCell.java:120)
    at javafx.scene.control.skin.TableRowSkinBase.updateCells(TableRowSkinBase.java:539)
    at javafx.scene.control.skin.TreeTableRowSkin.updateCells(TreeTableRowSkin.java:276)
    at javafx.scene.control.skin.TableRowSkinBase.<init>(TableRowSkinBase.java:159)
    at javafx.scene.control.skin.TreeTableRowSkin.<init>(TreeTableRowSkin.java:102)
    at javafx.scene.control.TreeTableRow.createDefaultSkin(TreeTableRow.java:529)
    at javafx.scene.control.Control.doProcessCSS(Control.java:897)
    at javafx.scene.control.Control.access$000(Control.java:83)
    at javafx.scene.control.Control$1.doProcessCSS(Control.java:89)
    at com.sun.javafx.scene.control.ControlHelper.processCSSImpl(ControlHelper.java:67)
    at com.sun.javafx.scene.NodeHelper.processCSS(NodeHelper.java:145)
    at javafx.scene.Node.processCSS(Node.java:9529)
    at javafx.scene.Node.applyCss(Node.java:9616)
    at javafx.scene.control.skin.VirtualFlow.setCellIndex(VirtualFlow.java:1715)
    at javafx.scene.control.skin.VirtualFlow.getCell(VirtualFlow.java:1692)
    at javafx.scene.control.skin.VirtualFlow.getCellLength(VirtualFlow.java:1801)
    at javafx.scene.control.skin.VirtualFlow.computeViewportOffset(VirtualFlow.java:2639)
    at javafx.scene.control.skin.VirtualFlow.layoutChildren(VirtualFlow.java:1245)
    at javafx.scene.Parent.layout(Parent.java:1204)
    at javafx.scene.Parent.layout(Parent.java:1211)
    at javafx.scene.Parent.layout(Parent.java:1211)
    at javafx.scene.Parent.layout(Parent.java:1211)
    at javafx.scene.Parent.layout(Parent.java:1211)
    at javafx.scene.Parent.layout(Parent.java:1211)
    at javafx.scene.Parent.layout(Parent.java:1211)
    at javafx.scene.Parent.layout(Parent.java:1211)
    at javafx.scene.Parent.layout(Parent.java:1211)
    at javafx.scene.Parent.layout(Parent.java:1211)
    at javafx.scene.Scene.doLayoutPass(Scene.java:576)
    at javafx.scene.Scene.preferredSize(Scene.java:1748)
    at javafx.scene.Scene$2.preferredSize(Scene.java:393)
    at com.sun.javafx.scene.SceneHelper.preferredSize(SceneHelper.java:66)
    at javafx.stage.Window$12.invalidated(Window.java:1086)
    at javafx.beans.property.BooleanPropertyBase.markInvalid(BooleanPropertyBase.java:110)
    at javafx.beans.property.BooleanPropertyBase.set(BooleanPropertyBase.java:145)
    at javafx.stage.Window.setShowing(Window.java:1174)
    at javafx.stage.Window.show(Window.java:1189)
    at javafx.stage.Stage.show(Stage.java:273)
    at javafx.stage.Stage$show.call(Unknown Source)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:115)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:119)
    at core.GraphBuilder.attachAndShow(main.groovy:111)
    at core.GraphBuilder$attachAndShow$4.call(Unknown Source)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:115)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:135)
    at core.App.start(main.groovy:56)
    at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:846)
    at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:455)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:428)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:427)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
    at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
    at com.sun.glass.ui.gtk.GtkApplication.lambda$runLoop$11(GtkApplication.java:277)
    at java.base/java.lang.Thread.run(Thread.java:834)

This Throwable (IllegalState is a RuntimeException) doesn't get caught in the above try ... catch. Why not? Is there any way to catch such exceptions?

This certainly appears to be the JAT: you can see App.start() in that stack trace (and also GraphBuilder.attachAndShow())

I just performed a couple of experiments: throwing first a RuntimeException and then a standard Exception in this attachAndShow() method, before any of the 4 lines in the method.

In both cases the (distributed) application failed to run, and displayed nothing... but the log message was indeed logged, i.e. these were both caught. Totally expected behaviour, so it appears that the JAT itself is not the "culprit".

I also note that the trace shown above is not a "normal" stack trace: the first two lines indicate that something is handling it and, it appears, rather unfortunately swallowing it. At the very least I'd like to be able to log such problems!

mike rodent
  • 14,126
  • 11
  • 103
  • 157
  • This doesn't answer the question, but there's really no reason to use `PropertyValueFactory` implementations any more. Just implement the callback with a lambda (or suitable equivalent in Groovy), and then these errors will be caught at compile time anyway. I think that `PropertyValueFactory` implementations are designed to "silently" fail when the reflection fails, so an exception is not actually thrown, but a warning is issued. – James_D Mar 23 '20 at 18:41
  • OK thanks for the tip. I should make it plain that these are my first faltering steps with JavaFX so I know nothing about such matters. Presumably this can't be a one-off anomaly though: presumably there are other things which can arise under the stewardship of `show()` which can cause similar phenomena? – mike rodent Mar 23 '20 at 18:44
  • No, not really. If an exception is thrown (actually thrown, rather than caught, handled, and reported by the library classes as in this case), then it will propagate in the normal way (so you'd catch it). It's just because the particular class you're using (`TreeItemPropertyValueFactory`) relies on reflection and chooses to issue a warning if there's a name mismatch between the property name provided and the actual properties present in the model object. – James_D Mar 23 '20 at 18:46
  • Right. So I've just been unlucky enough to have picked the one thing that would totally confuse a JavaFX newb. Will now search for this callback solution (NB Java lambdas don't yet work in Groovy (not 2 at least) as you may know, but other tools are available, yes). Any link you might recommend showing a how-to for this? Hah... no need: https://stackoverflow.com/a/38050982/595305.... thanks again! – mike rodent Mar 23 '20 at 18:50

1 Answers1

0

The basic answer is here.

Groovy users can smugly slice through the vast forest of pointy brackets though: my solution became:

fxmlController.treeTableView.columns.get( 0 ).cellValueFactory =
new Callback() {
    @Override
    Binding call(Object cellDataFeatures ) {
        // NB it turns out that the first "value" is a TreeItem
        Task task  = cellDataFeatures.value.value
        Bindings.createStringBinding({ task.title })
    }
}

or stick some minimal ones in maybe:

fxmlController.treeTableView.columns.get( 0 ).cellValueFactory =
new Callback<TreeTableColumn.CellDataFeatures, ObservableValue>() {
    @Override
    Binding call(TreeTableColumn.CellDataFeatures cellDataFeatures ) {
        Task task  = cellDataFeatures.value.value
        Bindings.createStringBinding({ task.title })
    }
}

For the Groovy "Java lambda" substitute, no surprise that we use a closure:

fxmlController.treeTableView.columns.get( 0 ).cellValueFactory 
    = { cellDataFeatures -> cellDataFeatures.value.titleProperty() }

NB this involves a little understanding of JavaFX Property (see here) and a little tailoring of the class whose instances are displayed.

mike rodent
  • 14,126
  • 11
  • 103
  • 157