com.cedarsoft.commons
test-utils
diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java
index 97071d741..885a9eb29 100644
--- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java
+++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java
@@ -46,9 +46,7 @@
public interface ViewModel {
/**
- * Publishes a notification to the subscribers of the messageName. This notification will be send to the
- * UI-Thread (if the UI-toolkit was bootstrapped). If no UI-Toolkit is available the notification will be directly
- * published. This is typically the case in unit tests.
+ * Publishes a notification to the subscribers of the messageName.
*
*
* This notification mechanism uses the {@link NotificationCenter} internally with the difference that messages send
@@ -64,22 +62,7 @@ public interface ViewModel {
* to be send
*/
default void publish(String messageName, Object... payload) {
- if (Platform.isFxApplicationThread()) {
- MvvmFX.getNotificationCenter().publish(this, messageName, payload);
- } else {
- try {
- Platform.runLater(() -> MvvmFX.getNotificationCenter().publish(this, messageName, payload));
- } catch(IllegalStateException e) {
-
- // If the toolkit isn't initialized yet we will publish the notification directly.
- // In most cases this means that we are in a unit test and not JavaFX application is running.
- if(e.getMessage().equals("Toolkit not initialized")) {
- MvvmFX.getNotificationCenter().publish(this, messageName, payload);
- } else {
- throw e;
- }
- }
- }
+ MvvmFX.getNotificationCenter().publish(this, messageName, payload);
}
/**
diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/FxmlViewLoader.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/FxmlViewLoader.java
index 0e8ed7019..e5e6aa19e 100644
--- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/FxmlViewLoader.java
+++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/FxmlViewLoader.java
@@ -135,15 +135,27 @@ public , ViewModelType extends Vi
throw new IOException("Could not load the controller for the View " + resource
+ " maybe your missed the fx:controller in your fxml?");
}
+
+
+ // the actually used ViewModel instance. We need this so we can return it in the ViewTuple
+ ViewModelType actualViewModel;
-
- ViewModelType loadedViewModel = ViewLoaderReflectionUtils.getExistingViewModel(loadedController);
-
- if (loadedViewModel == null) {
- loadedViewModel = ViewLoaderReflectionUtils.createViewModel(loadedController);
+ // if no existing viewModel was provided...
+ if(viewModel == null) {
+ // ... we try to find the created ViewModel from the codeBehind.
+ // this is only possible when the codeBehind has a field for the VM and the VM was injected
+ actualViewModel = ViewLoaderReflectionUtils.getExistingViewModel(loadedController);
+
+ // otherwise we create a new ViewModel. This is needed because the ViewTuple has to contain a VM even if the codeBehind doesn't need one
+ if (actualViewModel == null) {
+ actualViewModel = ViewLoaderReflectionUtils.createViewModel(loadedController);
+ }
+ } else {
+ actualViewModel = viewModel;
}
- return new ViewTuple<>(loadedController, loadedRoot, loadedViewModel);
+
+ return new ViewTuple<>(loadedController, loadedRoot, actualViewModel);
} catch (final IOException ex) {
throw new RuntimeException(ex);
diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtils.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtils.java
index 612676625..219aa9796 100644
--- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtils.java
+++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtils.java
@@ -34,19 +34,44 @@ public class ViewLoaderReflectionUtils {
* @return an Optional that contains the Field when the field exists.
*/
public static Optional getViewModelField(Class extends View> viewType, Class> viewModelType) {
- List viewModelFields = Arrays.stream(viewType.getDeclaredFields())
- .filter(field -> field.isAnnotationPresent(InjectViewModel.class))
- .filter(field -> field.getType().isAssignableFrom(viewModelType))
- .collect(Collectors.toList());
- if (viewModelFields.isEmpty()) {
+ List allViewModelFields = getViewModelFields(viewType);
+
+ if(allViewModelFields.isEmpty()) {
return Optional.empty();
}
- if (viewModelFields.size() > 1) {
+
+ if (allViewModelFields.size() > 1) {
throw new RuntimeException("The View <" + viewType + "> may only define one viewModel but there were <"
- + viewModelFields.size() + "> viewModel fields!");
+ + allViewModelFields.size() + "> viewModel fields with the @InjectViewModel annotation!");
+ }
+
+ Field field = allViewModelFields.get(0);
+
+ if(! ViewModel.class.isAssignableFrom(field.getType())) {
+ throw new RuntimeException("The View <" + viewType + "> has a field annotated with @InjectViewModel but the type of the field doesn't implement the 'ViewModel' interface!");
}
- return Optional.of(viewModelFields.get(0));
+
+ if(! field.getType().isAssignableFrom(viewModelType)) {
+ throw new RuntimeException("The View <" + viewType + "> has a field annotated with @InjectViewModel but the type of the field doesn't match the generic ViewModel type of the View class. "
+ + "The declared generic type is <" + viewModelType + "> but the actual type of the field is <" + field.getType() + ">.");
+ }
+
+ return Optional.of(field);
}
+
+
+ /**
+ * Returns a list of all {@link Field}s of ViewModels for a given view type that are annotated with {@link InjectViewModel}.
+ *
+ * @param viewType the type of the view.
+ * @return a list of fields.
+ */
+ private static List getViewModelFields(Class extends View> viewType) {
+ return Arrays.stream(viewType.getDeclaredFields())
+ .filter(field -> field.isAnnotationPresent(InjectViewModel.class))
+ .collect(Collectors.toList());
+ }
+
/**
* This method is used to get the ViewModel instance of a given view/codeBehind.
@@ -114,6 +139,8 @@ public static void injectViewModel(final View view, ViewModel viewModel) {
* the generic type of the ViewModel.
* @return an Optional containing the ViewModel if it was created or already existing. Otherwise the Optional is
* empty.
+ *
+ * @throws RuntimeException if there is a ViewModel field in the View with the {@link InjectViewModel} annotation whose type doesn't match the generic ViewModel type from the View class.
*/
@SuppressWarnings("unchecked")
public static , VM extends ViewModel> Optional createAndInjectViewModel(
diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/itemlist/ListTransformation.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/itemlist/ListTransformation.java
index 83da43f68..36936710b 100644
--- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/itemlist/ListTransformation.java
+++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/itemlist/ListTransformation.java
@@ -89,6 +89,7 @@ public void onChanged(
// targetList
List deleteStaging = new ArrayList<>();
+
while (listEvent.next()) {
if (listEvent.wasUpdated()) {
processUpdateEvent(listEvent);
@@ -118,10 +119,13 @@ public void onChanged(
*/
private void processAddEvent(
ListChangeListener.Change extends SourceType> listEvent) {
- for (int i = listEvent.getFrom(); i < listEvent.getTo(); i++) {
- SourceType item = listEvent.getList().get(i);
- viewModelList.add(i, ListTransformation.this.function.apply(item));
+
+ final List toAdd = new ArrayList<>();
+ for (int index = listEvent.getFrom(); index < listEvent.getTo(); index++) {
+ final SourceType item = listEvent.getList().get(index);
+ toAdd.add(function.apply(item));
}
+ viewModelList.addAll(listEvent.getFrom(), toAdd);
}
/**
diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapper.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapper.java
index 268f2fec2..13561f253 100644
--- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapper.java
+++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapper.java
@@ -12,6 +12,9 @@
import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.IntGetter;
import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.IntPropertyAccessor;
import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.IntSetter;
+import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ListGetter;
+import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ListPropertyAccessor;
+import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ListSetter;
import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.LongGetter;
import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.LongPropertyAccessor;
import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.LongSetter;
@@ -23,6 +26,9 @@
import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.StringSetter;
import eu.lestard.doc.Beta;
import javafx.beans.property.*;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
import java.util.*;
import java.util.function.BiConsumer;
@@ -341,6 +347,119 @@ public boolean isDifferent(M wrappedObject) {
}
}
+ /**
+ * An implementation of {@link PropertyField} that is used when the field of the model class is a {@link List} and
+ * and is a JavaFX {@link ListProperty} too.
+ *
+ * @param
+ * @param
+ * the type of the list elements.
+ */
+ private class FxListPropertyField, R extends Property>
+ implements PropertyField {
+
+ private final List defaultValue;
+ private final ListPropertyAccessor accessor;
+ private final ListProperty targetProperty = new SimpleListProperty<>(FXCollections.observableArrayList());
+
+ public FxListPropertyField(ListPropertyAccessor accessor) {
+ this(accessor, Collections.emptyList());
+ }
+
+ public FxListPropertyField(ListPropertyAccessor accessor, List defaultValue) {
+ this.accessor = accessor;
+ this.defaultValue = defaultValue;
+
+ this.targetProperty.addListener((ListChangeListener) change -> propertyWasChanged());
+ }
+
+ @Override
+ public void commit(M wrappedObject) {
+ accessor.apply(wrappedObject).setAll(targetProperty.getValue());
+ }
+
+ @Override
+ public void reload(M wrappedObject) {
+ targetProperty.setAll(accessor.apply(wrappedObject).getValue());
+ }
+
+ @Override
+ public void resetToDefault() {
+ targetProperty.setAll(defaultValue);
+ }
+
+ @Override
+ public R getProperty() {
+ return (R) targetProperty;
+ }
+
+ @Override
+ public boolean isDifferent(M wrappedObject) {
+ final List modelValue = accessor.apply(wrappedObject).getValue();
+ final List wrapperValue = targetProperty;
+
+ return !(modelValue.containsAll(wrapperValue) && wrapperValue.containsAll(modelValue));
+ }
+ }
+
+ /**
+ * An implementation of {@link PropertyField} that is used when the field of the model class is a {@link List} and
+ * is not a JavaFX ListProperty but is following the old Java-Beans standard, i.e. there is getter and
+ * setter method for the field.
+ *
+ * @param
+ * @param
+ * the type of the list elements.
+ */
+ private class BeanListPropertyField, R extends Property>
+ implements PropertyField {
+
+ private final ListGetter getter;
+ private final ListSetter setter;
+
+ private final List defaultValue;
+ private final ListProperty targetProperty = new SimpleListProperty<>(FXCollections.observableArrayList());
+
+ public BeanListPropertyField(ListGetter getter, ListSetter setter) {
+ this(getter, setter, Collections.emptyList());
+ }
+
+ public BeanListPropertyField(ListGetter getter, ListSetter setter, List defaultValue) {
+ this.defaultValue = defaultValue;
+ this.getter = getter;
+ this.setter = setter;
+
+ this.targetProperty.addListener((ListChangeListener) change -> propertyWasChanged());
+ }
+
+ @Override
+ public void commit(M wrappedObject) {
+ setter.accept(wrappedObject, targetProperty.getValue());
+ }
+
+ @Override
+ public void reload(M wrappedObject) {
+ targetProperty.setAll(getter.apply(wrappedObject));
+ }
+
+ @Override
+ public void resetToDefault() {
+ targetProperty.setAll(defaultValue);
+ }
+
+ @Override
+ public R getProperty() {
+ return (R) targetProperty;
+ }
+
+ @Override
+ public boolean isDifferent(M wrappedObject) {
+ final List modelValue = getter.apply(wrappedObject);
+ final List wrapperValue = targetProperty;
+
+ return !(modelValue.containsAll(wrapperValue) && wrapperValue.containsAll(modelValue));
+ }
+ }
private Set> fields = new HashSet<>();
private Map> identifiedFields = new HashMap<>();
@@ -862,6 +981,47 @@ public ObjectProperty field(String identifier, ObjectPropertyAccessor(accessor::apply, defaultValue, SimpleObjectProperty::new));
}
+
+
+ /** Field type list **/
+
+ public ListProperty field(ListGetter getter, ListSetter setter) {
+ return add(new BeanListPropertyField<>(getter::apply, (m, list)
+ -> setter.accept(m, FXCollections.observableArrayList(list))));
+ }
+
+ public ListProperty field(ListGetter getter, ListSetter setter, List defaultValue) {
+ return add(new BeanListPropertyField<>(getter::apply, (m, list)
+ -> setter.accept(m, FXCollections.observableArrayList(list)), defaultValue));
+ }
+
+ public ListProperty field(ListPropertyAccessor accessor) {
+ return add(new FxListPropertyField<>(accessor::apply));
+ }
+
+ public ListProperty field(ListPropertyAccessor accessor, List defaultValue) {
+ return add(new FxListPropertyField<>(accessor::apply, defaultValue));
+ }
+
+
+ public ListProperty field(String identifier, ListGetter getter, ListSetter setter) {
+ return addIdentified(identifier, new BeanListPropertyField<>(getter::apply, (m, list)
+ -> setter.accept(m, FXCollections.observableArrayList(list))));
+ }
+
+ public ListProperty field(String identifier, ListGetter getter, ListSetter setter,
+ List defaultValue) {
+ return addIdentified(identifier, new BeanListPropertyField<>(getter::apply, (m, list)
+ -> setter.accept(m, FXCollections.observableArrayList(list)), defaultValue));
+ }
+
+ public ListProperty field(String identifier, ListPropertyAccessor accessor) {
+ return addIdentified(identifier, new FxListPropertyField<>(accessor::apply));
+ }
+
+ public ListProperty field(String identifier, ListPropertyAccessor accessor, List defaultValue) {
+ return addIdentified(identifier, new FxListPropertyField<>(accessor::apply, defaultValue));
+ }
private > R add(PropertyField field) {
fields.add(field);
diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ListGetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ListGetter.java
new file mode 100644
index 000000000..f36f97d80
--- /dev/null
+++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ListGetter.java
@@ -0,0 +1,24 @@
+package de.saxsys.mvvmfx.utils.mapping.accessorfunctions;
+
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * A functional interface to define a getter method of a list type.
+ *
+ * @param
+ * the generic type of the model.
+ * @param
+ * the type of the list elements.
+ */
+@FunctionalInterface
+public interface ListGetter extends Function> {
+
+ /**
+ * @param model
+ * the model instance.
+ * @return the value of the field.
+ */
+ @Override
+ List apply(M model);
+}
diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ListPropertyAccessor.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ListPropertyAccessor.java
new file mode 100644
index 000000000..28d7b2e5d
--- /dev/null
+++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ListPropertyAccessor.java
@@ -0,0 +1,25 @@
+package de.saxsys.mvvmfx.utils.mapping.accessorfunctions;
+
+import javafx.beans.property.ListProperty;
+
+import java.util.function.Function;
+
+/**
+ * A functional interface to define an accessor method for a property of a list type.
+ *
+ * @param
+ * the generic type of the model.
+ * @param
+ * the type of the list elements
+ */
+@FunctionalInterface
+public interface ListPropertyAccessor extends Function> {
+
+ /**
+ * @param model
+ * the model instance.
+ * @return the property field of the model.
+ */
+ @Override
+ ListProperty apply(M model);
+}
diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ListSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ListSetter.java
new file mode 100644
index 000000000..62d71fcfa
--- /dev/null
+++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ListSetter.java
@@ -0,0 +1,25 @@
+package de.saxsys.mvvmfx.utils.mapping.accessorfunctions;
+
+import java.util.List;
+import java.util.function.BiConsumer;
+
+/**
+ * A functional interface to define a setter method of a list type.
+ *
+ * @param
+ * the generic type of the model.
+ * @param
+ * the type of the list elements.
+ */
+@FunctionalInterface
+public interface ListSetter extends BiConsumer> {
+
+ /**
+ * @param model
+ * the model instance.
+ * @param value
+ * the new value to be set.
+ */
+ @Override
+ void accept(M model, List value);
+}
diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenter.java
index 4eba99b6c..23b6f2892 100644
--- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenter.java
+++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenter.java
@@ -15,17 +15,18 @@
******************************************************************************/
package de.saxsys.mvvmfx.utils.notifications;
+import de.saxsys.mvvmfx.ViewModel;
+import javafx.application.Platform;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
-import de.saxsys.mvvmfx.ViewModel;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
/**
* Default implementation of {@link NotificationCenter}.
*
@@ -61,30 +62,54 @@ public void unsubscribe(NotificationObserver observer) {
public void publish(String messageName, Object... payload) {
publish(messageName, payload, globalObservers);
}
-
+
+ /**
+ * This notification will be send to the UI-Thread (if the UI-toolkit was bootstrapped).
+ * If no UI-Toolkit is available the notification will be directly published. This is typically the case in unit tests.
+ *
+ * @param viewModel the ViewModel
+ * @param messageName the message to sent
+ * @param payload additional arguments to the message
+ */
@Override
public void publish(ViewModel viewModel, String messageName, Object[] payload) {
- ObserverMap observerMap = viewModelObservers.get(viewModel);
- if (observerMap != null) {
- publish(messageName, payload, observerMap);
+ if(viewModelObservers.containsKey(viewModel)) {
+ final ObserverMap observerMap = viewModelObservers.get(viewModel);
+
+ if (Platform.isFxApplicationThread()) {
+ publish(messageName, payload, observerMap);
+ } else {
+ try {
+ Platform.runLater(() -> publish(messageName, payload, observerMap));
+ } catch(IllegalStateException e) {
+
+ // If the toolkit isn't initialized yet we will publish the notification directly.
+ // In most cases this means that we are in a unit test and not JavaFX application is running.
+ if(e.getMessage().equals("Toolkit not initialized")) {
+ publish(messageName, payload, observerMap);
+ } else {
+ throw e;
+ }
+ }
+ }
}
}
@Override
- public void subscribe(ViewModel view, String messageName, NotificationObserver observer) {
- ObserverMap observerMap = viewModelObservers.get(view);
- if (observerMap == null) {
- observerMap = new ObserverMap();
- viewModelObservers.put(view, observerMap);
+ public void subscribe(ViewModel viewModel, String messageName, NotificationObserver observer) {
+ if(!viewModelObservers.containsKey(viewModel)) {
+ viewModelObservers.put(viewModel, new ObserverMap());
}
+
+ final ObserverMap observerMap = viewModelObservers.get(viewModel);
addObserver(messageName, observer, observerMap);
}
@Override
- public void unsubscribe(ViewModel view, String messageName, NotificationObserver observer) {
- ObserverMap observerMap = viewModelObservers.get(view);
- if (observerMap != null) {
+ public void unsubscribe(ViewModel viewModel, String messageName, NotificationObserver observer) {
+ if(viewModelObservers.containsKey(viewModel)) {
+ final ObserverMap observerMap = viewModelObservers.get(viewModel);
removeObserversForMessageName(messageName, observer, observerMap);
}
}
@@ -92,8 +117,10 @@ public void unsubscribe(ViewModel view, String messageName, NotificationObserver
@Override
public void unsubscribe(ViewModel viewModel, NotificationObserver observer) {
- ObserverMap observerMap = viewModelObservers.get(viewModel);
- removeObserverFromObserverMap(observer, observerMap);
+ if(viewModelObservers.containsKey(viewModel)){
+ ObserverMap observerMap = viewModelObservers.get(viewModel);
+ removeObserverFromObserverMap(observer, observerMap);
+ }
}
/*
@@ -103,18 +130,22 @@ public void unsubscribe(ViewModel viewModel, NotificationObserver observer) {
private void publish(String messageName, Object[] payload, ObserverMap observerMap) {
Collection notificationReceivers = observerMap.get(messageName);
if (notificationReceivers != null) {
- for (NotificationObserver observer : notificationReceivers) {
+
+ // make a copy to prevent ConcurrentModificationException if inside of an observer a new observer is subscribed.
+ final Collection copy = new ArrayList<>(notificationReceivers);
+
+ for (NotificationObserver observer : copy) {
observer.receivedNotification(messageName, payload);
}
}
}
private void addObserver(String messageName, NotificationObserver observer, ObserverMap observerMap) {
- List observers = observerMap.get(messageName);
- if (observers == null) {
- observerMap.put(messageName, new ArrayList());
+ if(!observerMap.containsKey(messageName)) {
+ observerMap.put(messageName, new ArrayList<>());
}
- observers = observerMap.get(messageName);
+
+ final List observers = observerMap.get(messageName);
if(observers.contains(observer)) {
LOG.warn("Subscribe the observer ["+ observer + "] for the message [" + messageName +
@@ -129,21 +160,19 @@ private void removeObserverFromObserverMap(NotificationObserver observer, Observ
for (String key : observerMap.keySet()) {
final List observers = observerMap.get(key);
- final List observersToBeRemoved = observers
- .stream()
- .filter(actualObserver -> actualObserver.equals(observer))
- .collect(Collectors.toList());
-
- observers.removeAll(observersToBeRemoved);
+ observers.removeIf(actualObserver -> actualObserver.equals(observer));
}
}
private void removeObserversForMessageName(String messageName, NotificationObserver observer,
ObserverMap observerMap) {
- List observers = observerMap.get(messageName);
- observers.remove(observer);
- if (observers.size() == 0) {
- observerMap.remove(messageName);
+
+ if(observerMap.containsKey(messageName)) {
+ final List observers = observerMap.get(messageName);
+ observers.removeIf(actualObserver -> actualObserver.equals(observer));
+ if (observers.size() == 0) {
+ observerMap.remove(messageName);
+ }
}
}
diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenterFactory.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenterFactory.java
index ec3481254..d220a20d0 100644
--- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenterFactory.java
+++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenterFactory.java
@@ -19,11 +19,16 @@
* This class is used to get instances of the {@link NotificationCenter} interface.
*/
public class NotificationCenterFactory {
-
- private static NotificationCenter singleton = new DefaultNotificationCenter();
-
+
+ private static final NotificationCenter defaultNotificationCenter = new DefaultNotificationCenter();
+ private static NotificationCenter currentNotificationCenter = defaultNotificationCenter;
+
public static NotificationCenter getNotificationCenter() {
- return singleton;
+ return currentNotificationCenter;
}
-
+
+ public static void setNotificationCenter(final NotificationCenter notificationCenter) {
+ currentNotificationCenter = notificationCenter;
+ }
+
}
diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/viewlist/CachedViewModelCellFactory.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/viewlist/CachedViewModelCellFactory.java
index 68a925e4c..7144fafb3 100644
--- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/viewlist/CachedViewModelCellFactory.java
+++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/viewlist/CachedViewModelCellFactory.java
@@ -1,5 +1,8 @@
package de.saxsys.mvvmfx.utils.viewlist;
+import de.saxsys.mvvmfx.FluentViewLoader;
+import de.saxsys.mvvmfx.FxmlView;
+import de.saxsys.mvvmfx.JavaView;
import de.saxsys.mvvmfx.ViewModel;
import de.saxsys.mvvmfx.ViewTuple;
import de.saxsys.mvvmfx.internal.viewloader.View;
@@ -13,7 +16,8 @@
/**
*
* An implementation of the {@link ViewListCellFactory} that can be used for {@link ListView}s that are based on a list
- * of ViewModels. Additionally this CellFactory has a cache for {@link ViewTuple}s that where already loaded before.
+ * of ViewModels. Additionally this CellFactory has a cache for {@link ViewTuple}s that where already loaded before.
+ *
*
*
* This can be useful because the ListView can call the CellFactory not only when the items list changes but also on
@@ -24,22 +28,37 @@
*
*
* public class OverviewView implements FxmlView{@code } {
- *
+ *
* {@literal @}FXML
* public ListView{@code } itemList;
* {@literal @}InjectViewModel
* private OverviewViewModel viewModel;
- *
+ *
* public void initialize(){
* itemList.setItems(viewModel.itemsProperty());
- *
- * itemList.setCellFactory(CachedViewModelCellFactory.create(
- * vm -> FluentViewLoader.fxmlView(ItemView.class).viewModel(vm).load()));
+ *
+ * itemList.setCellFactory(CachedViewModelCellFactory.createForFxmlView(ItemView.class));
* }
* }
- *
*
*
+ * The example above uses the {@link #createForFxmlView(Class)} factory method for a specific View type.
+ *
+ *
+ *
+ * If you need more control over the loading process (like providing custom resourceBundles) you can use the
+ * {@link #create(Callback)} method instead. This method takes a {@link Callback} as argument. This callback gets an
+ * instance of the {@link ViewModel} as argument and has to return a {@link ViewTuple} for this viewModel. This means
+ * that you have to call the {@link FluentViewLoader} by yourself.
+ *
+ *
+ *
+ * See the following example which is equivalent to the one above:
+ *
+ *
+ * itemList.setCellFactory(CachedViewModelCellFactory.create(
+ * vm -> FluentViewLoader.fxmlView(ItemView.class).viewModel(vm).load()));
+ *
*
*
* @author manuel.mauky
@@ -70,4 +89,15 @@ public static , VM extends ViewModel> CachedViewModelCellFact
Callback> callback) {
return new CachedViewModelCellFactory<>(callback);
}
+
+ public static , VM extends ViewModel> CachedViewModelCellFactory createForFxmlView(
+ Class viewType) {
+ return create(vm -> FluentViewLoader.fxmlView(viewType).viewModel(vm).load());
+ }
+
+ public static , VM extends ViewModel> CachedViewModelCellFactory createForJavaView(
+ Class viewType) {
+ return create(vm -> FluentViewLoader.javaView(viewType).viewModel(vm).load());
+ }
+
}
diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/viewlist/ViewListCell.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/viewlist/ViewListCell.java
index af4aa8f8e..52aea87a0 100644
--- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/viewlist/ViewListCell.java
+++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/viewlist/ViewListCell.java
@@ -31,7 +31,7 @@
* @param
* which is used to create get the {@link ViewTuple}
*/
-abstract class ViewListCell extends ListCell implements
+public abstract class ViewListCell extends ListCell implements
ViewTupleMapper {
@Override
diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/viewlist/ViewTupleMapper.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/viewlist/ViewTupleMapper.java
index e524ac27d..8e77ca0f7 100644
--- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/viewlist/ViewTupleMapper.java
+++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/viewlist/ViewTupleMapper.java
@@ -37,6 +37,6 @@ public interface ViewTupleMapper {
* to map
* @return created {@link ViewTuple}
*/
- public abstract ViewTuple extends View, ? extends ViewModel> map(T element);
+ ViewTuple extends View, ? extends ViewModel> map(T element);
}
diff --git a/mvvmfx/src/test/java/FxmlViewinDefaultPackageTest.java b/mvvmfx/src/test/java/FxmlViewinDefaultPackageTest.java
index 05839af9c..d5cc68d31 100644
--- a/mvvmfx/src/test/java/FxmlViewinDefaultPackageTest.java
+++ b/mvvmfx/src/test/java/FxmlViewinDefaultPackageTest.java
@@ -1,7 +1,7 @@
-import de.saxsys.javafx.test.JfxRunner;
import de.saxsys.mvvmfx.FluentViewLoader;
import de.saxsys.mvvmfx.ViewTuple;
import de.saxsys.mvvmfx.internal.viewloader.example.TestViewModel;
+import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_FxmlView_Test.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_FxmlView_Test.java
index 6cdc70edc..23af3ed48 100644
--- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_FxmlView_Test.java
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_FxmlView_Test.java
@@ -15,14 +15,18 @@
******************************************************************************/
package de.saxsys.mvvmfx.internal.viewloader;
-import de.saxsys.javafx.test.JfxRunner;
import de.saxsys.mvvmfx.FluentViewLoader;
+import de.saxsys.mvvmfx.InjectViewModel;
+import de.saxsys.mvvmfx.MvvmFX;
+import de.saxsys.mvvmfx.ViewModel;
import de.saxsys.mvvmfx.ViewTuple;
import de.saxsys.mvvmfx.internal.viewloader.example.*;
import de.saxsys.mvvmfx.testingutils.ExceptionUtils;
+import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner;
import javafx.fxml.LoadException;
import javafx.scene.layout.VBox;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -126,7 +130,6 @@ public void testViewWithoutViewModelType() {
.getCodeBehind();
assertThat(codeBehind.wasInitialized).isTrue();
- assertThat(codeBehind.viewModel).isNull();
}
@Test
@@ -272,6 +275,29 @@ public void testThrowExceptionWhenMoreThenOneViewModelIsDefinedInFxmlView() {
}
+ @Test
+ public void testThrowExceptionWhenWrongViewModelTypeIsInjected() {
+ try {
+ FluentViewLoader.fxmlView(TestFxmlViewWithWrongInjectedViewModel.class).load();
+ fail("Expected an Exception");
+ } catch (Exception e) {
+ assertThat(ExceptionUtils.getRootCause(e)).isInstanceOf(RuntimeException.class).hasMessageContaining("field doesn't match the generic ViewModel type ");
+ }
+ }
+
+ /**
+ * The {@link InjectViewModel} annotation may only be used on fields whose Type are implementing {@link ViewModel}.
+ */
+ @Test
+ public void testThrowExceptionWhenInjectViewModelAnnotationIsUsedOnOtherType() {
+ try {
+ FluentViewLoader.fxmlView(TestFxmlViewWithWrongAnnotationUsage.class).load();
+ fail("Expected an Exception");
+ } catch (Exception e) {
+ assertThat(ExceptionUtils.getRootCause(e)).isInstanceOf(RuntimeException.class).hasMessageContaining("doesn't implement the 'ViewModel' interface");
+ }
+ }
+
/**
* When a mvvmFX view A is part of another mvvmFX view B (i.e. referenced in the fxml file of B) we have to verify
* that both A and B are correctly initialized and that the viewModels are injected.
@@ -341,4 +367,41 @@ public void testViewModelIsAvailableInViewTupleEvenIfItIsntInjectedInTheView() {
assertThat(viewTuple.getViewModel()).isNotNull();
}
+
+ /**
+ * This test reproduces the bug #292
+ * Given the following conditions:
+ * 1. The View has no ViewModel field and not injection of the ViewModel.
+ * 2. While loading an existing ViewModel instance is passed to the {@link FluentViewLoader}
+ *
+ * Under this conditions the ViewLoader was still creating a new ViewModel instance or retrieved an instance
+ * from DI. This isn't expected because the user has passed an existing ViewModel instance into the ViewLoader.
+ *
+ */
+ @Test
+ public void testExistingViewModelWithoutInjectionInView() {
+ DependencyInjector.getInstance().setCustomInjector(type -> {
+ if(type.equals(TestViewModel.class)) {
+ fail("An instance of TestViewModel was requested!");
+ throw new IllegalStateException("An instance of TestViewModel was requested!");
+ } else {
+ try {
+ return type.newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+
+
+ TestViewModel viewModel = new TestViewModel();
+
+ final ViewTuple viewTuple = FluentViewLoader
+ .fxmlView(TestFxmlViewWithoutViewModelField.class).viewModel(viewModel).load();
+
+ assertThat(viewTuple.getViewModel()).isEqualTo(viewModel);
+
+ // we need to reset the DI
+ DependencyInjector.getInstance().setCustomInjector(null);
+ }
}
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_JavaView_Test.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_JavaView_Test.java
index 156714c91..bcb23bf2c 100644
--- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_JavaView_Test.java
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_JavaView_Test.java
@@ -12,8 +12,12 @@
import java.util.ResourceBundle;
import de.saxsys.mvvmfx.FluentViewLoader;
+import de.saxsys.mvvmfx.ViewModel;
import de.saxsys.mvvmfx.ViewTuple;
+import de.saxsys.mvvmfx.internal.viewloader.example.TestViewModelA;
+import de.saxsys.mvvmfx.internal.viewloader.example.TestViewModelB;
import de.saxsys.mvvmfx.internal.viewloader.example.TestViewModelWithResourceBundle;
+import de.saxsys.mvvmfx.testingutils.ExceptionUtils;
import javafx.fxml.Initializable;
import javafx.scene.layout.VBox;
@@ -221,8 +225,42 @@ class TestView extends VBox implements JavaView {
assertThat(e).isInstanceOf(RuntimeException.class).hasMessageContaining("<2> viewModel fields");
}
}
-
-
+
+ @Test
+ public void testThrowExceptionWhenWrongViewModelTypeIsInjected() {
+ class TestView extends VBox implements JavaView {
+ @InjectViewModel
+ public TestViewModelB viewModel;
+ }
+
+
+ try {
+ FluentViewLoader.javaView(TestView.class).load();
+ fail("Expected an Exception");
+ } catch (Exception e) {
+ assertThat(ExceptionUtils.getRootCause(e)).isInstanceOf(RuntimeException.class).hasMessageContaining("field doesn't match the generic ViewModel type ");
+ }
+ }
+
+ /**
+ * The {@link InjectViewModel} annotation may only be used on fields whose Type are implementing {@link ViewModel}.
+ */
+ @Test
+ public void testThrowExceptionWhenInjectViewModelAnnotationIsUsedOnOtherType() {
+ class TestView extends VBox implements JavaView {
+ @InjectViewModel
+ public Object viewModel;
+ }
+
+
+ try {
+ FluentViewLoader.javaView(TestView.class).load();
+ fail("Expected an Exception");
+ } catch (Exception e) {
+ Throwable rootCause = ExceptionUtils.getRootCause(e);
+ assertThat(rootCause).isInstanceOf(RuntimeException.class).hasMessageContaining("doesn't implement the 'ViewModel' interface");
+ }
+ }
/**
* When the ViewModel isn't injected in the view it should still be available in the ViewTuple.
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithWrongAnnotationUsage.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithWrongAnnotationUsage.java
new file mode 100644
index 000000000..b77c81bc9
--- /dev/null
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithWrongAnnotationUsage.java
@@ -0,0 +1,12 @@
+package de.saxsys.mvvmfx.internal.viewloader.example;
+
+import de.saxsys.mvvmfx.FxmlView;
+import de.saxsys.mvvmfx.InjectViewModel;
+
+public class TestFxmlViewWithWrongAnnotationUsage implements FxmlView {
+
+ // No ViewModel type
+ @InjectViewModel
+ private Object something;
+
+}
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithWrongInjectedViewModel.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithWrongInjectedViewModel.java
new file mode 100644
index 000000000..35834946c
--- /dev/null
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithWrongInjectedViewModel.java
@@ -0,0 +1,12 @@
+package de.saxsys.mvvmfx.internal.viewloader.example;
+
+import de.saxsys.mvvmfx.FxmlView;
+import de.saxsys.mvvmfx.InjectViewModel;
+
+public class TestFxmlViewWithWrongInjectedViewModel implements FxmlView {
+
+ // Wrong view model type
+ @InjectViewModel
+ private TestViewModelB viewModel;
+
+}
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithoutViewModelType.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithoutViewModelType.java
index 76e9cd92f..7750208bf 100644
--- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithoutViewModelType.java
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithoutViewModelType.java
@@ -9,10 +9,6 @@
public class TestFxmlViewWithoutViewModelType implements FxmlView, Initializable {
- // this injection point will be ignored as this view class doesn't define a ViewModelType
- @InjectViewModel
- public TestViewModel viewModel;
-
public boolean wasInitialized = false;
@Override
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/global/GlobalResourceBundleTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/global/GlobalResourceBundleTest.java
index dcb0567bd..1b9855247 100644
--- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/global/GlobalResourceBundleTest.java
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/global/GlobalResourceBundleTest.java
@@ -1,9 +1,9 @@
package de.saxsys.mvvmfx.resourcebundle.global;
-import de.saxsys.javafx.test.JfxRunner;
import de.saxsys.mvvmfx.FluentViewLoader;
import de.saxsys.mvvmfx.MvvmFX;
import de.saxsys.mvvmfx.ViewTuple;
+import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/included/IncludedViewsTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/included/IncludedViewsTest.java
index 8ba68bcd0..e8c885139 100644
--- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/included/IncludedViewsTest.java
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/included/IncludedViewsTest.java
@@ -1,9 +1,9 @@
package de.saxsys.mvvmfx.resourcebundle.included;
-import de.saxsys.javafx.test.JfxRunner;
import de.saxsys.mvvmfx.FluentViewLoader;
import de.saxsys.mvvmfx.MvvmFX;
import de.saxsys.mvvmfx.ViewTuple;
+import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CompositeCommandTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CompositeCommandTest.java
index ad9d7b103..adf262f0c 100644
--- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CompositeCommandTest.java
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CompositeCommandTest.java
@@ -1,7 +1,6 @@
package de.saxsys.mvvmfx.utils.commands;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.fail;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -9,6 +8,7 @@
import java.util.concurrent.TimeUnit;
import com.cedarsoft.test.utils.CatchAllExceptionsRule;
+import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@@ -16,11 +16,11 @@
import javafx.beans.value.ObservableValue;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import de.saxsys.javafx.test.JfxRunner;
import de.saxsys.mvvmfx.testingutils.GCVerifier;
@RunWith(JfxRunner.class)
@@ -155,6 +155,7 @@ public void allCommandsAreUnregistered() throws Exception {
compositeCommand.unregister(delegateCommand2);
}
+ @Ignore("unstable test. Needs to be fixed. see bug #260")
@Test
public void longRunningAsyncComposite() throws Exception {
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/DelegateCommandTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/DelegateCommandTest.java
index 2e4d3346d..f888d7416 100644
--- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/DelegateCommandTest.java
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/DelegateCommandTest.java
@@ -1,7 +1,6 @@
package de.saxsys.mvvmfx.utils.commands;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.fail;
import static org.assertj.core.api.Assertions.offset;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -10,21 +9,17 @@
import java.util.concurrent.TimeUnit;
import com.cedarsoft.test.utils.CatchAllExceptionsRule;
-import de.saxsys.javafx.test.TestInJfxThread;
-import javafx.application.Application;
+import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
-import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import de.saxsys.javafx.test.JfxRunner;
-import org.junit.runners.JUnit4;
@RunWith(JfxRunner.class)
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/ItemListTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/ItemListTest.java
index 8ed5c9005..e1337c2bf 100644
--- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/ItemListTest.java
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/ItemListTest.java
@@ -16,12 +16,19 @@
package de.saxsys.mvvmfx.utils.itemlist;
import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
/**
* Tests for {@link ItemList}.
*
@@ -143,5 +150,36 @@ public void replaceItemInItemListAtIndex() {
Assert.assertEquals(PREFIX + "replacedPerson", itemList
.stringListProperty().get(1));
}
-
+
+ /**
+ * This test is used to reproduce the bug #281.
+ *
+ * When multiple elements are added to the list in a single method call ({@link ObservableList#addAll(Object[])},
+ * only a single change event should be fired.
+ */
+ @Test
+ public void addMultipleItemsEventListener() {
+ AtomicInteger counter = new AtomicInteger(0);
+
+ itemList.getTargetList().addListener((ListChangeListener) c -> {
+ counter.incrementAndGet();
+ });
+
+ listWithModelObjects.addAll(new Person("one"), new Person("two"), new Person("three"));
+
+ assertThat(counter.get()).isEqualTo(1);
+ }
+
+ @Test
+ public void removeMultipleItemsEventListener() {
+ AtomicInteger counter = new AtomicInteger(0);
+
+ itemList.getTargetList().addListener((ListChangeListener) c -> {
+ counter.incrementAndGet();
+ });
+
+ listWithModelObjects.removeAll(person1, person3);
+
+ assertThat(counter.get()).isEqualTo(1);
+ }
}
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ExampleModel.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ExampleModel.java
index bd5b556f6..3e85d2440 100644
--- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ExampleModel.java
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ExampleModel.java
@@ -2,6 +2,8 @@
import javafx.beans.property.*;
+import java.util.List;
+
public class ExampleModel {
private IntegerProperty integerProperty = new SimpleIntegerProperty();
@@ -14,6 +16,8 @@ public class ExampleModel {
private ObjectProperty objectProperty = new SimpleObjectProperty<>();
private BooleanProperty booleanProperty = new SimpleBooleanProperty();
+
+ private ListProperty listProperty = new SimpleListProperty<>();
public int getInteger() {
@@ -99,4 +103,16 @@ public BooleanProperty booleanProperty() {
public void setBoolean(boolean booleanProperty) {
this.booleanProperty.set(booleanProperty);
}
+
+ public List getList() {
+ return listProperty.get();
+ }
+
+ public ListProperty listProperty() {
+ return listProperty;
+ }
+
+ public void setList(List listProperty) {
+ this.listProperty.setAll(listProperty);
+ }
}
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapperTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapperTest.java
index 5f79e487b..45cadbef1 100644
--- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapperTest.java
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapperTest.java
@@ -1,9 +1,13 @@
package de.saxsys.mvvmfx.utils.mapping;
import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.ListProperty;
import javafx.beans.property.StringProperty;
+import javafx.collections.FXCollections;
import org.junit.Test;
+import java.util.Arrays;
+
import static org.assertj.core.api.Assertions.assertThat;
public class ModelWrapperTest {
@@ -14,22 +18,27 @@ public void testWithGetterAndSetter() {
Person person = new Person();
person.setName("horst");
person.setAge(32);
+ person.setNicknames(Arrays.asList("captain"));
ModelWrapper personWrapper = new ModelWrapper<>(person);
final StringProperty nameProperty = personWrapper.field(Person::getName, Person::setName);
final IntegerProperty ageProperty = personWrapper.field(Person::getAge, Person::setAge);
+ final ListProperty nicknamesProperty = personWrapper.field(Person::getNicknames, Person::setNicknames);
assertThat(nameProperty.getValue()).isEqualTo("horst");
assertThat(ageProperty.getValue()).isEqualTo(32);
+ assertThat(nicknamesProperty.getValue()).containsOnly("captain");
nameProperty.setValue("hugo");
ageProperty.setValue(33);
+ nicknamesProperty.add("player");
// still the old values
assertThat(person.getName()).isEqualTo("horst");
assertThat(person.getAge()).isEqualTo(32);
+ assertThat(person.getNicknames()).containsOnly("captain");
personWrapper.commit();
@@ -37,50 +46,60 @@ public void testWithGetterAndSetter() {
// now the new values are reflected in the wrapped person
assertThat(person.getName()).isEqualTo("hugo");
assertThat(person.getAge()).isEqualTo(33);
+ assertThat(person.getNicknames()).containsOnly("captain", "player");
nameProperty.setValue("luise");
ageProperty.setValue(15);
+ nicknamesProperty.setValue(FXCollections.observableArrayList("student"));
personWrapper.reset();
assertThat(nameProperty.getValue()).isEqualTo(null);
assertThat(ageProperty.getValue()).isEqualTo(0);
+ assertThat(nicknamesProperty.getValue().size()).isEqualTo(0);
// the wrapped object has still the values from the last commit.
assertThat(person.getName()).isEqualTo("hugo");
assertThat(person.getAge()).isEqualTo(33);
+ assertThat(person.getNicknames()).containsOnly("captain", "player");
personWrapper.reload();
// now the properties have the values from the wrapped object
assertThat(nameProperty.getValue()).isEqualTo("hugo");
assertThat(ageProperty.getValue()).isEqualTo(33);
+ assertThat(nicknamesProperty.get()).containsOnly("captain", "player");
Person otherPerson = new Person();
otherPerson.setName("gisela");
otherPerson.setAge(23);
+ otherPerson.setNicknames(Arrays.asList("referee"));
personWrapper.set(otherPerson);
personWrapper.reload();
assertThat(nameProperty.getValue()).isEqualTo("gisela");
assertThat(ageProperty.getValue()).isEqualTo(23);
+ assertThat(nicknamesProperty.getValue()).containsOnly("referee");
nameProperty.setValue("georg");
ageProperty.setValue(24);
+ nicknamesProperty.setValue(FXCollections.observableArrayList("spectator"));
personWrapper.commit();
// old person has still the old values
assertThat(person.getName()).isEqualTo("hugo");
assertThat(person.getAge()).isEqualTo(33);
+ assertThat(person.getNicknames()).containsOnly("captain", "player");
// new person has the new values
assertThat(otherPerson.getName()).isEqualTo("georg");
assertThat(otherPerson.getAge()).isEqualTo(24);
+ assertThat(otherPerson.getNicknames()).containsOnly("spectator");
}
@@ -90,23 +109,28 @@ public void testWithJavaFXPropertiesField() {
PersonFX person = new PersonFX();
person.setName("horst");
person.setAge(32);
+ person.setNicknames(Arrays.asList("captain"));
ModelWrapper personWrapper = new ModelWrapper<>(person);
final StringProperty nameProperty = personWrapper.field(PersonFX::nameProperty);
final IntegerProperty ageProperty = personWrapper.field(PersonFX::ageProperty);
+ final ListProperty nicknamesProperty = personWrapper.field(PersonFX::nicknamesProperty);
assertThat(nameProperty.getValue()).isEqualTo("horst");
assertThat(ageProperty.getValue()).isEqualTo(32);
+ assertThat(nicknamesProperty.getValue()).containsOnly("captain");
nameProperty.setValue("hugo");
ageProperty.setValue(33);
+ nicknamesProperty.add("player");
// still the old values
assertThat(person.getName()).isEqualTo("horst");
assertThat(person.getAge()).isEqualTo(32);
+ assertThat(person.getNicknames()).containsOnly("captain");
personWrapper.commit();
@@ -114,46 +138,55 @@ public void testWithJavaFXPropertiesField() {
// now the new values are reflected in the wrapped person
assertThat(person.getName()).isEqualTo("hugo");
assertThat(person.getAge()).isEqualTo(33);
+ assertThat(person.getNicknames()).containsOnly("captain", "player");
nameProperty.setValue("luise");
ageProperty.setValue(15);
+ nicknamesProperty.setValue(FXCollections.observableArrayList("student"));
personWrapper.reset();
assertThat(nameProperty.getValue()).isEqualTo(null);
assertThat(ageProperty.getValue()).isEqualTo(0);
+ assertThat(nicknamesProperty.getValue()).isEmpty();
// the wrapped object has still the values from the last commit.
assertThat(person.getName()).isEqualTo("hugo");
assertThat(person.getAge()).isEqualTo(33);
+ assertThat(person.getNicknames()).containsOnly("captain", "player");
personWrapper.reload();
// now the properties have the values from the wrapped object
assertThat(nameProperty.getValue()).isEqualTo("hugo");
assertThat(ageProperty.getValue()).isEqualTo(33);
+ assertThat(nicknamesProperty.get()).containsOnly("captain", "player");
PersonFX otherPerson = new PersonFX();
otherPerson.setName("gisela");
otherPerson.setAge(23);
+ otherPerson.setNicknames(Arrays.asList("referee"));
personWrapper.set(otherPerson);
personWrapper.reload();
assertThat(nameProperty.getValue()).isEqualTo("gisela");
assertThat(ageProperty.getValue()).isEqualTo(23);
+ assertThat(nicknamesProperty.get()).containsOnly("referee");
nameProperty.setValue("georg");
ageProperty.setValue(24);
+ nicknamesProperty.setValue(FXCollections.observableArrayList("spectator"));
personWrapper.commit();
// old person has still the old values
assertThat(person.getName()).isEqualTo("hugo");
assertThat(person.getAge()).isEqualTo(33);
+ assertThat(person.getNicknames()).containsOnly("captain", "player");
// new person has the new values
assertThat(otherPerson.getName()).isEqualTo("georg");
@@ -165,19 +198,25 @@ public void testIdentifiedFields() {
Person person = new Person();
person.setName("horst");
person.setAge(32);
+ person.setNicknames(Arrays.asList("captain"));
ModelWrapper personWrapper = new ModelWrapper<>();
final StringProperty nameProperty = personWrapper.field("name", Person::getName, Person::setName);
final IntegerProperty ageProperty = personWrapper.field("age", Person::getAge, Person::setAge);
+ final ListProperty nicknamesProperty = personWrapper.field("nicknames", Person::getNicknames,
+ Person::setNicknames);
final StringProperty nameProperty2 = personWrapper.field("name", Person::getName, Person::setName);
final IntegerProperty ageProperty2 = personWrapper.field("age", Person::getAge, Person::setAge);
+ final ListProperty nicknamesProperty2 = personWrapper.field("nicknames", Person::getNicknames,
+ Person::setNicknames);
assertThat(nameProperty).isSameAs(nameProperty2);
assertThat(ageProperty).isSameAs(ageProperty2);
+ assertThat(nicknamesProperty).isSameAs(nicknamesProperty2);
}
@@ -186,6 +225,7 @@ public void testDirtyFlag() {
Person person = new Person();
person.setName("horst");
person.setAge(32);
+ person.setNicknames(Arrays.asList("captain"));
ModelWrapper personWrapper = new ModelWrapper<>(person);
@@ -193,6 +233,7 @@ public void testDirtyFlag() {
final StringProperty name = personWrapper.field(Person::getName, Person::setName);
final IntegerProperty age = personWrapper.field(Person::getAge, Person::setAge);
+ final ListProperty nicknames = personWrapper.field(Person::getNicknames, Person::setNicknames);
name.set("hugo");
@@ -211,6 +252,15 @@ public void testDirtyFlag() {
assertThat(personWrapper.isDirty()).isFalse();
+ nicknames.add("player");
+ assertThat(personWrapper.isDirty()).isTrue();
+
+ nicknames.remove("player");
+ assertThat(personWrapper.isDirty()).isTrue(); // dirty is still true
+
+ personWrapper.commit();
+ assertThat(personWrapper.isDirty()).isFalse();
+
name.set("hans");
assertThat(personWrapper.isDirty()).isTrue();
@@ -221,6 +271,15 @@ public void testDirtyFlag() {
personWrapper.reload();
assertThat(personWrapper.isDirty()).isFalse();
+ nicknames.set(FXCollections.observableArrayList("player"));
+ assertThat(personWrapper.isDirty()).isTrue();
+
+ personWrapper.reset();
+ assertThat(personWrapper.isDirty()).isTrue();
+
+ personWrapper.reload();
+ assertThat(personWrapper.isDirty()).isFalse();
+
}
@Test
@@ -235,6 +294,7 @@ public void testDirtyFlagWithFxProperties() {
final StringProperty name = personWrapper.field(PersonFX::nameProperty);
final IntegerProperty age = personWrapper.field(PersonFX::ageProperty);
+ final ListProperty nicknames = personWrapper.field(PersonFX::nicknamesProperty);
name.set("hugo");
@@ -253,6 +313,15 @@ public void testDirtyFlagWithFxProperties() {
assertThat(personWrapper.isDirty()).isFalse();
+ nicknames.add("player");
+ assertThat(personWrapper.isDirty()).isTrue();
+
+ nicknames.remove("player");
+ assertThat(personWrapper.isDirty()).isTrue(); // dirty is still true
+
+ personWrapper.commit();
+ assertThat(personWrapper.isDirty()).isFalse();
+
name.set("hans");
assertThat(personWrapper.isDirty()).isTrue();
@@ -263,6 +332,14 @@ public void testDirtyFlagWithFxProperties() {
personWrapper.reload();
assertThat(personWrapper.isDirty()).isFalse();
+ nicknames.set(FXCollections.observableArrayList("player"));
+ assertThat(personWrapper.isDirty()).isTrue();
+
+ personWrapper.reset();
+ assertThat(personWrapper.isDirty()).isTrue();
+
+ personWrapper.reload();
+ assertThat(personWrapper.isDirty()).isFalse();
}
@Test
@@ -270,6 +347,7 @@ public void testDifferentFlag() {
Person person = new Person();
person.setName("horst");
person.setAge(32);
+ person.setNicknames(Arrays.asList("captain"));
ModelWrapper personWrapper = new ModelWrapper<>(person);
@@ -277,6 +355,7 @@ public void testDifferentFlag() {
final StringProperty name = personWrapper.field(Person::getName, Person::setName);
final IntegerProperty age = personWrapper.field(Person::getAge, Person::setAge);
+ final ListProperty nicknames = personWrapper.field(Person::getNicknames, Person::setNicknames);
name.set("hugo");
@@ -293,6 +372,43 @@ public void testDifferentFlag() {
assertThat(personWrapper.isDifferent()).isFalse();
+ nicknames.remove("captain");
+ assertThat(personWrapper.isDifferent()).isTrue();
+
+ nicknames.remove("captain");
+ assertThat(personWrapper.isDifferent()).isTrue();
+
+ nicknames.add("captain");
+ assertThat(personWrapper.isDifferent()).isFalse();
+
+ nicknames.add("player");
+ assertThat(personWrapper.isDifferent()).isTrue();
+
+ nicknames.remove("player");
+ assertThat(personWrapper.isDifferent()).isFalse();
+
+ nicknames.setValue(FXCollections.observableArrayList("spectator"));
+ assertThat(personWrapper.isDifferent()).isTrue();
+
+ personWrapper.reload();
+ assertThat(personWrapper.isDifferent()).isFalse();
+
+ nicknames.add("captain");
+ assertThat(personWrapper.isDifferent()).isFalse();
+
+ nicknames.add("player");
+ assertThat(personWrapper.isDifferent()).isTrue();
+
+ nicknames.remove("player");
+ assertThat(personWrapper.isDifferent()).isFalse();
+
+ nicknames.setValue(FXCollections.observableArrayList("spectator"));
+ assertThat(personWrapper.isDifferent()).isTrue();
+
+ personWrapper.reload();
+ assertThat(personWrapper.isDifferent()).isFalse();
+
+
name.setValue("hans");
assertThat(personWrapper.isDifferent()).isTrue();
@@ -309,6 +425,7 @@ public void testDifferentFlagWithFxProperties() {
PersonFX person = new PersonFX();
person.setName("horst");
person.setAge(32);
+ person.setNicknames(Arrays.asList("captain"));
ModelWrapper personWrapper = new ModelWrapper<>(person);
@@ -316,6 +433,7 @@ public void testDifferentFlagWithFxProperties() {
final StringProperty name = personWrapper.field(PersonFX::nameProperty);
final IntegerProperty age = personWrapper.field(PersonFX::ageProperty);
+ final ListProperty nicknames = personWrapper.field(PersonFX::nicknamesProperty);
name.set("hugo");
@@ -332,6 +450,25 @@ public void testDifferentFlagWithFxProperties() {
assertThat(personWrapper.isDifferent()).isFalse();
+ nicknames.remove("captain");
+ assertThat(personWrapper.isDifferent()).isTrue();
+
+ nicknames.add("captain");
+ assertThat(personWrapper.isDifferent()).isFalse();
+
+ nicknames.add("player");
+ assertThat(personWrapper.isDifferent()).isTrue();
+
+ nicknames.remove("player");
+ assertThat(personWrapper.isDifferent()).isFalse();
+
+ nicknames.setValue(FXCollections.observableArrayList("spectator"));
+ assertThat(personWrapper.isDifferent()).isTrue();
+
+ personWrapper.reload();
+ assertThat(personWrapper.isDifferent()).isFalse();
+
+
name.setValue("hans");
assertThat(personWrapper.isDifferent()).isTrue();
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/Person.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/Person.java
index 3dc4d6297..f882a8afc 100644
--- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/Person.java
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/Person.java
@@ -1,11 +1,26 @@
package de.saxsys.mvvmfx.utils.mapping;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+import java.util.List;
+
public class Person {
private String name;
private int age;
+ private ObservableList nicknames = FXCollections.observableArrayList();
+
+ public List getNicknames() {
+ return nicknames;
+ }
+
+ public void setNicknames (List nicknames) {
+ this.nicknames.setAll(nicknames);
+ }
+
public int getAge() {
return age;
}
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/PersonFX.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/PersonFX.java
index 445c0a512..c6a7275af 100644
--- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/PersonFX.java
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/PersonFX.java
@@ -1,15 +1,22 @@
package de.saxsys.mvvmfx.utils.mapping;
import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
+import javafx.collections.FXCollections;
+
+import java.util.List;
public class PersonFX {
private StringProperty name = new SimpleStringProperty();
private IntegerProperty age = new SimpleIntegerProperty();
+
+ private ListProperty nicknames = new SimpleListProperty<>(FXCollections.observableArrayList());
public String getName() {
return name.get();
@@ -34,4 +41,16 @@ public IntegerProperty ageProperty() {
public void setAge(int age) {
this.age.set(age);
}
+
+ public List getNicknames() {
+ return nicknames.get();
+ }
+
+ public ListProperty nicknamesProperty() {
+ return nicknames;
+ }
+
+ public void setNicknames(List nicknames) {
+ this.nicknames.setAll(nicknames);
+ }
}
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ReturnTypeTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ReturnTypeTest.java
index f55777e09..2a752672d 100644
--- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ReturnTypeTest.java
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ReturnTypeTest.java
@@ -4,6 +4,9 @@
import org.junit.Before;
import org.junit.Test;
+import java.util.Arrays;
+import java.util.Collections;
+
/**
* This test is used to check the return values when fields are mapped. See Issue 211 https://github.com/sialcasa/mvvmFX/issues/211
@@ -129,4 +132,19 @@ public void objectProperty() {
new Person());
}
+ @Test
+ public void listProperty() {
+ final ListProperty beanField = wrapper.field(ExampleModel::getList, ExampleModel::setList);
+ final ListProperty fxField = wrapper.field(ExampleModel::listProperty);
+ final ListProperty beanFieldDefault = wrapper.field(ExampleModel::getList, ExampleModel::setList,
+ Collections.emptyList());
+ final ListProperty fxFieldDefault = wrapper.field(ExampleModel::listProperty, Arrays.asList());
+
+ final ListProperty idBeanField = wrapper.field("list1", ExampleModel::getList, ExampleModel::setList);
+ final ListProperty idFxField = wrapper.field("list2", ExampleModel::listProperty);
+ final ListProperty idBeanFieldDefault = wrapper.field("list3", ExampleModel::getList,
+ ExampleModel::setList, Collections.emptyList());
+ final ListProperty idFxFieldDefault = wrapper.field("list4", ExampleModel::listProperty,
+ Arrays.asList());
+ }
}
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ConcurrentModificationBugTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ConcurrentModificationBugTest.java
new file mode 100644
index 000000000..2293ba7a0
--- /dev/null
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ConcurrentModificationBugTest.java
@@ -0,0 +1,36 @@
+package de.saxsys.mvvmfx.utils.notifications;
+
+import org.junit.Test;
+
+
+/**
+ * Test-case to reproduce bug #289.
+ */
+public class ConcurrentModificationBugTest {
+ @Test
+ public void shouldAllowSubscribeDuringPublish() throws Exception {
+ NotificationCenter notificationCenter = NotificationCenterFactory.getNotificationCenter();
+
+ /* Minimal example (triggers a ConcurrentModificationException in NotificationCenter */
+ notificationCenter.subscribe("MSG", (key1, payload1) -> {
+ notificationCenter.subscribe("MSG", (key2, payload2) -> {
+ System.out.println("I want to subscribe during a publish()");
+ });
+ });
+
+ notificationCenter.publish("MSG");
+
+ /*
+ * Real use case:
+ *
+ * ParentView | | ChildView1 ChildView2
+ *
+ *
+ * ChildView1 publishes a global event that causes ParentView to create a new instance of ChildView2. ChildView2
+ * tries to subscribe to another global event in its initialize() method.
+ *
+ * Because NotificationCenter.publish() does not make a copy before publishing events, we get a
+ * ConcurrentModificationException.
+ */
+ }
+}
\ No newline at end of file
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenterTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenterTest.java
index 1b1afe823..22fb8bb4e 100644
--- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenterTest.java
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenterTest.java
@@ -16,11 +16,22 @@
package de.saxsys.mvvmfx.utils.notifications;
+import de.saxsys.mvvmfx.ViewModel;
+import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner;
+import javafx.application.Platform;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.mockito.Mockito;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@RunWith(JfxRunner.class)
public class DefaultNotificationCenterTest {
private static final String TEST_NOTIFICATION = "test_notification";
@@ -38,7 +49,7 @@ public void init() {
observer1 = Mockito.mock(DummyNotificationObserver.class);
observer2 = Mockito.mock(DummyNotificationObserver.class);
observer3 = Mockito.mock(DummyNotificationObserver.class);
- defaultCenter = Mockito.spy(new DefaultNotificationCenter());
+ defaultCenter = new DefaultNotificationCenter();
}
@Test
@@ -108,6 +119,64 @@ public void unsubscribeObserverThatWasSubscribedMultipleTimes() {
defaultCenter.publish(TEST_NOTIFICATION);
Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION);
}
+
+
+ /**
+ * This is the same as {@link #unsubscribeObserverThatWasSubscribedMultipleTimes()} with the
+ * difference that here we use the overloaded unsubscribe method {@link NotificationCenter#unsubscribe(String, NotificationObserver)} that takes
+ * the message key as first parameter.
+ */
+ @Test
+ public void unsubscribeObserverThatWasSubscribedMultipleTimesViaMessageName() {
+ defaultCenter.subscribe(TEST_NOTIFICATION, observer1);
+ defaultCenter.subscribe(TEST_NOTIFICATION, observer1);
+ defaultCenter.subscribe(TEST_NOTIFICATION, observer1);
+
+ defaultCenter.unsubscribe(TEST_NOTIFICATION, observer1);
+
+ defaultCenter.publish(TEST_NOTIFICATION);
+ Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION);
+ }
+
+
+ /**
+ * In some use cases it's convenient to unregister an observer even if it wasn't subscribed yet.
+ * For example to prevent a duplicated subscription.
+ *
+ * This test case reproduces bug #301.
+ */
+ @Test
+ public void removeObserverThatWasNotRegisteredYet() {
+ defaultCenter.unsubscribe(TEST_NOTIFICATION, observer1);
+ }
+
+ @Test
+ public void observerForViewModelIsCalledFromUiThread() throws InterruptedException, ExecutionException, TimeoutException {
+ // Check that there is a UI-Thread available. This JUnit-Test isn't running on the UI-Thread but there needs to
+ // be a UI-Thread available in the background.
+ CompletableFuture uiThreadIsAvailable = new CompletableFuture<>();
+ Platform.runLater(() -> uiThreadIsAvailable.complete(null)); // This would throw an IllegalStateException if no
+ // UI-Thread is available.
+ uiThreadIsAvailable.get(1l, TimeUnit.SECONDS);
+
+ CompletableFuture future = new CompletableFuture<>();
+
+ // The test doesn't run on the FX thread.
+ assertThat(Platform.isFxApplicationThread()).isFalse();
+
+ final ViewModel viewModel = Mockito.mock(ViewModel.class);
+ defaultCenter.subscribe(viewModel, TEST_NOTIFICATION, (key, payload) -> {
+ // the notification is executed on the FX thread.
+ future.complete(Platform.isFxApplicationThread());
+ });
+
+ // view model publish() should be executed in the UI-thread
+ defaultCenter.publish(viewModel, TEST_NOTIFICATION, new Object[]{});
+
+ final Boolean wasCalledOnUiThread = future.get(1l, TimeUnit.SECONDS);
+
+ assertThat(wasCalledOnUiThread).isTrue();
+ }
private class DummyNotificationObserver implements NotificationObserver {
@Override
diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ViewModelTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ViewModelTest.java
index b6ea747a9..453b9a223 100644
--- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ViewModelTest.java
+++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ViewModelTest.java
@@ -1,24 +1,19 @@
package de.saxsys.mvvmfx.utils.notifications;
-import de.saxsys.javafx.test.JfxRunner;
import de.saxsys.mvvmfx.MvvmFX;
import de.saxsys.mvvmfx.ViewModel;
+import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner;
import javafx.application.Platform;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
-import de.saxsys.mvvmfx.utils.notifications.NotificationObserver;
-
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
-import static org.assertj.core.api.Assertions.assertThat;
-
@RunWith(JfxRunner.class)
public class ViewModelTest {
@@ -38,37 +33,7 @@ public void init() {
viewModel = new ViewModel() {
};
}
-
- @Test
- public void observerIsCalledFromUiThread() throws InterruptedException, ExecutionException, TimeoutException {
- // Check that there is a UI-Thread available. This JUnit-Test isn't running on the UI-Thread but there needs to
- // be a UI-Thread available in the background.
- CompletableFuture uiThreadIsAvailable = new CompletableFuture<>();
- Platform.runLater(() -> uiThreadIsAvailable.complete(null)); // This would throw an IllegalStateException if no
- // UI-Thread is available.
- uiThreadIsAvailable.get(1l, TimeUnit.SECONDS);
-
-
-
- CompletableFuture future = new CompletableFuture<>();
-
- // The test doesn't run on the FX thread.
- assertThat(Platform.isFxApplicationThread()).isFalse();
-
- viewModel.subscribe(TEST_NOTIFICATION, (key, payload) -> {
- // the notification is executed on the FX thread.
- future.complete(Platform.isFxApplicationThread());
- });
-
- viewModel.publish(TEST_NOTIFICATION);
-
-
- final Boolean wasCalledOnUiThread = future.get(1l, TimeUnit.SECONDS);
-
- assertThat(wasCalledOnUiThread).isTrue();
- }
-
-
+
@Test
public void observerFromOutsideDoesNotReceiveNotifications() {
MvvmFX.getNotificationCenter().subscribe(TEST_NOTIFICATION, observer1);
@@ -136,6 +101,17 @@ public void addMultipleObserverAndRemoveOneAndPublish() throws Exception {
Mockito.verify(observer3).receivedNotification(TEST_NOTIFICATION,
OBJECT_ARRAY_FOR_NOTIFICATION);
}
+
+
+ /**
+ * See {@link DefaultNotificationCenterTest#removeObserverThatWasNotRegisteredYet()}.
+ */
+ @Test
+ public void removeObserverThatWasNotRegisteredYet() {
+ viewModel.unsubscribe(observer1);
+
+ viewModel.unsubscribe(TEST_NOTIFICATION, observer1);
+ }
/**
* This method is used to wait until the UI thread has done all work that was queued via
diff --git a/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithWrongAnnotationUsage.fxml b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithWrongAnnotationUsage.fxml
new file mode 100644
index 000000000..765bf2537
--- /dev/null
+++ b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithWrongAnnotationUsage.fxml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithWrongInjectedViewModel.fxml b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithWrongInjectedViewModel.fxml
new file mode 100644
index 000000000..5371ce87c
--- /dev/null
+++ b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithWrongInjectedViewModel.fxml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/pom.xml b/pom.xml
index a87170cdd..2d21f8a53 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,7 +26,7 @@
de.saxsys
mvvmfx-parent
pom
- 1.3.1
+ 1.4.0
mvvmFX parent
Application Framework for MVVM with JavaFX.
http://www.saxsys.de
@@ -149,7 +149,7 @@