-
Notifications
You must be signed in to change notification settings - Fork 272
Components
Wiki ▸ Documentation ▸ Components
The visual parts of a Tornado FX application is comprised of UI Components called View
and Fragment
. They behave exactly the same with one crucial difference: View
is a singleton, so there will be only one instance of any given View
in your application, while Fragment
behaves like a prototype object, meaning that a new instance will be created every time you look one up.
Note: For all other purposes they are the same, so for brevity we will simply refer to UI Components as views from now on.
A View
will contain your view controller logic, as well as the actual hierarchy of Java FX nodes that comprises the user interface. You can choose to build your UI with Kotlin or FXML.
class HelloWorld : View() {
override val root = HBox(Label("Hello world"))
init {
root += Label("Another label")
}
}
A Simple View with an
HBox
root node and additional nodes added in theinit
block
Kotlin views can also utilize the [type safe builders](Type Safe Builders) that comes with Tornado FX.
with(root) {
hbox {
label("Name") {
addClass("fieldLabel")
}
textfield {
promptText = "Enter your name"
}
}
}
Additional nodes added using the [Type Safe Builders](type safe builder) pattern
Instead of building your UI in Kotlin directly, you can also pull in the root node from an FXML file with the same name as the view. When loading from FXML you can also optionally implement the Initializable
interface to do some post processing of the node hierarchy.
class HelloWorld : View() : Initializable {
override val root: HBox by fxml()
@FXML lateinit var myLabel: Label
fun initialize(location: URL, resources: ResourceBundle) {
myLabel.text = "Hello world"
}
}
The view is loaded from FXML and the Label is injected with the @FXML annotation.
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<HBox prefHeight="200" xmlns:fx="http://javafx.com/fxml/1">
<Label fx:id="myLabel" />
</HBox>
HelloWorld.fxml
Caveat: You cannot access the root node inside the initialize
function, as the actual node is not returned from the FXMLLoader at that point. If you need to access the actual root node, either inject it by placing an fx:id
attribute on it, or simply wrap the access code in a Platform.runLater { // access root here }
block. runLater
will always run after the fxml is loaded, so it is a good alternative to implementing Initializable
A View can also contain other views. You do this by adding the root node of a subview somewhere in the node hierarchy of the master view. The views themselves are not automatically linked, but you easily add a reference property to them if you need to. When you embed views, you can either look them up via the find
method, or inject them in the parent view.
class MasterView : View() {
override val root = BorderPane()
val detail: DetailView by inject()
init {
// Enable communication between the views
detail.master = this
// Assign the DetailView root node to the center property of the BorderPane
root.center = detail.root
// Find the HeaderView and assign it to the BorderPane top (alternative approach)
root.top = find(HeaderView::class)
}
}
A Master view with two embedded views. The DetailView has access to the MasterView via the master property.
It is important to note that the master
property of DetailView is not a framework feature - it is simply a property you might add to enable communication between views. You can alternatively communicate with events if you don't like the hard coupling between views.
When you add a view as a child node of another Pane
, you can use this shorthand syntax to extract the root node:
override val root = HBox()
val subview: MySubView by inject()
init {
root += subview
}
A subview added using the shorthand syntax to avoid refering to the actual root node inside the view
Note that the +=
syntax can be used to add both views and arbitrary nodes to any Pane
that can contain child nodes. It is actually just an extension function that basically just does pane.children.add(node)
for you.
When a user interface will only be used in one place at a time, a View is the better choice. For popups or other short lived objects, you might consider Fragments
instead. A complex view might contain both other Views and Fragments
. The Fragment
class also has a convenient openModal
and a corresponding closeModal
function that will open the fragment in a modal window. The openModal
function takes optional parameters to configure stageStyle
and modality
plus other options.
Business logic is contained in a Controller
. All controllers are singletons, and can be injected into both other controllers and views.
Note: From now on, components refers to any "Controller
, View
or Fragment
". They all extend the Component
base class
Controllers might perform long running tasks, and should not run on the Java FX UI thread. Calling code on the right thread can be tedius and error prone, but Tornado FX does all the heavy lifting, leaving you to focus on your business and view logic.
The examples below will use the included Rest
controller. Please see the [Rest Client](Rest Client Documentation) for further details.
The framework adds no restrictions or assumptions as to how you use your controllers. They are simply singleton objects that you can access from other controllers and views. However, some patterns have proven extremely useful, so we'll present them here.
class CustomerController : Controller() {
val api : Rest by inject()
fun listCustomers(): ObservableList<Customer> =
api.get("customers").list().toModel()
}
Controller that can load a JSON list of customers and convert them to a
Customer
model object
To access this controller from a view, you can inject it or look it up with the find
function. The listCustomers
function might take a long time to perform, and should not run on the JavaFX UI Thread. You need to run the call itself in a background thread, and update the UI on the JavaFX UI Thread when the call completes. This can easily be achived with the background
helper function:
background {
customerController.listCustomers()
} ui {
customerTable.items = it
}
See Async Task Execution for more information.
Next: Dependency Injection