Preface
Some of you might know the BusyIndicator
from the WPF Toolkit or from Silverlight.
I was using this control in Silverlight to disable user interaction while the application was performing some long running operations. This makes it really easy to make sure the user will not start a new task while another has not yet finished. And – very important – it does not block the UI thread, so the application does not seem to be unresponsive to the operation system.
Microsoft does not offer such a control for Windows Store apps. But even in a Windows Store app I needed to make sure the user ‘waits’ for the completion of a task, before a new is started. And I do not wanted to implement this by adding a flag telling no new task can be started (and/or disable controls based on that flag, …).
So what I wanted was a control that meets the following requirements:
-
Disable user interaction (‘lock’ the app)
Give visual feedback to let the user know interaction is disabled
Do not block the UI thread
Show some progress indication
Enable the user to cancel the operation
Disable User Interaction
Disabling the user to interact with the app is quite simple. Rob Caplan gave me the initial idea in his answer on how one might implement a modal popup. He suggested to use a full screen popup with a similar look to the MessageDialog
.
Creating a full screen popup, or better say a popup having the current size of the app, and setting the focus to that popup, no interaction with the app is possible any more. No matter if the user clicks, taps, or uses the keyboard or pen.
The first requirement is fulfilled.
Give Visual Feedback
Depending on how the full screen popup is implemented, the user might not recognize that the interaction is disabled.
Looking at MessageDialog
, one might notice that the area that is not covered by the dialog itself is dimmed out.
To achieve this, a UserControl
is implemented. In the sample code, this user control is named BusyIndicatingDialog
and can be found in UI/Xaml/Popups.
BusyIndicatingDialog
contains one Grid
without any content. This Grid
has a black background and an opacity of 0.4.
Setting an instance of BusyIndicatingDialog
as the child of the Popup
, the app’s window gets dimmed out when the Popup
is opened. The second requirement is fulfilled.
Do Not Block the UI Thread
Because common UI controls are used, this requirement can be fulfilled without any additional effort.
Of course, one has to take care that the operation to be performed is non-blocking. Means it should be implemented in an asynchronous way. But this is independent from the implementation of the BusyIndicatingDialog
.
Show Some Progress Indication
Now, the user sees that the interaction is disabled. But there is no information about what is going on. This might lead to the impression that the app has stopped working.
To show that there is something going on in the background, BusyIndicatingDialog
contains a second Grid
. It is something like a modal dialog, showing the current processing state. It contains some TextBlock
s and a ProgressBar
.
These controls are bound to a view model, implemented by BusyIndicatingViewModel
. This class can be found in ViewModels.
BusyIndicatingViewModel
implements an event handler, OnItemProcessed
. The class that implements the long-running operation, MainPage
in this sample, needs to fire an appropriate event every time an item is processed (or when an update of the progress should be displayed). This event is implemented by MainPage.ItemProcessed
.
And because BusyIndicatingViewModel
implements the INotifyPropertyChanged
interface (via its base class BindableBase
), setting the properties in OnItemProcessed
updates all the controls of the dialog.
Cancel the Operation
So far, we are able to stop user interaction with the app, give visual feedback on this, do not block the UI thread, and show how the operation proceeds. But maybe the user wants to cancel that operation, for whatever reason.
To enable the user to do this, a cancel button is added to the BusyIndicatingDialog
. Pressing this button, the Cancel
event is fired by the BusyIndicatingViewModel
. This is implemented by binding the Command
property of the Button
control to the CancelOperationCommand
of the view model.
Of course, just firing an event does not stop anything at all. The class that implements the long-running operation, MainPage
in this sample, has to register an event handler on this event. The event handler (OnCancelOperation
) sets a flag. This flag is evaluated on each iteration of the long-running operation. When it is set, the operation stops.
This feature makes the BusyIndicatingDialog
ready to use.
Creating the Busy-Indicating Dialog
The following code snippet shows all the steps that needs to be done to show the busy-indicating dialog.
// Implemented by BusyIndicatingDialog public static BusyIndicatingDialog LockScreen ( int numberOfItemsToProcess ) { // Create a popup with the size of the app's window. Popup popup = new Popup() { Height = Window.Current.Bounds.Height, IsLightDismissEnabled = false, Width = Window.Current.Bounds.Width }; // Create the busy-indicating dialog as a child, // having the same size as the app. BusyIndicatingDialog dialog = new BusyIndicatingDialog(numberOfItemsToProcess) { Height = popup.Height, Width = popup.Width }; // Set the child of the popop popup.Child = dialog; // Postion the popup to the upper left corner popup.SetValue(Canvas.LeftProperty, 0); popup.SetValue(Canvas.TopProperty, 0); // Open it. popup.IsOpen = true; // Set the focus to the dialog dialog.Focus(FocusState.Programmatic); // Return the dialog return (dialog); }
Prepare the Long Running Operation
Because there are several steps to be done before the long running operation can start, this is encapsulated in a separate method.
// Implemented by MainPage private BusyIndicatingDialog PrepareLongRunningOperation ( int numberOfItemsToProcess ) { // Disable the app bar BottomAppBar.IsEnabled = false; // Lock the screen BusyIndicatingDialog indicatorDialog = BusyIndicatingDialog.LockScreen(numberOfItemsToProcess); // Set the view model as the event handler for processed items ItemProcessed += indicatorDialog.ViewModel.OnItemProcessed; // Set the handler for the cancel event. indicatorDialog.ViewModel.Cancel += OnCancelOperation; // Reset the flag for canceling the operation. CancelOperation = false; // return the dialog return (indicatorDialog); }
Please notice the fact that the app bar is disabled explicitly. The popup does not ‘block’ the app bar.
Cleanup
When the long running operation has finished, some cleanup is required.
// Implemented by MainPage private void CleanUpLongRunningOperation ( BusyIndicatingDialog indicatorDialog ) { // Operation has finished => deregister the event handler // so the garbage collector can release the dialog ItemProcessed -= indicatorDialog.ViewModel.OnItemProcessed; // close the dialog indicatorDialog.Close(); // Enable the app bar BottomAppBar.IsEnabled = true; }
Additional Considerations
I consider this implementation to be useful in cases where many items (or tasks) should be processed sequentially, while the processing of each single item does not take too long. Having one task that takes very long, no progress can be displayed and no cancelation is possible. If you have to cover such a scenario, I think you need to find another solution.
One thing really important is to give the UI thread a chance to update the dialog control and react on user input. To do so, you should add an await Task.Delay(1);
between the processing of each item. Otherwise, in case the processing of the single item is really fast, the dialog might not be updated and(!) pressing the cancel button does not have any effect.
Consider Windows Store App Lifecycle
In the article “The Windows Store App Lifecycle“, Rachel Appel points out that “Windows 8 is changing how and when applications run”. In fact, it does!
Unlike desktop applications, Windows 8 suspends Windows Store apps (after a few seconds) when the user switches to another app. No matter what the app is currently doing. You cannot keep the app ‘alive’.
One should be aware of this. It means the user has to wait for long running operation to complete, and should not switch to another app meanwhile. This is kind of ‘blocking’ the device. Keeping this in mind, one should think twice before using Windows Store apps to execute long running operations, e.g. copy gigabytes of data.
Yes, there are background tasks available. But you cannot trigger them from within your app. So the usage is limited.
The Sample
Links
Forum question Modal popup
Simple and Generic ICommand Implementation for Usage in XAML
The Windows Store App Lifecycle
Forum question ‘Trigger background task manually‘