2

In Chapter 3 of "Pro JavaFX 8: A Definitive Guide to Building Desktop, Mobile, and Embedded Java Clients", an example illustrates how to specify objects directly in an FXML file.

You can find the complete FXML file, along with the other files in the example, at the end of this post.

Here is the snippet I am taking about. The sizes field uses the fx:factory attribute to indicate that factory method Utilities.createList() must be used to create a list of Integers, which is then populated with three Integers.

<sizes>
    <Utilities fx:factory="createMyCollection">
        <Integer fx:value="1"/>
        <Integer fx:value="2"/>
        <Integer fx:value="3"/>
    </Utilities>
</sizes>

Here is Utilities.java:

package projavafx.fxmlbasicfeatures;

import java.util.ArrayList;
import java.util.List;

public class Utilities {
    public static final Double TEN_PCT = 0.1d;
    public static final Double TWENTY_PCT = 0.2d;
    public static final Double THIRTY_PCT = 0.3d;

    public static List<Integer> createList() {
        return new ArrayList<>();
    }
}

My question is: what is the general mechanism involved in using these factory methods?

I would like to understand how FXMLLoader knows that the three Integers need to be added to the created object with the add method. Naturally, it must know somehow about List or perhaps Collection, but where is that knowledge specified? Is it built-in in FXMLLoader? If so, how can such factory methods be provided for user-defined classes?

I actually tried using it with a user-defined class. I added the following snippet to Utilities.java, which creates a MyCollection class that has a single method add(Integer) and defines a Utilities.createMyCollection method:

public class Utilities {
    (...)
    public static class MyCollection {
        private List<Integer> myList = new LinkedList<>();
        public void add(Integer o) {
            myList.add(o);
        }
        public String toString() {
            return myList.toString();
        }
    }

    public static MyCollection createMyCollection() {
    return new MyCollection();    
    }
    (...)
}    

When I substituted createMyCollection in the FXML file, however, I got the message "MyCollections does not have a default property. Place MyCollection content in a property element."

which makes me wonder how I can declare a default property for a user-defined class, and how List already has one.

Here's all the files (besides Utilities.java above):

FXMLBasicFeatures.fxml:

<?import javafx.scene.paint.Color?>
<?import projavafx.fxmlbasicfeatures.FXMLBasicFeaturesBean?>
<?import projavafx.fxmlbasicfeatures.Utilities?>
<?import java.lang.Double?>
<?import java.lang.Integer?>
<?import java.lang.Long?>
<?import java.util.HashMap?>
<?import java.lang.String?>
<FXMLBasicFeaturesBean name="John Smith"
                       flag="true"
                       count="12345"
                       xmlns:fx="http://javafx.com/fxml/1">
    <address>12345 Main St.</address>
    <foreground>#ff8800</foreground>
    <background>
        <Color red="0.0" green="1.0" blue="0.5"/>
    </background>
    <price>
        <Double fx:value="3.1415926"/>
    </price>
    <discount>
        <Utilities fx:constant="TEN_PCT"/>
    </discount>
    <sizes>
        <Utilities fx:factory="createList">
            <Integer fx:value="1"/>
            <Integer fx:value="2"/>
            <Integer fx:value="3"/>
        </Utilities>
    </sizes>
    <profits>
        <HashMap q1="1000" q2="1100" q3="1200" a4="1300"/>
    </profits>
    <fx:define>
        <Long fx:id="inv" fx:value="9765625"/>
    </fx:define>
    <inventory>
        <fx:reference source="inv"/>
    </inventory>
    <products>
        <String fx:value="widget"/>
        <String fx:value="gadget"/>
        <String fx:value="models"/>
    </products>
    <abbreviations CA="California" NY="New York" FL="Florida" MO="Missouri"/>

</FXMLBasicFeaturesBean>

FXMLBasicFeaturesBean.java:

package projavafx.fxmlbasicfeatures;

import javafx.scene.paint.Color;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class FXMLBasicFeaturesBean {
    private String name;
    private String address;
    private boolean flag;
    private int count;
    private Color foreground;
    private Color background;
    private Double price;
    private Double discount;
    private List<Integer> sizes;
    private Map<String, Double> profits;
    private Long inventory;
    private List<String> products = new ArrayList<String>();
    private Map<String, String> abbreviations = new HashMap<>();

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    public Color getForeground() {
        return foreground;
    }

    public void setForeground(Color foreground) {
        this.foreground = foreground;
    }

    public Color getBackground() {
        return background;
    }

    public void setBackground(Color background) {
        this.background = background;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    public Double getDiscount() {
        return discount;
    }

    public void setDiscount(Double discount) {
        this.discount = discount;
    }

    public List<Integer> getSizes() {
        return sizes;
    }

    public void setSizes(List<Integer> sizes) {
        this.sizes = sizes;
    }

    public Map<String, Double> getProfits() {
        return profits;
    }

    public void setProfits(Map<String, Double> profits) {
        this.profits = profits;
    }

    public Long getInventory() {
        return inventory;
    }

    public void setInventory(Long inventory) {
        this.inventory = inventory;
    }

    public List<String> getProducts() {
        return products;
    }

    public Map<String, String> getAbbreviations() {
        return abbreviations;
    }

    @Override
    public String toString() {
        return "FXMLBasicFeaturesBean{" +
            "name='" + name + '\'' +
            ",\n\taddress='" + address + '\'' +
            ",\n\tflag=" + flag +
            ",\n\tcount=" + count +
            ",\n\tforeground=" + foreground +
            ",\n\tbackground=" + background +
            ",\n\tprice=" + price +
            ",\n\tdiscount=" + discount +
            ",\n\tsizes=" + sizes +
            ",\n\tprofits=" + profits +
            ",\n\tinventory=" + inventory +
            ",\n\tproducts=" + products +
            ",\n\tabbreviations=" + abbreviations +
            '}';
    }
}

FXMLBasicFeaturesMain.java:

package projavafx.fxmlbasicfeatures;

import javafx.fxml.FXMLLoader;

import java.io.IOException;

public class FXMLBasicFeaturesMain {
    public static void main(String[] args) throws IOException {
        FXMLBasicFeaturesBean bean = FXMLLoader.load(
            FXMLBasicFeaturesMain.class.getResource(
                "/projavafx/fxmlbasicfeatures/FXMLBasicFeatures.fxml")
        );
        System.out.println("bean = " + bean);
    }
}
user118967
  • 4,895
  • 5
  • 33
  • 54
  • An implementation of `java.util.List` is treated as a special case by the FXMLLoader. Does your `MyCollection` class implement `List`? – James_D May 15 '15 at 20:38
  • Ah, thanks, that is what I wanted to know, if support for `List` was builtin. `MyCollection` does not implement `List` on purpose; I wanted to see if it was possible to add this type of support for an arbitrary class. – user118967 May 15 '15 at 23:34
  • If it contains a list and you are happy to expose the entire list via a get method, you can still make this work. I'll post an answer with that when I'm back at the computer if it's helpful – James_D May 16 '15 at 00:43
  • Thanks, I should probably do that myself as an exercise. My problem was that FXMLLoader's javadoc says basically nothing. But I found Oracle's page "Introduction to FXML" which seems pretty complete. – user118967 May 16 '15 at 05:41
  • The documentation for `FXMLLoader` is pretty woeful. You might want to vote for [this issue](https://javafx-jira.kenai.com/browse/RT-35522). – James_D May 16 '15 at 13:43

1 Answers1

5

There are actually a couple of different issues going on here. As you know, the basic usage is that the FXMLLoader looks for classical-style properties via JavaBean naming schemes. So if you have a class

public class Bean {

    private String text ;

    public void setText(String text) {
        this.text = text ;
    }

    public String getText() {
        return text ;
    }
}

Then (since the class has a default, no-arg constructor), you can instantiate Bean in FXML:

<Bean>

and you can invoke the setText method by referencing the property text either as an attribute:

<Bean text="Some text"/>

or as a property element:

<Bean>
    <text>
        <String fx:value="Some text"/>
    </text>
</Bean>

Instances of java.util.List get special treatment. If a property name matches a read-only List property: i.e. a property of type java.util.List which has a get... method but no set... method, child nodes in the FXML will be passed to the corresponding List instances add(...) method.

So if we add such a property to the Bean:

import java.util.List ;
import java.util.ArrayList ;

public class Bean {

    private String text ;

    private List<String> elements ;

    public Bean() {
        this.elements = new ArrayList<>();
    }

    public List<String> getElements() {
        return elements ;
    }

    public void setText(String text) {
        this.text = text ;
    }

    public String getText() {
        return text ;
    }
}

Then we can populate the list in FXML:

<Bean text="Some text">
  <elements>
    <String fx:value="One"/>
    <String fx:value="Two"/>
    <String fx:value="Three"/>
  </elements>
<Bean>

The other issue you refer to is the "default property". You can specify a default property of a class by using the @DefaultProperty annotation on the class, and specifying the name of the property which is to be considered the default:

import java.util.List ;
import java.util.ArrayList ;

@DefaultProperty("text")
public class Bean {

    private String text ;

    private List<String> elements ;

    public Bean() {
        this.elements = new ArrayList<>();
    }

    public List<String> getElements() {
        return elements ;
    }

    public void setText(String text) {
        this.text = text ;
    }

    public String getText() {
        return text ;
    }
}

Now if you specify child elements of the instance element <Bean> in FXML, without specifying a property, those will be used as values for the default property:

<Bean>
  <String fx:value="Some Text"/>
</Bean>

will invoke setText("Some Text") on the Bean instance.

And of course you can combine these ideas and make the List instance the default property (this is essentially how the layout containers work: Pane defines "children" as its default property):

import java.util.List ;
import java.util.ArrayList ;

@DefaultProperty("elements")
public class Bean {

    private String text ;

    private List<String> elements ;

    public Bean() {
        this.elements = new ArrayList<>();
    }

    public List<String> getElements() {
        return elements ;
    }

    public void setText(String text) {
        this.text = text ;
    }

    public String getText() {
        return text ;
    }
}

and now you can do

<Bean text="Some Text">
  <String fx:value="One"/>
  <String fx:value="Two" />
  <String fx:value="Three" />
</Bean>

will populate the elements list with ["One", "Two", "Three"].

James_D
  • 201,275
  • 16
  • 291
  • 322