Skip to content
Manuel Mauky edited this page Jun 16, 2016 · 3 revisions

To communicate between components mvvmFX has a notification mechanism. This allows sending messages between components without them needing static dependencies (a.k.a. imports) 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.

Notifications from ViewModel to View

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();
    });
}

Global Notifications

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.

Send a notification
@Inject
private NotificationCenter notificationCenter;

[...]

notificationCenter.publish("someNotification");

notificationCenter.publish("someNotification","arg1",new CustomArgTwo());
Receive a notification
@Inject
private NotificationCenter notificationCenter;

[...]
notificationCenter.subscribe("someNotification", (key, payload) -> {
    // trigger some actions
};

Notifications via Scopes

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.

Notice on Memory leaks

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
    }
  1. Create a field for the NotificationObserver in your class
  2. Initialize the field with a lambda
  3. 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.

Testing Notifications

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.