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..ea357794c --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/Initialize.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * 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 the method in a ViewModel to be called after all mvvmFx injections. + * If no method is marked, public initialize() method will be used, if present.
+ * Example:
+ *
+ * + *
+ * public class SomeViewModel implements {@link ViewModel} {
+ *
+ *         // mvvmFx injections
+ *        {@literal @}{@link InjectScope}
+ *         private SomeScope someScope;
+ *         ...
+ *
+ *        {@literal @}Initialize
+ *         private void init() {
+ *             someScope.subscribe(...);
+ *             ...
+ *         }
+ * }
+ * 
+ * + * @author Gleb Koval + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Initialize { +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ReflectionUtils.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ReflectionUtils.java index c9b681bbb..71939838e 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ReflectionUtils.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ReflectionUtils.java @@ -16,6 +16,7 @@ package de.saxsys.mvvmfx.internal.viewloader; import java.lang.annotation.Annotation; +import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.security.AccessController; import java.security.PrivilegedAction; @@ -89,13 +90,13 @@ public static List getFieldsFromClassHierarchy(Class type) { /** - * Helper method to execute a callback on a given field. This method encapsulates the error handling logic and the - * handling of accessibility of the field. + * Helper method to execute a callback on a given member. This method encapsulates the error handling logic and the + * handling of accessibility of the member. * - * After the callback is executed the accessibility of the field will be reset to the originally state. + * After the callback is executed the accessibility of the member will be reset to the originally state. * - * @param field - * the field that is made accessible to run the callback + * @param member + * the member that is made accessible to run the callback * @param callable * the callback that will be executed. * @param errorMessage @@ -106,19 +107,19 @@ public static List getFieldsFromClassHierarchy(Class type) { * @throws IllegalStateException * when something went wrong. */ - public static T accessField(final Field field, final Callable callable, String errorMessage) { + public static T accessMember(final AccessibleObject member, final Callable callable, String errorMessage) { if (callable == null) { return null; } return AccessController.doPrivileged((PrivilegedAction) () -> { - boolean wasAccessible = field.isAccessible(); + boolean wasAccessible = member.isAccessible(); try { - field.setAccessible(true); + member.setAccessible(true); return callable.call(); } catch (Exception exception) { throw new IllegalStateException(errorMessage, exception); } finally { - field.setAccessible(wasAccessible); + member.setAccessible(wasAccessible); } }); } @@ -136,21 +137,21 @@ public static T accessField(final Field field, final Callable callable, S * the new value that the field should be set to. */ public static void setField(final Field field, Object target, Object value) { - accessField(field, () -> field.set(target, value), + accessMember(field, () -> field.set(target, value), "Cannot set the field [" + field.getName() + "] of instance [" + target + "] to value [" + value + "]"); } /** - * Helper method to execute a callback on a given field. This method encapsulates the error handling logic and the - * handling of accessibility of the field. The difference to - * {@link ReflectionUtils#accessField(Field, Callable, String)} is that this method takes a callback that doesn't + * Helper method to execute a callback on a given member. This method encapsulates the error handling logic and the + * handling of accessibility of the member. The difference to + * {@link ReflectionUtils#accessMember(AccessibleObject, Callable, String)} is that this method takes a callback that doesn't * return anything but only creates a sideeffect. * - * After the callback is executed the accessibility of the field will be reset to the originally state. + * After the callback is executed the accessibility of the member will be reset to the originally state. * - * @param field - * the field that is made accessible to run the callback + * @param member + * the member that is made accessible to run the callback * @param sideEffect * the callback that will be executed. * @param errorMessage @@ -159,19 +160,19 @@ public static void setField(final Field field, Object target, Object value) { * @throws IllegalStateException * when something went wrong. */ - public static void accessField(final Field field, final SideEffect sideEffect, String errorMessage) { + public static void accessMember(final AccessibleObject member, final SideEffect sideEffect, String errorMessage) { if (sideEffect == null) { return; } - AccessController.doPrivileged((PrivilegedAction) () -> { - boolean wasAccessible = field.isAccessible(); + AccessController.doPrivileged((PrivilegedAction) () -> { + boolean wasAccessible = member.isAccessible(); try { - field.setAccessible(true); + member.setAccessible(true); sideEffect.call(); } catch (Exception exception) { throw new IllegalStateException(errorMessage, exception); } finally { - field.setAccessible(wasAccessible); + member.setAccessible(wasAccessible); } return null; }); diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtils.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtils.java index 30fe3cbc7..a9a6286eb 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtils.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtils.java @@ -16,6 +16,7 @@ package de.saxsys.mvvmfx.internal.viewloader; import de.saxsys.mvvmfx.Context; +import de.saxsys.mvvmfx.Initialize; import de.saxsys.mvvmfx.InjectContext; import de.saxsys.mvvmfx.InjectScope; import de.saxsys.mvvmfx.InjectViewModel; @@ -30,7 +31,6 @@ import javax.annotation.PostConstruct; import java.lang.annotation.Annotation; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.AccessController; import java.security.PrivilegedAction; @@ -166,7 +166,7 @@ public static , ViewModelType ext Optional fieldOptional = getViewModelField(view.getClass(), viewModelType); if (fieldOptional.isPresent()) { Field field = fieldOptional.get(); - return ReflectionUtils.accessField(field, () -> (ViewModelType) field.get(view), + return ReflectionUtils.accessMember(field, () -> (ViewModelType) field.get(view), "Can't get the viewModel of type <" + viewModelType + ">"); } else { return null; @@ -189,7 +189,7 @@ public static void injectViewModel(final View view, ViewModel viewModel) { final Optional fieldOptional = getViewModelField(view.getClass(), viewModel.getClass()); if (fieldOptional.isPresent()) { Field field = fieldOptional.get(); - ReflectionUtils.accessField(field, () -> { + ReflectionUtils.accessMember(field, () -> { Object existingViewModel = field.get(view); if (existingViewModel == null) { field.set(view, viewModel); @@ -258,7 +258,7 @@ public static , VM extends ViewModel> void createAn if (fieldOptional.isPresent()) { Field field = fieldOptional.get(); - ReflectionUtils.accessField(field, () -> { + ReflectionUtils.accessMember(field, () -> { Object existingViewModel = field.get(view); if (existingViewModel == null) { @@ -294,7 +294,7 @@ static void createAndInjectScopes(Object viewModel, ContextImpl context) { List scopeFields = getScopeFields(viewModel.getClass()); scopeFields.forEach(scopeField -> { - ReflectionUtils.accessField(scopeField, () -> injectScopeIntoField(scopeField, viewModel, context), + ReflectionUtils.accessMember(scopeField, () -> injectScopeIntoField(scopeField, viewModel, context), "Can't inject Scope into ViewModel <" + viewModel.getClass() + ">"); }); } @@ -305,7 +305,7 @@ public static void injectContext(View codeBehind, ContextImpl context) { if (contextField.isPresent()) { Field field = contextField.get(); - ReflectionUtils.accessField(field, () -> { + ReflectionUtils.accessMember(field, () -> { field.set(codeBehind, context); }, "Can't inject Context into the view <" + codeBehind + ">"); } @@ -372,10 +372,10 @@ public static , ViewModelType ext } /** - * If a ViewModel has a method with the signature - * public 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. @@ -387,7 +387,9 @@ public static void initializeViewModel(ViewMod return; } try { - final Method initMethod = viewModel.getClass().getMethod("initialize"); + Method annotatedMethod = getInitializeMethod(viewModel); + // find method annotated with @Initialize or use initialize() otherwise + final Method initMethod = annotatedMethod != null ? annotatedMethod : viewModel.getClass().getMethod("initialize"); // if there is a @PostConstruct annotation, throw an exception to prevent double injection if(initMethod.isAnnotationPresent(PostConstruct.class)) { throw new IllegalStateException(String.format("initialize method of ViewModel [%s] is annotated with @PostConstruct. " + @@ -397,19 +399,21 @@ public static void initializeViewModel(ViewMod "https://github.com/sialcasa/mvvmFX/wiki/Dependency-Injection#lifecycle-postconstruct", viewModel)); } - AccessController.doPrivileged((PrivilegedAction) () -> { - try { - return initMethod.invoke(viewModel); - } catch (InvocationTargetException | IllegalAccessException e) { - throw new IllegalStateException( - "mvvmFX wasn't able to call the initialize method of ViewModel [" + viewModel + "].", e); - } - }); + ReflectionUtils.accessMember(initMethod, () -> initMethod.invoke(viewModel), "mvvmFX wasn't able to call the initialize method of ViewModel [" + viewModel + "]."); } catch (NoSuchMethodException e) { // it's perfectly fine that a ViewModel has no initialize method. } } + private static Method getInitializeMethod(ViewModelType viewModel) { + for (Method method : viewModel.getClass().getDeclaredMethods()) { + if(method.isAnnotationPresent(Initialize.class)) { + return method; + } + } + return null; + } + /** * This method adds listeners for the {@link SceneLifecycle}. */ diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_FxmlView_Test.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_FxmlView_Test.java index 20f324120..aed33a79e 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_FxmlView_Test.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_FxmlView_Test.java @@ -470,4 +470,20 @@ public void testExistingViewModelWithoutInjectionInView() { assertThat(TestViewModel.wasInitialized).isFalse(); } + + /** + * Method annotated with {@link de.saxsys.mvvmfx.Initialize} annotation initializes the ViewModel + * */ + @Test + public void testViewModelIsInitializedWithAnnotatatedMethod() { + TestViewModelWithAnnotatedInitialize.wasInitialized = false; + + ViewTuple tuple + = FluentViewLoader.fxmlView(TestFxmlViewWithViewModelWithAnnotatedInitialize.class).load(); + + TestViewModelWithAnnotatedInitialize viewModel = tuple.getViewModel(); + + assertThat(TestViewModelWithAnnotatedInitialize.wasInitialized).isTrue(); + + } } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithAnnotatedInitialize.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithAnnotatedInitialize.java new file mode 100644 index 000000000..2358e739b --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithAnnotatedInitialize.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * 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.internal.viewloader.example; + +import de.saxsys.mvvmfx.FxmlView; +import de.saxsys.mvvmfx.InjectViewModel; + +/** + * This class is used as example View class that uses ViewModel initialized with + * method annotated with {@link de.saxsys.mvvmfx.Initialize} annotation + * + * @author Gleb Koval + */ +public class TestFxmlViewWithViewModelWithAnnotatedInitialize implements FxmlView { + + @InjectViewModel + private TestViewModelWithAnnotatedInitialize viewModel; + +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestViewModelWithAnnotatedInitialize.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestViewModelWithAnnotatedInitialize.java new file mode 100644 index 000000000..791abc9f7 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestViewModelWithAnnotatedInitialize.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * 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.internal.viewloader.example; + +import de.saxsys.mvvmfx.Initialize; +import de.saxsys.mvvmfx.ViewModel; + +/** + * This class is used as example ViewModel class that uses init method annotated with {@link Initialize} + * + * @author Gleb Koval + */ +public class TestViewModelWithAnnotatedInitialize implements ViewModel { + + public static boolean wasInitialized = false; + + @Initialize + private void init() { + wasInitialized = true; + } + +} diff --git a/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithAnnotatedInitialize.fxml b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithAnnotatedInitialize.fxml new file mode 100644 index 000000000..7bad2467f --- /dev/null +++ b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithAnnotatedInitialize.fxml @@ -0,0 +1,6 @@ + + + + + +