# Using fx ## Documentation map - [Build a complete application from scratch](docs/quick-start.md). - [Architecture, MVVM, identity, threading and lifetime](docs/architecture.md). - [Walk through the person desk](../fx-example/README.md). - This page is the detailed FXML and runtime reference. - [Generated ViewModel classes and registries](docs/generated-viewmodels.md). - [Bounded integer spinners](docs/integer-spinners.md) and [system tray support](docs/tray-support.md). - [Dependency injection](../injection/README.md). ## Introduction `fx` binds an FXML view to an ordinary Java model. The model stays a JavaBean. The library builds one view model for that instance, loads the view, and applies the binding elements declared in the FXML. You write the model, the FXML, and, where a screen needs them, a controller, reusable components, and converters. The library creates the view model. The [injection](../injection/README.md) container creates the controller, each component, and each converter. The worked example is the `fx-example` module, package `de.neitzel.fx.example`. From the NeitzelLib root, with Java 21: ```bash ./mvnw -pl fx-example -am -Dmaven.test.failure.ignore=false install ./mvnw -f fx-example/pom.xml javafx:run ``` The launch class is `de.neitzel.fx.example.Main`. `Main` does not extend `Application`. It calls `Application.launch(DeskApp.class, args)`, and `DeskApp.start` builds the scene on the JavaFX thread. Depend on `de.neitzel.lib:fx`. That module depends on `core`, `injection` and on JavaFX base, controls, graphics, fxml, and web. Application types the container constructs use `jakarta.inject.Singleton` or `jakarta.inject.Named`. ## Dependencies These are the versions configured by this source snapshot, not a promise of compatibility with every newer release: | Part | Baseline / relationship | Consumer responsibility | | --- | --- | --- | | JDK | Java 21 | Select a matching JDK for compilation and launch | | Maven | Minimum 3.8.6; wrapper 3.9.11 | Use the wrapper or an installed Maven | | Library | `de.neitzel.lib:fx:1.0-SNAPSHOT` | Install the local reactor first; no Central publication is assumed | | Internal modules | `fx` directly depends on `core` and `injection` | Resolved transitively through Maven | | JavaFX | 21.0.7: base, graphics, controls, FXML and web | Resolved transitively; deployment still needs platform-native artifacts | | Injection scanning | Reflections 0.10.2 | Brought by `injection` | | Injection annotations | `jakarta.inject-api` 2.0.1 | Use Jakarta annotations, not `javax.inject` | | Logging | SLF4J API 2.0.17 | Choose one logging backend; the example uses `slf4j-simple` 2.0.17 at runtime | | Library build | Lombok 1.18.42 and test/analysis tooling | Not required in ordinary consumer source; use your own annotations only if wanted | | Designer | Optional preview API / Model processor | Needed only for the selected Designer workflow, not normal `ViewLoader` use | The quick-start POM adds only `fx` and a logging backend. Do not copy library build-time dependencies into an application just to obtain runtime binding. A project using injection alone needs the `injection` artifact and does not need JavaFX. ## Quick start `DeskApp` scans its package, loads one person into `Desk.fxml`, and asks for a dialog when a new message is added: ```java ApplicationContext context = new ApplicationContext(DeskApp.class); Person person = DeskApp.sample(); LoadedView loaded = ViewLoader.load( DeskController.class.getResource("Desk.fxml"), person, context); MessagePopup.showNewMessages(loaded.getViewModel()); Parent root = loaded.getRoot(); ``` `Person` mentions no JavaFX type. `Desk.fxml` names `DeskController` and lists the bindings in `fx:define`, so they are not scene-graph children. A name field is a direct binding: ```xml ``` `ApplicationContext` scans the package of the class you pass, and its subpackages. `DeskApp` carries `@Config(basePackage = "de.neitzel.fx.example")`, which is that package. `ViewLoader.load` reuses the view model already built for that person, or builds one, then loads the FXML and applies the binding elements. One model instance has one view model. The first view fixes the view model class. The desk's controller names `PersonViewModel`, so that is the class for this person. ## The person desk The desk edits one person: name, age, greeting, title, role, active flag, home address, visits, and tags. The visits appear again in a table, with one row per visit, and again as repeated forms. | What you see | Feature | Where it is written | | --- | --- | --- | | Name | Direct binding, `both` | `Desk.fxml`, `nameField` | | Shown name | Direct binding, `view`, on an editable field | `nameEcho` | | Name draft | Direct binding, `model` | `nameDraft` | | Age, shown as `years 36` | Converter, constructor injection, `@Named` service | `AgeConverter`, `AgeSuffix` | | Greeting | Computed binding that reads the name field and the age | `DeskController.greeting` | | Title `titled-Ada Lovelace` | View model subclass | `PersonViewModel`, `@ViewModelClass` on the controller | | Active | Boolean attribute on a check box | `Person.active` | | Role | Enum attribute on a choice box | `Person.role`, `Role` | | Home address | Component, setter injection, `HasViewModel` | `AddressForm` | | Note under the home address | Nested component | `NoteForm` inside `AddressForm.fxml` | | Home street label | Path `home.street`, `view` | `homeStreetLabel` | | Empty street | Converter failure carried as `home.street` | `StreetConverter`, field injection of `StreetPolicy` | | Visit forms | Repeat | `VisitForm` in `visitPane` | | Visit combo and table selection | One `selectedVisit` attribute | `visitChoice`, `visitTable` | | Visit table | Columns, a date, a Remove button after the fields | `Table`, `VisitRow`, `VisitedOnConverter` | | Tags | List of simple values | `tagList` | | Notice, and the three buttons | Messages, including info, warning, and fatal | `noticeLabel`, `DeskController` | `DeskLoadsTest` loads this same desk and walks those bindings. ## Model A read-write attribute is a getter and a setter, and it is not declared on `Object`. `Person`, `Address`, `Note`, and `Role` are models. | Declared type | What the view model stores | | --- | --- | | `String` | `StringProperty` | | `int`, `Integer` | `IntegerProperty`. A null `Integer` becomes 0 | | `long`, `Long` | `LongProperty`. A null `Long` becomes 0 | | `float`, `Float`, `double`, `Double` | The matching primitive property. A null value becomes 0 | | `boolean`, `Boolean` | `BooleanProperty`. A null `Boolean` becomes false | | anything else | `ObjectProperty` holding the value, including null | Primitives, enums, arrays, `String`, `Boolean`, `Number`, `Collection`, `Map`, `Date`, `Calendar`, and any type in a `java.time` package stay simple values, even when they have setters. The desk uses that for `name`, `age`, `active`, `role`, `tags`, and `visitedOn`. A type that itself has a read-write attribute is complex. The view model holds the view model of that nested instance, or null. `Person.home` is an `Address`. `Address.note` is a `Note`. A `List` of either kind is a list, not a nested view model. `visits` is a list of addresses. `tags` is a list of strings. Instances are compared by identity. One person has one view model. The same address object on two attributes would be one view model, so an edit through either attribute would show on the other. Two equal addresses that are different objects keep two view models. A cycle, such as a person who points at themselves, reuses the view model already under construction. Start lists as a mutable `ArrayList`, as `Person` does for `visits` and `tags`. After the view loads, the model's list attribute is the `ObservableList` the view is watching. Add and remove on that list. The view model writes that observable list back through the setter when the setter's parameter type can accept it. A `List` setter does. A setter typed as `ArrayList` or `Set` does not, and the property then diverges from the field. After the view model exists, it is the writer. A property change calls the setter. A later call to the entity setter leaves the control on the value the view model holds. `DeskLoadsTest.entitySetterLeavesTheControls` sets the person's name to `Ignored` and the name field stays `Ada Lovelace`. The next edit of the field writes through the view model and replaces that name. ## Load a view ```java public static LoadedView load(URL fxml, Object model, ApplicationContext context) ``` The load reads `fx:controller` from the document first, so it can see `@ViewModelClass`, and builds or reuses the view model. It then loads the FXML. The container supplies the controller. Components come from the container as well. The root must be a `Parent`. Bindings run after `@FXML` fields are filled. Tables under the root open last, including a table inside a scroll pane. `LoadedView.getRoot()` is the scene graph. `getViewModel()` returns the view model when it is a `ViewModel`, which includes a subclass such as `PersonViewModel`. A replacement that does not extend `ViewModel` makes `getViewModel()` throw `IllegalStateException` with the message `View is bound to` and the class name. Use `property(String)` in that case. The desk's view model is a subclass, so `getViewModel()` returns it. `LoadedView.property(String)` returns the JavaFX property for an attribute name, for `latestMessage`, or for a `nameProperty()` method on the view model. A missing name returns null. A binding whose path does not resolve fails the load. FXML that cannot be read becomes `IllegalStateException` `Could not load view` and the URL. A missing controller class is `IllegalArgumentException` `No controller` and the name. `DeskLoadsTest.secondViewReusesTheViewModel` loads a second document, `NameLine.fxml`, for the same person. The second document names no controller and no view model class. It reuses `PersonViewModel`. Typing in the desk's name field updates the second field, and the title becomes `titled-Grace`. Loading `Desk.fxml` a second time while the first desk is showing does not work: the home `AddressForm` is the cached singleton and already has children. See [Component](#component). ## Direct binding A direct binding names a control by `fx:id`, a JavaFX property of that control, a path, and a direction. The property is resolved as `propertyName + "Property"`, so `text` means `textProperty()`. An optional `converter` is a class name. ```xml ``` For `both` and `model`, the write happens when the JavaFX property changes, including each change of a text property. There is no separate save. `view` does not write the attribute. `BindingList` is the element that holds direct bindings, computed bindings, and repeats. Its default property is `elements`, so the children in FXML are the list. An unknown child fails the load with `Not a binding element:`. The children that apply are a direct binding, a computed binding, and a repeat. When the attribute value is already an `ObservableList`, the control property is set to that same list. Direction and converter are not consulted. The tag binding names `direction="view"` and still shares the list. See [A list of simple values](#a-list-of-simple-values). A binding that names no such control fails with `No control "` and the id. A missing `xxxProperty()` method fails with `No property` and the property name. A method that returns something other than a `Property` fails with `did not return a property`. ## Directions | Direction | What it does | | --- | --- | | `both` | Shows the attribute and writes it when the control changes. | | `view` | Shows the attribute and follows later changes of the JavaFX property. Edits to the control stay on the control. | | `model` | Shows the current value, writes the attribute when the control changes, and leaves the control where it is when the attribute changes later. | The desk puts all three on `name`. The name field is `both`. Shown name (`nameEcho`) is `view`: typing there leaves `Person.name` and the name field unchanged, and typing in the name field updates the echo. Name draft (`nameDraft`) is `model`: it opens as `Ada Lovelace`, stays there while the name field changes, and typing in the draft writes `Person.name` and updates the name field and the echo. Always specify `direction`: it has no default. A non-null value other than `view`, `model`, or `both` fails the load with `Not a direction:`. Omitting it can currently fail with a `NullPointerException` during binding application. The ObservableList shortcut described above bypasses direction handling; do not rely on that shortcut to omit the declaration. For a complex last segment the control receives the model instance behind the view model, and the same three directions apply to later changes of that attribute. The home form's `model` property is `home` in both directions, so the form edits that address. A later change of the ViewModel's `home` property updates the Component's Model property, but its internal bindings do not reattach; create a new Component to edit another Model. The desk sets the home address once. ## Paths A path is attribute names separated by `.`. One name is an attribute of this view's view model, or `latestMessage`, or a `nameProperty()` on the view model. Earlier names must be complex attributes. The direction applies to the last name. `home.street` on the home street label steps through the home view model, and `view` applies to `street`. When a complex attribute along the path is null, that binding is left unapplied and both sides keep their current values. The load still succeeds. The path is resolved when the binding is applied. A binding whose last segment is complex follows later changes of that attribute. A longer path stays with the child view model that was in place when the binding was applied. The home street label is `home.street`. Replacing the ViewModel's `home` property later would leave that label on the street it was bound to. Calling the bean setter directly would not notify either binding. A list is not indexed in a path. Repeat a component, place a table, or hand a list of simple values to a control. A path that is not a read-write attribute fails the load with `No read-write attribute for path "` and the path. `Object.getClass` is not an attribute, and a getter without a setter is not one either. ## Converter A converter translates one direct binding between the attribute value and the control value. The container creates the class and injects its services. The class must implement `de.neitzel.fx.view.Converter`. Otherwise the load fails with `Not a converter:` or `No converter`. `toControlValue` runs toward the view, including the value shown at load. `toAttributeValue` runs only when the direction is `model` or `both`. A toward-view binding does not call it, and the default method throws `UnsupportedOperationException`. The age field is the desk's `both` converter. `AgeConverter` takes `AgeSuffix` in its constructor. `AgeSuffix` is `@Named` and `prefix()` returns `years `. Age 36 shows as `years 36`. Typing `years 41` sets the age to 41. ```xml ``` `StreetConverter` is the other shape: a no-arg constructor and a private `@Inject StreetPolicy` field. `toAttributeValue` trims the text and throws `IllegalArgumentException` with `Street is required` when the result is empty. The home form and each visit form use it on `street`. `VisitedOnConverter` turns a `LocalDate` into ISO text in the table, and `LocalDate.parse` back. An empty field writes null. A converter is not applied to a complex attribute or to an `ObservableList`. Those bindings assign the model instance or the list. When a conversion throws, the other side keeps its previous value. The control keeps the text that was typed, because the failed write does not replace it. The view model that owns the last path segment records one error message. The source is that segment's name. The text is the exception message, or `Conversion failed` when the exception message is null or empty. A later failure for that same Attribute updates the message in place. A later success removes it. On the desk, `nope` in the age field leaves the age at 41, leaves the field text `nope`, and sets `latestMessage` to an error whose source is `age`. Typing `years 41` again clears it. A blank home street leaves the street as it was and records source `home.street` on the person, because the failure is owned by the address and carried onto the parent. The notice label shows `Street is required`. A later real street clears that notice. See [Messages](#messages). ## Computed binding A computed binding names a public no-argument method on the controller. The method value in FXML is `#name`. A leading `#` is stripped. A value without `#` is used as the method name. The method returns the value written to the control. It may read the controller's fields and the view model. It does not write the model. ```xml ``` ```java public String greeting() { String name = nameField.getText(); Object age = viewModel.property("age").getValue(); if (name == null || name.isBlank()) { return "Hello (" + age + ")"; } return "Hello, " + name.trim() + " (" + age + ")"; } ``` The library calls `greeting` when the binding is applied, and again when a JavaFX property changes on a `Control` in this view, other than the target property, or when an attribute property on this view's `ViewModel` changes. Typing the name updates the greeting because the name field changed. Typing the age updates it because the age attribute changed. Writing the greeting label does not call `greeting` again. Nodes that are not controls are not watched, and controls added after binding setup are not automatically subscribed. Computed methods should be fast and free of side effects; this is broad notification-based refresh, not dependency analysis. A replacement view model that does not extend `ViewModel` is not watched. A missing method fails the load with `No computed method "` and the method value. No controller fails with `No controller for`. A method that throws fails with `Could not call`. An `onAction="#method"` handler is ordinary FXML. `addVisit`, `addTag`, `leaveNote`, `leaveWarning`, and `leaveFatal` are handlers. They are separate from the computed binding. ## Controller Name the class on the root with `fx:controller`. The container creates it with `getComponent`, so it is the cached singleton. `@FXML` fields are filled during load, before the bindings run. Leave `fx:controller` off and the library supplies a controller, which is enough for a view that has only direct bindings. `NameLine.fxml` in the test is that shape. You cannot obtain the supplied controller from `LoadedView`. Implement `HasViewModel` when your code needs the view model. The library calls `setViewModel` after construction and before the bindings run, and only when the selected object is a `ViewModel`. The desk stores it and reads `visits`, `tags`, `name`, and `age` from it. A second view whose document names the same controller class receives that same singleton. FXML then injects the second view's fields into it. The desk is one window, so its controller fields stay on that window. Give a second screen its own controller class when each screen needs its own `@FXML` fields. ## Choosing the ViewModel class With no annotation, the library uses a registered generated default when one applies, otherwise it builds `ViewModel`. See the [generated runtime contract](docs/generated-viewmodels.md). Put `@ViewModelClass` on the controller or the component to name a subclass, or another object that exposes JavaFX properties by name. The class needs a constructor that takes the model instance. If several one-argument constructors match, the most specific parameter type wins. The library calls that constructor. The container does not create the view model. ```java @Singleton @ViewModelClass(PersonViewModel.class) public class DeskController implements HasViewModel { } ``` ```java public class PersonViewModel extends ViewModel { private final StringProperty title = new SimpleStringProperty(this, "title"); public PersonViewModel(Object model) { super(model); title.set("titled-" + ((Person) model).getName()); property("name").addListener((observable, oldValue, newValue) -> title.set("titled-" + newValue)); } public StringProperty titleProperty() { return title; } } ``` The desk binds `titleLabel` to path `title`, direction `view`. The label shows `titled-Ada Lovelace`, and it follows the name because the subclass listens to the name property. A binding addresses `titleProperty` by the name `title`. The first view of an instance fixes the class. A later view requesting a class that the existing ViewModel does not satisfy fails the load with `ViewModel is ` the existing class ` but the View names ` the requested class, and the first view stays as it was. A compatible superclass/interface request reuses the existing instance. A later view with no annotation also reuses the view model. That is what `NameLine.fxml` does. No matching constructor fails with `No constructor taking the Model on` and the class name. A component's `@ViewModelClass` applies to that component's model, not to the parent. The library builds that class before the path is read, so a generic view model is not fixed first. An earlier binding in the same list that already resolved a path into that instance would have fixed the class, and an incompatible annotation would then fail the load. The desk does not do that. The person's class is fixed from the controller before the home form's model is set, and `AddressForm` names no view model class, so the address uses `ViewModel`. ## Component Component elements accept writable FXML properties, including custom attributes and inherited layout properties such as `prefWidth`. The container constructs the Component first; its own FXML and `initialize()` run before the containing document's property values are applied. JavaFX performs the normal property conversion and rejects unknown properties or invalid values. Object references such as `model="$address"` retain their identity and are assigned after the Component's model listener is installed. Static layout constraints such as `GridPane.rowIndex` remain handled by the containing FXMLLoader. A component is a class the container can create. It extends `Parent` (usually a Pane subclass such as `VBox`), loads `ClassName.fxml` from its own package through `fx:root`, and is the controller of that file. It has no `fx:controller`. It exposes one JavaFX property named `model`. `AddressForm` edits the home address. `VisitForm` edits one visit in the repeated list. `NoteForm` edits a note. `VisitRow` edits one table row. Each is `@Singleton`, extends `VBox` or `HBox`, and has `modelProperty()`. The home document binds the form and sets `model` from the path. When the path's last segment is complex, the value assigned is the model instance behind that view model. ```xml ``` The component's own bindings run once, the first time `model` becomes non-null. A later change of `model` does not apply them again. Set the model once. To show another instance, load another component. `initialize` runs during the component's FXML load, before `model` is set. `AddressForm` uses that moment to copy the injected heading onto its label, which does not need the address. For a Component implementing `HasViewModel`, `setViewModel` runs after `model` is set to a non-null Model and before its bindings, but only when the selected presentation object extends `ViewModel`. An arbitrary replacement does not receive this callback. `AddressForm` implements `HasViewModel` and writes `Opened as ` plus the city at that moment. Editing the city afterward leaves the label on the city the form opened with. The sample home opens as `Opened as Sample City`. The first component of a class in one load is `context.getComponent`, the cached singleton. Each further one in that same load is `context.createComponent`. Repeat and table rows always use `createComponent`, so they are new instances and the cached singleton of that class stays unused by the rows. `getComponent(VisitForm.class)` after the desk loads is still the singleton, and the three forms in the visit pane are other instances. A later load starts over. Its first component of a class is the cached singleton again. The home `AddressForm` is that singleton and is already in the scene, so loading `Desk.fxml` a second time fails while building the home form: a node cannot be added to a second parent. The test's second view is a document with no component. A component class needs `ClassName.fxml` beside it. A missing document fails with `No component document`. A missing `modelProperty` method fails with `No model property on`. A method that does not return a `Property` fails with `modelProperty did not return a property`. ## Nested component A nested component is the same mechanism one level down. The child document names the next class as an element and binds its `model`. `AddressForm.fxml` binds `noteForm` to path `note` and contains ``. `NoteForm.fxml` binds its text field to path `text`. The sample home note is `Kept at home`. Editing the note field writes `text` on the existing `Note` held by `Address.note`; it does not replace that Note object. `VisitForm` does not contain `NoteForm`. A visit added after the desk has loaded is built in a new load. The first component of a class in that load is the cached singleton again, so a `NoteForm` nested in the repeated form would be the instance already showing under the home address. Keeping the note on the home form, which is loaded once, leaves that singleton where it was placed. A null `note` leaves the nested binding unapplied. `NoteForm` stays empty and the load succeeds. The sample home has a note, so its bindings run. ## Repeat A `Repeat` element in the same binding list names a pane, a component class, and a collection attribute. The library places one new component in that pane for each model instance, in list order. Adding or removing an element updates the pane. A permutation or an extractor update notification does not. Replacing an element with `set(index, newValue)` produces remove/add notifications and is handled. ```xml ``` Each child is a new injected instance. Its `model` is the element itself. The desk's Add visit button appends to the list on the view model, which is the list stored on the person: ```java @SuppressWarnings("unchecked") private List
visits() { return (List
) viewModel.property("visits").getValue(); } ``` A missing pane, or a node that is not a `Pane`, fails with `No pane "`. A collection that is missing or is not an `ObservableList` fails with `No collection "`. A null list attribute is not an observable list. The desk starts `visits` as an `ArrayList`, which the view model wraps. An unknown component class fails with `No component`. ## Selection Selection is a separate attribute of the parent model. Null means nothing is selected. The repeated component and the table row do not store it. The desk uses one `selectedVisit` for the combo box and the table: ```xml ``` `Address.toString` is the combo box text, `street, city`. The items binding hits the list shortcut, so the combo box holds the same visit list. A primary click on a row, including a click in a field, sets the selection to that element. A second click on the row leaves it selected. A click on the blank body clears it. Removing the selected element clears it. The selected row has style class `selected`. The sample desk opens with the first visit selected. ## A list of simple values A list of strings, numbers, or other simple values is one shared `ObservableList`. A direct binding sets the control property to that list, so an add or a remove is visible on both sides. ```xml ``` The sample tags are `dns` and `math`. Add tag appends the field's text to the list on the view model and clears the field. Adding to `person.getTags()` after the load grows the list view, because that list is the one the view is watching. A list of model instances is the same kind of observable list of instances. The visit combo box is that list. It is not a table. ## Table A table shows a collection of model instances with column headers. Each element keeps one row, a component, for as long as it stays in the collection. The row binds its own fields. Filter and sort use the column attribute. They do not reorder the collection. ```xml
``` `Table` is a `VBox`. Its default property is `columns`. `VisitRow` is an `fx:root` whose first children are the fields, in column order, and whose last child is Remove. The table sets the width of the first children, one per column. It does not check that child i is bound to column i. Controls after them keep their own size, so Remove is not a column and keeps `USE_COMPUTED_SIZE`. Spacing on the row is repeated between the header cells. ```xml