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 ...

Tuesday, August 11, 2015

Using a Constants File for YES/NO JavaFX ChoiceBoxes

All of the Java projects I work use a constants or globals file.  These files hold codes, identifiers, or other resources shared within the app that aren't handled by the language (enums) or JavaFX (resource bundles).  This  blog post shows how to factor out ChoiceBox-filling code that can add up if your application gathers a lot of yes/no selections from the user.

The source is available on GitHub here.

This JavaFX application shows two ChoiceBoxes containing yes/no selections.  The bottom ChoiceBox allows nulls.  When you press the Save Button, the selection made by the user is printed to System.out.

Screenshot of ChoiceBox Demo App
The User Selected YES and Left the Bottom Choice Box Empty
Constants File

The constants file is presented in its entirety.  I'm using a JavaFX Pair object to hold my key/value pairs: Y/YES, N/NO, null/_.  These objects are static and final meaning that they don't require an object (I don't have to "new" the Constants class) and that they can't be changed.  I follow a convention of using all caps with my constants to distinguish them from regular variables.

public class Constants {

 public final static Pair<String, String> PAIR_NULL = new Pair<>(null, "");
 public final static Pair<String, String> PAIR_YES = new Pair<>("Y", "YES");
 public final static Pair<String, String> PAIR_NO = new Pair<>("N", "NO");
 
 public final static ObservableList<Pair<String, String>> LIST_YES_NO =
   FXCollections.observableArrayList();
 
 public final static ObservableList<Pair<String, String>> LIST_YES_NO_OPT =
   FXCollections.observableArrayList();

 static {
  LIST_YES_NO.add( PAIR_NO );
  LIST_YES_NO.add( PAIR_YES );
  
  LIST_YES_NO_OPT.add( PAIR_NULL );
  LIST_YES_NO_OPT.add( PAIR_NO );
  LIST_YES_NO_OPT.add( PAIR_YES );
 }
}

I then add two ObservableList constants.  This fits with what a ChoiceBox is expecting in its setItems() call.  I don't want to make the code too complex in the constants file, but this is a way to remove trivial FXCollections calls throughout the code.

You can wrap the ObservableLists in unmodifiableObservableLists to strengthen the protection on the list.  This will prevent callers from adding anything to the lists by taking away the "add" method".

Finally, there is a static initializer.  The ObservableLists are created as empty lists.  My static initializer fills these lists with the constants.  Because of the awkwardness in error handling with a static initializer, I never do anything more complex in this construct.  Because the list is initialized and final, I know that the add() call will never thrown an NPE.

StringConverter

To use the Pair objects in a ChoiceBox, we need to provide a renderer.  The class StringConverter can be extended to serialize a string to and from a Pair object.  The to string conversion is simple; just return the value: "", YES, or NO.  From a string, I'm going to compare the string against the values and return one of the Pair constants.

public class PairStringConverter extends StringConverter<Pair<String, String>> {

 private final Boolean nullable;
 
 public PairStringConverter() {
  this.nullable = false;
 }

 public PairStringConverter(Boolean nullable) {
  this.nullable = nullable;
 }
 
 @Override
 public String toString(Pair<String, String> pair) {
  
  if( !nullable && pair.getKey() == null ) {
   throw new IllegalArgumentException("non-null converter received null pair key; set the nullable flag?");
  }
  
  return pair.getValue();
 }

 @Override
 public Pair<String, String> fromString(String string) {

  if( string == null && !nullable ) {
   throw new IllegalArgumentException("non-null converter received null pair key; set the nullable flag?");
  }

  if( string == Constants.PAIR_NULL.getValue() ) {
   return Constants.PAIR_NULL;
  }
  
  if( string.equalsIgnoreCase(Constants.PAIR_NO.getValue()) ) {
   return Constants.PAIR_NO;
  }
  
  if( string.equalsIgnoreCase(Constants.PAIR_YES.getValue()) ) {
   return Constants.PAIR_YES;
  }
  
  throw new IllegalArgumentException("unrecognized pair parse value '" + string + "'");
 }
}

This class supports a nullable flag for handling both use cases.  If null is a valid value (the selection of yes or no is optional), then we'll use a nullable StringConverter.

ChoiceBox Code

The Application puts the ChoiceBoxes in a VBox along with the Labels and Button.  See the link at the start of the post to GitHub for the complete source.

This is the section of code that creates the ChoiceBox requiring a yes / no selection.  Note the usage of a PairStringConverter without the nullable flag.  setValue() is setting the initial value.

Label label = new Label("Make Yes/No Selection");
  
ChoiceBox<Pair<String, String>> cb = new ChoiceBox<>();
cb.setItems( Constants.LIST_YES_NO );
cb.setConverter( new PairStringConverter() );
cb.setValue( Constants.PAIR_NO );

This is a similar looking ChoiceBox block.  The nullable flag is set on the converter.

ChoiceBox<Pair<String, String>> cbOpt = new ChoiceBox<>();
cbOpt.setItems( Constants.LIST_YES_NO_OPT );
cbOpt.setConverter(new PairStringConverter(true) );
cbOpt.setValue( Constants.PAIR_NULL );

While you can deal with each ChoiceBox individually, dispensing with the need for the separate classes, you're better of centralizing the handling of these simple lists.  Centralization buys you consistency which is important as an app grows larger.  Adding a Y or N to a ChoiceBox is easy, but may be coded differently across developers.  Some may unpack the items list, some may clear it, some may set it directly, some may create their own version of the Pair class, etc.  This Constants file is an easy way to get the word out to the team that there is a standard available and can easily be applied to a common pattern.

No comments:

Post a Comment