Open a dialog or do UI updates in the onPreCommit method

There is some heavy operation going on in a PreCommitEvent method and I want to show an indeterminate progress dialog. The logic sometimes require the UI thread so a background work dialog is not ideal.

What I have tried:

    @Subscribe(target = Target.DATA_CONTEXT)
    private fun onPreCommit(event: DataContext.PreCommitEvent) {
        screens.create(IndeterminateProgressDialog::class.java).also {
            it.messageLabel.value = messageBundle.formatMessage("creatingSettlementInvoiceFor", employee.getFullName())
        }.show()
// long lasting code follows ...

But the IndeterminateProgressDialog is never shown. I probably lack some understanding when UI updates will be processed…

Also something like…

    @Subscribe(target = Target.DATA_CONTEXT)
    private fun onPreCommit(event: DataContext.PreCommitEvent) {
        mainBox.isVisible = false
        loadingBox.isVisible = true
...

won’t work.

You cannot interact with UI in the PreCommitEvent - it’s too late in the request lifecycle.

You have to pause the commit process, show a dialog and run the task, then resume the process. There are two options for this.

The first one is to use BeforeCommitChangesEvent and its preventCommit() and resume() methods:

@UiController("Customer.edit")
@UiDescriptor("customer-edit.xml")
@EditedEntityContainer("customerDc")
public class CustomerEdit extends StandardEditor<Customer> {

    @Autowired
    private Dialogs dialogs;

    @Subscribe
    public void onBeforeCommitChanges(BeforeCommitChangesEvent event) {
        dialogs.createBackgroundWorkDialog(this, new LongTask(event))
                .withCaption("Please wait...")
                .withMessage("Saving data...")
                .show();
        event.preventCommit();
    }

    private class LongTask extends BackgroundTask<Integer, Void> {

        private BeforeCommitChangesEvent event;

        protected LongTask(BeforeCommitChangesEvent event) {
            super(100, CustomerEdit.this);
            this.event = event;
        }

        @Override
        public Void run(TaskLifeCycle<Integer> taskLifeCycle) throws Exception {
            Thread.sleep(3000);
            return null;
        }

        @Override
        public void done(Void result) {
            event.resume();
        }
    }
}

Another option is to override the closeWithCommit() method of the editor screen controller and return OperationResult.fail() from it, then commit and close when the background operation is done:

@UiController("Customer.edit")
@UiDescriptor("customer-edit.xml")
@EditedEntityContainer("customerDc")
public class CustomerEdit extends StandardEditor<Customer> {

    @Autowired
    private Dialogs dialogs;

    @Override
    public OperationResult closeWithCommit() {
        dialogs.createBackgroundWorkDialog(this, new LongTask())
                .withCaption("Please wait...")
                .withMessage("Saving data...")
                .show();
        return OperationResult.fail();
    }

    private class LongTask extends BackgroundTask<Integer, Void> {

        protected LongTask() {
            super(100, CustomerEdit.this);
        }

        @Override
        public Void run(TaskLifeCycle<Integer> taskLifeCycle) throws Exception {
            Thread.sleep(3000);
            return null;
        }

        @Override
        public void done(Void result) {
            CustomerEdit.this.commitChanges()
                    .then(() -> CustomerEdit.this.close(StandardOutcome.COMMIT));
        }
    }
}

After a while, I realized that I didn’t actually answered your question. You want to interact with UI in your long task, so if you use a background task, it’s quite difficult - you can do it only in progress() and done() methods.

So we need to interrupt the commit process, show the progress bar and then resume the process somehow in a different client request. I think the latter part can be done with the Timer facet:

<facets>
    <timer id="timer" delay="1000"/>
@UiController("Customer.edit")
@UiDescriptor("customer-edit.xml")
@EditedEntityContainer("customerDc")
public class CustomerEdit extends StandardEditor<Customer> {

    @Autowired
    private ProgressBar progressBar;
    @Autowired
    private Timer timer;

    private BeforeCommitChangesEvent beforeCommitChangesEvent;

    @Subscribe
    public void onBeforeCommitChanges(BeforeCommitChangesEvent event) {
        this.beforeCommitChangesEvent = event;
        progressBar.setVisible(true);
        timer.start();
        event.preventCommit();
    }

    @Subscribe("timer")
    public void onTimerTimerAction(Timer.TimerActionEvent event) {
        timer.stop();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            //
        }
        beforeCommitChangesEvent.resume();
    }
}

Thanks @krivopustov , do you maybe have more documentation for the request lifecycle? I’m using CUBA and Jmix now for quite some time, but I still don’t know when exactly the UI gets refreshed. I guess this information is somewhere in the Vaadin docs or am I missing something in the Jmix docs?

We’ll do :slight_smile:

The rule of thumb is:
For any client request, e.g. clicking OK button in an editor, the user will see changes in UI only when all backend Java code is executed (unless you use background tasks).

That’s why you didn’t see your ProgressBar - it was created on the backend, but when the request was completed, the screen which contained the ProgressBar was already closed. If you rendered the component outside the editor screen (e.g. on the main screen), you would see it after your long-running task was completed.

1 Like