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

Saturday, October 25, 2014

JavaFX and the Delegate Pattern

The declarative power of SceneBuilder and CSS in today's Java desktop gives you flexibility and control over your UI separate from the Java code.

But these new artifacts -- the FXML of SceneBuilder and CSS -- need to be integrated in your app design using more than just JavaFX Controllers.  The FXML can become fragmented, requiring extra Java code to tie the fragments together.  Or, you'll have a monolithic FX program that will be difficult to maintain.

This blog post advocates for applying the Delegate Pattern to your Java FX app in conjunction with an expansive usage of FXML.  While you can refactor your way into the Delegate Pattern (do it later from a large Controller), it's far better to do this on the onset because the wiring and creating of Delegates will be cleaner.

App Overview


The Delegate Pattern (sometimes called "Helper Classes") is used widely in toolkits from iOS Cocoa Touch (UITextViewDelegate) to Java EE (the Business Delegate Pattern).  It can be formal, backed up with interfaces or annotations, or simply a farming out of a method from one class to another.  The design goal is to break your code up into manageable units.  Formal or informal, the more cohesive and consistent the delegates, the better.

App Screenshot

SceneBuilder and FXML View

In this program, there is a main view that consists of a MenuBar, a Toolbar, and a TabPane.  The TabPane itself contains Tabs.  All of this is defined in FXML.

App Structure from SceneBuilder
These big-ticket items (MenuBar, ToolBar, each of the Tabs) benefit from Delegation because each Delegate can a be cohesive block of functionality that can be cleanly lifted out from a JavaFX Controller.  This will make the JavaFX Controller smaller and focused in its role of linking FXML to Java source.

UML

From the Main -- a subclass of Application -- the FXML files are loaded using FXML.  Additionally, any inter-Controller wiring is performed in Main such as letting the MainViewController know about AlertController which is called in response to validation errors.  The Main also sets up the Stage and the styles used throughout the app.

From Main.java

     FXMLLoader mainViewLoader= new FXMLLoader(getClass().getResource("mavenpomupdater.fxml"));
    Parent mainView = (Parent)mainViewLoader.load();
    MainViewController mainViewController = mainViewLoader.getController();
    
    FXMLLoader alertViewLoader = new FXMLLoader(getClass().getResource("alert.fxml"));
    Parent alertView = (Parent)alertViewLoader.load();
    
    final AlertController alertController = alertViewLoader.getController();
    mainViewController.alertController = alertController;

This class diagram shows Main's loading relationship to the FXML files and how it gets a handle to the Controllers to allow for the alertController assignment.

(There is another view in the application - Alert View.  This is FXML created using the Alert Template that ships with SceneBuilder.)

App Structure in UML
MainViewController factors out code not directly associated with the Home Tab to other classes: MenuBarDelegate, ToolBarDelegate, AboutDelegate.  MainViewController contains all of the @FXML  annotations and is referenced in the FXML file.  MainViewController manages the life of the delegates, creating the instances and initializing them with the JavaFX Controls and Containers of interest.

Controller Constructor

In the Controller Constructor, I wire the Delegate instances.  I like to keep the Constructors as plain as possible, deferring the initialization to the @FXML initialize() method.  This way, the app can be wired up independently of JavaFX for unit testing and a possible dependency framework like Google Guice.

From MainViewController.java

    MenuBarDelegate menuBarDelegate;
    AboutDelegate aboutDelegate;
    ToolBarDelegate toolBarDelegate;

    public MainViewController() {
    
        aboutDelegate = new AboutDelegate();
        menuBarDelegate = new MenuBarDelegate();
        toolBarDelegate = new ToolBarDelegate();
    }

Initialize

The @FXML initialize() method contains active references to the FXML objects, making them suitable for assignment to a delegate.  I'm declining to use the JavaBeans pattern of getters and setters here since these are plain assignments on Delegates not built for general use.

From MainViewController.java's @FXML initialize() method

     //
        // wire up delegates
        //
        aboutDelegate.imageView = aboutImageView;
        aboutDelegate.tabPane = tabPane;
        aboutDelegate.aboutTab = aboutTab;
        aboutDelegate.version = version;
        aboutDelegate.aboutVersionLabel = aboutVersionLabel;
        
        menuBarDelegate.tabPane = tabPane;
        menuBarDelegate.homeTab = homeTab;
        menuBarDelegate.aboutTab = aboutTab;
        menuBarDelegate.supportURL = appProperties.getProperty(AppPropertiesKeys.SUPPORT_URL);
        menuBarDelegate.licenseURL = appProperties.getProperty(AppPropertiesKeys.LICENSE_URL);
        menuBarDelegate.miCut = miCut;
        menuBarDelegate.miCopy = miCopy;
        menuBarDelegate.miPaste = miPaste;
        menuBarDelegate.tfFilters = tfFilters;
        menuBarDelegate.tfNewVersion = tfNewVersion;
        menuBarDelegate.tfRootDir = tfRootDir;
        
        toolBarDelegate.tbCut = tbCut;
        toolBarDelegate.tbCopy = tbCopy;
        toolBarDelegate.tbPaste = tbPaste;
        toolBarDelegate.tfFilters = tfFilters;
        toolBarDelegate.tfNewVersion = tfNewVersion;
        toolBarDelegate.tfRootDir = tfRootDir;

        //
        // initialize delegates
        //
        aboutDelegate.init();
        toolBarDelegate.init();

Notice the cascading initialization to the Delegates (aboutBarDelegate.init(), toolBarDelegate.init()) which perform Delegate-specific initialization on the Controls.

At this point, the Delegate is wired to the Controller.  The next code snippets show the delegation from FXML file, to Controller, to Delegate.

The Cut Operation

Despite the use of Delegates, the SceneBuilder / Controller relationship is still the most important linkage between the FXML and the Java code.  The ToolBar Cut Button defines an fx:id and an On Action method.


The Cut Button in SceneBuilder
This is linked to MainViewController.java by a pair of @FXML-annotated members.  The tbCut member of the Controller is made known to the Delegate in the Controllers @FXML initialize() method (assignment toolBarDelegate.tbCut=tbCut).

    @FXML
    Button tbCut;

    
    @FXML
    public void tbCut() {
    toolBarDelegate.cut();

    }

Finally, the Delegate itself has sufficient objects to do its job.  The cut() operation is implemented as this in ToolBarDelegate.java.

public void cut() {
ClipboardContent content = new ClipboardContent();
content.putString(lastSelectedText);
systemClipboard.setContent(content);
String origText = lastFocusedTF.getText();
String firstPart = StringUtils.substring( origText, 0, lastSelectedRange.getStart() );
String lastPart = StringUtils.substring( origText, lastSelectedRange.getEnd(), StringUtils.length(origText) );
lastFocusedTF.setText( firstPart + lastPart );
lastFocusedTF.positionCaret( lastSelectedRange.getStart() );

}

While replacing 8 lines of code in the Controller with one might not seem significant, notice that the cut() operation's removal means that the Controller doesn't

  1. Interact with the System Clipboard
  2. Manage the internal state of the Cut, Copy, and Paste operations (lastFocusedTF, lastSelectedText, and lastSelectedRange)
  3. Involve itself in the Cut scenario expect to initiate it
There's nothing ground-breaking in using a Delegate in a Java program.  But if you're new to JavaFX, particularly if you have a strong Swing background, you may have trouble partitioning your application because many of the starting tutorials demonstrate only a single Controller. You can code yourself into a corner if you start a large porting-type of project over to JavaFX.  It's better to call out the delegates in advance because then you can make sure that they're used consistently throughout the project.

No comments:

Post a Comment