Monday, July 26, 2010

Create a Win Phone 7 Busy Service with Loady animation

Update: It looks like there may be a problem with wrapping the RootFrame in a Grid, but I haven't found a good answer to why.  Check this Win Phone 7 Forum Post about Changing the RootVisual.



Update 2: I've updated my example to not require changing the RootVisual.  But you will still need to have a Grid as the Root of your Page.  Check out the Updated Busy Service.



I've been spending a lot of time working on data driven Win Phone 7 apps in the past couple weeks and one of the most useful things I've created is a simple Busy Service for Win Phone 7 apps.


The Busy Service lets you signal that something is busy, like when retrieving data from a web service or doing a long running calculation. Here is a look at it's interface;

/// <summary>
/// An interface for busy services.
/// </summary>
public interface IBusyService
{
bool IsBusyShown { get; }

void ShowBusy(string message);
void ShowBusy(TimeSpan timeToShow, string message);
void ShowBusy(double milliSecondsToShow, string message);

void HideBusy(double millisecondsToWait);
void HideBusy(TimeSpan delay);
void HideBusy();
}



I decided to implement a RootPanelBusyService, which basically means that it expects a "Panel" type of element, a Grid for example, at the RootVisual. The problem with the Win Phone 7 Beta update is that the RootVisual is no longer defined in the App.Xaml, so it would seem like you can't just wrap the Root level Phone Application Frame in a Grid. It turns out, it just takes a little more work, if you open up the App.xaml.cs and take a look at the CompleteInitializePhoneApplication method you will see where the RootVisual is assigned. So with a little code we can add a Grid wrapper around the PhoneApplicationFrame and have a nice place to add our Busy Loady icon.

// Do not add any additional code to this method
private void CompleteInitializePhoneApplication(object sender, NavigationEventArgs e)
{
// Set the root visual to allow the application to render
if (RootVisual != RootFrame)
{
// Add a Grid wrapper around the Root application frame.
var root = new Grid();
root.Children.Add(RootFrame);

RootVisual = root;
}

// Remove this handler since it is no longer needed
RootFrame.Navigated -= CompleteInitializePhoneApplication;
}



Now that we have our Grid at the Root, we can instantiate a RootPanelBusyService and pass it to our ViewModel to start "Getting Busy...".

// From MainPage.xaml.cs
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);

if (App.Current.RootVisual is Grid)
{
var busyServ = new RootPanelBusyService(App.Current.RootVisual as Grid);
this.DataContext = new MainPageVM(busyServ);
}
}

// In our MainPage View Model (MainPageVM.cs)
/// <summary>
/// A view model for the Main Page.
/// </summary>
public class MainPageVM : ViewModelBase
{
/// <summary>
/// The busy service to use...
/// </summary>
private IBusyService busyService;

/// <summary>
/// Gets or sets the do busy command.
/// </summary>
/// <value>The do busy command.</value>
public ICommand DoBusy { get; private set; }

public MainPageVM(IBusyService busyServ)
{
busyService = busyServ;

DoBusy = new DelegateCommand&lt;object>(HandleDoBusy);
}

/// <summary>
/// Handles the do busy command.
/// </summary>
/// <param name="arg">Unused.
private void HandleDoBusy(object arg)
{
busyService.ShowBusy(2000, "Getting Busy...");
}
}


And there you have it, you've got a simple and easy to use busy service for your long running operations. One other trick that I occasionally use it to set a Busy Service static property on my App class, so all ViewModels have easy access to it. To do this, just add a static property to your App class, and assign it after setting the RootVisual, like so:

// Easy access to the busy service.
public static IBusyService BusyService { get; private set; }

// Rest of this class omitted for brevity....

// Do not add any additional code to this method
private void CompleteInitializePhoneApplication(object sender, NavigationEventArgs e)
{
// Set the root visual to allow the application to render
if (RootVisual != RootFrame)
{
// Add a Grid wrapper around the Root application frame.
var root = new Grid();
root.Children.Add(RootFrame);

RootVisual = root;

// Set our easy access busy service property.
BusyService = new RootPanelBusyService(root);
}

// Remove this handler since it is no longer needed
RootFrame.Navigated -= CompleteInitializePhoneApplication;
}


If you don't want to download the full example project to get the RootPanelBusyService implementation, here is the source, but you'll need to create your own Loady graphic / control.

/// &lt;summary>
/// An interface for busy services.
/// </summary>
public interface IBusyService
{
bool IsBusyShown { get; }

void ShowBusy(string message);
void ShowBusy(TimeSpan timeToShow, string message);
void ShowBusy(double milliSecondsToShow, string message);

void HideBusy(double millisecondsToWait);
void HideBusy(TimeSpan delay);
void HideBusy();
}

/// <summary>
/// A busy service that expects a Panel / Grid as the root visual.
/// </summary>
public class RootPanelBusyService : IBusyService
{
private UIElement currentBusyElement;
private Panel rootVisual;

/// <summary>
/// Initializes a new instance of the &lt;see cref="RootPanelBusyService"> class.
/// </see></summary>
/// The root visual element.
public RootPanelBusyService(Panel rootVisualElement)
{
rootVisual = rootVisualElement;
}

#region IBusyService Members

private bool _busyShown = false;
public bool IsBusyShown
{
get { return _busyShown; }
}

public void ShowBusy(double milliSecondsToShow, string message = "Loading...")
{
ShowBusy(TimeSpan.FromMilliseconds(milliSecondsToShow), message);
}

public void ShowBusy(TimeSpan timeToShow, string message = "Loading...")
{
ShowBusy(message);

ThreadPool.QueueUserWorkItem(new WaitCallback(time =>
{
Thread.Sleep((TimeSpan)time);

// Hide the busy after a sleep.
rootVisual.Dispatcher.BeginInvoke(() => HideBusy());
}), timeToShow);
}

public void ShowBusy(string message = "Loading...")
{
_busyShown = true;
var busyEl = CreateBusyElement(message);
busyEl.Opacity = 0;

try
{
if (rootVisual is Panel)
{
var root = rootVisual;
root.Children.Add(busyEl);

var anim = CreateAnimation(busyEl, Grid.OpacityProperty, 0.8, 0.00, 900, EasingMode.EaseOut);

var story = new Storyboard();
story.Children.Add(anim);
story.Begin();
}
}
catch
{
Debug.WriteLine("Problem showing busy.");
}

currentBusyElement = busyEl;
}

public void HideBusy(double milliseconds)
{
HideBusy(TimeSpan.FromMilliseconds(milliseconds));
}

public void HideBusy(TimeSpan delay)
{
var root = rootVisual;
ThreadPool.QueueUserWorkItem(new WaitCallback((arg) =>
{
Thread.Sleep(delay);

((Panel)arg).Dispatcher.BeginInvoke(() =>
{
HideBusy();
});
}), root);
}

public void HideBusy()
{
try
{
if (rootVisual is Panel)
{
var root = rootVisual;
var anim = CreateAnimation(currentBusyElement, Grid.OpacityProperty, 0.00, .80, 1000, EasingMode.EaseOut);

var story = new Storyboard();
story.Children.Add(anim);
story.Begin();

// Remove the element a second later; I do it this way because StoryCompleted is flaky.
ThreadPool.QueueUserWorkItem(new WaitCallback(obj =>
{
Thread.Sleep(900);

root.Dispatcher.BeginInvoke(() =>
{
root.Children.Remove(currentBusyElement);

currentBusyElement = null;
});
}), null);                  
}
}
catch
{
Debug.WriteLine("Problem hiding busy.");
}

_busyShown = false;
}

#endregion


/// <summary>
/// Creates the busy element; A Loady with a text block underneath.
/// </summary>
/// The message to show below the loady.      
private UIElement CreateBusyElement(string message = "Loading...")
{
// create a grid to hold the loady and swallow the entire screen.
var root = new Grid()
{
Background = new SolidColorBrush(Colors.Black)
};

// The spinny loady thingy...
root.Children.Add(new Loady()
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
});

// The text below the loady...
root.Children.Add(new TextBlock()
{
Text = message,
Margin = new Thickness(0, 150, 0, 0),
FontSize = 30,
HorizontalAlignment = System.Windows.HorizontalAlignment.Center,
VerticalAlignment = System.Windows.VerticalAlignment.Center,
Foreground = new SolidColorBrush(Colors.White)
});

return root;          
}

/// <summary>
/// A helper method for creating double animations.
/// </summary>
public DoubleAnimation CreateAnimation(DependencyObject obj, DependencyProperty prop, double value, double milliseconds, EasingMode easing = EasingMode.EaseOut)
{
var from = Convert.ToDouble(obj.GetValue(prop));
return CreateAnimation(obj, prop, value, from, milliseconds, easing);
}

/// <summary>
/// A helper method for creating double animations.
/// </summary>
public DoubleAnimation CreateAnimation(DependencyObject obj, DependencyProperty prop, double value, double from, double milliseconds, EasingMode easing = EasingMode.EaseOut)
{
CubicEase ease = new CubicEase() { EasingMode = easing };
DoubleAnimation animation = new DoubleAnimation
{
Duration = new Duration(TimeSpan.FromMilliseconds(milliseconds)),
From = from,
To = value,
FillBehavior = FillBehavior.HoldEnd,
EasingFunction = ease
};
Storyboard.SetTarget(animation, obj);
Storyboard.SetTargetProperty(animation, new PropertyPath(prop));

return animation;
}
}




Now Playing: The Roots - Get Busy

No comments:

Post a Comment

Post a Comment