67
MVVM with JavaFX Alexander Casall @sialcasa 2016-09-21 Manuel Mauky @manuel_mauky &

Alexander Casall Manuel Mauky - RainFocus · MVVM with JavaFX Alexander Casall @sialcasa 2016-09-21 ... a.bindBidirectional(b); ... Alexander Casall [email protected]

  • Upload
    doque

  • View
    215

  • Download
    0

Embed Size (px)

Citation preview

MVVM with JavaFXAlexander Casall

@sialcasa

2016-09-21

Manuel Mauky@manuel_mauky

&

● Custom Software Development

● Dresden, München, Berlin, Hamburg, Leipzig, Görlitz

● 200+ Employees

MODEL VIEW VIEWMODEL

● Based on Presentation Model Pattern

● 2005 published by Microsoft

● WPF (.NET), JavaScript (knockout.js), ...

Model View ViewModel

Model

● Application model

● Independent from UI

● Backend systems etc.

Model

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

KEEPVIEW AND

VIEWMODEL SYNCHRONIZED?

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

Data Binding in MVVM

Model firstName lastName

“Max” “Wielsch”

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”

View Hierarchies

View View

View

ViewModel ViewModel ViewModel

? ?

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

How to create a MVVM Component?

View

CodeBehind class

FXML

ViewModel

Component

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

How to trigger an event in the View?

ViewModel

View

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 {…}

SCOPES

Component

Views are hierarchical

Chuck Norris

Duke

Duke

Duke

Duke

Duke DetailMasterM D

Views need to share state

Component

Scope

Views are hierarchical

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

Scenarios

MODELWRAPPER

ModelWrapper

The ModelWrapper optimizes reading and writing of model data.

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

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

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 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);

}

}

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

> 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