Skip to content

Commit

Permalink
Merge pull request #96 from sialcasa/#78_access_viewModel
Browse files Browse the repository at this point in the history
#78 access view model
  • Loading branch information
manuel-mauky committed Jul 28, 2014
2 parents da7bb91 + 9f5517d commit 896ca71
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -80,34 +87,73 @@ <T> T getInstanceOf(Class<? extends T> 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<Object>() {
@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 <ViewType> the generic type of the View
* @param <ViewModelType> the generic type of the ViewModel
* @return the ViewModel instance or null if no viewModel could be found.
*/
@SuppressWarnings("unchecked")
<ViewType extends View<? extends ViewModelType>, 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> T accessField(final Field field, final Callable<T> callable, String errorMessage){
return AccessController.doPrivileged((PrivilegedAction<T>) ()->{
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) {

Expand All @@ -123,7 +169,6 @@ private Field getViewModelField(Class<?> viewType, Class<?> viewModelType) {
}



private <T> T getUninitializedInstanceOf(Class<? extends T> type) {
if (isCustomInjectorDefined()) {
return (T) customInjector.call(type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,17 @@ <ViewType extends View<? extends ViewModelType>, ViewModelType extends ViewModel

loader.load();

final View loadedController = loader.getController();
final ViewType loadedController = loader.getController();
final Parent loadedRoot = loader.getRoot();

if (loadedController == null) {
throw new IOException("Could not load the controller for the View " + resource
+ " 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ <ViewType extends View<? extends ViewModelType>, 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);
}

/**
Expand Down
13 changes: 11 additions & 2 deletions mvvmfx/src/main/java/de/saxsys/jfx/mvvm/viewloader/ViewTuple.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,22 @@ public class ViewTuple<ViewType extends View<? extends ViewModelType>, ViewModel

private final ViewType codeBehind;
private final Parent view;
private final ViewModelType viewModel;

/**
* @param codeBehind
* to set
* @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;
Expand All @@ -53,4 +55,11 @@ public ViewType getCodeBehind() {
public Parent getView() {
return view;
}

/**
* @return the viewModel
*/
public ViewModelType getViewModel(){
return viewModel;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<TestFxmlViewWithMissingController, TestViewModel> 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<TestFxmlView, TestViewModel> 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<TestFxmlViewWithMissingController, TestViewModel> viewTuple = viewLoader
.loadViewTuple(TestFxmlViewWithMissingController.class);

assertThat(viewTuple.getCodeBehind()).isNotNull();
assertThat(viewTuple.getCodeBehind().viewModel).isEqualTo(existingViewModel);
}


@Test
public void testViewModelIsAvailableInViewTupleForFXMLView(){

ViewTuple<TestFxmlView, TestViewModel> viewTuple = viewLoader
.loadViewTuple(TestFxmlView.class);

TestViewModel viewModel = viewTuple.getViewModel();

assertThat(viewModel).isNotNull();
assertThat(viewModel).isEqualTo(viewTuple.getCodeBehind().viewModel);
}

@Test
public void testViewModelIsAvailableInViewTupleForJavaView(){

ViewTuple<TestJavaView, TestViewModel> viewTuple = viewLoader
.loadViewTuple(TestJavaView.class);

TestViewModel viewModel = viewTuple.getViewModel();

assertThat(viewModel).isNotNull();
assertThat(viewModel).isEqualTo(viewTuple.getCodeBehind().viewModel);
}
}

Original file line number Diff line number Diff line change
@@ -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<TestViewModel> {

@InjectViewModel
public TestViewModel viewModel;
}

0 comments on commit 896ca71

Please sign in to comment.