TornadoFX How to create MDI with list of child win

2019-09-14 20:43发布

问题:

I have following components:

class ChildModel:ViewModel() { //or it may be an POJO, it does not matter
  val value ....      
} 

class ParentView: View() {
  ...
  //Maybe this should be implemented into ParentViewModel   
  val childrenList:List<ChildModel>

  fun addFragmentAsChild() {
    //should:
    // 1. display fragment within ParentView
    // 2. add fragment into modelList (or fragmentList - it does not matter -  important to have access to the model of every child )  
  }

  fun deleteFragmentAsChild() {
    //should destroy child and remove item from childrenList   
    //should work also on manual closing 
  }         
}

class ChildFragment: Fragment() {
  val model = ChildModel()      
...
}

Summary: I want to create MDI and have access to model for every child.

I try do this with help "openInternalWindow" , but i can't create multiple child instances and i must manually manage the list - it's bad.

class InstrumentsView: View() {
  override val root = BorderPane()
  val instrumentList = ArrayList<InstrumentFragment>()

  init {
    with(root){
      top = menubar {
        menu("Tools") {
          menuitem("Add instrument", "Shortcut+A") {
            val newFragment = InstrumentFragment()
            instrumentList.add(newFragment)
            println(instrumentList.size)
            openInternalWindow(newFragment, modal = false)
          }

        }
      }
    }
  }
}

How to do it right tornadofx way?

回答1:

In this example I'll use a view model and scoping to keep track of the item for each instrument editor. We need to make sure the instruments are unique so we can remove them from the list when the editor is closed. I created an Instrument domain object with an id and a name:

class Instrument {
    val idProperty = SimpleObjectProperty<UUID>(UUID.randomUUID())
    var id by idProperty

    val nameProperty = SimpleStringProperty()
    var name by nameProperty

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other?.javaClass != javaClass) return false

        other as Instrument

        if (id != other.id) return false

        return true
    }

    override fun hashCode(): Int {
        return id.hashCode()
    }
}

We want a view model we can inject in the instrument editor. We'll make sure the view model contains a new instrument by default. It contains a facade for the name property so we can bind it to an editor input field.

class InstrumentModel: ItemViewModel<Instrument>() {
    init {
        item = Instrument()
        item.name = "New instrument"
    }
    val name = bind { item?.nameProperty }
}

A Fragment has callbacks for onDock and onUndock which can be used to keep track of the model for that fragment. We can use events to signal this. Declare the following events:

class InstrumentAdded(val instrument: Instrument) : FXEvent()
class InstrumentRemoved(val instrument: Instrument) : FXEvent()

Override the docking callbacks in InstrumentFragment to fire these events:

override fun onDock() {
    fire(InstrumentAdded(model.item))
}

override fun onUndock() {
    fire(InstrumentRemoved(model.item))
}

For now we'll keep the list of instruments in the main view, InstrumentsView. This could just as well be in a Controller.

val instruments = FXCollections.observableArrayList<Instrument>()

In the init class of the main view, we'll subscribe to the events we created and modify our list:

subscribe<InstrumentAdded> {
    instruments.add(it.instrument)
}
subscribe<InstrumentRemoved> {
    instruments.remove(it.instrument)
}

The "New Instrument" action will open a new InstrumentEditor in a new Scope so we can inject the view model into it and get an instance unique to that editor.

menuitem("Add instrument", "Shortcut+A") {
    find<InstrumentFragment>(Scope()).openWindow()
}

Unfortunately, we can't use openInternalWindow as it currently only supports one internal window at a time. Therefore I used openWindow instead.

If you want to close the editor from an action, you can call closeModal() from anywhere inside the fragment.

I've included a complete example app with a TableView that shows the currently open instruments. It will look like the image below. Notice that you need to hit save before the changes are flushed from the model and visible in the table.

I hope this is what you're looking for, or that you at least can modify it to fit your use case based on this sample.

import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.FXCollections
import tornadofx.*
import java.util.*

class Instrument {
    val idProperty = SimpleObjectProperty<UUID>(UUID.randomUUID())
    var id by idProperty

    val nameProperty = SimpleStringProperty()
    var name by nameProperty

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other?.javaClass != javaClass) return false

        other as Instrument

        if (id != other.id) return false

        return true
    }

    override fun hashCode(): Int {
        return id.hashCode()
    }
}

class InstrumentModel : ItemViewModel<Instrument>() {
    init {
        item = Instrument()
        item.name = "New instrument"
    }

    val name = bind { item?.nameProperty }
}

class InstrumentAdded(val instrument: Instrument) : FXEvent()
class InstrumentRemoved(val instrument: Instrument) : FXEvent()

class InstrumentFragment : Fragment("Instrument Editor") {
    val model: InstrumentModel by inject()

    override val root = form {
        prefWidth = 300.0
        fieldset("Edit instrument") {
            field("Name") {
                textfield(model.name)
            }
        }
        button("Save") {
            setOnAction {
                model.commit()
            }
        }
    }

    override fun onDock() {
        fire(InstrumentAdded(model.item))
    }

    override fun onUndock() {
        fire(InstrumentRemoved(model.item))
    }
}

class InstrumentsView : View() {
    val instruments = FXCollections.observableArrayList<Instrument>()

    override val root = borderpane {
        setPrefSize(400.0, 300.0)
        top {
            menubar {
                menu("Tools") {
                    menuitem("Add instrument", "Shortcut+A") {
                        find<InstrumentFragment>(Scope()).openWindow()
                    }
                }
            }
        }
        center {
            tableview(instruments) {
                column("Name", Instrument::nameProperty)
                columnResizePolicy = SmartResize.POLICY
            }
        }
    }

    init {
        subscribe<InstrumentAdded> {
            instruments.add(it.instrument)
        }
        subscribe<InstrumentRemoved> {
            instruments.remove(it.instrument)
        }
    }

}