The source code for this example can be found on GitHub.
This is the second post in a three-part series on building a JavaFX wizard app. The other posts are
- Annotation-Based Validation and Submission for a JavaFX Wizard App (3/3)
- Binding Step Indicators in a JavaFX Wizard App (1/3)
This video shows the wizard in operation.
Three screens -- steps one, two, and three -- are followed by a confirmation that displays the information gathered by the wizard for final review by the user. When the Next Button is pressed on the confirmation screen, the operation is confirmed by presenting the last screen in the wizard.
At any point, the user can reverse the iteration and edit previous values. Additionally, the user can cancel the wizard at any time. This will clear the form.
At the end of the wizard, the user can restart the sequence with an empty set of values.
Design
This UML diagram shows five .fxml / controller pairs: Step1, Step2, Step3, Confirmation, Completed. The navigation is managed by WizardController.
UML Class Diagram of Wizard Application |
WizardController's .fxml file is an empty shell with an HBox for the content. Depending on the position in the wizard, the root element of the different steps will be swapped out.
WizardController
Most of the non-boilerplate code in the app is in WizardController. These are the fields in the class.
@FXML VBox contentPanel; @FXML HBox hboxIndicators; @FXML Button btnNext, btnBack, btnCancel; @Inject Injector injector; @Inject WizardData model; private final List<Parent> steps = new ArrayList< >(); private final IntegerProperty currentStep = new SimpleIntegerProperty(-1);
WizardController uses a set of Circles to track the wizard progress. For information about this mechanism and the hboxIndicator field, visit this blog post.
The Injector object is from Google Guice. This is going to enable me to build a JavaFX Controller Factory so that any JavaFX controllers created by FXML will have their dependencies injected. For this app, that's limited to the WizardData object.
"steps" is a list of the Parents loaded in this class' @FXML initialize() method. "currentStep" keeps track of the wizard progress and serves as an index to retrieve a particular step.
initialize()
This is the initialize() method of the WizardController.
@FXML public void initialize() throws Exception { buildSteps(); initButtons(); buildIndicatorCircles(); setInitialContent(); }
buildSteps() uses FXMLLoader to create the Parent objects that will be swapped in and out as content for WizardController. It's using the expanded constructor so that the objects created will have their Google Guice dependencies (the WizardData object) injected.
private void buildSteps() throws java.io.IOException { final JavaFXBuilderFactory bf = new JavaFXBuilderFactory(); final Callback<Class<?>, Object> cb = (clazz) -> injector.getInstance(clazz); Parent step1 = FXMLLoader.load( WizardController.class.getResource("/wizard-fxml/Step1.fxml"), null, bf, cb ); Parent step2 = FXMLLoader.load( WizardController.class.getResource("/wizard-fxml/Step2.fxml"), null, bf, cb ); Parent step3 = FXMLLoader.load( WizardController.class.getResource("/wizard-fxml/Step3.fxml"), null, bf, cb ); Parent confirm = FXMLLoader.load( WizardController.class.getResource("/wizard-fxml/Confirm.fxml"), null, bf, cb ); Parent completed = FXMLLoader.load( WizardController.class.getResource("/wizard-fxml/Completed.fxml"), null, bf, cb); steps.addAll( Arrays.asList( step1, step2, step3, confirm, completed )); }
initButtons() uses JavaFX binding to configure the Buttons based on the currentStep. btnBack and btnNext will be disabled if they are at the lower or upper bounds of the steps. The binding will prevent the user from attempting to access something before the first step or after the last step. btnCancel uses a different label at the last step. At the last step, the operation is finished and "Cancel" is no longer appropriate.
private void initButtons() { btnBack.disableProperty().bind( currentStep.lessThanOrEqualTo(0) ); btnNext.disableProperty().bind( currentStep.greaterThanOrEqualTo(steps.size()-1) ); btnCancel.textProperty().bind( new When( currentStep.lessThan(steps.size()-1) ) .then("Cancel") .otherwise("Start Over") ); }
buildIndicators() is covered in this post. setInitialContent() prepares the initial screen.
private void setInitialContent() { currentStep.set( 0 ); // first element contentPanel.getChildren().add( steps.get( currentStep.get() )); }
Wizard Operation
Navigation is implemented using two Buttons of WizardController: Next and Back. Next moves through the sequence of steps and manages the tracking field "currentStep". As currentStep changes, new screens area swapped in, the Circles indicator is updated, and the Buttons themselves may be changed via the binding mentioned in the previous section.
The next() and back() methods are also the places to put business logic. For example, after a particular step executes, you might validate the WizardData object or save the WizardData object using a service call.
cancel() will manipulate the WizardData object and reset the wizard.
@FXML public void next() { if( currentStep.get() < (steps.size()-1) ) { contentPanel.getChildren().remove( steps.get(currentStep.get()) ); currentStep.set( currentStep.get() + 1 ); contentPanel.getChildren().add( steps.get(currentStep.get()) ); } } @FXML public void back() { if( currentStep.get() > 0 ) { contentPanel.getChildren().remove( steps.get(currentStep.get()) ); currentStep.set( currentStep.get() - 1 ); contentPanel.getChildren().add( steps.get(currentStep.get()) ); } } @FXML public void cancel() { contentPanel.getChildren().remove( steps.get(currentStep.get()) ); currentStep.set( 0 ); // first screen contentPanel.getChildren().add( steps.get(currentStep.get()) ); model.reset(); }
The Steps
All of the steps, including the confirmation step, contain only a few fields and a binding expression put in the @FXML intialize() method. The code for Step1 is presented below.
public class Step1Controller { @FXML TextField tfField1, tfField2, tfField3; @Inject WizardData model; @FXML public void initialize() { tfField1.textProperty().bindBidirectional( model.field1Property() ); tfField2.textProperty().bindBidirectional( model.field2Property() ); tfField3.textProperty().bindBidirectional( model.field3Property() ); } }
When the user updates tfField1, the first field in the first step, the model is also updated. This update is retained as the user navigates through the wizard. At any point in the operation, the full model can be examined or manipulated. This helps to decrease coupling.
WizardData
public class WizardData { private final StringProperty field1 = new SimpleStringProperty(); private final StringProperty field2 = new SimpleStringProperty(); private final StringProperty field3 = new SimpleStringProperty(); private final StringProperty field4 = new SimpleStringProperty(); private final StringProperty field5 = new SimpleStringProperty(); private final StringProperty field6 = new SimpleStringProperty(); private final StringProperty field7 = new SimpleStringProperty(); //...
The WizardData class contains JavaFX properties that enable binding. This class is used as a singleton as implemented by Google Guice. The Google Guice module says that any @Inject annotated WizardData fields will receive the shared object.
public class WizardModule extends AbstractModule { @Override protected void configure() { WizardData model = new WizardData(); bind(WizardData.class).toInstance(model); } }
Enhancements
next() and back() provide an entry point for validations and other business logic. An enhancement to this app might delegate this to the relevant step. For example, next() might call a controller's validate() method and display an error message and prevent the navigation if a field is missing. The controllers -- which are very plain in this implementation -- might also be marked-up with a pre and post navigation handler.
This blog post showed you how to build your own JavaFX wizard with a little help from Google Guice. CDI doesn't have the same adoption in desktop Java as it does on the server-side, but the 668k and few lines of code to enable Guice in a JavaFX app seems like a simple enough addition to justify a consistent pattern.
No comments:
Post a Comment