-
Notifications
You must be signed in to change notification settings - Fork 0
MVCfx Pattern
Package org.scalafx.extras.mvcfx
helps in implementation of Model-View-Controller-like patters, we call it the MVCfx
Pattern. The pattern is built around use of views defined in FXML (the view) with binding to ScalaFX for the
controller and the model.
There are two cooperating classes ControllerFX
for binding FXML to Scala code and ModelFX
that contains logic for
the component. An additional helper MVCfx
class is provided to correctly instantiate the ControllerFX
and the
corresponding ModelFX
.
The layout of the UI component is defined in a standard JavaFX FXML file. The Scala side of the FXML is in a
class ControllerFX
. ControllerFX
controls corresponding to FXML are automatically instantiated by the FXML loader.
The component logic is represented by the ModelFX
. The MVCfx
class is used to simplify instantiation of
the ControllerFX
and the ModelFX
.
Note, there are some slight differences how the pattern is used in Scala 3 and Scala 2.
We will show use of the MVCfx Pattern using an example of a StopWatch app.
The app has a single screen with a stopwatch. It displays the elapsed time and has buttons to start, stop, and reset the stopwatch.
The main pane of the app is implemented using the MVCfx Pattern. The pane implementation has 4 pieces:
- StopWatch.fxml - the FXML declaration of the view layout and styling of the elapsed time text
-
StopWatchController - implements
ControllerFX
-
StopWatchModel - implements
ModelFX
-
StopWatch - implements
MVCfx
loader
First, we have the FXML definitions representing the structure of the user interface StopWatch.fxml. It defined position and font of the elapsed time text and positions of the buttons. It can be graphically edited using tools like Scene Builder or IntelliJ IDEA build-in FXML editor.
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.text.*?>
<BorderPane xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="org.scalafx.extras.mvcfx.stopwatch.StopWatchController">
<padding>
<Insets bottom="7.0" left="7.0" right="7.0" top="7.0"/>
</padding>
<bottom>
<ButtonBar BorderPane.alignment="CENTER">
<buttons>
<Button fx:id="startButton" mnemonicParsing="false" text="Start"/>
<Button fx:id="stopButton" mnemonicParsing="false" text="Stop"/>
<Button fx:id="resetButton" mnemonicParsing="false" text="Reset"/>
</buttons>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
</padding>
</ButtonBar>
</bottom>
<center>
<HBox>
<children>
<Label fx:id="minutesLabel" alignment="CENTER" contentDisplay="CENTER" text="99" textAlignment="CENTER"
BorderPane.alignment="CENTER">
<font>
<Font name="Lucida Sans Typewriter Regular" size="96.0"/>
</font>
</Label>
<Label text=":">
<font>
<Font name="Lucida Sans Typewriter Regular" size="96.0"/>
</font>
</Label>
<Label fx:id="secondsLabel" alignment="CENTER" contentDisplay="CENTER" text="99" textAlignment="CENTER">
<font>
<Font name="Lucida Sans Typewriter Regular" size="96.0"/>
</font>
</Label>
<Label text=".">
<font>
<Font name="Lucida Sans Typewriter Regular" size="96.0"/>
</font>
</Label>
<Label fx:id="fractionLabel" alignment="CENTER" contentDisplay="CENTER" text="99"
textAlignment="CENTER">
<font>
<Font name="Lucida Sans Typewriter Regular" size="96.0"/>
</font>
</Label>
</children>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
</padding>
</HBox>
</center>
</BorderPane>
While FXML provides static layout of a component, the Controller defines how it will behave. Typically, the Controller will declare objects for controls of the UI that needs some custom behavior, like a button. The Controller typically delegates to the Model the details of the behavior.
The ControllerFX
contains references to selected controls defined in the FXML. It connects those controls to event
handlers and logic contained in an instance of ModelFX
. The Controller declares which references it needs using names
corresponding to fx:id
used in the FXML. For instance, for the fx:id="startButton"
from FXML it will declare
a Button
named startButton
in the Controller.
To illustrate, let's focus for moment only on the startButton
and ignore other controls. Here is a fragment
from StopWatch.fxml
that defines the button:
<Button fx:id="startButton" text="Start"/>
If we want to use that button in the Controller, we need to define variable with name corresponding to fx:id
correct
type (Button
).
In Scala 3 we need to use JavaFX types, so the full type will be javafx.scene.control.Button
. That variable needs to
be annotated with JavaFX @FXML
so JavaFX FXML Loader can match it to FXML. The declaration part for startButton
will
look something like this:
import javafx.scene.control as jfxsc
import javafx.fxml as jfxf
//...
@jfxf.FXML
private var startButton: jfxsc.Button = _
There are a couple of things to point out:
- In ScalaFX code it is customary to prefix JavaFX types, so they do not conflict with similarly named ScalaFX types
and to make it clear that those are JavaFX types, for instance
jfxsc.Button
- It is also important to declare
startButton
as an uninitialized variable (var
notval
). The Controlled is instantiated during loading of the associated FXML code. The FXML loader looks for variables annotated with@FXML
and injects their correct values based in FXML declarations - Any code that is performing initialization using the injected variables should not be done in the constructor. The
FXML variables are not initiated yet when the constructor of the Controller ia called. That code should be in a
method called
initialize()
. That method is automatically called after the FXML loader completes instantiation of the variables
Here is a more complete fragment of the controller implementation related to startButton
:
import javafx.scene.control as jfxsc
import javafx.fxml as jfxf
import org.scalafx.extras.mvcfx.ControllerFX
import scalafx.Includes.*
class StopWatchController(model: StopWatchModel) extends ControllerFX:
@jfxf.FXML
private var startButton: jfxsc.Button = _
// ...
override def initialize(): Unit =
startButton.disable <== model.running
startButton.onAction = () => model.onStart()
// ...
Note the method initialize()
, we use it to define startButton
behaviour. We bind the disable
property to model
'
s running
property. We also assign event handling to onAction
. At this point we can use all the ScalaFX goodness, is
enabled by import scalafx.Includes.*
, we can now treat startButton
as any ScalaFX object.
Here are the details of the Scala 3 implementation of StopWatchController:
import javafx.scene.control as jfxsc
import javafx.fxml as jfxf
import org.scalafx.extras.mvcfx.ControllerFX
import scalafx.Includes.*
class StopWatchController(model: StopWatchModel) extends ControllerFX:
@jfxf.FXML
private var minutesLabel: jfxsc.Label = _
@jfxf.FXML
private var secondsLabel: jfxsc.Label = _
@jfxf.FXML
private var fractionLabel: jfxsc.Label = _
@jfxf.FXML
private var startButton: jfxsc.Button = _
@jfxf.FXML
private var stopButton: jfxsc.Button = _
@jfxf.FXML
private var resetButton: jfxsc.Button = _
override def initialize(): Unit =
minutesLabel.text.value = format2d(model.minutes.longValue)
model.minutes.onChange { (_, _, v) =>
minutesLabel.text.value = format2d(v.longValue)
}
secondsLabel.text.value = format2d(model.seconds.longValue())
model.seconds.onChange { (_, _, v) =>
secondsLabel.text.value = format2d(v.longValue())
}
fractionLabel.text.value = format2d(model.secondFraction.longValue() / 10)
model.secondFraction.onChange { (_, _, v) =>
fractionLabel.text.value = format2d(v.longValue() / 10)
}
startButton.disable <== model.running
stopButton.disable <== !model.running
resetButton.disable <== model.running
startButton.onAction = () => model.onStart()
stopButton.onAction = () => model.onStop()
resetButton.onAction = () => model.onReset()
private def format2d(t: Number) = f"${t.longValue()}%02d"
In Scala 2 we can use ScalaFXML help avoid annotating every variable and use ScalaFX types directly. We can use
single @sfxml
annotation for the StopWatchController
class and define variables as constructor arguments.
Initialization is done in the constructor. Here is a code related to stopButton
in Scala 2 version:
import org.scalafx.extras.mvcfx.ControllerFX
import scalafx.Includes._
import scalafx.scene.control.{Button, Label}
import scalafxml.core.macros.sfxml
@sfxml
class StopWatchController( //..
stopButton: Button,
model : StopWatchModel) extends ControllerFX {
startButton.disable <== model.running
startButton.onAction = () => model.onStart()
}
And the full code implementing Scala 2 version of StopWatchController
:
import org.scalafx.extras.mvcfx.ControllerFX
import scalafx.Includes._
import scalafx.scene.control.{Button, Label}
@sfxml
class StopWatchController(
minutesLabel : Label,
secondsLabel : Label,
fractionLabel: Label,
startButton : Button,
stopButton : Button,
resetButton : Button,
model : StopWatchModel
) extends ControllerFX {
minutesLabel.text.value = format2d(model.minutes.longValue)
model.minutes.onChange { (_, _, newValue) =>
minutesLabel.text.value = format2d(newValue.longValue)
}
secondsLabel.text.value = format2d(model.seconds.longValue())
model.seconds.onChange { (_, _, newValue) =>
secondsLabel.text.value = format2d(newValue.longValue())
}
fractionLabel.text.value = format2d(model.secondFraction.longValue() / 10)
model.secondFraction.onChange { (_, _, newValue) =>
fractionLabel.text.value = format2d(newValue.longValue() / 10)
}
startButton.disable <== model.running
stopButton.disable <== !model.running
resetButton.disable <== model.running
startButton.onAction = () => model.onStart()
stopButton.onAction = () => model.onStop()
resetButton.onAction = () => model.onReset()
private def format2d(t: Number) = f"${t.longValue()}%02d"
}
The ModelFX
implements the logic of the stopwatch operation. Notice that there are no direct references to UI
controls. The connection to the UI is through the properties (like minutes
). The Model is not aware how the Controller
is implemented. As the Model does not depend on ScalaFXML - it can be implemented the same in Scala 2 and Scala 3:
import javafx.concurrent as jfxc
import org.scalafx.extras.*
import org.scalafx.extras.mvcfx.ModelFX
import scalafx.Includes.*
import scalafx.beans.property.{LongProperty, ReadOnlyBooleanProperty, ReadOnlyBooleanWrapper}
class StopWatchModel extends ModelFX {
private val _running = ReadOnlyBooleanWrapper(false)
val running: ReadOnlyBooleanProperty = _running.readOnlyProperty
private val counterService = new CounterService()
counterService.period = 10.ms
val minutes = new LongProperty()
val seconds = new LongProperty()
val secondFraction = new LongProperty()
counterService.elapsedTime.onChange { (_, _, newValue) =>
val t = newValue.longValue()
secondFraction.value = t % 1000
seconds.value = (t / 1000) % 60
minutes.value = t / 1000 / 60
}
def onStart(): Unit = {
counterService.doResume()
_running.value = true
}
def onStop(): Unit = {
counterService.doPause()
_running.value = false
}
def onReset(): Unit = {
counterService.doReset()
}
private class CounterService extends jfxc.ScheduledService[Long] {
private var timeAccumulator: Long = 0
private var restartTime : Long = 0
val elapsedTime = new LongProperty()
override def createTask(): jfxc.Task[Long] = {
new jfxc.Task[Long]() {
override protected def call(): Long = {
val ct = System.currentTimeMillis()
val et = timeAccumulator + (ct - restartTime)
onFX {
elapsedTime.value = et
}
et
}
}
}
def doPause(): Unit = {
val ct = System.currentTimeMillis()
timeAccumulator += (ct - restartTime)
onFX {
elapsedTime.value = timeAccumulator
}
this.cancel()
}
def doResume(): Unit = {
restartTime = System.currentTimeMillis()
this.restart()
}
def doReset(): Unit = {
timeAccumulator = 0
onFX {
elapsedTime.value = 0
}
}
}
}
An instance of MVCfx
will give as access to the model and the view. The model can be integrated with the logic of the
application. The view can be integrated with other UI components. The MVCfx
implementation is typically simple, it
needs instance of the model and information about location of the FXML resource.
import org.scalafx.extras.mvcfx.MVCfx
class StopWatch(val model: StopWatchModel = new StopWatchModel())
extends MVCfx[StopWatchController]("StopWatch.fxml") {
def controllerInstance: StopWatchController = new StopWatchController(model)
}
The method controllerInstance
creates an instance of the controller, we can pass any needed arguments to the
controller, here we only pass an instance of the Model.
The Scala 2 version relying on the ScalaFXML help can automatically discover how to create the controller.
import org.scalafx.extras.mvcfx.MVCfx
class StopWatch(val model: StopWatchModel = new StopWatchModel())
extends MVCfx("StopWatch.fxml")
The model
is passed as the last argument of the controller constructor. It is possible to pass additional argument to
the controller's constructor through injection. Consult ScalaFXML documentation for details.
We used the MVCfx Pattern to create a pane for the stopwatch. In the application code we only need access to the view
created by the StopWatch
class:
new StopWatch().view
WE simply add the View to the main pane of the application. Here we use the BorderPane
and we put the StopWatch
view
in the center:
import scalafx.application.JFXApp3
import scalafx.scene.Scene
import scalafx.scene.layout.BorderPane
object StopWatchApp extends JFXApp3 {
override def start(): Unit = {
stage = new JFXApp3.PrimaryStage {
title = "StopWatch"
scene = new Scene {
root = new BorderPane {
center = new StopWatch().view
}
}
}
}
}
Full implementation of the StopWatch demo is in the scalafx-extras-demos project.