-
Notifications
You must be signed in to change notification settings - Fork 104
Scopes
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.
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.
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.
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.
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:
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.
TODO
TODO
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
.
TODO
TODO
- Scope isolation isn't working correctly at the moment. TODO