Balance Test (2D)

Test your hand’s coordination and ability to hold your phone still in Balance Test, a fun little 2D-accelerometerbased game. You’ve got one minute to line up the images as many times as possible. You must keep the phone still at the correct angle for 3 seconds to earn each point. If the images become unaligned for just a moment, the hold-still time resets. This app keeps track of your best score, average score, and number of attempts, so you can keep track of your performance and watch it improve over time.

Balance Test is a lot like a 2D level app, in that you can move an image around the screen by tilting your phone in the X and Y dimensions. However, the image always “sinks” toward the corner of the screen closest to the Earth, whereas a bubble in a level would float to the highest corner.

From a code perspective, Balance Test is like a cross between the Moo Cow and Reflex Test apps. It uses Moo Cow’s accelerometer-based motion extended in an additional dimension, and its scoring feature (which keeps track of the best and average scores) is almost identical to Reflex Test.

The Main Page

This app uses a single page (ignoring the standard calibration and about pages, as well as an instructions page). It features the two images that need to be aligned to earn points, shown in Figure 49.1. The one shown in white is referred to as the target, and gets randomly placed on the screen. The one shown in blue is referred to as the moving piece and is controlled by tilting the phone.

When you’re not actively playing, you can see the score stats, shown in Figure 49.2. This app does something a little different—it gives the application bar a background color matching the theme accent color. The application bar is shown expanded in Figure 49.3.

Listing 49.1 contains the XAML for the main page, and Listing 49.2 contains its code-behind.

The images must be aligned for three seconds to get a point for your good balance.
FIGURE 49.1 The images must be aligned for three seconds to get a point for your good balance.
The score display is just like the one used in the Reflex Test app.
FIGURE 49.2 The score display is just like the one used in the Reflex Test app.
The application bar makes a bold statement with its accent-colored background.
FIGURE 49.3 The application bar makes a bold statement with its accent-colored background.

LISTING 49.1 MainPage.xaml—The User Interface for Balance Test’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait”>
<!– Add four animations to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– Move the target and scale both images –>
<Storyboard x:Name=”MoveTargetStoryboard”
Storyboard.TargetName=”TargetTransform”>
<DoubleAnimation x:Name=”TargetXAnimation”
Storyboard.TargetProperty=”TranslateX” Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuarticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation x:Name=”TargetYAnimation”
Storyboard.TargetProperty=”TranslateY” Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuarticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation x:Name=”TargetScaleXAnimation”
Storyboard.TargetProperty=”ScaleX” Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuarticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation x:Name=”TargetScaleYAnimation”
Storyboard.TargetProperty=”ScaleY” Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuarticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation x:Name=”MovingPieceScaleXAnimation”
Storyboard.TargetName=”MovingPieceTransform”
Storyboard.TargetProperty=”ScaleX” Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation x:Name=”MovingPieceScaleYAnimation”
Storyboard.TargetName=”MovingPieceTransform”
Storyboard.TargetProperty=”ScaleY” Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Rotate the target, just for fun –>
<DoubleAnimation Storyboard.TargetProperty=”Rotation” By=”360”
Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuarticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Slide the best score out then back in –>
<Storyboard x:Name=”SlideBestScoreStoryboard”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName=”BestScoreTransform”
Storyboard.TargetProperty=”TranslateX”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.4” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:.4” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.8” Value=”0”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName=”BestScoreTextBlock”
Storyboard.TargetProperty=”Visibility”>
<!– Ensure the score is visible on the way in,
even if collapsed on the way out –>
<DiscreteObjectKeyFrame KeyTime=”0:0:.4” Value=”Visible”/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<!– Slide the average score out then back in –>
<Storyboard x:Name=”SlideAvgScoreStoryboard”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName=”AvgScoreTransform”
Storyboard.TargetProperty=”TranslateX”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.4” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:.4” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.8” Value=”0”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName=”AvgScoreTextBlock”
Storyboard.TargetProperty=”Visibility”>
<!– Ensure the score is visible on the way in,
even if collapsed on the way out –>
<DiscreteObjectKeyFrame KeyTime=”0:0:.4” Value=”Visible”/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<!– Animate in (then out) a message –>
<Storyboard x:Name=”ShowMessageStoryboard”
Storyboard.TargetName=”MessageTransform”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”TranslateY”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.5” Value=”50”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:2.5” Value=”50”/>
<EasingDoubleKeyFrame KeyTime=”0:0:3” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with four menu items and one button –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.999”
BackgroundColor=”{StaticResource PhoneAccentColor}”>
<shell:ApplicationBarIconButton Text=”start”
IconUri=”/Shared/Images/appbar.play.png”
Click=”StartButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”instructions”
Click=”InstructionsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”calibrate”
Click=”CalibrateMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”clear scores”
Click=”ClearScoresMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<!– These two elements are shown while the images are aligned –>
<ProgressBar x:Name=”ProgressBar” Maximum=”3000” VerticalAlignment=”Top”/>
<Rectangle x:Name=”HighlightRectangle” Opacity=”0”
Fill=”{StaticResource PhoneAccentBrush}”/>
<!– The canvas with the two images –>
<Canvas>
<Rectangle x:Name=”Target” Fill=”{StaticResource PhoneAccentBrush}”
Width=”480” Height=”482”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/target.png”/>
</Rectangle.OpacityMask>
<Rectangle.RenderTransform>
<CompositeTransform x:Name=”TargetTransform” ScaleX=”0” ScaleY=”0”/>
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Fill=”{StaticResource PhoneForegroundBrush}”
Width=”480” Height=”482”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/movingPiece.png”/>
</Rectangle.OpacityMask>
<Rectangle.RenderTransform>
<CompositeTransform x:Name=”MovingPieceTransform”
ScaleX=”0” ScaleY=”0”/>
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>
<!– A display for the best score, average score, and # of tries –>
<StackPanel x:Name=”ScorePanel” VerticalAlignment=”Bottom”
HorizontalAlignment=”Right” Margin=”12,72”>
<TextBlock Text=”BEST SCORE” Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”BestScoreTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-15,0,30”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”BestScoreTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock x:Name=”AvgScoreHeaderTextBlock” Text=”AVG SCORE”
Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”AvgScoreTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-15,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”AvgScoreTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
<!– An animated message –>
<Grid RenderTransformOrigin=”.5,.5” HorizontalAlignment=”Center”
VerticalAlignment=”Top”>
<Grid.RenderTransform>
<CompositeTransform x:Name=”MessageTransform” TranslateY=”800”/>
</Grid.RenderTransform>
<TextBlock x:Name=”MessageTextBlockShadow” FontWeight=”Bold” FontSize=”90”
Margin=”4,4,0,0” Foreground=”{StaticResource PhoneBackgroundBrush}”/>
<TextBlock x:Name=”MessageTextBlock” FontWeight=”Bold” FontSize=”90”/>
</Grid>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • Notice that the application bar is given an opacity of .999. This is used instead of an opacity of 1 (which looks the same) to make the code-behind a little simpler. This is explained after the next listing.
  • The page isn’t using two Image elements, but rather the familiar trick of using theme-colored rectangles with image brush opacity masks. This gives the target the appropriate accent color, and the moving piece the appropriate foreground color.

LISTING 49.2 MainPage.xaml.cs—The Code-Behind for Balance Test’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Media.Animation;
using System.Windows.Navigation;
using Microsoft.Phone.Applications.Common; // For AccelerometerHelper
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// The bounds for the moving piece
double minX, maxX, lengthX, midX;
double minY, maxY, lengthY, midY;
DateTime? timeEntered;
DateTime beginTime;
Random random;
// Persistent settings
Setting<int> bestScore = new Setting<int>(“BestScore”, 0);
Setting<double> avgScore = new Setting<double>(“AvgScore”, 0);
Setting<int> numTries = new Setting<int>(“NumTries”, 0);
int score;
bool isRunning;
const double TOLERANCE = 6;
public MainPage()
{
InitializeComponent();
// Use the accelerometer via Microsoft’s helper
AccelerometerHelper.Instance.ReadingChanged += Accelerometer_ReadingChanged;
this.random = new Random();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the persisted values
UpdateLabels(true);
// Reset
this.isRunning = false;
// Start the accelerometer with Microsoft’s helper
AccelerometerHelper.Instance.Active = true;
// While on this page, don’t allow the screen to auto-lock
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the ability for the screen to auto-lock when on other pages
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Enabled;
}
void UpdateLabels(bool animateBestScore)
{
if (this.numTries.Value > 0)
{
// Ensure the panel is visible and update the text blocks
this.ScorePanel.Visibility = Visibility.Visible;
this.BestScoreTextBlock.Text = this.bestScore.Value.ToString();
this.AvgScoreTextBlock.Text = this.avgScore.Value.ToString(“##0.##”);
if (this.numTries.Value == 1)
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE (1 TRY)”;
else
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE (“ + this.numTries.Value
+ “ TRIES)”;
// Animate the text blocks out then in. The animations take care of
// showing the text blocks if they are collapsed.
this.SlideAvgScoreStoryboard.Begin();
if (animateBestScore)
this.SlideBestScoreStoryboard.Begin();
else
this.BestScoreTextBlock.Visibility = Visibility.Visible;
}
else
{
// Hide everything
this.ScorePanel.Visibility = Visibility.Collapsed;
this.BestScoreTextBlock.Visibility = Visibility.Collapsed;
this.AvgScoreTextBlock.Visibility = Visibility.Collapsed;
}
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerHelperReadingEventArgs e)
{
// Transition to the UI thread
this.Dispatcher.BeginInvoke(delegate()
{
if (!this.isRunning)
return;
// End the game after 1 minute
if ((DateTime.Now – beginTime).TotalMinutes >= 1)
{
GameOver();
return;
}
// Move the object based on the horizontal and vertical forces
this.MovingPieceTransform.TranslateX =
Clamp(this.midX + this.lengthX * e.LowPassFilteredAcceleration.X,
this.minX, this.maxX);
this.MovingPieceTransform.TranslateY =
Clamp(this.midY – this.lengthY * e.LowPassFilteredAcceleration.Y,
this.minY, this.maxY);
// Check if the two elements are aligned, with a little bit of wiggle room
if (Math.Abs(this.MovingPieceTransform.TranslateX
– this.TargetTransform.TranslateX) <= TOLERANCE &&
Math.Abs(this.MovingPieceTransform.TranslateY
– this.TargetTransform.TranslateY) <= TOLERANCE)
{
if (this.timeEntered == null)
{
// Start tracking the time
this.timeEntered = DateTime.Now;
this.HighlightRectangle.Opacity = .5;
}
// Show the progress
this.ProgressBar.Value =
(DateTime.Now – this.timeEntered.Value).TotalMilliseconds;
if (this.ProgressBar.Value >= this.ProgressBar.Maximum)
{
// Success!
this.score++;
// Animate in the score
this.MessageTextBlock.Text = this.score.ToString();
this.MessageTextBlockShadow.Text = this.score.ToString();
this.ShowMessageStoryboard.Begin();
// Move the target to the next location
MoveTarget();
}
}
else
{
// The elements are not aligned, so reset everything
this.HighlightRectangle.Opacity = 0;
this.timeEntered = null;
this.ProgressBar.Value = 0;
}
});
}
void GameOver()
{
this.isRunning = false;
this.ApplicationBar.IsVisible = true;
if (this.MoveTargetStoryboard.GetCurrentState() == ClockState.Active)
this.MoveTargetStoryboard.Stop();
// Shrink both elements to nothing
this.TargetScaleXAnimation.To = 0;
this.TargetScaleYAnimation.To = 0;
this.MovingPieceScaleXAnimation.To = 0;
this.MovingPieceScaleYAnimation.To = 0;
this.MoveTargetStoryboard.Begin();
// Record this attempt and update the UI
double oldTotal = this.avgScore.Value * this.numTries.Value;
// New average
this.avgScore.Value = (oldTotal + this.score) / (this.numTries.Value + 1);
// New total number of tries
this.numTries.Value++;
if (this.score > this.bestScore.Value)
{
// New best score
this.bestScore.Value = this.score;
UpdateLabels(true);
// Animate in a congratulations message
this.MessageTextBlock.Text = “NEW BEST!”;
this.MessageTextBlockShadow.Text = “NEW BEST!”;
this.ShowMessageStoryboard.Begin();
}
else
{
UpdateLabels(false);
}
}
void MoveTarget()
{
// Choose a random scale for the images, from .1 to .5
double scale = (random.Next(5) + 1) / 10d;
// Adjust the horizontal bounds of the moving piece accordingly
this.maxY = this.ActualHeight – 379 * scale;
this.minY = -104 * scale;
this.lengthY = Math.Abs(this.minY) + this.maxY;
this.midY = this.minY + this.lengthY / 2;
// Adjust the vertical bounds of the moving piece accordingly
this.maxX = this.ActualWidth – 280 * scale;
this.minX = -224 * scale;
this.lengthX = Math.Abs(this.minX) + this.maxX;
this.midX = this.minX + this.lengthX / 2;
// Prepare and begin the animation to a new location & size
this.TargetScaleXAnimation.To = this.TargetScaleYAnimation.To = scale;
this.MovingPieceScaleXAnimation.To = scale;
this.MovingPieceScaleYAnimation.To = scale;
this.TargetXAnimation.To = this.minX + random.Next((int)this.lengthX);
this.TargetYAnimation.To = this.minY + random.Next((int)this.lengthY);
this.MoveTargetStoryboard.Begin();
}
// “Clamp” the incoming value so it’s no lower than min & no larger than max
static double Clamp(double value, double min, double max)
{
return Math.Max(min, Math.Min(max, value));
}
// Application bar handlers
void StartButton_Click(object sender, EventArgs e)
{
// Get started
this.isRunning = true;
this.ApplicationBar.IsVisible = false;
this.ScorePanel.Visibility = Visibility.Collapsed;
// Center the target before it animates to a new location
this.TargetTransform.TranslateX = this.ActualWidth – this.Target.Width / 2;
this.TargetTransform.TranslateY = this.ActualHeight – this.Target.Height / 2;
MoveTarget();
this.score = 0;
this.beginTime = DateTime.Now;
}
void InstructionsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void CalibrateMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/Calibrate/CalibratePage.xaml?appName=Balance Test&”
+ “calibrateX=true&calibrateY=false”, UriKind.Relative));
}
void ClearScoresMenuItem_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to clear your scores?”,
“Clear scores”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
this.numTries.Value = 0;
this.bestScore.Value = 0;
UpdateLabels(true);
}
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Balance Test”, UriKind.Relative));
}
}
}

[/code]

  • This app uses the low-pass-filtered acceleration data from the AccelerometerHelper library. This gives a nice balance of smoothness and the right amount of latency. (Using the optimally filtered data ends up being a bit too jumpy in this case.)
  • The calculations that adjust the moving piece’s position should look familiar from the Moo Can app. Notice that the acceleration-based offset is added to the X midpoint, whereas it’s subtracted from the Y midpoint. As illustrated back in Figure 44.1, the Y-acceleration dimension grows in the opposite direction than Silverlight’s typical Y dimension.
  • The page’s ActualWidth and ActualHeight properties are used throughout. If the application bar had an opacity of 1, the page’s ActualHeight would be 728 while it is visible versus 800 while it is hidden. However, in StartButton_Click, accessing ActualHeight after hiding the application bar would still report 728, as it hasn’t given the layout system a chance to make the change. Thanks to the application bar’s custom opacity that enables the page to extend underneath it, the timing of this is no longer a concern; ActualHeight reports 800 at all times.

The Finished Product

Balance Test (2D)

Level (Determining Angle)

No book covering the use of an accelerometer would be complete without showing you how to create a level! This chapter’s Level app not only features four classic tubular bubble levels (one on each edge), but it also shows the current angle of the phone with little accent lines that line up with companion lines when one of the edges of the phone is parallel to the ground. This makes it even easier to visually align the phone exactly as you wish.

Getting smooth, stable results from the accelerometer is important for an app such as this that relies on slow, small movements. Therefore, as in the preceding chapter, Level makes use of Microsoft’s AccelerometerHelper class to perform data smoothing. The key to this app is to use a little bit of trigonometry in order to determine the angle of the phone based on the accelerometer data.

The BubbleWindow User Control

The classic bubble display, shown in Figure 48.1, is implemented as a user control. That’s because Level’s main page uses four instances of this display. Listing 48.1 contains its XAML and Listing 48.2 contains its code-behind.

The BubbleWindow user control displays a colored bubble inside a marked rectangle.
FIGURE 48.1 The BubbleWindow user control displays a colored bubble inside a marked rectangle.

LISTING 48.1 BubbleWindow.xaml—The User Interface for the BubbleWindow User Control

[code]

<UserControl x:Class=”WindowsPhoneApp.BubbleWindow”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
<!– Add one storyboard to the control’s resource dictionary –>
<UserControl.Resources>
<Storyboard x:Name=”BubbleStoryboard” Storyboard.TargetName=”BubbleTransform”>
<!– Stretch the bubble horizontally –>
<DoubleAnimation Storyboard.TargetProperty=”ScaleX” By=”.5”
Duration=”0:0:.8” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Shrink the bubble horizontally –>
<DoubleAnimation Storyboard.TargetProperty=”ScaleY” By=”-.2”
Duration=”0:0:.8” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</UserControl.Resources>
<Canvas Background=”#333”>
<Ellipse x:Name=”Bubble” Width=”115” Height=”115” Visibility=”Collapsed”
Fill=”{StaticResource PhoneAccentBrush}” RenderTransformOrigin=”.5,.5”>
<Ellipse.RenderTransform>
<!– Used in the animations –>
<CompositeTransform x:Name=”BubbleTransform”/>
</Ellipse.RenderTransform>
</Ellipse>
<Line x:Name=”Line1” Stroke=”White”/>
<Line x:Name=”Line2” Stroke=”White”/>
<Rectangle x:Name=”Rectangle” Stroke=”#A555” StrokeThickness=”6”/>
</Canvas>
</UserControl>

[/code]

LISTING 48.2 BubbleWindow.xaml.cs—The Code-Behind for the BubbleWindow User Control

[code]

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace WindowsPhoneApp
{
public partial class BubbleWindow : UserControl
{
public BubbleWindow()
{
InitializeComponent();
this.Loaded += BubbleWindow_Loaded;
}
void BubbleWindow_Loaded(object sender, RoutedEventArgs e)
{
// Adjust the two centered lines and the rectangular border to fit
// the size given to this instance of the control
this.Line1.X1 = this.Line1.X2 =
(this.ActualWidth / 2) – (this.Bubble.ActualWidth / 2);
this.Line1.Y2 = this.ActualHeight;
this.Line2.X1 = this.Line2.X2 =
(this.ActualWidth / 2) + (this.Bubble.ActualWidth / 2);
this.Line2.Y2 = this.ActualHeight;
this.Rectangle.Width = this.ActualWidth;
this.Rectangle.Height = this.ActualHeight;
// Don’t allow the bubble to render past this control’s border
this.Clip = new RectangleGeometry {
Rect = new Rect(0, 0, this.ActualWidth, this.ActualHeight) };
this.Bubble.Visibility = Visibility.Visible;
}
// Set the horizontal position of the bubble on a scale from 0 to 100
public void SetXPercentage(double percentage)
{
percentage = percentage / 100;
double left = (-this.Bubble.ActualWidth/2) + this.ActualWidth*percentage;
Canvas.SetLeft(this.Bubble, left);
}
// Set the vertical position of the bubble on a scale from 0 to 100
public void SetYPercentage(double percentage)
{
percentage = percentage / 100;
// Allow the bubble to go more off-screen with a taller possible range
double range = this.ActualHeight + 20 * 2;
double top = -20 + range * percentage – range / 2;
Canvas.SetTop(this.Bubble, top);
}
public void Animate()
{
// Stretch out the bubble if the animation isn’t already running
if (this.BubbleStoryboard.GetCurrentState() != ClockState.Active)
{
this.BubbleStoryboard.Stop();
this.BubbleStoryboard.Begin();
}
}
}
}

[/code]

This user control has nothing to do with the accelerometer. It simply enables its consumer to do three things:

  • Set the horizontal position of the bubble from 0% (all the way to the left) to 100% (all the way to the right).
  • Set the vertical position of the bubble from 0% (all the way to the top) to 100% (all the way to the bottom).
  • Trigger an animation that stretches the bubble, appropriate to use when the bubble is moving quickly to give it more realism. This stretching is shown in Figure 48.2.
The stretched bubble, simulating the expected deformation from fast motion.
FIGURE 48.2 The stretched bubble, simulating the expected deformation from fast motion.

Note that the manual layout code in Listing 48.2 that adjusts the lines and rectangle could be eliminated by leveraging a grid’s automatic layout rather than using a canvas.

The Main Page

Besides the same calibration page used by Moo Can and shown in the preceding chapter, Level only has the single main page. Listing 48.3 contains the XAML for the main page, and Listing 48.4 contains its code-behind.

LISTING 48.3 MainPage.xaml—The User Interface for Level’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
SupportedOrientations=”Portrait”>
<Grid>
<Grid.Background>
<!– The metallic background –>
<ImageBrush ImageSource=”Images/background.png”/>
</Grid.Background>
<!– The four lines in the middle that don’t move –>
<Line Y1=”240” Y2=”290” X1=”240” X2=”240” Stroke=”#555” StrokeThickness=”6”/>
<Line Y1=”510” Y2=”560” X1=”240” X2=”240” Stroke=”#555” StrokeThickness=”6”/>
<Line Y1=”400” Y2=”400” X1=”80” X2=”130” Stroke=”#555” StrokeThickness=”6”/>
<Line Y1=”400” Y2=”400” X1=”350” X2=”400” Stroke=”#555” StrokeThickness=”6”/>
<!– The four lines that tilt based on the phone’s angle –>
<Canvas RenderTransformOrigin=”.5,.5” Width=”480” Height=”800”>
<Canvas.RenderTransform>
<RotateTransform x:Name=”CenterTransform” Angle=”45”/>
</Canvas.RenderTransform>
<Line Y1=”240” Y2=”290” X1=”240” X2=”240”
Stroke=”{StaticResource PhoneAccentBrush}” StrokeThickness=”6”/>
<Line Y1=”510” Y2=”560” X1=”240” X2=”240”
Stroke=”{StaticResource PhoneAccentBrush}” StrokeThickness=”6”/>
<Line Y1=”400” Y2=”400” X1=”80” X2=”130”
Stroke=”{StaticResource PhoneAccentBrush}” StrokeThickness=”6”/>
<Line Y1=”400” Y2=”400” X1=”350” X2=”400”
Stroke=”{StaticResource PhoneAccentBrush}” StrokeThickness=”6”/>
</Canvas>
<!– The display for the exact angle –>
<TextBlock x:Name=”AngleTextBlock” Foreground=”#555”
HorizontalAlignment=”Center” VerticalAlignment=”Center”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
RenderTransformOrigin=”.5,.5”>
<TextBlock.RenderTransform>
<RotateTransform x:Name=”AngleTextBlockTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
<!– The circle in the center –>
<Ellipse Width=”220” Height=”220” Stroke=”#555” StrokeThickness=”6”/>
<!– The four bubble level displays –>
<local:BubbleWindow x:Name=”TopWindow” Width=”275” Height=”75”
VerticalAlignment=”Top”/>
<local:BubbleWindow x:Name=”BottomWindow” Width=”275” Height=”75”
VerticalAlignment=”Bottom”/>
<local:BubbleWindow x:Name=”LeftWindow” Width=”622” Height=”75”
HorizontalAlignment=”Left” Margin=”-274,0,0,0” RenderTransformOrigin=”.5,.5”>
<local:BubbleWindow.RenderTransform>
<CompositeTransform Rotation=”-90”/>
</local:BubbleWindow.RenderTransform>
</local:BubbleWindow>
<local:BubbleWindow x:Name=”RightWindow” Width=”622” Height=”75”
HorizontalAlignment=”Right” Margin=”0,0,-274,0” RenderTransformOrigin=”.5,.5”>
<local:BubbleWindow.RenderTransform>
<CompositeTransform Rotation=”-90”/>
</local:BubbleWindow.RenderTransform>
</local:BubbleWindow>
<!– The calibrate pseudo-button that tilts based on the phone’s angle –>
<Border Margin=”0,0,0,120” HorizontalAlignment=”Center”
VerticalAlignment=”Bottom” local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”Calibrate_MouseLeftButtonUp”>
<TextBlock Text=”calibrate” Padding=”36” Foreground=”#555”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”
RenderTransformOrigin=”.5,.5”>
<TextBlock.RenderTransform>
<RotateTransform x:Name=”CalibrateTextBlockTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</Border>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 48.4 MainPage.xaml.cs—The Code-Behind for Level’s Main Page

[code]

using System;
using System.Windows.Input;
using System.Windows.Navigation;
using Microsoft.Phone.Applications.Common; // For AccelerometerHelper
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Used to track every 5th call to Accelerometer_ReadingChanged
double readingCount = 0;
// The angle 5 calls ago
double previousAngle = 0;
public const double RADIANS_TO_DEGREES = 180 / Math.PI;
public MainPage()
{
InitializeComponent();
// Use the accelerometer via Microsoft’s helper
AccelerometerHelper.Instance.ReadingChanged += Accelerometer_ReadingChanged;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Start the accelerometer with Microsoft’s helper
AccelerometerHelper.Instance.Active = true;
// While on this page, don’t allow the screen to auto-lock
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the ability for the screen to auto-lock when on other pages
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Enabled;
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerHelperReadingEventArgs e)
{
// This is the key formula
double rawAngle =
Math.Atan2(e.AverageAcceleration.X, e.AverageAcceleration.Y)
* RADIANS_TO_DEGREES;
// Express the angle from 0-90°, used by some calculations
double angle0to90 = Math.Abs(rawAngle) % 90;
// Calculate the horizontal % of the left & right bubbles by
// using the angle as an offset from the midpoint (50%)
double landscapeXPercentage = Clamp(Math.Abs(rawAngle) > 90
? 50 + angle0to90
: 50 – (90 – angle0to90), 0, 100);
// The horizontal % of the top & bottom bubbles requires more cases
double portraitXPercentage;
// When the bottom bubble window is on top
if (Math.Abs(rawAngle) <= 25)
portraitXPercentage = Clamp(rawAngle > 0
? 50 – angle0to90 * 2
: 50 + angle0to90 * 2, 0, 100);
// When the top bubble window is on top
else if (Math.Abs(rawAngle) >= 155)
portraitXPercentage = Clamp(rawAngle > 0
? 50 – (90 – angle0to90) * 2
: 50 + (90 – angle0to90) * 2, 0, 100);
// When the right bubble window is on top
else if (rawAngle < 0)
portraitXPercentage = 100;
// When the left bubble window is on top
else
portraitXPercentage = 0;
// The Y% for the left/right bubbles is the same
// as the X% for the top/bottom bubbles
double landscapeYPercentage = portraitXPercentage;
// The Y% for the top/bottom bubbles is the same
// as the inverse of the X% for the left/right bubbles
double portraitYPercentage = 100 – landscapeXPercentage;
this.Dispatcher.BeginInvoke(delegate()
{
// Set the primary (horizontal) position of each bubble
this.TopWindow.SetXPercentage(portraitXPercentage);
this.BottomWindow.SetXPercentage(portraitXPercentage);
this.LeftWindow.SetXPercentage(landscapeXPercentage);
this.RightWindow.SetXPercentage(landscapeXPercentage);
// Set the vertical position of each bubble
this.TopWindow.SetYPercentage(portraitYPercentage);
this.BottomWindow.SetYPercentage(portraitYPercentage);
this.LeftWindow.SetYPercentage(landscapeYPercentage);
this.RightWindow.SetYPercentage(landscapeYPercentage);
// On every 5th call, check to see if significant motion has occurred.
// If the angle has changed by more than 8 degrees, trigger the
// animation on any bubbles that are vertically close to the top
// or bottom of their window (< 10% or > 90%).
readingCount = (readingCount + 1) % 5;
if (readingCount == 0)
{
if (Math.Abs(previousAngle – rawAngle) > 8)
{
if (portraitYPercentage < 10 || portraitYPercentage > 90)
{
this.TopWindow.Animate();
this.BottomWindow.Animate();
}
if (landscapeYPercentage < 10 || landscapeYPercentage > 90)
{
this.LeftWindow.Animate();
this.RightWindow.Animate();
}
}
this.previousAngle = rawAngle;
}
// Update the exact angle display, using values from 0 to 45°
double angle0to45 = angle0to90 <= 45 ? angle0to90 : 90 – angle0to90;
this.AngleTextBlock.Text = angle0to45.ToString(“##0.0°”);
// Tilt the non-bubble pieces of UI accordingly
this.AngleTextBlockTransform.Angle = rawAngle – 180;
this.CalibrateTextBlockTransform.Angle = rawAngle – 180;
this.CenterTransform.Angle = rawAngle – 180;
});
}
void Calibrate_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/Shared/Calibrate/CalibratePage.xaml?” +
“appName=Level&calibrateX=true&calibrateY=true”, UriKind.Relative));
}
// “Clamp” the incoming value so it’s no lower than min & no larger than max
static double Clamp(double value, double min, double max)
{
return Math.Max(min, Math.Min(max, value));
}
}
}

[/code]

  • The most important calculation in this app is done as the first line of Accelerometer_ReadingChanged. The X and Y acceleration vectors are perpendicular to each other. Therefore, you can imagine the X and Y values from the accelerometer as two sides of a right triangle, as pictured in Figure 48.3. Trigonometry tells us that the tangent of an angle is the length of the opposite side divided by the length of the adjacent side (the “TOA” in the classic “SOHCAH- TOA” mnemonic). Therefore, the angle equals the arctangent of the opposite length divided by the adjacent length. Math.Atan2 performs this calculation for us, and even handles the potential divide-by-zero case so we don’t have to.
In a right triangle, the tangent of an angle equals the opposite length divided by the adjacent length.
FIGURE 48.3 In a right triangle, the tangent of an angle equals the opposite length divided by the adjacent length.

The trigonometric functions in System.Math return angles specified in radians!

This is why Listing 48.4 multiplies the result of Math.Atan2 by the RADIANS_TO_DEGREES constant.This converts the value expressed in radians to a value expressed in degrees by multiplying the value by 180 / π.

  • For the call to Math.Atan2, the X axis is chosen as the opposite side of the right triangle and the Y axis is chosen as the adjacent side. This choice is arbitrary, but it does impact the resultant angle. Using X as the opposite side produces the values for rawAngle illustrated in Figure 48.4.
The raw angle reported by Math.Atan2, where each arrow indicates the direction that must point toward the sky.
FIGURE 48.4 The raw angle reported by Math.Atan2, where each arrow indicates the direction that must point toward the sky.
  • The bulk of the code in Accelerometer_ReadingChanged determines the appropriate horizontal percentage for the four BubbleWindow user controls based on the current angle. (The two “portrait” bubbles—top and bottom— always have the same values as each other, as do the two “landscape” bubbles.)
  • Because of the symmetry between the raw angle reported when the left side is facing the sky and when the right side is facing the sky, calculating the horizontal percentage for the landscape bubbles is relatively straightforward. Each degree away from 90° (or –90°) adds or subtracts one percentage point from the 50% midpoint.
  • Because of the asymmetry of the top/bottom raw angles, the horizontal percentage calculation for the portrait bubbles is more complex.
  • The angle displayed in the middle of the screen is 0.0° when any of the four sides is exactly level with the ground. Therefore, the displayed angle never gets higher than 45°. At such a point, the value starts decreasing as the next side gets closer to becoming level.
  • Rather than checking on every ReadingChanged event, this listing checks for large motion that should trigger the bubble animation every fifth event. This is a tradeoff between performance and latency.

To get even more stability in its readings, the Level app tweaks the code for AccelerometerHelper to make AverageAcceleration examine the previous 50 samples rather than 25.This was done by simply changing the value of a SamplesCount static variable from 25 to 50. This has the side effect of doubling the latency in the reported data, but this is acceptable for a level app.The latency simply makes the liquid in each bubble window act a little “thicker.”

The Finished Product

Level (Determining Angle)

Moo Can (Turn Over)

Do you remember those cans that moo when you turn them upside down? The Moo Can app brings this classic children’s toy back to life in digital form! Moo Can makes a moo sound when you turn your phone upside down. Gravity also affects the cow on the screen, which rotates and falls toward whatever edge of the screen is currently on the bottom.

Just like the real cans, you can shake the phone to make a harsh sound that is different from the desired “moo” caused by gently turning the phone upside down. You can also change the cow to a sheep or cat, each of which makes its own unique sounds.

The Main Page

Moo Can has a main page, a calibration page, and an instructions page (not shown in this chapter). Listing 47.1 contains the XAML for the main page, and Listing 47.2 contains its code-behind.

LISTING 47.1 MainPage.xaml—The User Interface for Moo Can’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
SupportedOrientations=”Portrait”>
<!– The application bar, with five menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar BackgroundColor=”#9CB366” ForegroundColor=”White”>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”cow” Click=”AnimalMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”sheep” Click=”AnimalMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”cat” Click=”AnimalMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”instructions”
Click=”InstructionsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”calibrate”
Click=”CalibrateMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Canvas>
<Canvas.Background>
<ImageBrush ImageSource=”Images/background.png”/>
</Canvas.Background>
<!– The cow, sheep, or cat –>
<Image x:Name=”AnimalImage” RenderTransformOrigin=”.5,.5”
Canvas.Left=”32” Canvas.Top=”50” Width=”434” Height=”507”>
<Image.RenderTransform>
<CompositeTransform x:Name=”AnimalTransform”/>
</Image.RenderTransform>
</Image>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

The user interface contains an animal image (whose source is set in code-behind) placed over the background image. It also uses an application bar menu, shown in Figure 47.1, for switching the animal or navigating to either of the other two pages. The application bar is given hard-coded colors, so it blends in with the grass in the background image when the menu is closed.

The application bar menu on the main page.
FIGURE 47.1 The application bar menu on the main page.

LISTING 47.2 MainPage.xaml.cs—The Code-Behind for Moo Can’s Main Page

[code]

using System;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using Microsoft.Phone.Applications.Common; // For AccelerometerHelper
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
Setting<string> chosenAnimal = new Setting<string>(“ChosenAnimal”, “cow”);
bool upsideDown = false;
// The start, middle, end, and length of the
// vertical path that the animal moves along
const double MAX_Y = 170;
const double MIN_Y = -75;
const double MID_Y = 47.5;
const double LENGTH_Y = 245;
// The start, middle and end of the clockwise rotation of the animal
const double MAX_ANGLE_CW = 180;
const double MIN_ANGLE_CW = 0;
const double MID_ANGLE_CW = 90;
// The start, middle and end of the counter-clockwise rotation of the animal
const double MIN_ANGLE_CCW = -180;
const double MAX_ANGLE_CCW = 0;
const double MID_ANGLE_CCW = -90;
// The length of the rotation, regardless of which direction
const double LENGTH_ANGLE = 180;
public MainPage()
{
InitializeComponent();
// Use the accelerometer via Microsoft’s helper
AccelerometerHelper.Instance.ReadingChanged += Accelerometer_ReadingChanged;
SoundEffects.Initialize();
// Allow the app to run (producing sounds) even when the phone is locked.
// Once disabled, you cannot re-enable the default behavior!
PhoneApplicationService.Current.ApplicationIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the chosen animal
ChangeAnimal();
// Start the accelerometer with Microsoft’s helper
AccelerometerHelper.Instance.Active = true;
// While on this page, don’t allow the screen to auto-lock
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the ability for the screen to auto-lock when on other pages
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Enabled;
}
void ChangeAnimal()
{
switch (this.chosenAnimal.Value)
{
case “cow”:
this.AnimalImage.Source = new BitmapImage(
new Uri(“Images/cow.png”, UriKind.Relative));
break;
case “sheep”:
this.AnimalImage.Source = new BitmapImage(
new Uri(“Images/sheep.png”, UriKind.Relative));
break;
case “cat”:
this.AnimalImage.Source = new BitmapImage(
new Uri(“Images/cat.png”, UriKind.Relative));
break;
}
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerHelperReadingEventArgs e)
{
// Transition to the UI thread
this.Dispatcher.BeginInvoke(delegate()
{
// Move the animal vertically based on the vertical force
this.AnimalTransform.TranslateY =
Clamp(MID_Y – LENGTH_Y * e.AverageAcceleration.Y, MIN_Y, MAX_Y);
// Clear the upside-down flag, only when completely upright
if (this.AnimalTransform.TranslateY == MAX_Y)
this.upsideDown = false;
// Rotate the animal to always be upright
if (e.AverageAcceleration.X <= 0)
this.AnimalTransform.Rotation = Clamp(MID_ANGLE_CW +
LENGTH_ANGLE * e.AverageAcceleration.Y, MIN_ANGLE_CW, MAX_ANGLE_CW);
else
this.AnimalTransform.Rotation = Clamp(MID_ANGLE_CCW –
LENGTH_ANGLE * e.AverageAcceleration.Y, MIN_ANGLE_CCW, MAX_ANGLE_CCW);
// Play the appropriate shake sound when shaken
if (ShakeDetection.JustShook(e.OriginalEventArgs))
{
switch (this.chosenAnimal.Value)
{
case “cow”: SoundEffects.MooShake.Play(); break;
case “sheep”: SoundEffects.BaaShake.Play(); break;
case “cat”: SoundEffects.MeowShake.Play(); break;
}
}
// Play the normal sound when first turned upside-down
if (!this.upsideDown && this.AnimalTransform.TranslateY == MIN_Y)
{
this.upsideDown = true;
switch (this.chosenAnimal.Value)
{
case “cow”: SoundEffects.Moo.Play(); break;
case “sheep”: SoundEffects.Baa.Play(); break;
case “cat”: SoundEffects.Meow.Play(); break;
}
}
});
}
// “Clamp” the incoming value so it’s no lower than min & no larger than max
static double Clamp(double value, double min, double max)
{
return Math.Max(min, Math.Min(max, value));
}
// Application bar handlers
void AnimalMenuItem_Click(object sender, EventArgs e)
{
this.chosenAnimal.Value = (sender as IApplicationBarMenuItem).Text;
ChangeAnimal();
}
void InstructionsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void CalibrateMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/Calibrate/CalibratePage.xaml?appName=Moo Can”, UriKind.Relative));
}
}
}

[/code]

  • This app moves and rotates the animal based on the accelerometer’s data, as shown in Figure 47.2. However, the raw data has a lot of noise, so using it directly would produce a jerky result. (This noise didn’t matter for the previous three apps because the punch, throw, and shake detection are coarse.) Therefore, this listing makes use of an AccelerometerHelper class published by Microsoft that performs data smoothing for you. It also simplifies the starting/stopping interaction with the accelerometer.
The chosen animal rotates and falls as you turn the phone upside down.
FIGURE 47.2 The chosen animal rotates and falls as you turn the phone upside down.
  • AccelerometerHelper exposes its functionality via a static Instance property, so the constructor uses this to attach a handler to its ReadingChanged event. This event is just like the ReadingChanged event from the preceding chapters, but with richer data passed to handlers.
  • This project uses a SoundEffects class just like the one from previous apps but with six properties for six possible sounds: Moo, MooShake, Baa, BaaShake, Meow, and MeowShake.
  • This app runs while locked and this page prevents the screen from auto-locking. This way, you can make the noises without keeping the phone screen on. At the same time, you can continue to watch the cow fall up and down without having to periodically tap the screen to keep it on.
  • To start the accelerometer, OnNavigatedTo simply sets AccelerometerHelper’s Instance’s Active property to true. Internally, this calls Start (if not already started) with the same sort of exception handling done in preceding chapters. This app doesn’t bother stopping the accelerometer, but this could be done by setting Active to false inside OnNavigatedFrom.
  • Inside Accelerometer_ReadingChanged, the AccelerometerHelper-specific AccelerometerHelperReadingEventArgs instance is leveraged to get the average acceleration in the X and Y directions. This class is different from the AccelerometerReadingEventArgs used by the preceding apps. It exposes several properties for getting the data with various types of smoothing applied.

AccelerometerHelperReadingEventArgs exposes four properties that enable you to choose how raw or smooth you want your data to be:

  • RawAcceleration—The same noisy data you would get from the Accelerometer class’s ReadingChanged event (but with calibration potentially applied, as described later in this chapter).
  • LowPassFilteredAcceleration—Applies a first-order low-pass filter over the raw data. The result is smoother data with a little bit of latency.
  • OptimalyFilteredAcceleration [sic]—This uses the same low-pass filter, but only when the current value is close enough to a rolling average value. If the value is sufficiently greater, the raw value is reported instead. (This algorithm is done independently for all three axes.) This gives a nice balance between having smooth results and low latency. Small changes are handled smoothly and large changes are reported quickly.
  • AverageAcceleration—Reports the mean of the most recent 25 data points collected from the “optimally filtered” algorithm.This gives the smoothest result of any of the choices, but it also has the highest latency.

Each property exposes X, Y, Z, and Magnitude properties (but unlike AccelerometerReadingEventArgs, no Timestamp property). Magnitude reports the length of the 3D vector formed by the other three values, which is the square root of X2 + Y2 + Z2. For more details about the algorithms behind these properties, see http://bit.ly/accelerometerhelper.

  • This app uses the same ShakeDetection class shown in the preceding chapter. Because the JustShook method is defined to accept an instance of AccelerometerReadingEventArgs—not AccelerometerHelperReadingEventArgs—this listing uses an OriginalEventArgs property to retrieve it. This property actually isn’t exposed by the AccelerometerHelperReadingEventArgs class; I added it directly to my copy of the source code.
  • A turn upside down is detected by noticing the first moment that the Y acceleration value is large enough after a point in time when it has been small enough. In other words, after the upside-down orientation has been detected, the user must turn the phone right side up to clear the value of upsideDown before another upside-down turn will be detected. Because the animal transform’s TranslateY value is proportional to the Y-axis acceleration (and restricted to a range of MIN_Y to MAX_Y), the phone is completely upside down when TranslateY is MIN_Y and the phone is completely upright when TranslateY is MAX_Y.
  • This app purposely does not make any noise when turned right side up, because a real can does not either. (I didn’t realize this until I bought a few of the cans and tried for myself.)

The Calibration Page

Moo Can enables users to calibrate the accelerometer in case its notion of right side up or upside down are slightly askew from reality. The process of calibration simply involves asking the user when they believe the phone is level, remembering the accelerometer’s reading at that moment, and then using those values as an offset to the accelerometer data from that point onward.

One benefit of using AccelerometerHelper rather than the raw accelerometer APIs is that calibration functionality is built in. It collects the data, stores it as isolated storage application settings (named “AccelerometerCalibrationX” and “AccelerometerCalibrationY”), and automatically offsets the data it returns—even the supposedly “raw” data.

The calibration page, designed to be shared among multiple apps, shows you how to take advantage of it. Listing 47.3 contains the XAML and Listing 47.4 contains the codebehind. It produces the page shown in Figure 47.3.

The calibration page enables calibrating just the X dimension, just the Y dimension, or both, but only when the phone is held fairly still and level.
FIGURE 47.3 The calibration page enables calibrating just the X dimension, just the Y dimension, or both, but only when the phone is held fairly still and level.

LISTING 47.3 CalibratePage.xaml—The User Interface for The Accelerometer Calibration Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.CalibratePage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock x:Name=”ApplicationName”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”calibrate” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<StackPanel>
<TextBlock Margin=”24” TextWrapping=”Wrap” Text=”Tap the button …”/>
<Button x:Name=”CalibrateButton” Content=”calibrate” IsEnabled=”False”
Height=”150” Click=”CalibrateButton_Click”
local:Tilt.IsEnabled=”True”/>
<TextBlock x:Name=”WarningText” Visibility=”Collapsed” Margin=”24,0”
TextWrapping=”Wrap” FontWeight=”Bold”
Text=”Your phone is not still or level enough!”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 47.4 CalibratePage.xaml.cs—The Code-Behind for the Accelerometer Calibration Page

[code]

using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Applications.Common; // For AccelerometerHelper
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class CalibratePage : PhoneApplicationPage
{
bool calibrateX = true, calibrateY = true;
public CalibratePage()
{
InitializeComponent();
// Use the accelerometer via Microsoft’s helper
AccelerometerHelper.Instance.ReadingChanged += Accelerometer_ReadingChanged;
// Ensure it is active
AccelerometerHelper.Instance.Active = true;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Set the application name in the header
if (this.NavigationContext.QueryString.ContainsKey(“appName”))
{
this.ApplicationName.Text =
this.NavigationContext.QueryString[“appName”].ToUpperInvariant();
}
// Check for calibration parameters
if (this.NavigationContext.QueryString.ContainsKey(“calibrateX”))
{
this.calibrateX =
bool.Parse(this.NavigationContext.QueryString[“calibrateX”]);
}
if (this.NavigationContext.QueryString.ContainsKey(“calibrateY”))
{
this.calibrateY =
bool.Parse(this.NavigationContext.QueryString[“calibrateY”]);
}
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerHelperReadingEventArgs e)
{
this.Dispatcher.BeginInvoke(delegate()
{
bool canCalibrateX = this.calibrateX &&
AccelerometerHelper.Instance.CanCalibrate(this.calibrateX, false);
bool canCalibrateY = this.calibrateY &&
AccelerometerHelper.Instance.CanCalibrate(false, this.calibrateY);
// Update the enabled state and text of the calibration button
this.CalibrateButton.IsEnabled = canCalibrateX || canCalibrateY;
if (canCalibrateX && canCalibrateY)
this.CalibrateButton.Content = “calibrate (flat)”;
else if (canCalibrateX)
this.CalibrateButton.Content = “calibrate (portrait)”;
else if (canCalibrateY)
this.CalibrateButton.Content = “calibrate (landscape)”;
else
this.CalibrateButton.Content = “calibrate”;
this.WarningText.Visibility = this.CalibrateButton.IsEnabled ?
Visibility.Collapsed : Visibility.Visible;
});
}
void CalibrateButton_Click(object sender, RoutedEventArgs e)
{
if (AccelerometerHelper.Instance.Calibrate(this.calibrateX,
this.calibrateY) ||
AccelerometerHelper.Instance.Calibrate(this.calibrateX, false) ||
AccelerometerHelper.Instance.Calibrate(false, this.calibrateY))
{
// Consider it a success if we were able to
// calibrate in either direction (or both)
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
else
{
MessageBox.Show(“Unable to calibrate. Make sure you’re holding your “ +
“phone still, even when tapping the button!”, “Calibration Error”,
MessageBoxButton.OK);
}
}
}
}

[/code]

  • This page enables calibration in just one dimension—or both—based on the query parameters passed when navigating to it. Listing 47.2 simply passes “appName=Moo Can” as the query string, so this app will enable any kind of calibration. Most likely, the user will be holding the phone in the portrait orientation when attempting to calibrate.
  • The calibration method— AccelerometerHelper.Instance.Calibrate—only succeeds if the phone is sufficiently still and at least somewhat-level in the relevant dimension(s). The AccelerometerHelper.Instance.CanCalibrate method tells you whether calibration will succeed, although the answer can certainly change between the time you call CanCalibrate and the time you call Calibrate, so you should always be prepared for Calibrate to fail.
  • The ReadingChanged event handler continually checks CanCalibrate so it can enable/disable the calibration button appropriately. CanCalibrate has two Boolean parameters that enable you to specify whether you care about just the X dimension, just the Y dimension, or both. (Calibrating the Z axis is not meaningful.) Listing 47.4 checks each dimension individually (if the app cares about the dimension) so it can display a helpful message to the user. The only way you can calibrate both X and Y dimensions simultaneously is by placing the phone flat on a surface parallel to the ground.
  • CalibrateButton_Click tries to calibrate whatever will succeed. Calibrate has the same two parameters as CanCalibrate, so this listing first attempts to calibrate both dimensions, but if that fails (by returning false), it tries to calibrate each dimension individually.

Moo Can (Turn Over)

Noise Maker (Shake)

Noise Maker makes a loud, annoying noise when you shake the phone. This app is meant for sporting events, much like the vuvuzelas that became infamous during the 2010 FIFA World Cup. You can use it to help cheer on your team, or to distract the opposition. The main lesson of this chapter—figuring out when the phone is being shaken— can be useful for many apps. You might wish to use the same shake-detection algorithm for more serious apps, perhaps as a gesture to refresh data.

The Main Page

Noise Maker has a main page, a settings page, and an about page (not shown in this chapter). Listing 46.1 contains the XAML for the main page. This produces the user interface shown to the right. Listing 46.2 contains its code-behind.

LISTING 46.1 MainPage.xaml—The User Interface for Noise Maker’s Main Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
SupportedOrientations=”Portrait”>
<!– The application bar, with two menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”settings”
Click=”SettingsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<StackPanel>
<!– An accent-colored image –>
<Rectangle Fill=”{StaticResource PhoneAccentBrush}” Width=”456” Height=”423”
Margin=”{StaticResource PhoneMargin}”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/logo.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<!– Accent-colored rotated text –>
<TextBlock FontFamily=”Segoe WP Black” Text=”SHAKE TO MAKE NOISE!”
FontSize=”100” Foreground=”{StaticResource PhoneAccentBrush}”
TextWrapping=”Wrap” TextAlignment=”Center” Margin=”0,20,0,0”
LineHeight=”80” LineStackingStrategy=”BlockLineHeight”
RenderTransformOrigin=”.5,.5”>
<TextBlock.RenderTransform>
<RotateTransform Angle=”-10”/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
</phone:PhoneApplicationPage>

[/code]

LISTING 46.2 MainPage.xaml.cs—The Code-Behind for Noise Maker’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Navigation;
using Microsoft.Devices.Sensors;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
Accelerometer accelerometer;
public MainPage()
{
InitializeComponent();
// Initialize the accelerometer
this.accelerometer = new Accelerometer();
this.accelerometer.ReadingChanged += Accelerometer_ReadingChanged;
SoundEffects.Initialize();
// Allow the app to run (producing sounds) even when the phone is locked.
// Once disabled, you cannot re-enable the default behavior!
PhoneApplicationService.Current.ApplicationIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Start the accelerometer
try
{
this.accelerometer.Start();
}
catch
{
MessageBox.Show(
“Unable to start your accelerometer. Please try running this app again.”,
“Accelerometer Error”, MessageBoxButton.OK);
}
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Stop the accelerometer
try
{
this.accelerometer.Stop();
}
catch { /* Nothing to do */ }
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerReadingEventArgs e)
{
if (ShakeDetection.JustShook(e))
{
// We’re on a different thread, so transition to the UI thread
this.Dispatcher.BeginInvoke(delegate()
{
// Play each sound, which builds on top
// of previously-playing sound effects
if (Settings.IsLowChosen.Value)
SoundEffects.Low.Play();
if (Settings.IsMediumChosen.Value)
SoundEffects.Medium.Play();
if (Settings.IsHighChosen.Value)
SoundEffects.High.Play();
});
}
}
// Application bar handlers
void SettingsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/AboutPage.xaml”,
UriKind.Relative));
}
}
}

[/code]

  • This listing makes use of three persisted settings defined in a separate Settings.cs file as follows:

    [code]
    public static class Settings
    {
    public static readonly Setting<bool> IsLowChosen =
    new Setting<bool>(“IsLowChosen”, false);
    public static readonly Setting<bool> IsMediumChosen =
    new Setting<bool>(“IsMediumChosen”, true);
    public static readonly Setting<bool> IsHighChosen =
    new Setting<bool>(“IsHighChosen”, false);
    }
    [/code]

  • This app supports playing one, two, or three sounds simultaneously when a shake is detected. Any previously playing sound effects are not stopped by the calls to Play. The resulting additive effect means that shaking more vigorously (technically, more frequently) causes the sound to be louder.
  • The shake detection is done with a simple JustShook method defined in Listing 46.3.

LISTING 46.3 ShakeDetection.cs—The Shake Detection Algorithm

[code]

using System;
using Microsoft.Devices.Sensors;
namespace WindowsPhoneApp
{
public static class ShakeDetection
{
static AccelerometerReadingEventArgs previousData;
static int numShakes;
// Two properties for controlling the algorithm
public static int RequiredConsecutiveShakes { get; set; }
public static double Threshold { get; set; }
static ShakeDetection()
{
RequiredConsecutiveShakes = 1;
Threshold = .7;
}
// Call this with the accelerometer data
public static bool JustShook(AccelerometerReadingEventArgs e)
{
if (previousData != null)
{
if (IsShaking(previousData, e, Threshold))
{
numShakes++;
if (numShakes == RequiredConsecutiveShakes)
{
// Just shook!
numShakes = 0;
return true;
}
}
else if (!IsShaking(previousData, e, .2))
numShakes = 0;
}
previousData = e;
return false;
}
// It’s a shake if the values in at least two dimensions
// are different enough from the previous values
static bool IsShaking(AccelerometerReadingEventArgs previous,
AccelerometerReadingEventArgs current, double threshold)
{
double deltaX = Math.Abs(previous.X – current.X);
double deltaY = Math.Abs(previous.Y – current.Y);
double deltaZ = Math.Abs(previous.Z – current.Z);
return (deltaX > threshold && deltaY > threshold) ||
(deltaY > threshold && deltaZ > threshold) ||
(deltaX > threshold && deltaZ > threshold);
}
}
}

[/code]

  • The core part of the algorithm—the IsShaking method—simply checks for two out of the three data points being sufficiently different from the previous set of data points. “Sufficiently different” is determined by a threshold that defaults to .7 but can be changed by the consumer of this class.
  • JustShook keeps track of the previous data and calls IsShaking. It uses the RequiredConsecutiveShakes property to support specifying a number of consecutive shakes required before considering that a shake has happened.
  • Although it’s a good idea for apps to provide calibration functionality when the accelerometer is used, it’s not really necessary for something as coarse as shake detection. Any differences between phones are not likely to be noticed.

The Settings Page

The settings page enables customization of the sound effects.
FIGURE 46.1 The settings page enables customization of the sound effects.

The settings page, shown in Figure 46.1, enables the user to turn on/off any of the three noises with simple check boxes. Listing 46.4 contains the XAML, and Listing 46.5 contains the codebehind.

LISTING 46.4 SettingsPage.xaml—The User Interface for Noise Maker’s Settings Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.SettingsPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<Grid Background=”{StaticResource PhoneBackgroundBrush}”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard settings header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”SETTINGS” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”noise maker”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<StackPanel Margin=”{StaticResource PhoneMargin}”>
<TextBlock Text=”Choose one, two, or three noises”
Foreground=”{StaticResource PhoneSubtleBrush}”
Margin=”{StaticResource PhoneMargin}”/>
<CheckBox x:Name=”LowCheckBox” Content=”low”
Checked=”CheckBox_IsCheckedChanged”
Unchecked=”CheckBox_IsCheckedChanged” local:Tilt.IsEnabled=”True”/>
<CheckBox x:Name=”MediumCheckBox” Content=”medium”
Checked=”CheckBox_IsCheckedChanged”
Unchecked=”CheckBox_IsCheckedChanged” local:Tilt.IsEnabled=”True”/>
<CheckBox x:Name=”HighCheckBox” Content=”high”
Checked=”CheckBox_IsCheckedChanged”
Unchecked=”CheckBox_IsCheckedChanged” local:Tilt.IsEnabled=”True”/>
<!– A warning for when no sounds are checked –>
<StackPanel x:Name=”WarningPanel” Visibility=”Collapsed”
Orientation=”Horizontal” Margin=”12,4,0,0”>
<!– Use the image as an opacity mask for the rectangle, so the image
visible in both dark and light themes –>
<Rectangle Fill=”{StaticResource PhoneForegroundBrush}”
Width=”48” Height=”48”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Shared/Images/normal.error.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<TextBlock Text=”No sounds will be made unless you check at least one!”
TextWrapping=”Wrap” Width=”350”
Margin=”{StaticResource PhoneMargin}”/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 46.5 SettingsPage.xaml.cs—The Code-Behind for Noise Maker’s Settings Page

[code]

using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
public SettingsPage()
{
InitializeComponent();
ShowOrHideWarning();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Save the settings
Settings.IsLowChosen.Value = this.LowCheckBox.IsChecked.Value;
Settings.IsMediumChosen.Value = this.MediumCheckBox.IsChecked.Value;
Settings.IsHighChosen.Value = this.HighCheckBox.IsChecked.Value;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the settings
this.LowCheckBox.IsChecked = Settings.IsLowChosen.Value;
this.MediumCheckBox.IsChecked = Settings.IsMediumChosen.Value;
this.HighCheckBox.IsChecked = Settings.IsHighChosen.Value;
}
void CheckBox_IsCheckedChanged(object sender, RoutedEventArgs e)
{
ShowOrHideWarning();
}
void ShowOrHideWarning()
{
if (!this.LowCheckBox.IsChecked.Value &&
!this.MediumCheckBox.IsChecked.Value &&
!this.HighCheckBox.IsChecked.Value)
this.WarningPanel.Visibility = Visibility.Visible;
else
this.WarningPanel.Visibility = Visibility.Collapsed;
}
}
}

[/code]

Other than a straightforward mapping of three check boxes to three settings, this page contains a warning that gets shown when all three check boxes are unchecked. This is shown in Figure 46.2.

The settings page shows a warning if all check boxes are unchecked.
FIGURE 46.2 The settings page shows a warning if all check boxes are unchecked.

The Finished Product

Noise Maker (Shake)

Boxing Glove (Accelerometer Basics)

Boxing Glove is an app for people who feel like being immature (or people who simply are immature). With it, you can throw punches into the air and hear a variety of punching/groaning sound effects. The punching sound effects occur right when you hit your imaginary target.

Boxing Glove supports right- or left-handed punching, and has a few entertaining features, such as a button that makes a “ding ding ding” bell sound as if to signify the start of a fight.

To provide the effect of making a punching sound at the appropriate time, this app uses the phone’s accelerometer.

The Accelerometer

Several times a second, the phone’s accelerometer reports the direction and magnitude of the total force being applied to the phone. This force is expressed with three values—X, Y, and Z—where X is horizontal, Y is vertical, and Z is perpendicular to the screen. This is illustrated in Figure 44.1.

The three accelerometer dimensions, relative to the phone screen.
FIGURE 44.1 The three accelerometer dimensions, relative to the phone screen.

The magnitude of each value is a multiplier of g (the gravitational force on the surface of Earth). Each value is restricted to a range from -2 to 2. If the phone is resting flat on a table with the screen up, the values reported for X and Y are roughly zero, and the value of Z is roughly -1 (1 g into the screen toward the ground). That’s because the only force being applied to the phone in this situation is gravity. By shifting the phone’s angle and orientation and then keeping it roughly still, the values of X, Y, and Z reveal which way is down in the real world thanks to the ever-present force of gravity. When you abruptly move or shake the phone, the X, Y, and Z values are able to reveal this activity as well.

Regardless of how you contort your phone, the X,Y, and Z axes used for the accelerometer data remain fixed to the phone. For example, the Y axis always points toward the top edge of the phone.

To get the accelerometer data, you create an instance of the Accelerometer class from the Microsoft.Devices.Sensors namespace in the Microsoft.Devices.Sensors assembly.

This assembly is not referenced by Windows Phone projects by default, so you must add it via the Add Reference dialog in Visual Studio.

The Accelerometer class exposes Start and Stop methods and—most importantly— a ReadingChanged event. This event gets raised many times a second (after Start is called) and reports the data via properties on the event-args parameter passed to handlers. These properties are X, Y, Z, and Timestamp. The physical accelerometer is always running; Start and Stop simply start/stop the data reporting to your app.

Sounds pretty simple, right? The class is indeed simple although, as you’ll see in the remaining chapters, interpreting the data in a satisfactory way can be complicated.

To get the best performance and battery life, it’s good to stop the accelerometer data reporting when you don’t need the data and then restart it when you do.

The main page, with its application bar menu expanded.
FIGURE 44.2 The main page, with its application bar menu expanded.

The User Interface

Boxing Glove has a main page, a settings page, an instructions page, and an about page. The latter two pages aren’t interesting and therefore aren’t shown in this chapter, but Listing 44.1 contains the XAML for the main page. The page, with its application bar menu expanded, is shown in Figure 44.2.

LISTING 44.1 MainPage.xaml—The User Interface for Boxing Glove’s Main Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
SupportedOrientations=”Portrait”>
<!– The application bar, with two buttons and three menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.5”>
<shell:ApplicationBarIconButton Text=”ring bell”
IconUri=”/Images/appbar.bell.png” Click=”RingBellButton_Click” />
<shell:ApplicationBarIconButton Text=”switch hand”
IconUri=”/Images/appbar.leftHand.png”
Click=”SwitchHandButton_Click” />
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”instructions”
Click=”InstructionsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”settings”
Click=”SettingsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about”
Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Border Background=”{StaticResource PhoneAccentBrush}”>
<Image Source=”Images/hand.png” RenderTransformOrigin=”.5,.5”>
<Image.RenderTransform>
<!– ScaleX is 1 for right-handed or -1 for left-handed –>
<CompositeTransform x:Name=”ImageTransform” ScaleX=”1”/>
</Image.RenderTransform>
</Image>
</Border>
</phone:PhoneApplicationPage>

[/code]

Besides the application bar, this page basically contains an image that instructs the user how to hold the phone. The background takes on the theme accent color, so the screen in Figure 44.2 is from a phone whose accent color is set to red.

The application bar contains a button for performing a “ding ding ding” bell sound on demand (to mimic the start of a fight in a boxing ring), and a button for swapping between right-handed mode and left-handed mode. The composite transform flips the image horizontally (in code-behind) when left-handed mode is in use.

The Code-Behind

Listing 44.2 contains the code-behind for the main page. It makes use of two persisted settings defined in a separate Settings.cs file as follows:

[code]

public static class Settings
{
public static readonly Setting<bool> IsRightHanded =
new Setting<bool>(“IsRightHanded”, true);
public static readonly Setting<double> Threshold =
new Setting<double>(“Threshold”, 1.5);
}

[/code]

LISTING 44.2 MainPage.xaml.cs—The Code-Behind for Boxing Glove’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Devices.Sensors;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
Accelerometer accelerometer;
IApplicationBarIconButton switchHandButton;
DateTimeOffset acceleratingQuicklyForwardTime = DateTimeOffset.MinValue;
Random random = new Random();
double currentThreshold;
public MainPage()
{
InitializeComponent();
this.switchHandButton = this.ApplicationBar.Buttons[1]
as IApplicationBarIconButton;
// Initialize the accelerometer
this.accelerometer = new Accelerometer();
this.accelerometer.ReadingChanged += Accelerometer_ReadingChanged;
SoundEffects.Initialize();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Start the accelerometer
try
{
this.accelerometer.Start();
}
catch
{
MessageBox.Show(
“Unable to start your accelerometer. Please try running this app again.”,
“Accelerometer Error”, MessageBoxButton.OK);
}
// Also ensures the threshold is updated on return from settings page
UpdateForCurrentHandedness();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Stop the accelerometer
try
{
this.accelerometer.Stop();
}
catch { /* Nothing to do */ }
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerReadingEventArgs e)
{
// Only pay attention to large-enough magnitudes in the X dimension
if (Math.Abs(e.X) < Math.Abs(this.currentThreshold))
return;
// See if the force is in the same direction as the threshold
// (forward punching motion)
if (e.X * this.currentThreshold > 0)
{
// Forward acceleration
this.acceleratingQuicklyForwardTime = e.Timestamp;
}
else if (e.Timestamp – this.acceleratingQuicklyForwardTime
< TimeSpan.FromSeconds(.2))
{
// This is large backward force shortly after the forward force.
// Time to make the punching noise!
this.acceleratingQuicklyForwardTime = DateTimeOffset.MinValue;
// We’re on a different thread, so transition to the UI thread.
// This is a requirement for playing the sound effect.
this.Dispatcher.BeginInvoke(delegate()
{
switch (this.random.Next(0, 4))
{
case 0: SoundEffects.Punch1.Play(); break;
case 1: SoundEffects.Punch2.Play(); break;
case 2: SoundEffects.Punch3.Play(); break;
case 3: SoundEffects.Punch4.Play(); break;
}
switch (this.random.Next(0, 10)) // Only grunt some of the time
{
case 0: SoundEffects.Grunt1.Play(); break;
case 1: SoundEffects.Grunt2.Play(); break;
case 2: SoundEffects.Grunt3.Play(); break;
}
});
}
}
void UpdateForCurrentHandedness()
{
this.currentThreshold = (Settings.IsRightHanded.Value ?
Settings.Threshold.Value :
-Settings.Threshold.Value);
this.ImageTransform.ScaleX = (Settings.IsRightHanded.Value ? 1 : -1);
// Show the opposite hand on the application bar button
if (Settings.IsRightHanded.Value)
this.switchHandButton.IconUri = new Uri(“/Images/appbar.leftHand.png”,
UriKind.Relative);
else
this.switchHandButton.IconUri = new Uri(“/Images/appbar.rightHand.png”,
UriKind.Relative);
}
// Application bar handlers
void RingBellButton_Click(object sender, EventArgs e)
{
SoundEffects.DingDingDing.Play();
}
void SwitchHandButton_Click(object sender, EventArgs e)
{
Settings.IsRightHanded.Value = !Settings.IsRightHanded.Value;
UpdateForCurrentHandedness();
}
void InstructionsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void SettingsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/AboutPage.xaml”,
UriKind.Relative));
}
}
}

[/code]

  • The constructor contains the code for initializing the accelerometer. It constructs an instance of Accelerometer and attaches a handler to its ReadingChanged event. The SoundEffects class, defined in Listing 44.3, is also initialized.
  • OnNavigatedTo contains the code for starting the accelerometer, and OnNavigatedTo stops it. If it weren’t stopped, the handler would still be called (and the sound effects would still be made) while the main page is on the back stack. This would happen when the user visits the settings, instructions, or about pages. Leaving the accelerometer running would not be a problem, and may even be desirable for some apps. However, Listing 44.1 stops it to avoid two threads potentially reading/writing the threshold variable at the same time, as discussed in a later sidebar.

The calls to Accelerometer.Start and Accelerometer.Stop can throw an exception!

This can happen at development-time if you omit the ID_CAP_SENSORS capability from your app manifest. For a published app in the marketplace (which automatically gets the appropriate capability), this should not happen unless you have previously called the Accelerometer instance’s Dispose method.Of course, there’s not much that many accelerometer-based apps can do when the accelerometer fails to start, so Boxing Glove simply instructs to user to try closing and reopening the app and hope for the best.

  • Inside Accelerometer_ReadingChanged, the handler for the ReadingChanged event, only two properties of the AccelerometerReadingEventArgs instance are examined: X and Timestamp. The algorithm is as follows: If the app detects a strong forward horizontal force followed quickly by a strong backward horizontal force, it’s time to make a punching sound. Making the sound when the forward force is detected isn’t good enough, because the sound would be made too early. The sound should occur when the punching motion stops (i.e. hits the imaginary target of the punch). The detected backward force does not result from the phone being moved backward, but rather from the fast deceleration that occurs when the user stops their flying fist in mid-air.
  • The definition of “strong” used by Accelerometer_ReadingChanged’s algorithm is determined by a threshold that is configurable by the user on the settings page. The absolute value of the threshold ranges from almost 0 (.1) to almost 2 (1.9). The sign of the threshold depends on whether the app is in right-handed mode or lefthanded mode. In right-handed mode, forward motion means pushing to phone toward its left, so the threshold of forward force is negative. In left-handed mode, forward motion means pushing the phone toward its right, so the threshold of forward force is positive. This adjustment is made inside UpdateForCurrentHandedness.
  • When it’s time to make a punching noise, the sound is randomly chosen, potentially along with a randomly chosen grunting sound.

The accelerometer’s ReadingChanged event is raised on a non-UI thread!

This is great for processing the data without creating a bottleneck on the UI thread, but it does mean that you must explicitly transition to the UI thread before performing any work that requires it.This includes updating any UI elements or, as in this app, playing a sound effect. Listing 44.2 uses the page dispatcher’s BeginInvoke method to play the sound effect on the UI thread.

Although you can start the accelerometer from a page’s constructor, it’s normally better to wait until an event such as Loaded.That’s because starting it within the constructor could cause the ReadingChanged event to be raised earlier than when you’re prepared to handle it. For example, it can be raised before (or during) the deserialization of persisted settings, causing a failure if a setting is accessed in the event handler. It can be raised before the CompositionTarget.Rendering event is raised,which would be a problem if the implementation of SoundEffects (shown in the next listing) waited for the first Rendering event to call XNA’s FrameworkDispatcher.Update method.

Boxing Glove chooses to start the accelerometer in OnNavigatedTo and stop it in OnNavigatedFrom so the ReadingChanged event-handler thread doesn’t try to access the threshold member while the call to UpdateForCurrentHandedness inside OnNavigatedTo potentially updates it on the UI thread.

The SoundEffects class used by Listing 44.2 is shown in Listing 44.3. It encapsulates the work of setting up the sound effects, with code similar to the apps from Part V, “Audio & Video.”

LISTING 44.3 SoundEffects.cs—Initializes and Exposes Boxing Glove’s Eight Sound Effects

[code]

using System;
using System.Windows.Resources;
using Microsoft.Xna.Framework.Audio; // For SoundEffect
namespace WindowsPhoneApp
{
public static class SoundEffects
{
public static void Initialize()
{
StreamResourceInfo info;
info = App.GetResourceStream(new Uri(“Audio/punch1.wav”, UriKind.Relative));
Punch1 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/punch2.wav”, UriKind.Relative));
Punch2 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/punch3.wav”, UriKind.Relative));
Punch3 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/punch4.wav”, UriKind.Relative));
Punch4 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/grunt1.wav”, UriKind.Relative));
Grunt1 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/grunt2.wav”, UriKind.Relative));
Grunt2 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/grunt3.wav”, UriKind.Relative));
Grunt3 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/dingDingDing.wav”,
UriKind.Relative));
DingDingDing = SoundEffect.FromStream(info.Stream);
CompositionTarget.Rendering += delegate(object sender, EventArgs e)
{
// Required for XNA Sound Effect API to work
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
};
// Call also once at the beginning
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
}
public static SoundEffect Punch1 { get; private set; }
public static SoundEffect Punch2 { get; private set; }
public static SoundEffect Punch3 { get; private set; }
public static SoundEffect Punch4 { get; private set; }
public static SoundEffect Grunt1 { get; private set; }
public static SoundEffect Grunt2 { get; private set; }
public static SoundEffect Grunt3 { get; private set; }
public static SoundEffect DingDingDing { get; private set; }
}
}

[/code]

The Settings Page

The settings page, shown in Figure 44.3, contains a slider and a reset button for adjusting the threshold value from .1 to 1.9. The XAML is shown in Listing 44.4 and its codebehind is in Listing 44.5.

The settings page enables the user to adjust the accelerometer threshold, described as “required punching strength.”
FIGURE 44.3 The settings page enables the user to adjust the accelerometer threshold, described as “required punching strength.”

LISTING 44.4 SettingsPage.xaml—The User Interface for Boxing Glove’s Settings Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.SettingsPage” x:Name=”Page”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”
shell:SystemTray.IsVisible=”True”>
<Grid Background=”{StaticResource PhoneBackgroundBrush}”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard settings header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”SETTINGS” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”boxing glove”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<StackPanel Margin=”{StaticResource PhoneMargin}”>
<TextBlock Text=”Required punching strength”
Foreground=”{StaticResource PhoneSubtleBrush}”
Margin=”{StaticResource PhoneMargin}”/>
<Slider x:Name=”StrengthSlider” Minimum=”.1” Maximum=”1.9”
LargeChange=”.1”
Value=”{Binding Threshold, Mode=TwoWay, ElementName=Page}”/>
<Button Content=”reset” Click=”ResetButton_Click”
local:Tilt.IsEnabled=”True”/>
<TextBlock TextWrapping=”Wrap” Margin=”{StaticResource PhoneMargin}”
Text=”…”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 44.5 SettingsPage.xaml.cs—The Code-Behind for Boxing Glove’s Settings Page

[code]

using System.Windows;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
public SettingsPage()
{
InitializeComponent();
}
// Simple property bound to the slider
public double Threshold
{
get { return Settings.Threshold.Value; }
set { Settings.Threshold.Value = value; }
}
void ResetButton_Click(object sender, RoutedEventArgs e)
{
this.StrengthSlider.Value = Settings.Threshold.DefaultValue;
}
}
}

[/code]

The Finished Product

Boxing Glove (Accelerometer Basics)

Darts (Gesture Listener & Flick Gesture)

Darts is an addictive and slick game that provides a great reason to demonstrate support for the flick gesture (a single-finger, rapid swipe). To make detecting flicks easy, this app leverages a feature in the Silverlight for Windows Phone Toolkit called the gesture listener.

In this one-player game, you throw a dart by flicking the screen. The direction and strength of your flick determines the angle and distance of the throw. Depending on where the dart lands, you could earn 0–60 points with each throw. Try to get the highest score possible with 20 darts!

This game has a lot of potential for easy-to-add enhancements. For some examples, check out my “Hooked on Darts” version of this app in the Windows Phone Marketplace.

Detecting Gestures

The three preceding apps in this part of the book leverage touch points in a straightforward fashion. Often, however, you want to support standard gestures such as flicks, pinch-and-stretch zooming, and rotation. Detecting such gestures based on raw touch point data from the FrameReported event would be a major undertaking.

Fortunately, two options exist that make detecting gestures much easier: Silverlight’s manipulation events and the Silverlight for Windows Phone Toolkit’s gesture listener.

Manipulation Events

Silverlight defines three manipulation events on every UI element:

  • ManipulationStarted
  • ManipulationDelta
  • ManipulationCompleted

These events combine the information from all active touch points and package the data in an easy-to-consume form. ManipulationStarted gets raised when the TouchDown action happens for the first finger. ManipulationDelta gets raised for each TouchMove. ManipulationCompleted gets raised after TouchUp is reported for all fingers.

The ManipulationDelta event gives you information about how the relevant element is expected to be translated or scaled based on a one-finger panning gesture or a two-finger pinch or stretch. The ManipulationDelta and ManipulationCompleted events also provide translation and scale velocities in both X and Y dimensions.

Gesture Listener

Although the manipulation events are helpful for detecting gestures, the Silverlight for Windows Phone Toolkit’s gesture listener raises the bar for simplicity and ease-of-use. In fact, all the remaining apps in this part of the book use the gesture listener instead of the manipulation events.

The GestureListener class exposes nothing but 12 events for detecting 6 types of gestures:

  • Tap (with the Tap event)
  • Double Tap (with the DoubleTap event)
  • Touch & Hold (with the Hold event)
  • Flick (with the Flick event)
  • Pinch & Stretch (with the PinchStarted, PinchDelta, and PinchCompleted events)
  • Drag (with the DragStarted, DragDelta, and DragCompleted events)

The remaining two events— GestureBegin and GestureCompleted— are generic events raised for any gestures, analogous to the ManipulationStarted and ManipulationCompleted events.

The Tap event is similar to MouseLeftButtonUp, except that it only gets raised if there’s no movement between the finger press and release.

A gesture listener can be attached to any element with the GestureService class, which exposes a GestureListener attachable property. For example, the following XAML detects a touch & hold gesture (pressing an element for one second) on a text block:

[code]

<TextBlock …>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener Hold=”GestureListener_Hold”/>
</toolkit:GestureService.GestureListener>
</TextBlock>

[/code]

The gesture listener can cause performance problems!

As of the February 2011 version of the Silverlight for Windows Phone Toolkit, the gesture listener internally uses XNA’s TouchPanel class to detect gestures.However, this can cause performance problems and can even interfere with the input received by other Silverlight controls.Therefore, until these issues are resolved in a future version of the toolkit, you should avoid using the gesture listener (or the XNA TouchPanel) in traditional apps with typical controls.Darts and the other apps in this book that use the gesture listener do not have such problems.

The User Interface

Darts has a main page, an instructions page, and an about page. The latter two pages aren’t interesting and therefore aren’t shown in this chapter, but Listing 40.1 contains the XAML for the main page. The main page has an “intro panel” canvas that does triple duty as a welcome screen, a game-over screen, as well as a paused screen. Underneath the intro panel is a canvas with all the game elements. Figure 40.1 shows the intro panel in its initial state and then the appearance of the page once the user flicks the screen to start the game.

The main page overlays an intro panel on top of the game canvas.
FIGURE 40.1 The main page overlays an intro panel on top of the game canvas.

LISTING 40.1 MainPage.xaml—The User Interface for Darts’Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
FontFamily=”Segoe WP Black” FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”White” SupportedOrientations=”Portrait”>
<!– Allow the gesture anywhere on the page –>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener Flick=”GestureListener_Flick”/>
</toolkit:GestureService.GestureListener>
<!– Add several animations to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– The dart-throwing animation, adjusted from code-behind based on
the angle and velocity of the flick –>
<Storyboard x:Name=”DartThrowStoryboard”
Completed=”DartThrowStoryboard_Completed”>
<!– Animate the horizontal position –>
<DoubleAnimation x:Name=”DartXAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateX” Duration=”0:0:.5”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Animate the vertical position –>
<DoubleAnimation x:Name=”DartYAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateY” Duration=”0:0:.5”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Animate the horizontal scale –>
<DoubleAnimation x:Name=”DartScaleXAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”ScaleX” By=”1.2”
Duration=”0:0:.25” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Animate the vertical scale –>
<DoubleAnimation x:Name=”DartScaleYAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”ScaleY” By=”4”
Duration=”0:0:.25” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Leave the dart in its ending position for half a second before returning
it to the bottom of the page for another throw –>
<Storyboard x:Name=”DartDelayStoryboard” Duration=”0:0:.5”
Completed=”DartDelayStoryboard_Completed”/>
<!– Move the dart back to the starting position –>
<Storyboard x:Name=”DartReturnStoryboard”>
<DoubleAnimation Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateX” To=”0”
Duration=”0:0:.2”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateY” To=”0”
Duration=”0:0:.2”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Slide the dart completely off the bottom of the screen –>
<Storyboard x:Name=”DartOffScreenStoryboard”>
<DoubleAnimation Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateY” To=”130”
Duration=”0:0:.6”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Slide the intro panel off the top of the screen –>
<Storyboard x:Name=”IntroOffStoryboard”
Completed=”IntroOffStoryboard_Completed”>
<DoubleAnimation Storyboard.TargetName=”IntroPanelTransform”
Storyboard.TargetProperty=”TranslateY” To=”-800”
Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Slide the intro panel back onto the screen from up above –>
<Storyboard x:Name=”IntroOnStoryboard”>
<DoubleAnimation Storyboard.TargetName=”IntroPanelTransform”
Storyboard.TargetProperty=”TranslateY” To=”0”
Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
… more storyboards …
<!– Animate in (then out) a message, such as the # of points just earned –>
<Storyboard x:Name=”ShowMessageStoryboard”
Storyboard.TargetName=”MessageTransform”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”TranslateY”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.5” Value=”430”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:2.5” Value=”430”/>
<EasingDoubleKeyFrame KeyTime=”0:0:3” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with two buttons and one menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar BackgroundColor=”#5F3000” ForegroundColor=”White”
Opacity=”.8”>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png” Click=”DeleteButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid Background=”#5F3000”>
<Grid.Clip>
<RectangleGeometry Rect=”0,0,480,800”/>
</Grid.Clip>
<!– The game canvas –>
<Canvas>
<!– The dartboard background –>
<Ellipse Width=”480” Height=”480” Fill=”#8000”/>
<!– The dartboard segments –>
<Canvas x:Name=”DartboardSegments”>
<!– Vector art created in Expression Blend. Each region with a different
point value is a distinct Path element (82 total) –>
<Path x:Name=”D1” Data=”M268.269483095,61.50827908C287.000166325,
64.476988515,305.148564115,70.37132529,322.042831735,
78.980320525L240.000027675,240.000437465z” Fill=”Green”
Canvas.Left=”239.417” Canvas.Top=”60.925” Stretch=”Fill”
Width=”83.209” Height=”179.658”/>
… 81 more paths …
</Canvas>
<!– The dartboard numbers –>
<Canvas Opacity=”.5”>
<TextBlock Text=”20” Canvas.Left=”218” Canvas.Top=”1”
FontFamily=”Segoe WP” FontSize=”40”/>
… 19 more text blocks …
</Canvas>
<!– Each time a dart lands, a little black “hole” is placed in this
canvas to mark the position –>
<Canvas x:Name=”HolesCanvas”/>
<!– The dart –>
<Image x:Name=”DartImage” Canvas.Top=”675” Canvas.Left=”202”
Width=”100”>
<Image.RenderTransform>
<CompositeTransform x:Name=”DartTransform” TranslateY=”130”/>
</Image.RenderTransform>
</Image>
</Canvas>
<!– A display for the current score and # of darts remaining –>
<StackPanel x:Name=”ScorePanel” Visibility=”Collapsed”
VerticalAlignment=”Bottom” HorizontalAlignment=”Right”
Margin=”18,64”>
<TextBlock Text=”SCORE” Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”ScoreTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-20,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”ScoreTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock Text=”DARTS REMAINING”
Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”DartsRemainingTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-20,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”DartsRemainingTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
<!– The welcome/paused/game-over screen –>
<Grid x:Name=”IntroPanel”>
<Grid.RenderTransform>
<CompositeTransform x:Name=”IntroPanelTransform”/>
</Grid.RenderTransform>
<Rectangle Fill=”#5F3000” Opacity=”.6”/>
<!– The title and subtitle –>
<TextBlock Text=”DARTS” Margin=”-19,200,0,0” FontSize=”140”>
<TextBlock.RenderTransform>
<RotateTransform Angle=”-20”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock x:Name=”Subtitle” Text=”DO A PRACTICE FLICK TO BEGIN”
Margin=”40,340,-40,0” FontSize=”27”>
<TextBlock.RenderTransform>
<RotateTransform Angle=”-20”/>
</TextBlock.RenderTransform>
</TextBlock>
<!– A display for the best score, average score, and # of games –>
<StackPanel x:Name=”StatsPanel” VerticalAlignment=”Bottom”
HorizontalAlignment=”Right” Margin=”18,64”>
<TextBlock Text=”BEST SCORE” Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”BestScoreTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-20,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”BestScoreTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock x:Name=”AvgScoreHeaderTextBlock” Text=”AVG SCORE”
Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”AvgScoreTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-20,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”AvgScoreTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
</Grid>
<!– An animated message –>
<TextBlock x:Name=”MessageTextBlock” RenderTransformOrigin=”.5,.5”
FontWeight=”Bold” HorizontalAlignment=”Center” FontSize=”120”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”MessageTransform” TranslateY=”800”/>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • To detect the flick gesture, this app handles the Flick event of a gesture listener attached to the whole page.
  • This page contains lots of storyboards for smoothly transitioning elements on and off the screen. The most important one is DartThrowStoryboard, which makes the dart travel based on the flick data. Its first two animations, which alter the dart transform’s TranslateX and TranslateY values, have their By values set in code-behind. The second two animations, which alter ScaleX and ScaleY, make the dart appear to jump out of and then back into the screen in a pseudo-3D arc motion. ScaleY is increased much more than ScaleX to make the image appear to flatten out, because the image already has Y-rotation perspective applied to it. The next section shows how the in-flight dart appears.
  • This app’s dartboard is one of the more interesting chunks of XAML in the whole book. It consists of 82 path elements and 19 text blocks on top of an ellipse. (Although I usually type all my XAML by hand, I created this part in Expression Blend!) Representing each region with a unique score value as a distinct element makes it easy to figure out how many points to award the user when the dart lands. The code-behind performs hit testing to find the topmost element hit by the dart and then awards points accordingly.

    Why are there 82 paths? Because there are 4 for each of the 20 wedges (the outer ring that awards double points, the outer “normal” wedge, the inner ring that awards triple points, and then the inner “normal” wedge), plus the outer and inner bull’s-eyes. Figure 40.2 illustrates how these 82 elements stack to form the final dartboard, as if the elements were separated and viewed at a 3D perspective.

  • The DartImage element doesn’t have its Source assigned. This is done in codebehind, because the source is continually changed between three different images.
  • The root grid has a clip applied to avoid a far-flung dart from causing the screen to go blank. This happens whenever Silverlight tries to render an element that is too far away.
A visualization of the 82 distinct dartboard segments.
FIGURE 40.2 A visualization of the 82 distinct dartboard segments.

The Code-Behind

Listing 40.2 contains the code-behind for the main page.

LISTING 40.2 MainPage.xaml.cs—The Code-Behind for Darts’Main Page

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
The Code-Behind 885
{
// Persistent settings
Setting<int> bestScore = new Setting<int>(“BestScore”, 0);
Setting<double> avgScore = new Setting<double>(“AvgScore”, 0);
Setting<int> numGames = new Setting<int>(“NumGames”, 0);
// Current game state
int dartsRemaining;
int score;
// Three different dart images
BitmapImage dartStartImageSource =
new BitmapImage(new Uri(“Images/dartStart.png”, UriKind.Relative));
BitmapImage dartBigImageSource =
new BitmapImage(new Uri(“Images/dartBig.png”, UriKind.Relative));
BitmapImage dartSmallImageSource =
new BitmapImage(new Uri(“Images/dartSmall.png”, UriKind.Relative));
public MainPage()
{
InitializeComponent();
// Use the starting dart image
this.DartImage.Source = this.dartStartImageSource;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the persisted values
UpdateStatsLabels();
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
// If the game is in progress, pause it rather than exiting the app
if (this.IntroPanel.Visibility == Visibility.Collapsed)
{
this.Subtitle.Text =
“PAUSED.nFLICK TO RESUME THE GAME,nOR PRESS BACK TO EXIT.”;
ShowIntroPanel();
e.Cancel = true;
}
}
// The event handler for flicks
void GestureListener_Flick(object sender, FlickGestureEventArgs e)
{
if (this.IntroPanel.Visibility == Visibility.Visible)
{
// Start a new game if we’re out of darts
if (this.dartsRemaining == 0)
{
// Remove all holes and clear the tracking variables
this.HolesCanvas.Children.Clear();
this.dartsRemaining = 20;
this.score = 0;
// Animate the off-screen dart to the starting position
this.DartReturnStoryboard.Begin();
// Animate a message
this.MessageTextBlock.Text = “GO!”;
this.ShowMessageStoryboard.Begin();
}
// Remove the intro panel (for both new-game and unpause cases)
HideIntroPanel();
}
else
{
// First ensure that a dart throw is not in progress
if (this.DartThrowStoryboard.GetCurrentState() == ClockState.Active ||
this.DartDelayStoryboard.GetCurrentState() == ClockState.Active)
return;
// Throw the dart!
this.DartTransform.TranslateX = 0;
this.DartTransform.TranslateY = 0;
// Here is where the flick data is used
this.DartXAnimation.By = e.HorizontalVelocity / 10;
this.DartYAnimation.By = e.VerticalVelocity / 10;
// The animation scales the image up by as much as 4 vertically, so
// replace the image with a higher-resolution one
this.DartImage.Source = this.dartBigImageSource;
this.DartThrowStoryboard.Begin();
}
}
void IntroOffStoryboard_Completed(object sender, EventArgs e)
{
// The intro panel is now off-screen, so collapse it for better performance
this.IntroPanel.Visibility = Visibility.Collapsed;
}
void DartThrowStoryboard_Completed(object sender, EventArgs e)
{
// The dart has landed, so change the image to a small one
// that looks better that this scale (and is angled like it’s sticking into
// the wall)
this.DartImage.Source = this.dartSmallImageSource;
// Determine the exact point where the tip of the dart has landed
Point point = new Point(
// X: The tip isn’t quite centered in the image, hence the slight offset
Canvas.GetLeft(this.DartImage) + this.DartTransform.TranslateX
+ this.DartImage.Width / 2 – 11.5,
// Y: The tip is at the top edge of the image
Canvas.GetTop(this.DartImage) + this.DartTransform.TranslateY
);
// Place a “hole” where the tip of the dart landed
Ellipse hole = new Ellipse { Fill = new SolidColorBrush(Colors.Black),
Width = 5, Height = 5 };
Canvas.SetLeft(hole, point.X – hole.Width / 2);
Canvas.SetTop(hole, point.Y – hole.Height / 2);
this.HolesCanvas.Children.Add(hole);
// Calculate the score for this throw
int pointsEarned = DetermineScoreAtPoint(point);
// Update the game state and display
this.score += pointsEarned;
this.dartsRemaining–;
UpdateScoreLabels(pointsEarned > 0);
// Let the dart sit for a while before it animates back to the start
this.DartDelayStoryboard.Begin();
if (this.dartsRemaining > 0)
{
// Animate in the score message
this.MessageTextBlock.Text = pointsEarned.ToString();
this.ShowMessageStoryboard.Begin();
}
else
{
// Game over!
// Update the stats
double oldTotal = this.avgScore.Value * this.numGames.Value;
// New average
this.avgScore.Value = (oldTotal + score) / (this.numGames.Value + 1);
// New total number of games
this.numGames.Value++;
this.Subtitle.Text = “FINAL SCORE: “ + this.score + “. FLICK AGAIN!”;
if (this.score > this.bestScore.Value)
{
// New best score
this.bestScore.Value = this.score;
// Animate a best-score message
this.MessageTextBlock.Text = “BEST!”;
this.ShowMessageStoryboard.Begin();
}
ShowIntroPanel();
}
}
void DartDelayStoryboard_Completed(object sender, EventArgs e)
{
// Restore the image
this.DartImage.Source = this.dartStartImageSource;
// Move the dart to the starting position for the next throw,
// or off-screen if the game is over
if (this.dartsRemaining > 0)
this.DartReturnStoryboard.Begin();
else
this.DartOffScreenStoryboard.Begin();
}
int DetermineScoreAtPoint(Point p)
{
// Retrieve all elements inside DartboardSegments that intersect with p
foreach (UIElement element in
VisualTreeHelper.FindElementsInHostCoordinates(p,
this.DartboardSegments))
{
// Fortunately, the list of elements is ordered from top-to-bottom.
// We only care about the top-most element, so we can directly return a
// value based on the first item in this collection.
if (element == this.InnerBull)
return 50;
else if (element == this.OuterBull)
return 25;
else if (element is Path)
{
// The elements are named with a letter and a number.
// The letter is D for double score, T for triple score, or something
// else (A or B) for a normal region.
// The number is the score.
string name = (element as FrameworkElement).Name;
// Retrieve the score from the name
int score = int.Parse(name.Substring(1));
// Apply a multiplier, if applicable
if (name[0] == ‘D’)
score *= 2;
else if (name[0] == ‘T’)
score *= 3;
return score;
}
}
// No relevant element was hit
return 0;
}
void ShowIntroPanel()
{
UpdateStatsLabels();
this.IntroOnStoryboard.Begin();
this.IntroPanel.Visibility = Visibility.Visible;
this.ScorePanel.Visibility = Visibility.Collapsed;
this.ApplicationBar.IsVisible = true;
}
void HideIntroPanel()
{
UpdateScoreLabels(true);
this.IntroOffStoryboard.Begin();
this.ScorePanel.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = false;
}
void UpdateStatsLabels()
{
if (this.numGames.Value > 0)
{
this.BestScoreTextBlock.Text = this.bestScore.Value.ToString();
this.AvgScoreTextBlock.Text = this.avgScore.Value.ToString(“0.#”);
if (this.numGames.Value == 1)
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE (1 GAME)”;
else
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE (“ + this.numGames.Value
+ “ GAMES)”;
}
else
{
this.BestScoreTextBlock.Text = “0”;
this.AvgScoreTextBlock.Text = “0”;
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE”;
}
// Animate the textblocks out then in
this.SlideAvgScoreStoryboard.Begin();
this.SlideBestScoreStoryboard.Begin();
}
void UpdateScoreLabels(bool animateScore)
{
this.ScoreTextBlock.Text = this.score.ToString();
this.DartsRemainingTextBlock.Text = this.dartsRemaining.ToString();
// Animate the textblocks out then in
this.SlideDartsRemainingStoryboard.Begin();
if (animateScore)
this.SlideScoreStoryboard.Begin();
}
// Application bar handlers
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to clear your scores?”,
“Delete history”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
this.numGames.Value = 0;
this.bestScore.Value = 0;
UpdateStatsLabels();
}
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Darts”, UriKind.Relative));
}
}
}

[/code]

  • Three dart image sources are created (once) and then assigned as DartImage’s Source value at various stages of the dart-throwing procedure. Figure 40.3 shows all three. The start image looks more vertical, whereas the big and small images have a lot more perspective. The small image is used while the dart is sticking into the dartboard (or wall). The big image is used only while the dart is in flight, because otherwise the up-scaling of the small image would produce a pixelated result. The combination of DartThrowStoryboard’s motion and the image-swapping produces a pretty realistic pseudo-3D dart motion demonstrated in Figure 40.4.
The three dart images are swapped in and out to provide the best-looking result.
FIGURE 40.3 The three dart images are swapped in and out to provide the best-looking result.
The dart’s flight path when it hits a double 3 (6 points).
FIGURE 40.4 The dart’s flight path when it hits a double 3 (6 points).
  • OnBackKeyPress is overridden to show a paused screen when the back key is pressed during a game, canceling the exiting of the app. When the intro panel is shown as a paused screen or game-over screen, the text under the “DARTS” title is updated to explain what is going on, as shown in Figure 40.5.
The intro screen subtitle updates to show that the game is paused or to show the final score.
FIGURE 40.5 The intro screen subtitle updates to show that the game is paused or to show the final score.

 

  • GestureListener_Flick is the flick handler that processes the data from FlickGestureEventArgs and adjusts the two relevant dartthrowing animations appropriately. The Flick event is described in the next section.
  • DetermineScoreAtPoint uses a static method called FindElementsInHostCoordinates on System.Windows.Media. VisualTreeHelper to find the topmost element underneath the passed-in point. (FindElementsInHostCoordinates also exposes an overload that accepts a rectangular region rather than a point.) Because of the naming scheme used by the dartboard elements, the hit element’s name can be converted into the correct point value.

Your game might fail marketplace certification if pressing the Back hardware button doesn’t pause the game!

The Windows Phone 7 Certification Requirements (http://go.microsoft.com/ ?linkid=9730558) state:

For games, when the Back button is pressed during gameplay, the game can choose to present a pause context menu or dialog or navigate the user to the prior menu screen. Pressing the Back button again while in a paused context menu or dialog closes the menu or dialog.

Although this makes the pausing behavior seem optional, I’ve had a game fail certification because it chose not to provide a pausing experience. (And this was a pool game in which pausing is meaningless!) Even if you can get away without doing this, it’s probably a good idea to have your app include this pausing behavior for consistency with user expectations for Windows Phone games.

VisualTreeHelper.FindElementsInHostCoordinates is the best way to determine what element or elements are underneath a specific point or region. Unlike approaches from previous chapters that involve mapping a point into an element’s coordinate space one element at a time, FindElementsInHostCoordinates examines all elements that are children of the passed-in element. (It also examines the passed-in element itself.)

The Flick Event

The FlickGestureEventArgs instance passed to the Flick event exposes the following properties:

  • Angle—A double value expressed in degrees
  • Direction—Either Horizontal or Vertical, revealing the primary direction of the flick
  • HorizontalVelocity—A double value expressing the horizontal magnitude of the 2D velocity vector in pixels per second
  • VerticalVelocity—A double value expressing the vertical magnitude of the 2D velocity vector in pixels per second

This app has no use for the Direction property, which is meant for elements whose motion should be locked to a single direction when the flick occurs. Instead, this app’s dart freely moves in whatever angle the flick reports, even if it’s completely horizontal or downward instead of upward.

Because Listing 40.2 applies the horizontal and vertical velocities as offsets to the dart’s position, it doesn’t even need to check the Angle property. The ratio between the two velocities effectively gives the appropriate angle. When set as offsets to the dart’s position, the velocity values are divided by 10 to give a distance range that works well for this app. This factor was arrived at by trial and error.

A flick event is not raised until the finger has broken contact with the screen. Before then, the finger motion raises drag (and other) events.

If you want to detect flicks without using the gesture listener (perhaps to avoid the problems discussed at the beginning of this chapter), you can use the ManipulationCompleted event defined on all UI elements.The data passed to its handlers includes a FinalVelocities property that is equivalent to the pair of HorizontalVelocity and VerticalVelocity properties exposed to Flick event handlers.

The Finished Product

Darts (Gesture Listener & Flick Gesture)

Reflex Test (Single Touch)

Reflex Test is a simple game in which you see how quickly you can tap the screen when a target appears. (You can tap anywhere on the screen; the target is just a visual gimmick.) The app keeps track of your fastest time as well as your average time.

The tap detection done by this app could easily be done with a simple MouseLeftButtonDown event handler, but instead this app serves as an introduction to multi-touch functionality. You can easily use the multi-touch infrastructure for single touches, as this app does.

The User Interface

Reflex Test has a main page, an instructions page, and an about page. The latter two pages aren’t interesting and therefore aren’t shown in this chapter, but Listing 37.1 contains the XAML for the main page.

Figure 37.1 illustrates main page’s user interface during the four stages of the reflex-testing process.

The main page goes through four stages as the app is used.
FIGURE 37.1 The main page goes through four stages as the app is used.

LISTING 37.1 MainPage.xaml—The User Interface for Reflex Test’s Main Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”>
<!– Add three animations to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– Slide the best time out then back in –>
<Storyboard x:Name=”SlideBestTimeStoryboard”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName=”BestTimeTransform”
Storyboard.TargetProperty=”TranslateX”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.4” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:.4” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.8” Value=”0”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName=”BestTimeTextBlock”
Storyboard.TargetProperty=”Visibility”>
<!– Ensure the time is visible on the way in,
even if collapsed on the way out –>
<DiscreteObjectKeyFrame KeyTime=”0:0:.4” Value=”Visible”/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<!– Slide the average time out then back in –>
<Storyboard x:Name=”SlideAvgTimeStoryboard”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName=”AvgTimeTransform”
Storyboard.TargetProperty=”TranslateX”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.4” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:.4” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.8” Value=”0”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName=”AvgTimeTextBlock”
Storyboard.TargetProperty=”Visibility”>
<!– Ensure the time is visible on the way in,
even if collapsed on the way out –>
<DiscreteObjectKeyFrame KeyTime=”0:0:.4” Value=”Visible”/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<!– Animate in (then out) a message, which will either say
“CONGRATULATIONS!” or “TOO EARLY!” –>
<Storyboard x:Name=”ShowMessageStoryboard”
Storyboard.TargetName=”MessageTransform”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”TranslateY”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.5” Value=”50”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:2.5” Value=”50”/>
<EasingDoubleKeyFrame KeyTime=”0:0:3” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with two buttons and one menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png” Click=”DeleteButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid Background=”Transparent”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– A target vector graphic and ellipse to mark where the screen
was touched, collapsed until appropriate to show –>
<Grid Grid.RowSpan=”2”>
<Grid x:Name=”TargetGrid” Visibility=”Collapsed”>
<Ellipse Width=”440” Height=”440” StrokeThickness=”35”
Stroke=”{StaticResource PhoneAccentBrush}”/>
<Ellipse Width=”300” Height=”300” StrokeThickness=”35”
Stroke=”{StaticResource PhoneAccentBrush}”/>
<Ellipse Width=”160” Height=”160” StrokeThickness=”35”
Stroke=”{StaticResource PhoneAccentBrush}”/>
<Ellipse x:Name=”TouchEllipse” Visibility=”Collapsed” Opacity=”.9”
Fill=”{StaticResource PhoneForegroundBrush}” Width=”100”
Height=”100” HorizontalAlignment=”Left” VerticalAlignment=”Top”/>
</Grid>
</Grid>
<!– Show indeterminate progress (dancing dots) while waiting for the target
to appear (IsIndeterminate is set from code-behind for perf reasons) –>
<ProgressBar x:Name=”ProgressBar” Visibility=”Collapsed”
VerticalAlignment=”Top”/>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”REFLEX TEST”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock x:Name=”PageTitle” Text=”tap to begin”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<Grid Grid.Row=”1” Margin=”{StaticResource PhoneHorizontalMargin}”>
<!– A display for the best time, average time, and # of tries –>
<StackPanel x:Name=”TimesPanel” VerticalAlignment=”Bottom”
HorizontalAlignment=”Right”>
<TextBlock Text=”BEST TIME” Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”BestTimeTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-15,0,30”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”BestTimeTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock x:Name=”AvgTimeHeaderTextBlock” Text=”AVG TIME”
Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”AvgTimeTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-15,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”AvgTimeTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
<!– A “CONGRATULATIONS!” or “TOO EARLY!” message –>
<TextBlock x:Name=”MessageTextBlock” RenderTransformOrigin=”.5,.5”
FontWeight=”Bold” HorizontalAlignment=”Center”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”MessageTransform” TranslateY=”800”/>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
</Grid>
</phone:PhoneApplicationPage>

[/code]

The first two animations are used to slide in the best time and average time text blocks the first time they are displayed, and to slide them out and then in when they are updated. The last animation slides a message onto and then off the screen: “CONGRATULATIONS!” when the user gets a new best time, or “TOO EARLY!” when the user taps the screen before the target appears.

The Code-Behind

Listing 37.2 contains the code-behind for the main page.

LISTING 37.2 MainPage.xaml.cs—The Code-Behind for Reflex Test’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Navigation;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Persistent settings
Setting<TimeSpan> bestTime = new Setting<TimeSpan>(“BestTime”,
TimeSpan.MaxValue);
Setting<TimeSpan> avgTime = new Setting<TimeSpan>(“AvgTime”,
TimeSpan.MaxValue);
Setting<int> numTries = new Setting<int>(“NumTries”, 0);
DispatcherTimer timer = new DispatcherTimer();
Random random = new Random();
DateTime beginTime;
DateTime targetShownTime;
bool tapToBegin;
public MainPage()
{
InitializeComponent();
this.timer.Tick += Timer_Tick;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Subscribe to the touch/multi-touch event.
// This is application-wide, so only do this when on this page.
Touch.FrameReported += Touch_FrameReported;
// Respect the persisted values
UpdateLabels(true);
// Reset
this.tapToBegin = true;
this.PageTitle.Text = “tap to begin”;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Unsubscribe from this application-wide event
Touch.FrameReported -= Touch_FrameReported;
}
void Touch_FrameReported(object sender, TouchFrameEventArgs e)
{
TouchPoint point = e.GetPrimaryTouchPoint(this);
if (point != null && point.Action == TouchAction.Down)
{
if (this.tapToBegin)
{
// Get started
this.tapToBegin = false;
this.PageTitle.Text = “”;
this.TargetGrid.Visibility = Visibility.Collapsed;
this.TouchEllipse.Visibility = Visibility.Collapsed;
// Show the indeterminate progress bar
this.ProgressBar.IsIndeterminate = true;
this.ProgressBar.Visibility = Visibility.Visible;
this.beginTime = DateTime.Now;
// Make the target appear between .5 sec and 7 sec from now
timer.Interval = TimeSpan.FromMilliseconds(random.Next(500, 7000));
timer.Start();
}
else if (this.TargetGrid.Visibility == Visibility.Visible)
{
// The target has been tapped
DateTime endTime = DateTime.Now;
// Position and show the ellipse where the screen was touched
this.TouchEllipse.Margin = new Thickness(
point.Position.X – this.TouchEllipse.Height / 2,
point.Position.Y – this.TouchEllipse.Height / 2, 0, 0);
this.TouchEllipse.Visibility = Visibility.Visible;
// Show the elapsed time
TimeSpan newTime = endTime – this.targetShownTime;
this.PageTitle.Text = newTime.TotalSeconds + “ sec”;
this.tapToBegin = true;
// Record this attempt and update the UI
double oldTotal = this.avgTime.Value.TotalSeconds * this.numTries.Value;
// New average
this.avgTime.Value = TimeSpan.FromSeconds(
(oldTotal + newTime.TotalSeconds) / (this.numTries.Value + 1));
// New total number of tries
this.numTries.Value++;
if (newTime < this.bestTime.Value)
{
// New best time
this.bestTime.Value = newTime;
UpdateLabels(true);
// Animate in a congratulations message
this.MessageTextBlock.Text = “CONGRATULATIONS!”;
this.ShowMessageStoryboard.Begin();
}
else
{
UpdateLabels(false);
}
}
else
{
// The screen has been tapped too early
// Cancel the timer that would show the target
this.timer.Stop();
// Hide the progress bar and turn off the indeterminate
// animation to avoid poor performance
this.ProgressBar.Visibility = Visibility.Collapsed;
this.ProgressBar.IsIndeterminate = false;
// Show exactly how early the tap was
DateTime endTime = this.beginTime + this.timer.Interval;
this.PageTitle.Text = (DateTime.Now – endTime).TotalSeconds + “ sec”;
this.tapToBegin = true;
// Animate in an explanatory message
this.MessageTextBlock.Text = “TOO EARLY!”;
this.ShowMessageStoryboard.Begin();
}
}
}
void Timer_Tick(object sender, EventArgs e)
{
// Show the target
this.TargetGrid.Visibility = Visibility.Visible;
// Hide the progress bar and turn off the indeterminate
// animation to avoid poor performance
this.ProgressBar.Visibility = Visibility.Collapsed;
this.ProgressBar.IsIndeterminate = false;
this.targetShownTime = DateTime.Now;
// We only want the Tick once
this.timer.Stop();
}
void UpdateLabels(bool animateBestTime)
{
if (this.numTries.Value > 0)
{
// Ensure the panel is visible and update the text blocks
this.TimesPanel.Visibility = Visibility.Visible;
this.BestTimeTextBlock.Text = this.bestTime.Value.TotalSeconds + “ sec”;
this.AvgTimeTextBlock.Text = this.avgTime.Value.TotalSeconds + “ sec”;
if (this.numTries.Value == 1)
this.AvgTimeHeaderTextBlock.Text = “AVG TIME (1 TRY)”;
else
this.AvgTimeHeaderTextBlock.Text = “AVG TIME (“ + this.numTries.Value
+ “ TRIES)”;
// Animate the textblocks out then in. The animations take care of
// showing the textblocks if they are collapsed.
this.SlideAvgTimeStoryboard.Begin();
if (animateBestTime)
this.SlideBestTimeStoryboard.Begin();
else
this.BestTimeTextBlock.Visibility = Visibility.Visible;
}
else
{
// Hide everything
this.TimesPanel.Visibility = Visibility.Collapsed;
this.BestTimeTextBlock.Visibility = Visibility.Collapsed;
this.AvgTimeTextBlock.Visibility = Visibility.Collapsed;
}
}
// Application bar handlers
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void DeleteButton_Click(object sender, EventArgs e)
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Reflex Text”, UriKind.Relative));
}
}
}

[/code]

  • The event for capturing touch and multi-touch activity has the odd name FrameReported and is exposed on a static class called Touch (from the System.Windows.Input namespace). This event gets raised for touch activity across the entire application, so Listing 37.1 attaches a handler to this event in OnNavigatedTo but removes the handler in OnNavigatedFrom.
  • Inside the FrameReported event handler (Touch_FrameReported), GetPrimaryTouchPoint is called to get single-touch data, which is all this app cares about. If multiple fingers are pressed on the screen, GetPrimaryTouchPoint returns data about the first finger that makes contact with the screen.
  • FrameReported gets raised for three types of actions: a finger making contact with the screen, a finger moving on the screen, and a finger being released from the screen. These actions are analogous to mouse down, mouse move, and mouse up events, although here they apply per finger. This app only cares about taps, so the logic inside Touch_FrameReported only runs when the primary touch point is a touching-down action.

Be sure to detach any FrameReported handlers as soon as possible!

In addition to the performance implication of FrameReported handlers being invoked when they don’t need to be, forgetting to detach from the event can cause other problems if the page containing the handler is no longer active.

  • In addition to the Action property, the TouchPoint class returned by GetPrimaryTouchPoint exposes Position and Size properties. This app only makes use of the position of the touch point in order to place TouchEllipse in the spot that was tapped.
  • Notice that the progress bar’s IsIndeterminate property is only set to true while it is visible.

You should ignore TouchPoint’s Size property!

The current version of Windows Phone doesn’t actually support the discovery of a touch point’s size, so the reported Size.Width and Size.Height are always 1. This is unfortunate, as it would be a nice touch (no pun intended) for Reflex Test to make TouchEllipse the size of the fingertip that touched the screen.

The Finished Product

Reflex Test (Single Touch)

Groceries (Panorama)

Groceries is a flexible shopping list app that enables you to set up custom aisle-by-aisle lists. Name and arrange as many isles as you want to match the layout of your favorite store! This app has a lot of features to make adding items easy, such as adding in bulk, selecting favorite items, and selecting recent items.

The Groceries app showcases the panorama control, which enables the other signature Windows Phone user interface paradigm—the one used by every “hub” on the phone (People, Pictures, and so on). Roughly speaking, a panorama acts very similarly to a pivot: It enables horizontal swiping between multiple sections on the same page. What makes it distinct is its appearance and complex animations.

The idea of a panorama is that the user is looking at one piece of a long, horizontal canvas. The user is given several visual hints to swipe horizontally. For example, the application title is larger than what fits on the screen at a single time (unless the title is really short) and each section is a little narrower than the screen, so the left edge of the next section is visible even when not actively panning the page. A panorama wraps around, so panning forward from the last section goes to the first section, and panning backward from the first section goes to the last section.

Figure 27.1 demonstrates how Groceries leverages the panorama control. The first section contains the entire shopping list and the last section contains the cart (items that the user has already grabbed). In between are a dynamic number of sections based on the user-defined aisles and whether there are any items still left to grab in each aisle.

FIGURE 27.1 The grocery panorama, shown the way panoramas are typically shown in marketing materials.
FIGURE 27.1 The grocery panorama, shown the way panoramas are typically shown in marketing materials.

Although the viewport-on-a-long-canvas presentation in Figure 27.1 is the way panoramas are usually shown, that image does not consist of five concatenated screenshots. The reality is much more complex. A panorama consists of three separate layers that each pan at a different speeds, producing a parallax effect. The background pans at the slowest rate, followed by the title, followed by the rest of the content, which moves at the typical scrolling/swiping speed. Figure 27.2 shows what the screen really looks like when visiting each of the five sections in Figure 27.1.

FIGURE 27.2 Real screenshots when visiting each of the five panorama sections from Figure 27.1.
FIGURE 27.2 Real screenshots when visiting each of the five panorama sections from Figure 27.1.

How should I choose between using a panorama versus using a pivot in my app?

The main consideration is your desired visual appearance. A panorama with a good background can provide a more attractive and interesting user interface than a pivot.This is true even if you use the same background for a pivot, thanks to panorama’s parallax panning. A panorama also has better support for horizontal scrolling within a single section, making it easier to have variable- width sections. In just about every other way, a pivot has advantages over a panorama. A pivot gives you more screen real estate for each section. A pivot can perform better for a large number of items and/or content for three reasons: its layout and animations are simpler, it delay-loads its items, and it provides APIs for advanced delay-loading or unloading. It’s also okay to use an application bar (and status bar) with a pivot, whereas it’s considered bad form to use one with a panorama. So if you want to expose several page-level actions, a pivot with an application bar is probably the best choice. The Groceries app is actually a more natural fit for a pivot rather than a panorama, as each section is nothing more than a filtered view of the same list. A typical panorama has sections that are more varied and visually interesting than what is used in Groceries, with plenty of thumbnails (like what you see in the phone’s Marketplace app). However, by using a panorama, Groceries leaves more of an impression with users and is more fun to use.

The Panorama Control

After reading about the Pivot control in the preceding chapter, the Panorama control should look familiar. Panorama, in the Microsoft.Phone.Controls namespace and Microsoft.Phone.Controls assembly, is an items control designed to work with content controls called PanoramaItem.

Although the behavior exhibited by a panorama is more complex than the behavior exhibited by a pivot, it exposes fewer APIs. Like Pivot, Panorama has Title and TitleTemplate properties and a HeaderTemplate property for customizing the headers of its children. Under normal circumstances, there’s no need to use these template properties because the control does a good job of providing the correct look and feel.

PanoramaItem has a Header property, but unlike PivotItem, it also exposes a HeaderTemplate property for customizing an individual header’s appearance. (Of course, you could always directly set Header to a custom UI element without the need for HeaderTemplate.) PanoramaItem has also exposes an Orientation property that indicates the intended direction of scrolling when content doesn’t fit. This property is Vertical by default, but setting it to Horizontal enables a single panorama item to extend wider than the screen. Note that you must add your own scroll viewer if you want scrolling in a vertical panorama item. In a horizontal panorama item, you don’t want to use a scroll viewer; the panorama handles it. Each horizontal panorama item has a maximum width of two screens (960 pixels).

Horizontal Panorama Items and Their Headers

In the panoramas used by the built-in apps, the panorama item header scrolls more slowly than the rest of the content when the panorama item is horizontal and wider than the screen. (This ensures that you can see at least part of the item’s header as long as you’re viewing some of that item’s content.) However, the Panorama control does not provide this behavior. Each panorama item’s header always scrolls at the same rate as the rest of the panorama item’s content, no matter how wide it is.

As for the layout of items inside a panorama item, you’re on your own. Although certain arrangements of square images and text are commonly used in a panorama, there are no special controls that automatically give you these specific layouts. You should use the general-purpose panels such as a grid or a wrap panel.

The Main Page

The Groceries app’s main page, shown earlier in Figure 27.2, is the only one that uses a panorama. It provides links to the four other pages in this app: an add-items page, an edit-items page, a settings page, and an instructions page.

The User Interface

Listing 27.1 contains the XAML for the main page.

LISTING 27.1 MainPage.xaml—The Main User Interface for Groceries

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:controls=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”White”
SupportedOrientations=”Portrait” shell:SystemTray.IsVisible=”False”>
<!– Two storyboards for animating items into and out of the cart –>
<phone:PhoneApplicationPage.Resources>
<Storyboard x:Name=”MoveToCartStoryboard”
Completed=”MoveToCartStoryboard_Completed”>
<DoubleAnimation To=”-400” Duration=”0:0:.2”/>
</Storyboard>
<Storyboard x:Name=”MoveFromCartStoryboard”
Completed=”MoveFromCartStoryboard_Completed”>
<DoubleAnimation To=”400” Duration=”0:0:.2”/>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<controls:Panorama x:Name=”Panorama” Title=”groceries” Foreground=”White”
SelectionChanged=”Panorama_SelectionChanged”>
<controls:Panorama.Background>
<ImageBrush ImageSource=”Images/background.jpg”/>
</controls:Panorama.Background>
<!– The “list” item –>
<controls:PanoramaItem Foreground=”White”>
<!– A complex header that contains buttons –>
<controls:PanoramaItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”194”/>
<ColumnDefinition Width=”Auto”/>
<ColumnDefinition Width=”Auto”/>
<ColumnDefinition Width=”Auto”/>
</Grid.ColumnDefinitions>
<!– The normal header text –>
<TextBlock Text=”list”/>
<!– add –>
<Button Grid.Column=”1” Margin=”0,20,36,0” Click=”AddButton_Click”
Style=”{StaticResource SimpleButtonStyle}”>
<Image Source=”Shared/Images/normal.add.png”/>
</Button>
<!– settings –>
<Button Grid.Column=”2” Margin=”0,20,36,0” Click=”SettingsButton_Click”
Style=”{StaticResource SimpleButtonStyle}”>
<Image Source=”Shared/Images/normal.settings.png”/>
</Button>
<!– instructions –>
<Button Grid.Column=”3” Margin=”0,20,36,0”
Click=”InstructionsButton_Click”
Style=”{StaticResource SimpleButtonStyle}”>
<Image Source=”Shared/Images/normal.instructions.png”/>
</Button>
</Grid>
</controls:PanoramaItem.Header>
<!– The panorama item’s content is just a list box –>
<ListBox x:Name=”MainListBox” ItemsSource=”{Binding}”>
<!– Give each item a complex template –>
<ListBox.ItemTemplate>
<DataTemplate>
<!– A horizontal stack panel with two buttons –>
<StackPanel Orientation=”Horizontal” Margin=”0,0,0,16”>
<!– The first button sends the item to the cart –>
<Button Style=”{StaticResource SimpleButtonStyle}”
Click=”AddToCartButton_Click”>
<Button.RenderTransform>
<CompositeTransform/>
</Button.RenderTransform>
<StackPanel Orientation=”Horizontal”>
<Image Source=”Shared/Images/normal.done.png”/>
<TextBlock Text=”{Binding Name}” Width=”300” TextWrapping=”Wrap”
Style=”{StaticResource PhoneTextExtraLargeStyle}”
Foreground=”White”/>
</StackPanel>
</Button>
<!– The second button edits the item –>
<Button Style=”{StaticResource SimpleButtonStyle}”
Click=”EditItemButton_Click”>
<Image Source=”Shared/Images/normal.edit.png”/>
</Button>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</controls:PanoramaItem>
<!– The “in cart” item –>
<controls:PanoramaItem Foreground=”White”>
<!– A complex header that contains a button –>
<controls:PanoramaItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”286”/>
<ColumnDefinition Width=”Auto”/>
</Grid.ColumnDefinitions>
<!– The normal header text –>
<TextBlock Text=”in cart”/>
<!– delete –>
<Button Grid.Column=”1” Margin=”0,20,36,0” Click=”DeleteButton_Click”
Style=”{StaticResource SimpleButtonStyle}”>
<Image Source=”Shared/Images/normal.delete.png”/>
</Button>
</Grid>
</controls:PanoramaItem.Header>
<!– This panorama item’s content is a list box in front of a cart image –>
<Grid>
<Image Source=”Images/cart.png” Opacity=”.3” Stretch=”None”/>
<ListBox x:Name=”InCartListBox” ItemsSource=”{Binding}”>
<!– Give each item a complex template –>
<ListBox.ItemTemplate>
<DataTemplate>
<Button Margin=”0,0,0,16” Style=”{StaticResource SimpleButtonStyle}”
Click=”RemoveFromCartButton_Click”>
<Button.RenderTransform>
<CompositeTransform/>
</Button.RenderTransform>
<StackPanel Orientation=”Horizontal”>
<Image Source=”Images/normal.outOfCart.png”/>
<TextBlock Text=”{Binding Name}” Width=”359” TextWrapping=”Wrap”
Style=”{StaticResource PhoneTextExtraLargeStyle}”
Foreground=”White”/>
</StackPanel>
</Button>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</controls:PanoramaItem>
</controls:Panorama>
</phone:PhoneApplicationPage>

[/code]

  • The controls XML namespace is once again used to reference the panorama.
  • This page is portrait-only, which is the expected behavior for any page with a panorama. Although the control works in the landscape orientation, there’s not much room for the content!
  • This page is filled with hard-coded white foregrounds. This is necessary to ensure that the app looks the same under the light theme as it does under the dark theme. Because the background image doesn’t change, we don’t want the text turning black.
  • The panorama’s Background works just like the Background property on other elements. You can set it to any brush, although design guidelines dictate that you use a solid color brush or an image brush. This listing sets the background to background.jpg with an image brush.

Be sure to test your panorama under both dark and light themes!

This is true for any app, of course, but you’re more likely to make a mistake on a page with a panorama that has a fixed background image. If the background never changes, then you probably need to ensure that the color of your content never changes.

To avoid stretching,make sure your panorama’s background image is 800 pixels tall.To avoid performance problems, the image should not be much wider than about 1024 pixels, and it should be a JPEG. Groceries uses a 1024×800 JPEG. When I decided to build this app, I anxiously went to a local grocery store with my wife’s new camera because it has the ability to take panoramic photos.This was before I realized that the best background image dimensions are not panoramic at all! Figure 27.3 shows this app’s background.jpg file.

FIGURE 27.3 The not-so-panoramic background image used by the Groceries app’s panorama.
FIGURE 27.3 The not-so-panoramic background image used by the Groceries app’s panorama.

The effect of a super-wide background image is an illusion caused by the slow, parallax scrolling of the background. In fact, the amount of background scrolling depends on the number of panorama items, because the panorama ensures that you don’t reach the end of the background image until you reach the end of the panorama. In Groceries, it just so happens that the length of the “groceries” title and the length of the background image cause the title and background to scroll at roughly the same rate.To get a richer parallax effect, you could change the length of either one.

For the best results, your panorama’s background image should be given a Build Action of Resource—not Content! This is one of those rare cases where a resource file is recommended, due to the difference between synchronous and asynchronous loading/decoding. If the image is large and included as a content file, the panorama might appear before its background does.When included as a resource file, the panorama will never appear until the image is ready.The synchronous loading done for resource files, which is normally considered to be a problem, actually gives more desirable behavior in this case.Despite increasing the amount of time before the panorama appears,most people do not want their background image appearing later.

You can actually use live UI elements for your panorama’s background! This involves a hack shared by Microsoft’s Dave Relyea, the author of the Panorama control and technical editor for this book.You can read about it at http://bit.ly/panoramaxaml.

  • Because the panorama wraps around, there is always a visible “seam” where the right edge of the background meets the left edge of the background unless you use specially crafted artwork (as in the Games hub) or a solid background (as in the People hub). The seam is okay; users are used to it, and it helps to indicate that a wraparound is occurring. (You can see this seam when wrapping around in the Pictures and Marketplace hubs, among many others.) However, the background image used by Groceries has a little bit of shading on the edges to make the transition a little smoother. This is shown in Figure 27.4.
FIGURE 27.4 Shading in the background image makes the seam less jarring when wrapping from the last panorama item to the first one.
FIGURE 27.4 Shading in the background image makes the seam less jarring when wrapping from the last panorama item to the first one.

Even when using a specially crafted image, a 1-pixel-wide background-color seam can still occasionally be seen while the user scrolls past the wraparound point.You can get rid of this seam by giving Panorama a new control template. It can be a copy of the default one, with a single negative margin added to a border named background as follows:

[code]

<Border x:Name=”background” Background=”{TemplateBinding Background}”
CacheMode=”BitmapCache” Margin=”-1,0”/>

[/code]

  • In Listing 27.1, the panorama contains the two items that are always there: the list of all items left to find, and the cart. The dynamic aisle items are added in codebehind.
  • The “list” panorama item is given a custom header with three buttons next to the typical header text: one for adding a new item, one for settings, and one for instructions. You can see these in Figure 27.2. Ordinarily, these would be application bar buttons, but because an application bar is not meant to be used with a panorama, they are placed in this available area instead.
  • The “cart” panorama item is also given a custom header with a delete button next to the header text. Whereas the other panorama items (including the ones added in code-behind) contain just a list box, the cart item contains a grid in order to place a distinguishing cart icon behind the list box.
  • Buttons are used throughout this app, and they are all marked with a custom style called SimpleButtonStyle. This style gives each button a new control template that removes the border, padding, and other behaviors, so all you see is the content. (It also adds the tilt effect used throughout this book.) It is defined in App.xaml as follows:

    [code]
    <!– A button style that removes the border, padding, state changes for
    pressing/disabling, and ignores various properties like Foreground. It
    simply displays its content with no frills other than the tilt. –>
    <Style x:Key=”SimpleButtonStyle” TargetType=”Button”>
    <Setter Property=”local:Tilt.IsEnabled” Value=”True”/>
    <Setter Property=”Template”>
    <Setter.Value>
    <ControlTemplate TargetType=”Button”>
    <ContentControl x:Name=”ContentContainer”
    Content=”{TemplateBinding Content}”
    ContentTemplate=”{TemplateBinding ContentTemplate}”
    HorizontalContentAlignment=
    ”{TemplateBinding HorizontalContentAlignment}”
    VerticalContentAlignment=
    ”{TemplateBinding VerticalContentAlignment}”/>
    </ControlTemplate>
    </Setter.Value>
    </Setter>
    </Style>
    [/code]

    Figure 27.5 shows what the panorama looks like if each button is left with its default style (with layout adjusted so all the buttons still fit on the screen). The reason that real buttons are used in all these places is that a button’s Click event is only raised for a real tap as opposed to a swiping motion. This enables the user to swipe the panorama on top of a button without inadvertently tapping it. If the MouseLeftButtonUp event were instead used to detect a tap on elements, a swipe that happens to be done on top of an element would trigger the action that’s only supposed to happen on a tap.

FIGURE 27.5 Groceries is filled with buttons that easily detect non-swiping taps, which is obvious when the custom button style is removed.
FIGURE 27.5 Groceries is filled with buttons that easily detect non-swiping taps, which is obvious when the custom button style is removed.

Avoid using raw mouse events like MouseLeftButtonDown, MouseMove, and MouseLeftButtonUp inside a panorama (or pivot)!

Because the entire control pans in response to these gestures, any extra logic you associate with these events is likely to interfere with the user’s panning expectations. Find other relevant events to use instead that aren’t also triggered by swiping gestures, such as a button’s Click event or a list box’s SelectionChanged event.

The Code-Behind

Listing 27.2 contains the code-behind for the main page.

LISTING 27.2 MainPage.xaml.cs—The Code-Behind for the Groceries App’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
Item pendingIntoCartItem;
Item pendingOutOfCartItem;
public MainPage()
{
InitializeComponent();
this.Loaded += MainPage_Loaded;
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
// Fill the two list boxes that are always there
this.MainListBox.DataContext = FilteredLists.Need;
this.InCartListBox.DataContext = FilteredLists.InCart;
// Add and fill the other aisles based on the user’s data
RefreshAisles();
}
void RefreshAisles()
{
// Remove all aisles. Leave the list and cart items.
while (this.Panorama.Items.Count > 2)
this.Panorama.Items.RemoveAt(1);
// Get the list of dynamic aisles
string[] aisles = Settings.AislesList.Value;
for (int i = aisles.Length – 1; i >= 0; i–)
{
string aisle = aisles[i];
AislePanoramaItem panoramaItem = new AislePanoramaItem { Header = aisle };
// Fill the aisle with relevant items
panoramaItem.Items = new FilteredObservableCollection<Item>(
Settings.AvailableItems.Value, delegate(Item item)
{
return (item.Status == Status.Need && item.Aisle == aisle);
});
// Only add aisles that contain items we still need to get
if (panoramaItem.Items.Count > 0)
this.Panorama.Items.Insert(1, panoramaItem);
}
}
void Panorama_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Check to see if the item we’re leaving is now empty
if (e.RemovedItems.Count == 1)
{
AislePanoramaItem aisle = e.RemovedItems[0] as AislePanoramaItem;
if (aisle != null && aisle.Items.Count == 0)
{
// It’s empty, so remove it.
// But wait .5 seconds to avoid interfering with the animation!
DispatcherTimer timer = new DispatcherTimer {
Interval = TimeSpan.FromSeconds(.5) };
timer.Tick += delegate(object s, EventArgs args)
{
this.Panorama.Items.Remove(aisle);
timer.Stop();
};
timer.Start();
}
}
}
// The three “list” header button handlers
void SettingsButton_Click(object sender, RoutedEventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
void AddButton_Click(object sender, RoutedEventArgs e)
{
this.NavigationService.Navigate(new Uri(“/AddItemsPage.xaml”,
UriKind.Relative));
}
void InstructionsButton_Click(object sender, RoutedEventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
// The two button handlers for each item in “list”
void AddToCartButton_Click(object sender, RoutedEventArgs e)
{
if (this.MoveToCartStoryboard.GetCurrentState() != ClockState.Stopped)
return;
this.pendingIntoCartItem = (sender as FrameworkElement).DataContext as Item;
Storyboard.SetTarget(this.MoveToCartStoryboard,
(sender as UIElement).RenderTransform);
Storyboard.SetTargetProperty(this.MoveToCartStoryboard,
new PropertyPath(“TranslateX”));
this.MoveToCartStoryboard.Begin();
}
void EditItemButton_Click(object sender, RoutedEventArgs e)
{
Item item = (sender as FrameworkElement).DataContext as Item;
Settings.EditedItem.Value = item;
this.NavigationService.Navigate(new Uri(“/EditItemPage.xaml”,
UriKind.Relative));
}
// The one “in cart” header button handler
void DeleteButton_Click(object sender, RoutedEventArgs e)
{
if (MessageBox.Show(
“Are you sure you want to remove all the items from the cart?”,
“Clear cart?”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
foreach (Item item in Settings.AvailableItems.Value)
{
// Nothing is actually deleted, just marked Unused
if (item.Status == Status.InCart)
item.Status = Status.Unused;
}
}
}
// The one button handler for each item in the cart
void RemoveFromCartButton_Click(object sender, RoutedEventArgs e)
{
if (this.MoveFromCartStoryboard.GetCurrentState() != ClockState.Stopped)
return;
this.pendingOutOfCartItem =
(sender as FrameworkElement).DataContext as Item;
Storyboard.SetTarget(this.MoveFromCartStoryboard,
(sender as UIElement).RenderTransform);
Storyboard.SetTargetProperty(this.MoveFromCartStoryboard,
new PropertyPath(“TranslateX”));
this.MoveFromCartStoryboard.Begin();
}
// Storyboard-completed handlers
void MoveFromCartStoryboard_Completed(object sender, EventArgs e)
{
this.pendingOutOfCartItem.Status = Status.Need;
// This may have caused the need to add an aisle
RefreshAisles();
this.MoveFromCartStoryboard.Stop();
}
void MoveToCartStoryboard_Completed(object sender, EventArgs e)
{
this.pendingIntoCartItem.Status = Status.InCart;
// This may have caused the need to remove an aisle
RefreshAisles();
this.MoveToCartStoryboard.Stop();
}
}
}

[/code]

  • RefreshAisles is responsible for dynamically filling in the aisles in-between the list and cart panorama items. Each dynamic aisle is encapsulated by a custom AislePanoramaItem control that derives from PanoramaItem. This control is shown in the next section. Panorama items are only added for each user-defined aisle that has active items in it that need to be added to the cart.
  • The creation of each filtered collection for each dynamic panorama item is not very efficient, because each FilteredObservableCollection (whose implementation is shown later) must iterate through the passed-in list of available items. If the list of items becomes sufficiently large, a new strategy might need to be chosen.
  • This app demonstrates dynamic removal of panorama items, which happens when all of a dynamic aisle’s items have been moved to the cart. Unfortunately, like a pivot, a panorama does not handle removal of its items very gracefully. There are two problems: finding a good time to remove the panorama item, and its impact on the parallax effect.

    To avoid confusion, an empty panorama item is removed after the user has panned away from it, so the code checks for this condition inside panorama’s SelectionChanged event handler. In this handler, the previous selection is exposed as the only item in the RemovedItems collection. Because removing it instantly would interfere with the panning animation that is causing the SelectionChanged event to be raised, the handler uses a DispatcherTimer to remove it half a second later. In practice, this works pretty well. The only remaining issue is that because the scrolling of the background and title is based on the total panorama width, and removing an item shortens that width, it causes a jarring jump in the placement of the background and title unless you happen to be on the first panorama item when this happens. There is no way to avoid this behavior, other than not removing panorama items!

  • Storyboards are used to animate items to/from the cart. The actual change to the lists occurs in the Completed event handlers, which either set the item’s Status property to InCart or Need. This causes a property-changed notification that flows to each of the filtered lists, causing both lists to update automatically thanks to data binding.

Panoramas do not enable programmatic setting of the selected panorama item!

One thing conspicuously missing from Listing 27.2 is a setting that remembers the current panorama item so the page’s state can be restored on the next launch or activation. Strangely, panorama’s SelectedIndex and SelectedItem properties are readonly, so although you can save either value when the selection changes, you cannot restore it later.

Panorama does expose a read/write property called DefaultItem that can instantly change the panorama item on the screen, but not in the way that you’d expect. It shifts the items such that DefaultItem becomes the first section on the virtual canvas, as illustrated in Figure 27.6.This means that the title now aligns with the new default item and the image seam is now immediately to the left of this item! In the Groceries app, having the image seam move anywhere other than between the cart and the list sections would be a confusing experience.Therefore, the DefaultItem property is not suitable for attempting to return a user to where they left off.

FIGURE 27.6 Setting DefaultItem shifts the panorama items, but it does not shift the title or the background image.
FIGURE 27.6 Setting DefaultItem shifts the panorama items, but it does not shift the title or the background image.

The AislePanoramaItem Control

AislePanoramaItem was added to the Visual Studio project as a user control, but then its base class was changed from UserControl to PanoramaItem. This was done to get the same kind of convenient XAML support as a user control, but applied to a PanoramaItem subclass. Listing 27.3 contains this control’s XAML and Listing 27.4 contains its codebehind.

LISTING 27.3 AislePanoramaItem.xaml—The User Interface for the Custom PanoramaItem Subclass

[code]

<controls:PanoramaItem x:Class=”WindowsPhoneApp.AislePanoramaItem”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:controls=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”White”>
<!– A storyboards for animating items into the cart –>
<controls:PanoramaItem.Resources>
<Storyboard x:Name=”MoveToCartStoryboard” Completed=”Storyboard_Completed”>
<DoubleAnimation To=”-400” Duration=”0:0:.2”/>
</Storyboard>
</controls:PanoramaItem.Resources>
<!– The panorama item’s content is just a list box –>
<ListBox x:Name=”ListBox” ItemsSource=”{Binding}” >
<ListBox.ItemTemplate>
<DataTemplate>
<Button Margin=”0,0,0,16” Style=”{StaticResource SimpleButtonStyle}”
Click=”ItemButton_Click”>
<Button.RenderTransform>
<CompositeTransform/>
</Button.RenderTransform>
<StackPanel Orientation=”Horizontal”>
<Image Source=”Shared/Images/normal.done.png”/>
<TextBlock Text=”{Binding Name}” Width=”300” TextWrapping=”Wrap”
Style=”{StaticResource PhoneTextExtraLargeStyle}”
Foreground=”White”/>
</StackPanel>
</Button>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</controls:PanoramaItem>

[/code]

LISTING 27.4 AislePanoramaItem.xaml.cs—The Code-Behind for the Custom PanoramaItem Subclass

[code]

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media.Animation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class AislePanoramaItem : PanoramaItem
{
Item pendingItem;
public AislePanoramaItem()
{
InitializeComponent();
}
public ICollection<Item> Items
{
get { return this.ListBox.DataContext as ICollection<Item>; }
set { this.ListBox.DataContext = value; }
}
void ItemButton_Click(object sender, RoutedEventArgs e)
{
if (this.MoveToCartStoryboard.GetCurrentState() != ClockState.Stopped)
return;
// Animate the item when tapped
this.pendingItem = (sender as FrameworkElement).DataContext as Item;
Storyboard.SetTarget(this.MoveToCartStoryboard,
(sender as UIElement).RenderTransform);
Storyboard.SetTargetProperty(this.MoveToCartStoryboard,
new PropertyPath(“TranslateX”));
this.MoveToCartStoryboard.Begin();
}
void Storyboard_Completed(object sender, EventArgs e)
{
// Now place the item in the cart
this.pendingItem.Status = Status.InCart;
this.MoveToCartStoryboard.Stop();
}
}
}

[/code]

This panorama item is just like the first panorama item on the main page, but with no edit button in the item template. This convenient packaging enables it to be easily reused by main page, as is done in the RefreshAisles method in Listing 27.2.

Supporting Data Types

The Item data type that is used throughout this app is defined in Listing 27.5.

LISTING 27.5 Item.cs—The Type of Every Item in Every List Box

[code]

using System.ComponentModel;
namespace WindowsPhoneApp
{
public class Item : INotifyPropertyChanged
{
// The backing fields
string name;
string aisle;
bool isFavorite;
Status status;
// The properties, which raise change notifications
public string Name {
get { return this.name; }
set { this.name = value; OnPropertyChanged(“Name”); } }
public string Aisle {
get { return this.aisle; }
set { this.aisle = value; OnPropertyChanged(“Aisle”); } }
public bool IsFavorite {
get { return this.isFavorite; }
set { this.isFavorite = value; OnPropertyChanged(“IsFavorite”); } }
public Status Status {
get { return this.status; }
set { this.status = value; OnPropertyChanged(“Status”); } }
void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
// Treat items with the same name as equal
public override bool Equals(object obj)
{
if (!(obj is Item))
return false;
return (this.Name == (obj as Item).Name);
}
// This matches the implementation of Equals
public override int GetHashCode()
{
return this.Name.GetHashCode();
}
public event PropertyChangedEventHandler PropertyChanged;
}
}

[/code]

  • The Status enumeration is defined as follows:
    public enum Status
    {
    Need, // In the current shopping list (but not in the cart yet)
    InCart, // In the cart
    Unused // Added at some point in the past, but not currently used
    }
  • The IsFavorite property is leveraged by the add-items and edit-item pages to help the user organize their entries.
  • The property-changed notifications enable the filtered collections to keep items in the appropriate filtered lists at all times. They also keep the rendering of individual items up-to-date. For example, the add-items page uses several value converters to show/hide buttons when an item’s IsFavorite status changes.

The AvailableItems setting that persists the list of all items is defined as follows inside the Settings class:

[code]

public static readonly Setting<ObservableCollection<Item>> AvailableItems =
new Setting<ObservableCollection<Item>>(“AvailableItems”,
new ObservableCollection<Item>());

[/code]

The filtered lists used by this app are not persisted but rather initialized from the single persisted list once the app runs. They are defined as follows:

[ocde]

public static class FilteredLists
{
// A list of items in the current shopping list (but not in the cart yet)
public static readonly ReadOnlyObservableCollection<Item> Need =
new ReadOnlyObservableCollection<Item>(
new FilteredObservableCollection<Item>(Settings.AvailableItems.Value,
delegate(Item item) { return item.Status == Status.Need; }));
// A list of items in the cart
public static readonly ReadOnlyObservableCollection<Item> InCart =
new ReadOnlyObservableCollection<Item>(
new FilteredObservableCollection<Item>(Settings.AvailableItems.Value,
delegate(Item item) { return item.Status == Status.InCart; }));
// A list of items marked as favorites
public static readonly ReadOnlyObservableCollection<Item> Favorites =
new ReadOnlyObservableCollection<Item>(
new FilteredObservableCollection<Item>(Settings.AvailableItems.Value,
delegate(Item item) { return item.IsFavorite; }));
}

[/code]

Each FilteredObservableCollection is wrapped in a ReadOnlyObservableCollection to prevent consumers from accidentally attempting to modify the collection directly.

Listing 27.6 contains the implementation of the custom FilteredObservableCollection class.

LISTING 27.6 FilteredObservableCollection.cs—Exposes a Subset of a Separate Observable Collection Based on a Custom Filter

[code]

using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace WindowsPhoneApp
{
public class FilteredObservableCollection<T> : ObservableCollection<T>
where T : INotifyPropertyChanged
{
ObservableCollection<T> sourceCollection;
Predicate<T> belongs;
public FilteredObservableCollection(ObservableCollection<T> sourceCollection,
Predicate<T> filter)
{
this.sourceCollection = sourceCollection;
this.belongs = filter;
// Listen for any changes in the source collection
this.sourceCollection.CollectionChanged +=
SourceCollection_CollectionChanged;
foreach (T item in this.sourceCollection)
{
// We must also listen for changes on each item, because property changes
// are not reported through the CollectionChanged event
item.PropertyChanged += Item_PropertyChanged;
// Add the item to this list if it passes the filter
if (this.belongs(item))
this.Add(item);
}
}
// Handler for each item’s property changes
void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
T item = (T)sender;
if (this.belongs(item))
{
// The item belongs in this list, so add it (if it wasn’t already added)
if (!this.Contains(item))
this.Add(item);
}
else
{
// The item does not belong in this list, so remove it if present.
// Remove simply returns false if the item is not in this list.
this.Remove(item);
}
}
// Handler for collection changes
void SourceCollection_CollectionChanged(object sender,
NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add ||
e.Action == NotifyCollectionChangedAction.Replace)
{
// Insert any relevant item(s) at the end of the list
foreach (T item in e.NewItems)
{
// We must start tracking property changes in this item as well
item.PropertyChanged += Item_PropertyChanged;
if (this.belongs(item))
this.Add(item);
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove ||
e.Action == NotifyCollectionChangedAction.Replace)
{
// Try removing each one
foreach (T item in e.OldItems)
{
// We can stop tracking property changes on this item
item.PropertyChanged -= Item_PropertyChanged;
this.Remove(item);
}
}
else // e.Action == NotifyCollectionChangedAction.Reset
{
throw new NotSupportedException();
}
}
}
}

[/code]

This class is constructed with a source collection and a callback that returns whether an individual item belongs in the filtered list. This enables each instance to use a different filter, as done in the FilteredLists static class. The type of item used with this class must implement INotifyPropertyChanged, as this class tracks item-by-item property changes as well as additions and removals to the source collection. (This is a requirement for Groceries, as changing a property like Status or IsFavorite must instantly impact the filtered lists.)

The Finished Product

Groceries (Panorama)

Book Reader (Pagination & List Picker)

To get the best reading experience, this app enables you to customize the foreground and background colors, the text size, and even the font family. Book Reader provides easy page navigation and enables jumping to any chapter or page number.

It might not be immediately obvious, but the biggest challenge to implementing this app is pagination—dividing the book’s contents into discrete pages based on the font settings. Avoiding this challenge by placing the entire book’s contents in a scroll viewer wouldn’t be a great user experience. It also wouldn’t be feasible without extra trickery due to the size limitation of UI elements. Therefore, this app shows one page of text at a time. The user can tap the screen to advance the page, or tap a button on the application bar to go back by one page.

The Main Page

The main page, pictured in Figure 25.1 with its application bar expanded, shows the current page and an application bar with a button to go back one page, a button to jump to any chapter or page, and a button to change settings. The application bar area also shows the current page number as well as the total number of pages in the book (based on the current font settings). Listing 25.1 contains this page’s XAML.

FIGURE 25.1 The main page, with its default Amazon Kindle-inspired color scheme that provides just enough contrast for reading.
FIGURE 25.1 The main page, with its default Amazon Kindle-inspired color scheme that provides just enough contrast for reading.

LISTING 25.1 MainPage.xaml—The User Interface for Book Reader’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait”>
<!– The application bar, with three buttons –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”0”>
<shell:ApplicationBarIconButton Text=”previous” IsEnabled=”False”
IconUri=”/Shared/Images/appbar.left.png” Click=”PreviousButton_Click”/>
<shell:ApplicationBarIconButton Text=”page jump”
IconUri=”/Images/appbar.book.png” Click=”JumpButton_Click”/>
<shell:ApplicationBarIconButton Text=”settings” IconUri=
“/Shared/Images/appbar.settings.png” Click=”SettingsButton_Click”/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid x:Name=”LayoutRoot”>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height=”56”/>
</Grid.RowDefinitions>
<!– The document that takes up most of the page –>
<local:PaginatedDocument x:Name=”Document” Margin=”12”
Width=”456” Height=”720”/>
<!– The footer that shows the page number and total page count –>
<TextBlock x:Name=”Footer” Grid.Row=”1” Margin=”14,0,0,17”
HorizontalAlignment=”Left” VerticalAlignment=”Center”/>
<!– The full-screen panel with the text box and chapter list –>
<Grid x:Name=”JumpPanel” Grid.RowSpan=”2” Visibility=”Collapsed”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition/>
</Grid.RowDefinitions>
<Rectangle Grid.RowSpan=”2” Fill=”{StaticResource PhoneBackgroundBrush}”
Opacity=”.9”/>
<!– Enter a page number –>
<StackPanel Orientation=”Horizontal” Margin=”12”>
<TextBlock Text=”Jump to page:” VerticalAlignment=”Center”/>
<TextBox x:Name=”PageTextBox” InputScope=”Number” MinWidth=”150”
GotFocus=”PageTextBox_GotFocus” KeyUp=”PageTextBox_KeyUp”/>
<Button Content=”Go” MinWidth=”150” local:Tilt.IsEnabled=”True”
Click=”GoButton_Click”/>
</StackPanel>
<!– Choose a chapter from the list box –>
<ListBox x:Name=”ChaptersListBox” Grid.Row=”1” Margin=”12”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”
SelectionChanged=”ChaptersListBox_SelectionChanged”>
<!– This is done so the chapter page numbers are right-aligned –>
<ListBox.ItemContainerStyle>
<Style TargetType=”ListBoxItem”>
<Setter Property=”HorizontalContentAlignment” Value=”Stretch”/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid local:Tilt.IsEnabled=”True”>
<!– The left-aligned chapter title –>
<TextBlock Text=”{Binding Key}”/>
<!– The right-aligned page number –>
<TextBlock Text=”{Binding Value}” HorizontalAlignment=”Right”/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • The Footer text block appears in the application bar area because it is placed underneath its area, and the application bar is marked with an opacity of 0.
  • The list box filled with chapters, shown in Figure 25.2, uses an important but hard-to-discover trick to enable the list box items to stretch to fill the width of the list box. This enables elements of each item (the page number, in this case) to be right-aligned without giving each item an explicit width.
FIGURE 25.2 The list box with chapters uses a HorizontalContentAlignment of Stretch, so the page numbers can be right-aligned without giving each item an explicit width.
FIGURE 25.2 The list box with chapters uses a HorizontalContentAlignment of Stretch, so the page numbers can be right-aligned without giving each item an explicit width.

To make the content of list box items stretch to fill the width of the list box, give the list box an ItemContainerStyle as follows:

[code]

<ListBox.ItemContainerStyle>
<Style TargetType=”ListBoxItem”>
<Setter Property=”HorizontalContentAlignment” Value=”Stretch”/>
</Style>
</ListBox.ItemContainerStyle>

[/code]

Listing 25.2 contains the code-behind for the main page.

LISTING 25.2 MainPage.xaml.cs—The Code-Behind for Book Reader’s Main Page

[code]

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using System.Windows.Resources;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
IApplicationBarIconButton previousButton;
public MainPage()
{
InitializeComponent();
this.previousButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the saved settings
this.ApplicationBar.ForegroundColor = Settings.TextColor.Value;
this.LayoutRoot.Background = new SolidColorBrush(Settings.PageColor.Value);
this.Document.Foreground = this.Footer.Foreground =
new SolidColorBrush(Settings.TextColor.Value);
this.Document.FontSize = Settings.TextSize.Value;
this.Document.FontFamily = new FontFamily(Settings.Font.Value);
if (this.Document.Text == null)
{
// Load the book as one big string from the included file
LoadBook(delegate(string s)
{
// This happens on a background thread, but that’s okay
this.Document.Text = s;
UpdatePagination();
});
}
else if (this.State.ContainsKey(“TextSize”))
{
if (((int)this.State[“TextSize”] != Settings.TextSize.Value ||
(string)this.State[“Font”] != Settings.Font.Value))
{
// If the font family or size changed, the book needs to be repaginated
UpdatePagination();
}
else if ((Color)this.State[“TextColor”] != Settings.TextColor.Value)
{
// If only the color changed, simply re-render the current page
this.Document.RefreshCurrentPage();
}
}
// Remember the current text settings so we can detect if they
// were changed when returning from the settings page
this.State[“TextSize”] = Settings.TextSize.Value;
this.State[“Font”] = Settings.Font.Value;
this.State[“TextColor”] = Settings.TextColor.Value;
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
// If the page/chapter jump panel is open, make the back button close it
if (this.JumpPanel.Visibility == Visibility.Visible)
{
e.Cancel = true;
CloseJumpPanel();
}
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
// Treat any tap as a page advance,
// unless the page/chapter jump panel is open
if (this.JumpPanel.Visibility == Visibility.Collapsed)
{
this.Document.ShowNextPage();
RefreshFooter();
}
}
// Retrieve the text from the included text file
public static void LoadBook(Action<string> callback)
{
string s = null;
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += delegate(object sender, DoWorkEventArgs e)
{
// Do this work on a background thread
StreamResourceInfo info = Application.GetResourceStream(
new Uri(“1342.txt”, UriKind.Relative));
using (info.Stream)
using (StreamReader reader = new StreamReader(info.Stream))
s = reader.ReadToEnd();
if (callback != null)
callback(s);
};
worker.RunWorkerAsync();
}
void UpdatePagination()
{
this.Document.UpdatePagination(delegate()
{
// Now that the book has been repaginated, refresh some UI
// on the main thread
this.Dispatcher.BeginInvoke(delegate()
{
// Move to the page we were previously on based on the character index
// in the string (because the old page numbers are now meaningless)
this.Document.ShowPageWithCharacterIndex(
Settings.CurrentCharacterIndex.Value);
RefreshFooter();
// Fill the chapters list box based on the current page numbers
this.ChaptersListBox.Items.Clear();
for (int i = 0; i < this.Document.Chapters.Count; i++)
{
this.ChaptersListBox.Items.Add(new KeyValuePair<string, string>(
“Chapter “ + (i + 1), // Title
this.Document.Chapters[i].ToString(“N0”) // Page number
));
}
});
});
}
void RefreshFooter()
{
// Because this is called whenever the page is changed, this is a good
// spot to store the current spot in the book
Settings.CurrentCharacterIndex.Value = this.Document.CurrentCharacterIndex;
this.Footer.Text = this.Document.CurrentPage.ToString(“N0”) + “ / “ +
this.Document.TotalPages.ToString(“N0”);
this.previousButton.IsEnabled = (this.Document.CurrentPage > 1);
}
void OpenJumpPanel()
{
this.JumpPanel.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = false;
// Fill the text box with the current page number
// (without thousands separator)
this.PageTextBox.Text = this.Document.CurrentPage.ToString();
// Temporarily support landscape hardware keyboards
this.SupportedOrientations = SupportedPageOrientation.PortraitOrLandscape;
}
void CloseJumpPanel()
{
this.JumpPanel.Visibility = Visibility.Collapsed;
this.ApplicationBar.IsVisible = true;
this.SupportedOrientations = SupportedPageOrientation.Portrait;
}
void ChaptersListBox_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
if (this.ChaptersListBox.SelectedIndex >= 0)
{
// Jump to the selected page
this.Document.ShowPage(
this.Document.Chapters[this.ChaptersListBox.SelectedIndex]);
RefreshFooter();
// Clear the selection so consecutive taps on the same item works
this.ChaptersListBox.SelectedIndex = -1;
// Delay the closing of the panel so OnMouseLeftButtonUp
// doesn’t advance the page
this.Dispatcher.BeginInvoke(delegate() { CloseJumpPanel(); });
}
}
void PageTextBox_GotFocus(object sender, RoutedEventArgs e)
{
this.PageTextBox.SelectAll();
}
void PageTextBox_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
// Make pressing Enter do the same thing as tapping “Go”
if (e.Key == Key.Enter)
GoButton_Click(this, null);
}
void GoButton_Click(object sender, RoutedEventArgs e)
{
// If the page number is valid, jump to it
int pageNumber;
if (int.TryParse(this.PageTextBox.Text, out pageNumber))
{
this.Document.ShowPage(pageNumber);
RefreshFooter();
CloseJumpPanel();
}
}
// Application bar handlers
void PreviousButton_Click(object sender, EventArgs e)
{
this.Document.ShowPreviousPage();
RefreshFooter();
}
void JumpButton_Click(object sender, EventArgs e)
{
OpenJumpPanel();
}
void SettingsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
}
}

[/code]

  • The book is a text file included as content (Build Action = Content), just like the database files in the preceding chapter. The filename is 1342.txt, matching the document downloaded from the Project Gutenberg website.
  • This app uses the following settings:

    [code]
    public static class Settings
    {
    // The current position in the book
    public static readonly Setting<int> CurrentCharacterIndex =
    new Setting<int>(“CurrentCharacterIndex”, 0);
    // The user-configurable settings
    public static readonly Setting<string> Font =
    new Setting<string>(“Font”, “Georgia”);
    public static readonly Setting<Color> PageColor =
    new Setting<Color>(“PageColor”, Color.FromArgb(0xFF, 0xA1, 0xA1, 0xA1));
    public static readonly Setting<Color> TextColor =
    new Setting<Color>(“TextColor”, Colors.Black);
    public static readonly Setting<int> TextSize =
    new Setting<int>(“TextSize”, 32);
    }
    [/code]

    The reader’s position in the book is stored as a character index—the index of the first character on the current page in the string containing the entire contents of the book. This is done because the page number associated with any spot in the book can vary dramatically based on the font settings. With this scheme, the user’s true position in the book is always maintained.

  • The key-value pair added to the chapters list box is a convenient type to use because it exposes two separate string properties that the data template in Listing 25.1 is able to bind to. The “key” is the left-aligned chapter title and the “value” is the rightaligned page number.

The Settings Page

Book Reader’s settings page is almost identical to the settings page for Notepad. The difference is a font picker on top of the other controls, shown in Figure 25.3. This font picker is created with the list picker control from the Silverlight for Windows Phone Toolkit.

FIGURE 25.3 The font picker shows ten fonts in a WYSIWYG picker.
FIGURE 25.3 The font picker shows ten fonts in a WYSIWYG picker.

A list picker is basically a combo box. It initially looks like a text box but, when tapped, it enables the user to pick one value out of a list of possible values.

To get the WYSIWYG font list inside the list picker, Book Reader’s settings page uses the following XAML:

[code]

<toolkit:ListPicker x:Name=”FontPicker” Header=”Font” Grid.ColumnSpan=”2”
SelectionChanged=”FontPicker_SelectionChanged” ItemCountThreshold=”10”>
<toolkit:ListPicker.ItemTemplate>
<DataTemplate>
<TextBlock FontFamily=”{Binding}” Text=”{Binding}”/>
</DataTemplate>
</toolkit:ListPicker.ItemTemplate>
<sys:String>Arial</sys:String>
<sys:String>Calibri</sys:String>
<sys:String>Georgia</sys:String>
<sys:String>Lucida Sans Unicode</sys:String>
<sys:String>Segoe WP</sys:String>
<sys:String>Segoe WP Black</sys:String>
<sys:String>Tahoma</sys:String>
<sys:String>Times New Roman</sys:String>
<sys:String>Trebuchet MS</sys:String>
<sys:String>Verdana</sys:String>
</toolkit:ListPicker>

[/code]

The data template binds both FontFamily and Text properties of each text block to display each string in the list.

List pickers support two different ways of presenting their list of items: an inline mode and a full mode. In the inline mode, the control expands and collapses with smooth animations. This is what is happening in Figure 25.3. In the full mode, the control displays a full-screen popup that presents its list of items. This is pictured in Figure 25.4.

Why does the ComboBox control look so strange when I try to use it in a Windows Phone app?

The ComboBox control is a core Silverlight control frequently used on the web, but it was never given a style that is appropriate for Windows Phone. It is not intended to be used. (The control should have been removed to avoid confusion.) If you find yourself wanting to use a combo box, use the list picker instead.

By default, a list picker uses its inline mode if there are five or fewer items; otherwise, it uses full mode. This is consistent with Windows Phone design guidelines. However, you can force either mode by setting the value of ItemCountThreshold appropriately. The list picker will stay in its inline mode as long as the number of items is less than or equal to ItemCountThreshold. Book Reader chooses to keep the font picker with 10 fonts in inline mode, so it sets this property to 10.

FIGURE 25.4 A variation of Book Reader’s font picker, configured to use full mode.
FIGURE 25.4 A variation of Book Reader’s font picker, configured to use full mode.

List picker defines a Header and corresponding HeaderTemplate property, and an ItemTemplate property for customizing the appearance of each item in the inline mode. Even if you use full mode, these properties are still important for the appearance of the list picker when the full-screen list isn’t showing. For the full-screen list, list picker also defines separate FullModeHeader and FullModeItemTemplate properties. The full-mode list picker shown in Figure 25.4 takes advantage of these two properties as follows:

[code]

<toolkit:ListPicker Header=”Font” FullModeHeader=”FONT”>
<toolkit:ListPicker.ItemTemplate>
<!– For displaying the selected item inline –>
<DataTemplate>
<TextBlock FontFamily=”{Binding}” Text=”{Binding}”/>
</DataTemplate>
</toolkit:ListPicker.ItemTemplate>
<toolkit:ListPicker.FullModeItemTemplate>
<!– For displaying each item in full mode –>
<DataTemplate>
<TextBlock FontFamily=”{Binding}” Text=”{Binding}” Margin=”12”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”/>
</DataTemplate>
</toolkit:ListPicker.FullModeItemTemplate>
<sys:String>Arial</sys:String>
<sys:String>Calibri</sys:String>
<sys:String>Georgia</sys:String>
<sys:String>Lucida Sans Unicode</sys:String>
<sys:String>Segoe WP</sys:String>
<sys:String>Segoe WP Black</sys:String>
<sys:String>Tahoma</sys:String>
<sys:String>Times New Roman</sys:String>
<sys:String>Trebuchet MS</sys:String>
<sys:String>Verdana</sys:String>
</toolkit:ListPicker>

[/code]

If you don’t specify a FullModeItemTemplate, the full mode will use ItemTemplate.

List pickers cannot contain UI elements when full mode is used!

If you directly place UI elements such as text blocks or the toolkit’s own ListPickerItem controls inside a list picker, an exception is thrown when attempting to display the full-mode popup.That’s because the control attempts to add each item to the additional full-screen list, but a single UI element can only be in one place at a time.The solution is to place nonvisual data items in the list picker then use item template(s) to control each item’s visual appearance.

Avoid putting an inline-mode list picker at the bottom of a scroll viewer!

List picker behaves poorly in this situation. When it first expands, the view is not shifted to ensure its contents are on-screen.Then, when attempting to scroll to view the off-screen contents, the list picker collapses!

For the best performance, elements below an inline-mode list picker should be marked with CacheMode=”BitmapCache”.That’s because the expansion and contraction of the list picker animates the positions of these elements.

The PaginatedDocument User Control

To determine where page breaks occur, the PaginatedDocument user control must measure the width and height of each character under the current font settings. The only way to perform this measurement is to place text in a text block and check the values of its ActualWidth and ActualHeight properties. Therefore, PaginatedDocument uses the following three-step algorithm:

  1. Find each unique character in the document. (The Pride and Prejudice document contains only 85 unique characters.)
  2. Measure the width and height of each character by placing each one in a text block, one at a time. The height of all characters is always the same (as the reported height is the line height, padding and all), so the height only needs to be checked once.
  3. Go through the document from beginning to end and, using the precalculated widths of each character, figure out where each line break occurs. With this information, and with the precalculated line height, we know where each page break occurs. Determining line breaks can be a bit tricky due to the need to wrap words appropriately.

The control renders any page by adding a text block for each line, based on the calculated page breaks and line breaks. This is done to ensure that every line break occurs exactly where we expect it to.

Listing 25.3 contains the user control’s XAML and Listing 25.4 contains its code-behind.

LISTING 25.3 PaginatedDocument.xaml—The User Interface for the PaginatedDocument User Control

[code]

<UserControl x:Class=”WindowsPhoneApp.PaginatedDocument”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”>
<Canvas>
<!– Contains the lines of text –>
<StackPanel x:Name=”StackPanel” Margin=”0,-6,0,0”/>
<!– Used for measurements –>
<TextBlock x:Name=”MeasuringTextBlock”/>
</Canvas>
</UserControl>

[/code]

LISTING 25.4 PaginatedDocument.xaml.cs—The Code-Behind for the PaginatedDocument User Control

[code]

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Controls;
namespace WindowsPhoneApp
{
public partial class PaginatedDocument : UserControl
{
Dictionary<char, double> characterWidths = new Dictionary<char, double>();
double characterHeight;
bool isUpdating;
int currentPageBreakIndex;
List<int> pageBreaks = new List<int>();
List<int> lineBreaks = new List<int>();
public List<int> Chapters = new List<int>();
public PaginatedDocument()
{
InitializeComponent();
}
public int CurrentPage
{
get { return this.currentPageBreakIndex + 1; }
}
public int TotalPages
{
get { return this.pageBreaks.Count – 1; }
}
public string Text { get; set; }
public int CurrentCharacterIndex { get; private set; }
public void UpdatePagination(Action doneCallback)
{
if (this.Text == null || this.isUpdating)
throw new InvalidOperationException();
this.isUpdating = true;
// Reset measurements
this.pageBreaks.Clear(); this.lineBreaks.Clear();
this.pageBreaks.Add(0); this.lineBreaks.Add(0);
this.Chapters.Clear();
this.characterWidths.Clear();
this.characterHeight = -1;
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += delegate(object sender, DoWorkEventArgs e)
{
// STEP 1: BACKGROUND THREAD
// Build up a dictionary of unique characters in the text
for (int i = 0; i < this.Text.Length; i++)
{
if (!this.characterWidths.ContainsKey(this.Text[i]))
this.characterWidths.Add(this.Text[i], -1);
}
// Copy the character keys so we can update the width values
// without affecting the enumeration
char[] chars = new char[this.characterWidths.Keys.Count];
this.characterWidths.Keys.CopyTo(chars, 0);
this.Dispatcher.BeginInvoke(delegate()
{
// STEP 2: MAIN THREAD
// Measure the height of all characters
// and the width of each character
foreach (char c in chars)
{
// The only way to measure the width is to place the
// character in a text block and ask for its ActualWidth
this.MeasuringTextBlock.Text = c.ToString();
this.characterWidths[c] = this.MeasuringTextBlock.ActualWidth;
// The height for all characters is the same
// (except for newlines, which are twice the height)
if (this.characterHeight == -1 && !Char.IsWhiteSpace(c))
this.characterHeight = this.MeasuringTextBlock.ActualHeight;
}
this.MeasuringTextBlock.Text = “”;
double pageWidth = this.Width + 1; // Allow one pixel more than width
double linesPerPage = this.Height / this.characterHeight;
BackgroundWorker worker2 = new BackgroundWorker();
worker2.DoWork += delegate(object sender2, DoWorkEventArgs e2)
{
// STEP 3: BACKGROUND THREAD
// Determine the index of each page break
int linesOnThisPage = 0;
double currentLineWidth = 0;
int lastWordEndingIndex = -1;
// Loop through each character and determine each line
// break based on character widths and text block wrapping behavior.
// A line break should then be a page break when the cumulative
// height of lines exceeds the page height.
for (int i = 0; i < this.Text.Length; i++)
{
char c = this.Text[i];
bool isLineBreak = false;
bool isForcedPageBreak = false;
if (c == ‘n’)
{
if (linesOnThisPage == 0 && currentLineWidth == 0)
continue; // Skip blank lines at the start of a page
isLineBreak = true;
lastWordEndingIndex = i;
}
else if (c == ‘r’)
{
isLineBreak = isForcedPageBreak = true;
lastWordEndingIndex = i;
// This is the start of a chapter
// Add 1 because the page break isn’t added yet
Chapters.Add(this.pageBreaks.Count + 1);
}
else
{
currentLineWidth += this.characterWidths[c];
// Check for a needed line break
if (currentLineWidth > pageWidth)
isLineBreak = true;
}
if (isLineBreak)
{
linesOnThisPage++;
if (lastWordEndingIndex<=this.lineBreaks[this.lineBreaks.Count-1])
{
// The last spot where the line can be broken was already
// used as a line break. Therefore, we have no choice but to
// force a line break right now.
}
else
{
// Move back to first character after the actual break, which
// we may have passed due to word wrapping
i = lastWordEndingIndex;
}
// Reset the width for the next line
currentLineWidth = 0;
// Skip the space between split words
int breakIndex;
if (i < this.Text.Length – 1 && this.Text[i + 1] == ‘ ‘)
breakIndex = i + 1;
else
breakIndex = i;
this.lineBreaks.Add(breakIndex);
// See if this is a page break.
// It is if the NEXT line would be cut off
bool isNaturalPageBreak = (linesOnThisPage + 1) > linesPerPage;
if (isForcedPageBreak || isNaturalPageBreak)
{
this.pageBreaks.Add(breakIndex);
// Reset
linesOnThisPage = 0;
}
}
else if (c == ‘ ‘ || c == ‘-’ || c == ‘–’)
lastWordEndingIndex = i; // This can be used as a line break
// if we run out of space
}
// Add a final line break and page break
// marking the end of the document
if (this.lineBreaks[this.lineBreaks.Count – 1] != this.Text.Length)
{
this.lineBreaks.Add(this.Text.Length);
this.pageBreaks.Add(this.Text.Length);
}
// We’re done!
doneCallback();
this.isUpdating = false;
};
worker2.RunWorkerAsync();
});
};
worker.RunWorkerAsync();
}
public void ShowPageWithCharacterIndex(int characterIndex)
{
if (characterIndex < 0 || characterIndex >= this.Text.Length ||
this.Text == null)
return;
int pageBreakIndex = this.pageBreaks.BinarySearch(characterIndex);
if (pageBreakIndex < 0)
{
// The characterIndex doesn’t match an exact page break, but BinarySearch
// has returned a negative number that is the bitwise complement of the
// index of the next element that is larger than characterIndex
// (or the list’s count if there is no larger element).
// By subtracting one, this gives the index of the smaller element, or
// the index of the last element if the index is too big.
// Because 0 is in the list, this will always give a valid index.
pageBreakIndex = ~pageBreakIndex – 1;
}
// If the page break index is the last one (signifying the last character
// of the book), go back one so we’ll render the whole last page
if (pageBreakIndex == this.pageBreaks.Count – 1)
pageBreakIndex–;
ShowPage(pageBreakIndex + 1); // 1-based instead of 0-based
}
public void ShowPage(int pageNumber)
{
if (pageNumber >= this.pageBreaks.Count || this.Text == null)
return;
this.currentPageBreakIndex = pageNumber – 1;
RefreshCurrentPage();
}
public void ShowPreviousPage()
{
if (this.currentPageBreakIndex == 0 || this.Text == null)
return;
this.currentPageBreakIndex–;
RefreshCurrentPage();
}
public void ShowNextPage()
{
if (this.currentPageBreakIndex >= this.pageBreaks.Count – 2 ||
this.Text == null)
return;
this.currentPageBreakIndex++;
RefreshCurrentPage();
}
public void RefreshCurrentPage()
{
// An exact match should always be found
int firstLineBreakIndex = this.lineBreaks.BinarySearch(
this.pageBreaks[this.currentPageBreakIndex]);
int lastLineBreakIndex = this.lineBreaks.BinarySearch(
this.pageBreaks[this.currentPageBreakIndex + 1]) – 1;
this.StackPanel.Children.Clear();
for (int i = firstLineBreakIndex; i <= lastLineBreakIndex; i++)
{
// We’re guaranteed that lastLineBreakIndex is always less than count – 1
string line = this.Text.Substring(this.lineBreaks[i],
this.lineBreaks[i + 1] – this.lineBreaks[i]);
line = line.Trim();
if (line.Length == 0)
line = “ “;
this.StackPanel.Children.Add(new TextBlock
{
Text = line,
Foreground = this.MeasuringTextBlock.Foreground,
FontSize = this.MeasuringTextBlock.FontSize,
FontFamily = this.MeasuringTextBlock.FontFamily
});
}
this.CurrentCharacterIndex = this.lineBreaks[firstLineBreakIndex];
}
}
}

[/code]

  • The index of each line break and page break is stored in respective lists. The list of page breaks is a subset of the list of line breaks, and this relationship is leveraged when a page must be rendered.
  • Inside UpdatePagination, as much work as possible is offloaded to a background thread. Because the actual measurement must be done on the UI thread, however, two background workers are used to transition from a background thread to the main thread then back to a background thread.
  • This control makes a few assumptions about the input text, and the Pride and Prejudice document included in the project has been preprocessed to make these assumptions true:
    • A line feed character (n) denotes a forced line break, which should only occur at the end of a paragraph. (The original text used a fixed line width and therefore places n characters at regular intervals, which defeats the purpose of the dynamic layout.)
    • A carriage return character (r) denotes the beginning of a chapter. This enables the automatic population of the chapters collection, which drives the population of the chapters list box on the main page.

The Finished Product

Book Reader (Pagination & List Picker)

 

Baby Name Eliminator (Local Databases & Embedded Resources)

Baby Name Eliminator provides the perfect technique for Type A personalities to name their babies. (It’s the technique my wife and I used to name our two sons!) Rather than trying to brainstorm names and worrying that you’re missing the perfect one, this app enables you to use the process of elimination to name your baby!

Baby Name Eliminator starts with a massive database of essentially every name ever used in the United States: 36,065 boy names and 60,438 girl names. After you choose a gender, the app enables you to quickly narrow down the list with a variety of filters. These filters are based on the popularity of each name, its starting/ending letter, and the year the name was first in use. Once you’ve finished filtering the list, you can eliminate names one-by-one until your decision is made.

When naming our sons, we went through several rounds, eliminating names that were obviously bad and leaving names that we had any hesitation about. Once we got down to about 20 names, my wife and I each picked our top 5 choices. With our first son, we only had one name in common, so our decision was made! If you and your spouse both have a Windows phone, independently eliminating names can be a fun way to come up with a final list of candidate names.

So where does this massive database of names come from? The Social Security Administration, which provides data about almost every first name used in a Social Security card application from 1880 to the present. There are a few caveats to this list:

  • For privacy reasons, only names used at least five times in any given year are included.
  • One-character names are excluded.
  • Many people born before 1937 never applied for a Social Security card, so data from these years is spotty.
  • Distinct spellings of the same name are treated as different names.
  • The data is raw and uncorrected. Sometimes the sex on an application is incorrect, causing some boy names to show up in the girl names list and vice versa. In addition, some names are recorded as “Unknown,” “Unnamed,” or “Baby.” Restricting your list to the top 1,000 or so names in any year generally gets rid of such artifacts.

To enable its filtering, this app makes use of two local databases—one for boy names and one for girl names.

Working with Local Databases

The lack of local database support in Windows Phone 7 is one of its more publicized shortcomings. Apps are encouraged to work with server-side databases instead, but this adds extra burden for developers and extra hassle for users (latency, a working data connection, and potential data charges). Fortunately, several third-party database options exist. My favorite is an open-source port of SQLite for Windows Phone 7 created by Dan Ciprian Ardelean. You can read about it at http://sviluppomobile.blogspot.com/ 2010/03/sqlite-for-wp-7-series-proof-of-concept.html and get the latest version (at the time of this writing) at http://www.neologics.eu/Dan/WP7_Sqlite_20.09.2010.zip. This includes C# source code and a Community.CsharpSqlite.WP.dll assembly that you can reference in your project. It’s certainly not bug-free, but it works quite well for a number of scenarios (such as the needs of this app).

SQLite for Windows Phone 7 reads from and writes to database files in isolated storage. If you want to ship a database with your app that’s already filled with data, you can include the database file in your project with a Build Action of Content. At run-time, your app can retrieve the file then save it to isolated storage before its first use of SQLite.

How can I create a .db file that contains the database I want to ship with my app?

I followed the somewhat-cumbersome approach of writing a Windows Phone app that

  1. Uses SQLite to generate the database, executing CREATE TABLE and INSERT commands
  2. Retrieves the raw bytes from the .db file saved by SQLite to isolated storage, using thenormal isolated storage APIs
  3. Copies the bytes from the Visual Studio debugger as a Base64-encoded string and saves them to the needed .db file with a separate (desktop) program that decodes the string

Listing 24.1 contains a DatabaseHelper class used by Baby Name Eliminator that handles all interaction with the two SQLite databases included in the app.

LISTING 24.1 DatabaseHelper.cs—A Class That Wraps SQLite

[code]

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.IsolatedStorage;
using System.Windows;
using System.Windows.Resources;
using SQLiteClient;
namespace WindowsPhoneApp
{
public class DatabaseHelper
{
// The name of the file included as content in this project,
// also used as the isolated storage filename
public static string DatabaseName { get; set; }
// “Load” the database. If the file does not yet exist in isolated storage,
// copy it from the original file. If the file already exists,
// this is a no-op.
public static void LoadAsync(Action callback)
{
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += delegate(object sender, DoWorkEventArgs e)
{
if (!HasLoadedBefore)
{
StreamResourceInfo info = Application.GetResourceStream(
new Uri(DatabaseName, UriKind.Relative));
using (info.Stream)
SaveFile(DatabaseName, info.Stream);
}
if (callback != null)
callback();
};
worker.RunWorkerAsync();
}
// Retrieve a single value from the database
public static void ExecuteScalar(string command, Action<object> onSuccess,
Action<Exception> onError = null)
{
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += delegate(object sender, DoWorkEventArgs e)
{
try
{
object result = null;
using (SQLiteConnection db = new SQLiteConnection(DatabaseName))
{
db.Open();
SQLiteCommand c = db.CreateCommand(command);
result = c.ExecuteScalar();
}
if (onSuccess != null)
onSuccess(result);
}
catch (Exception ex)
{
if (onError != null)
onError(ex);
}
};
worker.RunWorkerAsync();
}
// Retrieve a collection of items from the database
public static void ExecuteQuery<T>(string command,
Action<IEnumerable<T>> onSuccess,
Action<Exception> onError = null) where T : new()
{
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += delegate(object sender, DoWorkEventArgs e)
{
try
{
IEnumerable<T> result = null;
List<T> copy = new List<T>();
using (SQLiteConnection db = new SQLiteConnection(DatabaseName))
{
db.Open();
SQLiteCommand c = db.CreateCommand(command);
result = c.ExecuteQuery<T>();
// Copy the data, because enumeration only
// works while the connection is open
copy.AddRange(result);
}
if (onSuccess != null)
onSuccess(copy);
}
catch (Exception ex)
{
if (onError != null)
onError(ex);
}
};
worker.RunWorkerAsync();
}
public static bool HasLoadedBefore
{
get
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
return userStore.FileExists(DatabaseName);
}
}
// Save a stream to isolated storage
static void SaveFile(string filename, Stream data)
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream = userStore.CreateFile(filename))
{
// Get the bytes
byte[] bytes = new byte[data.Length];
data.Read(bytes, 0, bytes.Length);
// Write the bytes to the new stream
stream.Write(bytes, 0, bytes.Length);
}
}
}
}

[/code]

  • To enable a responsive user interface while expensive database operations are conducted, interaction with SQLite is done on a background thread with the help of BackgroundWorker, and success/failure is communicated via callbacks.
  • The command strings passed to ExecuteScalar and ExecuteQuery can be SQL commands like SELECT COUNT(*) FROM table.
  • ExecuteQuery is a generic method whose generic argument (T) must be a class with a property corresponding to each column selected in the query.

Application.GetResourceStream works with files included in your project with a Build Action of Content or with a Build Action of Resource. For the latter case, the passed-in URI must have the following syntax:

/dllName;component/pathAndFilename

Note that dllName can refer to any DLL inside the .xap file, as long as it contains the requested resource. It should not contain the .dll suffix.

For this app, the DatabaseName string would look as follows for the database of boy names (Boys.db) included in the root of the project as a resource rather than content:

/WindowsPhoneApp;component/Boys.db

However, if this were done, Listing 24.1’s use of SaveFile would have to change, because the DatabaseName string would no longer be a valid filename for isolated storage.

Application.GetResourceStream Versus Assembly.GetManifestResourceStream

You might stumble across the Assembly.GetManifestResourceStream API as a way to read files included with your app.This works, but only for files marked with a Build Action of Embedded Resource (not Resource).Using this in Listing 24.1 instead of Application.GetResourceStream would look as follows:

[code]

if (!HasLoadedBefore)
{
using (Stream stream = typeof(DatabaseHelper).
Assembly.GetManifestResourceStream(DatabaseName))
SaveFile(DatabaseName, stream);
}

[/code]

However, the string passed to GetManifestResourceStream has its own unique syntax: dllName.filename, where dllName is the name of the DLL containing the embedded resource. That’s because the C# compiler automatically prepends the DLL name (minus the .dll extension) to the filename when naming each embedded resource. (You can see these names by opening a DLL in a tool such as .NET Reflector.) For this app, the two valid strings would be “WindowsPhoneApp.Boys.db” and “WindowsPhoneApp.Girls.db”.

There’s no significant reason to use this approach rather than the more flexible Application. GetResourceStream. Using GetResourceStream with files included as content is generally preferable compared to either scheme with files embedded as resources, because resources increase the size of DLLs, and that can increase an app’s load time.

The Filter Page

Rather than examine this app’s main page, which you can view in the included source code, we’ll examine the filter page that makes use of the DatabaseHelper class. The filter page, shown in Figure 24.1, displays how many names are in your list then enables you to filter it further with several options that map to SQL queries performed on the database. (The choice of boy names versus girl names is done previously on the main page.)

FIGURE 24.1 The filter page supports five different types of filters.
FIGURE 24.1 The filter page supports five different types of filters.

Each button reveals a dialog or other display, shown in Figure 24.2, that enables the user to control each relevant filter. Tapping the count of names reveals the actual list of names, as shown in Figure 24.3. This list doesn’t enable interactive elimination, however, as that is handled on the main page.

FIGURE 24.2 The result of tapping each button on the filter page.
FIGURE 24.2 The result of tapping each button on the filter page.
FIGURE 24.3 Previewing the filtered list of names.
FIGURE 24.3 Previewing the filtered list of names.

Listing 24.2 contains the XAML for the filter page.

LISTING 24.2 FilterPage.xaml—The User Interface for Baby Name Eliminator’s Filter Page

[code]

<phone:PhoneApplicationPage x:Name=”Page”
x:Class=”WindowsPhoneApp.FilterPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”>
<Grid Background=”Transparent”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”BABY NAME ELIMINATOR”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”apply filters”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<Grid Margin=”12,0”>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<!– The current number of names –>
<StackPanel Background=”Transparent” local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”Preview_Click”>
<TextBlock Text=”Current # of names (tap to preview):”
HorizontalAlignment=”Center”
Style=”{StaticResource LabelStyle}”/>
<TextBlock x:Name=”NumberTextBlock” Text=”0” Margin=”0,-16,0,0”
HorizontalAlignment=”Center”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”/>
</StackPanel>
<!– Progress indicator while a query is running –>
<Grid x:Name=”ProgressPanel”>
<Rectangle Fill=”{StaticResource PhoneBackgroundBrush}” Opacity=”.9”/>
<ProgressBar x:Name=”ProgressBar” VerticalAlignment=”Top”/>
<TextBlock x:Name=”ProgressText” TextWrapping=”Wrap”
HorizontalAlignment=”Center”
VerticalAlignment=”Top” Margin=”0,60,0,0” Text=”Loading”/>
</Grid>
<!– The five filter buttons –>
<ToggleButton x:Name=”RankMaxButton” Grid.Row=”1”
Content=”eliminate low-ranked names”
local:Tilt.IsEnabled=”True” Click=”RankMaxButton_Click”/>
<ToggleButton x:Name=”NameStartButton” Grid.Row=”2”
Content=”eliminate names starting with…”
local:Tilt.IsEnabled=”True” Click=”NameStartButton_Click”/>
<ToggleButton x:Name=”NameEndButton” Grid.Row=”3”
Content=”eliminate names ending with…”
local:Tilt.IsEnabled=”True” Click=”NameEndButton_Click”/>
<ToggleButton x:Name=”YearMaxButton” Grid.Row=”4”
Content=”eliminate modern names”
local:Tilt.IsEnabled=”True” Click=”YearMaxButton_Click”/>
<ToggleButton x:Name=”YearMinButton” Grid.Row=”5”
Content=”eliminate old-fashioned names”
local:Tilt.IsEnabled=”True” Click=”YearMinButton_Click”/>
<!– A user control that displays the letter grid in a popup –>
<local:LetterPicker x:Name=”LetterPicker”
Page=”{Binding ElementName=Page}”
Closed=”LetterPicker_Closed”/>
</Grid>
</ScrollViewer>
<!– Eliminate low-ranked names dialog –>
<local:Dialog x:Name=”RankMaxDialog” Grid.RowSpan=”2” Closed=”Dialog_Closed”>
<local:Dialog.InnerContent>
<StackPanel>
<TextBlock Text=”…” TextWrapping=”Wrap” Margin=”11,5,0,-5”/>
<TextBox MaxLength=”5” InputScope=”Number”
Text=”{Binding Result, Mode=TwoWay}”/>
<TextBlock Text=”Enter a number, or leave blank to clear this filter.”
TextWrapping=”Wrap” Margin=”11,-10,0,-10”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
</StackPanel>
</local:Dialog.InnerContent>
</local:Dialog>
<!– Eliminate modern names dialog –>
<local:Dialog x:Name=”YearMaxDialog” Grid.RowSpan=”2” Closed=”Dialog_Closed”>
<local:Dialog.InnerContent>
<StackPanel>
<TextBlock TextWrapping=”Wrap” Margin=”11,5,0,-5”>

</TextBlock>
<TextBox MaxLength=”4” InputScope=”Number”
Text=”{Binding Result, Mode=TwoWay}”/>
<TextBlock Text=”…” TextWrapping=”Wrap” Margin=”11,-10,0,-10”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
</StackPanel>
</local:Dialog.InnerContent>
</local:Dialog>
<!– Eliminate old-fashioned names dialog –>
<local:Dialog x:Name=”YearMinDialog” Grid.RowSpan=”2” Closed=”Dialog_Closed”>
<local:Dialog.InnerContent>
<StackPanel>
<TextBlock TextWrapping=”Wrap” Margin=”11,5,0,-5”>

</TextBlock>
<TextBox MaxLength=”4” InputScope=”Number”
Text=”{Binding Result, Mode=TwoWay}”/>
<TextBlock Text=”…” TextWrapping=”Wrap” Margin=”11,-10,0,-10”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
</StackPanel>
</local:Dialog.InnerContent>
</local:Dialog>
<!– The list of names shown when tapping the current number –>
<Grid x:Name=”PreviewPane” Grid.RowSpan=”2” Visibility=”Collapsed”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<Rectangle Grid.RowSpan=”2” Fill=”{StaticResource PhoneChromeBrush}”
Opacity=”.9”/>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock x:Name=”PreviewHeader”
Style=”{StaticResource PhoneTextTitle0Style}”/>
</StackPanel>
<ListBox Grid.Row=”1” x:Name=”PreviewListBox” Margin=”24,0,0,0”/>
</Grid>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • The five filter buttons are toggle buttons whose IsChecked state is managed by code-behind. If a filter is active, its corresponding button is checked (highlighted) so the user can see this without tapping every button and double-checking its filter settings.
  • The progress bar and related user interface, shown while a query is executing on a background thread, is shown in Figure 24.4. Because it does not occupy the whole screen, it enables the user to continue working if he or she doesn’t care to wait for the current count of names.
FIGURE 24.4 Showing progress while a database query executes on a background thread.
FIGURE 24.4 Showing progress while a database query executes on a background thread.

Listing 24.3 contains the code-behind for the filter page.

LISTING 24.3 FilterPage.xaml.cs—The Code-Behind for Baby Name Eliminator’s Filter Page

[code]

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class FilterPage : PhoneApplicationPage
{
public FilterPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
RefreshCount();
RefreshButtons();
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
// If a dialog, letter picker, or preview pane is open,
// close it instead of leaving the page
if (this.RankMaxDialog.Visibility == Visibility.Visible)
{
e.Cancel = true;
this.RankMaxDialog.Hide(MessageBoxResult.Cancel);
}
else if (this.YearMaxDialog.Visibility == Visibility.Visible)
{
e.Cancel = true;
this.YearMaxDialog.Hide(MessageBoxResult.Cancel);
}
else if (this.YearMinDialog.Visibility == Visibility.Visible)
{
e.Cancel = true;
this.YearMinDialog.Hide(MessageBoxResult.Cancel);
}
else if (this.PreviewPane.Visibility == Visibility.Visible)
{
e.Cancel = true;
this.PreviewPane.Visibility = Visibility.Collapsed;
}
}
void RefreshCount()
{
// Choose one of the included databases: boy names or girl names
if (Settings.IsBoy.Value.Value)
DatabaseHelper.DatabaseName = “Boys.db”;
else
DatabaseHelper.DatabaseName = “Girls.db”;
if (!DatabaseHelper.HasLoadedBefore)
{
ShowProgress(“Preparing database for the first time…”);
this.RankMaxButton.IsEnabled = false;
this.NameStartButton.IsEnabled = false;
this.NameEndButton.IsEnabled = false;
this.YearMaxButton.IsEnabled = false;
this.YearMinButton.IsEnabled = false;
}
DatabaseHelper.LoadAsync(delegate()
{
// The callback is called on a background thread, so transition back
// to the main thread for manipulating UI
this.Dispatcher.BeginInvoke(delegate()
{
ShowProgress(“Counting names…”);
this.RankMaxButton.IsEnabled = true;
this.NameStartButton.IsEnabled = true;
this.NameEndButton.IsEnabled = true;
this.YearMaxButton.IsEnabled = true;
this.YearMinButton.IsEnabled = true;
// Execute a query
DatabaseHelper.ExecuteScalar(“SELECT COUNT(*) FROM Names “ +
Settings.BuildQuerySuffix(), delegate(object result)
{
// The callback is called on a background thread, so transition back
// to the main thread for manipulating UI
this.Dispatcher.BeginInvoke(delegate()
{
HideProgress();
this.NumberTextBlock.Text = ((int)result).ToString(“N0”);
});
});
});
});
}
void RefreshButtons()
{
// Check (highlight) any button whose filter is active
this.RankMaxButton.IsChecked =
Settings.RankMax.Value != Settings.RankMax.DefaultValue;
this.NameStartButton.IsChecked =
Settings.ExcludedStartingLetters.Value.Count > 0;
this.NameEndButton.IsChecked =
Settings.ExcludedEndingLetters.Value.Count > 0;
this.YearMaxButton.IsChecked =
Settings.YearMax.Value != Settings.YearMax.DefaultValue;
this.YearMinButton.IsChecked =
Settings.YearMin.Value != Settings.YearMin.DefaultValue;
}
void Preview_Click(object sender, MouseButtonEventArgs e)
{
this.PreviewHeader.Text = “LOADING…”;
this.PreviewListBox.ItemsSource = null;
this.PreviewPane.Visibility = Visibility.Visible;
// Choose one of the included databases: boy names or girl names
if (Settings.IsBoy.Value.Value)
DatabaseHelper.DatabaseName = “Boys.db”;
else
DatabaseHelper.DatabaseName = “Girls.db”;
DatabaseHelper.LoadAsync(delegate()
{
// It’s okay to execute this on the background thread
DatabaseHelper.ExecuteQuery<Record>(“SELECT Name FROM Names “ +
Settings.BuildQuerySuffix(), delegate(IEnumerable<Record> result)
{
// Transition back to the main thread for manipulating UI
this.Dispatcher.BeginInvoke(delegate()
{
this.PreviewHeader.Text = “PRESS BACK WHEN DONE”;
this.PreviewListBox.ItemsSource = result;
});
});
});
}
void ShowProgress(string message)
{
this.ProgressText.Text = message;
this.ProgressBar.IsIndeterminate = true;
this.ProgressPanel.Visibility = Visibility.Visible;
}
void HideProgress()
{
this.ProgressPanel.Visibility = Visibility.Collapsed;
this.ProgressBar.IsIndeterminate = false; // Avoid a perf problem
}
// A click handler for each of the five filter buttons
void RankMaxButton_Click(object sender, RoutedEventArgs e)
{
if (Settings.RankMax.Value != null)
RankMaxDialog.Result = Settings.RankMax.Value.Value;
RankMaxDialog.Show();
}
void NameStartButton_Click(object sender, RoutedEventArgs e)
{
this.LetterPicker.SetBinding(LetterPicker.ExcludedLettersProperty,
new Binding { Path = new PropertyPath(“Value”),
Source = Settings.ExcludedStartingLetters,
Mode = BindingMode.TwoWay });
this.LetterPicker.ShowPopup();
}
void NameEndButton_Click(object sender, RoutedEventArgs e)
{
this.LetterPicker.SetBinding(LetterPicker.ExcludedLettersProperty,
new Binding { Path = new PropertyPath(“Value”),
Source = Settings.ExcludedEndingLetters,
Mode = BindingMode.TwoWay });
this.LetterPicker.ShowPopup();
}
void YearMaxButton_Click(object sender, RoutedEventArgs e)
{
if (Settings.YearMax.Value != null)
YearMaxDialog.Result = Settings.YearMax.Value.Value;
YearMaxDialog.Show();
}
void YearMinButton_Click(object sender, RoutedEventArgs e)
{
if (Settings.YearMin.Value != null)
YearMinDialog.Result = Settings.YearMin.Value.Value;
YearMinDialog.Show();
}
// Two handlers for the dialog or letter picker being closed
void LetterPicker_Closed(object sender, EventArgs e)
{
RefreshCount();
RefreshButtons();
}
void Dialog_Closed(object sender, MessageBoxResultEventArgs e)
{
if (e.Result == MessageBoxResult.OK)
{
// Update or clear a setting, depending on which dialog was just closed
int result;
if (sender == RankMaxDialog)
{
if (RankMaxDialog.Result != null &&
int.TryParse(RankMaxDialog.Result.ToString(), out result))
Settings.RankMax.Value = result;
else
Settings.RankMax.Value = null;
}
if (sender == YearMaxDialog)
{
if (YearMaxDialog.Result != null &&
int.TryParse(YearMaxDialog.Result.ToString(), out result))
Settings.YearMax.Value = (short)result;
else
Settings.YearMax.Value = null;
}
if (sender == YearMinDialog)
{
if (YearMinDialog.Result != null &&
int.TryParse(YearMinDialog.Result.ToString(), out result))
Settings.YearMin.Value = (short)result;
else
Settings.YearMin.Value = null;
}
// Only bother refreshing the count if the dialog result is OK
RefreshCount();
}
// Refresh buttons when the dialog is closed for any reason,
// to undo automatic check-when-tapped
RefreshButtons();
}
}
}

[/code]

  • This project includes two databases (Boys.db and Girls.db) that have an identical schema. They contain a single table called Names with three columns: Name, BestRank (its best single-year ranking), and FirstYear (the first year the name appeared in Social Security data).
  • The query to refresh the count of names is “SELECT COUNT(*) FROM Names” with a WHERE clause based on settings whose values are determined by the filters. The settings and the BuildQuerySuffix method are defined in Listing 24.4.
  • The query to display the list of actual names is “SELECT Name FROM Names” with the same WHERE clause. The Record class used with ExecuteQuery is therefore a class with a single string Name property:

    [code]
    public class Record
    {
    public string Name { get; set; }
    public override string ToString()
    {
    return this.Name;
    }
    }
    [/code]
    The ToString method enables the collection of Records to be used as the data source for the preview list box without any item template, as the default ToStringin- a-text-block rendering is sufficient.

  • Just like the date picker in the preceding chapter, this app leverages two-way data binding with each letter picker.

LISTING 24.4 Settings.cs—The Settings Class for Baby Name Eliminator

[code]

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public static class Settings
{
// Step 1: Gender
public static readonly Setting<bool?> IsBoy =
new Setting<bool?>(“IsBoy”, null);
// Step 2: Filters
public static readonly Setting<int?> RankMax =
new Setting<int?>(“RankMax”, null);
public static readonly Setting<List<char>> ExcludedStartingLetters =
new Setting<List<char>>(“IncludedStartingLetters”, new List<char>());
public static readonly Setting<List<char>> ExcludedEndingLetters =
new Setting<List<char>>(“ExcludedEndingLetters”, new List<char>());
public static readonly Setting<short?> YearMax =
new Setting<short?>(“YearMax”, null);
public static readonly Setting<short?> YearMin =
new Setting<short?>(“YearMin”, null);
// Step 3: Elimination
public static readonly Setting<ObservableCollection<string>> FilteredList =
new Setting<ObservableCollection<string>>(“FilteredList”, null);
public static readonly Setting<double> ScrollPosition =
new Setting<double>(“ScrollPosition”, 0);
// Orientation lock for the main page
public static readonly Setting<SupportedPageOrientation>
SupportedOrientations = new Setting<SupportedPageOrientation>(
“SupportedOrientations”, SupportedPageOrientation.PortraitOrLandscape);
// Build up a WHERE clause if any filters have been chosen
public static string BuildQuerySuffix()
{
List<string> conditions = new List<string>();
if (Settings.RankMax.Value != null)
conditions.Add(“ BestRank <= “ + Settings.RankMax.Value.Value);
foreach (char c in Settings.ExcludedStartingLetters.Value)
conditions.Add(“ NOT Name LIKE ‘“ + c + “%’”);
foreach (char c in Settings.ExcludedEndingLetters.Value)
conditions.Add(“ NOT Name LIKE ‘%” + c + “‘“);
if (Settings.YearMax.Value != null)
conditions.Add(“ FirstYear <= “ + Settings.YearMax.Value.Value);
if (Settings.YearMin.Value != null)
conditions.Add(“ FirstYear >= “ + Settings.YearMin.Value.Value);
if (conditions.Count == 0)
return “”;
else
{
StringBuilder whereClause = new StringBuilder(“WHERE “);
whereClause.Append(conditions[0]);
for (int i = 1; i < conditions.Count; i++)
whereClause.Append(“ AND “ + conditions[i]);
return whereClause.ToString();
}
}
}
}

[/code]

The Finished Product

Baby Name Eliminator (Local Databases & Embedded Resources)