Friday, April 13, 2012

XAML App Review - Part 4: Please Wait Screen

This is a continuation of the XAML App Review. The articles in this series are collected together here: Jeremy Bytes - Downloads.

Last time, we saw the Popup Message that lets us give information to the user that doesn't require user interaction.  This time, we'll take a look at the Please Wait screen; this lets us turn asynchronous behavior into synchronous behavior.  Before we look at the implementation, let's think about why we would want to do this.

Why Have a "Please Wait" Screen?
Silverlight is asynchronous by nature.  This means that anytime we are going to our data store, we must use asynchronous calls.  This took me a while to get used to, but I understand why this decision was made.  At its core, Silverlight is a browser plug-in.  This means that if you "hang" Silverlight, you end up hanging the entire browser.  This would not make for good user experiences.  Instead, Silverlight does most of its work asynchronously and the browser remains responsive.

As developers, this means that we need to adapt the way that we have been programming to accommodate this behavior.  In the WinForms world, we primarily make synchronous calls where the applications waits for an action to complete (the exception is when we explicitly fire off an async call when the application calls for it).

In Part 3, we saw how the popup message can help us with this.  If we fire off an async call (such as to Save an object), the user can continue to use the application, and then we can inform him of the success after the call returns.  The best user experience is one where these asynchronous calls run quickly.  If they run quickly enough, then the user will not see any difference between synchronous and asynchronous behavior (because we're doing our jobs correctly).

But there are times when we can't make something run faster (such as when we are hampered by a database call we have no control over), and we need to keep the user from interacting with the application until the process is complete.

For example, let's say that we have a report that takes 30 seconds to run.  In the simple UI that was created for the application, we are simply filling a grid with data that is returned from the database (this is in the original application, not our XAML Tips replica).  We need to keep the user from doing a couple of things until the data is returned.  First, we need to keep him from clicking the "Run Report" button again; this would end up firing off another asynchronous call, and we just end up slowing everything down.  Next, we need to keep him from closing the current form since this is where the data will be displayed after it is returned.

Rather than enabling/disabling individual controls and functions (which may actually be the better answer), I created a "Please Wait" screen that prevents the user from interacting with the application until the asynchronous call returns.  This is a simple solution, but it has some pretty serious problems that need to be addressed.

"Please Wait" Screen in Action
First, let's see how the screen works.  As a reminder, you can run the application from here: Jeremy Bytes - Downloads.  If we run the application, and select "Please Wait" from the menu, we get the following screen:

This screen is here to simply show the Please Wait screen in action.  As mentioned above, it is designed to be used with asynchronous calls.  When we click the "Show Please Wait" button, we see the Please Wait message:

Notice that the message (and the semi-transparent background) covers the entire screen.  This prevents us from clicking any buttons or menu items.  Also, the "Please Wait" text is animated so that it fades in and out while the message is in place.  This (hopefully) gives the user the impression that the application is still responsive and is not hung.

The implementation is fairly straight forward.  The MainPage has the following XAML:

This is a simple border with a semi-transparent background.  Right in the middle is a text block with the "Please Wait" text.  The trick here is that the Height and Width properties on the border are each set to 1.  This means that by default, the element has almost no size.

The screen is shown or hidden with the PleaseWait method (in the MainPage.xaml.cs):

When the parameter is set to "true", the screen is shown.  This is done by setting the border's Height and Width properties so that the element covers the entire screen.  Next, it fires off the text animation that fades the message in and out.  The storyboard for the animation is in the Resources section of MainPage:

This just animates the opacity of the text from 1 (the default) to 0 (transparent).  In conjunction with the method, AutoReverse will send it from 1 to 0 and back to 1.  The RepeatBehavior tells the animation to keep going.

So, calling PleaseWait(true) will show the screen and start the animation.  Calling PleaseWait(false) will hide the screen (by setting the Height and Width back to 1) and stop the animation.  Having one call to start the "Please Wait" and a separate call to stop the "Please Wait" allows us to call the "show" part just before firing off an async call and then call the "hide" part in the callback after the async is complete.  This leads to some problems.

What if the async call does not return?  Maybe the database or webservice hangs for some reason.  If this happens, then the "Please Wait" screen will never go away, and the user has no choice but to close the application and try again.

Also, we have to be extremely careful in coding the callbacks.  We must make sure that we always call "hide".  This can make error handling a bit tricky.  If we forget to call "hide" for some reason, then the application is stuck and the user must restart it.

As a side note: if you're curious how the sample application works, when we click the button, it calls PleaseWait(true) and then starts a timer that is set to the number of seconds in the text box.  When the timer fires, it calls PleaseWait(false).  Again, in the real world, there would be some sort of asynchronous call in here rather than a timer.  This is just so that we can see the screen in operation.

This is a very simple implementation.  There is not much code required for it.  Also, it works with any screen that we have (since it covers the entire application).  For the simple selection and mapping application where this screen was used, it worked fine.  The only screen it is used on is an output/reporting screen, so there is no danger of data loss if the application does hang up for some reason.

The code is very unsafe.  If the "hide" call is never made, then the application is stuck; there is no way to get rid of the screen.  You can simulate this by setting the duration to a really long time in the sample implementation.

I would not recommend re-using this code in its current state.  The issues are just too big.  A more useful implementation would be to disable just the controls that we are concerned about.  For example, if we do not want someone to click a "Submit" button more than once, we should disable that control.  If we do not want someone to navigate to another screen, then we should disable the menu.  If we were to use the MVVM (Model-View-ViewModel) pattern, then we could come up with some creative ways of handling the disabling/enabling through databinding.  (As noted last time, we'll be talking a bit about MVVM in Part 5).

If we still need the "Please Wait" message, we can come up with a more robust solution by setting up an event with a callback.  This would allow us to hook the "hide" into the callback of the asynchronous call.  By doing this, we can be more confident that the "hide" method will get fired appropriately.

Next Time
This time, we saw an implementation of a Please Wait screen that allows us to make asynchronous calls behave like synchronous calls.  As noted, I would not use this implementation again; there are just too many ways for it to go wrong.

You've probably noticed by now that the sample application does not use the MVVM pattern.  Next time, we'll talk about why we might want to use MVVM and (specifically) why it was not used for this particular application.  We'll also summarize what we've seen and this will hopefully let us take the good with our next application, improve the bad, and just get rid of the ugly.

Happy Coding!

No comments:

Post a Comment