I am trying to set up a background service that would perform bulk loading of transaction data from a csv file. This background service would be initiated from a menu item action mapped to a method in the controller/presenter class.
Ever so often, some data turns up in the csv file for which no master data can be found in the database, this would normally cause the upload to choke and fail.
On such occasions, I would like to be able to have the background service pause its processing and invoke a dialog from a presenter class to take in user input. The user input would be used to add a master row in the database, after which the background service should resume from where it had left off (not from the beginning of the csv file, but from the row which caused the error).
Is this possible to achieve in JavaFX, perhaps with the javafx.concurrent API? How would I go about doing this?
Solution
When your background process encounters a situation where it requires a user to be prompted for input, use FutureTask executed in Platform.runLater to showAndWait the dialog prompt on the JavaFX application thread. In the background process use futureTask.get to pause the background process until the user has input the necessary values which will allow the process to continue.
Sample Code Snippet
Here is the essence of code for this approach which can be placed inside the call method of your background process:
String nextText = readLineFromSource();
if ("MISSING".equals(nextText)) {
updateMessage("Prompting for missing text");
FutureTask<String> futureTask = new FutureTask(
new MissingTextPrompt()
);
Platform.runLater(futureTask);
nextText = futureTask.get();
}
...
class MissingTextPrompt implements Callable<String> {
private TextField textField;
#Override public String call() throws Exception {
final Stage dialog = new Stage();
dialog.setScene(createDialogScene());
dialog.showAndWait();
return textField.getText();
}
...
}
Sample Application
I created a small, complete sample application to demonstrate this approach.
The output of the sample application is:
Sample Output Explanation
Lines read without missing values are just plain brown.
Lines with a prompt value entered have a pale green background.
Fourteen lines have been read, the background task has already paused once at the 6th line which was missing a value. The user was prompted for the missing value (to which the user entered xyzzy), then the process continued until line 14 which is also missing and the background task is again paused and another prompt dialog is being displayed.
Related
I'm facing quite simple, but general problem.
Problem setup
I have JavaFX application with main window (main stage). This stage works with some (keep it general) data. The data may be somehow loaded at the startup (for instance from server, database, XML file). The source of data (server adress, database name, XML file path) are to be prompted from user. (Keep in mind, that user can cancel the input and the data won't be loaded.)
Since I would like to show the stage up for the user (to get acquainted with the app and possibly browse the help) and then, by menu File -> Load data, let him to load the data.
But since the stage depends mostly on the data, I need to disable most of the controls until the user properly loads the data and makes the stage useable.
Question
Here arises dilema I cannot solve. AFAIK, I have two options, how to make most of stage disabled until the data are properly ready:
Bind disabled with "or data not loaded"
The more JavaFX-binding-friendly solution is to have boolean property like dataNotReadyProperty and then, when the controller is beeing initialized use things like:
private void init() { // call from initialize method
// ....
doFooButton.disableProperty().bind(
Bindings.or(
dataProperty().get().cannotBeFooDoneProperty(),
dataNotReadyProperty()
)
);
// ....
}
As you can see, this code is quite ugly and long (especialy when some controls' disability is given by additional or or and).
Disable and bind when data ready
The second option is to simply disable everything at startup (or even within the FXML). And also set up change listener for the dataProperty and when the data is loaded, do the proper initialization of bindings. Written in code:
private void init() { // call from initialize method
// ....
doFooButton.setDisabled(true);
// ....
dataProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue == null && newValue != null) {
runInitialization();
}
});
}
private void runInitialization() {
// ....
doFooButton.disableProperty().bind(
dataProperty().get().cannotBeFooDoneProperty());
// ....
}
This solution splits initialization into two, but the "real" initialization here is kept simple and understandable. Also, I am forced to modify each control twice.
Conclusions
What would you prefer? Do you have another suggestions, how to solve this situation?
Additional info
the stage does need any more initialization than the bindings of disabled properties
once the data is loaded, it cannot be "unloaded" (it can be only reloaded by other one)
there is approx. 20 controls to be disabled on startup and 4 to be accessible.
Finally, i decided to use solution base on the #ItachiUchiha's answer.
I've created two different Panes, one for proper work with data, another one with just label like: "Load data to start work" and load button.
The remaining (just about 5 of them) controls i disabled/enabled by bindings (option 1). So the amount of "ugly" code decreased rapidly.
This is a rather odd one. I am using a Swing button to launch a scan of a list of files. Because I want it to display updates on the status bar, I am using a Thread. Since Swing won't let anything draw until the button's code has finished, I am also using a Tread to allow me to change the 'Start Scan' button to a 'Stop Scan' button.
The problem is that if the wait cursor is placed over any other components, during the scan, the status messages are also being written onto those components, such as buttons (see sample button below code), check boxes, etc; which messes up the interface. Is this a major bug or is it not a good idea to do what I am doing? Is there a way around it?
private void jButton47ActionPerformed(java.awt.event.ActionEvent evt)
{
// Scan folders button.
//
this.getFrame().setCursor(new Cursor(Cursor.WAIT_CURSOR));
// If button is in stop mode then...
if (collection.isScanContinue())
{
collection.setScanContinue(false);
jButton47.setText(" Scan Folders For Files ");
jButton47.setBackground(view.getDefaultButtonCol());
}
else // in scan mode...
{
Thread t = new Thread(new Runnable()
{
#Override
public void run()
{
// Setup the stop scan process button (changes the scan button to a stop button).
//
collection.setScanContinue(true);
jButton47.setText(" Stop Scanning Folders ");
jButton47.setBackground(collection.getPrefs().getDeleteCol());
collection.scanSourceAndTargetFolders();
if(collection.isScanContinue())
{
// do scan
}
// Reset the stop scan button and flag.
//
collection.setScanContinue(false);
jButton47.setText(" Scan Folders For Files ");
jButton47.setToolTipText("Scans Source and, if required, Target folders.");
jButton47.setBackground(view.getDefaultButtonCol());
view.getFrame().setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
}
});
t.start();
}
}
It cleans up fine if I re-validate the main frame, but it looks terrible during the file scan.
Any action involving swing, such as changing button text etc, should be performed on event dispatch thread using SwingUtilities.invokeLater. Otherwise, you'll run into concurrency issues like you see here. See this question for more details about how event thread works: Java Event-Dispatching Thread explanation
Also, for doing background tasks like this, Swing provides a handy utility called SwingWorker: http://docs.oracle.com/javase/tutorial/uiswing/concurrency/worker.html
I'm using Eclipse RAP and have the following use case:
enter search text in a Text field and start search
change a Label text to "searching...."
do actual search (async) and display result in a Table
The problem, even though the label should change to "searching...." before the actual search is started, it is changed to "searching...." after the search is done.
What I'm looking for is a way to push/force/update the current UI state to the client after the label changed, prior to searching:
enter search text in a Text field and start search
change a Label text to "searching...."
push current UI state to client
do actual search (async) and display result in a Table
Here some sample code:
Label statusLabel = new Label(parent, SWT.NONE);
Text searchText = new Text(parent, SWT.SEARCH);
searchText.addSelectionListener(new SelectionAdapter() {
#Override
public void widgetDefaultSelected(SelectionEvent e) {
// change label
statusLabel.setText("searching...");
// HERE force client update
// start searching
Display.getCurrent().asyncExec(new Runnable() {
#Override
public void run() {
// do actual search
}
});
}
});
asyncExec still runs the code in the UI thread it just delays it slightly until the next call to Display.readAndDispatch. So your search code will still block and isn't allowing the label to update.
You need to actually run the search in a separate thread.
asyncExec is intended to be used in a background thread to run a small amount of code in the UI thread when possible. (You need to use Display.getDefault() rather than Display.getCurrent() in a background thread).
So in your background thread you do something like:
while (more to do)
{
.... do a step of the search
Display.getDefault().asyncExec(.... UI update code ....);
}
I want to save a file before closing my JavaFX application.
This is how I'm setting up the handler in Main::start:
primaryStage.setOnCloseRequest(event -> {
System.out.println("Stage is closing");
// Save file
});
And the controller calling Stage::close when a button is pressed:
#FXML
public void exitApplication(ActionEvent event) {
((Stage)rootPane.getScene().getWindow()).close();
}
If I close the window clicking the red X on the window border (the normal way) then I get the output message "Stage is closing", which is the desired behavior.
However, when calling Controller::exitApplication the application closes without invoking the handler (there's no output).
How can I make the controller use the handler I've added to primaryStage?
If you have a look at the life-cycle of the Application class:
The JavaFX runtime does the following, in order, whenever an
application is launched:
Constructs an instance of the specified Application class
Calls the init() method
Calls the start(javafx.stage.Stage) method
Waits for the application to finish, which happens when either of the following occur:
the application calls Platform.exit()
the last window has been closed and the implicitExit attribute on Platform is true
Calls the stop() method
This means you can call Platform.exit() on your controller:
#FXML
public void exitApplication(ActionEvent event) {
Platform.exit();
}
as long as you override the stop() method on the main class to save the file.
#Override
public void stop(){
System.out.println("Stage is closing");
// Save file
}
As you can see, by using stop() you don't need to listen to close requests to save the file anymore (though you can do it if you want to prevent window closing).
Suppose you want to ask the user if he want to exit the application without saving the work. If the user choose no, you cannot avoid the application to close within the stop method. In this case you should add an EventFilter to your window for an WINDOW_CLOSE_REQUEST event.
In your start method add this code to detect the event:
(Note that calling Platform.exit(); doesn't fire the WindowEvent.WINDOW_CLOSE_REQUEST event, see below to know how to fire the event manually from a custom button)
// *** Only for Java >= 8 ****
// ==== This code detects when an user want to close the application either with
// ==== the default OS close button or with a custom close button ====
primaryStage.getScene().getWindow().addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, this::closeWindowEvent);
Then add your custom logic. In my example i use an Alert popup to ask the user if he/she want to close the application without saving.
private void closeWindowEvent(WindowEvent event) {
System.out.println("Window close request ...");
if(storageModel.dataSetChanged()) { // if the dataset has changed, alert the user with a popup
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.getButtonTypes().remove(ButtonType.OK);
alert.getButtonTypes().add(ButtonType.CANCEL);
alert.getButtonTypes().add(ButtonType.YES);
alert.setTitle("Quit application");
alert.setContentText(String.format("Close without saving?"));
alert.initOwner(primaryStage.getOwner());
Optional<ButtonType> res = alert.showAndWait();
if(res.isPresent()) {
if(res.get().equals(ButtonType.CANCEL))
event.consume();
}
}
}
The event.consume() method prevents the application from closing. Obviously you should add at least a button that permit the user to close the application to avoid the force close application by the user, that in some cases can corrupt data.
Lastly, if you have to fire the event from a custom close button, you can use this :
Window window = Main.getPrimaryStage() // Get the primary stage from your Application class
.getScene()
.getWindow();
window.fireEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSE_REQUEST));
Ahh this is a known bug in JavaFX where the Stage will not close if a modal dialog is present at the time of closing. I will link you to the bug report which I just saw today. I think it is fixed in the latest release.
Here you go:
https://bugs.openjdk.java.net/browse/JDK-8093147?jql=text%20~%20%22javafx%20re-entrant%22
resolved in 8.4 it says. I think this what you are describing.
public Stage getParentStage() {
return (Stage) getFxmlNode().getScene().getWindow();
}
btnCancel.setOnAction(e -> {
getParentStage().close();
});
I have written following code.I have not shown full source but psudo code.
class UI extends JFrame
{
//created UI with one Button
onButtonclick()
{
//did some operation before set icon to button
//say opened fileopen dialog and get file
button.setText("");
ImageIcon progressbar = new
ImageIcon(DatasetExporterUI.class.getResource("/progreassbar.gif"));
buttonExport.setIcon(progressbar);
// did some database operations
//again removed icon from button
button.setIcon(null);
button.setText("click");
}
}
When I click on button It opens file open dialog and and button text get set to empty.
But It doesn't set Icon to button.When all Database operation are done which are performed after Icon set to button that time Icon is appeared on button.
Why this behavior is?
How to set Icon to button and do some Database operations and again remove it?
Thank you. :)
The GUI system can only do one thing at a time, like most code (except for code that uses threads). Calling your listener is a thing. The GUI system cannot do anything else while your listener is running.
Your database operation needs to run on another thread (which you can create) and then update the GUI when it's done. Something like this:
void onButtonPressed() {
// The code to open the file dialog goes here
button.setText("");
ImageIcon progressbar = new
ImageIcon(DatasetExporterUI.class.getResource("/progreassbar.gif"));
buttonExport.setIcon(progressbar);
new Thread() {
#Override
public void run() {
// do some database operations here
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
//again remove icon from button
button.setIcon(null);
button.setText("click");
}
});
}
}.start();
}
Code in different threads runs at the same time. This is convenient but dangerous. Be extremely careful when accessing data from the new thread - if one thread changes a field and the other thread reads it, the results might not be what you expect. The simplest thing to do is to make sure the main thread doesn't change any variables used by the new thread while it's running.
When your database operations are finished, you can't set the button back to normal by just calling setText. Only the main thread is allowed to affect the GUI - what if the main thread was drawing the button on the screen at the same time the database operation thread was changing the text? The button might be drawn incorrectly. So you need to call EventQueue.invokeLater which tells the GUI system to run your code in the near future when it's not busy. The code inside new Runnable() {} is like the code in the button listener - no other GUI-related code will run while it does.
This should work:
Image progressbar= ImageIO.read(DatasetExporterUI.class.getResource("/progreassbar.gif"));
buttonExport.setIcon(new ImageIcon(progressbar));