In this second part of my series on BackgroundWorkers in Smart Office Scripts, I illustrate how to disable/enable the user interface, how to indicate activity, and how to show progress, because good usability is important to me. This article is a continuation of my previous article, Part 1.
When I put a time-consuming operation in a background thread, the user interface remains available to the user and I must take special consideration when crafting the code. First, I want to disable part of the user interface so the user doesn’t start the time-consuming operation again, or that would have unexpected results. Also, I want to indicate activity so the user knows the time-consuming operation is in progress. Also, I want to show progress – 10%, 20%, 30%…100% – so the user knows where the processing is at. I address these special considerations because good usability is important to me.
How to disable/enable the user interface
In some cases, I want to disable part of the user interface so the user doesn’t start the time-consuming operation again. Starting the time-consuming operation twice could have unexpected results, for example duplicate requests to the server could result in duplicate Customer Orders in M3; it depends on what the script does.
If the BackgroundWorker was started from a button I want to disable that button. But if the script was started from a Smart Office Shortcut (Tools > Personalize > Shortcuts) I would have to implement some kind of semaphore to not start the time-consuming operation again; that code is not shown here.
Finally, after the time-consuming operation is finished I enable the button back to its former state.
Also, I can only disable/enable the user interface in the UI thread, not in the background thread as that would throw an Exception.
My basic code to disable/enable the user interface is the following (in this case I do it in the OnClick of a button, with the methods DisableUI and EnableUI to be defined):
/* UI thread */ function OnClick(sender: Object, e: RoutedEventArgs) { DisableUI(); ... } /* background thread */ function OnDoWork(sender: Object, e: DoWorkEventArgs) { ... } /* UI thread */ function OnRunWorkerCompleted(sender: Object, e: RunWorkerCompletedEventArgs) { ... EnableUI(); }
Note that if multiple BackgroundWorkers were running in parallel, I would need to enable the user interface only after the last worker has completed; that code is not shown here.
How to indicate activity
I want to indicate activity so the user knows the time-consuming operation is in progress and doesn’t start it again.
Similarly, I can only indicate activity in the UI thread, not in the background thread as that would throw an Exception.
My basic code to indicate activity is the following (with the method IndicateActivity to be defined):
/* UI thread */ public function Init(element: Object, args: Object, controller: Object, debug: Object) { IndicateActivity('start'); ... } /* background thread */ function OnDoWork(sender: Object, e: DoWorkEventArgs) { ... } /* UI thread */ function OnRunWorkerCompleted(sender: Object, e: RunWorkerCompletedEventArgs) { ... IndicateActivity('done'); }
There are multiple techniques to indicate activity.
One technique to indicate activity is to use controller.RenderEngine.ShowMessage(x) to show a message in the status bar, but messages could show in a modal pop-up window if the user checked Settings > MForms > Display system messages in dialog window:
controller.RenderEngine.ShowMessage('start'); controller.RenderEngine.ShowMessage('done');
Another technique is to show an animated icon.
Another technique is to use debug.WriteLine(x), but that works only in the Script Tool for the developer:
debug.WriteLine('start'); debug.WriteLine('done');
Another technique is to change the cursor pointer, but the cursor is a shared resource on the M3 program’s panel so the script will compete for it with other threads:
content.Cursor = Cursors.Wait; content.Cursor = Cursors.Arrow;
Another technique is to add a Label to the user interface and set it to whatever text I want.
How to show progress
I want to show progress when I have multiple time-consuming operations so the user knows where the processing is at. For example, if I was processing a shopping cart from the script (in a parallel universe where shopping carts are processed on the client-side) I would want to call various transactions from OIS100MI: first AddBatchHead, then various AddBatchLine, and finally Confirm, and I would want the user to know what the progress is. So, supposing I have N requests, I want to show the progress i of N.
Similarly, I can only show progress in the UI thread, not in the background thread as that would throw an Exception.
To show progress, I use WorkerReportsProgress, add_ProgressChanged, ReportProgress, e.ProgressPercentage, and e.UserState:
public function Init(element: Object, args: Object, controller: Object, debug: Object) { ... worker.WorkerReportsProgress = true; worker.add_ProgressChanged(OnProgressChanged); ... } function OnDoWork(sender: Object, e: DoWorkEventArgs) { var worker: BackgroundWorker = sender; worker.ReportProgress(0, 'start'); doSomethingTimeConsuming1(); worker.ReportProgress(1/n*100, 'processed 1 of N'); doSomethingTimeConsuming2(); worker.ReportProgress(2/n*100, 'processed 2 of N'); doSomethingTimeConsuming3(); worker.ReportProgress(3/n*100, 'processed 3 of N'); ... doSomethingTimeConsumingN(); worker.ReportProgress(100, 'done'); } function OnProgressChanged(sender: Object, e: ProgressChangedEventArgs) { controller.RenderEngine.ShowMessage(e.ProgressPercentage + '% ' + e.UserState); }
Final source code
Here’s my final source code illustrating how to disable/enable the user interface, how to indicate activity, and how to show progress:
import System.ComponentModel; import System.Windows; import System.Windows.Controls; import System.Windows.Input; package MForms.JScript { class Test { var controller, content: Object; var startButton: Button; /* UI thread */ public function Init(element: Object, args: Object, controller: Object, debug: Object) { this.controller = controller; this.content = controller.RenderEngine.Content; startButton = new Button(); startButton.Content = 'Start'; startButton.Width = double.NaN; Grid.SetColumn(startButton, 0); Grid.SetRow(startButton, 0); Grid.SetColumnSpan(startButton, 5); startButton.add_Click(OnClick); controller.RenderEngine.Content.Children.Add(startButton); } /* UI thread */ function OnClick(sender: Object, e: RoutedEventArgs) { // disable the user interface sender.IsEnabled = false; // sender == startButton // indicate activity controller.RenderEngine.ShowMessage('start'); content.Cursor = Cursors.Wait; // start the worker var worker = new BackgroundWorker(); worker.WorkerReportsProgress = true; worker.add_DoWork(OnDoWork); worker.add_ProgressChanged(OnProgressChanged); worker.add_RunWorkerCompleted(OnRunWorkerCompleted); worker.RunWorkerAsync(); } /* background thread */ function OnDoWork(sender: Object, e: DoWorkEventArgs) { var worker: BackgroundWorker = sender; // time-consuming operation + show progress worker.ReportProgress(0, 'start'); doSomethingTimeConsuming1(); worker.ReportProgress(25, 'processed 1 of 4'); doSomethingTimeConsuming2(); worker.ReportProgress(50, 'processed 2 of 4'); doSomethingTimeConsuming3(); worker.ReportProgress(75, 'processed 3 of 4'); doSomethingTimeConsuming4(); worker.ReportProgress(100, 'done'); } /* UI thread */ function OnProgressChanged(sender: Object, e: ProgressChangedEventArgs) { // show progress controller.RenderEngine.ShowMessage(e.ProgressPercentage + '% ' + e.UserState); } /* UI thread */ function OnRunWorkerCompleted(sender: Object, e: RunWorkerCompletedEventArgs) { // cleanup worker var worker: BackgroundWorker = sender; worker.remove_DoWork(OnDoWork); worker.remove_ProgressChanged(OnProgressChanged); worker.remove_RunWorkerCompleted(OnRunWorkerCompleted); // indicate activity content.Cursor = Cursors.Arrow; controller.RenderEngine.ShowMessage('done'); // enable the user interface startButton.IsEnabled = true; } function doSomethingTimeConsuming1() { System.Threading.Thread.Sleep(1000); } function doSomethingTimeConsuming2() { System.Threading.Thread.Sleep(2000); } function doSomethingTimeConsuming3() { System.Threading.Thread.Sleep(3000); } function doSomethingTimeConsuming4() { System.Threading.Thread.Sleep(4000); } } }
That code was tested in Smart Office 10.0.4.0.38.
Here are three mini-screenshots of the result where I disable the ‘Start’ button, I indicate activity with a message in the status bar, I show progress, and I enable the button again:
That’s it!
Conclusion
In this article I illustrated how to disable/enable the user interface, how to indicate activity, and how to show progress when using BackgroundWorkers in Smart Office Scripts because good usability is important to me.
Next
In my next article I will illustrate:
- How to handle worker cancellation
- How to handle worker errors
- How to handle exceptions
Related articles
All articles in this series:
- BackgroundWorkers in Smart Office Scripts – Part 1 – How to set input parameters, and how to receive output values
- BackgroundWorkers in Smart Office Scripts – Part 2 – How to disable/enable the user interface, how to indicate activity, and how to show progress
- BackgroundWorkers in Smart Office Scripts – Part 3 – How to handle worker cancellation
- BackgroundWorkers in Smart Office Scripts – Part 4 – How to handle worker errors and how to handle exceptions
Awesome. A must have BackgroundWorker example. Thanks Thibaud.
LikeLike
Gracias por el feedback Juan!
LikeLike