← About Jeff

Creating a global ProgressIndicator experience using the Windows Phone 7.1 SDK Beta 2

July 7, 2011

One of the nice new features in the 7.1 release of Windows Phone for developers is ProgressIndicator support right within the app platform, removing the need in many situations for the Silverlight-based PerformanceProgressBar control that I created some time ago.

(The PerformanceProgressBar was a decent performance improvement over the stock ProgressBar in the first release of the OS, but offloading the control to the OS improves performance even more. As many people pointed out, the rich visualization wasn’t free.)

So my initial thought was that I could use the new ProgressIndicator control and have a single instance to use throughout my app. Not so fast! Unfortunately (in my mind) the ProgressIndicator property is a SystemTray property and it’s set on every single page instance.

In my applications, I prefer to wire all key events (data downloads, long-running tasks, etc.) to a single, centralized ‘global loading’ control, removing the need to have more than a single progress bar instance in the app.

So in this post quickly we’ll:

  • See how to hook up the GlobalLoading when your app starts up
  • Note that the navigation framework’s events are used to auto-hookup the same shared progress indicator instance to every page
  • See how to use the IsLoading setter to turn on and off the loading experience

My old & new strategies

In my 7.0 apps, I would re-template the root phone application frame and have the single indeterminate progress bar in there, so it’s always available.

In 7.1, my goal is to be able to re-purpose my existing global loading system. I want to create a single instance of the new ProgressIndicator type, and then automatically have it set to every single page’s appropriate system tray property automatically. This will eliminate any manual hook-up required, so it will just always be present and work.

Initializing the global loading system at startup

In order to always have the progress indicator visible, I need to make sure it’s always set when a page navigation happens, sharing that single instance. Let’s use the Navigated event of the PhoneApplicationFrame that is created in App.xaml.cs.

  • Open up your 7.1 app’s App.xaml.cs
  • Expand the “Phone application initialization” region in the editor
  • At line ~120 (in a new project), where the RootFrame is set, add the initialize call.
GlobalLoading.Instance.Initialize(RootFrame);

Hooking up to the Navigated event

Now, you’ll see that inside the global loading system implementation below, I’m using the Navigated event to set the ProgressIndicator property with every complete navigation:

private void OnRootFrameNavigated(object sender, NavigationEventArgs e)
{
    // Use in Mango to share a single progress indicator instance.
    var ee = e.Content;
    var pp = ee as PhoneApplicationPage;
    if (pp != null)
    {
        pp.SetValue(SystemTray.ProgressIndicatorProperty, _mangoIndicator);
    }
}

Nothing for you to do other than include the GlobalLoading.cs file in your project.

Using the global loading system

Now, in my app, I have two ways of providing data to this global loader, one of which is pretty automatic:

  • AgFx: I hook up to the DataManager’s property change event, looking for changes in its own central IsLoading property
  • You can set GlobalLoading.Instance.IsLoading to true or false

Note that setting IsLoading to true increments an internal counter, and setting to false decrements; the idea is that you match operations to results: if you start a web browser navigation and set IsLoaded to True, make sure that when the navigation is complete or canceled, you set it to False.

However, if multiple requests are happening at once, the loading indication will not stop until all requests have completed.

Now, one of the downfalls of using simple integer arithmetic here is that you could potentially forget to increment or decrement, leaving the indicator always going, and likely keeping your app from making it through the marketplace ingestion requirements.

A better solution might be to request and use “tokens” or some sort of instance that could either timeout or be used to debug your app’s logic; it would also allow you to use the new progress indicator’s text property to provide information about loading operations (“Currently loading 2 things…”, “Refreshing your places list”), which is more interesting.

The cool Rowi app does this.

Data binding or not

In my 7.0 implementation, I actually directly data bind a central PerformanceProgressBar’s IsLoading property to the GlobalLoading.Instance.ActualIsLoading property; in 7.1, I decided for simplicity to instead just directly set the progress indicator’s values, but I maybe could have data bound to the indicator. Let me know if you try that and have any issues!

Here’s the source to GlobalLoading.cs. I’ve commented out the AgFx parts in case you’re not using that system.

GlobalLoading.cs

using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
// using AgFx;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;

namespace JeffWilcox.FourthAndMayor
{
    public class GlobalLoading : INotifyPropertyChanged
    {
        private ProgressIndicator _mangoIndicator;

        private GlobalLoading()
        {
        }

        public void Initialize(PhoneApplicationFrame frame)
        {
            // If using AgFx:
            // DataManager.Current.PropertyChanged += OnDataManagerPropertyChanged;

            _mangoIndicator = new ProgressIndicator();

            frame.Navigated += OnRootFrameNavigated;
        }

        private void OnRootFrameNavigated(object sender, NavigationEventArgs e)
        {
            // Use in Mango to share a single progress indicator instance.
            var ee = e.Content;
            var pp = ee as PhoneApplicationPage;
            if (pp != null)
            {
                pp.SetValue(SystemTray.ProgressIndicatorProperty, _mangoIndicator);
            }
        }

        private void OnDataManagerPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if ("IsLoading" == e.PropertyName)
            {
                // if AgFx: IsDataManagerLoading = DataManager.Current.IsLoading;
                NotifyValueChanged();
            }
        }

        private static GlobalLoading _in;
        public static GlobalLoading Instance
        {
            get
            {
                if (_in == null)
                {
                    _in = new GlobalLoading();
                }

                return _in;
            }
        }

        public bool IsDataManagerLoading { get; set; }

        public bool ActualIsLoading
        {
            get
            {
                return IsLoading || IsDataManagerLoading;
            }
        }

        private int _loadingCount;

        public bool IsLoading
        {
            get
            {
                return _loadingCount > 0;
            }
            set
            {
                bool loading = IsLoading;
                if (value)
                {
                    ++_loadingCount;
                }
                else
                {
                    --_loadingCount;
                }

                NotifyValueChanged();
            }
        }

        private void NotifyValueChanged()
        {
            if (_mangoIndicator != null)
            {
                _mangoIndicator.IsIndeterminate = _loadingCount > 0 || IsDataManagerLoading;

                // for now, just make sure it's always visible.
                if (_mangoIndicator.IsVisible == false)
                {
                    _mangoIndicator.IsVisible = true;
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void RaisePropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

Hope this helps!