November 11, 2018
When building windows in TornadoFX, developers choose between two superclasses: View and Fragment. A View is a singleton object, meaning that there will be one and only one object per class in the application. This is contrasted with a Fragment. There can be multiple Fragment objects in an application.
There is a feature in TorandoFX called Scopes which can partition an application so that there can be more than one instance of a View. That feature is not covered here.
This video shows an app that displays Views and Fragments using several mechanisms. These mechanisms are
The video shows a View opened via a Menubar item and then a Button. A TextField on the main screen provides a data displayed by the View. Multiple attempts to display the already-showing View do not bring up additional windows. Next, multiple Fragment-based windows are displayed. These windows get their data from a different TextField. Changing the data for the Fragments and bringing up additional Fragments produces screens with different data.
The View has bound its control to a shared field such that updating the field on the main screen also updates the value shown on the View.
For the purpose of this article, Views are singletons. There will be one and only one object for a given View subclass. This presents an opportunity for Context Dependency Injection (CDI). Controllers are also singleton. With only one instance of each (View and Controller), it's easy for TornadoFX's inject() to find and wire up the instances.
This class diagram shows the arrangement of the View classes. ViewAndFragDemoView is the main view and is shown automatically by the App subclass ViewandFragDemo. MyView is the demo of a View being shown in response to a user event. MyController contains globally-accessible data dataForView
that will be set through binding.
There is a class MyDispatcher that mediates the interaction between action initiator (button press, menubar, keyboard shortcut) and the target (MyView). This class -- globally aware since it itself is a Controller subclass -- registers for the windowing events. When one of the events is received by MyDispatcher, the object will use the find() method to locate the single MyView object instance. It then shows the window.
This following object diagram shows the relationship between the main view, Controllers, and MyView. The main view and MyView have access to a shared data structure, the dataForViews
property. Hence, MyDispatcher only needs to receive an un-parameterized event to trigger the window open command.
The following code snippet shows the View and Controller subclasses "MyView" and "MyController".
class MyView : View("My View") {
val controller : MyController by inject()
override val root = vbox {
label(controller.dataForView)
prefWidth = 568.0
prefHeight = 320.0
}
}
class MyController : Controller() {
val dataForView = SimpleStringProperty()
}
MyView has access to the JavaFX Property dataForView
which is the shared MyController field. This field of MyController is bound to a TextField on the main screen. The field is also bound to a MyView Label. The main screen binding is shown in the textfield(controller.dataforView)
line listed below.
class ViewAndFragsDemoView : View("View and Fragments Demo") {
val controller : MyController by inject()
private val dataForFrags = SimpleStringProperty()
override val root = vbox {
menubar {
menu("Windows") {
item("View") {
action { openViewWindow() }
}
item("Fragment") {
action { openFragWindow() }
}
}
}
vbox {
label("Opens the Singleton MyView")
textfield(controller.dataForView)
hbox {
button("Open") {
action { openViewWindow() }
}
label("Shortcut: F1")
alignment = Pos.CENTER_LEFT
spacing = 4.0
}
separator()
label("Opens a New MyFragment")
textfield(dataForFrags)
hbox {
button("Open") {
action { openFragWindow() }
}
label("Shortcut: F2")
alignment = Pos.CENTER_LEFT
spacing = 4.0
}
prefHeight = 320.0
prefWidth = 480.0
padding = Insets(10.0)
spacing = 4.0
}
}
init {
find<MyDispatcher>()
}
override fun onDock() {
primaryStage.scene.addEventFilter(KeyEvent.KEY_PRESSED) {
when( it.code ) {
KeyCode.F1 -> {
openViewWindow()
it.consume()
}
KeyCode.F2 -> {
openFragWindow()
it.consume()
}
}
}
}
private fun openViewWindow() = fire(OpenWindowEvent(WINDOW_ID_VIEW, ""))
private fun openFragWindow() = fire(OpenWindowEvent(WINDOW_ID_FRAGMENT, dataForFrags.value ?: ""))
}
With the main screen and the View subclass sharing a field, the next code to analyze is the trigger that initiates the open. This is done indirectly through the Event Bus. Handlers are attached to the event sources - a Menubar item, a Button, and a keyboard shortcut. An event is posted using the fire() function. See the private function openViewWindow().
These lines are some constants used in the program and the FXEvent subclass.
const val WINDOW_ID_VIEW = "view"
const val WINDOW_ID_FRAGMENT = "fragment"
const val PARAMETER_DATA = "data"
class OpenWindowEvent(val windowId : String, val data : String) : FXEvent()
Rather than subscribing directly to the FXEvent, a centralized dispatcher is used to decouple the event delivery. This dispatcher class is another singleton Controller subclass. The dispatcher uses the TornadoFX find() method to lookup the only MyView object in the application. It then opens the window.
class MyDispatcher : Controller() {
init {
subscribe<OpenWindowEvent> {
if( it.windowId.equals(WINDOW_ID_VIEW) ) {
find<MyView>().openWindow()
} else if( it.windowId.equals(WINDOW_ID_FRAGMENT) ) {
val params = mutableMapOf( PARAMETER_DATA to it.data )
find<MyFrag>(params).openWindow()
}
}
}
}
While you can move the subscribe() call into the main class or to MyView, it's separated here to provide a single point for managing the openWindow() calls. This class can contain centralized code for runtime discovery of window modules, logging, and security.
This UML Object Diagram summarizes the View behavior captured in the video. The collaboration shows the singleton ViewAndFragDemoView object receiving TextField input and a Button press from the user. The TextField is bound to the singleton MyController object. The ViewAndFragDemoView object posts an OpenWindowEvent to the Event Bus. This prompts the singleton MyDispatcher object to open the MyView window. That window retrieves the singleton MyController dataForViews field and displays the data in MyView.
There can be many objects of a given Fragment subclass in an application. When you specify a Fragment as an argument to the find() function, you get back a new instance. The new instances can bind to globally-shared singleton objects as the View does. However, I prefer to pass parameters to the Fragment. This allows me to use the Fragment in different contexts.
Binding is still used in the Fragment example indirectly as a source for parameters. This information is retrieved by the caller, making the Fragment unlinked to any particular field. Another caller can choose to pass different data to the reusable Fragment. "New" versus "edit" scenarios come to mind. On action might display an empty Fragment (new) and another action might display a filled-in Fragment (edit).
This UML object diagram shows the interaction between the main view, Dispatcher, and Fragment.
The following code listing shows the MyFrag class.
class MyFrag : Fragment("My Fragment") {
val data = SimpleStringProperty()
override val root = vbox {
label(data)
prefWidth = 568.0
prefHeight = 320.0
}
override fun onDock() {
data.value = params["data"] as String
}
}
This code focuses on standalone Fragments displayed in separate windows. Fragments are also used in composition of complex Views. In that usage, the object instances are created and restricted to a particular View. If the Fragments are used in this limited manner, those Fragments can access shared singleton data like the View. If the Fragment is reusable -- other Views may create their own objects based on this Fragment -- then use the params / onDock() technique.
Bind your Views to shared Controller data. When you need reusable UI parts, use a Fragment. Unless completely tied to a particular View, the Fragment should use parameters (rather than shared data) so that the invocation and parameterization of the Fragment isn't restricted. For truly global data, say a global config value, feel free to inject that into a Fragment. There is an important extension to this article which is Scopes. That's a feature the perturbs the singleton characteristic mentioned throughout the article.
By Carl Walker
President and Principal Consultant of Bekwam, Inc