Alexander Casall Manuel Mauky - RainFocus · MVVM with JavaFX Alexander Casall @sialcasa 2016-09-21...
Transcript of Alexander Casall Manuel Mauky - RainFocus · MVVM with JavaFX Alexander Casall @sialcasa 2016-09-21...
● Custom Software Development
● Dresden, München, Berlin, Hamburg, Leipzig, Görlitz
● 200+ Employees
● Based on Presentation Model Pattern
● 2005 published by Microsoft
● WPF (.NET), JavaScript (knockout.js), ...
Model View ViewModel
ViewModel
● UI state
● Presentation logic
● Communication with backend
● Preparation of model data
Model
ViewModel
View
● Display data from ViewModel
● Pass user input to ViewModel
● Update UI state in ViewModel
Model
ViewModel
View
Data Binding and Properties
StringPropertyString Binding
= StringPropertyString
notifications about changes (events)
StringProperty a = new SimpleStringProperty();StringProperty b = new SimpleStringProperty();
a.bindBidirectional(b);
a.setValue(“Hallo”);System.out.println(b.getValue()); // “Hallo”b.setValue(“World”);System.out.println(a.getValue()); // “World”
Data Binding in MVVM
Model
ViewModel
welcomeMessageProperty.set( “Hallo “
+ user.getFirstName() + “ ” + user.getLastName());
firstName lastName
“Hallo Max Wielsch”
“Max” “Wielsch”
Data Binding in MVVMwelcomeLabel.textProperty().bind( viewModel.welcomeMessageProperty());
Model
ViewModel
View
welcomeMessageProperty.set( “Hallo “
+ user.getFirstName() + “ ” + user.getLastName());
firstName lastName
“Hallo Max Wielsch”
“Max” “Wielsch”
Benefits
● all presentation logic is located only in the ViewModel
● ViewModel is testable with unit tests
→ High testability of frontend code
→ Test-driven-development in the UI
UI Tests
System Tests
Integration Tests
Unit Tests
Challenges of MVVM
● Communication between VMs in more complex applications
● Demands discipline to not violate visibility constraints
● More code necessary for layers of indirection
… is an open-source application framework providing necessary components for the usage of the Model View ViewModel pattern with JavaFX.
https://github.com/sialcasa/mvvmFX
… is an open-source application framework providing necessary components for the usage of the Model View ViewModel pattern with JavaFX.
https://github.com/sialcasa/mvvmFX
Basic Classes for MVVMExtended FXML Loader
Dependency Injection SupportResourceBundlesModelWrapper
NotificationsCommandsValidation
Scopes
Base Classes / Interfaces
class TodolistViewModel implements ViewModel {…
}
class TodolistView implements FxmlView<TodolistViewModel> {
@InjectViewModelprivate TodolistViewModel viewModel;
}
TodolistView.fxml:
<?xml version="1.0" encoding="UTF-8"?>
...<VBox xmlns:fx="http://javafx.com/fxml"
fx:controller="de.saxsys.mvvmfx.examples.todo.TodolistView"> <children> ... </children></VBox>
But how to load a MVVM component?URL url = getClass().getResource(“/de/saxsys/MyView.fxml”);FXMLLoader loader = new FXMLLoader(url);loader.load();loader.getRoot(); // loaded nodeloader.getController(); // controller class
But how to load a MVVM component?URL url = getClass().getResource(“/de/saxsys/MyView.fxml”);FXMLLoader loader = new FXMLLoader(url);loader.load();loader.getRoot(); // loaded nodeloader.getController(); // controller class
ViewTuple tuple = FluentViewLoader.fxmlView(MyView.class).load();tuple.getView(); // loaded node (View)tuple.getCodeBehind(); // controller class (View)tuple.getViewModel(); // ViewModel
Notificationspublic class MyView implements FxmlView<MyViewModel> { @InjectViewModel private MyViewModel viewModel; … viewModel.subscribe(“messageKey”, (k,v) -> doSomething()); …}public class MyViewModel implements ViewModel { … publish(“messageKey”); …}
How to handle dependencies of components?
class MyViewModel implements ViewModel {
private Service service = new ServiceImpl(); // ?
}
How to handle dependencies of components?
class MyViewModel implements ViewModel {
private Service service = new ServiceImpl(); // ?
@Injectprivate Service service;
}
Dependency Injection
● Inversion of Control
● No static dependency to a specific implementation, only to
interfaces
● Use mock implementations for unit tests
● Configure lifecycle of instances
Dependency Injection Support
MvvmFX.setCustomDependencyInjector(...);
or
<dependency> <groupId>de.saxsys</groupId> <artifactId>mvvmfx-cdi</artifactId></dependency>
<dependency> <groupId>de.saxsys</groupId> <artifactId>mvvmfx-guice</artifactId></dependency>
<dependency> <groupId>de.saxsys</groupId> <artifactId>mvvmfx-easydi</artifactId></dependency>
Dependency Injection Support
public class MyFxApp extends Application { … }
public class MyFxApp extends MvvmfxCdiApplication { … }
public class MyFxApp extends MvvmfxGuiceApplication { … }
public class MyFxApp extends MvvmfxEasyDIApplication {…}
Define a Scope
public class PersonScope implements Scope {
private ObjectProperty<Person> selectedPerson = new SimpleObjectProperty();
//Getter Setters
}
@ScopeProvider(scopes=PersonScope.class})
public class PersonViewModel implements ViewModel {}
A component in the hierarchy declares the scope
ScopeProvider
Components below can inject the same scope instance
public class PersonsOverviewViewModel implements
ViewModel {
@InjectScope
private PersonScope scope;
}
public class PersonDetailView implements ViewModel {
@InjectScope
private PersonScope scope;
}
Scopes can be used to decouple the communication between components by using a hierarchical dependency injection
Communication
Negative Examplepublic class ContactFormViewModel implements ViewModel {
private StringProperty firstname = new SimpleStringProperty(); private StringProperty lastname = new SimpleStringProperty(); private StringProperty emailAddress = new SimpleStringProperty(); private StringProperty phoneNumber = new SimpleStringProperty();
public StringProperty firstnameProperty() { return firstname; } public StringProperty lastnameProperty() { return lastname; } public StringProperty emailAddressProperty() { return emailAddress; } public StringProperty phoneNumberProperty() { return phoneNumber; }}
public class ContactFormViewModel implements ViewModel { // Properties and Property-Getter …
@Inject private Repository repository; private person;
public void showPerson(String id) { person = repository.find(id);
firstname.setValue(person.getFirstName()); lastname.setValue(person.getLastName()); emailAddress.setValue(person.getEmailAddress()); phoneNumber.setValue(person.getPhoneNumber()); } public void save() { person.setFirstName(firstname.get()); person.setLastName(lastname.get()); person.setEmailAddress(emailAddress.get()); person.setPhoneNumber(phoneNumber.get());
repository.persist(person); }}
public class ContactFormViewModel implements ViewModel { // Properties and Property-Getter …
@Inject private Repository repository; private person;
public void showPerson(String id) { person = repository.find(id);
firstname.setValue(person.getFirstName()); lastname.setValue(person.getLastName()); emailAddress.setValue(person.getEmailAddress()); phoneNumber.setValue(person.getPhoneNumber()); } public void save() { person.setFirstName(firstname.get()); person.setLastName(lastname.get()); person.setEmailAddress(emailAddress.get()); person.setPhoneNumber(phoneNumber.get());
repository.persist(person); }}
put data from properties back to model object
copy data from model objectto properties
public class ContactFormViewModel implements ViewModel {
private ModelWrapper<Person> modelWrapper = new ModelWrapper<>(); @Inject private Repository repository;
public void showPerson(String id) { Person person = repository.find(id);
modelWrapper.set(person);modelWrapper.reload();
} public void save() {
modelWrapper.commit();repository.persist(modelWrapper.get());
}
public StringProperty firstnameProperty() {return modelWrapper.field(Person::getFirstName, Person::setFirstName);
} public StringProperty lastnameProperty() {
return modelWrapper.field(Person::getLastName, Person::setLastName); } public StringProperty emailAddressProperty() {
return modelWrapper.field(Person::getEmailAddress, Person::setEmailAddress); } public StringProperty phoneNumberProperty() {
return modelWrapper.field(Person::getPhoneNumber, Person::setPhoneNumber); }}
Validation
Validation logic
boolean isPhoneNumberValid(String input) {
return Pattern.compile("\\+?[0-9\\s]{3,20}")
.matcher(input).matches();
}
Visualization
Validation
ControlsFX ValidationSupport
TextField textField = ...;
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.registerValidator(textField,
Validator.createRegexValidator("Wrong Number", "\\+?[0-9\\s]{3,20}", Severity.ERROR));
Validation// ViewModel
private final Validator phoneValidator = ...
public ValidationStatus phoneValidation() { return phoneValidator.getValidationStatus();}
// View
ValidationVisualizer validVisualizer= new ControlsFxVisualizer();validVisualizer.initVisualization(viewModel.phoneValidation(), textField);
ValidationStringProperty phoneNumber = new SimpleStringProperty();
Predicate<String> predicate = input -> Pattern.compile("\\+?[0-9\\s]{3,20}").matcher(input).matches();
Validator phoneValidator = new FunctionBasedValidator<>(phoneNumber, predicate, ValidationMessage.error("Not a valid phone number");
ValidationValidator phoneValidator = new FunctionBasedValidator(...);Validator emailValidator = new ObservableRuleBasedValidator(...);
Validator formValidator = new CompositeValidator();formValidator.addValidators(phoneValidator, emailValidator);
Lifecycle
● React when component is added/removed to scene
● add/remove listeners
● Example: Dialog is closed
Lifecycleclass DialogViewModel implements ViewModel, SceneLifecycle {
private NotificationObserver observer = (k,v) -> …;
@Overridepublic void onViewAdded() {
notificationCenter.subscribe("something", observer);}
@Overridepublic void onViewRemoved() {
notificationCenter.unsubscribe(observer);}
}
How to verify that the pattern was adhered to?
● Advantages of MVVM only available when all developers adhere to
the pattern
● How to find mistakes and wrong usage of API?
How to verify that the pattern was adhered to?
● Advantages of MVVM only available when all developers adhere to
the pattern
● How to find mistakes and wrong usage of API?
● AspectJ compile time checking (beta)
class MyViewModel implements ViewModel {
public void initValidation(Label usernameLabel) {
validationSupport.registerValidator(label,
Validator
.createRegexValidator("Error", "...", Severity.ERROR);
}
}
class MyViewModel implements ViewModel {
public void initValidation(Label usernameLabel) {
validationSupport.registerValidator(label,
Validator
.createRegexValidator("Error", "...", Severity.ERROR);
}
}
javafx.controls.Label used in ViewModel
> mvn aspectj:compile
…
[WARNING] Methods taking UI elements as arguments is invoked within the ViewModel
layer
/.../PersonsViewModel.java:34
validationSupport.registerValidator(label,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Source Code, Wiki, Tutorials: https://github.com/sialcasa/mvvmFX
Feedback, Bug Reports, Feature Requests welcome :-)
www.mvvmfx.de
Alexander Casall
[email protected]@sialcasa
Manuel Mauky
[email protected]://lestard.eu@manuel_mauky
Q & A