JavaFX Dialog Service

One of the nice features added as of JavaFX 2.x is the ability to refactor GUI related background tasks into Services using worker Tasks. This approach allows background tasks to run in a thread safe manner that doesn’t lock up your application while they are being run.

One of the most common application requirements is the need for a dialog window. JavaFX has nicely equipped us with a few options such as a PopupWindow or even an entirely different Stage. The Stage option provides for a more granular approach with more control over it’s behavior. So, it only seems natural that the marriage of a Stage and a Service can provide a reusable template for interactive dialog poups while avoiding unnecessary boilerplate code.

We’ll start by creating a simple example dialog box for logging into an application.

Validation using a Service

When the user clicks on the login button we would like to validate that the username and password fields are filled out. We could of course add a much more robust validation mechanism, but for demonstration purposes we just want to validate that they have not left the fields blank. To do this we have added a simple message header text to display exception messages via our dialog service (like the one below)

How it works

The first thing we need to do is create a Service that will create Tasks for showing and hiding our login window. When creating a task to run the Service determines if the supplied Stage is showing or not. If it’s showing than it knows to create a hide Task that will be responsible for hiding the dialog Stage when ran. When the Stage hasn’t been shown yet we create a show Task that will show the dialog Stage when ran. We also added a ChangeListener to the show Task that will auto-hide the dialog Stage when the show Task fails or is cancelled.

		@Override
		protected Task<Void> createTask() {
			return window.isShowing() ? createHideTask() : createShowTask();
		}

		protected Task<Void> createShowTask() {
			final Task<Void> showTask = new Task<Void>() {
				@Override
				protected Void call() throws Exception {
					Platform.runLater(new Runnable() {
						public void run() {
							window.show();
							window.centerOnScreen();
						}
					});
					return null;
				}
			};
			showTask.stateProperty().addListener(new ChangeListener<State>() {
				@Override
				public void changed(final ObservableValue<? extends State> observable, 
						final State oldValue, final State newValue) {
					if (newValue == State.FAILED || newValue == State.CANCELLED) {
						Platform.runLater(createHideTask());
					}
				}
			});
			return showTask;
		}

		protected Task<Void> createHideTask() {
			final Task<Void> closeTask = new Task<Void>() {
				@Override
				protected Void call() throws Exception {
					window.hide();
					window.getScene().getRoot().setDisable(false);
					return null;
				}
			};
			return closeTask;
		}

Now that we have created the Tasks to show and hide the dialog Stage we need a way to run the dialog Service. What better way to do this than to pass in another Service into our dialog Service. That way we can listen for a SUCEEDED state from the passed “submit” service. When it completes successfully we know that it’s time to hide our dialog Stage. If an exception is thrown in the provided submit Service we know something went wrong and should show the exception message and prevent our dialog window from being hid. To do this we also need to pass in a Text node that we can update with the exception message when one is thrown by the passed submit service.

		protected DialogService(final Stage parent, final Stage window, 
				final Text messageHeader, final Service<Void> submitService) {
			this.window = window;
			this.parent = parent;
			this.submitService = submitService;
			this.submitService.stateProperty().addListener(new ChangeListener<State>() {
				@Override
				public void changed(final ObservableValue<? extends State> observable, 
						final State oldValue, final State newValue) {
					if (submitService.getException() != null) {
						// service indicated that an error occurred
						messageHeader.setText(submitService.getException().getMessage());
					} else if (newValue == State.SUCCEEDED) {
						// run a hide task... we are done!
						Platform.runLater(createHideTask());
					}
				}
			});
		}

The only thing remaining is creating a method for adding visual components to our dialog service. There are numerous ways to do this, but to keep it simple we created a static utility method that will construct our stage and dialog Service. It allows us to pass in an arbitrary number of child Nodes that will be added to a VBox that appears in-between the header Text and our Button FlowPane at the bottom. If any of the specified Nodes are a Button they will be added to the same FlowPane that the “submit” button that starts/restarts our dialog Service. The important thing is to ensure that the submit button starts/restarts the dialog Service when clicked. That way we can rerun the dialog service as many times as our application needs to. For example, if the supplied submit Service throws an exception to indicate the form entries are not valid we don’t want the dialog Service to successfully complete and close our dialog Stage, but rather wait for the user to correct the problem and proceed by clicking the login/submit button (at which point the supplied submit Service will be reran).

	public static DialogService dialog(final Stage parent, final String title, 
			final String headerText, final Image icon, final String submitLabel, 
			final double width, final double height, final Service<Void> submitService, 
			final Node... children) {
		final Stage window = new Stage();
		final Text header = TextBuilder.create().text(headerText).styleClass(
				"dialog-title").wrappingWidth(width / 1.2d).build();
		final Text messageHeader = TextBuilder.create().styleClass("dialog-message"
				).wrappingWidth(width / 1.2d).build();
		final DialogService service = new DialogService(parent, window, 
				messageHeader, submitService);
		window.initModality(Modality.APPLICATION_MODAL);
		window.initStyle(StageStyle.TRANSPARENT);
		if (icon != null) {
			window.getIcons().add(icon);
		}
		if (title != null) {
			window.setTitle(title);
		}
		final VBox content = VBoxBuilder.create().styleClass("dialog").build();
		content.setMaxSize(width, height);
		window.setScene(new Scene(content, width, height, Color.TRANSPARENT));
		if (parent != null) {
			window.getScene().getStylesheets().setAll(parent.getScene().getStylesheets());
		}
		final Button submitBtn = ButtonBuilder.create().text(submitLabel).defaultButton(
				true).onAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(final ActionEvent actionEvent) {
				submitService.restart();
			}
		}).build();
		final FlowPane flowPane = new FlowPane();
		flowPane.setAlignment(Pos.CENTER);
		flowPane.setVgap(20d);
		flowPane.setHgap(10d);
		flowPane.setPrefWrapLength(width);
		flowPane.getChildren().add(submitBtn);
		content.getChildren().addAll(header, messageHeader);
		if (children != null && children.length > 0) {
			for (final Node node : children) {
				if (node == null) {
					continue;
				}
				if (node instanceof Button) {
					flowPane.getChildren().add(node);
				} else {
					content.getChildren().add(node);
				}
			}
		}
		content.getChildren().addAll(flowPane);
		return service;
	}

CSS Mojo

Finally, we can provide a little CSS mojo to our login form. We’ll call it dialog.css and place it in the same path as our final Application example below.

/*******************************************************************************
*                                                                             *
* Dialog CSS                                                                  *
*                                                                             *
******************************************************************************/
.dialog-title {
    -fx-fill: white;
    -fx-font-style: oblique;
    -fx-font-size: 14px;
    -fx-effect: dropshadow( one-pass-box , rgba(75,75,75,0.8) , 0, 0.0 , 2, 2);
}
.dialog-message {
    -fx-fill: crimson;
    -fx-font-style: oblique;
    -fx-font-size: 12px;
    -fx-effect: dropshadow( one-pass-box , rgba(0,0,0,0.8) , 0, 0.0 , 1, 1);
}
.dialog {
    -fx-base: #505359;
    -fx-background: #505359;
    -fx-spacing: 10;
    -fx-padding: 10 10 10 10;
    -fx-background-color:
        linear-gradient(#686868 0%, #232723 25%, #373837 75%, #757575 100%),
        linear-gradient(#020b02, #3a3a3a),
        linear-gradient(#9d9e9d 0%, #6b6a6b 20%, #343534 80%, #242424 100%),
        linear-gradient(#8a8a8a 0%, #6b6a6b 20%, #343534 80%, #262626 100%),
        linear-gradient(#777777 0%, #606060 50%, #505250 51%, #2a2b2a 100%);
      -fx-alignment: center;
      -fx-border-width: 3;
    -fx-border-color: 
      linear-gradient(#686868 0%, #232723 25%, #373837 75%, #757575 100%) 
            linear-gradient(#686868 10%, white 40%, #343534 90%, #757575 100%)
            linear-gradient(#686868 0%, #232723 25%, #373837 75%, #757575 100%)
            linear-gradient(#686868 10%, white 40%, #343534 90%, #757575 100%);
      -fx-border-insets: 1 1 1 1;
      -fx-border-radius: 8 8 8 8;
      -fx-background-radius: 10 10 10 10;
}
/*******************************************************************************
 *                                                                             *
 * Button                                                                      *
 *                                                                             *
 ******************************************************************************/
.button {
    -fx-background-color:
        #a6b5c9,
        linear-gradient(#303842 0%, #3e5577 20%, #375074 100%),
        linear-gradient(#768aa5 0%, #849cbb 5%, #5877a2 50%, #486a9a 51%, #4a6c9b 100%);
    -fx-background-insets: 0 0 -1 0,0,1;
    -fx-background-radius: 0;
    -fx-padding: 7 30 7 30;
    -fx-font-size: 12px;
    -fx-text-fill: white;
}
.button Text {
    -fx-effect: dropshadow( one-pass-box , rgba(0,0,0,0.8) , 0, 0.0 , 0 , -1 );
}
.button:focused {
    -fx-color: -fx-focused-base;
    -fx-effect: dropshadow( one-pass-box , black, 0, 0.0 , 2, 2);
    -fx-background-insets: -1.4, 0, 1, 2;
}
.button:hover {
    -fx-color: -fx-focused-base;
    -fx-background-color:
        #b6c5d9,
        linear-gradient(#404852 0%, #4e6587 20%, #476084 100%),
        linear-gradient(#869ab5 0%, #94accb 5%, #6887b2 50%, #587aaa 51%, #5a7cab 100%);
    -fx-effect: dropshadow( one-pass-box , rgba(0,0,0,0.8) , 0, 0.0 , 2, 2);
    -fx-background-insets: -1.4, 0, 1, 2;
    -fx-background-radius:  6.4, 5, 4, 3;
}
.button:armed {
    -fx-color: -fx-pressed-base;
}
.button:default {
    -fx-base: -fx-accent;
}
.button:cancel {
     -fx-base: -fx-accent;
 }
.button:disabled {
    -fx-opacity: -fx-disabled-opacity;
}
.button:show-mnemonics .mnemonic-underline {
    -fx-stroke: -fx-text-fill;
}

Putting it all together

Below is an example Application that demonstrates the use of a dialog Service. The primary Stage of the application consists of a simple window with a Button that will call our dialog method that will launch a new dialog window. You may also notice that we also added some additional features such as a “lightbox” modality that will dim the primary Stage while the dialog window is shown (hard to see in the example because the background is black).

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBuilder;
import javafx.scene.control.PasswordField;
import javafx.scene.control.PasswordFieldBuilder;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFieldBuilder;
import javafx.scene.effect.ColorAdjustBuilder;
import javafx.scene.effect.Effect;
import javafx.scene.image.Image;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.VBox;
import javafx.scene.layout.VBoxBuilder;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.scene.text.TextBuilder;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

/**
 * {@linkplain DialogService} Demo
 */
public class DialogServiceTest extends Application {

	private DialogService dialogService;

	/**
	 * Main {@linkplain Application} entry point
	 *
	 * @param args
	 *            passed arguments
	 */
	public static void main(final String[] args) {
		try {
			Application.launch(DialogServiceTest.class, args);
		} catch (final Throwable t) {
			t.printStackTrace();
		}
	}

	/**
	 * Shows an example usage of a {@linkplain DialogService} that displays a
	 * login screen
	 *
	 * @param primaryStage
	 *            the primary application {@linkplain Stage}
	 * @throws Exception
	 *             when something goes wrong
	 */
	@Override
	public void start(final Stage primaryStage) throws Exception {
		// setup the primary stage with a simple button that will open
		final VBox rootNode = new VBox();
		rootNode.setAlignment(Pos.CENTER);
		final Button btn = new Button("Launch Login Dialog Window");
		btn.setOnMouseClicked(new EventHandler<MouseEvent>() {
			@Override
			public void handle(final MouseEvent event) {
				if (dialogService != null) {
					dialogService.hide();
				}
				dialogService = createLoginDialog(primaryStage);
		        dialogService.start();
			}
		});
		rootNode.getChildren().add(btn);
		primaryStage.setTitle("Dialog Service Demo");
		primaryStage.setScene(new Scene(rootNode, 800, 500, Color.BLACK));
		primaryStage.getScene().getStylesheets().add(
				DialogServiceTest.class.getResource("dialog.css").toExternalForm());
		primaryStage.show();
	}

	/**
	 * Creates a {@linkplain DialogService} that displays a
	 * login screen
	 *
	 * @param primaryStage
	 *            the primary application {@linkplain Stage}
	 */
	public DialogService createLoginDialog(final Stage primaryStage) {
        final TextField username = TextFieldBuilder.create().promptText(
				"Username").build();
		final PasswordField password = PasswordFieldBuilder.create().promptText(
				"Password").build();
		final Button closeBtn = ButtonBuilder.create().text("Close").build();
		final Service<Void> submitService = new Service<Void>() {
			@Override
			protected Task<Void> createTask() {
				return new Task<Void>() {
					@Override
					protected Void call() throws Exception {
						final boolean hasUsername = !username.getText()
								.isEmpty();
						final boolean hasPassword = !password.getText()
								.isEmpty();
						if (hasUsername && hasPassword) {
							// TODO : perform some sort of authentication here
							// or you can throw an exception to see the error
							// message in the dialog window
						} else {
							final String invalidFields = (!hasUsername ? username
									.getPromptText() : "")
									+ ' '
									+ (!hasPassword ? password.getPromptText()
											: "");
							throw new RuntimeException("Invalid "
									+ invalidFields);
						}
					}
				};
			}
		};
		final DialogService dialogService = dialog(primaryStage,
				"Test Dialog Window",
				"Please provide a username and password to access the application",
				null, "Login", 550d, 300d, submitService, closeBtn, username, password);
		if (closeBtn != null) {
		      closeBtn.setOnMouseClicked(new EventHandler<MouseEvent>() {
		            @Override
		            public void handle(final MouseEvent event) {
		                  dialogService.hide();
		            }
		      });
		}
		return dialogService;
	}

	/**
	 * Creates a dialog window {@linkplain Stage} that is shown when the
	 * {@linkplain DialogService#start()} is called and hidden when the submit
	 * {@linkplain Service#restart()} returns {@linkplain State#SUCCEEDED}. When
	 * a {@linkplain Task} throws an {@linkplain Exception} the
	 * {@linkplain Exception#getMessage()} will be used to update the
	 * messageHeader of the dialog.
	 *
	 * @param parent
	 *            the parent {@linkplain Stage}
	 * @param title
	 *            the text for the {@linkplain Stage#setTitle(String)}
	 * @param headerText
	 *            the text for the {@linkplain Text#setText(String)} header
	 * @param icon
	 *            the icon of the {@linkplain Stage}
	 * @param submitLabel
	 *            the text for the submit {@linkplain Button#setText(String)}
	 * @param width
	 *            the width of the {@linkplain Stage}
	 * @param height
	 *            the height of the {@linkplain Stage}
	 * @param submitService
	 *            the {@linkplain Service} ran whenever the submit
	 *            {@linkplain Button} is clicked
	 * @param children
	 *            the child {@linkplain Node}s that will be added between the
	 *            messageHeader and submit {@linkplain Button} (if any). If any
	 *            of the {@linkplain Node}s are {@linkplain Button}s they will
	 *            be added to the internal {@linkplain Button}
	 *            {@linkplain FlowPane} added to the bottom of the dialog.
	 * @return the {@linkplain DialogService}
	 */
	public static DialogService dialog(final Stage parent, final String title,
			final String headerText, final Image icon, final String submitLabel,
			final double width, final double height, final Service<Void> submitService,
			final Node... children) {
		final Stage window = new Stage();
		final Text header = TextBuilder.create().text(headerText).styleClass(
				"dialog-title").wrappingWidth(width / 1.2d).build();
		final Text messageHeader = TextBuilder.create().styleClass("dialog-message"
				).wrappingWidth(width / 1.2d).build();
		final DialogService service = new DialogService(parent, window,
				messageHeader, submitService);
		window.initModality(Modality.APPLICATION_MODAL);
		window.initStyle(StageStyle.TRANSPARENT);
		if (icon != null) {
			window.getIcons().add(icon);
		}
		if (title != null) {
			window.setTitle(title);
		}
		final VBox content = VBoxBuilder.create().styleClass("dialog").build();
		content.setMaxSize(width, height);
		window.setScene(new Scene(content, width, height, Color.TRANSPARENT));
		if (parent != null) {
			window.getScene().getStylesheets().setAll(parent.getScene().getStylesheets());
		}
		final Button submitBtn = ButtonBuilder.create().text(submitLabel).defaultButton(
				true).onAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(final ActionEvent actionEvent) {
				submitService.restart();
			}
		}).build();
		final FlowPane flowPane = new FlowPane();
		flowPane.setAlignment(Pos.CENTER);
		flowPane.setVgap(20d);
		flowPane.setHgap(10d);
		flowPane.setPrefWrapLength(width);
		flowPane.getChildren().add(submitBtn);
		content.getChildren().addAll(header, messageHeader);
		if (children != null && children.length > 0) {
			for (final Node node : children) {
				if (node == null) {
					continue;
				}
				if (node instanceof Button) {
					flowPane.getChildren().add(node);
				} else {
					content.getChildren().add(node);
				}
			}
		}
		content.getChildren().addAll(flowPane);
		return service;
	}

	/**
	 * A {@linkplain Service} for showing and hiding a {@linkplain Stage}
	 */
	public static class DialogService extends Service<Void> {

		private final Stage window;
		private final Stage parent;
		private final Effect origEffect;
		private final Service<Void> submitService;

		/**
		 * Creates a dialog service for showing and hiding a {@linkplain Stage}
		 *
		 * @param parent
		 *            the parent {@linkplain Stage}
		 * @param window
		 *            the window {@linkplain Stage} that will be shown/hidden
		 * @param messageHeader
		 *            the messageHeader {@linkplain Text} used for the service
		 *            that will be updated with exception information as the
		 *            submitService informs the {@linkplain DialogService} of
		 * @param submitService
		 *            the {@linkplain Service} that will be listened to for
		 *            {@linkplain State#SUCCEEDED} at which point the
		 *            {@linkplain DialogService} window {@linkplain Stage} will
		 *            be hidden
		 */
		protected DialogService(final Stage parent, final Stage window,
				final Text messageHeader, final Service<Void> submitService) {
			this.window = window;
			this.parent = parent;
			this.origEffect = hasParentSceneRoot() ? this.parent.getScene(
					).getRoot().getEffect() : null;
			this.submitService = submitService;
			this.submitService.stateProperty().addListener(new ChangeListener<State>() {
				@Override
				public void changed(final ObservableValue<? extends State> observable,
						final State oldValue, final State newValue) {
					if (submitService.getException() != null) {
						// service indicated that an error occurred
						messageHeader.setText(submitService.getException().getMessage());
					} else if (newValue == State.SUCCEEDED) {
						window.getScene().getRoot().setEffect(
								ColorAdjustBuilder.create().brightness(-0.5d).build());
						Platform.runLater(createHideTask());
					}
				}
			});
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		protected Task<Void> createTask() {
			return window.isShowing() ? createHideTask() : createShowTask();
		}

		/**
		 * @return a task that will show the service {@linkplain Stage}
		 */
		protected Task<Void> createShowTask() {
			final Task<Void> showTask = new Task<Void>() {
				@Override
				protected Void call() throws Exception {
					Platform.runLater(new Runnable() {
						public void run() {
							if (hasParentSceneRoot()) {
								parent.getScene().getRoot().setEffect(
										ColorAdjustBuilder.create().brightness(-0.5d).build());
							}
							window.show();
							window.centerOnScreen();
						}
					});
					return null;
				}
			};
			showTask.stateProperty().addListener(new ChangeListener<State>() {
				@Override
				public void changed(final ObservableValue<? extends State> observable,
						final State oldValue, final State newValue) {
					if (newValue == State.FAILED || newValue == State.CANCELLED) {
						Platform.runLater(createHideTask());
					}
				}
			});
			return showTask;
		}

		/**
		 * @return a task that will hide the service {@linkplain Stage}
		 */
		protected Task<Void> createHideTask() {
			final Task<Void> closeTask = new Task<Void>() {
				@Override
				protected Void call() throws Exception {
					window.hide();
					if (hasParentSceneRoot()) {
						parent.getScene().getRoot().setEffect(origEffect);
					}
					window.getScene().getRoot().setDisable(false);
					return null;
				}
			};
			return closeTask;
		}

		/**
		 * @return true when the parent {@linkplain Stage#getScene()} has a
		 *         valid {@linkplain Scene#getRoot()}
		 */
		private boolean hasParentSceneRoot() {
			return this.parent != null && this.parent.getScene() != null
					&& this.parent.getScene().getRoot() != null;
		}

		/**
		 * Hides the dialog used in the {@linkplain Service}
		 */
		public void hide() {
			Platform.runLater(createHideTask());
		}
	}
}

Conclusion

As you can see there a numerious applications for JavaFX Service abstractions that can be used to reduce boilerplate code. Another possible useage could be a progress dialog Service that will automatically close when a ProgressIndicator has completed. That doesn’t render anything that’s much different than a normal progress bar, but what if we need some intermittent user interaction before it completes (like a wizard)? That is where using a Service would come in handy!

About these ads

About ugate
UGate is a fully open source solution for a do-it-yourself configurable indoor/outdoor security system. It's built with Arduino Uno32 (ChipKIT) + JavaFX and a number of other leading software/hardware technologies. Our goal is to provide individuals with access to the most popular hardware sensor technology used in industrial today. Our attempt is to bridge the gap between the hardware and software in an extendable and intuitive GUI interface that is free and open to the public. Visit our code page at: http://ugate.org

4 Responses to JavaFX Dialog Service

  1. Pingback: JavaFX links of the week, May 21 // JavaFX News, Demos and Insight // FX Experience

  2. Not Relevant says:

    Lots of line of code and CSS for a simple dialog. No more comments!

    • ugate says:

      True. It was intended for applications that make a lot of calls to background tasks that take a significant amount of time to process. If you are not using a lot of dialog windows in your application this would be overkill. However, if you are using a lengthy wizard or a lot of dialog windows this may actually reduce the amount of code.

  3. baikelin says:

    Thank you for sharing !!!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: