Pedometer (Walking Motion)

Apedometer counts how many steps you take. It is a handy device for people who are interested in getting enough exercise and perhaps need a little motivation. Pedometers—especially good ones—are expensive. Thanks to the built-in accelerometer, the Pedometer app enables you to turn your phone into a pedometer without the need for a separate device.

Current Windows phones do not report accelerometer data while the screen is turned off!

Although this app runs while the phone is locked, phones do not report accelerometer data while the screen is off. Unfortunately, this means that no steps can be registered while the screen is off.You must keep the screen on (and therefore the phone unlocked) for the entire time you use this app.This app disables the screen time-out, so you don’t have to worry about your phone automatically locking in your pocket.You do, however, have to worry about accidentally bumping buttons. For the best results, the screen should face away from your body.

The Main Page

This app has a main page, a settings page, and an instructions page . The main page shows the current number of steps and converts that number to miles and kilometers, based on a stride-length setting customized on the settings page. It starts out in a “paused” state, shown in Figure 50.1, in which no steps are registered. This cuts down on the reporting of bogus steps. The idea is that users press the start button while the phone is close to their pocket, and then they slide the phone in carefully. (The step-detection algorithm can easily register two bogus steps with a little bit of jostling of the phone.)

This page also shows a welcome message urging the user to calibrate the pedometer, but it only shows it the first time the page is loaded (on the first run of the app).

Listing 50.1 contains the XAML for the main page, and Listing 50.2 contains its code-behind.

The app starts out in a paused state to avoid registering bogus steps.
FIGURE 50.1 The app starts out in a paused state to avoid registering bogus steps.

LISTING 50.1 MainPage.xaml—The User Interface for Pedometer’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” shell:SystemTray.IsVisible=”True”>
<!– The application bar, with four buttons –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.5”
BackgroundColor=”{StaticResource PhoneAccentColor}”>
<shell:ApplicationBarIconButton Text=”start”
IconUri=”/Shared/Images/appbar.play.png”
Click=”StartPauseButton_Click”/>
<shell:ApplicationBarIconButton Text=”settings” Click=”SettingsButton_Click”
IconUri=”/Shared/Images/appbar.settings.png”/>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBarIconButton Text=”reset”
IconUri=”/Shared/Images/appbar.delete.png”
Click=”ResetButton_Click”/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<!– The accent-colored foot –>
<Rectangle Opacity=”.5” Fill=”{StaticResource PhoneAccentBrush}” Width=”277”
Height=”648” Margin=”0,12,12,0” VerticalAlignment=”Top”
HorizontalAlignment=”Right”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/foot.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<StackPanel>
<!– PAUSED –>
<TextBlock x:Name=”PausedTextBlock” FontFamily=”Segoe WP Black”
HorizontalAlignment=”Center” Text=”PAUSED”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”/>
<!– Steps –>
<TextBlock x:Name=”StepsTextBlock” Margin=”12,12,0,0” Opacity=”.5”
FontSize=”130”/>
<TextBlock Text=”steps” Margin=”18,-24” FontWeight=”Bold”
FontSize=”{StaticResource PhoneFontSizeMedium}”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<!– Miles –>
<TextBlock x:Name=”MilesTextBlock” Margin=”12,72,0,0” Opacity=”.5”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”/>
<TextBlock Text=”miles” Margin=”18,-12” FontWeight=”Bold”
FontSize=”{StaticResource PhoneFontSizeMedium}”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<!– Kilometers –>
<TextBlock x:Name=”KilometersTextBlock” Margin=”12,72,0,0” Opacity=”.5”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”/>
<TextBlock Text=”kilometers” Margin=”18,-12” FontWeight=”Bold”
FontSize=”{StaticResource PhoneFontSizeMedium}”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
</StackPanel>
<!– The special one-time message –>
<Border x:Name=”FirstRunPanel” Visibility=”Collapsed” Width=”300”
Margin=”0,0,24,0” HorizontalAlignment=”Right” VerticalAlignment=”Center”
Background=”{StaticResource PhoneAccentBrush}” Padding=”24”>
<TextBlock TextWrapping=”Wrap” Text=”Welcome! Be sure to calibrate …”/>
</Border>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Both the foot and the application bar are given the phone theme’s accent color, but with 50% opacity to make them more subtle.

LISTING 50.2 MainPage.xaml.cs—The Code-Behind for Pedometer’s Main Page

[code]

using System;
using System.ComponentModel;
using System.Windows;
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
{
IApplicationBarIconButton startPauseButton;
IApplicationBarIconButton settingsButton;
IApplicationBarIconButton instructionsButton;
IApplicationBarIconButton resetButton;
bool pastPositiveThreshold = true;
public MainPage()
{
InitializeComponent();
this.startPauseButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
this.settingsButton = this.ApplicationBar.Buttons[1]
as IApplicationBarIconButton;
this.instructionsButton = this.ApplicationBar.Buttons[2]
as IApplicationBarIconButton;
this.resetButton = this.ApplicationBar.Buttons[3]
as IApplicationBarIconButton;
SoundEffects.Initialize();
// Use the accelerometer via Microsoft’s helper
AccelerometerHelper.Instance.ReadingChanged
+= Accelerometer_ReadingChanged;
// Allow the app to run 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);
RefreshDisplay();
// Show the welcome message if this is the first time ever
if (Settings.FirstRun.Value)
{
this.FirstRunPanel.Visibility = Visibility.Visible;
Settings.FirstRun.Value = false;
}
else
{
this.FirstRunPanel.Visibility = Visibility.Collapsed;
}
// 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;
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
if (AccelerometerHelper.Instance.Active)
{
MessageBox.Show(“You must pause the pedometer before leaving. …”,
“Pedometer Still Collecting Data”, MessageBoxButton.OK);
e.Cancel = true;
}
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerHelperReadingEventArgs e)
{
bool newStep = false;
double magnitude = e.OptimalyFilteredAcceleration.Magnitude;
if (!pastPositiveThreshold && magnitude > 1 + Settings.Threshold.Value)
{
newStep = true;
pastPositiveThreshold = true;
}
if (magnitude < 1 – Settings.Threshold.Value)
pastPositiveThreshold = false;
if (newStep)
{
// We only know about one leg, so count each new step as two
Settings.NumSteps.Value += 2;
this.Dispatcher.BeginInvoke(delegate()
{
RefreshDisplay();
if (Settings.PlaySound.Value)
SoundEffects.Ding.Play();
});
}
}
void RefreshDisplay()
{
this.StepsTextBlock.Text = Settings.NumSteps.Value.ToString(“N0”);
double totalInches = Settings.NumSteps.Value * Settings.Stride.Value;
this.MilesTextBlock.Text = (totalInches / 63360).ToString(“##0.####”);
this.KilometersTextBlock.Text =
(totalInches * 0.0000254).ToString(“##0.####”);
}
// Application bar handlers
void StartPauseButton_Click(object sender, EventArgs e)
{
if (AccelerometerHelper.Instance.Active)
{
// Stop the accelerometer with Microsoft’s helper
AccelerometerHelper.Instance.Active = false;
this.startPauseButton.Text = “start”;
this.startPauseButton.IconUri
= new Uri(“/Shared/Images/appbar.play.png”, UriKind.Relative);
this.PausedTextBlock.Text = “PAUSED”;
this.StepsTextBlock.Opacity = .5;
this.MilesTextBlock.Opacity = .5;
this.KilometersTextBlock.Opacity = .5;
this.settingsButton.IsEnabled = true;
this.instructionsButton.IsEnabled = true;
this.resetButton.IsEnabled = true;
}
else
{
// Start the accelerometer with Microsoft’s helper
AccelerometerHelper.Instance.Active = true;
this.startPauseButton.Text = “pause”;
this.startPauseButton.IconUri
= new Uri(“/Shared/Images/appbar.pause.png”, UriKind.Relative);
this.PausedTextBlock.Text = “”;
this.StepsTextBlock.Opacity = 1;
this.MilesTextBlock.Opacity = 1;
this.KilometersTextBlock.Opacity = 1;
this.settingsButton.IsEnabled = false;
this.instructionsButton.IsEnabled = false;
this.resetButton.IsEnabled = false;
}
}
void SettingsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/SettingsPage.xaml”, UriKind.Relative));
}
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/InstructionsPage.xaml”, UriKind.Relative));
}
void ResetButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to clear your data?”, “Reset”,
MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
Settings.NumSteps.Value = 0;
RefreshDisplay();
}
}
}
}

[/code]

  • This listing uses the following settings defined in a separate Settings.cs file as follows:
    [code]
    public static class Settings
    {
    // Configurable settings
    public static readonly Setting<double> Threshold =
    new Setting<double>(“Threshold”, .43);
    public static readonly Setting<bool> PlaySound =
    new Setting<bool>(“PlaySound”, true);
    public static readonly Setting<double> Stride =
    new Setting<double>(“Stride”, 28);
    // Current state
    public static readonly Setting<int> NumSteps =
    new Setting<int>(“NumSteps”, 0);
    // A special flag that is set to false during the app’s first run
    public static readonly Setting<bool> FirstRun =
    new Setting<bool>(“FirstRun”, true);
    }
    [/code]
  • The walking detection, done inside Accelerometer_ReadingChanged, uses the magnitude of the acceleration vector reported by the AccelerometerHelper library (the square root of X2 + Y2 + Z2). Therefore, the direction of the motion doesn’t matter; just the amount of motion. A phone at rest has a magnitude of 1 (pointing toward the Earth), so this algorithm looks for a sufficiently smaller magnitude followed by a sufficiently larger magnitude. This represents the motion of the single leg holding the phone, so the detection of each step is actually counted as two steps, one per leg. The definition of sufficient is determined by the Threshold setting customized on the settings page.

The Settings Page

The settings page must be visited frequently in order to calibrate the pedometer just right.
FIGURE 50.2 The settings page must be visited frequently in order to calibrate the pedometer just right.

The settings page, shown in Figure 50.2, enables the user to change the values of the Threshold, PlaySound, and Stride settings used in the previous listing.

Users can try different sensitivity values (which map to Threshold) and then manually count their steps while Pedometer runs to see if the two counts match. To make this process even easier, this app supports playing a sound every time a step is detected. When the value is correct, the user will hear a sound every other step (when the leg carrying the phone makes a step). Once a good sensitivity value is found, the user can turn off the sound.

The best sensitivity value can vary based on where the user puts the phone, the depth of the pocket containing the phone, and other factors. Therefore, a sensitivity value that works best one day may not work well the next.

To give accurate values for miles and kilometers, users must enter their stride length at the bottom of the page. If users are unable to measure their stride length, they can multiply their height (in inches) by .413 for a woman or .415 for a man. The average stride length for a woman is 26.4 inches, and the average stride length for a man is 30 inches. This information is explained on this app’s instructions page.

Listing 50.3 contains the XAML for the settings page, and Listing 50.4 contains its codebehind.

LISTING 50.3 SettingsPage.xaml—The User Interface for Pedometer’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”
xmlns:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<Grid Background=”Transparent”>
<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=”pedometer”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<StackPanel Margin=”{StaticResource PhoneMargin}”>
<TextBlock Text=”Tap &amp; hold …” TextWrapping=”Wrap” Margin=”12,0”/>
<!– The slider with three supporting text blocks –>
<Grid Height=”150”>
<TextBlock HorizontalAlignment=”Right” Margin=”0,12,12,0”
VerticalAlignment=”Center” Text=”report more steps”/>
<TextBlock HorizontalAlignment=”Left” Margin=”12,12,0,0”
VerticalAlignment=”Center” Text=”report fewer steps”/>
<TextBlock x:Name=”ThresholdTextBlock” Margin=”12,6,0,0”
Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Left” VerticalAlignment=”Top”
FontSize=”{StaticResource PhoneFontSizeLarge}”/>
<Slider x:Name=”ThresholdSlider” Minimum=”.1” Maximum=”1”
LargeChange=”.001” IsDirectionReversed=”True”
Value=”{Binding Threshold, Mode=TwoWay, ElementName=Page}”/>
</Grid>
<!– reset –>
<Button Content=”reset sensitivity” Click=”ResetButton_Click”
local:Tilt.IsEnabled=”True”/>
<!– Play a sound toggle switch –>
<toolkit:ToggleSwitch x:Name=”PlaySoundToggleSwitch” Margin=”0,30,0,0”
Header=”Play a sound every other step”
IsChecked=”{Binding PlaySound, Mode=TwoWay, ElementName=Page}”/>
<!– Stride length –>
<TextBlock Style=”{StaticResource LabelStyle}”
Text=”Stride length (in inches)”/>
<TextBox x:Name=”StrideLengthTextBox” InputScope=”Number”
Text=”{Binding StrideLength, Mode=TwoWay, ElementName=Page}”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 50.4 SettingsPage.xaml.cs—The Code-Behind for Pedometer’s Settings Page

[code]

using System.Windows;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
public SettingsPage()
{
InitializeComponent();
this.ThresholdTextBlock.Text =
(Settings.Threshold.Value * 1000).ToString(“N0”);
}
// Simple property bound to the slider
public double Threshold
{
get { return Settings.Threshold.Value; }
set { this.ThresholdTextBlock.Text = (value * 1000).ToString(“N0”);
Settings.Threshold.Value = value; }
}
// Simple property bound to the toggle switch
public bool PlaySound
{
get { return Settings.PlaySound.Value; }
set { Settings.PlaySound.Value = value; }
}
// Simple property bound to the text box
public string StrideLength
{
get { return Settings.Stride.Value.ToString(); }
set
{
try { Settings.Stride.Value = int.Parse(value); }
catch { }
}
}
void ResetButton_Click(object sender, RoutedEventArgs e)
{
this.ThresholdSlider.Value = Settings.Threshold.DefaultValue;
}
}
}

[/code]

The Finished Product

Pedometer (Walking Motion)

 

 

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)

Talking Parrot (Recording & Playing)

The Talking Parrot app provides more entertainment with the microphone. After greeting you with a friendly “hello,” the parrot listens to what you say and then repeats it in its own voice, with a whistle or squawk thrown in. This app must not only turn the buffer collected from the microphone into a playable XNA sound effect, but it also must determine when is a good time to listen to the user and when is a good time to play the captured audio.

The Main User Interface

Listing 35.1 contains the XAML for the main page. It consists of three parts: a bunch of animations that are triggered by code-behind, an application bar, and several images (plus one ellipse) placed in specific spots on a canvas. The individual images that form the parrot are shown in Figure 35.1.

The parrot consists of nine images that can be individually animated.
FIGURE 35.1 The parrot consists of nine images that can be individually animated.

LISTING 35.1 MainPage.xaml—The User Interface for Talking Parrot’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”>
<!– Animations as named resources –>
<phone:PhoneApplicationPage.Resources>
<Storyboard x:Name=”BlinkStoryboard” Duration=”0:0:4” RepeatBehavior=”Forever”
Storyboard.TargetProperty=
“(UIElement.RenderTransform).(CompositeTransform.TranslateY)”>
<DoubleAnimation Storyboard.TargetName=”TopEyelidImage” To=”0”
Duration=”0:0:.1” AutoReverse=”True”/>
<DoubleAnimation Storyboard.TargetName=”BottomEyelidImage” To=”77”
Duration=”0:0:.1” AutoReverse=”True”/>
</Storyboard>
<Storyboard x:Name=”HeadUpStoryboard” Storyboard.TargetProperty=
“(UIElement.RenderTransform).(CompositeTransform.TranslateY)”>
<DoubleAnimation Storyboard.TargetName=”HeadCanvas” To=”-7”
Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuinticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<Storyboard x:Name=”HeadDownStoryboard” Storyboard.TargetProperty=
“(UIElement.RenderTransform).(CompositeTransform.TranslateY)”>
<DoubleAnimation Storyboard.TargetName=”HeadCanvas” To=”0” Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<Storyboard x:Name=”WingFlutterStoryboard” Storyboard.TargetProperty=
“(UIElement.RenderTransform).(CompositeTransform.Rotation)”>
<DoubleAnimation Storyboard.TargetName=”WingImage” To=”35” Duration=”0:0:1”
AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<BounceEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<Storyboard x:Name=”StompStoryboard” Storyboard.TargetProperty=
“(UIElement.RenderTransform).(CompositeTransform.TranslateY)”>
<DoubleAnimation Storyboard.TargetName=”LeftFootImage” To=”-30”
Duration=”0:0:.2” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<QuinticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetName=”RightFootImage” To=”-10”
BeginTime=”0:0:.1” Duration=”0:0:.2” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<QuinticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<Storyboard x:Name=”SpeakStoryboard” Storyboard.TargetProperty=
“(UIElement.RenderTransform).(CompositeTransform.Rotation)”>
<DoubleAnimationUsingKeyFrames x:Name=”TopBeakAnimation”
Storyboard.TargetName=”TopBeakImage” />
<DoubleAnimationUsingKeyFrames x:Name=”BottomBeakAnimation”
Storyboard.TargetName=”BottomBeakImage” />
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– The application bar –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.5”>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<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>
<!– Many images placed in a canvas –>
<Canvas>
<Image Source=”Images/background.png” Stretch=”None”/>
<Image x:Name=”RightFootImage” Source=”Images/rightFoot.png”
Canvas.Left=”159” Canvas.Top=”537”>
<Image.RenderTransform>
<CompositeTransform TranslateY=”0”/>
</Image.RenderTransform>
</Image>
<Image x:Name=”LeftFootImage” Source=”Images/leftFoot.png”
Canvas.Left=”109” Canvas.Top=”532”>
<Image.RenderTransform>
<CompositeTransform TranslateY=”0”/>
</Image.RenderTransform>
</Image>
<Image Source=”Images/body.png” Canvas.Left=”-164” Canvas.Top=”346”/>
<Canvas x:Name=”HeadCanvas”>
<Canvas.RenderTransform>
<CompositeTransform TranslateY=”0”/>
</Canvas.RenderTransform>
<Image Source=”Images/head.png” Canvas.Left=”155” Canvas.Top=”203”/>
<Image x:Name=”BottomBeakImage” Source=”Images/bottomBeak.png”
Canvas.Left=”282” Canvas.Top=”294” RenderTransformOrigin=”.5,0”>
<Image.RenderTransform>
<CompositeTransform Rotation=”0” CenterX=”15” CenterY=”15”/>
</Image.RenderTransform>
</Image>
<Image x:Name=”TopBeakImage” Source=”Images/topBeak.png”
Canvas.Left=”279” Canvas.Top=”252”>
<Image.RenderTransform>
<CompositeTransform Rotation=”0” CenterX=”38” CenterY=”38”/>
</Image.RenderTransform>
</Image>
<Image Source=”Images/eyeball.png” Canvas.Left=”198” Canvas.Top=”248”/>
<Ellipse x:Name=”Pupil” Fill=”Black” Width=”18” Height=”18”
Canvas.Left=”240” Canvas.Top=”275”/>
<Image x:Name=”AngryEyelidImage” Source=”Images/eyelid.png”
Visibility=”Collapsed” Canvas.Left=”196” Canvas.Top=”246”>
<Image.RenderTransform>
<CompositeTransform Rotation=”15” CenterX=”76” CenterY=”39”/>
</Image.RenderTransform>
</Image>
<Image x:Name=”TopEyelidImage” Source=”Images/eyelid.png”
Canvas.Left=”196” Canvas.Top=”246”>
<Image.RenderTransform>
<CompositeTransform TranslateY=”-37” Rotation=”0”
CenterX=”76” CenterY=”39”/>
</Image.RenderTransform>
</Image>
<Image x:Name=”BottomEyelidImage” Source=”Images/eyelid.png”
Canvas.Left=”196” Canvas.Top=”246”>
<Image.RenderTransform>
<CompositeTransform ScaleY=”-1” TranslateY=”112”/>
</Image.RenderTransform>
</Image>
</Canvas>
<Image x:Name=”WingImage” Source=”Images/wing.png”
Canvas.Left=”68” Canvas.Top=”414”>
<Image.RenderTransform>
<CompositeTransform Rotation=”0” CenterX=”190” CenterY=”26”/>
</Image.RenderTransform>
</Image>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

  • The page is portrait-only due to the exact layout required by the background and pieces of the parrot.
  • The animations are given names rather than dictionary keys, so they can be easily referenced from code-behind.
  • The animations lower, raise, and rotate various pieces of the parrot when triggered by code-behind. Every animatable piece uses a composite transform for consistency and for ease in animating multiple aspects (such as rotation and translation).
  • SpeakStoryboard is special. Although it has two keyframe animations to animate the top and bottom beak, they start out empty. These are continually updated from code-behind based on the audio to be spoken, so the parrot appears to mouth the actual words and sounds it speaks.
  • Any of the parrot pieces (as well as the background) could have been created as vector shapes instead of images. Instead, the black pupil is the only vector shape used in the parrot (an ellipse). This gives us the flexibility to grow/shrink the pupil without pixelation and the flexibility to change its color, although this app doesn’t take advantage of these capabilities.

The Main Code-Behind

The code-behind for the main page is shown in Listing 35.2.

LISTING 35.2 MainPage.xaml.cs—The Code-Behind for Talking Parrot’s Main Page

[code]

using System;
using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using Microsoft.Phone.Controls;
using Microsoft.Xna.Framework.Audio;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
byte[] buffer;
// Used for capturing audio from the microphone
MemoryStream recordedStream;
long playbackStartPosition = -1;
int consecutiveSilentSamples;
DateTime? speakingDoneTime;
// Used for playing the three included sounds: hello, whistle, and squawk
bool playingIncludedSound;
Random random = new Random();
public MainPage()
{
InitializeComponent();
SoundEffects.Initialize();
CompositionTarget.Rendering += CompositionTarget_Rendering;
// Start blinking, which runs the whole time
this.BlinkStoryboard.Begin();
// Prevent the off-screen tail from being seen when
// animating to the instructions or about pages
this.Clip = new RectangleGeometry {
Rect = new Rect(0, 0, Constants.SCREEN_WIDTH, Constants.SCREEN_HEIGHT) };
// Configure the microphone with the smallest supported BufferDuration (.1)
Microphone.Default.BufferDuration = TimeSpan.FromSeconds(.1);
Microphone.Default.BufferReady += Microphone_BufferReady;
// Initialize the buffer for holding microphone data
int size = Microphone.Default.GetSampleSizeInBytes(
Microphone.Default.BufferDuration);
this.buffer = new byte[size];
// Initialize the stream used to record microphone data
this.recordedStream = new MemoryStream();
// Speak a “hello” greeting
PrepareStoryboardForIncludedSound(SoundEffects.HelloBuffer);
Speak(SoundEffects.Hello, 0);
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
// Get mad!
this.AngryEyelidImage.Visibility = Visibility.Visible;
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
// Become happy again
this.AngryEyelidImage.Visibility = Visibility.Collapsed;
}
void CompositionTarget_Rendering(object sender, EventArgs e)
{
// Required for XNA Microphone API to work
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
// Check if currently-playing audio has finished
if (this.speakingDoneTime != null && DateTime.Now > this.speakingDoneTime)
{
if (!this.playingIncludedSound)
{
// Don’t restart the microphone yet! We just played audio from the
// microphone, so add either a whistle or squawk to make it sound more
// like a parrot. This will reset speakingDoneTime.
int choice = random.Next(2); // A random number: either 0 or 1
byte[] buffer = (choice == 0 ? SoundEffects.WhistleBuffer :
SoundEffects.SquawkBuffer);
SoundEffect effect = (choice == 0 ? SoundEffects.Whistle :
SoundEffects.Squawk);
PrepareStoryboardForIncludedSound(buffer);
Speak(effect, 0); // Play at the normal speed (0)
}
else
{
// Now it’s time to restart the microphone
Microphone.Default.Start();
// Reset state
this.speakingDoneTime = null;
this.playingIncludedSound = false;
// Smoothly return the head to its resting position
this.HeadDownStoryboard.Begin();
}
}
}
void Speak(SoundEffect effect, float speed)
{
// Stop listening, because it is time to talk
Microphone.Default.Stop();
// Determine when the audio will be done playing and it’s time to either
// add a squawk/whistle or restart the microphone.
// The length is halved for microphone-recorded sounds to avoid extra lag
// time seen in practice.
this.speakingDoneTime = DateTime.Now +
TimeSpan.FromTicks((long)(effect.Duration.Ticks *
(speed == 0 ? 1 : speed / 2)));
// Stop any in-progress storyboards
this.SpeakStoryboard.Stop();
this.WingFlutterStoryboard.Stop();
this.HeadUpStoryboard.Stop();
this.StompStoryboard.Stop();
// Start the storyboards
this.SpeakStoryboard.Begin();
this.WingFlutterStoryboard.Begin();
this.HeadUpStoryboard.Begin();
this.StompStoryboard.Begin();
// Play the audio at full volume with the passed-in speed (pitch)
effect.Play(1, speed, 0);
}
// Changes the contents of TopBeakAnimation and BottomBeakAnimation
// to match the audio in the buffer, so the beak appears to speak the sounds
void PrepareStoryboardForIncludedSound(byte[] buffer)
{
ResetSpeakStoryboard();
// Loop through the buffer in 100-millisecond chunks
for (int i = 0; i < buffer.Length;
i += Constants.INCLUDED_SOUND_BYTES_PER_100_MILLISECONDS)
{
// Cast from short to int to prevent -32768 from overflowing Math.Abs
int currentVolume = Math.Abs((int)BitConverter.ToInt16(buffer, i));
// Add a keyframe to the top & bottom beak animations based on the
// current audio level. ANIMATION_ADJUSTMENT is a fudge factor that
// slightly speeds-up the animation to account for lag.
KeyTime keyTime = TimeSpan.FromSeconds(Math.Max(0,
(double)i / Constants.INCLUDED_SOUND_BYTES_PER_SECOND
– Constants.ANIMATION_ADJUSTMENT));
AddSpeakKeyFrame(currentVolume, keyTime);
}
// Add the final keyframe 100 ms later that smoothly closes the beak
KeyTime finalKeyTime = TimeSpan.FromSeconds(Math.Max(0, (double)
(buffer.Length + Constants.INCLUDED_SOUND_BYTES_PER_100_MILLISECONDS) /
Constants.INCLUDED_SOUND_BYTES_PER_SECOND));
AddFinalSpeakKeyFrame(finalKeyTime);
// The preceding work was computationally expensive, so it’s time for
// another update before attempting to play the sound
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
this.playingIncludedSound = true;
}
// Stop the storyboard and empty the keyframes in its two animations
void ResetSpeakStoryboard()
{
SpeakStoryboard.Stop();
TopBeakAnimation.KeyFrames.Clear();
BottomBeakAnimation.KeyFrames.Clear();
}
// Position the top and bottom beak based on the current volume.
// A louder volume results in a wider opening.
void AddSpeakKeyFrame(int currentVolume, KeyTime keyTime)
{
// The top beak rotation should always be an angle between 0 and -50
TopBeakAnimation.KeyFrames.Add(new DiscreteDoubleKeyFrame {
KeyTime = keyTime, Value = Math.Max(-50, currentVolume / -15) });
// The bottom beak rotation should always be an angle between 0 and 30
BottomBeakAnimation.KeyFrames.Add(new DiscreteDoubleKeyFrame {
KeyTime = keyTime, Value = Math.Min(30, currentVolume / 15) });
}
// Close the beak
void AddFinalSpeakKeyFrame(KeyTime keyTime)
{
// Use keyframes that do a smooth quintic ease from the previous values
TopBeakAnimation.KeyFrames.Add(new EasingDoubleKeyFrame {
EasingFunction = new QuinticEase(), KeyTime = keyTime, Value = 0 });
BottomBeakAnimation.KeyFrames.Add(new EasingDoubleKeyFrame {
EasingFunction = new QuinticEase(), KeyTime = keyTime, Value = 0 });
}
void Microphone_BufferReady(object sender, EventArgs e)
{
int size = Microphone.Default.GetData(this.buffer);
if (size == 0)
return;
// Unconditionally record the audio data by writing it to the stream
this.recordedStream.Write(this.buffer, 0, size);
int currentVolume = SoundEffects.GetAverageVolume(this.buffer, size);
if (currentVolume > Settings.VolumeThreshold.Value)
{
// The current volume is loud enough to be considered talking
this.consecutiveSilentSamples = 0;
if (this.playbackStartPosition == -1)
{
// Start a new phrase.
// Back up half a second if we’ve got the data, for a smoother result.
this.playbackStartPosition = Math.Max(0, this.recordedStream.Position
– Constants.MICROPHONE_BYTES_PER_100_MILLISECONDS * 5);
ResetSpeakStoryboard();
}
// Add a keyframe to the beak animations based on the current volume
// ANIMATION_ADJUSTMENT is a fudge factor that slightly speeds-up the
// animation to account for lag.
KeyTime keyTime = TimeSpan.FromSeconds(Math.Max(0,
Constants.SOUND_SPEED_FACTOR * (this.recordedStream.Position
– Constants.MICROPHONE_BYTES_PER_100_MILLISECONDS
– this.playbackStartPosition) / Constants.MICROPHONE_BYTES_PER_SECOND
– Constants.ANIMATION_ADJUSTMENT));
AddSpeakKeyFrame(currentVolume, keyTime);
}
else
{
// The current volume is NOT loud enough to be considered talking
this.consecutiveSilentSamples++; // 10 times == 1 second
// Check for the end of a spoken phrase. This happens when we’ve got a
// nonnegative playback start position followed by a second (10 samples)
// of silence.
if (this.playbackStartPosition != -1 &&
this.consecutiveSilentSamples == 10)
{
this.consecutiveSilentSamples = 0;
// Add the final keyframe that smoothly closes the beak
KeyTime keyTime = TimeSpan.FromSeconds(Math.Max(0,
Constants.SOUND_SPEED_FACTOR * (this.recordedStream.Position
– Constants.MICROPHONE_BYTES_PER_100_MILLISECONDS
– this.playbackStartPosition) / Constants.MICROPHONE_BYTES_PER_SECOND
– Constants.ANIMATION_ADJUSTMENT));
AddFinalSpeakKeyFrame(keyTime);
// Copy the appropriate slice of audio from the recorded stream into
// a buffer
byte[] buffer = new byte[this.recordedStream.Position –
this.playbackStartPosition];
this.recordedStream.Seek(this.playbackStartPosition, SeekOrigin.Begin);
this.recordedStream.Read(buffer, 0, buffer.Length);
// Amplify the recorded audio, as it tends to be softer than desired
if (Settings.VolumeMultiplier.Value > 1)
SoundEffects.AmplifyAudio(buffer, Settings.VolumeMultiplier.Value);
// Reset variables
this.playbackStartPosition = -1;
this.recordedStream.Position = 0;
// Create a new sound effect from the buffer and speak it
SoundEffect effect = new SoundEffect(buffer,
Microphone.Default.SampleRate, AudioChannels.Mono);
Speak(effect, Constants.SOUND_SPEED_FACTOR);
}
}
}
// Application bar handlers
void InstructionsButton_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(“/Shared/About/AboutPage.xaml?appName=Talking Parrot”,
UriKind.Relative));
}
}
}

[/code]

  • The constructor is nearly identical to the preceding chapter’s constructor, although it doesn’t start the microphone right away because the parrot speaks a greeting instead. We never use the microphone while the parrot is speaking because it might hear itself talk and begin an infinite pattern of repeating itself! Other than speaking the greeting, the other additional tasks are initializing the SoundEffects class, shown in Listing 35.3, initializing a stream for recording microphone audio, and starting the blinking animation that continuously moves the parrot’s eyelids for the duration of the app. The microphone is once again given the shortest possible buffer duration, so the app can remain as responsive as possible.
  • The handlers for screen taps (OnMouseLeftButtonDown and OnMouseLeftButtonUp) toggle the visibility of an “angry eyelid” to give the parrot an annoyed appearance when touched. This could be changed to handle taps specifically on the parrot’s body, but handling taps anywhere on the screen is simpler and likely good enough.
  • CompositionTarget_Rendering, besides calling Update, is responsible for taking action each time the parrot is done speaking. It determines when the parrot is done by comparing the current time to speakingDoneTime, a field set later in the code. If the parrot has just finished speaking audio recorded from the microphone, it makes the parrot speak either a whistle or a squawk (randomly chosen), which resets speakingDoneTime to the later time when the sound effect will finish. If the parrot has just finished speaking one of the included sounds (hello, whistle, or squawk), it restarts the microphone, so it can listen for something new to repeat. As with the preceding chapter, all this code continues to run when navigating forward to a different page. However, this is quite handy for this app because it enables you to aurally test the two settings in real-time as you adjust the sliders on the settings page.
  • The Speak method does the important work of stopping the microphone, setting speakingDoneTime, playing the passed-in sound effect at the passed-in speed, and animating the parrot to mouth the words, flutter its wing, raise its head, and stomp its feet. The magic of SpeakStoryboard mouthing the words in the current audio is enabled by manually updating its animation based on the raw audio data that will be played. This is done inside PrepareStoryboardForIncludedSound for the three included sounds and inside Microphone_BufferReady for the audio recorded from the microphone.
  • PrepareStoryboardForIncludedSound grabs a sample from the passed-in buffer at every 100 milliseconds (mimicking the behavior of the microphone event) and adds a keyframe to the top and bottom beak animations based on the volume of each sample. The louder the sound, the wider the beak needs to be open. The included sounds have a different bitrate than audio captured from the microphone, so the mapping of time to bytes in the buffer is handled by constants specific to these sounds. A final keyframe is added at the end to handle smoothly closing the beak. Because the three included sounds never change, the animations for each one could have been precalculated (or cached after the first time). However, this dynamic approach is done for simplicity and consistency with the code for sounds recorded from the microphone. It also means you can swap in different sound files, and things will work as expected.
  • AddSpeakKeyFrame adds each keyframe as a discrete keyframe (meaning no interpolation between each frame). This is reasonable considering the speed at which the frames advance. It manipulates the value of currentVolume to give it an appropriate range for the angle of rotation for each piece of the beak.
  • AddFinalSpeakKeyFrame gives the final keyframe a quintic (power of five) ease from the preceding value to zero, so the beak snaps shut smoothly.
  • Microphone_BufferReady uses the same approach as the preceding chapter to determine the volume of the last .1 seconds of audio. If the audio is loud enough and we haven’t started tracking the audio as a phrase to play back, we start paying attention to the audio by marking the starting position in the recorded stream and adding a keyframe to the beak animations (as done previously with the included sounds). We continue to listen to the audio (and add keyframes to the animations) until there has been a full second of silence, which equates to ten consecutive samples where the average volume was below the threshold.
  • Because human speech gradually ramps up to a volume above the threshold, simply starting playback at the point where the volume is loud enough would cut off the beginning of whatever was spoken and sound strange. Therefore, the starting position in the recorded stream is backed up half a second from the current point when set inside Microphone_BufferReady. This is why the microphone audio is always appended to the stream, regardless of volume.
  • When the end of relevant audio (a second of silence) has been detected inside Microphone_BufferReady, it copies all the data placed into the recorded stream from the chosen starting position onward into a new byte array. Although previous apps have obtained a sound effect from the static SoundEffect.FromStream method, this code—after potentially amplifying the audio—calls a constructor that enables passing in the raw audio data as a byte array. It then plays the dynamic sound effect (along with the appropriate animations) by calling Speak. It chooses a playback speed (pitch) 80% higher than normal (specified by the SOUND_SPEED_FACTOR constant) so it sounds more like a parrot speaking than the original person whose voice was recorded.
  • The default volume threshold used by Talking Parrot is lower than the one used by Bubble Blower. Here are the two settings used by this app, with their default values:[code]
    public static class Settings
    {
    public static readonly Setting<int> VolumeThreshold =
    new Setting<int>(“VolumeThreshold”, 500);
    public static readonly Setting<int> VolumeMultiplier =
    new Setting<int>(“VolumeMultiplier”, 4);
    }
    [/code]

Talking Parrot’s SoundEffects class is similar to the same-named class in previous chapters, but it also exposes the AmplifyAudio and GetAverageVolume methods used by Listing 35.2. Listing 35.3 contains the implementation.

LISTING 35.3 SoundEffects.cs—Exposes the Built-In Sound Effects and Audio Utility Methods

[code]

using System;
using System.IO;
using System.Windows.Resources;
using Microsoft.Xna.Framework.Audio;
namespace WindowsPhoneApp
{
public static class SoundEffects
{
public static SoundEffect Hello { get; private set; }
public static SoundEffect Squawk { get; private set; }
public static SoundEffect Whistle { get; private set; }
public static byte[] HelloBuffer { get; private set; }
public static byte[] SquawkBuffer { get; private set; }
public static byte[] WhistleBuffer { get; private set; }
public static void Initialize()
{
StreamResourceInfo info;
info = App.GetResourceStream(new Uri(“Audio/hello.wav”, UriKind.Relative));
HelloBuffer = GetBytes(info.Stream);
info.Stream.Position = 0;
Hello = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(
new Uri(“Audio/squawk.wav”, UriKind.Relative));
SquawkBuffer = GetBytes(info.Stream);
info.Stream.Position = 0;
Squawk = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(
new Uri(“Audio/whistle.wav”, UriKind.Relative));
WhistleBuffer = GetBytes(info.Stream);
info.Stream.Position = 0;
Whistle = SoundEffect.FromStream(info.Stream);
// Required for XNA Microphone API to work
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
}
static byte[] GetBytes(Stream stream)
{
byte[] bytes = new byte[stream.Length];
stream.Read(SquawkBuffer, 0, (int)stream.Length);
return bytes;
}
// Make the sound louder by modifying the raw audio samples
public static void AmplifyAudio(byte[] buffer, int multiplier)
{
// Buffer is an array of bytes, but we want to examine each 2-byte value
for (int i = 0; i < buffer.Length; i += 2)
{
int value = BitConverter.ToInt16(buffer, i);
if (value > Settings.VolumeThreshold.Value)
{
// Only amplify samples that are loud enough to not
// be considered background noise
value *= Settings.VolumeMultiplier.Value;
// Make sure the multiplied value stays within bounds
if (value > short.MaxValue)
value = short.MaxValue;
else if (value < short.MinValue)
value = short.MinValue;
// Replace the two bytes with the amplified value
byte[] newValue = BitConverter.GetBytes(value);
buffer[i] = newValue[0];
buffer[i + 1] = newValue[1];
}
}
}
// Returns the average value among the first numBytes in the buffer
public static int GetAverageVolume(byte[] buffer, int numBytes)
{
long total = 0;
// Buffer is an array of bytes, but we want to examine each 2-byte value
for (int i = 0; i < numBytes; i += 2)
{
// Cast from short to int to prevent -32768 from overflowing Math.Abs:
int value = Math.Abs((int)BitConverter.ToInt16(buffer, i));
total += value;
}
return (int)(total / (numBytes / 2));
}
}
}

[/code]

Unlike in past apps, the raw audio data for each sound file is copied into a byte array exposed as a property. Listing 35.2 used these byte arrays to determine the volume over time, just like what is done for audio from the microphone.

The audio captured from the microphone can often be much softer than desired.To combat this, the AmplifyAudio method in Listing 35.3 increases the volume of the recorded microphone audio by manually multiplying the value of each sample in the buffer (if the sample is louder than a threshold, to avoid amplifying background noise). Although the volume of the played-back audio is ultimately limited by the phone’s volume setting, this technique can make the audio surprisingly loud.Of course, the more that the audio is amplified, the more distorted it may sound.

You can play music from the music library while using this app to make the parrot “sing” the song.You just have to pause the music, so the parrot gets the second of silence needed to prompt it to speak! This can be controlled via the top bar that gets displayed while adjusting the phone’s volume, shown in Figure 35.2.

Music can be played and paused while using Talking Parrot.
FIGURE 35.2 Music can be played and paused while using Talking Parrot.

Here are the constants (and read-only fields) used by this app:

[code]

public static class Constants
{
// Screen
public const int SCREEN_WIDTH = 480;
public const int SCREEN_HEIGHT = 800;
public const float SOUND_SPEED_FACTOR = .8f;
public const float ANIMATION_ADJUSTMENT = .1f;
public static readonly long MICROPHONE_BYTES_PER_SECOND =
Microphone.Default.GetSampleSizeInBytes(TimeSpan.FromSeconds(1));
public static readonly long MICROPHONE_BYTES_PER_100_MILLISECONDS =
Constants.MICROPHONE_BYTES_PER_SECOND / 10;
public const int INCLUDED_SOUND_BYTES_PER_SECOND = 141100;
public const int INCLUDED_SOUND_BYTES_PER_100_MILLISECONDS = 14110;
}

[/code]

The Settings Page

The settings page, shown in Figure 35.3, is like the settings page from the Bubble Blower app, but with two sliders instead of one. The first slider adjusts the “parrot voice volume,” which maps to the VolumeMultiplier setting. The second slider is just like the one from Bubble Blower, which maps to the VolumeThreshold setting. It is labeled as “parrot hearing sensitivity” instead of “microphone sensitivity” to be more appropriate to the theme of this app.

The settings page for Talking Parrot enables changing and resetting VolumeMultiplier and VolumeThreshold.
FIGURE 35.3 The settings page for Talking Parrot enables changing and resetting VolumeMultiplier and VolumeThreshold.

The XAML for Figure 35.3 is shown in Listing 35.4. The differences from Bubble Blower’s settings page are emphasized.

LISTING 35.4 SettingsPage.xaml—The Settings User Interface for Talking Parrot

[code]

<phone:PhoneApplicationPage x:Name=”Page”
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=”talking parrot”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<!– Stacked contents inside a ScrollViewer,
for the benefit of landscape orientation: –>
<ScrollViewer Grid.Row=”1”>
<StackPanel Margin=”{StaticResource PhoneMargin}”>
<TextBlock Text=”Parrot voice volume”
Foreground=”{StaticResource PhoneSubtleBrush}”
Margin=”{StaticResource PhoneMargin}”/>
<Slider x:Name=”VolumeSlider” Minimum=”1” Maximum=”18”
Value=”{Binding Volume, Mode=TwoWay, ElementName=Page}”/>
<TextBlock Text=”Parrot hearing sensitivity”
Foreground=”{StaticResource PhoneSubtleBrush}”
Margin=”{StaticResource PhoneMargin}”/>
<Slider x:Name=”SensitivitySlider” Maximum=”1000” LargeChange=”100”
IsDirectionReversed=”True”
Value=”{Binding Threshold, Mode=TwoWay, ElementName=Page}”/>
<Button Content=”reset” Click=”ResetButton_Click”
local:Tilt.IsEnabled=”True”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • Because the page is now a little too tall for the landscape orientations, the StackPanel is wrapped inside a ScrollViewer.
  • The allowed range for VolumeMultiplier is 1–18.
  • Although SensitivitySlider needs to be reversed to map to the underlying threshold value, VolumeSlider does not.

The code-behind for the settings page is shown in Listing 35.5.

LISTING 35.5 SettingsPage.xaml.cs—The Settings Code-Behind for Talking Parrot

[code]

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

[/code]

The Finished Product

Talking Parrot (Recording & Playing)

Baby Milestones (Reading & Writing Pictures)

Baby Milestones informs parents about typical milestones in a baby’s development from birth to the age of 2. This app enables parents to keep track of developmental milestones and ensure that their baby is developing on schedule. It presents month-by-month lists of skills that most babies can accomplish at each age, and enables the parent to record the date that the baby demonstrated each skill. The main page of the app shows a dashboard with the current month-by-month progress.

A little bonus feature in this app happens to be the main reason that it is included in this part of the book. It demonstrates how to store a photo in isolated storage, and later retrieve and display it. Each month’s list in this app (from 1 to 24) supports specifying a custom image as the page’s background. The idea is that the parent can take a photo of their baby at the appropriate age to provide a bit of nostalgic context to each list.

The Main Page

The main page, shown in Figure 23.1, contains a list box that links to each of the 24 monthly lists. Each label in the list is accompanied with a progress bar that reveals the current progress in each month. Completed months are displayed in the phone’s foreground color, whereas incomplete months are displayed in the phone’s accent color.

FIGURE 23.1 The progress bars turn an otherwise-simple list box into a useful dashboard view.
FIGURE 23.1 The progress bars turn an otherwise-simple list box into a useful dashboard view.

Listing 23.1 contains the XAML for this main page, and Listing 23.2 contains the codebehind.

LISTING 23.1 MainPage.xaml—The User Interface for Baby Milestones’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”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”
x:Name=”Page” shell:SystemTray.IsVisible=”True”>
<phone:PhoneApplicationPage.Resources>
<!– A value converter for the binding of each item’s foreground –>
<local:PercentageToBrushConverter x:Key=”PercentageToBrushConverter”/>
</phone:PhoneApplicationPage.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”BABY MILESTONES”
Style=”{StaticResource PhoneTextTitle0Style}”/>
</StackPanel>
<!– The list box that fills the page –>
<ListBox x:Name=”ListBox” Grid.Row=”1” ItemsSource=”{Binding}”
Margin=”{StaticResource PhoneHorizontalMargin}”
SelectionChanged=”ListBox_SelectionChanged”>
<ListBox.ItemTemplate>
<!– The data template controls how each item renders –>
<DataTemplate>
<!– The explicit width is only here for the sake of the tilt effect –>
<StackPanel Orientation=”Horizontal” local:Tilt.IsEnabled=”True”
Width=”{Binding ActualWidth, ElementName=Page}”
Background=”Transparent”>
<ProgressBar Value=”{Binding PercentComplete}” Width=”120”/>
<!– The text block displays the Name property value, colored based
on the % complete –>
<TextBlock Text=”{Binding Name}” Margin=”{StaticResource PhoneMargin}”
Style=”{StaticResource PhoneTextExtraLargeStyle}”
Foreground=”{Binding PercentComplete, Converter=
{StaticResource PercentageToBrushConverter}}”/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 23.2 MainPage.xaml.cs—The Code-Behind for Baby Milestones’Main Page

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
this.Loaded += MainPage_Loaded;
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
if (this.ListBox.Items.Count > 0)
return; // We already added the data
// Fill the list box with the ages
this.DataContext = Settings.List.Value;
// Ensure that the most-recently-selected item is scrolled into view.
// Do this delayed to ensure the list box has been filled
this.Dispatcher.BeginInvoke(delegate()
{
if (this.ListBox.Items.Count > Settings.CurrentAgeIndex.Value)
this.ListBox.ScrollIntoView(
this.ListBox.Items[Settings.CurrentAgeIndex.Value]);
});
}
void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (this.ListBox.SelectedIndex >= 0)
{
Settings.CurrentAgeIndex.Value = this.ListBox.SelectedIndex;
// Navigate to the details page for this age
this.NavigationService.Navigate(new Uri(
“/DetailsPage.xaml”, UriKind.Relative));
// Clear the selection so the same item can be selected
// again on subsequent visits to this page
this.ListBox.SelectedIndex = -1;
}
}
}
}

[/code]

  • This app makes use of the following two settings defined in Settings.cs:

    [code]

    public static class Settings
    {
    public static readonly Setting<IList<Age>> List =
    new Setting<IList<Age>>(“List”, Data.Ages);
    public static readonly Setting<int> CurrentAgeIndex =
    new Setting<int>(“CurrentAgeIndex”, 0);
    }
    Data.Ages represents the list of 24 ages, each of which contains a list of skills:
    public class Data
    {
    public static readonly Age[] Ages = {
    new Age { Name = “1 month”,
    Skills = new Skill[] { new Skill(“lifts head”),
    new Skill(“stares at faces”), new Skill(“responds to sound”) }
    },

    };
    }

    [/code]
    The Age and Skill classes are defined in Listings 23.3 and 23.4.

  • In this page’s XAML, the progress bar inside the data template binds directly to each Age item’s PercentComplete property. To give each text block the appropriate foreground brush, however, a custom value converter is used. This app makes use of three value converters, all shown in the next section.
  • In the code-behind, the MainPage_Loaded method ensures that the most recently selected age is scrolled into view, because it would be annoying to constantly scroll the page down once the baby is older than 9 months. This is done via a BeginInvoke call, because attempting to scroll the list box immediately after setting the data context would not work. You need to let the binding complete before manipulating the list box in this fashion.
  • As with most uses of a list box in a Windows Phone app, the SelectionChanged event’s behavior (only raising when the selection changes rather than on each tap) is undesirable. Therefore, the ListBox_SelectionChanged handler clears the justselected item, so consecutive taps on the same item work as expected.

LISTING 23.3 Age.cs—The Age Class Used to Represent Each List Box Item

[code]

using System.Collections.Generic;
using System.ComponentModel;
namespace WindowsPhoneApp
{
public class Age : INotifyPropertyChanged
{
public string Name { get; set; }
public IList<Skill> Skills { get; set; }
public string PhotoFilename { get; set; }
// A readonly property that calculates completion on-the-fly
public double PercentComplete
{
get
{
int total = this.Skills.Count;
int numComplete = 0;
foreach (Skill s in this.Skills)
if (s.Date != null)
numComplete++;
return ((double)numComplete / total) * 100;
}
}
// Enables any consumer to trigger the
// property changed event for PercentComplete
public void RefreshPercentComplete()
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(“PercentComplete”));
}
public event PropertyChangedEventHandler PropertyChanged;
}
}

[/code]

LISTING 23.4 Skill.cs—The Skill Class Used by Each Age Instance

[code]

using System;
using System.ComponentModel;
namespace WindowsPhoneApp
{
public class Skill : INotifyPropertyChanged
{
// A default constructor (normally implicit) is required for serialization
public Skill()
{
}
public Skill(string name)
{
this.Name = name;
}
public string Name { get; set; }
public Age Age { get; set; }
// The only property that raises the PropertyChanged event
DateTime? date;
public DateTime? Date
{
get { return this.date; }
set
{
this.date = value;
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(“Date”));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}

[/code]

  • Both Age and Skill implement INotifyPropertyChanged and raise PropertyChanged events for their properties used as the source of data binding. This enables the display on the main page and the details page (shown next) to remain up-to-date without any manual work in those pages.
  • Because Age’s PercentComplete property is based on the value of each Date in its Skills list (null means incomplete, whereas any date means complete), raising a PropertyChanged event for PercentComplete at the appropriate time is tricky. The Age class could have subscribed to the PropertyChanged event on each of its Skill instances and raise one for PercentComplete whenever a date has been changed. Instead, this class simply requires its consumer to call RefreshPercentComplete whenever a relevant date has been changed.
  • Skill is given an explicit default constructor because this is required for it to be properly serialized to isolated storage. Normally the default constructor is implicitly generated by the C# compiler. However, when a nondefault constructor is defined, as in Listing 23.4, you must explicitly define a default constructor (if you want one).

Serialization and Isolated Storage Application Settings

Every object that gets placed in the IsolatedStorageSettings.ApplicationSettings dictionary (or assigned to an instance of the Settings class used throughout this book)— including the transitive closure of all of its members—must be serializable. As mentioned in the preceding chapter, the contents of this dictionary get serialized to XML inside a file called __ApplicationSettings. If any piece of data is not serializable, none of the dictionary’s contents get persisted. This failure can appear to happen silently, unless you happen to catch the unhandled exception in the debugger.

Most of the time, this requirement is satisfied without any extra work. None of the apps in this book (until now) had to take any special action to ensure that their settings were serializable, as all the basic data types (string, numeric values, DateTime, and so on), the generic List used with such basic data types, and classes with members of those types are all serializable.

Sometimes, however, you need to go out of your way to ensure that the data you persist is represented with serializable data types. This could be as simple as adding an explicit default constructor, as in Listing 23.4, or it could involve more work such as changing your data types or decorating them with custom attributes. The System.Runtime.Serialization namespace defines a DataContract attribute, along with attributes such as DataMember and IgnoreDataMember that enable you to customize how your classes get serialized. For example, if a class has a member than can’t be serialized (and doesn’t need to be serialized), you can mark it with the IgnoreDataMember attribute to exclude it.

Avoid persisting multiple references to the same object!

Although you can store more than one reference to the same object in the isolated storage application settings dictionary, the references will no longer point to the same instance the next time the app runs.That’s because when each reference is serialized, its data is persisted as an individual copy.On deserialization, each copy of the data becomes a distinct object instance. This is why Baby Milestones uses a CurrentAgeIndex setting rather than a setting that stores a reference to the relevant Age instance. After serialization and deserialization, the logic in Listing 23.2 to automatically scroll the list box would no longer do anything, because the Age instance would no longer be in the list box.

You can add custom logic to the serialization and deserialization process by marking a method with one of several custom attributes from the System.Runtime. Serialization namespace: OnSerializing, OnSerialized, OnDeserializing, and OnDeserialized. For your marked methods to be called at the appropriate times, they must be public (or internal with an appropriate InternalsVisibleTo attribute) and have a single StreamingContext parameter.

The Details Page

The details page, shown in Figure 23.2, appears when the user taps an age on the main page. This page displays the age-specific list of skills that can be tapped to record the date the skill was acquired. The tap brings up a date picker initialized to today’s date, as shown in Figure 23.3. Listing 23.5 contains this page’s XAML.

FIGURE 23.2 The details page, shown with the “1 month” list.
FIGURE 23.2 The details page, shown with the “1 month” list.
FIGURE 23.3 The details page after the first item is tapped.
FIGURE 23.3 The details page after the first item is tapped.

LISTING 23.5 DetailsPage.xaml—The User Interface for Baby Milestones’Details Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.DetailsPage”
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”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”
shell:SystemTray.IsVisible=”True”>
<phone:PhoneApplicationPage.Resources>
<!– Two value converters –>
<local:NullableObjectToVisibilityConverter
x:Key=”NullableObjectToVisibilityConverter”/>
<local:NullableObjectToBrushConverter
x:Key=”NullableObjectToBrushConverter”/>
</phone:PhoneApplicationPage.Resources>
<phone:PhoneApplicationPage.ApplicationBar>
<!– The application bar, with two buttons –>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”picture”
IconUri=”/Shared/Images/appbar.picture.png”
Click=”PictureButton_Click”/>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The optional background image –>
<Image x:Name=”BackgroundImage” Grid.RowSpan=”2” Opacity=”.5”
Stretch=”UniformToFill”/>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”BABY MILESTONES”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”{Binding Name}”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<!– The list box that fills the page –>
<ListBox x:Name=”ListBox” Grid.Row=”1” ItemsSource=”{Binding Skills}”
Margin=”{StaticResource PhoneHorizontalMargin}”
SelectionChanged=”ListBox_SelectionChanged”>
<ListBox.ItemTemplate>
<!– The data template controls how each item renders –>
<DataTemplate>
<!– The explicit width is only here for the sake of the tilt effect –>
<StackPanel local:Tilt.IsEnabled=”True”
Width=”{Binding ActualWidth, ElementName=Page}”>
<TextBlock Text=”{Binding Name}” TextWrapping=”Wrap”
Margin=”{StaticResource PhoneMargin}”
Style=”{StaticResource PhoneTextExtraLargeStyle}”
Foreground=”{Binding Date, Converter=
{StaticResource NullableObjectToBrushConverter}}”/>
<StackPanel Orientation=”Horizontal” Margin=”0,-12,0,0”
Visibility=”{Binding Date, Converter=
{StaticResource NullableObjectToVisibilityConverter}}”>
<toolkit:DatePicker Value=”{Binding Date, Mode=TwoWay}”/>
<Button Content=”clear” Click=”ClearButton_Click”/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • The visibility of each item’s date picker and the color of each item’s text block are based on the value of the Skill instance’s Date property. This is done with two value converters. These two classes, along with the value converter used by the main page, are shown in Listing 23.6.
  • The value for the date picker uses two-way data binding, which is useful for any property whose value can be manipulated by the user. Changes to the Skill’s Date property are not only automatically reflected in the date picker, but also changes that the user makes via the date picker user interface are automatically reflected back to the Date property.

LISTING 23.6 ValueConverters.cs—The Three Value Converters Used by Baby Milestones

[code]

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace WindowsPhoneApp
{
// Return Collapsed for null and Visible for nonnull
public class NullableObjectToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
if (value != null)
return Visibility.Visible;
else
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
return DependencyProperty.UnsetValue;
}
}
// Return the accent brush for null and the foreground brush for nonnull
public class NullableObjectToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
if (value != null)
return Application.Current.Resources[“PhoneForegroundBrush”];
else
return Application.Current.Resources[“PhoneAccentBrush”];
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
return DependencyProperty.UnsetValue;
}
}
// Return the accent brush for any value other than 100 and
// the foreground brush for 100
public class PercentageToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
if ((double)value == 100)
return Application.Current.Resources[“PhoneForegroundBrush”];
else
return Application.Current.Resources[“PhoneAccentBrush”];
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
return DependencyProperty.UnsetValue;
}
}
}

[/code]

Listing 23.7 contains the code-behind for the details page.

LISTING 23.7 DetailsPage.xaml.cs—The Code-Behind for Baby Milestones’Details Page

[code]

using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Tasks;
namespace WindowsPhoneApp
{
public partial class DetailsPage : PhoneApplicationPage
{
public DetailsPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
Age age = Settings.List.Value[Settings.CurrentAgeIndex.Value];
// Update the UI
this.DataContext = age;
if (age.PhotoFilename != null)
this.BackgroundImage.Source =
IsolatedStorageHelper.LoadFile(age.PhotoFilename);
}
void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (this.ListBox.SelectedIndex >= 0)
{
// Set the date to today
(this.ListBox.SelectedItem as Skill).Date = DateTime.Now;
// Trigger the property changed event that will refresh the UI
Settings.List.Value[
Settings.CurrentAgeIndex.Value].RefreshPercentComplete();
// Clear the selection so the same item can be selected
// multiple times in a row
this.ListBox.SelectedIndex = -1;
}
}
void ClearButton_Click(object sender, RoutedEventArgs e)
{
Skill skill = (sender as Button).DataContext as Skill;
if (MessageBox.Show(“Are you sure you want to clear the date for ”” +
skill.Name + “”?”, “Clear Date”, MessageBoxButton.OKCancel)
== MessageBoxResult.OK)
{
skill.Date = null;
Settings.List.Value[
Settings.CurrentAgeIndex.Value].RefreshPercentComplete();
}
}
// Application bar handlers
void PictureButton_Click(object sender, EventArgs e)
{
Microsoft.Phone.Tasks.PhotoChooserTask task = new PhotoChooserTask();
task.ShowCamera = true;
task.Completed += delegate(object s, PhotoResult args)
{
if (args.TaskResult == TaskResult.OK)
{
string filename = Guid.NewGuid().ToString();
IsolatedStorageHelper.SaveFile(filename, args.ChosenPhoto);
Age age = Settings.List.Value[Settings.CurrentAgeIndex.Value];
if (age.PhotoFilename != null)
IsolatedStorageHelper.DeleteFile(age.PhotoFilename);
age.PhotoFilename = filename;
// Seek back to the beginning of the stream
args.ChosenPhoto.Seek(0, SeekOrigin.Begin);
// Set the background image instantly from the stream
// Turn the stream into an ImageSource
this.BackgroundImage.Source = PictureDecoder.DecodeJpeg(
args.ChosenPhoto, (int)this.ActualWidth, (int)this.ActualHeight);
}
};
task.Show();
}
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
}
}

[/code]

This listing uses a custom IsolatedStorageHelper class to load, save, and delete image files. The images originate from the photo chooser, shown in Figure 23.4, which returns the selected photo as a stream.

FIGURE 23.4 The photo chooser supports choosing a picture from the media library or taking a new photo from the camera.
FIGURE 23.4 The photo chooser supports choosing a picture from the media library or taking a new photo from the camera.

IsolatedStorageHelper is implemented in Listing 23.8.

LISTING 23.8 IsolatedStorageHelper.cs—A Class That Stores and Retrieves Image Files to/from Isolated Storage

[code]

using System.Collections.Generic;
using System.IO;
using System.IO.IsolatedStorage;
using System.Windows.Media;
using Microsoft.Phone;
namespace WindowsPhoneApp
{
public static class IsolatedStorageHelper
{
static Dictionary<string, ImageSource> cache =
new Dictionary<string, ImageSource>();
public static void SaveFile(string filename, Stream data)
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream = userStore.CreateFile(filename))
{
// Get the bytes from the input stream
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);
}
}
public static ImageSource LoadFile(string filename)
{
if (cache.ContainsKey(filename))
return cache[filename];
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream =
userStore.OpenFile(filename, FileMode.Open))
{
// Turn the stream into an ImageSource
ImageSource source = PictureDecoder.DecodeJpeg(stream);
cache[filename] = source;
return source;
}
}
public static void DeleteFile(string filename)
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
userStore.DeleteFile(filename);
}
}
}

[/code]

  • The DeleteFile method is identical to the code to delete files in the preceding chapter, and SaveFile is not specific to images but rather generically saves the input stream’s bytes to a new file stream. The picture-specific part is in LoadFile, which calls PictureDecoder.DecodeJpeg (in the Microsoft.Phone namespace) to convert the stream into an ImageSource that can be set as the source to any Image or ImageBrush element.
  • The DecodeJpeg method is fairly slow and must be called on the UI thread, so this class caches each ImageSource it creates so it can be instantly returned the next time its filename is passed to LoadFile. (The same ImageSource instance can be shared by multiple UI elements, so there’s no danger in reusing one.)

The Finished Product

Baby Milestones (Reading & Writing Pictures)

Notepad (Reading & Writing Files)

The Notepad app enables fast, efficient note-taking. It boasts the following features:

  • Auto-save, which makes jotting down notes fast and easy
  • Quick previews of each note
  • The ability to customize each note’s background/foreground colors and text size
  • The ability to email your notes

Does this sound familiar? It should, because to a user this app behaves exactly like the preceding chapter’s Passwords & Secrets app, but without the master password and associated encryption. There is one important difference in its implementation, however, that makes it interesting for this chapter. Because the notes stored by Notepad are expected to be longer than the notes stored by Passwords & Secrets, each note is persisted as a separate file in isolated storage. This enables the app to load a note’s contents on-demand rather than loading everything each time the app launches/activates (which is what happens with application settings).

The Main Page

Notepad’s main page works just like the previous chapter’s main page, but without the LoginControl user control and associated logic. Listing 22.1 contains the XAML for the main page, with differences emphasized.

LISTING 22.1 MainPage.xaml—The User Interface for Notepad’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=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<phone:PhoneApplicationPage.Resources>
<local:DateConverter x:Key=”DateConverter”/>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with one button and one menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”new”
IconUri=”/Shared/Images/appbar.add.png” Click=”NewButton_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>
<!– The standard header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”NOTEPAD” Style=”{StaticResource PhoneTextTitle0Style}”/>
</StackPanel>
<!– Show this when there are no notes –>
<TextBlock Name=”NoItemsTextBlock” Grid.Row=”1” Text=”No notes”
Visibility=”Collapsed” Margin=”22,17,0,0”
Style=”{StaticResource PhoneTextGroupHeaderStyle}”/>
<!– The list box containing notes –>
<ListBox x:Name=”ListBox” Grid.Row=”1” ItemsSource=”{Binding}”
SelectionChanged=”ListBox_SelectionChanged”>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<!– The title, in a style matching the note –>
<Border Background=”{Binding ScreenBrush}” Margin=”24,0” Width=”800”
MinHeight=”60” local:Tilt.IsEnabled=”True”>
<TextBlock Text=”{Binding Title}” FontSize=”{Binding TextSize}”
Foreground=”{Binding TextBrush}” Margin=”12”
VerticalAlignment=”Center”/>
</Border>
<!– The modified date –>
<TextBlock Foreground=”{StaticResource PhoneSubtleBrush}”
Text=”{Binding Modified, Converter={StaticResource DateConverter}}”
Margin=”24,0,0,12”/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</phone:PhoneApplicationPage>

[/code]

The application bar now has the “new” button from the start because there is no mode in which adding new notes is forbidden.

The code-behind for the main page is shown in Listing 22.2.

LISTING 22.2 MainPage.xaml.cs—The Code-Behind for Notepad’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Bind the notes list as the data source for the list box
if (this.DataContext == null)
this.DataContext = Settings.NotesList.Value;
// Clear the selection so selecting the same item twice in a row will
// still raise the SelectionChanged event
Settings.CurrentNoteIndex.Value = -1;
this.ListBox.SelectedIndex = -1;
if (Settings.NotesList.Value.Count == 0)
NoItemsTextBlock.Visibility = Visibility.Visible;
else
NoItemsTextBlock.Visibility = Visibility.Collapsed;
}
void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ListBox.SelectedIndex >= 0)
{
// Navigate to the details page for the selected item
Settings.CurrentNoteIndex.Value = ListBox.SelectedIndex;
this.NavigationService.Navigate(new Uri(“/DetailsPage.xaml”,
UriKind.Relative));
}
}
// Application bar handlers
void NewButton_Click(object sender, EventArgs e)
{
// Create a new note and add it to the top of the list
Note note = new Note();
note.Filename = Guid.NewGuid().ToString();
note.Modified = DateTimeOffset.Now;
note.ScreenColor = Settings.ScreenColor.Value;
note.TextColor = Settings.TextColor.Value;
note.TextSize = Settings.TextSize.Value;
Settings.NotesList.Value.Insert(0, note);
// “Select” the new note
Settings.CurrentNoteIndex.Value = 0;
// Navigate to the details page for the newly created note
this.NavigationService.Navigate(new Uri(“/DetailsPage.xaml”,
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Notepad”, UriKind.Relative));
}
}
}

[/code]

  • The list box is now filled immediately inside OnNavigatedTo by setting the page’s data context to the list of notes.
  • The Note class, shown at the end of this chapter, is slightly different from the preceding chapter in order to accommodate its file-based storage. Inside NewButton_Click, you can see that it now has a Filename property that points to the file containing its contents. The filename is never shown in this app’s user interface; internally, each note just needs to know where to fetch its content. Therefore, when a new note is created, it is given a unique filename thanks to the Guid.NewGuid method. This returns a Globally Unique Identifier (GUID) that is unique for all practical purposes.

The Details Page

The details page, just like in Passwords & Secrets, displays the entire contents of the note and enables the user to edit it, delete it, change its settings, or email its contents. The page’s XAML is identical to DetailsPage.xaml in the preceding chapter, except the application bar is not marked with IsVisible=”False” because it doesn’t need to be hidden while the LoginControl is shown. Listing 22.3 contains the code-behind for this page with differences emphasized.

LISTING 22.3 DetailsPage.xaml.cs—The Code-Behind for Passwords & Secrets’Details Page

[code]

using System;
using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Tasks;
namespace WindowsPhoneApp
{
public partial class DetailsPage : PhoneApplicationPage
{
bool navigatingFrom;
string initialText = “”;
public DetailsPage()
{
InitializeComponent();
this.Loaded += DetailsPage_Loaded;
}
void DetailsPage_Loaded(object sender, RoutedEventArgs e)
{
// Automatically show the keyboard for new notes.
// This also gets called when navigating away, hence the extra check
// to make sure we’re only doing this when navigating to the page
if (this.TextBox.Text.Length == 0 && !this.navigatingFrom)
this.TextBox.Focus();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
this.navigatingFrom = true;
base.OnNavigatedFrom(e);
if (this.initialText != this.TextBox.Text)
{
// Automatically save the new content
Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
n.SaveContent(this.TextBox.Text);
// Update the title now, so each one can be accessed
// later without reading the file’s contents
string title = this.TextBox.Text.TrimStart();
// Don’t include more than the first 100 characters, which should be long
// enough, even in landscape with a small font
if (title.Length > 100)
title = title.Substring(0, 100);
// Fold the remaining content into a single line. We can’t use
// Environment.NewLine because it’s rn, whereas newlines inserted from
// a text box are just r
n.Title = title.Replace(‘r’, ‘ ‘);
n.Modified = DateTimeOffset.Now;
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Show the note’s contents
Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
if (n != null)
{
this.initialText = this.TextBox.Text = n.GetContent();
this.TextBox.Background = n.ScreenBrush;
this.TextBox.Foreground = n.TextBrush;
this.TextBox.FontSize = n.TextSize;
}
}
void TextBox_GotFocus(object sender, RoutedEventArgs e)
{
this.ApplicationBar.IsVisible = false;
}
void TextBox_LostFocus(object sender, RoutedEventArgs e)
{
this.ApplicationBar.IsVisible = true;
}
// Application bar handlers:
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to delete this note?”,
“Delete note?”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
n.DeleteContent();
Settings.NotesList.Value.Remove(n);
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
void EmailButton_Click(object sender, EventArgs e)
{
EmailComposeTask launcher = new EmailComposeTask();
launcher.Body = this.TextBox.Text;
launcher.Subject = “Note”;
launcher.Show();
}
void SettingsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
}
}

[/code]

  • In OnNavigatedFrom, the content of the text box is saved to a file via the SaveContent method shown later in this chapter. The Title property for each note is set at this point rather than dynamically when the property is accessed because it enables each title to be shown without reading each file. Otherwise, rendering the list on the main page would end up reading the contents of every file and take away the advantage of storing the notes in files!
  • The note’s contents are immediately shown inside OnNavigatedTo with the help of a GetContent method defined on Note.
  • Inside DeleteButton_Click, the note’s DeleteContent method ensures that the backing file doesn’t get left behind when a note is deleted.

The Note Class

Listing 22.4 shows the implementation of the modified Note class used by this app, with differences from the preceding chapter emphasized.

LISTING 22.4 Note.cs—The Code-Behind for Passwords & Secrets’Details Page

[code]

using System;
using System.ComponentModel;
using System.IO;
using System.IO.IsolatedStorage;
using System.Windows.Media;
namespace WindowsPhoneApp
{
public class Note : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
// A helper method used by the properties
void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
DateTimeOffset modified;
public DateTimeOffset Modified
{
get { return this.modified; }
set { this.modified = value; OnPropertyChanged(“Modified”); }
}
int textSize;
public int TextSize
{
get { return this.textSize; }
set { this.textSize = value; OnPropertyChanged(“TextSize”); }
}
Color screenColor;
public Color ScreenColor
{
get { return this.screenColor; }
set { this.screenColor = value;
OnPropertyChanged(“ScreenColor”); OnPropertyChanged(“ScreenBrush”); }
}
Color textColor;
public Color TextColor
{
get { return this.textColor; }
set { this.textColor = value;
OnPropertyChanged(“TextColor”); OnPropertyChanged(“TextBrush”); }
}
// Three readonly properties whose value is computed from other properties:
public Brush ScreenBrush
{
get { return new SolidColorBrush(this.ScreenColor); }
}
public Brush TextBrush
{
get { return new SolidColorBrush(this.TextColor); }
}
string title;
public string Title
{
get { return this.title; }
set { this.title = value; OnPropertyChanged(“Title”); }
}
public string Filename { get; set; }
public void SaveContent(string content)
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream =
userStore.CreateFile(this.Filename))
using (StreamWriter writer = new StreamWriter(stream))
{
writer.Write(content);
}
}
public string GetContent()
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
{
if (!userStore.FileExists(this.Filename))
return “”;
else
{
using (IsolatedStorageFileStream stream =
userStore.OpenFile(this.Filename, FileMode.Open))
using (StreamReader reader = new StreamReader(stream))
return reader.ReadToEnd();
}
}
}
public void DeleteContent()
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
userStore.DeleteFile(this.Filename);
}
}
}

[/code]

  • As implied earlier, the Title property is now a normal read-write property rather than a read-only property whose value is determined dynamically.
  • To save a new file, SaveContent first calls IsolatedStorageFile. GetUserStoreForApplication. This is the first step in any code that interacts directly with the isolated storage file system. The IsolatedStoreFile instance returned contains several methods for creating, enumerating, opening, and deleting files and folders. Once CreateFile is called, SaveContent uses a StreamWriter to easily write the passed-in string to the stream.
  • GetContent and DeleteContent work similarly to SaveContent, making use of three more methods on IsolatedStorageFile: FileExists, OpenFile, and DeleteFile. To keep the UI responsive while interacting with large files, this would be a good place to use BackgroundWorker.

When managing files, it’s tempting to simply use the IsolatedStorageFile. GetFileNames method to enumerate and perhaps display the files.This approach has problems, however. For example:

  • The isolated storage APIs don’t expose any way to discover the created/modified dates of files.Therefore, sorting files by such properties rather than the default alphabetical order requires you to store extra information (stored in the Note class in this app).
  • The list includes an extra file if you use any isolated storage application settings.These get persisted in an XML file called _ _ApplicationSettings in the root of your app’s isolated storage folder. Although you could manually filter this out, there’s no guarantee that there won’t be other special files in the future.
  • As in Windows, filenames have restrictions on their characters (such as no colons or question marks). If you use filenames as user-visible and potentially editable labels, you need to make sure you don’t introduce invalid characters.

The Settings Page

The settings page, shown in Figure 22.1, enables the customization of any note’s foreground color, background color, and text size. Although these settings are only applied to the current note (stored as properties on the Note instance), the user can check a check box to automatically apply the chosen settings to any new notes created in the future. Listing 22.5 contains the XAML for this page, and Listing 22.6 contains the code-behind.

FIGURE 22.1 The settings page exposes per-note settings and enables you to apply them to all future notes.
FIGURE 22.1 The settings page exposes per-note settings and enables you to apply them to all future notes.

LISTING 22.5 SettingsPage.xaml—The User Interface for Notepad’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>
<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=”notepad” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<Grid Margin=”{StaticResource PhoneMargin}”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CheckBox x:Name=”MakeDefaultCheckBox” Grid.ColumnSpan=”2”
Content=”Make these the default settings” Margin=”0,-4,0,0”
Checked=”MakeDefaultCheckBox_IsCheckedChanged”
Unchecked=”MakeDefaultCheckBox_IsCheckedChanged”
local:Tilt.IsEnabled=”True”/>
<!– The two colors –>
<TextBlock Grid.Row=”1” Text=”Screen color”
Foreground=”{StaticResource PhoneSubtleBrush}” Margin=”12,8”/>
<Rectangle Grid.Row=”2” x:Name=”ScreenColorRectangle”
Margin=”{StaticResource PhoneHorizontalMargin}” Height=”90”
Stroke=”{StaticResource PhoneForegroundBrush}”
StrokeThickness=”3” local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”ScreenColorRectangle_MouseLeftButtonUp”/>
<TextBlock Grid.Row=”1” Grid.Column=”1” Text=”Text color”
Foreground=”{StaticResource PhoneSubtleBrush}” Margin=”12,8”/>
<Rectangle Grid.Row=”2” Grid.Column=”1” x:Name=”TextColorRectangle”
Height=”90” StrokeThickness=”3” local:Tilt.IsEnabled=”True”
Margin=”{StaticResource PhoneHorizontalMargin}”
Stroke=”{StaticResource PhoneForegroundBrush}”
MouseLeftButtonUp=”TextColorRectangle_MouseLeftButtonUp”/>
<!– Text size –>
<TextBlock Grid.Row=”3” Grid.ColumnSpan=”2” Text=”Text size”
Foreground=”{StaticResource PhoneSubtleBrush}”
Margin=”12,20,12,-14”/>
<Grid Grid.Row=”4” Grid.ColumnSpan=”2”>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width=”Auto”/>
</Grid.ColumnDefinitions>
<Slider x:Name=”TextSizeSlider” Minimum=”12” Maximum=”100”
ValueChanged=”TextSizeSlider_ValueChanged”/>
<Button x:Name=”ResetButton” Grid.Column=”1” Content=”reset”
VerticalAlignment=”Top” Click=”ResetButton_Click”
local:Tilt.IsEnabled=”True”/>
</Grid>
<!– Sample text –>
<Rectangle x:Name=”SampleBackground” Grid.Row=”5” Grid.ColumnSpan=”2”
Margin=”-12,0,-12,-12”/>
<TextBlock x:Name=”SampleTextBlock” Grid.Row=”5” Grid.ColumnSpan=”2”
Text=”Sample text.” Padding=”12”/>
</Grid>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 22.6 SettingsPage.xaml.cs—The Code-Behind for Notepad’s Settings Page

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
public SettingsPage()
{
InitializeComponent();
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
// Doing this here instead of OnNavigatedFrom, so it’s not
// applied when navigating forward to color picker pages
if (Settings.MakeDefault.Value)
{
// Apply everything as defaults, too
Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
Settings.ScreenColor.Value = n.ScreenColor;
Settings.TextColor.Value = n.TextColor;
Settings.TextSize.Value = n.TextSize;
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
// Apply any color just selected from the color picker
if (Settings.TempScreenColor.Value != null)
{
n.ScreenColor = Settings.TempScreenColor.Value.Value;
Settings.TempScreenColor.Value = null;
}
if (Settings.TempTextColor.Value != null)
{
n.TextColor = Settings.TempTextColor.Value.Value;
Settings.TempTextColor.Value = null;
}
// Respect the saved settings
this.MakeDefaultCheckBox.IsChecked = Settings.MakeDefault.Value;
this.ScreenColorRectangle.Fill = new SolidColorBrush(n.ScreenColor);
this.TextColorRectangle.Fill = new SolidColorBrush(n.TextColor);
this.SampleBackground.Fill = this.ScreenColorRectangle.Fill;
this.SampleTextBlock.Foreground = this.TextColorRectangle.Fill;
this.TextSizeSlider.Value = n.TextSize;
}
void ScreenColorRectangle_MouseLeftButtonUp(object sender,
MouseButtonEventArgs e)
{
// Get a string representation of the colors we need to pass to the color
// picker, without the leading #
string currentColorString = Settings.NotesList.Value
[Settings.CurrentNoteIndex.Value].ScreenColor.ToString().Substring(1);
string defaultColorString =
Settings.ScreenColor.Value.ToString().Substring(1);
// The color picker works with the same isolated storage value that the
// Setting works with, but we have to clear its cached value to pick up
// the value chosen in the color picker
Settings.TempScreenColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=TempScreenColor”, UriKind.Relative));
}
void TextColorRectangle_MouseLeftButtonUp(object sender,
MouseButtonEventArgs e)
{
// Get a string representation of the colors, without the leading #
string currentColorString = Settings.NotesList.Value
[Settings.CurrentNoteIndex.Value].TextColor.ToString().Substring(1);
string defaultColorString =
Settings.TextColor.Value.ToString().Substring(1);
// The color picker works with the same isolated storage value that the
// Setting works with, but we have to clear its cached value to pick up
// the value chosen in the color picker
Settings.TempTextColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “showOpacity=false”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=TempTextColor”, UriKind.Relative));
}
void TextSizeSlider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
// Gets called during InitializeComponent
if (this.TextSizeSlider != null)
{
int textSize = (int)Math.Round(this.TextSizeSlider.Value);
Settings.NotesList.Value[Settings.CurrentNoteIndex.Value].TextSize =
textSize;
this.SampleTextBlock.FontSize = textSize;
}
}
void MakeDefaultCheckBox_IsCheckedChanged(object sender, RoutedEventArgs e)
{
Settings.MakeDefault.Value = this.MakeDefaultCheckBox.IsChecked.Value;
}
void ResetButton_Click(object sender, RoutedEventArgs e)
{
int textSize = Settings.TextSize.DefaultValue;
this.TextSizeSlider.Value = textSize;
Settings.NotesList.Value[Settings.CurrentNoteIndex.Value].TextSize =
textSize;
this.SampleTextBlock.FontSize = textSize;
}
}
}

[/code]

To work with the color picker that writes directly to a key in the isolated storage application settings, TempScreenColor and TempTextColor settings are used. These values are then applied to the current note’s properties inside OnNavigatedTo.

The Finished Product

Notepad (Reading & Writing Files)

Passwords & Secrets (Encryption & Observable Collections)

Passwords & Secrets is a notepad-style app that you can protect with a master password. Therefore, it’s a great app for storing a variety of passwords and other secrets that you don’t want getting into the wrong hands. The note-taking functionality is top-notch, supporting

  • Auto-save, which makes jotting down notes fast and easy
  • Quick previews of each note
  • The ability to customize each note’s background/foreground colors and text size
  • The ability to email your notes

On top of this, the data in each note is encrypted with 256-bit Advanced Encryption Standard (AES) encryption to keep prying eyes from discovering the data. This encryption is done based on the master password, so it’s important that the user never forgets their password! There is no way for the app to retrieve the data without it, as the app does not store the password for security reasons.

To make management of the master password as easy as possible, Passwords & Secrets supports specifying and showing a password hint. It also enables you to change your password (but only if you know the current password).

Why would I need to encrypt data stored in isolated storage? Isn’t my app the only thing that can access it?

Barring any bugs in the Windows Phone OS, another app should never be able to read your app’s isolated storage. And nobody should be able to remotely peer into your isolated storage. But if skilled hackers get physical access to your phone, they could certainly read the data stored on it. Encryption makes it virtually impossible for hackers to make any sense of the stored data.

Basic Cryptography

Silverlight’s System.Security.Cryptography namespace contains quite a bit of functionality for cryptographic tasks. This app wraps the necessary pieces of functionality from this namespace in order to expose an easy-to-use Crypto class. This class exposes two simple methods—Encrypt and Decrypt—that accept the decrypted/encrypted data along with a password to use as the basis for the encryption and decryption. Listing 21.1 contains the implementation.

LISTING 21.1 Crypto.cs—The Crypto Class That Exposes Simple Encrypt and Decrypt Methods

[code]

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace WindowsPhoneApp
{
public static class Crypto
{
public static string Encrypt(string data, string password)
{
if (data == null)
return null;
using (SymmetricAlgorithm algorithm = GetAlgorithm(password))
using (MemoryStream memoryStream = new MemoryStream())
using (CryptoStream cryptoStream = new CryptoStream(
memoryStream, algorithm.CreateEncryptor(), CryptoStreamMode.Write))
{
// Convert the original data to bytes then write them to the CryptoStream
byte[] buffer = Encoding.UTF8.GetBytes(data);
cryptoStream.Write(buffer, 0, buffer.Length);
cryptoStream.FlushFinalBlock();
// Convert the encrypted bytes back into a string
return Convert.ToBase64String(memoryStream.ToArray());
}
}
public static string Decrypt(string data, string password)
{
if (data == null)
return null;
using (SymmetricAlgorithm algorithm = GetAlgorithm(password))
using (MemoryStream memoryStream = new MemoryStream())
using (CryptoStream cryptoStream = new CryptoStream(
memoryStream, algorithm.CreateDecryptor(), CryptoStreamMode.Write))
{
// Convert the encrypted string to bytes then write them
// to the CryptoStream
byte[] buffer = Convert.FromBase64String(data);
cryptoStream.Write(buffer, 0, buffer.Length);
cryptoStream.FlushFinalBlock();
// Convert the original data back to a string
buffer = memoryStream.ToArray();
return Encoding.UTF8.GetString(buffer, 0, buffer.Length);
}
}
// Hash the input data with a salt, typically used for storing a password
public static string Hash(string data)
{
// Convert the data to bytes
byte[] dataBytes = Encoding.UTF8.GetBytes(data);
// Create a new array with the salt bytes followed by the data bytes
byte[] allBytes = new byte[Settings.Salt.Value.Length + dataBytes.Length];
// Copy the salt at the beginning
Settings.Salt.Value.CopyTo(allBytes, 0);
// Copy the data after the salt
dataBytes.CopyTo(allBytes, Settings.Salt.Value.Length);
// Compute the hash for the combined set of bytes
byte[] hash = new SHA256Managed().ComputeHash(allBytes);
// Convert the bytes into a string
return Convert.ToBase64String(hash);
}
public static byte[] GenerateNewSalt(int length)
{
Byte[] bytes = new Byte[length];
// Fill the array with random bytes, using a cryptographic
// random number generator (RNG)
new RNGCryptoServiceProvider().GetBytes(bytes);
return bytes;
}
static SymmetricAlgorithm GetAlgorithm(string password)
{
// Use the Advanced Encryption Standard (AES) algorithm
AesManaged algorithm = new AesManaged();
// Derive an encryption key from the password
Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password,
Settings.Salt.Value);
// Initialize, converting the two values in bits to bytes (dividing by 8)
algorithm.Key = bytes.GetBytes(algorithm.KeySize / 8);
algorithm.IV = bytes.GetBytes(algorithm.BlockSize / 8);
return algorithm;
}
}
}

[/code]

  • Both Encrypt and Decrypt call a GetAlgorithm helper method (defined at the end of the file) to get started. The returned algorithm can create an encryptor or a decryptor, which is passed to a crypto stream that is used to drive the encryption/decryption work.
  • In Encrypt, the input string is converted to bytes based on a UTF8 encoding. These bytes can then be written to the crypto stream to perform the encryption. The encrypted bytes are retrieved by using the ToArray method on the underlying memory stream used by the crypto stream. These bytes are converted back to a stream using Base64 encoding, which is a common approach for representing binary data in a string.
  • Decrypt starts with the Base64-encoded string and converts it to bytes to be written to the crypto stream. It then uses the underlying memory stream’s ToArray method to convert the decrypted UTF8 bytes back into a string.
  • The Hash function computes a SHA256 (Secure Hash Algorithm with a 256-bit digest) cryptographic hash of the input string prepended with a random “salt.” This is sometimes called a salted hash. This app calls this method in order to store a salted hash of the password rather than the password itself, for extra security. After all, if a hacker got a hold of the data in isolated storage, the encryption would be pointless if the password were stored along with it in plain text!
  • GenerateNewSalt simply produces a random byte array of the desired length. Rather than using the simple Random class used in other apps, this method uses RNGCryptoServiceProvider, a higher-quality pseudo-random number generator that is more appropriate to use in cryptographic applications. As shown in the next section, this app calls this method only once, and only the first time the app is run. It stores the randomly generated salt in isolated storage and then uses that for all future encryption, decryption, and hashing.
  • GetAlgorithm constructs the only built-in encryption algorithm, AesManaged, which is the AES symmetric algorithm. The algorithm needs to be initialized with a secret key and an initialization vector (IV), so this is handled by the Rfc2898DeriveBytes instance.
  • Rfc2898DeriveBytes is an implementation of a password-based key derivation function—PBKDF2. This uses the password and a random “salt” value, and applies a pseudorandom function based on a SHA1 hash function many times (1000 by default). All this makes the password much harder to crack.
  • The default value of AesManaged’s KeySize property is also its maximum supported value: 256. This means that the key is 256-bits long, which is why this process is called 256-bit encryption.

Salt in Cryptography

Using salt can provide a number of benefits for slowing down hackers, especially when the salt can be kept a secret. In this app, although a salt must be passed to the constructor of Rfc2898DeriveBytes, it doesn’t really add value because the salt must be stored along with the encrypted data.The same goes for the salting of the hash inside the Hash function. Although this is good practice for a server managing multiple passwords (so dictionary-based attacks must be regenerated for each user, and so users with the same password won’t have the same hash), it is done in this app mainly for show.

The LoginControl User Control

With the Crypto class in place, we can create a login control that handles all the user interaction needed for the app’s master password. The LoginControl user control used by this app is shown in Figure 21.1. It has three different modes:

  • The new user mode, in which the user must choose their master password for the first time
  • The normal login mode, in which the user must enter their previously chosen password
  • The change password mode, in which the user can change their password (after entering their existing password)
FIGURE 21.1 The three modes of the LoginControl user control in action.
FIGURE 21.1 The three modes of the LoginControl user control in action.

Listing 21.2 contains the XAML for this control.

LISTING 21.2 LoginControl.xaml—The User Interface for the LoginControl User Control

[code]

<UserControl x:Class=”WindowsPhoneApp.LoginControl”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”>
<Grid Background=”{StaticResource PhoneBackgroundBrush}”>
<!– A dim accent-colored padlock image –>
<Rectangle Fill=”{StaticResource PhoneAccentBrush}” Width=”300” Height=”364”
VerticalAlignment=”Bottom” HorizontalAlignment=”Right”
Margin=”{StaticResource PhoneMargin}” Opacity=”.5”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/lock.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<ScrollViewer>
<Grid>
<!– This panel is used for both New User and Change Password modes –>
<StackPanel x:Name=”ChangePasswordPanel” Visibility=”Collapsed”
Margin=”{StaticResource PhoneMargin}”>
<!– Welcome! –>
<TextBlock x:Name=”WelcomeTextBlock” Visibility=”Collapsed”
Margin=”{StaticResource PhoneHorizontalMargin}” TextWrapping=”Wrap”>
<Run FontWeight=”Bold”>Welcome!</Run>
<LineBreak/>
Choose a password that you’ll remember. There is no way to recover …
</TextBlock>
<!– Old password –>
<TextBlock Text=”Old password” x:Name=”OldPasswordLabel”
Style=”{StaticResource LabelStyle}”/>
<PasswordBox x:Name=”OldPasswordBox” KeyUp=”PasswordBox_KeyUp”/>
<!– New password –>
<TextBlock Text=”New password” Style=”{StaticResource LabelStyle}”/>
<PasswordBox x:Name=”NewPasswordBox” KeyUp=”PasswordBox_KeyUp”/>
<!– Confirm new password –>
<TextBlock Text=”Type new password again”
Style=”{StaticResource LabelStyle}”/>
<PasswordBox x:Name=”ConfirmNewPasswordBox” KeyUp=”PasswordBox_KeyUp”/>
<!– Password hint –>
<TextBlock Text=”Password hint (optional)”
Style=”{StaticResource LabelStyle}”/>
<TextBox x:Name=”PasswordHintTextBox” InputScope=”Text”
KeyUp=”PasswordBox_KeyUp”/>
<Button Content=”ok” Click=”OkButton_Click” MinWidth=”226”
HorizontalAlignment=”Left” Margin=”0,12,0,0”
local:Tilt.IsEnabled=”True”/>
</StackPanel>
<!– This panel is used only for the Normal Login mode –>
<StackPanel x:Name=”NormalLoginPanel” Visibility=”Collapsed”
Margin=”{StaticResource PhoneMargin}”>
<TextBlock Text=”Enter your password”
Style=”{StaticResource LabelStyle}”/>
<PasswordBox x:Name=”NormalLoginPasswordBox” KeyUp=”PasswordBox_KeyUp”/>
<Button Content=”ok” Click=”OkButton_Click” MinWidth=”226”
HorizontalAlignment=”Left” local:Tilt.IsEnabled=”True”/>
</StackPanel>
</Grid>
</ScrollViewer>
</Grid>
</UserControl>

[/code]

  • This control uses a password box wherever a password should be entered. A password box is just like a text box, except that it displays each character as a circle (after a brief moment in which you see the letter you just typed). This matches the behavior of password entry in all the built-in apps. Instead of a Text property, it has a Password property.

Listing 21.3 contains the code-behind.

LISTING 21.3 LoginControl.xaml.cs—The Code-Behind for the LoginControl User Control

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace WindowsPhoneApp
{
public partial class LoginControl : UserControl
{
// A custom event
public event EventHandler Closed;
public LoginControl()
{
InitializeComponent();
// Update the UI depending on which of the three modes we’re in
if (Settings.HashedPassword.Value == null)
{
// The “new user” mode
this.WelcomeTextBlock.Visibility = Visibility.Visible;
this.OldPasswordLabel.Visibility = Visibility.Collapsed;
this.OldPasswordBox.Visibility = Visibility.Collapsed;
this.ChangePasswordPanel.Visibility = Visibility.Visible;
}
else if (CurrentContext.IsLoggedIn)
{
// The “change password” mode
this.ChangePasswordPanel.Visibility = Visibility.Visible;
}
else
{
// The “normal login” mode
this.NormalLoginPanel.Visibility = Visibility.Visible;
}
}
void OkButton_Click(object sender, RoutedEventArgs e)
{
string currentHashedPassword = Settings.HashedPassword.Value;
if (currentHashedPassword != null && !CurrentContext.IsLoggedIn)
{
// We’re in “normal login” mode
// If the hash of the attempted password matches the stored hash,
// then we know the user entered the correct password.
if (Crypto.Hash(this.NormalLoginPasswordBox.Password)
!= currentHashedPassword)
{
MessageBox.Show(“”, “Incorrect password”, MessageBoxButton.OK);
return;
}
// Keep the unencrypted password in-memory,
// only until this app is deactivated/closed
CurrentContext.Password = this.NormalLoginPasswordBox.Password;
}
else
{
// We’re in “new user” or “change password” mode
// For “change password,” be sure that the old password is correct
if (CurrentContext.IsLoggedIn && Crypto.Hash(this.OldPasswordBox.Password)
!= currentHashedPassword)
{
MessageBox.Show(“”, “Incorrect old password”, MessageBoxButton.OK);
return;
}
// Now validate the new password
if (this.NewPasswordBox.Password != this.ConfirmNewPasswordBox.Password)
{
MessageBox.Show(“The two passwords don’t match. Please try again.”,
“Oops!”, MessageBoxButton.OK);
return;
}
string newPassword = this.NewPasswordBox.Password;
if (newPassword == null || newPassword.Length == 0)
{
MessageBox.Show(“The password cannot be empty. Please try again.”,
“Nice try!”, MessageBoxButton.OK);
return;
}
// Store a hash of the password so we can check for the correct
// password in future logins without storing the actual password
Settings.HashedPassword.Value = Crypto.Hash(newPassword);
// Store the password hint as plain text
Settings.PasswordHint.Value = this.PasswordHintTextBox.Text;
// Keep the unencrypted password in-memory,
// only until this app is deactivated/closed
CurrentContext.Password = newPassword;
// If there already was a password, we must decrypt all data with the old
// password (then re-encrypt it with the new password) while we still
// know the old password! Otherwise the data will be unreadable!
if (currentHashedPassword != null)
{
// Each item in the NotesList setting has an EncryptedContent property
// that must be processed
for (int i = 0; i < Settings.NotesList.Value.Count; i++)
{
// Encrypt with the new password the data that is decrypted
// with the old password
Settings.NotesList.Value[i].EncryptedContent =
Crypto.Encrypt(
Crypto.Decrypt(Settings.NotesList.Value[i].EncryptedContent,
this.OldPasswordBox.Password),
newPassword
);
}
}
}
CurrentContext.IsLoggedIn = true;
Close();
}
void PasswordBox_KeyUp(object sender, KeyEventArgs e)
{
// Allow the Enter key to cycle between text boxes and to press the ok
// button when on the last text box
if (e.Key == Key.Enter)
{
if (sender == this.PasswordHintTextBox ||
sender == this.NormalLoginPasswordBox)
OkButton_Click(sender, e);
else if (sender == this.OldPasswordBox)
this.NewPasswordBox.Focus();
else if (sender == this.NewPasswordBox)
this.ConfirmNewPasswordBox.Focus();
else if (sender == this.ConfirmNewPasswordBox)
this.PasswordHintTextBox.Focus();
}
}
public void Close()
{
if (this.Visibility == Visibility.Collapsed)
return; // Already closed
// Clear all
this.OldPasswordBox.Password = “”;
this.NewPasswordBox.Password = “”;
this.ConfirmNewPasswordBox.Password = “”;
this.NormalLoginPasswordBox.Password = “”;
The LoginControl User Control 503
this.PasswordHintTextBox.Text = “”;
// Close by becoming invisible
this.Visibility = Visibility.Collapsed;
// Raise the event
if (this.Closed != null)
this.Closed(this, EventArgs.Empty);
}
}
}

[/code]

  • This listing makes use of some of the following settings defined in a separate Settings.cs file:

    [code]

    public static class Settings
    {
    // Password-related settings
    public static readonly Setting<byte[]> Salt =
    new Setting<byte[]>(“Salt”, Crypto.GenerateNewSalt(16));
    public static readonly Setting<string> HashedPassword =
    new Setting<string>(“HashedPassword”, null);
    public static readonly Setting<string> PasswordHint =
    new Setting<string>(“PasswordHint”, null);
    // The user’s data
    public static readonly Setting<ObservableCollection<Note>> NotesList =
    new Setting<ObservableCollection<Note>>(“NotesList”,
    new ObservableCollection<Note>());
    // User settings
    public static readonly Setting<bool> MakeDefault =
    new Setting<bool>(“MakeDefault”, false);
    public static readonly Setting<Color> ScreenColor =
    new Setting<Color>(“ScreenColor”, Color.FromArgb(0xFF, 0xFE, 0xCF, 0x58));
    public static readonly Setting<Color> TextColor =
    new Setting<Color>(“TextColor”, Colors.Black);
    public static readonly Setting<int> TextSize = new Setting<int>(“TextSize”,
    22);
    // Temporary state
    public static readonly Setting<int> CurrentNoteIndex =
    new Setting<int>(“CurrentNoteIndex”, -1);
    public static readonly Setting<Color?> TempScreenColor =
    new Setting<Color?>(“TempScreenColor”, null);
    public static readonly Setting<Color?> TempTextColor =
    new Setting<Color?>(“TempTextColor”, null);
    }

    [/code]

    The salt required by Rfc2898DeriveBytes used by the Crypto class must be at least 8 bytes. With the call to GenerateNewSalt, this app generates a 16-byte salt.

  • In the normal login mode, the control must determine whether the entered password is correct. But the app doesn’t store the user’s password. Instead, it stores a salted hash of the password. Therefore, to validate the entered password, it calls the same Crypto.Hash function and checks if it matches the stored hashed value.
  • Although the unencrypted password is not persisted, it is kept in memory while the app runs so it can decrypt the user’s saved content and encrypt any new content. This is done with the CurrentContext class, defined as follows in CurrentContext.cs:

    [code]

    public static class CurrentContext
    {
    public static bool IsLoggedIn = false;
    public static string Password = null;
    }

    [/code]

  • In the change password mode, something very important must be done before the old password is forgotten. Everything that has been encrypted with the old password must be decrypted then re-encrypted with the new password. Otherwise, the data would become unreadable because the new password cannot be used to decrypt data that was encrypted with the old password!
  • Inside Close, the Password property of each password box is set to an empty string instead of null because the Password property throws an exception if set to null.
  • You can see that LoginControl is not a general-purpose control but rather tailored to this app. (Although it wouldn’t be hard to generalize it by providing a hook for the consumer to perform the data re-encryption during the password-change process.) It is used in three separate places.

The Change Password Page

The change password page, seen previously in Figure 21.1, is nothing more than a page hosting a LoginControl instance. The user can only reach this page when already signed in, so the control is automatically initialized to the “change password” mode thanks to the code in Listing 21.3. Listings 21.4 and 21.5 contain the simple XAML and codebehind for the change password page.

LISTING 21.4 ChangePasswordPage.xaml—The User Interface for Password & Secrets’ Change Password Page

[code]
<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.ChangePasswordPage”
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>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”PASSWORDS &amp; SECRETS”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”change password”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<!– The user control –>
<local:LoginControl Grid.Row=”1” Closed=”LoginControl_Closed”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 21.5 ChangePasswordPage.xaml.cs—The Code-Behind for Password & Secrets’ Change Password Page

[code]

using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class ChangePasswordPage : PhoneApplicationPage
{
public ChangePasswordPage()
{
InitializeComponent();
}
void LoginControl_Closed(object sender, System.EventArgs e)
{
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
}

[/code]

The Main Page

This app’s main page contains the list of user’s notes, as demonstrated in Figure 21.2. Each one can be tapped to view and/or edit it. A button on the application bar enables adding new notes. But before the list is populated and any of this is shown, the user must enter the correct password. When the user isn’t logged in, the LoginControl covers the entire page except its header, and the application bar doesn’t have the add-note button.

FIGURE 21.2 A list of notes on the main page, in various colors and sizes.
FIGURE 21.2 A list of notes on the main page, in various colors and sizes.

The User Interface

Listing 21.6 contains the XAML for the main page.

 

LISTING 21.6 MainPage.xaml—The User Interface for Password & Secrets’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=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<phone:PhoneApplicationPage.Resources>
<local:DateConverter x:Key=”DateConverter”/>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with 3 menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”show password hint”
Click=”PasswordMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”instructions”
Click=”InstructionsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”more apps”
Click=”MoreAppsMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid Background=”Transparent”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Grid.Row=”0”
Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”PASSWORDS &amp; SECRETS”
Style=”{StaticResource PhoneTextTitle0Style}”/>
</StackPanel>
<!– Show this when there are no notes –>
<TextBlock Name=”NoItemsTextBlock” Grid.Row=”1” Text=”No notes”
Visibility=”Collapsed” Margin=”22,17,0,0”
Style=”{StaticResource PhoneTextGroupHeaderStyle}”/>
<!– The list box containing notes –>
<ListBox x:Name=”ListBox” Grid.Row=”1” ItemsSource=”{Binding}”
SelectionChanged=”ListBox_SelectionChanged”>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<!– The title, in a style matching the note –>
<Border Background=”{Binding ScreenBrush}” Margin=”24,0” Width=”800”
MinHeight=”60” local:Tilt.IsEnabled=”True”>
<TextBlock Text=”{Binding Title}” FontSize=”{Binding TextSize}”
Foreground=”{Binding TextBrush}” Margin=”12”
VerticalAlignment=”Center”/>
</Border>
<!– The modified date –>
<TextBlock Foreground=”{StaticResource PhoneSubtleBrush}”
Text=”{Binding Modified, Converter={StaticResource DateConverter}}”
Margin=”24,0,0,12”/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!– The user control –>
<local:LoginControl x:Name=”LoginControl” Grid.Row=”1”
Closed=”LoginControl_Closed”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • The ampersand in the app’s title is XML encoded to avoid a XAML parsing error.
  • The LoginControl user control is used as a part of this page, rather than as a separate login page, to ensure a sensible navigation flow. When the user opens the app, logs in, and then sees the data on the main page, pressing the hardware Back button should exit the app, not go back to a login page!
  • LoginControl doesn’t protect the data simply by visually covering it up; you’ll see in the code-behind that it isn’t populated until after login. And there’s no way for the app to show the data before login because the correct password is needed to properly decrypt the stored notes.
  • The list box’s item template binds to several properties of each note. (The Note class used to represent each one is shown later in this chapter.) The binding to the Modified property uses something called a value converter to change the resultant display. Value converters are discussed next.

Value Converters

In data binding, value converters can morph a source value into a completely different target value. They enable you to plug in custom logic without giving up the benefits of data binding. Value converters are often used to reconcile a source and target that are different data types. For example, you could change the background or foreground color of an element based on the value of some nonbrush data source, à la conditional formatting in Microsoft Excel. As another example, the toggle switch in the Silverlight for Windows Phone Toolkit leverages a value converter called OnOffConverter that converts the nullable Boolean IsChecked value to an “On” or “Off” string used as its default content.

In Passwords & Secrets, we want to slightly customize the display of each note’s Modified property. Modified is of type DateTimeOffset, so without a value converter applied, it would appear as follows:

[code]12/11/2012 10:18:49 PM -08:00[/code]

The -08:00 represents the time zone. It is expressed as an offset from Coordinated Universal Time (UTC).

Our custom value converter strips off the time zone information and the seconds, as that’s more information than we need. It produces a result like the following:

[code]12/11/2010 10:18 PM[/code]

Even if Modified were a DateTime instead of a DateTimeOffset, the value converter would still be useful for stripping the seconds value out of the string.

What’s the difference between the DateTime data type and DateTimeOffset?

Whereas DateTime refers to a logical point in time that is independent of any time zone, DateTimeOffset is a real point in time with an offset relative to the UTC time zone. In this app, DateTimeOffset is appropriate to use for the modified time of each note because users shouldn’t expect that point in time to change even if they later travel to a different time zone.The preceding chapter’s Alarm Clock, however, appropriately uses DateTime for the alarm time. Imagine that you set the alarm while in one time zone but you’re in a different time zone when it’s time for it to go off. If you had set your alarm for 8:00 AM, you probably expect it to go off at 8:00 AM no matter what time zone you happen to be in at the time. For most scenarios, using DateTimeOffset is preferable to DateTime.However, it was introduced into the .NET Framework years after DateTime, so the better name was already taken. (Designers of the class rejected calling it DateTime2 or DateTimeEx). Fortunately, consumers of these data types can pretty much use them interchangeably.

To create a value converter, you must write a class that implements an IValueConverter interface in the System.Windows.Data namespace. This interface has two simple methods—Convert, which is passed the source instance that must be converted to the target instance, and ConvertBack, which does the opposite. Listing 21.7 contains the implementation of the DateConverter value converter used in Listing 21.6.

LISTING 21.7 DateConverter.cs—A Value Converter That Customizes the Display of a DateTimeOffset

[code]

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace WindowsPhoneApp
{
public class DateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
DateTimeOffset date = (DateTimeOffset)value;
// Return a custom format
return date.LocalDateTime.ToShortDateString() + “ “
+ date.LocalDateTime.ToShortTimeString();
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
return DependencyProperty.UnsetValue;
}
}
}

[/code]

The Convert method is called every time the source value changes. It’s given the DateTimeOffset value and returns a string with the date and time in a short format. The ConvertBack method is not needed, as it is only invoked in two-way data binding. Therefore, it returns a dummy value.

Value converters can be applied to any data binding with its Converter parameter. This was done in Listing 21.6 as follows:

[code]

<!– The modified date –>
<TextBlock Foreground=”{StaticResource PhoneSubtleBrush}”
Text=”{Binding Modified, Converter={StaticResource DateConverter}}”
Margin=”24,0,0,12”/>

[/code]

Setting this via StaticResource syntax requires an instance of the converter class to be defined in an appropriate resource dictionary. Listing 21.6 added an instance with the DateConverter key to the page’s resource dictionary:

[code]

<phone:PhoneApplicationPage.Resources>
<local:DateConverter x:Key=”DateConverter”/>
</phone:PhoneApplicationPage.Resources>

[/code]

Additional Data for Value Converters

The methods of IValueConverter are passed a parameter and a culture. By default, parameter is set to null and culture is set to the value of the target element’s Language property. However, the consumer of bindings can control these two values via Binding.ConverterParameter and Binding.ConverterCulture. For example:

[code]

<!– The modified date –>
<TextBlock Foreground=”{StaticResource PhoneSubtleBrush}”
Text=”{Binding Modified, Converter={StaticResource DateConverter},
ConverterParameter=custom data, ConverterCulture=en-US}”
Margin=”24,0,0,12”/>

[/code]

The ConverterParameter can be any custom data for the converter class to act upon, much like the Tag property on elements. ConverterCulture can be set to an Internet Engineering Task Force (IETF) language tag (such as en-US or ko-KR), and the converter receives the appropriate CultureInfo object. In DateConverter, the ToString methods already respect the current culture, so there’s no need to do anything custom with the culture.

Value converters are the key to plugging any kind of custom logic into the data-binding process that goes beyond basic formatting.Whether you want to apply some sort of transformation to the source value before displaying it or change how the target gets updated based on the value of the source, you can easily accomplish this with a class that implements IValueConverter. A very common value converter that people create is a Boolean-to-Visibility converter (usually called BooleanToVisibilityConverter) that can convert between the Visibility enumeration and a Boolean or nullable Boolean. In one direction, true is mapped to Visible, whereas false and null are mapped to Collapsed. In the other direction, Visible is mapped to true, whereas Collapsed is mapped to false.This is useful for toggling the visibility of elements based on the state of an otherwise unrelated element, all in XAML. For example, the following snippet of XAML implements a Show Button check box without requiring any procedural code (other than the value converter):

[code]

<phone:PhoneApplicationPage.Resources>
<local:BooleanToVisibilityConverter x:Key=”BooltoVis”/>
</phone:PhoneApplicationPage.Resources>

<CheckBox x:Name=”CheckBox”>Show Button</CheckBox>

<Button Visibility=”{Binding IsChecked, ElementName=CheckBox,
Converter={StaticResource BoolToVis}}”…/>

[/code]

In this case, the button is visible when (and only when) the check box’s IsChecked property is true.

The Code-Behind

The code-behind for the main page is shown in Listing 21.8.

LISTING 21.8 MainPage.xaml.cs—The Code-Behind for Password & Secrets’Main Page

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
IApplicationBarMenuItem passwordMenuItem;
public MainPage()
{
InitializeComponent();
this.passwordMenuItem = this.ApplicationBar.MenuItems[0]
as IApplicationBarMenuItem;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// The password menu item is “show password hint” when not logged in,
// or “change password” when logged in
if (CurrentContext.IsLoggedIn)
{
this.passwordMenuItem.Text = “change password”;
// This is only needed when reactivating app and navigating back to this
// page from the details page, because going back can instantiate
// this page in a logged-in state
this.LoginControl.Close();
}
else
{
this.passwordMenuItem.Text = “show password hint”;
}
// Clear the selection so selecting the same item twice in a row will
// still raise the SelectionChanged event
Settings.CurrentNoteIndex.Value = -1;
this.ListBox.SelectedIndex = -1;
if (Settings.NotesList.Value.Count == 0)
this.NoItemsTextBlock.Visibility = Visibility.Visible;
else
this.NoItemsTextBlock.Visibility = Visibility.Collapsed;
}
void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ListBox.SelectedIndex >= 0)
{
// Navigate to the details page for the selected item
Settings.CurrentNoteIndex.Value = ListBox.SelectedIndex;
this.NavigationService.Navigate(new Uri(“/DetailsPage.xaml”,
UriKind.Relative));
}
}
void LoginControl_Closed(object sender, EventArgs e)
{
// Now that we’re logged-in, add the “new” button to the application bar
ApplicationBarIconButton newButton = new ApplicationBarIconButton
{
Text = “new”,
IconUri = new Uri(“/Shared/Images/appbar.add.png”, UriKind.Relative)
};
newButton.Click += NewButton_Click;
this.ApplicationBar.Buttons.Add(newButton);
// The password menu item is “show password hint” when not logged in,
// or “change password” when logged in
this.passwordMenuItem.Text = “change password”;
// Now bind the notes list as the data source for the list box,
// because its contents can be decrypted
this.DataContext = Settings.NotesList.Value;
}
// Application bar handlers
void NewButton_Click(object sender, EventArgs e)
{
// Create a new note and add it to the top of the list
Note note = new Note();
note.Modified = DateTimeOffset.Now;
note.ScreenColor = Settings.ScreenColor.Value;
note.TextColor = Settings.TextColor.Value;
note.TextSize = Settings.TextSize.Value;
Settings.NotesList.Value.Insert(0, note);
// “Select” the new note
Settings.CurrentNoteIndex.Value = 0;
// Navigate to the details page for the newly created note
this.NavigationService.Navigate(new Uri(“/DetailsPage.xaml”,
UriKind.Relative));
}
void PasswordMenuItem_Click(object sender, EventArgs e)
{
if (CurrentContext.IsLoggedIn)
{
// Change password
this.NavigationService.Navigate(new Uri(“/ChangePasswordPage.xaml”,
UriKind.Relative));
}
else
{
// Show password hint
if (Settings.PasswordHint.Value == null ||
Settings.PasswordHint.Value.Trim().Length == 0)
{
MessageBox.Show(“Sorry, but there is no hint!”, “Password hint”,
MessageBoxButton.OK);
}
else
{
MessageBox.Show(Settings.PasswordHint.Value, “Password hint”,
MessageBoxButton.OK);
}
}
}
void InstructionsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/Shared/About/AboutPage.xaml?appName=Passwords %26 Secrets”,
UriKind.Relative));
}
}
}

[/code]

  • The first menu item on the application bar, shown expanded in Figure 21.3, reveals the password hint when the user is logged out and navigates to the change password page when the user is logged in.
FIGURE 21.3 The expanded application bar menu shows “change password” when the user is logged in.
FIGURE 21.3 The expanded application bar menu shows “change password” when the user is logged in.
  • As seen earlier, the NotesList collection used as the data context for the list box is not just any collection (like List<Note>); it’s an observable collection:

    [code]
    public static readonly Setting<ObservableCollection<Note>> NotesList =
    new Setting<ObservableCollection<Note>>(“NotesList”,
    new ObservableCollection<Note>());
    [/code]

    Observable collections raise a CollectionChanged event whenever any changes
    occur, such as items being added or removed. Data binding automatically leverages
    this event to keep the target (the list box, in this page) up-to-date at all times.
    Thanks to this, Listing 21.8 simply sets the page’s data context to the list and the
    rest takes care of itself.

The INotifyPropertyChanged Interface

Although the observable collection takes care off additions and deletions being reflected in the list box, each Note item must provide notifications to ensure that item-specific property changes are reflected in the databound list box. Note does this by implementing INotifyPropertyChanged, as shown in Listing 21.9.

LISTING 21.9 Note.cs—The Note Class Representing Each Item in the List

[code]

using System;
using System.ComponentModel;
using System.Windows.Media;
namespace WindowsPhoneApp
{
public class Note : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
// A helper method used by the properties
void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
string encryptedContent;
public string EncryptedContent
{
get { return this.encryptedContent; }
set { this.encryptedContent = value;
OnPropertyChanged(“EncryptedContent”); OnPropertyChanged(“Title”); }
}
DateTimeOffset modified;
public DateTimeOffset Modified
{
get { return this.modified; }
set { this.modified = value; OnPropertyChanged(“Modified”); }
}
int textSize;
public int TextSize
{
get { return this.textSize; }
set { this.textSize = value; OnPropertyChanged(“TextSize”); }
}
Color screenColor;
public Color ScreenColor
{
get { return this.screenColor; }
set { this.screenColor = value;
OnPropertyChanged(“ScreenColor”); OnPropertyChanged(“ScreenBrush”); }
}
Color textColor;
public Color TextColor
{
get { return this.textColor; }
set { this.textColor = value;
OnPropertyChanged(“TextColor”); OnPropertyChanged(“TextBrush”); }
}
// Three readonly properties whose value is computed from other properties:
public Brush ScreenBrush
{
get { return new SolidColorBrush(this.ScreenColor); }
}
public Brush TextBrush
{
get { return new SolidColorBrush(this.TextColor); }
}
public string Title
{
get
{
// Grab the note’s content
string title =
Crypto.Decrypt(this.EncryptedContent, CurrentContext.Password) ?? “”;
// Don’t include more than the first 100 characters, which should be long
// enough, even in landscape with a small font
if (title.Length > 100)
title = title.Substring(0, 100);
// Fold the remaining content into a single line. We can’t use
// Environment.NewLine because it’s rn, whereas newlines inserted from
// a text box are just r
return title.Replace(‘r’, ‘ ‘);
}
}
}
}

[/code]

  • INotifyPropertyChanged has a single member—a PropertyChanged event. If the implementer raises this event at the appropriate time with the name of each property that has changed, data binding takes care of refreshing any targets.
  • The raising of the PropertyChanged event is handled by the OnPropertyChanged helper method. The event handler field is assigned to a handler variable to avoid a potential bug. Without this, if a different thread removed the last handler between the time that the current thread checked for null and performed the invocation, a NullReferenceException would be thrown. (The event handler field becomes null when no more listeners are attached.)
  • Notice that some properties, when changed, raise the PropertyChanged event for an additional property. For example, when EncryptedContent is set to a new value, a PropertyChanged event is raised for the readonly Title property. This is done because the value of Title is based on the value of EncryptedContent, so a change to EncryptedContent may change Title.

INotifyCollectionChanged

Observable collections perform their magic by implementing INotifyCollectionChanged, an interface that is very similar to INotifyPropertyChanged.This interface contains a single CollectionChanged event. It is very rare,however, for people to write their own collection class and implement INotifyCollectionChanged rather than simply using the ObservableCollection class.

The Details Page

The details page, shown in Figure 21.4, appears when the user taps a note in the list box on the main page. This page displays the entire contents of the note and enables the user to edit it, delete it, or email its contents. It also provides access to a per-note settings page that gives control over the note’s colors and text size. Listing 21.10 contains this page’s XAML.

FIGURE 21.4 The details page, shown for a white-on-red note.
FIGURE 21.4 The details page, shown for a white-on-red note.

LISTING 21.10 DetailsPage.xaml—The User Interface for Passwords & Secrets’Details Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.DetailsPage”
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”>
<!– The application bar, with three buttons –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar IsVisible=”False”>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png”
Click=”DeleteButton_Click”/>
<shell:ApplicationBarIconButton Text=”email”
IconUri=”/Shared/Images/appbar.email.png”
Click=”EmailButton_Click”/>
<shell:ApplicationBarIconButton Text=”settings”
IconUri=”/Shared/Images/appbar.settings.png”
Click=”SettingsButton_Click”/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<phone:PhoneApplicationPage.Resources>
<!– A copy of the text box default style with its border removed and
background applied differently. Compare with the style in Program Files
Microsoft SDKsWindows Phonev7.0DesignSystem.Windows.xaml –>

</phone:PhoneApplicationPage.Resources>
<ScrollViewer>
<Grid>
<!– The full-screen text box –>
<TextBox x:Name=”TextBox” InputScope=”Text”
Style=”{StaticResource PhoneTextBox}”
AcceptsReturn=”True” TextWrapping=”Wrap”
GotFocus=”TextBox_GotFocus” LostFocus=”TextBox_LostFocus”/>
<!– The user control –>
<local:LoginControl x:Name=”LoginControl” Closed=”LoginControl_Closed”/>
</Grid>
</ScrollViewer>
</phone:PhoneApplicationPage>

[/code]

The text box that basically occupies the whole screen is given a custom style that removes its border and ensures the desired background color remains visible whether the text box has focus. The style was created by copying the default style from %ProgramFiles%Microsoft SDKsWindows Phonev7.0DesignSystem.Windows.xaml then making a few tweaks.

Listing 21.11 contains the code-behind for this page.

LISTING 21.11 DetailsPage.xaml.cs—The Code-Behind for Passwords & Secrets’Details Page

[code]

using System;
using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Tasks;
namespace WindowsPhoneApp
{
public partial class DetailsPage : PhoneApplicationPage
{
bool navigatingFrom;
string initialText = “”;
public DetailsPage()
{
InitializeComponent();
this.Loaded += DetailsPage_Loaded;
}
void DetailsPage_Loaded(object sender, RoutedEventArgs e)
{
if (CurrentContext.IsLoggedIn)
{
// Automatically show the keyboard for new notes.
// This also gets called when navigating away, hence the extra check
// to make sure we’re only doing this when navigating to the page
if (this.TextBox.Text.Length == 0 && !this.navigatingFrom)
this.TextBox.Focus();
}
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
this.navigatingFrom = true;
base.OnNavigatedFrom(e);
if (this.initialText != this.TextBox.Text)
{
// Automatically save the new content
Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
n.EncryptedContent =
Crypto.Encrypt(this.TextBox.Text, CurrentContext.Password) ?? “”;
n.Modified = DateTimeOffset.Now;
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (CurrentContext.IsLoggedIn)
this.LoginControl.Close();
}
void TextBox_GotFocus(object sender, RoutedEventArgs e)
{
this.ApplicationBar.IsVisible = false;
}
void TextBox_LostFocus(object sender, RoutedEventArgs e)
{
this.ApplicationBar.IsVisible = true;
}
void LoginControl_Closed(object sender, EventArgs e)
{
this.ApplicationBar.IsVisible = true;
// Show the note’s contents
Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
if (n != null)
{
this.TextBox.Background = n.ScreenBrush;
this.TextBox.Foreground = n.TextBrush;
this.TextBox.FontSize = n.TextSize;
this.initialText = this.TextBox.Text =
Crypto.Decrypt(n.EncryptedContent, CurrentContext.Password) ?? “”;
}
}
// Application bar handlers:
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to delete this note?”,
“Delete note?”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
Settings.NotesList.Value.Remove(
Settings.NotesList.Value[Settings.CurrentNoteIndex.Value]);
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
void EmailButton_Click(object sender, EventArgs e)
{
EmailComposeTask launcher = new EmailComposeTask();
launcher.Body = this.TextBox.Text;
launcher.Subject = “Note”;
launcher.Show();
}
void SettingsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
}
}

[/code]

  • This page uses a navigatingFrom flag to check whether the page is in the process of navigating away. That’s because the Loaded event gets raised a second time after OnNavigatedFrom, and applying focus to the text box at this time could cause an unwanted flicker from the on-screen keyboard briefly appearing.
  • The code for the settings page linked to this page is shown in the next chapter, because it is identical to the one used by this app!

A page’s Loaded event is incorrectly raised when navigating away!

This is simply a bug in the current version of Windows Phone.To avoid performance problems, potential flickering, or other issues, consider setting a flag in OnNavigatedFrom that you can check inside Loaded, as done in Listing 21.11.That way, you can be sure that your page-loading logic only runs when the page is actually loading.

The Finished Product

Passwords & Secrets (Encryption & Observable Collections)

Lottery Numbers Picker (Sharing Animations)

Lottery Numbers Picker helps you choose potentially winning numbers to use when you buy a lottery ticket. First, you can tell it how many numbers it needs to select, what the range of the numbers should be, and whether duplicate numbers are allowed. Then, Lottery Numbers Picker selects the numbers with a fun animation that mimics a machine filled with percolating lottery balls (the kind you see on TV).

The Main Page

Lottery Numbers Picker has a main page, a settings page, and the standard about page.

The User Interface

Listing 16.1 contains the XAML for the main page.

LISTING 16.1 MainPage.xaml—The Main User Interface for Lottery Numbers Picker

[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” shell:SystemTray.IsVisible=”True”>
<!– The application bar –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”pick”
IconUri=”/Shared/Images/appbar.play.png”
Click=”PickButton_Click”/>
<shell:ApplicationBarIconButton Text=”settings”
IconUri=”/Shared/Images/appbar.settings.png”
Click=”SettingsButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<!– Add one storyboard to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– This storyboard is applied to each chosen ball, one at a time –>
<Storyboard x:Name=”ChosenBallStoryboard”
Completed=”ChosenBallStoryboard_Completed”>
<!– Makes the chosen ball “blow” upward from the bottom –>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”(Canvas.Top)”>
<LinearDoubleKeyFrame KeyTime=”0:0:0” Value=”728”/>
<EasingDoubleKeyFrame KeyTime=”0:0:1.5” Value=”100”>
<EasingDoubleKeyFrame.EasingFunction>
<ElasticEase Oscillations=”1” Springiness=”6” EasingMode=”EaseOut”/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<!– Makes the chosen ball slide to the left, once at the top –>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”(Canvas.Left)”>
<LinearDoubleKeyFrame KeyTime=”0:0:0” Value=”424”/>
<LinearDoubleKeyFrame KeyTime=”0:0:1.2” Value=”424”/>
<EasingDoubleKeyFrame x:Name=”FinalLeftKeyFrame” KeyTime=”0:0:2”>
<EasingDoubleKeyFrame.EasingFunction>
<BounceEase EasingMode=”EaseOut” Bounces=”1” Bounciness=”8”/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<!– Spins the chosen ball while it moves up and left, and end upright –>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=
“(local:LotteryBall.RenderTransform).(RotateTransform.Angle)”>
<LinearDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
<EasingDoubleKeyFrame KeyTime=”0:0:1.2” Value=”360”>
<EasingDoubleKeyFrame.EasingFunction>
<BounceEase Bounces=”10”/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime=”0:0:2” Value=”0”>
<EasingDoubleKeyFrame.EasingFunction>
<BounceEase EasingMode=”EaseOut” Bounces=”1” Bounciness=”8”/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– Prevent off-screen visuals from appearing during a page transition –>
<phone:PhoneApplicationPage.Clip>
<RectangleGeometry Rect=”0,0,480,728”/>
</phone:PhoneApplicationPage.Clip>
<Canvas>
<!– Mini-header –>
<TextBlock Text=”LOTTERY NUMBERS PICKER” Margin=”24,16,0,12”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<!– Chosen balls get dynamically added to this canvas –>
<Canvas x:Name=”ChosenBallsCanvas”/>
<!– A canvas filled with percolating plain balls –>
<Canvas>
<local:LotteryBall Percolating=”True” Canvas.Top=”630”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”40” Canvas.Top=”635”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”80” Canvas.Top=”630”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”120” Canvas.Top=”635”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”160” Canvas.Top=”630”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”200” Canvas.Top=”635”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”240” Canvas.Top=”630”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”280” Canvas.Top=”635”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”320” Canvas.Top=”630”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”360” Canvas.Top=”635”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”5” Canvas.Top=”650”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”45” Canvas.Top=”655”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”85” Canvas.Top=”650”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”125” Canvas.Top=”655”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”165” Canvas.Top=”650”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”205” Canvas.Top=”655”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”245” Canvas.Top=”650”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”285” Canvas.Top=”655”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”325” Canvas.Top=”650”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”365” Canvas.Top=”655”/>
<local:LotteryBall Percolating=”True” Canvas.Top=”670”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”40” Canvas.Top=”675”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”80” Canvas.Top=”670”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”120” Canvas.Top=”675”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”160” Canvas.Top=”670”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”200” Canvas.Top=”675”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”240” Canvas.Top=”670”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”280” Canvas.Top=”675”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”320” Canvas.Top=”670”/>
<local:LotteryBall Percolating=”True” Canvas.Left=”360” Canvas.Top=”675”/>
</Canvas>
<!– The container of balls –>
<Rectangle Fill=”{StaticResource PhoneAccentBrush}” Canvas.Top=”153”
Width=”421” Height=”600” Opacity=”.5”/>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

Notes:

  • The single storyboard in Listing 16.1 doesn’t have Storyboard.TargetName assigned. That’s because this storyboard is dynamically assigned to each new ball that reveals a lottery number, as you’ll see in the code-behind. It contains three animations: one to move the ball upward along the right side of the screen, one to move it over to the left, and one to spin it the whole time. The animations are given various easing functions with various properties set to give a lifelike appearance of each ball being blown into place.
  • Although the chosen lottery balls are dynamically added to ChosenBallsCanvas, the percolating balls in the ball machine have been statically added to the next canvas in hardcoded locations. Each ball is represented by a LotteryBall user control, shown in the next section. Because their custom Percolating property is set to true, these balls bounce erratically.

The Code-Behind

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

LISTING 16.2 MainPage.xaml.cs—The Code-Behind for Lottery Numbers Picker’s Main Page

[code]

using System;
using System.Collections.Generic;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
Random random = new Random();
List<int> chosenNumbers = new List<int>();
LotteryBall mostRecentBall;
IApplicationBarIconButton pickButton;
public MainPage()
{
InitializeComponent();
this.pickButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
}
void ShowNewBall()
{
// Create a new ball with the correct chosen number.
// The RotateTransform is there so the spinning animation works.
this.mostRecentBall = new LotteryBall {
RenderTransform = new RotateTransform(),
Number = this.chosenNumbers[this.ChosenBallsCanvas.Children.Count]
};
// Assign the storyboard to this new ball
Storyboard.SetTarget(this.ChosenBallStoryboard, this.mostRecentBall);
// Adjust the final horizontal position of the ball based on which one it is
this.FinalLeftKeyFrame.Value = this.mostRecentBall.Width *
this.ChosenBallsCanvas.Children.Count;
// Add the new ball to the canvas
this.ChosenBallsCanvas.Children.Add(this.mostRecentBall);
// Start animating
this.ChosenBallStoryboard.Begin();
}
void ChosenBallStoryboard_Completed(object sender, EventArgs e)
{
// The storyboard must be stopped before its
// target is changed again inside ShowNewBall
this.ChosenBallStoryboard.Stop();
// Manually position the ball in the same spot where the animation left it
Canvas.SetTop(this.mostRecentBall, 100);
Canvas.SetLeft(this.mostRecentBall,
this.mostRecentBall.Width * (this.ChosenBallsCanvas.Children.Count – 1));
// Keep going until enough balls have been chosen
if (this.ChosenBallsCanvas.Children.Count < Settings.NumBalls.Value)
ShowNewBall();
else
this.pickButton.IsEnabled = true;
}
// Application bar handlers
void PickButton_Click(object sender, EventArgs e)
{
this.pickButton.IsEnabled = false;
this.chosenNumbers.Clear();
this.ChosenBallsCanvas.Children.Clear();
// Pick all the numbers
for (int i = 0; i < Settings.NumBalls.Value; i++)
{
// If no duplicate numbers are allowed, keep
// picking until the number is unique
int num;
do
{
num = this.random.Next(Settings.MinNumber.Value,
Settings.MaxNumber.Value + 1);
}
while (!Settings.AllowDuplicates.Value &&
this.chosenNumbers.Contains(num));
this.chosenNumbers.Add(num);
}
// Sort the chosen numbers in increasing numeric order
this.chosenNumbers.Sort();
// Reveal the first ball
ShowNewBall();
}
void SettingsButton_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(
“/Shared/About/AboutPage.xaml?appName=Lottery Numbers Picker”,
UriKind.Relative));
}
}
}

[/code]

Notes:

  • Inside ShowNewBall, a number is assigned to the new ball from the chosenNumbers list that is filled inside PickButton_Click. The number of children in ChosenBallsCanvas can be used as the list’s index before the new ball is added to the canvas because the number is 0 for the first ball, 1 for the second ball, and so on.
  • Rather than calling Storyboard.SetTargetName (which effectively sets the Storyboard.TargetName attachable property), the code calls Storyboard.SetTarget to assign the target element to the storyboard. This is easier to use in C#, because you can simply pass it the instance of the element to animate even when it doesn’t have a name.
  • The storyboard’s Completed event handler (ChosenBallStoryboad_ Completed) calls ShowNewBall to repeat the creation and animation of a new ball until the correct number of balls have been shown. It must stop the storyboard, rather than having it remain in a filling state, because otherwise the next call to Storyboard.SetTarget inside ShowNewBall would fail. You cannot change a storyboard’s target unless it is completely stopped. Because the storyboard is stopped, the just-animated ball is manually given its ending position. Without this code, the ball would snap back to its pre-animation position. An alternative approach would be to get the ball’s position before stopping the storyboard and then setting it to that position after stopping it.

The LotteryBall User Control

The LotteryBall user control used by the main page represents each circle with an optional number centered on it. The XAML for this user control is shown in Listing 16.3. The most interesting part of this XAML is that the control uses a TransformGroup for its translation and rotation rather than a CompositeTransform. This is because a CompositeTransform performs rotation before translation, but the functionality of this control requires that the rotation happens after the translation. Therefore, TransformGroup is used with its TranslateTransform child placed before the RotateTransform child.

LISTING 16.3 LotteryBall.xaml—The User Interface for the LotteryBall User Control

[code]

<UserControl x:Class=”WindowsPhoneApp.LotteryBall”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
FontFamily=”Segoe WP Black” FontSize=”{StaticResource PhoneFontSizeLarge}”
Width=”53” Height=”53” RenderTransformOrigin=”.5,.5”>
<UserControl.RenderTransform>
<TransformGroup>
<TranslateTransform x:Name=”TranslateTransform”/>
<RotateTransform x:Name=”RotateTransform”/>
</TransformGroup>
</UserControl.RenderTransform>
<Grid>
<Ellipse Fill=”{StaticResource PhoneForegroundBrush}”/>
<TextBlock x:Name=”NumberTextBlock” Margin=”1,0,0,3”
Foreground=”{StaticResource PhoneBackgroundBrush}”
HorizontalAlignment=”Center” VerticalAlignment=”Center”/>
</Grid>
</UserControl>

[/code]

Listing 16.4 contains the code-behind for this user control.

LISTING 16.4 LotteryBall.xaml.cs—The Code-Behind for the LotteryBall User Control

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
namespace WindowsPhoneApp
{
public partial class LotteryBall : UserControl
{
int number;
bool percolating;
Storyboard percolatingStoryboard;
DoubleAnimation percolatingAnimation;
Random random = new Random();
public LotteryBall()
{
InitializeComponent();
}
public int Number
{
get { return this.number; }
set
{
this.NumberTextBlock.Text = value.ToString();
this.number = value;
}
}
public bool Percolating
{
get { return this.percolating; }
set
{
this.percolating = value;
if (this.percolating)
{
// Create a new single-animation storyboard
this.percolatingStoryboard = new Storyboard();
this.percolatingAnimation = new DoubleAnimation { AutoReverse = true };
this.percolatingAnimation.EasingFunction = new QuadraticEase {
EasingMode = EasingMode.EaseInOut };
this.percolatingStoryboard.Children.Add(this.percolatingAnimation);
// Assign the storyboard to this instance’s TranslateTransform and
// animate its Y property to create a “bounce”
Storyboard.SetTarget(this.percolatingStoryboard,
this.TranslateTransform);
Storyboard.SetTargetProperty(this.percolatingStoryboard,
new PropertyPath(“Y”));
// When the “bounce” completes, choose new random values and start
// it again. Repeat indefinitely.
this.percolatingStoryboard.Completed += delegate(object s, EventArgs e)
{
Randomize();
this.percolatingStoryboard.Begin();
};
// Choose random values related to the animation
// and start it for the first time
Randomize();
percolatingStoryboard.Begin();
}
}
}
void Randomize()
{
// Vary the distance and duration of the bounce
this.percolatingAnimation.To = this.random.Next(20, 60) * -1;
this.percolatingAnimation.Duration = TimeSpan.FromMilliseconds(
this.random.Next(50, 200));
// Very the angle of the bounce
this.RotateTransform.Angle = this.random.Next(0, 90) * -1;
}
}
}

[/code]

Notes:

  • This user control exposes two properties: Number, which displays the input number on top of the ellipse, and Percolating, which makes the ball bounce erratically when set to true. In this app, only Number is set for the chosen balls and only Percolating is set for the balls inside the machine.
  • The erratic bouncing of a percolating ball is enabled by a short random bounce storyboard that, when completed, is given new random values before beginning again. This storyboard and animation is created entirely in C#, which has not previously been seen in this book. The code mirrors what you would write in XAML, except that the adding of the animation to the storyboard is more explicit (with a call to Children.Add), and the property path used for specifying the target property is also more explicit. As with Listing 16.2, SetTarget is used to directly associate the storyboard with the target object.
  • Inside Randomize, the length of each bounce is allowed to vary from 20 to 59 pixels upward (in the negative direction), and the duration of the upward motion is allowed to vary from 50 to 199 milliseconds. To prevent each ball from bouncing straight up, the angle of the user control is randomly set to a range of 0 to –90°. Although the angle is not part of the animation, it affects the animation because it changes the meaning of translating upward. This is why it’s important that the RotateTransform appears after the TranslateTransform in Listing 16.3. Although the ball is translated upward from its origin, the whole coordinate space is then rotated, causing the ball to translate at whatever angle has been chosen. (If the rotation happened first, it would have no effect on the symmetric plain circle.) The range of 0 to –90° is chosen so the balls don’t ever bounce to the right and break the illusion that they are contained inside the machine.

The Settings Page

The Lottery Numbers Picker app uses the following settings defined in Settings.cs:

[code]

public static class Settings
{
public static readonly Setting<int> NumBalls =
new Setting<int>(“NumBalls”, 5);
public static readonly Setting<int> MinNumber =
new Setting<int>(“MinNumber”, 1);
public static readonly Setting<int> MaxNumber =
new Setting<int>(“MaxNumber”, 56);
public static readonly Setting<bool> AllowDuplicates =
new Setting<bool>(“AllowDuplicates”, false);
}

[/code]

The settings page, shown in Figure 16.1, enables the user to adjust all these settings.

FIGURE 16.1 The settings page contains a check box and three sliders.
FIGURE 16.1 The settings page contains a check box and three sliders.

The User Interface

Listing 16.5 contains the XAML for the settings page.

LISTING 16.5 SettingsPage.xaml—The User Interface for Lottery Numbers Picker’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>
<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=”lottery numbers picker”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<Grid Margin=”{StaticResource PhoneMargin}”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
</Grid.RowDefinitions>
<!– Allow duplicate numbers –>
<CheckBox x:Name=”AllowDuplicatesCheckBox” HorizontalAlignment=”Left”
Content=”Allow duplicate numbers” local:Tilt.IsEnabled=”True”/>
<!– Number of balls –>
<TextBlock Grid.Row=”1” Text=”Number of balls” Margin=”12,16,0,8”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBlock x:Name=”NumBallsTextBlock” Grid.Row=”1” Margin=”0,16,23,0”
HorizontalAlignment=”Right”
Text=”{Binding Value, ElementName=NumBallsSlider}”/>
<Slider x:Name=”NumBallsSlider” Grid.Row=”2” Minimum=”1” Maximum=”8”
Tag=”{Binding ElementName=NumBallsTextBlock}”
ValueChanged=”Slider_ValueChanged” />
<!– Smallest possible number –>
<TextBlock Grid.Row=”3” Text=”Smallest possible number” Margin=”12,7,0,8”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBlock x:Name=”MinNumberTextBlock” Grid.Row=”3” Margin=”0,7,23,0”
HorizontalAlignment=”Right”
Text=”{Binding Value, ElementName=MinNumberSlider}”/>
<Slider x:Name=”MinNumberSlider” Grid.Row=”4” Minimum=”0” Maximum=”98”
Tag=”{Binding ElementName=MinNumberTextBlock}”
ValueChanged=”Slider_ValueChanged” />
<!– Largest possible number –>
<TextBlock Grid.Row=”5” Text=”Largest possible number” Margin=”12,7,0,8”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBlock x:Name=”MaxNumberTextBlock” Grid.Row=”5” Margin=”0,7,23,0”
HorizontalAlignment=”Right”
Text=”{Binding Value, ElementName=MaxNumberSlider}”/>
<Slider x:Name=”MaxNumberSlider” Grid.Row=”6” Minimum=”1” Maximum=”99”
Tag=”{Binding ElementName=MaxNumberTextBlock}”
ValueChanged=”Slider_ValueChanged” />
</Grid>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Notes:

  • AllowDuplicatesCheckBox is aligned to the left to prevent accidental tapping and over-tilting if the right side of the screen is tapped.
  • NumBallsSlider enables a range from 1 to 8, MinNumberSlider enables a range from 0 to 98, and MaxNumberSlider enables a range from 1 to 99. Each slider has a corresponding text block that binds its text to the slider’s current value. This only works appropriately due to logic in code-behind in an event handler for each slider’s ValueChanged event.

The Code-Behind

Listing 16.6 contains the code-behind for the settings page.

LISTING 16.6 SettingsPage.xaml.cs—The Code-Behind for Lottery Numbers Picker’s Settings Page

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
bool initialized = false;
public SettingsPage()
{
InitializeComponent();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Save the chosen settings
Settings.AllowDuplicates.Value =
this.AllowDuplicatesCheckBox.IsChecked.Value;
Settings.NumBalls.Value = (int)this.NumBallsSlider.Value;
Settings.MinNumber.Value = (int)this.MinNumberSlider.Value;
Settings.MaxNumber.Value = (int)this.MaxNumberSlider.Value;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the saved settings
this.AllowDuplicatesCheckBox.IsChecked = Settings.AllowDuplicates.Value;
this.NumBallsSlider.Value = Settings.NumBalls.Value;
this.MinNumberSlider.Value = Settings.MinNumber.Value;
this.MaxNumberSlider.Value = Settings.MaxNumber.Value;
this.initialized = true;
}
void Slider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
Slider slider = sender as Slider;
// Round the value so it’s a whole number even when the slider is dragged
slider.Value = Math.Round(slider.Value);
// Don’t do anything until all initial values have been set
if (this.initialized)
{
// Don’t allow min to be higher than max
if (this.MinNumberSlider.Value > this.MaxNumberSlider.Value)
this.MaxNumberSlider.Value = this.MinNumberSlider.Value;
// If the range is too small, auto-allow duplicates
if (this.MinNumberSlider.Value >=
this.MaxNumberSlider.Value – this.NumBallsSlider.Value)
{
this.AllowDuplicatesCheckBox.IsChecked = true;
this.AllowDuplicatesCheckBox.IsEnabled = false;
}
else
{
this.AllowDuplicatesCheckBox.IsEnabled = true;
}
}
}
bool IsMatchingOrientation(PageOrientation orientation)
{
return ((this.Orientation & orientation) == orientation);
}
}
}

[/code]

Slider_ValueChanged forces each slider’s value to an integral value, because it could otherwise become fractional when the user drags the slider. (When this Math.Round assignment actually changes the value, it causes Slider_ValueChanged to be called again. When the value is already a whole number, this does not happen, which is important for preventing an infinite loop.) This method also guards against conditions that would cause the app to crash or hang, enforcing a valid relationship between the minimum number and the maximum number.

The Finished Product

Lottery Numbers Picker (Sharing Animations)