-
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. You can access the root node created from FXML directly in init
.
class HelloWorld : View() {
override val root: HBox by fxml()
@FXML lateinit var myLabel: Label
init {
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
All views have a title property. The title property of the primary view is automatically bound to
the primary stage. The same goes for Fragments - if you open a Fragment in a modal using the openModal()
function, the title of the modal is bound to the Fragment title.
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