-
Notifications
You must be signed in to change notification settings - Fork 104
Notifications
To communicate between components mvvmFX has a notification mechanism. This allows sending messages between components without them needing static dependencies (a.k.a. import
s) between each other.
The notifications system uses a String key to define the topic. A subscribe can add an observer for a specific topic. When a publisher sends a notification for this topic, the observer of the subscriber will be invoked.
There are 3 variants of notifications available for different use cases.
While the ViewModel in MVVM should contains all presentation logic there are parts of logic that have to remain in the View. This is true for all code that is tightly coupled to UI specific components like the definition of animations or the loading of new Views. Such code can't be placed in the ViewModel because the most important rule of MVVM is to not have UI dependencies in the ViewModel.
However it is the task of the ViewModel to define when and under which conditions an animation should be started or a new view should be loaded. For this purpose the ViewModel needs a way of communicating with the View without violating the visibility constraints of MVVM (no visibility from ViewModel to View). For this mvvmFX has a notification mechanism to send messages from the ViewModel to the View.
The ViewModel
class contains two default methods: viewModel.publish(String messageKey, Object...payload)
and viewModel.subscribe(String messageKey, NotificationObserver observer)
public class MyViewModel implements ViewModel {
public final static String OPEN_ALERT = "OPEN_ALERT";
public void someAction() {
publish(OPEN_ALERT, "Some Error has happend");
}
}
public class MyView implements FxmlView<MyViewModel> {
@InjectViewModel
MyViewModel viewModel;
viewModel.subscribe(MyViewModel.OPEN_ALERT, (key, payload) -> {
String message = (String) payload[0];
Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle(message);
alert.setContentText(message);
alert.show();
});
}
de.saxsys.mvvmfx.utils.notifications
For some usecases you need to send global notifications that other components can react to. For this purpose
mvvmFX provides the NotificationCenter
that can be obtained by using the factory method MvvmFX.getNotificationCenter()
in by injecting it when the CDI or Guice module is used as Dependency Injection container.
@Inject
private NotificationCenter notificationCenter;
[...]
notificationCenter.publish("someNotification");
notificationCenter.publish("someNotification","arg1",new CustomArgTwo());
@Inject
private NotificationCenter notificationCenter;
[...]
notificationCenter.subscribe("someNotification", (key, payload) -> {
// trigger some actions
};
MvvmFX version 1.5.0 introduces a mechanism called "Scopes" (see Scopes for more details) to loosly group together different components that are using the same data. A scope instance provides a notification mechanism similar to that provided by ViewModels like described above. A message send to the scope will only be available for components that are using the same scope instance.
public class MyScope implements Scope {
...
}
public class MyViewModel implements ViewModel {
@InjectScope
private MyScope scope;
public void initialize() {
scope.subscribe("TEST", (key, payload) -> {
// some action
}
}
}
public class OtherViewModel implements ViewModel {
@InjectScope
private MyScope scope;
public void action() {
scope.publish("TEST");
}
}
Keep in mind that at the moment the Scopes feature is BETA.
Using observers within Java allways adds the possibility of introducing memory leaks. This is the reason why JavaFX provides the WeakChangeListener class for listeners on JavaFX Properties. MvvmFX provides a similar class to prevent memory leaks when using our notification mechanism.
See the following example for a memory leak:
public class MyViewModel implements ViewModel {
@Inject
NotificationCenter notificationCenter;
private IntegerProperty counter = new SimpleIntegerProperty();
public void initialize() {
notificationCenter.subscribe("test", (key, payload) -> {
counter.set(counter.get() + 1);
}
}
}
The observer is written as lambda expression. Under some conditions Java 8 will compile lambda expressions to static method calls which isn't introducing memory leaks. However in this example the lambda will instead be compiled to an anonymous inner class because inside of the lambda we are accessing a field of the containing class.
An anonymous inner class will always keep a reference to it's parent (in our case the MyViewModel
instance).
As the notificationCenter will keep a reference to the observer and the observer keeps a reference to the ViewModel,
the ViewModel instance will never be available for garbage collection even if the View isn't visible anymore.
To fix this we can use the class de.saxsys.mvvmfx.notifications.WeakNotificationObserver
like this:
@Inject
NotificationCenter notificationCenter;
private IntegerProperty counter = new SimpleIntegerProperty();
private NotificationObserver observer; // 1
public void initialize() {
// 2
observer = (key, payload) -> {
counter.set(counter.get() + 1);
};
notificationCenter.subscribe("test", new WeakNotificationObserver(observer)); // 3
}
- Create a field for the
NotificationObserver
in your class - Initialize the field with a lambda
- Subscribe by wrapping the observer in a new instance of
WeakNotificationObserver
As the name suggests the WeakNotificationObserver
only keeps a weak reference to the wrapped observer.
Therefore creating a field in the class for the actual observer is crucial. Otherwise the actual observer would be
collected by the garbage collector to early.
When notifications are send with the ViewModel.publish
method, we are delivering the notification on the JavaFX Application thread to make the handling in the View easier. While this is generally a good approach it can make testing harder because in a JUnit test there is no FX thread. To make testing easy again we are checking if there is a FX thread available to send the notification. This is the case at runtime but not when a JUnit test is executed. When no FX thread is present the notification will be send directly on the same thread that is currently running (the JUnit thead in this case). This way it's easy to subscribe to the message in the unit test and make the needed assertions.
The downside of this approach is that the handling in the unit test differs slightly from the handling at runtime. In most cases this won't be a problem but maybe in some it is.
To be able to test the handling of notifications in a more "realistic" way, i.e. sending and receiving notifications on the FX thread like it is done at runtime, we have included a utility called "NotificationTestHelper".
The test helper implements the NotificationObserver
interface and can be used to subscribe to notifications. It will record all received notifications and can be used for assertions afterwards:
public class MyViewModel implements ViewModel {
public static final String ACTION_KEY = "my-action";
public void someAction() {
...
publish(ACTION_KEY);
}
}
// unit test
@Test
public void testSomething() {
// given
MyViewModel viewModel = new MyViewModel();
NotificationTestHelper helper = new NotificationTestHelper();
viewModel.subscribe(MyViewModel.ACTION_KEY, helper);
// when
viewModel.someAction();
// then
assertEquals(1, helper.numberOfReceivedNotifications());
}
The NotificationTestHelper
takes care for the handling of the JavaFX Application Thread. The numberOfReceivedNotifications
method is a blocking operation that will wait for other actions on the UI-Thread (like the publishing of notifications).
Additionally you can use the NotificationTestHelper
to test notifications send from other threads as well. In this case you will need to provide a timeout (in milliseconds) as constructor parameter. The test helper will wait for the given amout of millis until it checks how many notifications where received.