Sunday, May 2, 2010

Flex4: Show busy / loading state per component - going beyond the busy cursor

In Flex 4, the easiest way to indicate a busy or loading state for an application is to use the CursorManager's setBusyCursor() and removeBusyCursor() methods.

This is made even easier when all you need to do is set the showBusyCursor property of the SWFLoader, WebService, HttpService, and RemoteObject classes to automatically display the busy cursor.

But what if you need to go beyond that? What if there are multiple service calls that various components in your application need to make for their respective data sets? How can one go about clearly indicating which area of the application is ready for use and which one isn't?

Implementation:
1) BusyStateManager.as (+/-)
package com.xxx.util
{
   
    import flash.events.EventDispatcher;
    import flash.utils.Dictionary;
   
    import mx.collections.ArrayCollection;

    public class BusyStateManager extends EventDispatcher
    {
        /**
         * Keys for some commonly used components/areas
         */
        public static const ALL:String = "all";
        public static const HEADER:String = "header";
        public static const FOOTER:String = "footer";
        public static const DASHBOARD:String = "dashboard";

        /**
         * Use a string key to pull the component that
         * should be shown as busy. This really only
         * needs to happen IF a component wants to show
         * a busy state for a area bigger than itself.
         */
        private var availableComponents:Dictionary = new Dictionary(true);

        /**
         * Sometimes we may want to show a component/area
         * as busy but it might not have been added to stage
         * yet!
         * So the request will wait in this queue until:
         * a) either, the component is registered so that it
         *    may be marked as busy and removed from this queue
         * b) or, the busy state ends before the component is
         *    ever added to stage and thus it will be removed
         *    from the queue.
         */
        private var busyQueue:ArrayCollection = new ArrayCollection();

        [MessageHandler(selector="registerMyPersonalComponent")]
        public function registerComponent(event:BusyEvent):void {
            availableComponents[event.key] = event.component;
            // check if someone is waiting for this component to be marked as busy
            for each (var key:String in busyQueue) {
                if (key == event.key) {
                    //immediately mark the component/area as BUSY
                    showAsBusy(event.key);
                    //remove the key from the busy queue
                    busyQueue.removeItemAt(busyQueue.getItemIndex(key));
                    break;
                }
            }
        }

        public function showAsBusy(key:String):void {
            var value:Object = availableComponents[key];
            if (value != null) {
                //immediately mark the component/area as BUSY
                value.busy = true;
            } else {
                //add the key to the busy queue to be serviced later
                busyQueue.addItem(key);
            }
        }

        public function showAsReady(key:String):void {
            var value:Object = availableComponents[key];
            if (value != null) {
                //immediately mark the component/area as READY
                value.busy = false;
            } else {
                //remove the key from the busy queue
                busyQueue.removeItemAt(busyQueue.getItemIndex(key));
            }
        }
    }
}
2) Overlay for Skin (+/-)
<s:Group height="100%" width="100%" click="group1_clickHandler(event)" visible="{hostComponent.busy}">
        <s:Rect height="100%" width="100%" alpha="0">
            <s:fill>
                <s:SolidColor color="black" />
            </s:fill>
        </s:Rect>
        <ns:Image
            id="searchInProgressIcon"
            source="@Embed(source='/assets/icons/loading_spinner.swf')"
            verticalCenter="0" horizontalCenter="0"
        />
</s:Group>
3) Eat up the events in the skin (+/-)
    <fx:Script>
        <![CDATA[
            protected function group1_clickHandler(event:MouseEvent):void
            {
                event.stopImmediatePropagation();
            }
        ]]>
    </fx:Script>
4) Skin's Host Component should have the flag which toggles the overlay on & off (+/-)
[Bindable]
public var busy:Boolean = false;

How to use the code above:
1) Add a black rectangle with an alpha to indicate an overlay and the busy-spinner to the component’s skin class ... this is shown in the code.
2) Dispatch an Event from all the involved component’s view class to help them register themselves with unique names so that they may be marked as busy/ready as needed.
3) Have a wrapper around the server side action (for which the component should be busy).
4) In the wrapper class, call the BusyStateManager’s showAsBusy and showAsReady methods ... before and after making the service call. You can specifically ask for the busy or ready state to take effect on the desired components by using the name that you register them with.
5) Hopefully, you'll find it easy to plug it in and get it working for yourself as well ... good luck :)

Design:
1) Decide how you want to indicate the busy state to the user. (+/-)
1.1) You can add a s:Rect inside a s:Group at the end of each of your component skins
1.2) Assign the s:Rect an aplha (see-through) value of 0.25 to 0.75
1.3) Give the s:Group a click handler that will stop the propagation of any mouse events.
1.4) The users will feel as if there is a dark overlay or shield of sorts that is preventing them from interacting with the component when its busy.
1.5) You can always throw in some text like busy or loading and some sort of spinning graphic to polish it up a bit more.
2) Decide which service calls are made for individual components on the stage versus the ones that might provide data used by multiple components on the stage.
3) Also you may realize that it would be better to lock the entire application if certain service operations haven't finished, even if they are localized to only one of the components on the stage.
4) Often enough, the service calls might be made even before the components have been added to stage, for example when the application has just started. Even with a slow start, some components may still not have the data they need as their respective service calls might not have yet returned any results so they will need to be marked as busy whenever they are added to stage.
5) Certain service calls may need to mark the entire application or multiple components or individual ones as busy. Therefore, it is necessary for any and all components who participate in this process to register themselves with some sort of a busy state manager which can then toggle their busy state on & off for them.


0 comments:

Post a Comment