Skip to content

MVCfx Pattern

Jarek Sacha edited this page Dec 16, 2021 · 2 revisions

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.

Example - StopWatch App

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

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>

StopWatchController

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).

Scala 3 version of StopWatchController

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:

  1. 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
  2. It is also important to declare startButton as an uninitialized variable (var not val). 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
  3. 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"

Scala 2 version of StopWatchController

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"
}

StopWatchModel

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
      }
    }
  }
} 

StopWatch MVCfx

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.

Scala 3 version of StopWatch MVCfx

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.

Scala 2 version of StopWatch MVCfx

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.

StopWatchApp

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.