Skip to content

Latest commit

 

History

History
163 lines (110 loc) · 8.15 KB

Scopes.md

File metadata and controls

163 lines (110 loc) · 8.15 KB

Scopes

Scope is a simple construct that enables some interesting and helpful behavior in a TornadoFX application. A Scope can be viewed as the "context" with which the parent singleton Component and any possible children Components that may exist in the same context. Within that context, it is easy to pass around the subset of instances from one Component to another.

alttext

When you use inject() or find() to locate a Controller or a View, you will, by default, get back a singleton instance, meaning that wherever you locate that object in your code, you will get back the same instance. Scopes provide a way to make a View or Controller unique to a smaller subset of instances in your application.

Each Component, like View, Fragment and Controller, inherit whatever scope they were looked up in, so you normally don't need to mention the scope after looking up the "root" of your tree of elements.

It can also be used to run multiple versions of the same application inside the same JVM, for example with JPro, which exposes TornadoFX application in a web browser.

A Master/Detail example

In an MDI Application you can open an editor in a new window, and ensure that all the injected resources are unique to that window. We will leverage that technique to create a person editor that allows you to open a new window to edit each person.

We start by defining a table interface where you can double click to open the person editor in a separate window.

class PersonList : View("Person List") {
    val ctrl: PersonController by inject()

    override val root = tableview<Person>() {
        column("#", Person::idProperty)
        column("Name", Person::nameProperty)
        onUserSelect { editPerson(it) }
        asyncItems { ctrl.people() }
    }

    fun editPerson(person: Person) {
        val editScope = Scope()
        val model = PersonModel()
        model.item = person
        setInScope(model, editScope)
        find(PersonEditor::class, editScope).openWindow()
    }
}

The edit function creates a new Scope and injects a PersonModel configured with the selected user into that scope. Finally, it retrieves a PersonEditor in the context of the new scope and opens a new window. find allows for the ability to pass in scopes as a parameter easily between classes, so be sure not to forget this step! TornadoFX gives more insight on the ability for passing scopes in new instances of components:

fun <T: Component> find(componentType: Class<T>, scope: Scope = FX.defaultScope): T = 
     inline fun <reified T: Component> find(scope: Scope = FX.defaultScope): T = 
         find(T::class, scope)

When the PersonEditor is initialized, it will look up a PersonModel via injection. The default context for inject and find is always the scope that created the component, so it will look in the personScope we just created.

val model: PersonModel by inject()

Breaking Out of the Current Scope

When no scope is defined, injectable resources are looked up in the default scope. There is an item representing that scope called FX.defaultScope. In the above example, the editor might have called out to a PersonController to perform a save operation in a database or via a REST call. This PersonController is most probably stateless, so there is no need to create a separate controller for each edit window. To access the same controller in all editor windows, we supply the scope we want to find the controller in:

val controller: PersonController by inject(FX.defaultScope)

This effectively makes the PersonController a true singleton object again, with only a single instance in the whole application.

The default scope for new injected objects are always the current scope for the component that calls inject or find, and consequently all objects created in that injection run will belong to the supplied scope.

Keeping State in Scopes

In the previous example we used injection on a scope level to get a hold of our resources. It is also possible to subclass Scope and put arbitrary data in there. Each TornadoFX Component has a scope property that gives you access to that scope instance. You can even override it to provide the custom subclass so you don't need to cast it on every occasion:

override val scope = super.scope as PersonScope

Now whenever you access the scope property from your code, it will be of type PersonScope. It now contains a PersonModel that will only be available to this scope:

class PersonScope : Scope() {
    val model = PersonModel()
}

Let's change our previous example slightly to access the model inside the scope instead of using injection. First we change the editPerson function:

fun editPerson(person: Person) {
    val editScope = PersonScope()
    editScope.model.item = person
    find(PersonEditor::class, editScope).openWindow()
}

The custom scope already has an instance of PersonModel, so we just configure the item for that scope and open the editor. Now the editor can override the type of scope and access the model:

// Cast scope
override val scope = super.scope as PersonScope
// Extract our view model from the scope
val model = scope.model

Both approaches work equally well, but depending on your use case you might prefer one over the other.

Global application scope

As we hinted to initially, you can run multiple applications in the same JVM and keep them completely separate by using scopes. By default, JavaFX does not support multi tenancy, and can only start a single JavaFX application per JVM, but new technologies are emerging that leverages multitenancy and will even expose your JavaFX based applications to the web. One such technology is JPro.one, and TornadoFX supports multitenancy for JPro applications by leveraging scopes.

There is no special JPro classes in TornadoFX, but supporting JPro is very simple by leveranging scopes:

Using TornadoFX with JPro

JPro will create a new instance of your App class for each new web user. Also, to access the JPro WebAPI you need to get access to the stage created for each user. In this example we subclass Scope to create a special JProScope that contains the stage that was given to each application instance:

class JProScope(val stage: Stage) : Scope() {
    val webAPI: WebAPI get() = WebAPI.getWebAPI(stage)
}

The next step is to subclass JProApplication to define our entry point. This app class is in addition to our existing TornadoFX App class, which boots the actual application:

class Main : JProApplication() {
    val app = OurTornadoFXApp()

    override fun start(primaryStage: Stage) {
        app.scope = JProScope(primaryStage)
        app.start(primaryStage)
    }

    override fun stop() {
        app.stop()
        super.stop()
    }
}

Whenever a new user visits our site, the Main class is created, together with a new instance of our actual TornadoFX application.

In the start function we assign a new JProScope to the TornadoFX app instance and then call app.start. From there on out, all instances created using inject and find will be in the context of that JPro instance.

As usual, you can break out of the JProScope to access JVM level globals by supplying the DefaultScope or any other shared scope to the inject or find functions.

We should provide a utility function that makes it easy to access the JPro WebAPI from any Component:

val Component.webAPI: WebAPI get() = (scope as JProScope).webAPI

The scope property of any Component will be the JProScope so we can cast it and access the webAPI property we defined in our custom scope class.

Testing with Scopes

Since Scopes allow you to create separate instances of components that are usually singletons, you can leverage Scopes to test Views and even whole App instances.

For example, to generate a new Scope and lookup a View in that scope, you can use the following code:

val testScope = Scope()
val myView = find<MyView>(testScope)