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

Sunday, May 1, 2016

Annotation-Based Validation and Submission for a JavaFX Wizard App

Two previous posts demonstrated a JavaFX wizard.  In the wizard, the user iterates through a sequence of screens gathering values.  The final step submits the aggregated data.  This post addresses wizard step validation and provides a submission hook that can track the user's progress from a back-end perspective and also shows where to put the final submission code.

The validation and submission mechanism is an annotation based framework and using it on a particular JavaFX Controller is optional.  A step that simply gathers optional values with no validation will not need to implement empty methods in its Controller.

If form validation is required, the user adds an @Validate-annotated method to the Controller.  If an action needs to take place after the step is executed -- including the final step -- the user provides an @Submit-annotated method.  This separates the concerns of the wizard, delegating the details to the classes that most understand the concern (ex, Field 1 is best understood in Step 1) and forgoes coupling these details to the calling class (ex, making the toplevel wizard class know all the details of every step).

All of the code in this example is on GitHub.

This is the third post in a three-part series and focuses on Core Java coding.  The JavaFX details of the UI are found in these two posts.
The following video demonstrates the wizard.  Validation warnings are displayed if validation fails.  Currently this is limited to required fields.  If the validation fails, the wizard prevents navigation to the next step.  If the step validates, a submission handler is called.  After the confirmation step, that particular submission handler is used to to execute the business logic.


Annotations

Prior to annotations, the Core Java programmer would implement something like this using interfaces.  That would force every class participating in the framework to implement boilerplate empty methods.  It would also give rise to a parallel set of abstract or base classes (ex, a Swing WindowAdapter for a WindowListener) to try to cut down on the boiler plate.  But class hierarchies are rigid and if your classes are already participating in a hierarchy, they can't simply extend another class.

Annotations give you a way to mark up a class, providing cues to Reflection-based code.  This is extremely flexible because the Reflection code can handle non-annotated methods.  Additionally, you can be given a lot of flexibility in the call signature.  Return values and exception throwing can vary. This helps to lower the learning curve on using your framework.

This wizard will make use of two annotations that are simple markers to be put on methods.  The annotations do not take any arguments (though a future "async=true" might be nice).

// Validate.java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Validate {
}

// Submit.java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Submit {
}

Now the word "framework" might conjure up images of "JAR hell" where something like Spring will wreck havoc with your classpath.  This framework is really limited to the 2 annotations plus some Reflection code presented later on.

Requirements on Controller

@Validate and @Submit will be added to Controller class methods.  To be able to get to the Controllers, I'm "tunneling" the Controller under the JavaFX Parents.  In working with FXML, there is a trinity of FXMLLoader, Parent, and Controller.  My previous posts showed the static use of FXMLoader, but in order for me to get to the Controller -- so that I can examine it's methods -- I have to use the object instance version.

Moreover, since I'm keeping the root Parent objects around for the operation of the wizard, I've decided to reference each Controller object with its root Node, the Parent.  I'm doing this with the Properties map that's available in all notes.  This is repeated for each step in a method called by the @FXML initialize().

final JavaFXBuilderFactory bf = new JavaFXBuilderFactory();

final Callback<Class<?>, Object> cb = (clazz) -> injector.getInstance(clazz);
  
FXMLLoader fxmlLoaderStep1 = new FXMLLoader( WizardController.class.getResource("/wizard-fxml/Step1.fxml"), null, bf, cb);
Parent step1 = fxmlLoaderStep1.load( );
step1.getProperties().put( CONTROLLER_KEY, fxmlLoaderStep1.getController() );

"CONTROLLER_KEY" is a constant I'm using so that I don't bobble the String value between the put and get calls.

Reimplemented Next Action

In the previous posts, the Next Action called after the user presses "Next" removed the current screen, incremented the currentStep variable, and added the index next screen.  Now, the Next Action will validate.  If the validation fails, the method will return.  Any visual cues or other logic will be handled exclusively by the Controller's @Validate.  If the validation succeeds, the Controller's @Submit method will be invoked.  The method wraps up by updating the display to the next step.

If an @Validate method is not provided, the operation is skipped.  This is the same for the @Submit method.  If you review the code on GitHub, you'll notice that Step2Controller does not have either of these methods.  This means that if you have a long sequence of steps in a Wizard, you can opt out of the whole @Validate/@Submit framework until the end.   This is key to a successful framework since it doesn't impose itself when not needed.

This is code found in the toplevel JavaFX Controller "WizardController".  WizardController manages the navigation.

@FXML
public void next() {

 Parent p = steps.get(currentStep.get());
 Object controller = p.getProperties().get(CONTROLLER_KEY);

 // validate
 Method v = getMethod( Validate.class, controller );
 if( v != null ) {
  try {
   Object retval = v.invoke(controller);
   if( retval != null && ((Boolean)retval) == false ) {
    return;
   }
  } catch (IllegalAccessException | InvocationTargetException e) {
   e.printStackTrace();
  }
 }

 // submit
 Method sub = getMethod( Submit.class, controller );
 if( sub != null ) {
  try {
   sub.invoke(controller);
  } catch (IllegalAccessException | InvocationTargetException e) {
   e.printStackTrace();
  }
 }

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

This code grabs all the Methods in the Controller object.  Using the utility getMethod(), it returns any matching the annotation.  If there is a match, the @Validate method is called.  Similarly, for the @Submit, the code grabs all the Methods in the Controller object.  getMethod() returns any matching @Submit.  If there is a match, the @Submit method is called.

In production code, I would probably wrap the whole operation in a try / catch and expect any errors at this level to be system errors.  For example, if a Controller's @Submit failed after all the @Validate methods were verified, I might have a global handler to notify the user and ask if they'd like to restart or go back a step.

getMethod() Utility


This is a small bit of Reflection code that returns a matching Method.

private Method getMethod(Class<? extends Annotation> an, Object obj) {

 if( an == null ) {
  return null;
 }

 if( obj == null ) {
  return null;
 }

 Method[] methods = obj.getClass().getMethods();
 if( methods != null && methods.length > 0 ) {
  for( Method m : methods ) {
   if( m.isAnnotationPresent(an)) {
    return m;
   }
  }
 }
 return null;
}

Controllers

This is the validation and submission code executed after the Next Button is pressed while on Step 1.  When validating, the @Validate method is called and if there is a field missing, a dialog is displayed informing the user of the specific violation.  The method returns false if the validation failed which will tell the caller WizardController what happened.

The @Submit method logs the completed step for debugging purposes.

In production code, I'd definitely refactor the required check into a separate class.  It's spelled out here for demonstration purposes because I don't want to introduce more indirection.

This is from Step1Controller.java.

@Validate
public boolean validate() throws Exception {

    if( tfField1.getText() == null || tfField1.getText().isEmpty() ) {
        Alert alert = new Alert(Alert.AlertType.ERROR);
        alert.setTitle("Step 1");
        alert.setHeaderText( "Missing Field" );
        alert.setContentText( "Field 1 is required." );
        alert.showAndWait();
        return false;
    }

    if( tfField2.getText() == null || tfField2.getText().isEmpty() ) {
        Alert alert = new Alert(Alert.AlertType.ERROR);
        alert.setTitle("Step 1");
        alert.setHeaderText( "Missing Field" );
        alert.setContentText( "Field 2 is required." );
        alert.showAndWait();
        return false;
    }

    if( tfField3.getText() == null || tfField3.getText().isEmpty() ) {
        Alert alert = new Alert(Alert.AlertType.ERROR);
        alert.setTitle("Step 3");
        alert.setHeaderText( "Missing Field" );
        alert.setContentText( "Field 3 is required." );
        alert.showAndWait();
        return false;
    }

   return true;
}

@Submit
public void submit() throws Exception {
     if( log.isDebugEnabled() ) {
        log.debug("[SUBMIT] the user has completed step 1");
    }
}

You'll find similar code in Step3Controller and ConfirmController.  ConfirmController's @Submit is intended to be the final business logic invocation.

Step2Controller does not have @Validate or @Submit methods.  Completed.fxml, which displays the final status, does not even have a Controller assigned in the FXML.

Building a "framework" may seem intimidating and reserved for big projects like the Spring Framework or the Simple Logging Framework for Java (SLF4J).  Hopefully, this post removes that fear and shows that a framework may simply be a few well-placed annotations in some files.  It's really important to have things like this in place in a project when working with a team. Validating and submitting forms is a common and usually simple task performed by Java developers.  However, I've seen highly skilled developers give widely divergent implementations on a project.  This is a killer in the maintenance phase

Platform-Independence Epilog


This posted demonstrated the wizard on Ubuntu (Linux).  The first post in the series featured a video running the wizard on a PC.  This is a short video running the example on a Mac.

When learning JavaFX, don't forget about the big picture!  Objective-C on Xcode might make the best Mac app.  C# in VisualStudio might make the best PC app.  GTK might make the best Ubuntu app.  But you'll need 3 different developers for this.  JavaFX covers all three platforms, running the same IDE, same class files, and same deployment everywhere.



No comments:

Post a Comment