diff --git a/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/DependencyInjector.java b/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/DependencyInjector.java index 056c61c5d..3f8c62109 100644 --- a/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/DependencyInjector.java +++ b/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/DependencyInjector.java @@ -18,6 +18,13 @@ import java.lang.reflect.Field; import java.security.AccessController; import java.security.PrivilegedAction; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import de.saxsys.jfx.mvvm.api.ViewModel; import javafx.util.Callback; import net.jodah.typetools.TypeResolver; @@ -80,34 +87,73 @@ T getInstanceOf(Class type) { return instance; } + void injectViewModel(final View view) { final Class viewModelType = TypeResolver.resolveRawArgument(View.class, view.getClass()); final Field field = getViewModelField(view.getClass(), viewModelType); - if (field != null) { - AccessController.doPrivileged(new PrivilegedAction() { - @Override - public Object run() { - boolean wasAccessible = field.isAccessible(); - - try { - Object viewModel = DependencyInjector.getInstance().getInstanceOf(viewModelType); - field.setAccessible(true); - field.set(view, viewModel); - } catch (IllegalAccessException exception) { - throw new IllegalStateException("Can't inject ViewModel of type <" + viewModelType - + "> into the view <" + view + ">"); - } finally { - field.setAccessible(wasAccessible); - } - return null; + if(field != null){ + accessField(field, () -> { + Object existingViewModel = field.get(view); + + if (existingViewModel == null) { + Object viewModel = DependencyInjector.getInstance().getInstanceOf(viewModelType); + field.setAccessible(true); + field.set(view, viewModel); } - }); + + return null; + }, "Can't inject ViewModel of type <" + viewModelType + + "> into the view <" + view + ">"); } } - + + + + /** + * This method is used to get the ViewModel instance of a given view/codeBehind. + * + * @param view the view instance where the viewModel will be looked for. + * @param the generic type of the View + * @param the generic type of the ViewModel + * @return the ViewModel instance or null if no viewModel could be found. + */ + @SuppressWarnings("unchecked") + , ViewModelType extends ViewModel> ViewModelType getViewModel(ViewType view){ + + final Class viewModelType = TypeResolver.resolveRawArgument(View.class, view.getClass()); + final Field field = getViewModelField(view.getClass(), viewModelType); + + if(field != null){ + return accessField(field, ()-> (ViewModelType)field.get(view), "Can't get the viewModel of type <" + viewModelType + ">"); + } else { + return null; + } + + } + + /** + * 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. + */ + private T accessField(final Field field, final Callable callable, String errorMessage){ + return AccessController.doPrivileged((PrivilegedAction) ()->{ + boolean wasAccessible = field.isAccessible(); + + try{ + if(callable != null){ + return callable.call(); + } + }catch(Exception exception){ + throw new IllegalStateException(errorMessage, exception); + }finally{ + field.setAccessible(wasAccessible); + } + return null; + }); + } private Field getViewModelField(Class viewType, Class viewModelType) { @@ -123,7 +169,6 @@ private Field getViewModelField(Class viewType, Class viewModelType) { } - private T getUninitializedInstanceOf(Class type) { if (isCustomInjectorDefined()) { return (T) customInjector.call(type); diff --git a/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/FxmlViewLoader.java b/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/FxmlViewLoader.java index a664605d7..24a67210b 100644 --- a/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/FxmlViewLoader.java +++ b/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/FxmlViewLoader.java @@ -57,7 +57,7 @@ , ViewModelType extends ViewModel loader.load(); - final View loadedController = loader.getController(); + final ViewType loadedController = loader.getController(); final Parent loadedRoot = loader.getRoot(); if (loadedController == null) { @@ -65,7 +65,9 @@ , ViewModelType extends ViewModel + " maybe your missed the fx:controller in your fxml?"); } - return new ViewTuple(loadedController, loadedRoot); + final ViewModelType viewModel = DependencyInjector.getInstance().getViewModel(loadedController); + + return new ViewTuple(loadedController, loadedRoot, viewModel); } catch (final IOException ex) { throw new RuntimeException(ex); diff --git a/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/JavaViewLoader.java b/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/JavaViewLoader.java index 6392d89c0..5728da2da 100644 --- a/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/JavaViewLoader.java +++ b/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/JavaViewLoader.java @@ -78,8 +78,10 @@ , ViewModelType extends ViewModel injectResourceBundle(view, resourceBundle); callInitialize(view); } - - return new ViewTuple<>(view, (Parent) view); + + final ViewModelType viewModel = DependencyInjector.getInstance().getViewModel(view); + + return new ViewTuple<>(view, (Parent) view, viewModel); } /** diff --git a/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/ViewTuple.java b/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/ViewTuple.java index 154b3092a..9cef11f09 100644 --- a/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/ViewTuple.java +++ b/mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/ViewTuple.java @@ -28,6 +28,7 @@ public class ViewTuple, ViewModel private final ViewType codeBehind; private final Parent view; + private final ViewModelType viewModel; /** * @param codeBehind @@ -35,13 +36,14 @@ public class ViewTuple, ViewModel * @param view * to set */ - public ViewTuple(final ViewType codeBehind, final Parent view) { + public ViewTuple(final ViewType codeBehind, final Parent view, final ViewModelType viewModel) { this.codeBehind = codeBehind; this.view = view; + this.viewModel = viewModel; } /** - * @return the code behind of the FXML File (known as controller class in JavaFX) + * @return the code behind of the View. (known as controller class in JavaFX FXML) */ public ViewType getCodeBehind() { return codeBehind; @@ -53,4 +55,11 @@ public ViewType getCodeBehind() { public Parent getView() { return view; } + + /** + * @return the viewModel + */ + public ViewModelType getViewModel(){ + return viewModel; + } } diff --git a/mvvmfx/src/test/java/de/saxsys/jfx/mvvm/viewloader/ViewLoaderIntegrationTest.java b/mvvmfx/src/test/java/de/saxsys/jfx/mvvm/viewloader/ViewLoaderIntegrationTest.java index 9f79a56ac..a0c17f640 100644 --- a/mvvmfx/src/test/java/de/saxsys/jfx/mvvm/viewloader/ViewLoaderIntegrationTest.java +++ b/mvvmfx/src/test/java/de/saxsys/jfx/mvvm/viewloader/ViewLoaderIntegrationTest.java @@ -17,6 +17,8 @@ import static org.assertj.core.api.Assertions.*; +import de.saxsys.jfx.mvvm.viewloader.example.TestFxmlViewWithMissingController; +import javafx.fxml.LoadException; import javafx.scene.layout.VBox; import org.junit.Before; @@ -119,4 +121,94 @@ public void testLoadFxmlViewWithoutViewModel() { assertThat(viewTuple.getView()).isNotNull(); assertThat(viewTuple.getCodeBehind()).isNotNull(); } + + /** + * It is possible to use an existing instance of the codeBehind/controller. + */ + @Test + public void testUseExistingCodeBehind(){ + + TestFxmlViewWithMissingController codeBehind = new TestFxmlViewWithMissingController(); + + viewLoader.setCodeBehind(codeBehind); + + ViewTuple viewTuple = viewLoader + .loadViewTuple(TestFxmlViewWithMissingController.class); + + assertThat(viewTuple).isNotNull(); + + assertThat(viewTuple.getCodeBehind()).isEqualTo(codeBehind); + assertThat(viewTuple.getCodeBehind().viewModel).isNotNull(); + } + + /** + * When there is already a Controller defined in the fxml file (fx:controller) then + * it is not possible to use an existing controller instance with the viewLoader. + */ + @Test + public void testUseExistingCodeBehindFailWhenControllerIsDefinedInFXML() { + + try { + TestFxmlView codeBehind = new TestFxmlView(); // the fxml file for this class has a fx:controller defined. + + viewLoader.setCodeBehind(codeBehind); + + ViewTuple viewTuple = viewLoader + .loadViewTuple(TestFxmlView.class); + + fail("Expected a LoadException to be thrown"); + }catch(Exception e) { + assertThat(e).hasCauseInstanceOf(LoadException.class).hasMessageContaining( + "Controller value already specified"); + } + } + + + /** + * The user can define a codeBehind instance that should be used by the viewLoader. + * When this codeBehind instance has already has a ViewModel it should not be overwritten when the view is loaded. + */ + @Test + public void testAlreadyExistingViewModelShouldNotBeOverwritten(){ + + TestFxmlViewWithMissingController codeBehind = new TestFxmlViewWithMissingController(); + + TestViewModel existingViewModel = new TestViewModel(); + + codeBehind.viewModel = existingViewModel; + + viewLoader.setCodeBehind(codeBehind); + + ViewTuple viewTuple = viewLoader + .loadViewTuple(TestFxmlViewWithMissingController.class); + + assertThat(viewTuple.getCodeBehind()).isNotNull(); + assertThat(viewTuple.getCodeBehind().viewModel).isEqualTo(existingViewModel); + } + + + @Test + public void testViewModelIsAvailableInViewTupleForFXMLView(){ + + ViewTuple viewTuple = viewLoader + .loadViewTuple(TestFxmlView.class); + + TestViewModel viewModel = viewTuple.getViewModel(); + + assertThat(viewModel).isNotNull(); + assertThat(viewModel).isEqualTo(viewTuple.getCodeBehind().viewModel); + } + + @Test + public void testViewModelIsAvailableInViewTupleForJavaView(){ + + ViewTuple viewTuple = viewLoader + .loadViewTuple(TestJavaView.class); + + TestViewModel viewModel = viewTuple.getViewModel(); + + assertThat(viewModel).isNotNull(); + assertThat(viewModel).isEqualTo(viewTuple.getCodeBehind().viewModel); + } } + diff --git a/mvvmfx/src/test/java/de/saxsys/jfx/mvvm/viewloader/example/TestFxmlViewWithMissingController.java b/mvvmfx/src/test/java/de/saxsys/jfx/mvvm/viewloader/example/TestFxmlViewWithMissingController.java index 60ffc7d81..a4ec3f603 100644 --- a/mvvmfx/src/test/java/de/saxsys/jfx/mvvm/viewloader/example/TestFxmlViewWithMissingController.java +++ b/mvvmfx/src/test/java/de/saxsys/jfx/mvvm/viewloader/example/TestFxmlViewWithMissingController.java @@ -1,7 +1,10 @@ package de.saxsys.jfx.mvvm.viewloader.example; import de.saxsys.jfx.mvvm.api.FxmlView; +import de.saxsys.jfx.mvvm.api.InjectViewModel; -public class TestFxmlViewWithMissingController implements FxmlView { +public class TestFxmlViewWithMissingController implements FxmlView { + @InjectViewModel + public TestViewModel viewModel; }