Featured Post

Applying Email Validation to a JavaFX TextField Using Binding

This example uses the same controller as in a previous post but adds a use case to support email validation.  A Commons Validator object is ...

Monday, September 14, 2015

JavaFX Binding 2 : Subforms

This example presents one of two address subforms based on the selection of a modified ToggleButton.   The subforms are VBoxes and their visibleProperties are bound to the selectedProperties of the ToggleButtons.  The ToggleButtons have been modified to give RadioButton like behavior as in the Today / Notifications screen you see in iOS 8 when you pull down from the top of an iPhone.



All of the source code in this post can be found on GitHub.

The App

This screenshot shows an app with two forms: a Canada-specific address subform and an a US-specific address subform.  The Canadian subform has a list of provinces while the US subform has a list of states and an extended zip code field.

Screenshot of Both Subforms

The top ToggleButtons drive the presentation of the subforms.  A StackPane contains both of the subforms, and a ToggleButton selection will make one form visible and the other invisible.  Apparently, the visibility property also affects the input focus, so I didn't have to shuffle the order using toFront() or toBack().

Classes

An Application subclass, Binding2App uses several FXMLLoader objects to load .fxml files: AccessController2.fxml, AccessController_US.fxml, AccessController_CA.fxml.  Each of the .fxml files is backed by a JavaFX controller of the same name.  The following UML shows the classes involved in the app.

Class Model
The following code shows the creation of the JavaFX root objects and controllers.  In GitHub, look for Binding2App.java

  AddressController2 ac2 = new AddressController2();
  
  FXMLLoader loader = new FXMLLoader( 
                  getClass().getResource("/binding2-fxml/Address.fxml") );
  loader.setRoot(ac2);
  Parent pAddr = loader.load();
  
  FXMLLoader usLoader = new FXMLLoader( 
                  getClass().getResource("/binding2-fxml/Address_US.fxml") );
  usLoader.load();
  
  FXMLLoader caLoader = new FXMLLoader( 
                  getClass().getResource("/binding2-fxml/Address_CA.fxml") );
  caLoader.load();
  
  //
  // Setters must be called before postInit()
  //
  ((AddressController2)loader.getController()).setUSForm( 
                  usLoader.getRoot(), usLoader.getController() );
  ((AddressController2)loader.getController()).setCAForm( 
                  caLoader.getRoot(), caLoader.getController() );
  ((AddressController2)loader.getController()).postInit();
  
  Scene scene = new Scene(pAddr);
  
  primaryStage.setScene(scene);
  primaryStage.setTitle( "Address Form" );

  primaryStage.show();



The three .fxml files are loaded independently, then the two subforms (Address_US and Address_CA) are wired into the main controller, Address.  A postInit() allows for additional initialization once the dependent objects are made available.

The AddressController2 class contains the two ToggleButtons, some setters, and stubbed-out save() and cancel() methods.  The class also has a postInit() method that binds the visibleProperties to the selectedProperties.  Finally, I provide some Event Filters to make the ToggleButtons behave as they do on the iOS screen mentioned earlier (basically RadioButtons).  If I leave this off, you can produce an empty form by de-selecting the ToggleButton.

Here is the postInit() method.

public void postInit() {
  
  usForm.visibleProperty().bind( tbUS.selectedProperty() );
  
  caForm.visibleProperty().bind( tbCanada.selectedProperty() ); 
  
  //
  // Makes controls behave like RadioButtons (always one selected)
  //
  tbUS.addEventFilter(EventType.ROOT, (evt) -> {
   
   if( evt.getEventType() == ActionEvent.ACTION ) {
    usController.clearForm();
    caController.clearForm();
   }

   if( tbUS.isSelected() ) {
    evt.consume();    
   }
  });
  
  tbCanada.addEventFilter(EventType.ROOT, (evt) -> {

   if( evt.getEventType() == ActionEvent.ACTION ) {
    usController.clearForm();
    caController.clearForm();
   }
   
   if( tbCanada.isSelected() ) {
    evt.consume();
   } 
  });

 }

Both of the subforms are contains of country-specific fields.  Their properties -- the JavaFX controls -- are packaged-scoped, so they can be retrieved by the main controller AddressController2.

This is a follow-up post to JavaFX Binding 1 : A Conditional Address Form.  This example is more intuitive than the previous code in that there aren't any "holes" left between fields that aren't relevant for the current submission.  Be sure to visit GitHub to browse the complete source code.  Both examples are in the Examples project

No comments:

Post a Comment