I'm building a JavaFX + Spring Boot application using the MVVM pattern.
I’m building a generic "wizard" in a JavaFX + Spring Boot MVVM app. A WizardViewModel drives a sequence of steps (STEP_ONE → STEP_TWO → STEP_THREE → ...), and each step has its own StepViewModel (e.g. StepOneViewModel) that performs some background work via javafx.concurrent.Task.
Current (tightly coupled) approach Each StepViewModel currently does this when it finishes:
// in StepOneViewModel
masterWizardViewModel.nextStep(); // directly advances the wizard
That creates a direct dependency between child and master VMs.
Two candidate decoupling strategies
1. JavaFX Property Binding
Have each StepViewModel expose a simple BooleanProperty doneProperty(), and let WizardViewModel listen:
// StepOneViewModel.java
@Component
public class StepOneViewModel {
private final BooleanProperty done = new SimpleBooleanProperty(false);
public BooleanProperty doneProperty() { return done; }
public void completeStep() {
// your background Task succeeded...
done.set(true);
}
}
// WizardViewModel.java
@Component
public class WizardViewModel {
@Autowired
StepOneViewModel stepOneVm;
public void initialize() {
stepOneVm.doneProperty().addListener((obs, was, done) -> {
if (done) advanceTo(Step.TWO);
});
}
private void advanceTo(Step next) { /* ... */ }
}
Pros
No extra Spring boilerplate.
Direct, type-safe JavaFX binding.
Cons
WizardViewModel must know and inject every StepViewModel.
Harder to reuse outside JavaFX thread.
2. Spring ApplicationEvents
Publish an event from the child VM and let WizardViewModel subscribe:
// StepCompletedEvent.java
public class StepCompletedEvent extends ApplicationEvent {
private final Step step;
public StepCompletedEvent(Object src, Step step) { super(src); this.step = step; }
public Step getStep() { return step; }
}
// StepOneViewModel.java
@Autowired ApplicationEventPublisher events;
public void completeStep() {
events.publishEvent(new StepCompletedEvent(this, Step.ONE));
}
// WizardViewModel.java
@Component
public class WizardViewModel {
@EventListener
public void onStepCompleted(StepCompletedEvent e) {
advanceTo(e.getStep().next());
}
private void advanceTo(Step next) { /* ... */ }
}
Pros
Loose coupling—publisher doesn’t need to know subscribers.
Works across UI, background services, other modules.
Cons
More boilerplate (event class + listener).
Must ensure UI updates happen on the JavaFX thread.
Questions
- Did I miss any significant trade-offs between these two patterns?
- In a mid-sized JavaFX + Spring Boot MVVM project, which approach have you found works best for decoupling ViewModels—and why?
- If you’ve implemented one or both, what pitfalls or gotchas did you encounter?
Thanks for any advice or example code you can share!
1 Answer 1
A Clean Alternative: Mediator-Based MVVM for Wizards in JavaFX + Spring Boot
After exploring multiple architectural options and learning from excellent feedback on StackOverflow and SoftwareEngineering.SE (thanks @James_D, @DaveB, @Ewan, and others), I implemented a third solution that closely follows the Mediator Pattern, and it’s worked great in production.
Using WizardViewModel as a Mediator
Each StepViewModel is fully decoupled — it doesn’t call the master directly or publish Spring Events.
Instead:
- The WizardViewModel tracks the current workflow step and ViewKey.
- The Controller (not the ViewModel!) listens for validation triggers and invokes wizardViewModel.onStepCompleted() when appropriate.
- All validation, error state, and workflow logic is centralized in the mediator.
- UI transitions are driven reactively using JavaFX Properties.
Controller → Mediator Communication (Example)
@Component
public class AdminPinController {
@FXML private PasswordField user1EnterPin;
private final WizardViewModel wizardVm;
private final ValidationHelper validationHelper;
private final ValidationState validationState;
private final Validator validator = new Validator();
public AdminPinController(WizardViewModel wizardVm,
ValidationHelper validationHelper,
ValidationState validationState) {
this.wizardVm = wizardVm;
this.validationHelper = validationHelper;
this.validationState = validationState;
}
@FXML
public void initialize() {
validationHelper.registerAdminsLoginValidations(validator, user1EnterPin);
validationState.formInvalidProperty().bind(validator.containsErrorsProperty());
wizardVm.validationRequestedProperty().addListener((obs, oldVal, newVal) -> {
if (Boolean.TRUE.equals(newVal) && isCurrentStepRelevant()) {
handleValidation();
wizardVm.validationProcessed();
}
});
}
private boolean isCurrentStepRelevant() {
return wizardVm.getCurrentContext().getStep() == WizardStep.LOGIN_USER1;
}
private void handleValidation() {
if (!validator.validate()) {
wizardVm.setErrorMessage("PIN validation failed.");
return;
}
wizardVm.onUserCompletedStep();
}
}
Inside the Mediator: WizardViewModel
@Component
public class WizardViewModel {
private final ObjectProperty<WizardStep> currentWorkflowStep = new SimpleObjectProperty<>(WizardStep.LOGIN_USER1);
private final ObjectProperty<ViewKey> currentViewKey = new SimpleObjectProperty<>();
public void onUserCompletedStep() {
WizardStep next = currentWorkflowStep.get().next();
currentWorkflowStep.set(next);
currentViewKey.set(resolveViewFor(next));
}
public void setErrorMessage(String message) { /* ... */ }
public void validationProcessed() { /* ... */ }
public ReadOnlyObjectProperty<WizardStep> currentWorkflowStepProperty() { return currentWorkflowStep; }
public ReadOnlyObjectProperty<ViewKey> currentViewKeyProperty() { return currentViewKey; }
}
Validation and Error Management
Validation is performed in the Controller using ValidatorFX.
ViewModel exposes a BooleanProperty for form validity:
validationState.formInvalidProperty().bind(validator.containsErrorsProperty());
Errors are managed centrally via:
wizardViewModel.setErrorMessage("PIN validation failed.");
Why Not Use Spring Events or Property Binding?
JavaFX Property Binding:
Pros: Reactive, type-safe
Cons: Wizard must reference every StepViewModel
Spring Events
Pros: Fully decoupled, modular
Cons: Async, UI-thread issues, more boilerplate
Mediator (this)
Pros: Centralized logic, sync, testable
Cons: No boilerplate, Requires Controller to forward calls
📌 Summary & Benefits
✅ Centralized workflow logic
✅ Fully decoupled StepViewModels
✅ No Spring Events or property wiring overhead
✅ MVVM-pure: Controller handles UI → ViewModel handles state
✅ Reactive, testable, and easy to debug
✅ Works cleanly with JavaFX threading
Explore related questions
See similar questions with these tags.
button
to your VM. you need the equivalent to onClick for your sub VMs