null
if
* no country is selected.
*/
+ @Override
public void setCountry(Country country) {
if (country == null) {
subdivisionLabel.set(null);
@@ -169,11 +179,17 @@ void loadSubdivisions() {
Listsuper.stop()
when you
* override this method.
- *
+ *
* @throws Exception
*/
@Override
public final void stop() throws Exception {
stopMvvmfx();
-
+
injectionTarget.preDestroy(this);
injectionTarget.dispose(this);
-
+
ctx.release();
-
- weld.shutdown();
+
+ container.close();
}
}
diff --git a/mvvmfx-cdi/src/test/java/de/saxsys/mvvmfx/cdi/it/IntegrationTest.java b/mvvmfx-cdi/src/test/java/de/saxsys/mvvmfx/cdi/it/IntegrationTest.java
index 205a360ce..b6d3395e5 100644
--- a/mvvmfx-cdi/src/test/java/de/saxsys/mvvmfx/cdi/it/IntegrationTest.java
+++ b/mvvmfx-cdi/src/test/java/de/saxsys/mvvmfx/cdi/it/IntegrationTest.java
@@ -3,12 +3,12 @@
import static org.assertj.core.api.Assertions.assertThat;
import javafx.application.Application;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
public class IntegrationTest {
- @Before
+ @BeforeEach
public void setup() {
MyApp.wasPostConstructCalled = false;
MyApp.wasPreDestroyCalled = false;
diff --git a/mvvmfx-easydi/pom.xml b/mvvmfx-easydi/pom.xml
index b8315a1bf..8c028eece 100644
--- a/mvvmfx-easydi/pom.xml
+++ b/mvvmfx-easydi/pom.xml
@@ -5,7 +5,7 @@
@@ -33,7 +33,7 @@
*/
public class CompositeValidator implements Validator {
- CompositeValidationStatus status = new CompositeValidationStatus();
+ private CompositeValidationStatus status = new CompositeValidationStatus();
private ListProperty
+ * package example.view; + * + *{@literal @}FxmlPath("/fxml/CustomPathView.fxml") + * public class CustomView implements {@link FxmlView} { + * + * ... + * + * } + *+ * + * @author rafael.guillen + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface FxmlPath { + + /** + * Custom fxml file path, empty by default + * @return path to the fxml file + */ + String value() default ""; +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/Initialize.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/Initialize.java new file mode 100644 index 000000000..681cbf263 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/Initialize.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright 2017 Gleb Koval + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * This annotation is used to mark a method in a ViewModel to be called after all mvvmFX injections are finished. + * It's possible to use this annotation on multiple methods in the ViewModel and all these methods will be invoked. + * However, you may not depend on any invocation order.
public initialize()
will be invoked after initialization is finished.+ * public class SomeViewModel implements {@link ViewModel} { + * + * // mvvmFx injections + * {@literal @}{@link InjectScope} + * private SomeScope someScope; + * ... + * + * {@literal @}Initialize + * private void init() { + * someScope.subscribe(...); + * ... + * } + * } + *+ * + * @author Gleb Koval, Manuel Mauky + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Initialize { +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/InjectScope.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/InjectScope.java index 5bec70b34..4f77a9d12 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/InjectScope.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/InjectScope.java @@ -6,18 +6,11 @@ import java.lang.annotation.Target; /** + * This annotation is used to inject a {@link Scope} object into a {@link ViewModel}. * * @author alexander.casall - * */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface InjectScope { - - /** - * the id of the scope. - * Default is "" which means that the scope is global. - */ - String value() default ""; - } \ No newline at end of file diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/MvvmFX.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/MvvmFX.java index c24ac6196..8f710168e 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/MvvmFX.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/MvvmFX.java @@ -15,7 +15,9 @@ ******************************************************************************/ package de.saxsys.mvvmfx; +import de.saxsys.mvvmfx.internal.viewloader.GlobalBuilderFactory; import de.saxsys.mvvmfx.internal.viewloader.ResourceBundleManager; +import javafx.util.BuilderFactory; import javafx.util.Callback; import de.saxsys.mvvmfx.utils.notifications.NotificationCenter; @@ -65,4 +67,29 @@ public static void setCustomDependencyInjector(final Callback
null
+ * @return the loaded ViewTuple.
*/
public null
+ * @return the loaded ViewTuple.
*/
public null
is accepted.
+ * @return the merged resourceBundle.
+ */
+ public ResourceBundle mergeListWithGlobal(Listpublic void initialize()
it will be invoked. If no such
- * method is available nothing happens.
- *
+ * If a ViewModel has a method annotated with {@link Initialize}
+ * or method with the signature public void initialize()
+ * it will be invoked. If no such method is available nothing happens.
+ *
* @param viewModel
* the viewModel that's initialize method (if available) will be
* invoked.
@@ -386,30 +389,46 @@ public static - * + * * These requirements are quite common but there is a lot of code needed to copy between the model and the viewModel. * Additionally we have a tight coupling because every time the structure of the model changes (for example a field is * removed) we have several places in the viewModel that need to be adjusted. @@ -111,68 +112,68 @@ *
* The model class: *
- * + * *
* public class Person { * private String name; * private String familyName; * private int age; - * + * * public String getName() { * return name; * } - * + * * public void setName(String name) { * this.name = name; * } - * + * * public String getFamilyName() { * return familyName; * } - * + * * public void setFamilyName(String familyName) { * this.familyName = familyName; * } - * + * * public int getAge() { * return age; * } - * + * * public void setAge(int age) { * this.age = age; * } * } *- * + * * Without {@link ModelWrapper}: *
- * + * *
* public class PersonViewModel implements ViewModel { - * + * * private StringProperty name = new SimpleStringProperty(); * private StringProperty familyName = new SimpleStringProperty(); * private IntegerProperty age = new SimpleIntegerProperty(); - * + * * private Person person; - * + * * public void init(Person person) { * this.person = person; * reloadFromModel(); * } - * + * * public void reset() { * this.name.setValue(""); * this.familyName.setValue(""); * this.age.setValue(0); * } - * + * * public void reloadFromModel() { * this.name.setValue(person.getName()); * this.familyName.setValue(person.getFamilyName()); * this.age.setValue(person.getAge()); * } - * + * * public void save() { * if (someValidation() && person != null) { * person.setName(name.getValue()); @@ -180,61 +181,61 @@ * person.setAge(age.getValue()); * } * } - * + * * public StringProperty nameProperty() { * return name; * } - * + * * public StringProperty familyNameProperty() { * return familyName; * } - * + * * public IntegerProperty ageProperty() { * return age; * } * } *- * + * * With {@link ModelWrapper}: *
- * + * *
* public class PersonViewModel implements ViewModel { * private ModelWrapper{@code- * + * * In the first example without the {@link ModelWrapper} we have several lines of code that are specific for each field * of the model. If we would add a new field to the model (for example "email") then we would have to update several * pieces of code in the ViewModel. @@ -242,310 +243,35 @@ * On the other hand in the example with the {@link ModelWrapper} there is only the definition of the Property accessors * in the bottom of the class that is specific to the fields of the Model. For each field we have only one place in the * ViewModel that would need an update when the structure of the model changes. - * - * - * + * + * + * * @param} wrapper = new ModelWrapper{@code<>}(); - * + * * public void init(Person person) { * wrapper.set(person); * wrapper.reload(); * } - * + * * public void reset() { * wrapper.reset(); * } - * + * * public void reloadFromModel(){ * wrapper.reload(); * } - * + * * public void save() { * if (someValidation()) { * wrapper.commit(); * } * } - * + * * public StringProperty nameProperty(){ * return wrapper.field("name", Person::getName, Person::setName, ""); * } - * + * * public StringProperty familyNameProperty(){ * return wrapper.field("familyName", Person::getFamilyName, Person::setFamilyName, ""); * } - * + * * public IntegerProperty ageProperty() { * return wrapper.field("age", Person::getAge, Person::setAge, 0); * } * } *
false
if both the wrapped model object and the property field have the same value,
- * otherwise true
- */
- boolean isDifferent(M wrappedObject);
- }
-
- /**
- * An implementation of {@link PropertyField} that is used when the fields of the model class are JavaFX Properties
- * too.
- *
- * @param true
+ * to indicate that we are currently executing a commit.
*/
- private class BeanListPropertyFieldnull
.
*/
@@ -623,7 +355,8 @@ public ObjectProperty* ModelWrapper{@code- * + * * * If no model instance is set to be wrapped by the ModelWrapper, nothing will happen when this method is invoked. * Instead the old default values will still be available. - * + * */ public void useCurrentValuesAsDefaults() { - if(model.get() != null) { + M wrappedModelInstance = model.get(); + if(wrappedModelInstance != null) { for (final PropertyField, M, ?> field : fields) { - field.updateDefault(model.get()); + field.updateDefault(wrappedModelInstance); + } + + for (final ImmutablePropertyField, M, ?> field : immutableFields) { + field.updateDefault(wrappedModelInstance); } } } @@ -687,14 +425,33 @@ public void useCurrentValuesAsDefaults() { */ public void commit() { if (model.get() != null) { + + inCommitPhase = true; + fields.forEach(field -> field.commit(model.get())); - + + if(! immutableFields.isEmpty()) { + + M tmp = model.get(); + + for (ImmutablePropertyField, M, ?> immutableField : immutableFields) { + tmp = immutableField.commitImmutable(tmp); + } + + model.set(tmp); + + } + + inCommitPhase = false; + + + dirtyFlag.set(false); - + calculateDifferenceFlag(); } } - + /** * Take the current values from the wrapped model element and put them in the corresponding property fields. *} wrapper = new ModelWrapper{@code<>}(); - * + * * wrapper.field(Person::getName, Person::setName, "oldDefault"); - * + * * Person p = new Person(); * wrapper.set(p); - * - * + * + * * p.setName("Luise"); - * + * * wrapper.useCurrentValuesAsDefaults(); // now "Luise" is the default value for the name field. - * - * + * + * * name.set("Hugo"); * wrapper.commit(); - * + * * name.get(); // Hugo * p.getName(); // Hugo - * - * + * + * * wrapper.reset(); // reset to the new defaults * name.get(); // Luise - * + * * wrapper.commit(); // put values from properties to the wrapped model object * p.getName(); // Luise - * - * + * + * *
@@ -704,9 +461,12 @@ public void commit() {
* defined property fields.
*/
public void reload() {
- if (model.get() != null) {
- fields.forEach(field -> field.reload(model.get()));
-
+ M wrappedModelInstance = model.get();
+ if (wrappedModelInstance != null) {
+ fields.forEach(field -> field.reload(wrappedModelInstance));
+
+ immutableFields.forEach(field -> field.reload(wrappedModelInstance));
+
dirtyFlag.set(false);
calculateDifferenceFlag();
}
@@ -732,23 +492,129 @@ private void propertyWasChanged() {
dirtyFlag.set(true);
calculateDifferenceFlag();
}
-
+
private void calculateDifferenceFlag() {
- if (model.get() != null) {
+ M wrappedModelInstance = model.get();
+
+ if (wrappedModelInstance != null) {
for (final PropertyField, M, ?> field : fields) {
- if (field.isDifferent(model.get())) {
- diffFlag.set(true);
- return;
- }
- }
- diffFlag.set(false);
+ if (field.isDifferent(wrappedModelInstance)) {
+ diffFlag.set(true);
+ return;
+ }
+ }
+
+ for (final ImmutablePropertyField, M, ?> field : immutableFields) {
+ if(field.isDifferent(wrappedModelInstance)) {
+ diffFlag.set(true);
+ return;
+ }
+ }
+
+ diffFlag.set(false);
+ }
+ }
+
+
+ private
+ * Note the difference to {@link #dirtyProperty()}: This property will be
+ * Note the difference to {@link #differentProperty()}: This property will turn to
*
- * @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 defaultValue
- * the default value that is used when {@link #reset()} is invoked.
- *
- * @return The wrapped property instance.
- */
- public StringProperty field(StringGetter
+ *
+ * true
if the data of the
+ * wrapped model is different to the properties of this wrapper. If you change the data back to the initial state so
+ * that the data is equal again, this property will change back to false
while the
+ * {@link #dirtyProperty()} will still be true
.
+ *
+ * Simply speaking: This property indicates whether there is a difference in data between the model and the wrapper.
+ * The {@link #dirtyProperty()} indicates whether there was a change done.
+ *
+ *
+ * Note: Only those changes are observed that are done through the wrapped property fields of this wrapper. If you
+ * change the data of the model instance directly, this property won't turn to true
.
+ *
+ *
+ * @return a read-only property indicating a difference between model and wrapper.
+ */
+ public ReadOnlyBooleanProperty differentProperty() {
+ return diffFlag.getReadOnlyProperty();
+ }
+
+ /**
+ * See {@link #differentProperty()}.
+ */
+ public boolean isDifferent() {
+ return diffFlag.get();
+ }
+
+ /**
+ * This boolean flag indicates whether there was a change to at least one wrapped property.
+ * true
when the value
+ * of one of the wrapped properties is changed. It will only change back to false
when either the
+ * {@link #commit()} or {@link #reload()} method is called. This property will stay true
even if
+ * afterwards another change is done so that the data is equal again. In this case the {@link #differentProperty()}
+ * will switch back to false
.
+ *
* ModelWrapper{@code
+ * ModelWrapper{@code
+ *
+ *
+ * @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 immutableSetter
+ * a function that returns a clone of this the given model element that has the given value set. Typically you will use a method
+ * reference to the immutable setter method of the model element.
+ *
+ * @return The wrapped property instance.
+ */
+ public StringProperty immutableField(StringGetter
* ModelWrapper{@code