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 |
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.)
(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 |
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 |
@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() );
}
- Interact with the System Clipboard
- Manage the internal state of the Cut, Copy, and Paste operations (lastFocusedTF, lastSelectedText, and lastSelectedRange)
- 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