sialcasa / mvvmFX

an Application Framework for implementing the MVVM Pattern with JavaFX
Apache License 2.0
489 stars 104 forks source link

Is it possible to inject objects into classes instantiated by the FxmlLoader? #534

Closed marvk closed 6 years ago

marvk commented 6 years ago

If I'm using a custom class like this one as a CellFactory:

public class SomeCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
    ...
}

and include it into the FXML like this:

<TableView>
    <columns>
        <TableColumn>
                ...
            <cellFactory>
                <SomeCellFactory/>
            </cellFactory>
        </TableColumn>
</TableView>

Is it possible to inject objects into it, for example the view model? Or does mvvmFX ignore those classes completely? If it is not possible, are there any alternatives other than giving the concerning column an fx:id and setting the CellFacotry in the initialize method of the view, so that I can the view model or other objects as constructor parameters? I'm currently using EasyDI.

manuel-mauky commented 6 years ago

This is not possible out of the box but you can enable this with mvvmFX. In JavaFX everything defined in FXML files is created with Builders and you can define BuilderFactories for custom stuff.

With the newest version of mvvmFX 1.7.0 you can add your own BuilderFactories. The code should look more or less this way (though not fully tested):


public class MainApp extends Application {

    public void start(Stage state) throws Exception {
        EasyDI context = new EasyDI();
        MvvmFX.setCustomDependencyInjector(context::getInstance);

        MvvmFX.addBuilderFactory(type -> {
            if(SomeCellFactory.class.isAssignableFrom(type)) {
                return context.getInstance(type);
            } else {
                return null;
            }
        });

        // ...
    }
}

See javadoc for a detailed description. In the example I'm defining a global BuilderFactory for your custom CellFactory. This BuilderFactory directly uses EasyDI context to retrieve an instance for this CellFactory. This way you can inject things into this CellFactory. If you don't like to have a global BuilderFactory instead you can also define the BuilderFactory in the FluentViewLoader via fluent-api. Then the builderFactory is only active for this single loading process.

What you should keep in mind is that this way the CellFactory can be part of the "global" Dependency-Injector but it has no context of where it is. It doesn't "know" in which View it is and what the corrent ViewModel for this view is. This is OK if you only have a single instance of your ViewModel (marked as Singleton) but it won't be that easy if you have multiple instances of the same View/ViewModel at the same time. The Dependency-Injector wouldn't know which to choose. MvvmFX contains magic to connect a View with it's very own ViewModel but it has no magic for other ressources like this case. In this case you probably are better served with the manual approach of setting the CellFactory in Code in your initialiize method.