Skip to content
Manuel Mauky edited this page Jun 8, 2016 · 20 revisions

This feature is currently in beta state. It was released as part of version 1.5.0 so that interested users can test the feature and provide feedback. Keep in mind that maybe some parts of the API may change in the future.

What are Scopes in mvvmFX?

A Scope in mvvmFX is a data context between ViewModels. They are used to decouple dependant ViewModels from each other.

Scopes are classes that implement the marker interface de.saxsys.mvvmfx.Scope. At runtime an instance of a scope can be injected into interested ViewModels so that they can work on the same data while not being dependent on each other. Different parts of the application can use different instances of the same Scope type. An example for this is a component that can be reused in many places of the application. The component itself may be composed of multiple sub-components that share the same Scope instance. This scope instance has to be separated from the scope instance that is used by the component and it's subcomponents in another place of the application.

Details

Scope

The de.saxsys.mvvmfx.Scope interface is a marker interface that is implemented by actual scope classes. Besides this it provides a notification mechanism similar to that from ViewModels. The notification mechanism is described below in more detail.

@InjectScope

The de.saxsys.mvvmfx.InjectScope annotation is used to get an instance of a scope at runtime. It can only be used inside of ViewModels.

ScopeProvider

At runtime it is possible that multiple instances of the same scope type are existing in different places of the application. These scope instances have to be independend from each other. To make this possible scope instances are not global singletons by default. Instead you as a developer have to define where a scope is visible and available. This is done by defining "scope providers". There are 3 possible ways of defining scope providers at the moment:

@ScopeProvider annotation

The de.saxsys.mvvmfx.ScopeProvider annotation can be put on a ViewModel class. As an annotation argument a list of actual Scope types have to be provided.

@ScopeProvider(scopes= {MyScope1.class, MyScope2.class})
public class MyParentViewModel implements ViewModel {
    @InjectScope
    private MyScope1 scope1;
... 
}

All ViewModels that are below this ViewModel in the view hierarchy and the ViewModel itself will be able to inject instances of the scope types that are declared in the annotation.

Scope providers can be "overwritten": If somewhere in below in the view hierarchy another ViewModel exists with the @ScopeProvider annotation that declares an already declared Scope type, then starting from this point all ViewModels below will get a new instance of the scope.

Context

As described above the visibility of a scope depends on the view hierarchy. The view hierarchy is defined by the way the FXML files are composed via <fx:include ...> tag. A problem appears when a part of the UI isn't composed via fx:include tag but instead is loaded with the FluentViewLoader and put into the scenegraph via code. See the following example:

public class MyView implements FxmlView<MyViewModel> {
    @FXML
    private HBox content;

    public void someMethod() {
        Parent subRoot = FluentViewLoader.fxmlView(SubView.class)
            .load()
            .getView();
        content.getChildren().add(subRoot);
    }
}

From a technical perspective the loading process of MyView (and it's parent components) is completely independ from the loading process of SubView even through they are put together in the same SceneGraph. Due to technical restrictions of javaFX's FXMLLoader that is internally used in the FluentViewLoader, at the moment we can't know that SubView is a component in same view hierarchy as MyView. Therefore a @ScopeProvider that is defined in the hierarchy somewhere above MyView isn't recognized when loading SubView.

To fix this you need to tell the FluentViewLoader in which context the view is loaded. For this purpose we have introduced the interface de.saxsys.mvvmfx.Context which can be injected into your View class. For the injection you have to use the annotation @InjectContext. You can pass this context instance into the FluentViewLoader with the method context(...) while loading the sub view. See the following example:

public class MyView implements FxmlView<MyViewModel> {
    @FXML
    private HBox content;

    @InjectContext
    private Context context;

    public void someMethod() {
        Parent subRoot = FluentViewLoader.fxmlView(SubView.class)
            .context(context)  // new Code
            .load()
            .getView();
        content.getChildren().add(subRoot);
    }
}

This way the FluentViewLoader knows which scopes are defined and visible and injects them into the new view sub-hierarchy.

FluentViewLoader -> provideScopes

In some situations you want to have more power over the creation process of a scope before it is injected into ViewModels. For example you may want to prefill the scope instance with some data so that the views/viewModels can use this data as soon as they are loaded.

To do this you can provide existing Scope instances when loading a view via the FluentViewLoader with the method providedScopes(Scope...scopes).

A common use case for this is when a View component should be used as element in a javaFX ListView or TabPane. In such cases each View component could use a scope instance that defines the data that is shown by the List element or Tab. The following example shows the usage in a TabPane:

public class TabScope implements Scope {
    private StringProperty content = new SimpleStringProperty();
    public StringProperty contentProperty() {
        return content;
    }
}

public class ParentView implements FxmlView<ParentViewModel> {
    @FXML
    private TabPane tabPane;
    @FXML
    private TextField newTabContent;
    @InjectViewModel
    private ParentViewModel viewModel;

    public void openNewTab() {
        TabScope scope = new TabScope();   // create new Scope instance
        scope.contentProperty().setValue(newTabContent.getText());  // prefill with a value
 
        Parent tabView = FluentViewLoader.fxmlView(TabView.class)
            .providedScopes(scope)  // pass the existing scope instance
            .load()
            .getView();
        tabPane.getTabs().add(new Tab("TabTitle", tabView);
    }
}

Each time a new tab is created we also create an instance of our Scope type and initialize it with a value. After that the existing scope is passed to the FluentViewLoader with the providedScopes method. This way the scope can be injected in the TabViewViewModel that is loading together with the TabView class. At runtime for every tab there exists an instance of the TabScope that is only available for ViewModels inside of this tab.

The method providedScopes of the FluentViewLoader is overloaded. There is one version of the method that takes a var-arg array of Scope instances. The other version of the method takes a List<Scope> scopes list.

The way of creating scopes like shown above is possible but not very clean. Actually the Views shouldn't even know about any scopes. The creating and prefilling of Scopes should instead be done in the ViewModel. On the other hand the loading of new Views may not be done in the ViewModel but instead only in the View. To separate these tasks one could use the following pattern:

In the ViewModel has a Consumer<List<Scope>> that can be set from the view. This is a function that takes a list of Scopes as argument when called. The creating of the Scope is done in a method in the ViewModel. After that the consumer is called with the created Scope as argument (wrapped in a List).

public class ParentViewmodel implements ViewModel {
    private Consumer<List<Scope>> newTabAction;
    private StringProperty input = new SimpleStringProperty();

    public void setNewTabActio(Consumer<List<Scope>> newTabAction) { ... }
    public StringProperty inputProperty() {...}

    // This method is called when a new tab should be opened.
    public void addTab() {
        TabScope tabScope = new TabScope();
        tabScope.contentProperty.set(input.getValue());

        newTabAction.accept(Collections.singletonList(tabScope);
    }   
}

In the View we define the consumer as lambda. We take the list of scopes and pass them to the ViewLoader. This way the View only knows that there may be some scopes that are used to load the Tab but it has no clue about which actual types of scopes. If we need to add another Scope type in the future we can only need to update the ViewModel but don't need to touch the View.

Also notice that we now doesn't take the content value directly from the TextField but instead bind it's value to the ViewModel. This way the View doesn't know which data is put into the new scope.

public class ParentView implements FxmlView<ParentViewModel> {
    @FXML
    private TabPane tabPane;
    @FXML
    private TextField newTabContent;
    @InjectViewModel
    private ParentViewModel viewModel;

    public void openNewTab() {
        // bind UI controls to the ViewModel like it's typical for MVVM
        newTabContent.textProperty().bindBidirectional(viewModel.inputProperty());
        
        viewModel.setNewTabAction(scopes -> {
            Parent tabView = FluentViewLoader.fxmlView(TabView.class)
                .providedScopes(scopes)  // pass the existing scopes
                .load()
                .getView();
            tabPane.getTabs().add(new Tab("TabTitle", tabView);
        });       
    }
}

You can also see this example in the source code.

Examples

Master-Detail view

We want to create a master-detail component. In the MasterView there may be a ListView that shows many elements and the user can select a specific element. The DetailView then updates itself and shows details of the selected element. To do this we first create a scope class that contains the information that has to be shared between both components, in our example the id of the selected element.

public class MasterDetailScope implements Scope {
    private StringProperty selectedElementId = new SimpleStringProperty();

    public StringProperty selectedElementIdProperty() {
        return selectedElementId;
    }
}

In the MasterViewModel we will have an observable list of all available elements that can be visualized by the MasterView in form of a javaFX ListView or some other component. Additionally our MasterViewModel needs to provide a Property for the selected element so that the View can change the selection based on the user interaction. In this example we create a wrapper method for the property of the scope. Do not pass the Scope instance itself to the View. Views shouldn't know about such implementation details.

public class MasterViewModel implements ViewModel {

    @InjectScope
    private MasterDetailScope scope;

    private ObservableList<String> allElementIds = ...

    // bind to the selected item in the ListView
    public StringProperty selectedElementIdProperty() {
        return scope.selectedElementIdProperty();
    }

    // may be bound to a ListView in the view component
    public ObservableList<String> allElementIds() {
        return allElementIds;
    }
}

In the DetailViewModel we listen for changed of the selected id and load new data when the user changes the selection so that the DetailView can show the new informations.

public class DetailViewModel implements ViewModel {
    @InjectScope
    private MasterDetailScope scope;
    
    public void initialize() {
        scope.selectedElementIdProperty().addListener((obs, oldV,newV) -> {
            // load data for the selected id
        }
    }
    ...
}

To define where the scope is available we have to define a ScopeProvider. This is done by using the annotation de.saxsys.mvvmfx.ScopeProvider on a ViewModel class. After that the Scope is available in this ViewModel and all ViewModels that are below the ViewModel in the hierarchy. By defining a new ScopeProvider in another part of the application you can separate the Scope visibility for different parts of the application.

**ParentView.fxml

<?xml version="1.0" encoding="UTF-8"?>
...
<HBox fx:controller="ParentView">
    <children>
        <fx:include source="MasterView.fxml" />
        <fx:include source="DetailView.fxml" />
    </children>
</HBox>

ParentViewModel.java

@ScopeProvider(scopes = {MasterDetailScope.class})
public class ParentViewModel implements ViewModel {
}

Both MasterView/MasterViewModel and DetailView/DetailViewModel are children of ParentView/ParentViewModel. We declare ParentViewModel as a ScopeProvider of MasterDetailScope. This way both MasterViewModel and DetailViewModel will get the same instance of MasterDetailScope injected so they can both act on the same data while not having any dependency to each other. If in the future another component is added that needs informations of the MasterDetailScope it can be done without having to change any code in either MasterViewModel or DetailViewModel.

Notifications

The Scope provides a notification mechanism similar to the one provided by ViewModels. This way you can publish and subscribe to notifications that are only visible for this scope instance.

public class MasterViewModel implements ViewModel {

    @InjectScope
    private MasterDetailScope scope;

    public void someAction() {
        scope.publish("something_has_changed");
    }
}

public class DetailViewModel implements ViewModel {
    
    @InjectScope
    private MasterDetailScope scope;

    public void initialize() {
        scope.subscribe("something_has_changed", (key, payload) -> {
            // some code
        });
    }
}

Limitations and Known Issues in the current implementation

At the moment the scopes feature is still in development. It was released as part of version 1.5.0 to be able to test the feature in a wide range of applications and to gather feedback from users. However there are still some limitations and known issues that we try to fix in future releases. For this reason the API may change slightly in the future.

The biggest issue at the moment is to restrict the visibility of Scopes for different branches in the FXML hierarchy due to limited extensibility of JavaFX's FXMLLoader. At the moment we can't garantee that a Scope is only visible underneth a @ScopeProvider. Instead it is possible that another ViewModel that isn't a child of the ScopeProvider injects the Scope instance. In the future we are planning to restrict such misusage and provide descriptive error messages. A detailed description of the problem can be found in the issue tracker.