Reputation: 187
I'm trying to pass a variable list of strings into a custom controller in fxml, thus:
<Pane xmlns:fx="http://javafx.com/fxml" >
<VBox fx:id="vbox">
<StepChart fx:id="employment" title="Unemployment" enabled = "true" prefHeight="250.0" prefWidth="300.0">
<statistics>
<FXCollections fx:factory="observableArrayList" >
<String fx:value="employees"/>
<String fx:value="farms"/>
</FXCollections>
</statistics>
</StepChart>
</VBox>
</Pane>
statistics is labelled as an @DefaultProperty in StepChart, but included here for completeness. In StepChart I'm picking up the observable list values as follows:
ArrayList<String> list = new ArrayList<String>();
ObservableList<String> statistics = FXCollections.observableArrayList(list);
statistics.addListener(new ListChangeListener<String>()
{
@Override
public void onChanged(ListChangeListener.Change<? extends String> change)
{
for(String s: statistics)
{
System.out.println("s: " + s);
}
}
});
Which all works perfectly, except that rather than a list of separate strings, the fxml is generating a single string, with the fxml values in it:
s: [employees, farms]
even though Introduction to fxml says that it "creates an instance of an observable array list, populated with three string values". Am I missing something about how to handle the observablelist, or is this expected behaviour?
Upvotes: 2
Views: 769
Reputation: 209663
I suspect your StepChart
class doesn't have a setStatistics(...)
method. The FXMLLoader
will treat this in a special way, as follows:
Normally, when the FXMLLoader
encounters a structure like:
<ClassName fx:id="ref">
<propertyName>
<AnotherClass />
</propertyName>
</ClassName>
it executes code that translates to something like:
ClassName ref = new ClassName();
ref.setPropertyName(new AnotherClass());
(not literally, it does the equivalent but using reflection).
For the most part, if there is no setPropertyName(...)
defined in the specified class, an exception will be thrown.
Read-only lists are treated as a special case: so if ClassName
defines a List getList() {...}
method, and no void setList(List aList) {...}
method, then the following still works:
<ClassName fx:id="ref">
<list>
<!-- values -->
</list>
</ClassName>
The way this is handled, in this particular case (the name of the property corresponds to a property of type List
with a get
method and no set
method), then the FXMLLoader
executes the following pseudocode:
ClassName ref = new ClassName();
for (Object item : itemsDefinedInsideListElement) {
ref.getList().add(item);
}
where the for
loop iterates over all the elements defined inside the <list>
element. The is what enables you to do things like
<HBox>
<children>
<Label/>
<TextField/>
</children>
</HBox>
even though HBox
has no setChildren()
method.
So, with your FXML, assuming StepChart
has no setStatistics(...)
method, the FXMLLoader
iterates over all the direct child elements of <statistics>
, passing each one to getStatistics().add(...)
invoked on the StepChart
instance it created. In this case, there is only one child element, which is an ObservableList<String>
.
The FXMLLoader
is also smart enough to figure the generic type of the list at runtime (I actually have no idea how it does this). Since it sees the list is expecting a String
, and the element did not resolve to a String
, it attempts to coerce it to the correct type. Coercing to a String
is easy: it merely calls toString
on the result of creating the list, and passes the result in. So you end up with a list that contains a single String
: that String
is the result of calling toString
on a List
containing the elements "employees" and "farms". The equivalent Java code would look something like
StepChart employment = new StepChart();
employment.getStatistics().add(Arrays.asList("employees", "farms").toString());
and so you see the single element "["employees", "farms"]"
as the contents of statistics
.
You have two fixes for this, and the one you choose should depend on what API you want to expose in StepChart
. You can either add in a setStatistics(ObservableList<String>)
method. Then the FXMLLoader
will create the list, and pass it into that method.
Alternatively, you can continue to omit the setStatistics(...)
method (i.e. keep statistics
as a read-only list property) and modify the FXML to
<StepChart fx:id="employment" ... >
<statistics>
<String fx:value="employees"/>
<String fx:value="farms"/>
</statistics>
</StepChart>
so that you use the FXMLLoader
's read-only list property mechanism.
Upvotes: 3