edvin / tornadofx-guide

TornadoFX Guide
134 stars 67 forks source link

Inject parameters not found #114

Closed HoldYourWaffle closed 4 years ago

HoldYourWaffle commented 4 years ago

I'm trying to implement a basic MVC pattern for a long running calculation loop 'streaming' data into a linechart. I'm very new to this stuff so I could just be missing something very obvious here.

MyView is the view that contains the chart. It receives an Assignment and Dataset from previous steps. MyModel is the model that contains some properties for controlling the calculation process. Finally, MyController is the controller that handles the calculation and adds the new data to the chart data.

Although this kind of injection is a new concept to me, I think I have wrapped my head around it pertty well now. However, I can't figure out how to pass the Assignment and Dataset from MyView to MyModel (and thereby to MyController). It receives the Assignment and Dataset via the param delegate in the view. At least, that was the plan. As soon as I start the calculation I get the following error:

java.lang.IllegalStateException: param for name [val *package*.MyModel.dataset: *package*.Dataset /* = kotlin.collections.List<kotlin.String> */.name] has not been set

As far as I can tell I did in fact pass the dataset parameter (on the marked line). Am I missing something obvious here? Am I using this whole injection thing wrong? I've been searching for hours now but I can't figure it out.

Here's a (slimmed down) version of my code:

typealias Series = XYChart.Series<Number, Number>;

class MyView: View() {
    val assignment: Assignment by param();
    val dataset: Dataset by param();

    internal val controller: MyController by inject();
    internal val model: MyModel by inject(params = mapOf("assignment" to assignment, "dataset" to dataset)); // ****

    override val root = vbox {
        linechart("My awesome chart", NumberAxis(), NumberAxis()) {
            data.bind(model.graphData, { it }); // never tested this binding, would it work?
        }

        togglebutton("Calculate") {
            selectedProperty().set(false);
            model.shouldCalculate.bind(selectedProperty());
        }

        progressindicator {
            visibleWhen { model.calculating }
        }
    }

    init {
        runAsync {
            controller.startCalculationLoop();
        }
    }
}

internal class MyModel: ViewModel() {
    val assignment: Assignment by param();
    val dataset: Dataset by param();

    val shouldCalculate = SimpleBooleanProperty(false);
    val calculating = SimpleBooleanProperty(false);

    val graphData = mutableListOf<Series>().asObservable();
    val iterationCount = SimpleIntegerProperty(0);
}

internal class MyController: Controller() {
    internal val model: MyModel by inject();

    fun startCalculationLoop() = with (model) {
        shouldCalculate.onChange {
            if (!it) return@onChange;

            calculating.set(true);
            iterationCount += 1;

            val results = calculate(dataset, assignment);

            results.forEach { (seriesId, result) ->
                graphData.findOrAdd<Series>( // find or create the series for this id
                    { it.name == seriesId.toString() },
                    { XYChart.Series(seriesId.toString(), observableListOf()) }
                ).data(iterationCount.get(), result); // add the result to it
            }

            calculating.set(false);
        }
    }

    private fun calculate(dataset: Dataset, assignment: Assignment): List<Pair<Int, Int>> {
        // *complicated calculations*
        return listOf();
    }

}

Any help would be greatly appreciated!

HoldYourWaffle commented 4 years ago

Whoops I just noticed I created this issue in the wrong repository. Sorry...

edvin commented 4 years ago

You should inject MyModel into the scope and set the desired values on it. Alternatively, create MyModel manually, set values on it and set it into the scope (scope.set(MyModel())). Now you can just inject it into any other view or controller in the same scope, and you have access to your data. Using param is almost never the best solution. I believe this is pretty well covered in the guide, have you had a look at it?

HoldYourWaffle commented 4 years ago

Creating the model manually and inserting it into the scope did the trick for me, thanks!

I have read the guide extensively. Most parts are great, but I was honestly pretty confused by the whole injection section. This was the first time I've had to deal with something like this, so maybe that's just because of my lack of basic knowledge on the subject.