MVVM with JavaFX
Alexander Casall & Manuel Mauky
@sialcasa @manuel_mauky
2016-09-21
● Custom Software Development
● Dresden, München, Berlin, Hamburg, Leipzig, Görlitz
● 200+ Employees
MODEL VIEW
VIEWMODEL
Model View ViewModel
● Based on Presentation Model Pattern
● 2005 published by Microsoft
● WPF (.NET), JavaScript (knockout.js), ...
Model
● Application model
● Independent from UI
● Backend systems etc.
Model
ViewModel
● UI state
● Presentation logic
● Communication with backend
● Preparation of model data
ViewModel
Model
View
● Display data from ViewModel
● Pass user input to ViewModel View
● Update UI state in ViewModel
ViewModel
Model
KEEP
VIEW AND
VIEWMODEL
SYNCHRONIZED?
View
ViewModel
Model
Data Binding and Properties
notifications about changes
(events)
StringProperty = StringProperty
String Binding String
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
Data Binding in MVVM
Model firstName lastName
“Max” “Wielsch”
Data Binding in MVVM
welcomeMessageProperty.set(
“Hallo Max Wielsch”
“Hallo “
+ user.getFirstName() + “ ”
ViewModel + user.getLastName());
Model firstName lastName
“Max” “Wielsch”
Data Binding in MVVM
welcomeLabel.textProperty().bind(
viewModel.welcomeMessageProperty());
View
welcomeMessageProperty.set(
“Hallo Max Wielsch”
“Hallo “
+ user.getFirstName() + “ ”
ViewModel + user.getLastName());
Model firstName lastName
“Max” “Wielsch”
View Hierarchies
View View
View
ViewModel ViewModel ViewModel
? ?
Benefits
● all presentation logic is located only in the ViewModel
● ViewModel is testable with unit tests
UI
Tests
→ High testability of frontend code
System Tests
→ Test-driven-development in the UI
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 MVVM
Extended FXML Loader
Dependency Injection Support
ResourceBundles
ModelWrapper
Notifications
Commands
Validation
Scopes
How to create a MVVM Component?
Component
View FXML
CodeBehind class
ViewModel
Base Classes / Interfaces
class TodolistViewModel implements ViewModel {
…
}
class TodolistView implements FxmlView<TodolistViewModel> {
@InjectViewModel
private 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 node
loader.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 node
loader.getController(); // controller class
ViewTuple tuple = FluentViewLoader.fxmlView(MyView.class).load();
tuple.getView(); // loaded node (View)
tuple.getCodeBehind(); // controller class (View)
tuple.getViewModel(); // ViewModel
How to trigger an event in the View?
View
ViewModel
Notifications
public 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(); // ?
@Inject
private 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
<dependency> <dependency>
<groupId>de.saxsys</groupId> <groupId>de.saxsys</groupId>
<artifactId>mvvmfx-cdi</artifactId> <artifactId>mvvmfx-guice</artifactId>
</dependency> </dependency>
<dependency>
<groupId>de.saxsys</groupId>
<artifactId>mvvmfx-easydi</artifactId>
</dependency>
or
MvvmFX.setCustomDependencyInjector(...);
Dependency Injection Support
public class MyFxApp extends Application { … }
public class MyFxApp extends MvvmfxCdiApplication { … }
public class MyFxApp extends MvvmfxGuiceApplication { … }
public class MyFxApp extends MvvmfxEasyDIApplication {…}
SCOPES
Views are hierarchical
Component
Views need to share state
Chuck Norris
Duke
Duke
Duke
Duke
M D Master
Duke Detail
Views are hierarchical
Component
Scope
Define a Scope
public class PersonScope implements Scope {
private ObjectProperty<Person> selectedPerson = new SimpleObjectProperty();
//Getter Setters
}
A component in the hierarchy declares the scope
@ScopeProvider(scopes=PersonScope.class})
public class PersonViewModel implements ViewModel {}
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
Scenarios
MODEL
WRAPPER
ModelWrapper
The ModelWrapper optimizes reading and writing of model data.
Negative Example
public 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()); copy data from model object
lastname.setValue(person.getLastName()); to properties
emailAddress.setValue(person.getEmailAddress());
phoneNumber.setValue(person.getPhoneNumber());
}
public void save() {
person.setFirstName(firstname.get());
person.setLastName(lastname.get()); put data from properties
person.setEmailAddress(emailAddress.get());
back to model object
person.setPhoneNumber(phoneNumber.get());
repository.persist(person);
}
}
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
Validation logic Visualization
boolean isPhoneNumberValid(String input) {
return Pattern.compile("\\+?[0-9\\s]{3,20}")
.matcher(input).matches();
}
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);
Validation
StringProperty 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");
Validation
Validator phoneValidator = new FunctionBasedValidator(...);
Validator emailValidator = new ObservableRuleBasedValidator(...);
Validator formValidator = new CompositeValidator();
formValidator.addValidators(phoneValidator, emailValidator);
Lifecycle
Lifecycle
● React when component is added/removed to scene
● add/remove listeners
● Example: Dialog is closed
Lifecycle
class DialogViewModel implements ViewModel, SceneLifecycle {
private NotificationObserver observer = (k,v) -> …;
@Override
public void onViewAdded() {
notificationCenter.subscribe("something", observer);
}
@Override
public void onViewRemoved() {
notificationCenter.unsubscribe(observer);
}
}
How to Secure the
Architecture
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);
}
}
javafx.controls.Label used in
class MyViewModel implements ViewModel { ViewModel
public void initValidation(Label usernameLabel) {
validationSupport.registerValidator(label,
Validator
.createRegexValidator("Error", "...", Severity.ERROR);
}
}
> mvn aspectj:compile
> mvn aspectj:compile
[WARNING] Methods taking UI elements as arguments is invoked within the ViewModel
layer
/.../PersonsViewModel.java:34
validationSupport.registerValidator(label,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
www.mvvmfx.de
Source Code, Wiki, Tutorials: https://github.com/sialcasa/mvvmFX
Feedback, Bug Reports, Feature Requests welcome :-)
Q&A
Alexander Casall
alexander.casall@saxsys.de
@sialcasa
Manuel Mauky
manuel.mauky@saxsys.de
http://lestard.eu
@manuel_mauky