diff --git a/.travis.yml b/.travis.yml index efa218ffb..3c1baf29b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,15 @@ language: java +sudo: required + jdk: -- oraclejdk8 + - oraclejdk8 + +install: true + +script: + - sudo apt-get update && sudo apt-get install oracle-java8-installer + - java -version before_install: - export DISPLAY=:99.0 diff --git a/README.md b/README.md index 34771b3ee..d018d895e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ __MVVM__ is the enhanced version of the [Presentation Model](http://martinfowler de.saxsys mvvmfx - 1.1.0 + 1.2.0 ``` @@ -24,7 +24,7 @@ __MVVM__ is the enhanced version of the [Presentation Model](http://martinfowler de.saxsys mvvmfx - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT ``` @@ -36,9 +36,9 @@ If you need help you can use the forums on [Google Groups](https://groups.google ### Links - [Project Page](http://sialcasa.github.io/mvvmFX/) -- [javadoc mvvmfx core](http://sialcasa.github.io/mvvmFX/javadoc/1.1.0/mvvmfx/) -- [javadoc mvvmfx-cdi](http://sialcasa.github.io/mvvmFX/javadoc/1.1.0/mvvmfx-cdi/) -- [javadoc mvvmfx-guice](http://sialcasa.github.io/mvvmFX/javadoc/1.1.0/mvvmfx-guice/) -- [javadoc mvvmfx-utils](http://sialcasa.github.io/mvvmFX/javadoc/1.1.0/mvvmfx-utils/) -- [javadoc mvvmfx-testing-utils](http://sialcasa.github.io/mvvmFX/javadoc/1.1.0/mvvmfx-testing-utils/) +- [javadoc mvvmfx core](http://sialcasa.github.io/mvvmFX/javadoc/1.2.0/mvvmfx/) +- [javadoc mvvmfx-cdi](http://sialcasa.github.io/mvvmFX/javadoc/1.2.0/mvvmfx-cdi/) +- [javadoc mvvmfx-guice](http://sialcasa.github.io/mvvmFX/javadoc/1.2.0/mvvmfx-guice/) +- [javadoc mvvmfx-utils](http://sialcasa.github.io/mvvmFX/javadoc/1.2.0/mvvmfx-utils/) +- [javadoc mvvmfx-testing-utils](http://sialcasa.github.io/mvvmFX/javadoc/1.2.0/mvvmfx-testing-utils/) diff --git a/examples/mvvmfx-books-example/pom.xml b/examples/mvvmfx-books-example/pom.xml index d6973bcae..d59e0149f 100644 --- a/examples/mvvmfx-books-example/pom.xml +++ b/examples/mvvmfx-books-example/pom.xml @@ -5,7 +5,7 @@ mvvmfx-examples de.saxsys - 1.1.0 + 1.2.0 4.0.0 diff --git a/examples/mvvmfx-books-example/src/main/java/de/saxsys/mvvmfx/examples/books/MainView.java b/examples/mvvmfx-books-example/src/main/java/de/saxsys/mvvmfx/examples/books/MainView.java index d06af259d..33d6abb79 100644 --- a/examples/mvvmfx-books-example/src/main/java/de/saxsys/mvvmfx/examples/books/MainView.java +++ b/examples/mvvmfx-books-example/src/main/java/de/saxsys/mvvmfx/examples/books/MainView.java @@ -55,9 +55,12 @@ public void initialize() { viewModel.selectedBookProperty().bind(bookList.getSelectionModel().selectedItemProperty()); errorLabel.textProperty().bind(viewModel.errorProperty()); + + searchButton.disableProperty().bind(viewModel.getSearchCommand().executableProperty().not()); + } public void searchButtonPressed() { - viewModel.search(); + viewModel.getSearchCommand().execute(); } } diff --git a/examples/mvvmfx-books-example/src/main/java/de/saxsys/mvvmfx/examples/books/MainViewModel.java b/examples/mvvmfx-books-example/src/main/java/de/saxsys/mvvmfx/examples/books/MainViewModel.java index 96028b276..bab3f2d42 100644 --- a/examples/mvvmfx-books-example/src/main/java/de/saxsys/mvvmfx/examples/books/MainViewModel.java +++ b/examples/mvvmfx-books-example/src/main/java/de/saxsys/mvvmfx/examples/books/MainViewModel.java @@ -4,11 +4,11 @@ import de.saxsys.mvvmfx.examples.books.backend.Book; import de.saxsys.mvvmfx.examples.books.backend.Error; import de.saxsys.mvvmfx.examples.books.backend.LibraryService; +import de.saxsys.mvvmfx.utils.commands.Action; +import de.saxsys.mvvmfx.utils.commands.Command; +import de.saxsys.mvvmfx.utils.commands.DelegateCommand; import eu.lestard.advanced_bindings.api.ObjectBindings; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; +import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -32,17 +32,29 @@ public class MainViewModel implements ViewModel { private ObjectProperty selectedBook = new SimpleObjectProperty<>(); private StringProperty error = new SimpleStringProperty(); + + private Command searchCommand; public MainViewModel(LibraryService libraryService) { this.libraryService = libraryService; + + searchCommand = new DelegateCommand(() -> new Action() { + @Override + protected void action() throws Exception { + search(); + } + }); bookTitle.bind(ObjectBindings.map(selectedBook, bookItem -> bookItem.getBook().getTitle())); bookAuthor.bind(ObjectBindings.map(selectedBook, bookItem -> bookItem.getBook().getAuthor())); bookDescription.bind(ObjectBindings.map(selectedBook, bookItem -> bookItem.getBook().getDesc())); } - - public void search() { + public Command getSearchCommand() { + return searchCommand; + } + + void search() { Consumer errorHandler = err -> error.set(err.getMessage()); final List result = libraryService.search(searchString.get(), errorHandler); diff --git a/examples/mvvmfx-cdi-starter/pom.xml b/examples/mvvmfx-cdi-starter/pom.xml index df39f83f6..da6dace02 100644 --- a/examples/mvvmfx-cdi-starter/pom.xml +++ b/examples/mvvmfx-cdi-starter/pom.xml @@ -12,7 +12,7 @@ de.saxsys mvvmfx-examples - 1.1.0 + 1.2.0 diff --git a/examples/mvvmfx-complex-example/pom.xml b/examples/mvvmfx-complex-example/pom.xml index 96742d5b4..aff2b8ec5 100644 --- a/examples/mvvmfx-complex-example/pom.xml +++ b/examples/mvvmfx-complex-example/pom.xml @@ -7,7 +7,7 @@ de.saxsys mvvmfx-examples - 1.1.0 + 1.2.0 UTF-8 diff --git a/examples/mvvmfx-complex-example/src/main/java/de/saxsys/jfx/exampleapplication/view/personlogin/PersonLoginView.java b/examples/mvvmfx-complex-example/src/main/java/de/saxsys/jfx/exampleapplication/view/personlogin/PersonLoginView.java index 5a2918746..31b198164 100644 --- a/examples/mvvmfx-complex-example/src/main/java/de/saxsys/jfx/exampleapplication/view/personlogin/PersonLoginView.java +++ b/examples/mvvmfx-complex-example/src/main/java/de/saxsys/jfx/exampleapplication/view/personlogin/PersonLoginView.java @@ -7,10 +7,13 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Button; import javafx.scene.control.ChoiceBox; import javafx.scene.control.ProgressIndicator; import de.saxsys.jfx.exampleapplication.viewmodel.personlogin.PersonLoginViewModel; +import de.saxsys.jfx.exampleapplication.viewmodel.personlogin.PersonLoginViewModelNotifications; import de.saxsys.mvvmfx.FxmlView; import de.saxsys.mvvmfx.InjectViewModel; import de.saxsys.mvvmfx.utils.commands.Command; @@ -43,8 +46,17 @@ public void initialize(URL url, ResourceBundle resourceBundle) { loginCommand = getViewModel().getLoginCommand(); initChoiceBox(); loginButton.disableProperty() - .bind(loginCommand.executableProperty().not()); + .bind(loginCommand.notExecutableProperty()); loginProgressIndicator.visibleProperty().bind(loginCommand.runningProperty()); + + viewModel.subscribe(PersonLoginViewModelNotifications.OK.getId(), (key, payload) -> { + String message = (String) payload[0]; + Alert alert = new Alert(AlertType.INFORMATION); + alert.setTitle(message); + alert.setContentText(message); + alert.show(); + }); + } @FXML diff --git a/examples/mvvmfx-complex-example/src/main/java/de/saxsys/jfx/exampleapplication/viewmodel/personlogin/PersonLoginViewModel.java b/examples/mvvmfx-complex-example/src/main/java/de/saxsys/jfx/exampleapplication/viewmodel/personlogin/PersonLoginViewModel.java index 36a0e8de6..cf08bbcee 100644 --- a/examples/mvvmfx-complex-example/src/main/java/de/saxsys/jfx/exampleapplication/viewmodel/personlogin/PersonLoginViewModel.java +++ b/examples/mvvmfx-complex-example/src/main/java/de/saxsys/jfx/exampleapplication/viewmodel/personlogin/PersonLoginViewModel.java @@ -1,5 +1,6 @@ package de.saxsys.jfx.exampleapplication.viewmodel.personlogin; +import de.saxsys.mvvmfx.utils.commands.Action; import javafx.application.Platform; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.ReadOnlyIntegerProperty; @@ -73,7 +74,12 @@ public ReadOnlyIntegerProperty loggedInPersonIdProperty() { public Command getLoginCommand() { if (loginCommand == null) { - loginCommand = new DelegateCommand(() -> performLogin(), createLoginPossibleBinding(), true); + loginCommand = new DelegateCommand(()-> new Action() { + @Override + protected void action() throws Exception { + performLogin(); + } + }, createLoginPossibleBinding(), true); } return loginCommand; } @@ -88,6 +94,6 @@ private void performLogin() { Platform.runLater(() -> { loggedInPersonId.set(selectablePersons.getSelectedItem().getId()); }); + publish(PersonLoginViewModelNotifications.OK.getId(), PersonLoginViewModelNotifications.OK.getMessage()); } - } diff --git a/examples/mvvmfx-complex-example/src/main/java/de/saxsys/jfx/exampleapplication/viewmodel/personlogin/PersonLoginViewModelNotifications.java b/examples/mvvmfx-complex-example/src/main/java/de/saxsys/jfx/exampleapplication/viewmodel/personlogin/PersonLoginViewModelNotifications.java new file mode 100644 index 000000000..da892f95f --- /dev/null +++ b/examples/mvvmfx-complex-example/src/main/java/de/saxsys/jfx/exampleapplication/viewmodel/personlogin/PersonLoginViewModelNotifications.java @@ -0,0 +1,19 @@ +package de.saxsys.jfx.exampleapplication.viewmodel.personlogin; + +public enum PersonLoginViewModelNotifications { + OK("Das Einloggen war erfolgreich"); + + private String message; + + PersonLoginViewModelNotifications(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public String getId() { + return toString(); + } +} diff --git a/examples/mvvmfx-contacts/pom.xml b/examples/mvvmfx-contacts/pom.xml index 36979565e..b776e347b 100644 --- a/examples/mvvmfx-contacts/pom.xml +++ b/examples/mvvmfx-contacts/pom.xml @@ -6,7 +6,7 @@ mvvmfx-examples de.saxsys - 1.1.0 + 1.2.0 mvvmfx-contacts diff --git a/examples/mvvmfx-contacts/src/main/java/de/saxsys/mvvmfx/contacts/ui/contactform/ContactFormViewModel.java b/examples/mvvmfx-contacts/src/main/java/de/saxsys/mvvmfx/contacts/ui/contactform/ContactFormViewModel.java index b494c7125..a8a9d9a8f 100644 --- a/examples/mvvmfx-contacts/src/main/java/de/saxsys/mvvmfx/contacts/ui/contactform/ContactFormViewModel.java +++ b/examples/mvvmfx-contacts/src/main/java/de/saxsys/mvvmfx/contacts/ui/contactform/ContactFormViewModel.java @@ -78,23 +78,23 @@ public ReadOnlyBooleanProperty validProperty() { - public Property firstnameProperty() { + public StringProperty firstnameProperty() { return contactWrapper.field("firstname", Contact::getFirstname, Contact::setFirstname); } - public Property titleProperty() { + public StringProperty titleProperty() { return contactWrapper.field("title", Contact::getTitle, Contact::setTitle); } - public Property lastnameProperty() { + public StringProperty lastnameProperty() { return contactWrapper.field("lastname", Contact::getLastname, Contact::setLastname); } - public Property roleProperty() { + public StringProperty roleProperty() { return contactWrapper.field("role", Contact::getRole, Contact::setRole); } - public Property departmentProperty() { + public StringProperty departmentProperty() { return contactWrapper.field("department", Contact::getDepartment, Contact::setDepartment); } @@ -102,15 +102,15 @@ public Property birthdayProperty() { return contactWrapper.field("birthday", Contact::getBirthday, Contact::setBirthday); } - public Property emailProperty() { + public StringProperty emailProperty() { return contactWrapper.field("email", Contact::getEmailAddress, Contact::setEmailAddress); } - public Property mobileNumberProperty() { + public StringProperty mobileNumberProperty() { return contactWrapper.field("mobileNumber", Contact::getMobileNumber, Contact::setMobileNumber); } - public Property phoneNumberProperty() { + public StringProperty phoneNumberProperty() { return contactWrapper.field("phoneNumber", Contact::getPhoneNumber, Contact::setPhoneNumber); } } diff --git a/examples/mvvmfx-contacts/src/main/java/de/saxsys/mvvmfx/contacts/ui/master/MasterTableViewModel.java b/examples/mvvmfx-contacts/src/main/java/de/saxsys/mvvmfx/contacts/ui/master/MasterTableViewModel.java index 7d7822f47..12a18ec8c 100644 --- a/examples/mvvmfx-contacts/src/main/java/de/saxsys/mvvmfx/contacts/ui/master/MasterTableViewModel.java +++ b/examples/mvvmfx-contacts/src/main/java/de/saxsys/mvvmfx/contacts/ui/master/MasterTableViewModel.java @@ -2,6 +2,9 @@ import de.saxsys.mvvmfx.contacts.model.Contact; import de.saxsys.mvvmfx.contacts.util.CentralClock; +import de.saxsys.mvvmfx.utils.mapping.ModelWrapper; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.StringGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.StringSetter; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.ReadOnlyStringWrapper; @@ -13,31 +16,17 @@ import java.time.temporal.ChronoUnit; public class MasterTableViewModel { - - private ReadOnlyStringWrapper id = new ReadOnlyStringWrapper(); - private StringProperty firstname = new SimpleStringProperty(); - private StringProperty lastname = new SimpleStringProperty(); - private StringProperty title = new SimpleStringProperty(); - - private StringProperty emailAddress = new SimpleStringProperty(); + private final String id; private IntegerProperty age = new SimpleIntegerProperty(); - - private StringProperty city = new SimpleStringProperty(); - private StringProperty street = new SimpleStringProperty(); - private StringProperty postalCode = new SimpleStringProperty(); + private ModelWrapper contactWrapper = new ModelWrapper<>(); public MasterTableViewModel(Contact contact) { - id.set(contact.getId()); - setFirstname(contact.getFirstname()); - setLastname(contact.getLastname()); - setTitle(contact.getTitle()); - setEmailAddress(contact.getEmailAddress()); - setCity(contact.getAddress().getCity()); - setStreet(contact.getAddress().getStreet()); - setPostalCode(contact.getAddress().getPostalcode()); + id = contact.getId(); + contactWrapper.set(contact); + contactWrapper.reload(); if (contact.getBirthday() != null) { - setAge((int) ChronoUnit.YEARS.between(contact.getBirthday(), LocalDate.now(CentralClock.getClock()))); + age.set((int) ChronoUnit.YEARS.between(contact.getBirthday(), LocalDate.now(CentralClock.getClock()))); } } @@ -68,106 +57,48 @@ public int hashCode() { } public String getId() { - return id.get(); - } - - public ReadOnlyStringProperty idProperty() { - return id.getReadOnlyProperty(); - } - - public String getFirstname() { - return firstname.get(); + return id; } public StringProperty firstnameProperty() { - return firstname; + return contactWrapper.field("firstname", Contact::getFirstname, Contact::setFirstname); } - public void setFirstname(String firstname) { - this.firstname.set(firstname); - } - - public String getLastname() { - return lastname.get(); - } public StringProperty lastnameProperty() { - return lastname; - } - - public void setLastname(String lastname) { - this.lastname.set(lastname); + return contactWrapper.field("lastname", Contact::getLastname, Contact::setLastname); } - public String getTitle() { - return title.get(); - } public StringProperty titleProperty() { - return title; - } - - public void setTitle(String title) { - this.title.set(title); - } - - public String getEmailAddress() { - return emailAddress.get(); + return contactWrapper.field("title", Contact::getTitle, Contact::setTitle); } public StringProperty emailAddressProperty() { - return emailAddress; - } - - public void setEmailAddress(String emailAddress) { - this.emailAddress.set(emailAddress); - } - - public int getAge() { - return age.get(); + return contactWrapper.field("emailAddress", Contact::getEmailAddress, Contact::setEmailAddress); } public IntegerProperty ageProperty() { return age; } - public void setAge(int age) { - this.age.set(age); - } - - public String getCity() { - return city.get(); - } - public StringProperty cityProperty() { - return city; + return contactWrapper.field("city", + (StringGetter) model -> model.getAddress().getCity(), + (model, value) -> model.getAddress().setCity(value)); } - public void setCity(String city) { - this.city.set(city); - } - - public String getStreet() { - return street.get(); - } public StringProperty streetProperty() { - return street; - } - - public void setStreet(String street) { - this.street.set(street); + return contactWrapper.field("street", + (StringGetter) model -> model.getAddress().getStreet(), + (model, value) -> model.getAddress().setStreet(value)); } - public String getPostalCode() { - return postalCode.get(); - } public StringProperty postalCodeProperty() { - return postalCode; - } - - public void setPostalCode(String postalCode) { - this.postalCode.set(postalCode); + return contactWrapper.field("postalcode", + (StringGetter) model -> model.getAddress().getPostalcode(), + (model, value) -> model.getAddress().setPostalcode(value)); } } diff --git a/examples/mvvmfx-contacts/src/test/java/de/saxsys/mvvmfx/contacts/ui/master/MasterTableViewModelTest.java b/examples/mvvmfx-contacts/src/test/java/de/saxsys/mvvmfx/contacts/ui/master/MasterTableViewModelTest.java index 856292b44..c92d40074 100644 --- a/examples/mvvmfx-contacts/src/test/java/de/saxsys/mvvmfx/contacts/ui/master/MasterTableViewModelTest.java +++ b/examples/mvvmfx-contacts/src/test/java/de/saxsys/mvvmfx/contacts/ui/master/MasterTableViewModelTest.java @@ -30,7 +30,7 @@ public void testCalculationOfAge() { MasterTableViewModel tableViewModel = new MasterTableViewModel(contact); - assertThat(tableViewModel.getAge()).isEqualTo(22); + assertThat(tableViewModel.ageProperty().get()).isEqualTo(22); } } diff --git a/examples/mvvmfx-fx-root-example/pom.xml b/examples/mvvmfx-fx-root-example/pom.xml index e0d937162..ee648822d 100644 --- a/examples/mvvmfx-fx-root-example/pom.xml +++ b/examples/mvvmfx-fx-root-example/pom.xml @@ -7,7 +7,7 @@ de.saxsys mvvmfx-examples - 1.1.0 + 1.2.0 diff --git a/examples/mvvmfx-guice-starter/pom.xml b/examples/mvvmfx-guice-starter/pom.xml index c7f638b35..f5126ec63 100644 --- a/examples/mvvmfx-guice-starter/pom.xml +++ b/examples/mvvmfx-guice-starter/pom.xml @@ -12,7 +12,7 @@ de.saxsys mvvmfx-examples - 1.1.0 + 1.2.0 UTF-8 diff --git a/examples/mvvmfx-helloworld-without-fxml/pom.xml b/examples/mvvmfx-helloworld-without-fxml/pom.xml index 25405f948..46e096cd8 100644 --- a/examples/mvvmfx-helloworld-without-fxml/pom.xml +++ b/examples/mvvmfx-helloworld-without-fxml/pom.xml @@ -7,7 +7,7 @@ de.saxsys mvvmfx-examples - 1.1.0 + 1.2.0 diff --git a/examples/mvvmfx-helloworld/pom.xml b/examples/mvvmfx-helloworld/pom.xml index 3ddaf16d8..b5b24a092 100644 --- a/examples/mvvmfx-helloworld/pom.xml +++ b/examples/mvvmfx-helloworld/pom.xml @@ -6,7 +6,7 @@ de.saxsys mvvmfx-examples - 1.1.0 + 1.2.0 UTF-8 diff --git a/examples/mvvmfx-synchronizefx/pom.xml b/examples/mvvmfx-synchronizefx/pom.xml index 35dd05c12..0e2412f60 100644 --- a/examples/mvvmfx-synchronizefx/pom.xml +++ b/examples/mvvmfx-synchronizefx/pom.xml @@ -6,7 +6,7 @@ de.saxsys mvvmfx-examples - 1.1.0 + 1.2.0 UTF-8 diff --git a/examples/mvvmfx-todomvc/pom.xml b/examples/mvvmfx-todomvc/pom.xml index bd26ad217..0f28d7b91 100644 --- a/examples/mvvmfx-todomvc/pom.xml +++ b/examples/mvvmfx-todomvc/pom.xml @@ -5,7 +5,7 @@ mvvmfx-examples de.saxsys - 1.1.0 + 1.2.0 4.0.0 diff --git a/examples/pom.xml b/examples/pom.xml index 0de3d42b4..48faffef9 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -5,7 +5,7 @@ de.saxsys mvvmfx-parent - 1.1.0 + 1.2.0 mvvmfx-examples diff --git a/mvvmfx-archetype/pom.xml b/mvvmfx-archetype/pom.xml index a000fdd2a..021d9e68c 100644 --- a/mvvmfx-archetype/pom.xml +++ b/mvvmfx-archetype/pom.xml @@ -5,7 +5,7 @@ de.saxsys mvvmfx-parent - 1.1.0 + 1.2.0 @@ -18,7 +18,7 @@ - ${parent.version} + ${project.parent.version} @@ -43,6 +43,7 @@ maven-resources-plugin + 2.7 \ diff --git a/mvvmfx-cdi/pom.xml b/mvvmfx-cdi/pom.xml index a8770c37c..498d7d798 100644 --- a/mvvmfx-cdi/pom.xml +++ b/mvvmfx-cdi/pom.xml @@ -20,7 +20,7 @@ de.saxsys mvvmfx-parent - 1.1.0 + 1.2.0 mvvmfx-cdi @@ -34,6 +34,7 @@ maven-surefire-plugin + 2.18.1 always diff --git a/mvvmfx-guice/pom.xml b/mvvmfx-guice/pom.xml index e3286cff9..23d621070 100644 --- a/mvvmfx-guice/pom.xml +++ b/mvvmfx-guice/pom.xml @@ -20,7 +20,7 @@ de.saxsys mvvmfx-parent - 1.1.0 + 1.2.0 mvvmfx-guice @@ -33,6 +33,7 @@ maven-surefire-plugin + 2.18.1 always diff --git a/mvvmfx-testing-utils/pom.xml b/mvvmfx-testing-utils/pom.xml index 883a3dd80..e9ac21324 100644 --- a/mvvmfx-testing-utils/pom.xml +++ b/mvvmfx-testing-utils/pom.xml @@ -5,7 +5,7 @@ mvvmfx-parent de.saxsys - 1.1.0 + 1.2.0 4.0.0 diff --git a/mvvmfx-utils/pom.xml b/mvvmfx-utils/pom.xml index 0dba16603..2e610e4f7 100644 --- a/mvvmfx-utils/pom.xml +++ b/mvvmfx-utils/pom.xml @@ -5,7 +5,7 @@ mvvmfx-parent de.saxsys - 1.1.0 + 1.2.0 4.0.0 diff --git a/mvvmfx/pom.xml b/mvvmfx/pom.xml index 67508b864..f67c9800c 100644 --- a/mvvmfx/pom.xml +++ b/mvvmfx/pom.xml @@ -20,7 +20,7 @@ de.saxsys mvvmfx-parent - 1.1.0 + 1.2.0 mvvmfx @@ -83,6 +83,13 @@ test + + com.cedarsoft.commons + test-utils + 6.1.1 + test + + diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java index 5c6d60260..83c137462 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/ViewModel.java @@ -15,12 +15,22 @@ ******************************************************************************/ package de.saxsys.mvvmfx; +import javafx.application.Platform; import de.saxsys.mvvmfx.internal.viewloader.View; import de.saxsys.mvvmfx.utils.notifications.NotificationCenter; +import de.saxsys.mvvmfx.utils.notifications.NotificationObserver; /** *

- * Marker interface for a View Model. Some additional hints to this layer: + * Interface for a View Model. + *

+ *

+ * You can use a notification mechanism by using the {@link #publish(String, Object...)} method. In the View you can + * subscribe to this notifications by using viewModel. + * {@link #subscribe(String messageName, NotificationObserver observer)}. + *

+ *

+ * Some additional hints to this layer: *

* *

@@ -33,4 +43,55 @@ * */ public interface ViewModel { + + /** + * Publishes a notification to the subscribers of the notificationId. This notification will be send to the + * UI-Thread. + * + * @param messageName + * of the notification + * @param payload + * to be send + */ + default void publish(String messageName, Object... payload) { + if (!Platform.isFxApplicationThread()) { + MvvmFX.getNotificationCenter().publish(this, messageName, payload); + } else { + MvvmFX.getNotificationCenter().publish(this, messageName, payload); + } + } + + /** + * Subscribe to a notification with a given {@link NotificationObserver}. + * + * @param messageName + * of the Notification + * @param observer + * which should execute when the notification occurs + */ + default void subscribe(String messageName, NotificationObserver observer) { + MvvmFX.getNotificationCenter().subscribe(this, messageName, observer); + } + + /** + * Remove the observer for a specific notification by a given messageName. + * + * @param messageName + * of the notification for that the observer should be removed + * @param observer + * to remove + */ + default void unsubscribe(String messageName, NotificationObserver observer) { + MvvmFX.getNotificationCenter().unsubscribe(this, messageName, observer); + } + + /** + * Removes the observer for all messages. + * + * @param observer + * to be removed + */ + default void unsubscribe(NotificationObserver observer) { + MvvmFX.getNotificationCenter().unsubscribe(this, observer); + } } diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/Action.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/Action.java new file mode 100644 index 000000000..797a5c43b --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/Action.java @@ -0,0 +1,15 @@ +package de.saxsys.mvvmfx.utils.commands; + +import javafx.concurrent.Task; + +public abstract class Action extends Task { + + @Override + protected Void call() throws Exception { + action(); + return null; + } + + protected abstract void action() throws Exception; + +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/Command.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/Command.java index 55b565ac5..31a58e292 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/Command.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/Command.java @@ -15,8 +15,9 @@ ******************************************************************************/ package de.saxsys.mvvmfx.utils.commands; -import eu.lestard.doc.Beta; import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyDoubleProperty; +import eu.lestard.doc.Beta; /** * The {@link Command} encapsulates logic in the {@link #execute()} method which will be called later. This can be used @@ -50,6 +51,18 @@ public interface Command { */ ReadOnlyBooleanProperty executableProperty(); + /** + * Determines whether the command can not execute in it's current state. + * + * @return true if the {@link Command} can not execute, otherwise false. + */ + boolean isNotExecutable(); + + /** + * @see #isNotExecutable() + */ + ReadOnlyBooleanProperty notExecutableProperty(); + /** * Signals whether the command is currently executing. This can be useful especially for commands that are executed @@ -64,4 +77,31 @@ public interface Command { */ ReadOnlyBooleanProperty runningProperty(); + /** + * Signals whether the command is currently not executing. This can be useful especially for commands that are + * executed asynchronously. + * + * @return true if the {@link Command} is not running, otherwise false. + */ + boolean isNotRunning(); + + /** + * @see #isNotRunning() + */ + ReadOnlyBooleanProperty notRunningProperty(); + + /** + * Gets a double between 0.0 and 1.0 which represents the progress. + * + * @return progress + */ + double getProgress(); + + /** + * @see #getProgress() + */ + ReadOnlyDoubleProperty progressProperty(); + + + } \ No newline at end of file diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/CommandBase.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/CommandBase.java index 331af0f61..a81b04344 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/CommandBase.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/CommandBase.java @@ -32,24 +32,55 @@ public abstract class CommandBase implements Command { protected final ReadOnlyBooleanWrapper executable = new ReadOnlyBooleanWrapper(true); protected final ReadOnlyBooleanWrapper running = new ReadOnlyBooleanWrapper(false); + protected ReadOnlyBooleanWrapper notExecutable; + protected ReadOnlyBooleanWrapper notRunning; + + @Override + public final ReadOnlyBooleanProperty executableProperty() { + return executable.getReadOnlyProperty(); + } + + @Override + public final boolean isExecutable() { + return executableProperty().get(); + } + + @Override + public final ReadOnlyBooleanProperty notExecutableProperty() { + if (notExecutable == null) { + notExecutable = new ReadOnlyBooleanWrapper(); + notExecutable.bind(executableProperty().not()); + } + return notExecutable.getReadOnlyProperty(); + } + + @Override + public final boolean isNotExecutable() { + return notExecutableProperty().get(); + } + @Override - public ReadOnlyBooleanProperty executableProperty() { - return this.executable.getReadOnlyProperty(); + public final ReadOnlyBooleanProperty runningProperty() { + return running.getReadOnlyProperty(); } @Override - public boolean isExecutable() { - return this.executableProperty().get(); + public final boolean isRunning() { + return running.get(); } @Override - public ReadOnlyBooleanProperty runningProperty() { - return this.running.getReadOnlyProperty(); + public final ReadOnlyBooleanProperty notRunningProperty() { + if (notRunning == null) { + notRunning = new ReadOnlyBooleanWrapper(); + notRunning.bind(runningProperty().not()); + } + return notRunning.getReadOnlyProperty(); } @Override - public boolean isRunning() { - return this.running.get(); + public final boolean isNotRunning() { + return notRunningProperty().get(); } @Override diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/CompositeCommand.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/CompositeCommand.java index a36cd72ca..18ad8622a 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/CompositeCommand.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/CompositeCommand.java @@ -15,12 +15,21 @@ ******************************************************************************/ package de.saxsys.mvvmfx.utils.commands; -import eu.lestard.doc.Beta; +import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; +import javafx.beans.binding.DoubleBinding; +import javafx.beans.binding.DoubleExpression; import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyDoubleWrapper; +import javafx.beans.value.ObservableDoubleValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import eu.lestard.doc.Beta; + +import java.util.concurrent.Callable; +import java.util.function.Function; /** * CompositeCommand is an aggregation of other commands - a list of {@link Command} references internally. @@ -48,6 +57,8 @@ public class CompositeCommand extends CommandBase { private final ObservableList registeredCommands = FXCollections.observableArrayList(); + ReadOnlyDoubleWrapper progress = new ReadOnlyDoubleWrapper(); + /** * Creates a {@link CompositeCommand} with given commands. * @@ -83,39 +94,89 @@ public void unregister(Command command) { private void initRegisteredCommandsListener() { this.registeredCommands.addListener((ListChangeListener) c -> { while (c.next()) { - if(registeredCommands.isEmpty()) { + if (registeredCommands.isEmpty()) { executable.unbind(); running.unbind(); + progress.unbind(); } else { - BooleanBinding executableBinding = null; - BooleanBinding runningBinding = null; + BooleanBinding executableBinding = constantOf(true); + BooleanBinding runningBinding = constantOf(false); - for (int i = 0; i < registeredCommands.size(); i++) { - ReadOnlyBooleanProperty currentExecutable = registeredCommands.get(i).executableProperty(); - ReadOnlyBooleanProperty currentRunning = registeredCommands.get(i).runningProperty(); - - if (i == 0) { - executableBinding = currentExecutable.and(currentExecutable); - runningBinding = currentRunning.or(currentRunning); - } else { - executableBinding = executableBinding.and(currentExecutable); - runningBinding = runningBinding.or(currentRunning); - } + for (Command registeredCommand : registeredCommands) { + ReadOnlyBooleanProperty currentExecutable = registeredCommand.executableProperty(); + ReadOnlyBooleanProperty currentRunning = registeredCommand.runningProperty(); + executableBinding = executableBinding.and(currentExecutable); + runningBinding = runningBinding.or(currentRunning); } executable.bind(executableBinding); running.bind(runningBinding); + + initProgressBinding(); } } }); } + private void initProgressBinding() { + DoubleExpression tmp = constantOf(0); + + for (Command command : registeredCommands) { + final ReadOnlyDoubleProperty progressProperty = command.progressProperty(); + + /** + * When the progress of a command is "undefined", the progress property has a value of -1. + * But in our use case we like to have a value of 0 in this case. + * Therefore we create a custom binding here. + */ + final DoubleBinding normalizedProgress = Bindings + .createDoubleBinding(() -> (progressProperty.get() == -1) ? 0.0 : progressProperty.get(), + progressProperty); + + tmp = tmp.add(normalizedProgress); + } + + int divisor = registeredCommands.isEmpty() ? 1 : registeredCommands.size(); + progress.bind(Bindings.divide(tmp, divisor)); + } + @Override public void execute() { if (!isExecutable()) { throw new RuntimeException("Not executable"); } else { - registeredCommands.forEach(t -> t.execute()); + if (!registeredCommands.isEmpty()) { + registeredCommands.forEach(t -> t.execute()); + } } } + + @Override + public double getProgress() { + return progressProperty().get(); + } + + @Override + public ReadOnlyDoubleProperty progressProperty() { + return progress; + } + + private BooleanBinding constantOf(boolean defaultValue) { + return new BooleanBinding() { + @Override + protected boolean computeValue() { + return defaultValue; + } + }; + } + + private DoubleBinding constantOf(double defaultValue) { + return new DoubleBinding() { + @Override + protected double computeValue() { + return defaultValue; + } + }; + } + } diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/DelegateCommand.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/DelegateCommand.java index 82210e9b6..ac476b06e 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/DelegateCommand.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/commands/DelegateCommand.java @@ -15,32 +15,45 @@ ******************************************************************************/ package de.saxsys.mvvmfx.utils.commands; +import java.util.function.Supplier; + import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.value.ObservableBooleanValue; +import javafx.concurrent.Service; +import javafx.concurrent.Task; import eu.lestard.doc.Beta; /** - * A {@link Command} implementation that encapsulates an action ({@link Runnable}). It is possible to define that the - * action should be executed in the background (not on the JavaFX thread) so that long running actions can be - * implemented that aren't blocking the ui thread. + * A {@link Command} implementation of a {@link Service} that encapsulates an {@link Action} ({@link Task}) + * which can be called from the UI - for example after a button click. If the {@link Action} is a long running + * operation, which would block your UI, you can pass a parameter to perform the {@link Action} in a background thread. + * You can bind to the {@link #isRunning()} property while the action is executing. This can be used for a loading + * indication in the UI. * * @author alexander.casall */ @Beta -public class DelegateCommand extends CommandBase { +public class DelegateCommand extends Service implements Command { - private final Runnable action; + private final Supplier actionSupplier; private boolean inBackground = false; + protected final ReadOnlyBooleanWrapper executable = new ReadOnlyBooleanWrapper(true); + protected ReadOnlyBooleanWrapper notExecutable; + protected ReadOnlyBooleanWrapper notRunning; + + + /** - * Creates a command without a condition about the executability. The command will perform in the thread which - * executes the {@link Command}. + * Creates a command without a condition about the executability. * - * @param action - * which should execute + * @param actionSupplier + * a function that returns a new Action which should be executed */ - public DelegateCommand(Runnable action) { - this(action, null, false); + public DelegateCommand(final Supplier actionSupplier) { + this(actionSupplier, false); } /** @@ -49,28 +62,27 @@ public DelegateCommand(Runnable action) { * * IF YOU USE THE BACKGROUND THREAD: Your provided action will perform in a background thread. If you * manipulate data in your action, which will be propagated to the UI, use {@link Platform#runLater(Runnable)} for - * this manipulation, otherwise you get an Exception. - * - * @param action - * which should execute + * this manipulation, otherwise you get an Exception by JavaFX. + * + * @param actionSupplier + * a function that returns a new Action which should be executed * @param inBackground * defines whether the execution {@link #execute()} is performed in a background thread or not */ - public DelegateCommand(Runnable action, boolean inBackground) { - this(action, null, inBackground); + public DelegateCommand(final Supplier actionSupplier, boolean inBackground) { + this(actionSupplier, null, inBackground); } /** - * Creates a command with a condition about the executability by using the #executableBinding parameter. The command - * will perform in the thread which executes the {@link Command}. - * - * @param action - * which should execute + * Creates a command with a condition about the executability by using the #executableBinding parameter. + * + * @param actionSupplier + * a function that returns a new Action which should be executed * @param executableBinding * which defines whether the {@link Command} can execute */ - public DelegateCommand(Runnable action, ObservableBooleanValue executableBinding) { - this(action, executableBinding, false); + public DelegateCommand(final Supplier actionSupplier, ObservableBooleanValue executableBinding) { + this(actionSupplier, executableBinding, false); } /** @@ -79,20 +91,22 @@ public DelegateCommand(Runnable action, ObservableBooleanValue executableBinding * * IF YOU USE THE BACKGROUND THREAD: don't forget to return to the UI-thread by using * {@link Platform#runLater(Runnable)}, otherwise you get an Exception. - * - * @param action - * which should execute + * + * @param actionSupplier + * a function that returns a new Action which should be executed * @param executableBinding * which defines whether the {@link Command} can execute * @param inBackground * defines whether the execution {@link #execute()} is performed in a background thread or not */ - public DelegateCommand(Runnable action, ObservableBooleanValue executableBinding, boolean inBackground) { - this.action = action; + public DelegateCommand(final Supplier actionSupplier, ObservableBooleanValue executableBinding, + boolean inBackground) { + this.actionSupplier = actionSupplier; this.inBackground = inBackground; if (executableBinding != null) { executable.bind(runningProperty().not().and(executableBinding)); } + } /** @@ -100,27 +114,65 @@ public DelegateCommand(Runnable action, ObservableBooleanValue executableBinding */ @Override public void execute() { - - final boolean callerOnUIThread = Platform.isFxApplicationThread(); - if (!isExecutable()) { throw new RuntimeException("The execute()-method of the command was called while it wasn't executable."); } else { - running.set(true); if (inBackground) { - new Thread(() -> { - action.run(); - if (callerOnUIThread) { - Platform.runLater(() -> running.set(false)); - } else { - running.set(false); - } - }).start(); + if (!super.isRunning()) { + reset(); + start(); + } } else { - action.run(); - running.set(false); + try { + actionSupplier.get().action(); + } catch (Exception e) { + e.printStackTrace(); + } } } } + @Override + protected Task createTask() { + return actionSupplier.get(); + } + + @Override + public ReadOnlyBooleanProperty executableProperty() { + return this.executable.getReadOnlyProperty(); + } + + + @Override + public boolean isExecutable() { + return this.executableProperty().get(); + } + + @Override + public final ReadOnlyBooleanProperty notExecutableProperty() { + if (notExecutable == null) { + notExecutable = new ReadOnlyBooleanWrapper(); + notExecutable.bind(executableProperty().not()); + } + return notExecutable.getReadOnlyProperty(); + } + + @Override + public final boolean isNotExecutable() { + return notExecutableProperty().get(); + } + + @Override + public final ReadOnlyBooleanProperty notRunningProperty() { + if (notRunning == null) { + notRunning = new ReadOnlyBooleanWrapper(); + notRunning.bind(runningProperty().not()); + } + return notRunning.getReadOnlyProperty(); + } + + @Override + public final boolean isNotRunning() { + return notRunningProperty().get(); + } } 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 a42a62db3..e7c5e045f 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 @@ -1,10 +1,28 @@ package de.saxsys.mvvmfx.utils.mapping; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.BooleanGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.BooleanPropertyAccessor; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.BooleanSetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.DoubleGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.DoublePropertyAccessor; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.DoubleSetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.FloatGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.FloatPropertyAccessor; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.FloatSetter; +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.LongGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.LongPropertyAccessor; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.LongSetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ObjectGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ObjectPropertyAccessor; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ObjectSetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.StringGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.StringPropertyAccessor; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.StringSetter; import eu.lestard.doc.Beta; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.Property; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.WritableValue; +import javafx.beans.property.*; import java.util.HashMap; import java.util.HashSet; @@ -12,6 +30,7 @@ import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Function; +import java.util.function.Supplier; /** @@ -158,15 +177,15 @@ * } * } * - * public Property{@code} nameProperty(){ + * public StringProperty nameProperty(){ * return wrapper.field("name", Person::getName, Person::setName, ""); * } * - * public Property{@code} familyNameProperty(){ + * public StringProperty familyNameProperty(){ * return wrapper.field("familyName", Person::getFamilyName, Person::setFamilyName, ""); * } * - * public Property{@code} ageProperty() { + * public IntegerProperty ageProperty() { * return wrapper.field("age", Person::getAge, Person::setAge, 0); * } * } @@ -181,6 +200,10 @@ * ViewModel that would need an update when the structure of the model changes. * * + * + * + * + * * @param * the type of the model class. */ @@ -193,14 +216,14 @@ public class ModelWrapper { * @param * @param */ - private interface PropertyField { + private interface PropertyField> { void commit(M wrappedObject); void reload(M wrappedObject); void resetToDefault(); - Property getProperty(); + R getProperty(); } /** @@ -209,25 +232,26 @@ private interface PropertyField { * * @param */ - private class FxPropertyField implements PropertyField { + private class FxPropertyField> implements PropertyField { private final T defaultValue; - private final Function> accessor; - private final ObjectProperty targetProperty; + private final Function> accessor; + private final R targetProperty; - public FxPropertyField(Function> accessor) { - this(accessor, null); + public FxPropertyField(Function> accessor, Supplier> propertySupplier) { + this(accessor, null, propertySupplier); } - public FxPropertyField(Function> accessor, T defaultValue) { + @SuppressWarnings("unchecked") + public FxPropertyField(Function> accessor, T defaultValue, Supplier> propertySupplier) { this.accessor = accessor; this.defaultValue = defaultValue; - this.targetProperty = new SimpleObjectProperty<>(); + this.targetProperty = (R) propertySupplier.get(); } @Override public void commit(M wrappedObject) { - accessor.apply(wrappedObject).setValue(targetProperty.get()); + accessor.apply(wrappedObject).setValue(targetProperty.getValue()); } @Override @@ -241,7 +265,7 @@ public void resetToDefault() { } @Override - public Property getProperty() { + public R getProperty() { return targetProperty; } } @@ -252,30 +276,30 @@ public Property getProperty() { * * @param */ - private class BeanPropertyField implements PropertyField { + private class BeanPropertyField> implements PropertyField { - private final ObjectProperty targetProperty; + private final R targetProperty; private final T defaultValue; private final Function getter; private final BiConsumer setter; public BeanPropertyField(Function getter, - BiConsumer setter) { - this(getter, setter, null); + BiConsumer setter, Supplier propertySupplier) { + this(getter, setter, null, propertySupplier); } public BeanPropertyField(Function getter, - BiConsumer setter, T defaultValue) { + BiConsumer setter, T defaultValue, Supplier propertySupplier) { this.defaultValue = defaultValue; this.getter = getter; this.setter = setter; - this.targetProperty = new SimpleObjectProperty<>(); + this.targetProperty = propertySupplier.get(); } @Override public void commit(M wrappedObject) { - setter.accept(wrappedObject, targetProperty.get()); + setter.accept(wrappedObject, targetProperty.getValue()); } @Override @@ -289,14 +313,14 @@ public void resetToDefault() { } @Override - public Property getProperty() { + public R getProperty() { return targetProperty; } } - private Set> fields = new HashSet<>(); - private Map> identifiedFields = new HashMap<>(); + private Set> fields = new HashSet<>(); + private Map> identifiedFields = new HashMap<>(); private M model; @@ -375,90 +399,28 @@ public void reload() { } } - /** - * Add a new field to this instance of the wrapper. This method is used for model elements that are following the - * enhanced JavaFX-Beans-standard i.e. the model fields are available as JavaFX Properties. - *

- * Example: - *

- * - *

-	 *     ModelWrapper{@code} personWrapper = new ModelWrapper{@code<>}();
-	 *     
-	 *     Property{@code} wrappedNameProperty = personWrapper.field(person -> person.nameProperty());
-	 *     
-	 *     // or with a method reference
-	 *     Property{@code} wrappedNameProperty = personWrapper.field(Person::nameProperty);
-	 * 
-	 * 
- * - * - * @param accessor - * a function that returns the property for a given model instance. Typically you will use a method - * reference to the javafx-property accessor method. - * - * @param - * the type of the field. - * - * @return The wrapped property instance. - */ - public Property field(Function> accessor) { - return add(new FxPropertyField<>(accessor)); - } - /** - * Add a new field to this instance of the wrapper. This method is used for model elements that are following the - * enhanced JavaFX-Beans-standard i.e. the model fields are available as JavaFX Properties. - *

- * Additionally you can define a default value that is used as when {@link #reset()} is invoked. - * - *

- * - * Example: - *

- * - *

-	 *     ModelWrapper{@code} personWrapper = new ModelWrapper{@code<>}();
-	 * 
-	 *     Property{@code} wrappedNameProperty = personWrapper.field(person -> person.nameProperty(), "empty");
-	 * 
-	 *     // or with a method reference
-	 *     Property{@code} wrappedNameProperty = personWrapper.field(Person::nameProperty, "empty");
-	 *
-	 * 
- * - * - * @param accessor - * a function that returns the property for a given model instance. Typically you will use a method - * reference to the javafx-property accessor method. - * @param defaultValue - * the default value for the field. - * @param - * the type of the field. - * - * @return The wrapped property instance. - */ - public Property field(Function> accessor, T defaultValue) { - return add(new FxPropertyField<>(accessor, defaultValue)); - } + + /** Field type String **/ /** - * Add a new field to this instance of the wrapper. This method is used for model elements that are following the - * normal Java-Beans-standard i.e. the model fields are only available via getter and setter methods and not as - * JavaFX Properties. + * Add a new field of type String to this instance of the wrapper. This method is used for model elements that are + * following the normal Java-Beans-standard i.e. the model fields are only available via getter and setter methods + * and not as JavaFX Properties. * *

* * Example: *

- * + * *

-	 *     ModelWrapper{@code} personWrapper = new ModelWrapper{@code<>}();
+	 * ModelWrapper{@code} personWrapper = new ModelWrapper{@code<>}();
 	 * 
-	 *     Property{@code} wrappedNameProperty = personWrapper.field(person -> person.getName(), (person, value) -> person.setName(value), "empty");
+	 * StringProperty wrappedNameProperty = personWrapper.field(person -> person.getName(), (person, value)
+	 * 	 -> person.setName(value), "empty");
 	 * 
-	 *     // or with a method reference
-	 *     Property{@code} wrappedNameProperty = personWrapper.field(Person::getName, Person::setName, "empty");
+	 * // or with a method reference
+	 * StringProperty wrappedNameProperty = personWrapper.field(Person::getName, Person::setName, "empty");
 	 *
 	 * 
* @@ -469,36 +431,17 @@ public Property field(Function> accessor, T defaultVa * @param setter * a function that sets the given value to the given model element. Typically you will use a method * reference to the setter method of the model element. - * @param - * the type of the field. * * @return The wrapped property instance. */ - public Property field(Function getter, BiConsumer setter) { - return add(new BeanPropertyField<>(getter, setter)); + public StringProperty field(StringGetter getter, StringSetter setter) { + return add(new BeanPropertyField<>(getter, setter, SimpleStringProperty::new)); } /** - * Add a new field to this instance of the wrapper. This method is used for model elements that are following the - * normal Java-Beans-standard i.e. the model fields are only available via getter and setter methods and not as - * JavaFX Properties. - *

- * Additionally you can define a default value that is used as when {@link #reset()} is invoked. - * - *

- * - * Example: - *

- * - *

-	 *     ModelWrapper{@code} personWrapper = new ModelWrapper{@code<>}();
-	 * 
-	 *     Property{@code} wrappedNameProperty = personWrapper.field(person -> person.getName(), (person, value) -> person.setName(value), "empty");
-	 * 
-	 *     // or with a method reference
-	 *     Property{@code} wrappedNameProperty = personWrapper.field(Person::getName, Person::setName, "empty");
-	 *
-	 * 
+ * Add a new field of type String to this instance of the wrapper. See {@link #field(StringGetter, StringSetter)}. + * This method additionally has a parameter to define the default value that is used when the {@link #reset()} + * method is used. * * * @param getter @@ -508,140 +451,370 @@ public Property field(Function getter, BiConsumer setter) { * a function that sets the given value to the given model element. Typically you will use a method * reference to the setter method of the model element. * @param defaultValue - * the default value for the field. - * @param - * the type of the field. + * the default value that is used when {@link #reset()} is invoked. * * @return The wrapped property instance. */ - public Property field(Function getter, BiConsumer setter, T defaultValue) { - return add(new BeanPropertyField<>(getter, setter, defaultValue)); + public StringProperty field(StringGetter getter, StringSetter setter, String defaultValue) { + return add(new BeanPropertyField<>(getter, setter, defaultValue, SimpleStringProperty::new)); } - /** - * Add a new field to this instance of the wrapper that is identified by the given string. This method is basically - * the same as {@link #field(Function)} with one difference: This method can be invoked multiply times but will only - * create a single field instance for every given string identifier. This means that the returned property will be - * the same instance for each call with the same identifier. + * Add a new field of type {@link String} to this instance of the wrapper. This method is used for model elements + * that are following the enhanced JavaFX-Beans-standard i.e. the model fields are available as JavaFX Properties. *

- * This behaviour can be useful when you don't keep a reference to the returned property but directly call this - * method in a property accessor method in your viewModel. This way only a single field wrapping will be defined - * even when the accessor method is called multiple times. See the following example of a typical use case: + * + * Example: *

- * + * *

+	 * ModelWrapper{@code} personWrapper = new ModelWrapper{@code<>}();
 	 * 
-	 *     public class PersonViewModel extends ViewModel {
-	 *         
-	 *         // you only need a reference to the model wrapper itself but no additional references to each wrapped property.
-	 *         private ModelWrapper{@code} wrapper = new ModelWrapper{@code<>}();
-	 *         
-	 *         
-	 *         // This method will be used from the view. 
-	 *         // The view can call this method multiple times but will always get the same property instance.
-	 *         public Property{@code} nameProperty() {
-	 *              return wrapper.field("name", Person::nameProperty);
-	 *         }
-	 *     }
-	 * 
- * + * StringProperty wrappedNameProperty = personWrapper.field(person -> person.nameProperty()); * - * @param fieldName - * the identifier for this field. Typically you will use the name of the field in the model. + * // or with a method reference + * StringProperty wrappedNameProperty = personWrapper.field(Person::nameProperty); + * + * + * * @param accessor * a function that returns the property for a given model instance. Typically you will use a method * reference to the javafx-property accessor method. - * @param - * the type of the field. + * * @return The wrapped property instance. */ - public Property field(String fieldName, Function> accessor) { - return addIdentified(fieldName, new FxPropertyField<>(accessor)); + public StringProperty field(StringPropertyAccessor accessor) { + return add(new FxPropertyField<>(accessor::apply, SimpleStringProperty::new)); } /** - * See {@link #field(String, Function)}. The difference is that this method accepts an additional parameter to - * define the default value that will be used when {@link #reset()} is invoked. - * - * @param fieldName - * the identifier for this field. Typically you will use the name of the field in the model. + * Add a new field of type String to this instance of the wrapper. See {@link #field(StringGetter, StringSetter)}. + * This method additionally has a parameter to define the default value that is used when the {@link #reset()} + * method is used. + * * @param accessor * a function that returns the property for a given model instance. Typically you will use a method * reference to the javafx-property accessor method. - * @param - * the type of the field. + * @param defaultValue + * the default value that is used when {@link #reset()} is invoked. * @return The wrapped property instance. */ - public Property field(String fieldName, Function> accessor, T defaultValue) { - return addIdentified(fieldName, new FxPropertyField<>(accessor, defaultValue)); + public StringProperty field(StringPropertyAccessor accessor, String defaultValue) { + return add(new FxPropertyField<>(accessor::apply, SimpleStringProperty::new)); } + + + /** - * Add a new field to this instance of the wrapper that is identified by the given string. This method is basically - * the same as {@link #field(Function, BiConsumer)} with one difference: This method can be invoked multiply times - * but will only create a single field instance for every given string identifier. This means that the returned - * property will be the same instance for each call with the same identifier. - *

- * This behaviour can be useful when you don't keep a reference to the returned property but directly call this - * method in a property accessor method in your viewModel. This way only a single field wrapping will be defined - * even when the accessor method is called multiple times. See the following example of a typical use case: - *

- * - *

-	 *
-	 *     public class PersonViewModel extends ViewModel {
-	 * 
-	 *         // you only need a reference to the model wrapper itself but no additional references to each wrapped property.
-	 *         private ModelWrapper{@code} wrapper = new ModelWrapper{@code<>}();
-	 * 
-	 * 
-	 *         // This method will be used from the view. 
-	 *         // The view can call this method multiple times but will always get the same property instance.
-	 *         public Property{@code} nameProperty() {
-	 *              return wrapper.field("name", Person::getName, Person::setName);
-	 *         }
-	 *     }
-	 * 
+ * Add a new field of type String to this instance of the wrapper. See {@link #field(StringGetter, StringSetter)}. + * This method additionally takes a string identifier as first parameter. * + * This identifier is used to return the same property instance even when the method is invoked multiple times. * - * @param fieldName - * the identifier for this field. Typically you will use the name of the field in the model. + * @param identifier + * an identifier for the field. * @param getter * a function that returns the current value of the field for a given model element. Typically you will * use a method reference to the getter method of the model element. * @param setter * a function that sets the given value to the given model element. Typically you will use a method * reference to the setter method of the model element. - * @param - * the type of the field. * @return The wrapped property instance. */ - public Property field(String fieldName, Function getter, BiConsumer setter) { - return addIdentified(fieldName, new BeanPropertyField<>(getter, setter)); + public StringProperty field(String identifier, StringGetter getter, StringSetter setter) { + return addIdentified(identifier, new BeanPropertyField<>(getter, setter, SimpleStringProperty::new)); + } + + public StringProperty field(String identifier, StringGetter getter, StringSetter setter, String defaultValue) { + return addIdentified(identifier, new BeanPropertyField<>(getter, setter, defaultValue, + SimpleStringProperty::new)); } /** - * See {@link #field(String, Function, BiConsumer)}. The difference is that this method accepts an additional - * parameter to define the default value that will be used when {@link #reset()} is invoked. + * Add a new field of type String to this instance of the wrapper. See {@link #field(StringPropertyAccessor)}. This + * method additionally takes a string identifier as first parameter. * - * @param fieldName - * the identifier for this field. Typically you will use the name of the field in the model. - * @param getter - * a function that returns the current value of the field for a given model element. Typically you will - * use a method reference to the getter method of the model element. - * @param setter - * a function that sets the given value to the given model element. Typically you will use a method - * reference to the setter method of the model element. - * @param - * the type of the field. + * This identifier is used to return the same property instance even when the method is invoked multiple times. + * + * @param identifier + * an identifier for the field. + * + * @param accessor + * a function that returns the property for a given model instance. Typically you will use a method + * reference to the javafx-property accessor method. * @return The wrapped property instance. */ - public Property field(String fieldName, Function getter, BiConsumer setter, T defaultValue) { - return addIdentified(fieldName, new BeanPropertyField<>(getter, setter, defaultValue)); + public StringProperty field(String identifier, StringPropertyAccessor accessor) { + return addIdentified(identifier, new FxPropertyField<>(accessor::apply, SimpleStringProperty::new)); + } + + public StringProperty field(String identifier, StringPropertyAccessor accessor, String defaultValue) { + return addIdentified(identifier, + new FxPropertyField<>(accessor::apply, defaultValue, SimpleStringProperty::new)); + } + + /** Field type Boolean **/ + + public BooleanProperty field(BooleanGetter getter, BooleanSetter setter) { + return add(new BeanPropertyField<>(getter, setter, SimpleBooleanProperty::new)); + } + + public BooleanProperty field(BooleanGetter getter, BooleanSetter setter, boolean defaultValue) { + return add(new BeanPropertyField<>(getter, setter, defaultValue, SimpleBooleanProperty::new)); + } + + public BooleanProperty field(BooleanPropertyAccessor accessor) { + return add(new FxPropertyField<>(accessor, SimpleBooleanProperty::new)); + } + + public BooleanProperty field(BooleanPropertyAccessor accessor, boolean defaultValue) { + return add(new FxPropertyField<>(accessor, defaultValue, SimpleBooleanProperty::new)); + } + + public BooleanProperty field(String identifier, BooleanGetter getter, BooleanSetter setter) { + return addIdentified(identifier, new BeanPropertyField<>(getter, setter, SimpleBooleanProperty::new)); + } + + public BooleanProperty field(String identifier, BooleanGetter getter, BooleanSetter setter, + boolean defaultValue) { + return addIdentified(identifier, new BeanPropertyField<>(getter, setter, defaultValue, + SimpleBooleanProperty::new)); + } + + public BooleanProperty field(String identifier, BooleanPropertyAccessor accessor) { + return addIdentified(identifier, new FxPropertyField<>(accessor, SimpleBooleanProperty::new)); + } + + public BooleanProperty field(String identifier, BooleanPropertyAccessor accessor, boolean defaultValue) { + return addIdentified(identifier, new FxPropertyField<>(accessor, defaultValue, SimpleBooleanProperty::new)); + } + + + + /** Field type Double **/ + + + public DoubleProperty field(DoubleGetter getter, DoubleSetter setter) { + return add(new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.doubleValue()), + SimpleDoubleProperty::new)); + } + + public DoubleProperty field(DoubleGetter getter, DoubleSetter setter, double defaultValue) { + return add(new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.doubleValue()), + defaultValue, + SimpleDoubleProperty::new)); + } + + public DoubleProperty field(DoublePropertyAccessor accessor) { + return add(new FxPropertyField<>(accessor::apply, SimpleDoubleProperty::new)); + } + + public DoubleProperty field(DoublePropertyAccessor accessor, double defaultValue) { + return add(new FxPropertyField<>(accessor::apply, defaultValue, SimpleDoubleProperty::new)); + } + + public DoubleProperty field(String identifier, DoubleGetter getter, DoubleSetter setter) { + return addIdentified(identifier, + new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.doubleValue()), + SimpleDoubleProperty::new)); + } + + public DoubleProperty field(String identifier, DoubleGetter getter, DoubleSetter setter, double defaultValue) { + return addIdentified(identifier, + new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.doubleValue()), + defaultValue, + SimpleDoubleProperty::new)); + } + + public DoubleProperty field(String identifier, DoublePropertyAccessor accessor) { + return addIdentified(identifier, new FxPropertyField<>(accessor::apply, SimpleDoubleProperty::new)); + } + + public DoubleProperty field(String identifier, DoublePropertyAccessor accessor, double defaultValue) { + return addIdentified(identifier, + new FxPropertyField<>(accessor::apply, defaultValue, SimpleDoubleProperty::new)); + } + + + + + /** Field type Float **/ + + public FloatProperty field(FloatGetter getter, FloatSetter setter) { + return add(new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.floatValue()), + SimpleFloatProperty::new)); + } + + public FloatProperty field(FloatGetter getter, FloatSetter setter, float defaultValue) { + return add(new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.floatValue()), + defaultValue, + SimpleFloatProperty::new)); + } + + public FloatProperty field(FloatPropertyAccessor accessor) { + return add(new FxPropertyField<>(accessor::apply, SimpleFloatProperty::new)); + } + + public FloatProperty field(FloatPropertyAccessor accessor, float defaultValue) { + return add(new FxPropertyField<>(accessor::apply, defaultValue, SimpleFloatProperty::new)); + } + + public FloatProperty field(String identifier, FloatGetter getter, FloatSetter setter) { + return addIdentified(identifier, + new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.floatValue()), + SimpleFloatProperty::new)); + } + + public FloatProperty field(String identifier, FloatGetter getter, FloatSetter setter, float defaultValue) { + return addIdentified(identifier, + new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.floatValue()), + defaultValue, + SimpleFloatProperty::new)); + } + + public FloatProperty field(String identifier, FloatPropertyAccessor accessor) { + return addIdentified(identifier, new FxPropertyField<>(accessor::apply, SimpleFloatProperty::new)); + } + + public FloatProperty field(String identifier, FloatPropertyAccessor accessor, float defaultValue) { + return addIdentified(identifier, new FxPropertyField<>(accessor::apply, defaultValue, SimpleFloatProperty::new)); + } + + + /** Field type Integer **/ + + + public IntegerProperty field(IntGetter getter, IntSetter setter) { + return add(new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.intValue()), + SimpleIntegerProperty::new)); + } + + public IntegerProperty field(IntGetter getter, IntSetter setter, int defaultValue) { + return add(new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.intValue()), + defaultValue, + SimpleIntegerProperty::new)); + } + + + public IntegerProperty field(IntPropertyAccessor accessor) { + return add(new FxPropertyField<>(accessor::apply, SimpleIntegerProperty::new)); + } + + public IntegerProperty field(IntPropertyAccessor accessor, int defaultValue) { + return add(new FxPropertyField<>(accessor::apply, defaultValue, SimpleIntegerProperty::new)); + } + + public IntegerProperty field(String identifier, IntGetter getter, IntSetter setter) { + return addIdentified(identifier, + new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.intValue()), + SimpleIntegerProperty::new)); + } + + public IntegerProperty field(String identifier, IntGetter getter, IntSetter setter, int defaultValue) { + return addIdentified(identifier, + new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.intValue()), + defaultValue, + SimpleIntegerProperty::new)); + } + + + public IntegerProperty field(String identifier, IntPropertyAccessor accessor) { + return addIdentified(identifier, new FxPropertyField<>(accessor::apply, SimpleIntegerProperty::new)); + } + + public IntegerProperty field(String identifier, IntPropertyAccessor accessor, int defaultValue) { + return addIdentified(identifier, new FxPropertyField<>(accessor::apply, defaultValue, + SimpleIntegerProperty::new)); + } + + + + /** Field type Long **/ + + public LongProperty field(LongGetter getter, LongSetter setter) { + return add(new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.longValue()), + SimpleLongProperty::new)); + } + + public LongProperty field(LongGetter getter, LongSetter setter, long defaultValue) { + return add(new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.longValue()), + defaultValue, + SimpleLongProperty::new)); + } + + public LongProperty field(LongPropertyAccessor accessor) { + return add(new FxPropertyField<>(accessor::apply, SimpleLongProperty::new)); + } + + public LongProperty field(LongPropertyAccessor accessor, long defaultValue) { + return add(new FxPropertyField<>(accessor::apply, defaultValue, SimpleLongProperty::new)); + } + + + public LongProperty field(String identifier, LongGetter getter, LongSetter setter) { + return addIdentified(identifier, + new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.longValue()), + SimpleLongProperty::new)); + } + + public LongProperty field(String identifier, LongGetter getter, LongSetter setter, long defaultValue) { + return addIdentified(identifier, + new BeanPropertyField<>(getter::apply, (m, number) -> setter.accept(m, number.longValue()), + defaultValue, + SimpleLongProperty::new)); + } + + public LongProperty field(String identifier, LongPropertyAccessor accessor) { + return addIdentified(identifier, new FxPropertyField<>(accessor::apply, SimpleLongProperty::new)); + } + + public LongProperty field(String identifier, LongPropertyAccessor accessor, long defaultValue) { + return addIdentified(identifier, new FxPropertyField<>(accessor::apply, defaultValue, SimpleLongProperty::new)); + } + + + + /** Field type generic **/ + + + public ObjectProperty field(ObjectGetter getter, ObjectSetter setter) { + return add(new BeanPropertyField<>(getter, setter, SimpleObjectProperty::new)); + } + + public ObjectProperty field(ObjectGetter getter, ObjectSetter setter, T defaultValue) { + return add(new BeanPropertyField<>(getter, setter, defaultValue, SimpleObjectProperty::new)); + } + + public ObjectProperty field(ObjectPropertyAccessor accessor) { + return add(new FxPropertyField<>(accessor::apply, SimpleObjectProperty::new)); + } + + public ObjectProperty field(ObjectPropertyAccessor accessor, T defaultValue) { + return add(new FxPropertyField<>(accessor::apply, defaultValue, SimpleObjectProperty::new)); + } + + + public ObjectProperty field(String identifier, ObjectGetter getter, ObjectSetter setter) { + return addIdentified(identifier, new BeanPropertyField<>(getter, setter, SimpleObjectProperty::new)); + } + + public ObjectProperty field(String identifier, ObjectGetter getter, ObjectSetter setter, + T defaultValue) { + return addIdentified(identifier, new BeanPropertyField<>(getter, setter, defaultValue, + SimpleObjectProperty::new)); + } + + public ObjectProperty field(String identifier, ObjectPropertyAccessor accessor) { + return addIdentified(identifier, new FxPropertyField<>(accessor::apply, SimpleObjectProperty::new)); + } + + public ObjectProperty field(String identifier, ObjectPropertyAccessor accessor, T defaultValue) { + return addIdentified(identifier, + new FxPropertyField<>(accessor::apply, defaultValue, SimpleObjectProperty::new)); } - private Property add(PropertyField field) { + private > R add(PropertyField field) { fields.add(field); if (model != null) { field.reload(model); @@ -650,10 +823,10 @@ private Property add(PropertyField field) { } @SuppressWarnings("unchecked") - private Property addIdentified(String fieldName, PropertyField field) { + private > R addIdentified(String fieldName, PropertyField field) { if (identifiedFields.containsKey(fieldName)) { final Property property = identifiedFields.get(fieldName).getProperty(); - return (Property) property; + return (R) property; } else { identifiedFields.put(fieldName, field); return add(field); diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/BooleanGetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/BooleanGetter.java new file mode 100644 index 000000000..639e3daee --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/BooleanGetter.java @@ -0,0 +1,21 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.Function; + +/** + * A functional interface to define a getter method of type {@link Boolean}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface BooleanGetter extends Function { + + /** + * @param model + * the model instance. + * @return the value of the field. + */ + @Override + Boolean apply(M model); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/BooleanPropertyAccessor.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/BooleanPropertyAccessor.java new file mode 100644 index 000000000..f36e7aa18 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/BooleanPropertyAccessor.java @@ -0,0 +1,22 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import javafx.beans.property.Property; + +import java.util.function.Function; + +/** + * A functional interface to define an accessor method for a property of type {@link Boolean}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface BooleanPropertyAccessor extends Function> { + /** + * @param model + * the model instance. + * @return the property field of the model. + */ + @Override + Property apply(M model); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/BooleanSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/BooleanSetter.java new file mode 100644 index 000000000..05ad65113 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/BooleanSetter.java @@ -0,0 +1,22 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.BiConsumer; + +/** + * A functional interface to define a setter method of type {@link Boolean}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface BooleanSetter extends BiConsumer { + + /** + * @param model + * the model instance. + * @param value + * the new value to be set. + */ + @Override + void accept(M model, Boolean value); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/DoubleGetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/DoubleGetter.java new file mode 100644 index 000000000..32c33c75c --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/DoubleGetter.java @@ -0,0 +1,21 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.Function; + +/** + * A functional interface to define a getter method of type {@link Double}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface DoubleGetter extends Function { + + /** + * @param model + * the model instance. + * @return the value of the field. + */ + @Override + Double apply(M model); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/DoublePropertyAccessor.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/DoublePropertyAccessor.java new file mode 100644 index 000000000..ecfadcfed --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/DoublePropertyAccessor.java @@ -0,0 +1,24 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.Function; + +import javafx.beans.property.DoubleProperty; + +/** + * A functional interface to define an accessor method for a property of type {@link Double}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface DoublePropertyAccessor extends Function { + + /** + * @param model + * the model instance. + * @return the property field of the model. + */ + @Override + DoubleProperty apply(M model); + +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/DoubleSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/DoubleSetter.java new file mode 100644 index 000000000..74c8376b4 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/DoubleSetter.java @@ -0,0 +1,22 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.BiConsumer; + +/** + * A functional interface to define a setter method of type {@link Double}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface DoubleSetter extends BiConsumer { + + /** + * @param model + * the model instance. + * @param value + * the new value to be set. + */ + @Override + void accept(M model, Double value); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/FloatGetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/FloatGetter.java new file mode 100644 index 000000000..c04f95055 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/FloatGetter.java @@ -0,0 +1,21 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.Function; + +/** + * A functional interface to define a getter method of type {@link Float}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface FloatGetter extends Function { + + /** + * @param model + * the model instance. + * @return the value of the field. + */ + @Override + Float apply(M model); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/FloatPropertyAccessor.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/FloatPropertyAccessor.java new file mode 100644 index 000000000..09c914913 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/FloatPropertyAccessor.java @@ -0,0 +1,23 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import javafx.beans.property.FloatProperty; + +import java.util.function.Function; + +/** + * A functional interface to define an accessor method for a property of type {@link Float}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface FloatPropertyAccessor extends Function { + + /** + * @param model + * the model instance. + * @return the property field of the model. + */ + @Override + FloatProperty apply(M model); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/FloatSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/FloatSetter.java new file mode 100644 index 000000000..68199b5f0 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/FloatSetter.java @@ -0,0 +1,22 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.BiConsumer; + +/** + * A functional interface to define a setter method of type {@link Float}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface FloatSetter extends BiConsumer { + + /** + * @param model + * the model instance. + * @param value + * the new value to be set. + */ + @Override + void accept(M model, Float value); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/IntGetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/IntGetter.java new file mode 100644 index 000000000..c413c3379 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/IntGetter.java @@ -0,0 +1,21 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.Function; + +/** + * A functional interface to define a getter method of type {@link Integer}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface IntGetter extends Function { + + /** + * @param model + * the model instance. + * @return the value of the field. + */ + @Override + Integer apply(M model); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/IntPropertyAccessor.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/IntPropertyAccessor.java new file mode 100644 index 000000000..ceeeda826 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/IntPropertyAccessor.java @@ -0,0 +1,23 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import javafx.beans.property.IntegerProperty; + +import java.util.function.Function; + +/** + * A functional interface to define an accessor method for a property of type {@link Integer}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface IntPropertyAccessor extends Function { + + /** + * @param model + * the model instance. + * @return the property field of the model. + */ + @Override + IntegerProperty apply(M model); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/IntSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/IntSetter.java new file mode 100644 index 000000000..654f321f3 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/IntSetter.java @@ -0,0 +1,22 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.BiConsumer; + +/** + * A functional interface to define a setter method of type {@link Integer}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface IntSetter extends BiConsumer { + + /** + * @param model + * the model instance. + * @param value + * the new value to be set. + */ + @Override + void accept(M model, Integer value); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/LongGetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/LongGetter.java new file mode 100644 index 000000000..85d3f6a60 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/LongGetter.java @@ -0,0 +1,21 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.Function; + +/** + * A functional interface to define a getter method of type {@link Long}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface LongGetter extends Function { + + /** + * @param model + * the model instance. + * @return the value of the field. + */ + @Override + Long apply(M model); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/LongPropertyAccessor.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/LongPropertyAccessor.java new file mode 100644 index 000000000..145d20f34 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/LongPropertyAccessor.java @@ -0,0 +1,23 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import javafx.beans.property.LongProperty; + +import java.util.function.Function; + +/** + * A functional interface to define an accessor method for a property of type {@link Long}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface LongPropertyAccessor extends Function { + + /** + * @param model + * the model instance. + * @return the property field of the model. + */ + @Override + LongProperty apply(M model); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/LongSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/LongSetter.java new file mode 100644 index 000000000..d53d0613d --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/LongSetter.java @@ -0,0 +1,22 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.BiConsumer; + +/** + * A functional interface to define a setter method of type {@link Long}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface LongSetter extends BiConsumer { + + /** + * @param model + * the model instance. + * @param value + * the new value to be set. + */ + @Override + void accept(M model, Long value); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ObjectGetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ObjectGetter.java new file mode 100644 index 000000000..e035ff01b --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ObjectGetter.java @@ -0,0 +1,23 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.Function; + +/** + * A functional interface to define a getter method of a generic type. + * + * @param + * the generic type of the model. + * @param + * the generic type of the field. + */ +@FunctionalInterface +public interface ObjectGetter extends Function { + + /** + * @param model + * the model instance. + * @return the value of the field. + */ + @Override + T apply(M model); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ObjectPropertyAccessor.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ObjectPropertyAccessor.java new file mode 100644 index 000000000..5deca65ba --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ObjectPropertyAccessor.java @@ -0,0 +1,25 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import javafx.beans.property.Property; + +import java.util.function.Function; + +/** + * A functional interface to define an accessor method for a property of a generic type. + * + * @param + * the generic type of the model. + * @param + * the generic type of the field. + */ +@FunctionalInterface +public interface ObjectPropertyAccessor extends Function> { + + /** + * @param model + * the model instance. + * @return the property field of the model. + */ + @Override + Property apply(M model); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ObjectSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ObjectSetter.java new file mode 100644 index 000000000..aff8be60a --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ObjectSetter.java @@ -0,0 +1,25 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.BiConsumer; + + +/** + * A functional interface to define a setter method of a generic type. + * + * @param + * the generic type of the model. + * @param + * the generic type of the field. + */ +@FunctionalInterface +public interface ObjectSetter extends BiConsumer { + + /** + * @param model + * the model instance. + * @param value + * the new value to be set. + */ + @Override + void accept(M model, T value); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/StringGetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/StringGetter.java new file mode 100644 index 000000000..8c9838892 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/StringGetter.java @@ -0,0 +1,21 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.Function; + +/** + * A functional interface to define a getter method of type {@link String}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface StringGetter extends Function { + + /** + * @param model + * the model instance. + * @return the value of the field. + */ + @Override + String apply(M model); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/StringPropertyAccessor.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/StringPropertyAccessor.java new file mode 100644 index 000000000..3fd6772ba --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/StringPropertyAccessor.java @@ -0,0 +1,23 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import javafx.beans.property.Property; + +import java.util.function.Function; + +/** + * A functional interface to define an accessor method for a property of type {@link String}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface StringPropertyAccessor extends Function> { + + /** + * @param model + * the model instance. + * @return the property field of the model. + */ + @Override + Property apply(M model); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/StringSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/StringSetter.java new file mode 100644 index 000000000..0fa709647 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/StringSetter.java @@ -0,0 +1,23 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.BiConsumer; + + +/** + * A functional interface to define a setter method of type {@link String}. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface StringSetter extends BiConsumer { + + /** + * @param model + * the model instance. + * @param value + * the new value to be set. + */ + @Override + void accept(M model, String value); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/package-info.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/package-info.java new file mode 100644 index 000000000..8167f3d89 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/package-info.java @@ -0,0 +1,11 @@ +/** + * + * This package contains functional interfaces to define accessors of different types. + * These are used to define a mapping with the {@link de.saxsys.mvvmfx.utils.mapping.ModelWrapper}. + * + * We need these explicit interfaces to support Java with the type deduction when using method references + * of the getter/setter methods of the mapped model class. + * + * @author manuel.mauky + */ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; \ No newline at end of file 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 11faa8592..4eba99b6c 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 @@ -20,7 +20,11 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; -import java.util.Map; +import java.util.stream.Collectors; + +import de.saxsys.mvvmfx.ViewModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Default implementation of {@link NotificationCenter}. @@ -30,49 +34,74 @@ */ class DefaultNotificationCenter implements NotificationCenter { + private static final Logger LOG = LoggerFactory.getLogger(DefaultNotificationCenter.class); + DefaultNotificationCenter() { } - private final Map> observersForName = new HashMap>(); + private final ObserverMap globalObservers = new ObserverMap(); + private final ViewModelObservers viewModelObservers = new ViewModelObservers(); @Override public void subscribe(String messageName, NotificationObserver observer) { - List observers = this.observersForName.get(messageName); - if (observers == null) { - this.observersForName.put(messageName, new ArrayList()); - } - observers = this.observersForName.get(messageName); - observers.add(observer); + addObserver(messageName, observer, globalObservers); } @Override public void unsubscribe(String messageName, NotificationObserver observer) { - List observers = this.observersForName.get(messageName); - observers.remove(observer); - if (observers.size() == 0) { - this.observersForName.remove(messageName); - } + removeObserversForMessageName(messageName, observer, globalObservers); } @Override public void unsubscribe(NotificationObserver observer) { - Iterator iterator = this.observersForName.keySet().iterator(); - while (iterator.hasNext()) { - String key = iterator.next(); - Iterator iterator2 = this.observersForName.get(key).iterator(); - while (iterator2.hasNext()) { - NotificationObserver actualObserver = iterator2.next(); - if (actualObserver == observer) { - this.observersForName.remove(key); - break; - } - } - } + removeObserverFromObserverMap(observer, globalObservers); } @Override public void publish(String messageName, Object... payload) { - Collection notificationReceivers = observersForName.get(messageName); + publish(messageName, payload, globalObservers); + } + + @Override + public void publish(ViewModel viewModel, String messageName, Object[] payload) { + ObserverMap observerMap = viewModelObservers.get(viewModel); + if (observerMap != null) { + publish(messageName, payload, observerMap); + } + } + + + @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); + } + addObserver(messageName, observer, observerMap); + } + + @Override + public void unsubscribe(ViewModel view, String messageName, NotificationObserver observer) { + ObserverMap observerMap = viewModelObservers.get(view); + if (observerMap != null) { + removeObserversForMessageName(messageName, observer, observerMap); + } + } + + + @Override + public void unsubscribe(ViewModel viewModel, NotificationObserver observer) { + ObserverMap observerMap = viewModelObservers.get(viewModel); + removeObserverFromObserverMap(observer, observerMap); + } + + /* + * Helper + */ + + private void publish(String messageName, Object[] payload, ObserverMap observerMap) { + Collection notificationReceivers = observerMap.get(messageName); if (notificationReceivers != null) { for (NotificationObserver observer : notificationReceivers) { observer.receivedNotification(messageName, payload); @@ -80,4 +109,52 @@ public void publish(String messageName, Object... payload) { } } + private void addObserver(String messageName, NotificationObserver observer, ObserverMap observerMap) { + List observers = observerMap.get(messageName); + if (observers == null) { + observerMap.put(messageName, new ArrayList()); + } + observers = observerMap.get(messageName); + + if(observers.contains(observer)) { + LOG.warn("Subscribe the observer ["+ observer + "] for the message [" + messageName + + "], but the same observer was already added for this message in the past."); + } + observers.add(observer); + } + + + + private void removeObserverFromObserverMap(NotificationObserver observer, ObserverMap observerMap) { + 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); + } + } + + private void removeObserversForMessageName(String messageName, NotificationObserver observer, + ObserverMap observerMap) { + List observers = observerMap.get(messageName); + observers.remove(observer); + if (observers.size() == 0) { + observerMap.remove(messageName); + } + } + + @SuppressWarnings("serial") + private class ObserverMap extends HashMap> { + } + + @SuppressWarnings("serial") + private class ViewModelObservers extends HashMap { + } + + + } diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenter.java index c996645cf..4724860b3 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenter.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationCenter.java @@ -15,6 +15,8 @@ ******************************************************************************/ package de.saxsys.mvvmfx.utils.notifications; +import de.saxsys.mvvmfx.ViewModel; + /** * Central component to provide a notification mechanism. You can add observers by using keys to get notifications for @@ -32,6 +34,11 @@ public interface NotificationCenter { /** * Add an observer to the NotificationCenter which gets notifications for the given String. * + * Please note: It is possible (but yet untypical) to add the same observer for the same message multiple times. + * In this case the observer will be invoked multiple times too. + * As this behaviour is unusual, the default notification center will log a warning message when the same observer + * is added multiple times for the same message. + * * @param messageName * key of the notification to listen * @param observer @@ -71,4 +78,47 @@ void unsubscribe(String messageName, */ void publish(String messageName, Object... payload); + + /** + * Publishes a notification to the {@link ViewModel}-subscribers for the given notificationId. + * + * @param messageName + * of the notification + * @param payload + * to be send + */ + void publish(ViewModel viewModel, String messageName, Object[] payload); + + /** + * Subscribe to a {@link ViewModel}-notification with a given {@link NotificationObserver}. + * + * @param viewModel + * + * @param messageName + * of the Notification + * @param observer + * which should execute when the notification occurs + */ + void subscribe(ViewModel view, String messageName, + NotificationObserver observer); + + /** + * Removes a {@link NotificationObserver} for a given messageName. + * + * @param viewModel + * @param messageName + * @param observer + */ + void unsubscribe(ViewModel viewModel, String messageName, + NotificationObserver observer); + + /** + * Removes a {@link NotificationObserver} for all messageName. + * + * @param viewModel + * @param observer + */ + void unsubscribe(ViewModel view, + NotificationObserver observer); + } 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 8f1edd66a..56e4ba9a0 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,22 +1,36 @@ package de.saxsys.mvvmfx.utils.commands; -import de.saxsys.mvvmfx.testingutils.GCVerifier; +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; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import com.cedarsoft.test.utils.CatchAllExceptionsRule; +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 java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -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; +import de.saxsys.javafx.test.JfxRunner; +import de.saxsys.mvvmfx.testingutils.GCVerifier; +@RunWith(JfxRunner.class) public class CompositeCommandTest { - + + // Rule to get exceptions from the JavaFX Thread into the JUnit thread + @Rule + public CatchAllExceptionsRule catchAllExceptionsRule = new CatchAllExceptionsRule(); + + private BooleanProperty condition1; private BooleanProperty called1; private DelegateCommand delegateCommand1; @@ -28,11 +42,22 @@ public class CompositeCommandTest { public void init() { condition1 = new SimpleBooleanProperty(true); called1 = new SimpleBooleanProperty(); - delegateCommand1 = new DelegateCommand(() -> called1.set(true), condition1); + delegateCommand1 = new DelegateCommand(() -> new Action() { + + @Override + protected void action() throws Exception { + called1.set(true); + } + }, condition1); condition2 = new SimpleBooleanProperty(true); called2 = new SimpleBooleanProperty(); - delegateCommand2 = new DelegateCommand(() -> called2.set(true), condition2); + delegateCommand2 = new DelegateCommand(() -> new Action() { + @Override + protected void action() throws Exception { + called2.set(true); + } + }, condition2); } @Test @@ -42,22 +67,27 @@ public void executable() throws Exception { GCVerifier.forceGC(); assertTrue(compositeCommand.isExecutable()); + assertFalse(compositeCommand.isNotExecutable()); condition1.set(false); assertFalse(compositeCommand.isExecutable()); + assertTrue(compositeCommand.isNotExecutable()); condition2.set(false); assertFalse(compositeCommand.isExecutable()); + assertTrue(compositeCommand.isNotExecutable()); condition1.set(true); assertFalse(compositeCommand.isExecutable()); + assertTrue(compositeCommand.isNotExecutable()); condition2.set(true); assertTrue(compositeCommand.isExecutable()); + assertFalse(compositeCommand.isNotExecutable()); } @Test @@ -67,28 +97,35 @@ public void executable2() throws Exception { assertThat(compositeCommand.isExecutable()).isTrue(); + assertThat(compositeCommand.isNotExecutable()).isFalse(); compositeCommand.register(delegateCommand1); GCVerifier.forceGC(); assertThat(compositeCommand.isExecutable()).isTrue(); + assertThat(compositeCommand.isNotExecutable()).isFalse(); condition1.setValue(false); assertThat(compositeCommand.isExecutable()).isFalse(); + assertThat(compositeCommand.isNotExecutable()).isTrue(); condition1.setValue(true); assertThat(compositeCommand.isExecutable()).isTrue(); + assertThat(compositeCommand.isNotExecutable()).isFalse(); condition2.setValue(false); assertThat(compositeCommand.isExecutable()).isTrue(); + assertThat(compositeCommand.isNotExecutable()).isFalse(); compositeCommand.register(delegateCommand2); GCVerifier.forceGC(); assertThat(compositeCommand.isExecutable()).isFalse(); + assertThat(compositeCommand.isNotExecutable()).isTrue(); compositeCommand.unregister(delegateCommand2); GCVerifier.forceGC(); assertThat(compositeCommand.isExecutable()).isTrue(); + assertThat(compositeCommand.isNotExecutable()).isFalse(); } @Test @@ -97,41 +134,21 @@ public void register() throws Exception { CompositeCommand compositeCommand = new CompositeCommand(delegateCommand1); assertTrue(compositeCommand.isExecutable()); + assertFalse(compositeCommand.isNotExecutable()); // prepare delegateCommand2 condition2.set(false); + compositeCommand.register(delegateCommand2); assertFalse(compositeCommand.isExecutable()); + assertTrue(compositeCommand.isNotExecutable()); + compositeCommand.unregister(delegateCommand2); assertTrue(compositeCommand.isExecutable()); + assertFalse(compositeCommand.isNotExecutable()); } @Test - public void running() throws Exception { - BooleanProperty run = new SimpleBooleanProperty(); - BooleanProperty finished = new SimpleBooleanProperty(); - CompositeCommand compositeCommand = new CompositeCommand(delegateCommand1, delegateCommand2); - - // We have to check the running Property with this mechanism, because it is processed synchronously and we can't - // hook between the state changes. - compositeCommand.runningProperty().addListener((ChangeListener) (observable, oldValue, newValue) -> { - if (!oldValue && newValue) - run.set(true); - if (oldValue && !newValue) - finished.set(true); - }); - - compositeCommand.execute(); - - assertTrue(run.get()); - assertTrue(finished.get()); - } - - @Test - public void allCommandsAreUnregistered() throws Exception{ - - // UncaughtExceptionHandler is defined to be able to detect exception from listeners. - Thread.currentThread().setUncaughtExceptionHandler((thread, exception) -> fail("Exception was thrown", exception)); - + public void allCommandsAreUnregistered() throws Exception { CompositeCommand compositeCommand = new CompositeCommand(delegateCommand1, delegateCommand2); compositeCommand.unregister(delegateCommand1); @@ -142,43 +159,90 @@ public void allCommandsAreUnregistered() throws Exception{ public void longRunningAsyncComposite() throws Exception { BooleanProperty condition = new SimpleBooleanProperty(true); - + CompletableFuture commandStarted = new CompletableFuture<>(); + CompletableFuture commandCompleted = new CompletableFuture<>(); CompletableFuture future = new CompletableFuture<>(); - DelegateCommand delegateCommand1 = new DelegateCommand(() -> sleep(500), condition, true); + DelegateCommand delegateCommand1 = new DelegateCommand(() -> new Action() { + + @Override + protected void action() throws Exception { + sleep(500); + } + }, condition, true); - DelegateCommand delegateCommand2 = new DelegateCommand(() -> { - sleep(1000); - future.complete(null); + DelegateCommand delegateCommand2 = new DelegateCommand(() -> new Action() { + + @Override + protected void action() throws Exception { + sleep(1000); + future.complete(null); + } }, condition, true); - DelegateCommand delegateCommand3 = new DelegateCommand(() -> { - }, condition, false); + DelegateCommand delegateCommand3 = new DelegateCommand(() -> new Action() { + + @Override + protected void action() throws Exception { + } + }, condition, true); CompositeCommand compositeCommand = new CompositeCommand(delegateCommand1, delegateCommand2, delegateCommand3); - - GCVerifier.forceGC(); - - assertFalse(compositeCommand.runningProperty().get()); - assertFalse(delegateCommand1.runningProperty().get()); - assertFalse(delegateCommand2.runningProperty().get()); - assertFalse(delegateCommand3.runningProperty().get()); - compositeCommand.execute(); + compositeCommand.progressProperty().addListener(new ChangeListener() { - assertTrue(compositeCommand.runningProperty().get()); - assertTrue(delegateCommand1.runningProperty().get()); - assertTrue(delegateCommand2.runningProperty().get()); - assertFalse(delegateCommand3.runningProperty().get()); + @Override + public void changed(ObservableValue observable, Number oldValue, Number newValue) { + } + }); - future.get(3, TimeUnit.SECONDS); + GCVerifier.forceGC(); + + compositeCommand.runningProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) { + if (newValue && !oldValue) { + Platform.runLater(new Runnable() { + @Override + public void run() { + assertTrue(compositeCommand.runningProperty().get()); + assertTrue(delegateCommand1.runningProperty().get()); + assertTrue(delegateCommand2.runningProperty().get()); + assertTrue(delegateCommand3.runningProperty().get()); + assertFalse(compositeCommand.notRunningProperty().get()); + assertFalse(delegateCommand1.notRunningProperty().get()); + assertFalse(delegateCommand2.notRunningProperty().get()); + assertFalse(delegateCommand3.notRunningProperty().get()); + commandCompleted.complete(null); + } + }); + } + if (oldValue && !newValue) { + Platform.runLater(new Runnable() { + @Override + public void run() { + assertFalse(compositeCommand.runningProperty().get()); + assertFalse(delegateCommand1.runningProperty().get()); + assertFalse(delegateCommand2.runningProperty().get()); + assertFalse(delegateCommand3.runningProperty().get()); + assertTrue(compositeCommand.notRunningProperty().get()); + assertTrue(delegateCommand1.notRunningProperty().get()); + assertTrue(delegateCommand2.notRunningProperty().get()); + assertTrue(delegateCommand3.notRunningProperty().get()); + commandStarted.complete(null); + } + }); + } + } + }); - assertFalse(compositeCommand.runningProperty().get()); - assertFalse(delegateCommand1.runningProperty().get()); - assertFalse(delegateCommand2.runningProperty().get()); - assertFalse(delegateCommand3.runningProperty().get()); + compositeCommand.execute(); + commandStarted.get(3, TimeUnit.SECONDS); + future.get(3, TimeUnit.SECONDS); + commandCompleted.get(4, TimeUnit.SECONDS); } + private void sleep(long millis) { try { Thread.sleep(millis); 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 27d7e9491..2e4d3346d 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,36 +1,63 @@ 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; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import com.cedarsoft.test.utils.CatchAllExceptionsRule; +import de.saxsys.javafx.test.TestInJfxThread; +import javafx.application.Application; +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) public class DelegateCommandTest { - + + // Rule to get exceptions from the JavaFX Thread into the JUnit thread + @Rule + public CatchAllExceptionsRule catchAllExceptionsRule = new CatchAllExceptionsRule(); + + + @Test public void executable() { BooleanProperty condition = new SimpleBooleanProperty(true); - DelegateCommand delegateCommand = new DelegateCommand(() -> { + DelegateCommand delegateCommand = new DelegateCommand(() -> new Action() { + @Override + protected void action() { + } }, condition); assertTrue(delegateCommand.isExecutable()); + assertFalse(delegateCommand.isNotExecutable()); condition.set(false); assertFalse(delegateCommand.isExecutable()); + assertTrue(delegateCommand.isNotExecutable()); condition.set(true); assertTrue(delegateCommand.isExecutable()); + assertFalse(delegateCommand.isNotExecutable()); } @Test @@ -38,7 +65,12 @@ public void firePositive() { BooleanProperty condition = new SimpleBooleanProperty(true); BooleanProperty called = new SimpleBooleanProperty(); - DelegateCommand delegateCommand = new DelegateCommand(() -> called.set(true), condition); + DelegateCommand delegateCommand = new DelegateCommand(() -> new Action() { + @Override + protected void action() { + called.set(true); + } + }, condition); assertFalse(called.get()); delegateCommand.execute(); @@ -49,7 +81,10 @@ public void firePositive() { public void fireNegative() { BooleanProperty condition = new SimpleBooleanProperty(false); - DelegateCommand delegateCommand = new DelegateCommand(() -> { + DelegateCommand delegateCommand = new DelegateCommand(() -> new Action() { + @Override + protected void action() { + } }, condition); delegateCommand.execute(); @@ -57,52 +92,105 @@ public void fireNegative() { @Test - public void running() throws Exception { - BooleanProperty run = new SimpleBooleanProperty(); - BooleanProperty finished = new SimpleBooleanProperty(); + public void longRunningAsync() throws Exception { BooleanProperty condition = new SimpleBooleanProperty(true); - DelegateCommand delegateCommand = new DelegateCommand(() -> { - }, condition); + CompletableFuture commandStarted = new CompletableFuture<>(); + CompletableFuture commandCompleted = new CompletableFuture<>(); - delegateCommand.runningProperty().addListener((ChangeListener) (observable, oldValue, newValue) -> { - if (!oldValue && newValue) { - run.set(true); + DelegateCommand delegateCommand = new DelegateCommand(() -> new Action() { + @Override + protected void action() throws Exception { + Thread.sleep(1000); } - if (oldValue && !newValue) { - finished.set(true); + }, condition, true); + + assertFalse(delegateCommand.runningProperty().get()); + assertTrue(delegateCommand.notRunningProperty().get()); + + delegateCommand.runningProperty().addListener(new ChangeListener() { + + @Override + public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) { + if (newValue) { + assertTrue(delegateCommand.runningProperty().get()); + assertFalse(delegateCommand.notRunningProperty().get()); + assertFalse(delegateCommand.executableProperty().get()); + assertTrue(delegateCommand.notExecutableProperty().get()); + commandStarted.complete(null); + } + if (!newValue && oldValue) { + assertFalse(delegateCommand.runningProperty().get()); + assertTrue(delegateCommand.notRunningProperty().get()); + assertTrue(delegateCommand.executableProperty().get()); + assertFalse(delegateCommand.notExecutableProperty().get()); + commandCompleted.complete(null); + } + } }); delegateCommand.execute(); - - assertTrue(run.get()); - assertTrue(finished.get()); + commandStarted.get(3, TimeUnit.SECONDS); + commandCompleted.get(4, TimeUnit.SECONDS); } @Test - public void longRunningAsync() throws Exception { + public void progressProperty() throws Exception { + + CompletableFuture stepOne = new CompletableFuture<>(); + CompletableFuture stepTwo = new CompletableFuture<>(); + CompletableFuture stepThree = new CompletableFuture<>(); + CompletableFuture stepFour = new CompletableFuture<>(); + + DelegateCommand command = new DelegateCommand(()-> new Action() { + @Override + protected void action() throws Exception { + updateProgress(0, 3); + stepOne.complete(null); + sleep(500); + updateProgress(1, 3); + stepTwo.complete(null); + sleep(500); + updateProgress(2, 3); + stepThree.complete(null); + sleep(500); + updateProgress(3, 3); + stepFour.complete(null); + } + }, true); - BooleanProperty condition = new SimpleBooleanProperty(true); - CompletableFuture future = new CompletableFuture<>(); + command.execute(); - DelegateCommand delegateCommand = new DelegateCommand(() -> { - try { - Thread.sleep(1000); - future.complete(null); - } catch (Exception e) { - } - }, condition, true); + stepOne.get(1, TimeUnit.SECONDS); + Platform.runLater(() -> + assertThat(command.getProgress()).isEqualTo(0.0)); + + stepTwo.get(1, TimeUnit.SECONDS); + Platform.runLater(() -> + assertThat(command.getProgress()).isEqualTo(0.3, offset(0.1))); - assertFalse(delegateCommand.runningProperty().get()); - delegateCommand.execute(); - assertTrue(delegateCommand.runningProperty().get()); - assertFalse(delegateCommand.executableProperty().get()); - future.get(3, TimeUnit.SECONDS); - assertFalse(delegateCommand.runningProperty().get()); - assertTrue(delegateCommand.executableProperty().get()); + stepThree.get(1, TimeUnit.SECONDS); + Platform.runLater(() -> + assertThat(command.getProgress()).isEqualTo(0.6, offset(0.1))); + + stepFour.get(1, TimeUnit.SECONDS); + Platform.runLater(() -> + assertThat(command.getProgress()).isEqualTo(1, offset(0.1))); + + // sleep to prevent the Junit thread from exiting + // before eventual assertion errors from the JavaFX Thread are detected + sleep(500); } + + private void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/App.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/App.java new file mode 100644 index 000000000..347eec114 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/App.java @@ -0,0 +1,27 @@ +package de.saxsys.mvvmfx.utils.commands.testapp; + +/** + * @author manuel.mauky + */ + +import de.saxsys.mvvmfx.FluentViewLoader; +import javafx.application.Application; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +public class App extends Application { + + public static void main(String[] args) { + launch(args); + } + + @Override + public void start(Stage primaryStage) { + + final Parent view = FluentViewLoader.javaView(MainView.class).load().getView(); + + primaryStage.setScene(new Scene(view)); + primaryStage.show(); + } +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/MainView.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/MainView.java new file mode 100644 index 000000000..9b062e2ea --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/MainView.java @@ -0,0 +1,65 @@ +package de.saxsys.mvvmfx.utils.commands.testapp; + +import de.saxsys.mvvmfx.FluentViewLoader; +import de.saxsys.mvvmfx.InjectViewModel; +import de.saxsys.mvvmfx.JavaView; +import de.saxsys.mvvmfx.ViewTuple; +import de.saxsys.mvvmfx.utils.commands.Command; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.ProgressBar; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +/** + * @author manuel.mauky + */ +public class MainView extends VBox implements JavaView { + + private HBox container = new HBox(); + private Button rollAllDicesButton = new Button("Roll all Dices"); + private ProgressBar progressBar = new ProgressBar(); + + @InjectViewModel + private MainViewModel viewModel; + + public MainView(){ + this.getChildren().add(container); + + HBox footer = new HBox(); + footer.setSpacing(5); + footer.getChildren().addAll(rollAllDicesButton, progressBar); + progressBar.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(progressBar, Priority.ALWAYS); + + this.getChildren().add(footer); + this.setPadding(new Insets(5)); + this.setSpacing(5); + } + + public void initialize() { + final ViewTuple subViewTupleOne = FluentViewLoader.javaView(SubView.class).load(); + viewModel.setChildOne(subViewTupleOne.getViewModel()); + container.getChildren().add(subViewTupleOne.getView()); + + final ViewTuple subViewTupleTwo = FluentViewLoader.javaView(SubView.class).load(); + viewModel.setChildTwo(subViewTupleTwo.getViewModel()); + container.getChildren().add(subViewTupleTwo.getView()); + + final ViewTuple subViewTupleThree = FluentViewLoader.javaView(SubView.class).load(); + viewModel.setChildThree(subViewTupleThree.getViewModel()); + container.getChildren().add(subViewTupleThree.getView()); + + viewModel.init(); + + + final Command command = viewModel.getRollAllDicesCommand(); + + rollAllDicesButton.setOnAction(event -> command.execute()); + rollAllDicesButton.disableProperty().bind(command.notExecutableProperty()); + progressBar.progressProperty().bind(command.progressProperty()); + + } + +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/MainViewModel.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/MainViewModel.java new file mode 100644 index 000000000..ee37c39ec --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/MainViewModel.java @@ -0,0 +1,38 @@ +package de.saxsys.mvvmfx.utils.commands.testapp; + +import de.saxsys.mvvmfx.ViewModel; +import de.saxsys.mvvmfx.utils.commands.Command; +import de.saxsys.mvvmfx.utils.commands.CompositeCommand; + +/** + * @author manuel.mauky + */ +public class MainViewModel implements ViewModel { + + private SubViewModel childOne; + private SubViewModel childTwo; + private SubViewModel childThree; + + + private Command rollAllDicesCommand; + + public void init(){ + rollAllDicesCommand = new CompositeCommand(childOne.getRollDiceCommand(), childTwo.getRollDiceCommand(), childThree.getRollDiceCommand()); + } + + public Command getRollAllDicesCommand() { + return rollAllDicesCommand; + } + + public void setChildOne(SubViewModel childOne) { + this.childOne = childOne; + } + + public void setChildTwo(SubViewModel childTwo) { + this.childTwo = childTwo; + } + + public void setChildThree(SubViewModel childThree) { + this.childThree = childThree; + } +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/Service.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/Service.java new file mode 100644 index 000000000..c43959097 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/Service.java @@ -0,0 +1,24 @@ +package de.saxsys.mvvmfx.utils.commands.testapp; + +import java.util.Random; + +/** + * @author manuel.mauky + */ +public class Service { + + + + public int longRunningService() { + try { + int waitTime = (new Random().nextInt(3) + 1) * 1000; // between 1 and 3 seconds + + Thread.sleep(waitTime); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return new Random().nextInt(6)+1; + } + +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/SubView.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/SubView.java new file mode 100644 index 000000000..f46c84e6f --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/SubView.java @@ -0,0 +1,60 @@ +package de.saxsys.mvvmfx.utils.commands.testapp; + +import de.saxsys.mvvmfx.InjectViewModel; +import de.saxsys.mvvmfx.JavaView; +import de.saxsys.mvvmfx.utils.commands.Command; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ListView; +import javafx.scene.control.ProgressBar; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + + +/** + * @author manuel.mauky + */ +public class SubView extends VBox implements JavaView { + + private ListView numbers = new ListView<>(); + private Button rollDiceButton = new Button("Roll the Dice"); + private CheckBox activeCheckBox = new CheckBox(); + + private ProgressBar progressBar = new ProgressBar(); + + @InjectViewModel + private SubViewModel viewModel; + + public SubView(){ + this.getChildren().add(numbers); + HBox footer = new HBox(); + footer.getChildren().add(activeCheckBox); + footer.getChildren().add(rollDiceButton); + footer.getChildren().add(progressBar); + footer.setSpacing(5); + progressBar.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(progressBar, Priority.ALWAYS); + + this.getChildren().add(footer); + + this.setPadding(new Insets(5)); + this.setSpacing(5); + } + + public void initialize(){ + activeCheckBox.selectedProperty().bindBidirectional(viewModel.activeProperty()); + + numbers.setItems(viewModel.numbersProperty()); + + final Command rollDiceCommand = viewModel.getRollDiceCommand(); + rollDiceButton.setOnAction(event -> { + rollDiceCommand.execute(); + }); + rollDiceButton.disableProperty().bind(rollDiceCommand.notExecutableProperty()); + progressBar.progressProperty().bind(rollDiceCommand.progressProperty()); + } + + +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/SubViewModel.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/SubViewModel.java new file mode 100644 index 000000000..80e15b1a2 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/testapp/SubViewModel.java @@ -0,0 +1,60 @@ +package de.saxsys.mvvmfx.utils.commands.testapp; + +import de.saxsys.mvvmfx.ViewModel; +import de.saxsys.mvvmfx.utils.commands.Action; +import de.saxsys.mvvmfx.utils.commands.Command; +import de.saxsys.mvvmfx.utils.commands.DelegateCommand; +import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.stream.IntStream; + +/** + * @author manuel.mauky + */ +public class SubViewModel implements ViewModel { + + private static final int NUMBERS = 10; + + private ObservableList numbers = FXCollections.observableArrayList(); + + private Command rollDiceCommand; + + private BooleanProperty active = new SimpleBooleanProperty(true); + + public SubViewModel(){ + + Service service = new Service(); + + rollDiceCommand = new DelegateCommand(() -> new Action() { + @Override + protected void action() throws Exception { + updateProgress(0, NUMBERS); + Platform.runLater(numbers::clear); + for (int i = 0; i < NUMBERS; i++) { + final int newNumber = service.longRunningService(); + + updateProgress(i + 1, NUMBERS); + Platform.runLater(() -> numbers.add(newNumber)); + } + } + }, active, true); + + + } + + public Command getRollDiceCommand(){ + return rollDiceCommand; + } + + public ObservableList numbersProperty(){ + return numbers; + } + + public BooleanProperty activeProperty(){ + return active; + } +} 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 new file mode 100644 index 000000000..bd5b556f6 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ExampleModel.java @@ -0,0 +1,102 @@ +package de.saxsys.mvvmfx.utils.mapping; + +import javafx.beans.property.*; + +public class ExampleModel { + + private IntegerProperty integerProperty = new SimpleIntegerProperty(); + private DoubleProperty doubleProperty = new SimpleDoubleProperty(); + private FloatProperty floatProperty = new SimpleFloatProperty(); + private LongProperty longProperty = new SimpleLongProperty(); + + private StringProperty stringProperty = new SimpleStringProperty(); + + private ObjectProperty objectProperty = new SimpleObjectProperty<>(); + + private BooleanProperty booleanProperty = new SimpleBooleanProperty(); + + + public int getInteger() { + return integerProperty.get(); + } + + public IntegerProperty integerProperty() { + return integerProperty; + } + + public void setInteger(int integerProperty) { + this.integerProperty.set(integerProperty); + } + + public double getDouble() { + return doubleProperty.get(); + } + + public DoubleProperty doubleProperty() { + return doubleProperty; + } + + public void setDouble(double doubleProperty) { + this.doubleProperty.set(doubleProperty); + } + + public float getFloat() { + return floatProperty.get(); + } + + public FloatProperty floatProperty() { + return floatProperty; + } + + public void setFloat(float floatProperty) { + this.floatProperty.set(floatProperty); + } + + public long getLong() { + return longProperty.get(); + } + + public LongProperty longProperty() { + return longProperty; + } + + public void setLong(long longProperty) { + this.longProperty.set(longProperty); + } + + public String getString() { + return stringProperty.get(); + } + + public StringProperty stringProperty() { + return stringProperty; + } + + public void setString(String stringProperty) { + this.stringProperty.set(stringProperty); + } + + public Person getObject() { + return objectProperty.get(); + } + + public ObjectProperty objectProperty() { + return objectProperty; + } + + public void setObject(Person objectProperty) { + this.objectProperty.set(objectProperty); + } + + public boolean getBoolean() { + return booleanProperty.get(); + } + + public BooleanProperty booleanProperty() { + return booleanProperty; + } + + public void setBoolean(boolean booleanProperty) { + this.booleanProperty.set(booleanProperty); + } +} 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 0d2a334df..45f1448e8 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,190 +1,183 @@ package de.saxsys.mvvmfx.utils.mapping; -import javafx.beans.binding.Bindings; -import javafx.beans.binding.BooleanBinding; -import javafx.beans.binding.IntegerBinding; -import javafx.beans.binding.IntegerExpression; -import javafx.beans.binding.NumberBinding; import javafx.beans.property.IntegerProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.Property; -import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.StringProperty; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public class ModelWrapperTest { - - - @Test - public void testWithGetterAndSetter(){ - Person person = new Person(); - person.setName("horst"); - person.setAge(32); - - ModelWrapper personWrapper = new ModelWrapper<>(person); - - final Property nameProperty = personWrapper.field(Person::getName, Person::setName); - final Property ageProperty = personWrapper.field(Person::getAge, Person::setAge); - - assertThat(nameProperty.getValue()).isEqualTo("horst"); - assertThat(ageProperty.getValue()).isEqualTo(32); - - - nameProperty.setValue("hugo"); - ageProperty.setValue(33); - - // still the old values - assertThat(person.getName()).isEqualTo("horst"); - assertThat(person.getAge()).isEqualTo(32); - - - personWrapper.commit(); - - // now the new values are reflected in the wrapped person - assertThat(person.getName()).isEqualTo("hugo"); - assertThat(person.getAge()).isEqualTo(33); - - - - nameProperty.setValue("luise"); - ageProperty.setValue(15); - - personWrapper.reset(); - - assertThat(nameProperty.getValue()).isEqualTo(null); - assertThat(ageProperty.getValue()).isEqualTo(null); - - // the wrapped object has still the values from the last commit. - assertThat(person.getName()).isEqualTo("hugo"); - assertThat(person.getAge()).isEqualTo(33); - - - personWrapper.reload(); - // now the properties have the values from the wrapped object - assertThat(nameProperty.getValue()).isEqualTo("hugo"); - assertThat(ageProperty.getValue()).isEqualTo(33); - - - Person otherPerson = new Person(); - otherPerson.setName("gisela"); - otherPerson.setAge(23); - - personWrapper.set(otherPerson); - personWrapper.reload(); - - assertThat(nameProperty.getValue()).isEqualTo("gisela"); - assertThat(ageProperty.getValue()).isEqualTo(23); - - nameProperty.setValue("georg"); - ageProperty.setValue(24); - - personWrapper.commit(); - - // old person has still the old values - assertThat(person.getName()).isEqualTo("hugo"); - assertThat(person.getAge()).isEqualTo(33); - - // new person has the new values - assertThat(otherPerson.getName()).isEqualTo("georg"); - assertThat(otherPerson.getAge()).isEqualTo(24); - - } - - - @Test - public void testWithJavaFXPropertiesField() { - PersonFX person = new PersonFX(); - person.setName("horst"); - person.setAge(32); - - ModelWrapper personWrapper = new ModelWrapper<>(person); - - - final Property nameProperty = personWrapper.field(PersonFX::nameProperty); - final Property ageProperty = personWrapper.field(PersonFX::ageProperty); - - assertThat(nameProperty.getValue()).isEqualTo("horst"); - assertThat(ageProperty.getValue()).isEqualTo(32); - - - nameProperty.setValue("hugo"); - ageProperty.setValue(33); - - // still the old values - assertThat(person.getName()).isEqualTo("horst"); - assertThat(person.getAge()).isEqualTo(32); - - - personWrapper.commit(); - - // now the new values are reflected in the wrapped person - assertThat(person.getName()).isEqualTo("hugo"); - assertThat(person.getAge()).isEqualTo(33); - - - - nameProperty.setValue("luise"); - ageProperty.setValue(15); - - personWrapper.reset(); - - assertThat(nameProperty.getValue()).isEqualTo(null); - assertThat(ageProperty.getValue()).isEqualTo(null); - - // the wrapped object has still the values from the last commit. - assertThat(person.getName()).isEqualTo("hugo"); - assertThat(person.getAge()).isEqualTo(33); - - - personWrapper.reload(); - // now the properties have the values from the wrapped object - assertThat(nameProperty.getValue()).isEqualTo("hugo"); - assertThat(ageProperty.getValue()).isEqualTo(33); - - - PersonFX otherPerson = new PersonFX(); - otherPerson.setName("gisela"); - otherPerson.setAge(23); - - personWrapper.set(otherPerson); - personWrapper.reload(); - - assertThat(nameProperty.getValue()).isEqualTo("gisela"); - assertThat(ageProperty.getValue()).isEqualTo(23); - - nameProperty.setValue("georg"); - ageProperty.setValue(24); - - personWrapper.commit(); - - // old person has still the old values - assertThat(person.getName()).isEqualTo("hugo"); - assertThat(person.getAge()).isEqualTo(33); - - // new person has the new values - assertThat(otherPerson.getName()).isEqualTo("georg"); - assertThat(otherPerson.getAge()).isEqualTo(24); - } - - @Test - public void testIdentifiedFields(){ - Person person = new Person(); - person.setName("horst"); - person.setAge(32); - - ModelWrapper personWrapper = new ModelWrapper<>(); - - final Property nameProperty = personWrapper.field("name", Person::getName, Person::setName); - final Property ageProperty = personWrapper.field("age", Person::getAge, Person::setAge); - - - final Property nameProperty2 = personWrapper.field("name", Person::getName, Person::setName); - final Property ageProperty2 = personWrapper.field("age", Person::getAge, Person::setAge); - - - assertThat(nameProperty).isSameAs(nameProperty2); - assertThat(ageProperty).isSameAs(ageProperty2); - } - + + + @Test + public void testWithGetterAndSetter() { + Person person = new Person(); + person.setName("horst"); + person.setAge(32); + + ModelWrapper personWrapper = new ModelWrapper<>(person); + + final StringProperty nameProperty = personWrapper.field(Person::getName, Person::setName); + final IntegerProperty ageProperty = personWrapper.field(Person::getAge, Person::setAge); + + assertThat(nameProperty.getValue()).isEqualTo("horst"); + assertThat(ageProperty.getValue()).isEqualTo(32); + + + nameProperty.setValue("hugo"); + ageProperty.setValue(33); + + // still the old values + assertThat(person.getName()).isEqualTo("horst"); + assertThat(person.getAge()).isEqualTo(32); + + + personWrapper.commit(); + + // now the new values are reflected in the wrapped person + assertThat(person.getName()).isEqualTo("hugo"); + assertThat(person.getAge()).isEqualTo(33); + + + + nameProperty.setValue("luise"); + ageProperty.setValue(15); + + personWrapper.reset(); + + assertThat(nameProperty.getValue()).isEqualTo(null); + assertThat(ageProperty.getValue()).isEqualTo(0); + + // the wrapped object has still the values from the last commit. + assertThat(person.getName()).isEqualTo("hugo"); + assertThat(person.getAge()).isEqualTo(33); + + + personWrapper.reload(); + // now the properties have the values from the wrapped object + assertThat(nameProperty.getValue()).isEqualTo("hugo"); + assertThat(ageProperty.getValue()).isEqualTo(33); + + + Person otherPerson = new Person(); + otherPerson.setName("gisela"); + otherPerson.setAge(23); + + personWrapper.set(otherPerson); + personWrapper.reload(); + + assertThat(nameProperty.getValue()).isEqualTo("gisela"); + assertThat(ageProperty.getValue()).isEqualTo(23); + + nameProperty.setValue("georg"); + ageProperty.setValue(24); + + personWrapper.commit(); + + // old person has still the old values + assertThat(person.getName()).isEqualTo("hugo"); + assertThat(person.getAge()).isEqualTo(33); + + // new person has the new values + assertThat(otherPerson.getName()).isEqualTo("georg"); + assertThat(otherPerson.getAge()).isEqualTo(24); + + } + + + @Test + public void testWithJavaFXPropertiesField() { + PersonFX person = new PersonFX(); + person.setName("horst"); + person.setAge(32); + + ModelWrapper personWrapper = new ModelWrapper<>(person); + + + final StringProperty nameProperty = personWrapper.field(PersonFX::nameProperty); + final IntegerProperty ageProperty = personWrapper.field(PersonFX::ageProperty); + + assertThat(nameProperty.getValue()).isEqualTo("horst"); + assertThat(ageProperty.getValue()).isEqualTo(32); + + + nameProperty.setValue("hugo"); + ageProperty.setValue(33); + + // still the old values + assertThat(person.getName()).isEqualTo("horst"); + assertThat(person.getAge()).isEqualTo(32); + + + personWrapper.commit(); + + // now the new values are reflected in the wrapped person + assertThat(person.getName()).isEqualTo("hugo"); + assertThat(person.getAge()).isEqualTo(33); + + + + nameProperty.setValue("luise"); + ageProperty.setValue(15); + + personWrapper.reset(); + + assertThat(nameProperty.getValue()).isEqualTo(null); + assertThat(ageProperty.getValue()).isEqualTo(0); + + // the wrapped object has still the values from the last commit. + assertThat(person.getName()).isEqualTo("hugo"); + assertThat(person.getAge()).isEqualTo(33); + + + personWrapper.reload(); + // now the properties have the values from the wrapped object + assertThat(nameProperty.getValue()).isEqualTo("hugo"); + assertThat(ageProperty.getValue()).isEqualTo(33); + + + PersonFX otherPerson = new PersonFX(); + otherPerson.setName("gisela"); + otherPerson.setAge(23); + + personWrapper.set(otherPerson); + personWrapper.reload(); + + assertThat(nameProperty.getValue()).isEqualTo("gisela"); + assertThat(ageProperty.getValue()).isEqualTo(23); + + nameProperty.setValue("georg"); + ageProperty.setValue(24); + + personWrapper.commit(); + + // old person has still the old values + assertThat(person.getName()).isEqualTo("hugo"); + assertThat(person.getAge()).isEqualTo(33); + + // new person has the new values + assertThat(otherPerson.getName()).isEqualTo("georg"); + assertThat(otherPerson.getAge()).isEqualTo(24); + } + + @Test + public void testIdentifiedFields() { + Person person = new Person(); + person.setName("horst"); + person.setAge(32); + + 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 StringProperty nameProperty2 = personWrapper.field("name", Person::getName, Person::setName); + final IntegerProperty ageProperty2 = personWrapper.field("age", Person::getAge, Person::setAge); + + + assertThat(nameProperty).isSameAs(nameProperty2); + assertThat(ageProperty).isSameAs(ageProperty2); + } + } 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 89d8cda80..3dc4d6297 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,24 +1,24 @@ package de.saxsys.mvvmfx.utils.mapping; public class Person { - - private String name; - - private int age; - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } + + private String name; + + private int age; + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } 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 06f5366cb..445c0a512 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 @@ -6,32 +6,32 @@ import javafx.beans.property.StringProperty; public class PersonFX { - - private StringProperty name = new SimpleStringProperty(); - - private IntegerProperty age = new SimpleIntegerProperty(); - - public String getName() { - return name.get(); - } - - public StringProperty nameProperty() { - return name; - } - - public void setName(String name) { - this.name.set(name); - } - - public int getAge() { - return age.get(); - } - - public IntegerProperty ageProperty() { - return age; - } - - public void setAge(int age) { - this.age.set(age); - } + + private StringProperty name = new SimpleStringProperty(); + + private IntegerProperty age = new SimpleIntegerProperty(); + + public String getName() { + return name.get(); + } + + public StringProperty nameProperty() { + return name; + } + + public void setName(String name) { + this.name.set(name); + } + + public int getAge() { + return age.get(); + } + + public IntegerProperty ageProperty() { + return age; + } + + public void setAge(int age) { + this.age.set(age); + } } 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 new file mode 100644 index 000000000..f55777e09 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ReturnTypeTest.java @@ -0,0 +1,132 @@ +package de.saxsys.mvvmfx.utils.mapping; + +import javafx.beans.property.*; +import org.junit.Before; +import org.junit.Test; + +/** + * This test is used to check the return values when fields are mapped. See Issue 211 https://github.com/sialcasa/mvvmFX/issues/211 + */ +public class ReturnTypeTest { + + private ModelWrapper wrapper; + + private ExampleModel model; + + @Before + public void setup() { + wrapper = new ModelWrapper<>(); + model = new ExampleModel(); + } + + + @Test + public void integerProperty() { + final IntegerProperty beanField = wrapper.field(ExampleModel::getInteger, ExampleModel::setInteger); + final IntegerProperty fxField = wrapper.field(ExampleModel::integerProperty); + final IntegerProperty beanFieldDefault = wrapper.field(ExampleModel::getInteger, ExampleModel::setInteger, 5); + final IntegerProperty fxFieldDefault = wrapper.field(ExampleModel::integerProperty, 5); + + final IntegerProperty idBeanField = wrapper.field("int1", ExampleModel::getInteger, ExampleModel::setInteger); + final IntegerProperty idFxField = wrapper.field("int2", ExampleModel::integerProperty); + final IntegerProperty idBeanFieldDefault = wrapper.field("int3", ExampleModel::getInteger, + ExampleModel::setInteger, 5); + final IntegerProperty idFxFieldDefault = wrapper.field("int4", ExampleModel::integerProperty, 5); + + + } + + @Test + public void doubleProperty() { + final DoubleProperty beanField = wrapper.field(ExampleModel::getDouble, ExampleModel::setDouble); + final DoubleProperty fxField = wrapper.field(ExampleModel::doubleProperty); + final DoubleProperty beanFieldDefault = wrapper.field(ExampleModel::getDouble, ExampleModel::setDouble, 5.1); + final DoubleProperty fxFieldDefault = wrapper.field(ExampleModel::doubleProperty, 5.1); + + final DoubleProperty idBeanField = wrapper.field("double1", ExampleModel::getDouble, ExampleModel::setDouble); + final DoubleProperty idFxField = wrapper.field("double2", ExampleModel::doubleProperty); + final DoubleProperty idBeanFieldDefault = wrapper.field("double3", ExampleModel::getDouble, + ExampleModel::setDouble, 5.1); + final DoubleProperty idFxFieldDefault = wrapper.field("double4", ExampleModel::doubleProperty, 5.1); + } + + + @Test + public void longProperty() { + final LongProperty beanField = wrapper.field(ExampleModel::getLong, ExampleModel::setLong); + final LongProperty fxField = wrapper.field(ExampleModel::longProperty); + final LongProperty beanFieldDefault = wrapper.field(ExampleModel::getLong, ExampleModel::setLong, 5l); + final LongProperty fxFieldDefault = wrapper.field(ExampleModel::longProperty, 5l); + + final LongProperty idBeanField = wrapper.field("long1", ExampleModel::getLong, ExampleModel::setLong); + final LongProperty idFxField = wrapper.field("long2", ExampleModel::longProperty); + final LongProperty idBeanFieldDefault = wrapper + .field("long3", ExampleModel::getLong, ExampleModel::setLong, 5l); + final LongProperty idFxFieldDefault = wrapper.field("long4", ExampleModel::longProperty, 5l); + } + + + @Test + public void floatProperty() { + final FloatProperty beanField = wrapper.field(ExampleModel::getFloat, ExampleModel::setFloat); + final FloatProperty fxField = wrapper.field(ExampleModel::floatProperty); + final FloatProperty beanFieldDefault = wrapper.field(ExampleModel::getFloat, ExampleModel::setFloat, 5.1f); + final FloatProperty fxFieldDefault = wrapper.field(ExampleModel::floatProperty, 5.1f); + + final FloatProperty idBeanField = wrapper.field("float1", ExampleModel::getFloat, ExampleModel::setFloat); + final FloatProperty idFxField = wrapper.field("float2", ExampleModel::floatProperty); + final FloatProperty idBeanFieldDefault = wrapper.field("float3", ExampleModel::getFloat, + ExampleModel::setFloat, 5.1f); + final FloatProperty idFxFieldDefault = wrapper.field("float4", ExampleModel::floatProperty, 5.1f); + } + + + @Test + public void booleanProperty() { + final BooleanProperty beanField = wrapper.field(ExampleModel::getBoolean, ExampleModel::setBoolean); + final BooleanProperty fxField = wrapper.field(ExampleModel::booleanProperty); + final BooleanProperty beanFieldDefault = wrapper + .field(ExampleModel::getBoolean, ExampleModel::setBoolean, true); + final BooleanProperty fxFieldDefault = wrapper.field(ExampleModel::booleanProperty, true); + + final BooleanProperty idBeanField = wrapper.field("bool1", ExampleModel::getBoolean, ExampleModel::setBoolean); + final BooleanProperty idFxField = wrapper.field("bool2", ExampleModel::booleanProperty); + final BooleanProperty idBeanFieldDefault = wrapper.field("bool3", ExampleModel::getBoolean, + ExampleModel::setBoolean, true); + final BooleanProperty idFxFieldDefault = wrapper.field("bool4", ExampleModel::booleanProperty, true); + } + + + @Test + public void stringProperty() { + final StringProperty beanField = wrapper.field(ExampleModel::getString, ExampleModel::setString); + final StringProperty fxField = wrapper.field(ExampleModel::stringProperty); + final StringProperty beanFieldDefault = wrapper.field(ExampleModel::getString, ExampleModel::setString, "test"); + final StringProperty fxFieldDefault = wrapper.field(ExampleModel::stringProperty, "test"); + + final StringProperty idBeanField = wrapper.field("string1", ExampleModel::getString, ExampleModel::setString); + final StringProperty idFxField = wrapper.field("string2", ExampleModel::stringProperty); + final StringProperty idBeanFieldDefault = wrapper.field("string3", ExampleModel::getString, + ExampleModel::setString, "test"); + final StringProperty idFxFieldDefault = wrapper.field("string4", ExampleModel::stringProperty, "test"); + } + + @Test + public void objectProperty() { + final ObjectProperty beanField = wrapper.field(ExampleModel::getObject, ExampleModel::setObject); + final ObjectProperty fxField = wrapper.field(ExampleModel::objectProperty); + final ObjectProperty beanFieldDefault = wrapper.field(ExampleModel::getObject, ExampleModel::setObject, + new Person()); + final ObjectProperty fxFieldDefault = wrapper.field(ExampleModel::objectProperty, new Person()); + + final ObjectProperty idBeanField = wrapper.field("obj1", ExampleModel::getObject, + ExampleModel::setObject); + final ObjectProperty idFxField = wrapper.field("obj2", ExampleModel::objectProperty); + final ObjectProperty idBeanFieldDefault = wrapper.field("obj3", ExampleModel::getObject, + ExampleModel::setObject, new Person()); + final ObjectProperty idFxFieldDefault = wrapper.field("obj4", ExampleModel::objectProperty, + new Person()); + } + +} 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 3a352632f..c0c72cc63 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 @@ -17,6 +17,7 @@ package de.saxsys.mvvmfx.utils.notifications; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.mockito.Mockito; @@ -60,8 +61,11 @@ public void addAndRemoveObserverToDefaultNotificationCenterAndPostNotification() defaultCenter.subscribe(TEST_NOTIFICATION, observer2); defaultCenter.subscribe(TEST_NOTIFICATION, observer3); defaultCenter.unsubscribe(observer1); + defaultCenter.unsubscribe(observer2); defaultCenter.publish(TEST_NOTIFICATION); Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION); + Mockito.verify(observer2, Mockito.never()).receivedNotification(TEST_NOTIFICATION); + Mockito.verify(observer3).receivedNotification(TEST_NOTIFICATION); } @Test @@ -83,6 +87,27 @@ public void addAndRemoveObserverForNameToDefaultNotificationCenterAndPostNotific defaultCenter.publish(TEST_NOTIFICATION); Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION); } + + @Test + public void subscribeSameObserverMultipleTimes() { + defaultCenter.subscribe(TEST_NOTIFICATION, observer1); + defaultCenter.subscribe(TEST_NOTIFICATION, observer1); + + defaultCenter.publish(TEST_NOTIFICATION); + Mockito.verify(observer1, Mockito.times(2)).receivedNotification(TEST_NOTIFICATION); + } + + @Test + public void unsubscribeObserverThatWasSubscribedMultipleTimes() { + defaultCenter.subscribe(TEST_NOTIFICATION, observer1); + defaultCenter.subscribe(TEST_NOTIFICATION, observer1); + defaultCenter.subscribe(TEST_NOTIFICATION, observer1); + + defaultCenter.unsubscribe(observer1); + + defaultCenter.publish(TEST_NOTIFICATION); + Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION); + } 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 new file mode 100644 index 000000000..8ef1bafe9 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ViewModelTest.java @@ -0,0 +1,93 @@ +package de.saxsys.mvvmfx.utils.notifications; + +import de.saxsys.mvvmfx.MvvmFX; +import de.saxsys.mvvmfx.ViewModel; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import de.saxsys.mvvmfx.utils.notifications.NotificationObserver; + +public class ViewModelTest { + + private static final String TEST_NOTIFICATION = "test_notification"; + private static final Object[] OBJECT_ARRAY_FOR_NOTIFICATION = new String[] { "test" }; + + ViewModel viewModel; + DummyNotificationObserver observer1; + DummyNotificationObserver observer2; + DummyNotificationObserver observer3; + + @Before + public void init() { + observer1 = Mockito.mock(DummyNotificationObserver.class); + observer2 = Mockito.mock(DummyNotificationObserver.class); + observer3 = Mockito.mock(DummyNotificationObserver.class); + viewModel = new ViewModel() { + }; + } + + + @Test + public void observerFromOutsideDoesNotReceiveNotifications() { + MvvmFX.getNotificationCenter().subscribe(TEST_NOTIFICATION, observer1); + viewModel.publish(TEST_NOTIFICATION); + + Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION); + } + + @Test + public void addObserverAndPublish() throws Exception { + viewModel.subscribe(TEST_NOTIFICATION, observer1); + viewModel.publish(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); + Mockito.verify(observer1).receivedNotification(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); + } + + @Test + public void addAndRemoveObserverAndPublish() throws Exception { + viewModel.subscribe(TEST_NOTIFICATION, observer1); + viewModel.unsubscribe(observer1); + viewModel.publish(TEST_NOTIFICATION); + Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION); + + viewModel.subscribe(TEST_NOTIFICATION, observer1); + viewModel.unsubscribe(TEST_NOTIFICATION, observer1); + viewModel.publish(TEST_NOTIFICATION); + Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION); + } + + @Test + public void addMultipleObserverAndPublish() throws Exception { + viewModel.subscribe(TEST_NOTIFICATION, observer1); + viewModel.subscribe(TEST_NOTIFICATION, observer2); + viewModel.subscribe(TEST_NOTIFICATION, observer3); + viewModel.publish(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); + Mockito.verify(observer1).receivedNotification(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); + Mockito.verify(observer2).receivedNotification(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); + Mockito.verify(observer3).receivedNotification(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); + } + + + @Test + public void addMultipleObserverAndRemoveOneAndPublish() throws Exception { + viewModel.subscribe(TEST_NOTIFICATION, observer1); + viewModel.subscribe(TEST_NOTIFICATION, observer2); + viewModel.subscribe(TEST_NOTIFICATION, observer3); + viewModel.unsubscribe(observer1); + viewModel.publish(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); + + Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION, + OBJECT_ARRAY_FOR_NOTIFICATION); + Mockito.verify(observer2).receivedNotification(TEST_NOTIFICATION, + OBJECT_ARRAY_FOR_NOTIFICATION); + Mockito.verify(observer3).receivedNotification(TEST_NOTIFICATION, + OBJECT_ARRAY_FOR_NOTIFICATION); + } + + private class DummyNotificationObserver implements NotificationObserver { + @Override + public void receivedNotification(String key, Object... payload) { + + } + } +} diff --git a/pom.xml b/pom.xml index 582fbb308..3968249e7 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ de.saxsys mvvmfx-parent pom - 1.1.0 + 1.2.0 mvvmFX parent Application Framework for MVVM with JavaFX. http://www.saxsys.de