Reading and Writing Files Using Storage, Windows Phone Isolated Storage

Using Windows Phone Isolated Storage

XNA potentially supports many storage devices across all the game systems it targets, but on the WP7, there is only one type: isolated storage. Any type of game asset can be imported into the Content Manager through an existing or user-created content importer, and this is the preferred way to read game data that does not change. For data that might change, such as generated data files or user-created game levels, or saved games, we can access the file system to stream data to our game from isolated storage. Any type of data file can be opened and written to or read from using Storage.IO classes, which we will learn about here. Since XML is recognized as a resource file by Content Manager, we will use XML for our example in this hour. XML has the benefit of being versatile and human readable at the same time. But binary and text files can be used as well.

Saving a Data File

We will first learn to create a new file in isolated storage and then read the data back out of the file afterward. The first thing that must be done is to add the library System.Xml.Serialization to the project via the references. The Serialization library makes it very easy to convert a class or struct into a file and read it back again without our having to decode the file manually (by setting individual properties one at a time). Let’s add it to the project.

Adding XML Support to the Project

  1. Right-click References in the Content project and choose Add Reference from the pop-up context menu.
  2. Locate the library called System.Xml.Serialization in the list, as shown in Figure 19.1.

    The System.Xml. Serialization library.
    FIGURE 19.1 The System.Xml. Serialization library.

Now that the reference is set, we can use XML files in the project more easily.

Isolated Storage

To access a file in isolated storage, we have to create a file object using the IsolatedStorageFile class:

[code]
IsolatedStorageFile storage =
IsolatedStorageFile.GetUserStoreForApplication();
[/code]

IsolatedStorageFile.GetUserStoreForApplication() is a rather verbose method that creates the new storage object with linkage to the application’s (or game’s) private storage area, making it available for accessing files, directories, and so on. If the object doesn’t need to be made global to the project, a shorthand declaration can be used:

[code]
var storage = IsolatedStorageFile.GetUserStoreForApplication();
[/code]

Creating a New Directory

Next, a required step must be taken: A directory must be created for the application to store files in. The private or isolated storage area has room for dictionary-style key/value data as well as SQL database tables, so we can’t just toss files in there like one large file system—we have to create a directory. If you don’t create a directory first, an exception error will occur when you try to create a new file. We will use the storage object to create a directory. The IsolatedStorageFile class has a method called DirectoryExists() that returns true if a passed directory exists. CreateDirectory() is used to create a new directory. So, if the directory doesn’t already exist, we want to create it:

[code]
const string directory = “StorageDemo”;
if (!storage.DirectoryExists(directory))
storage.CreateDirectory(directory);
[/code]

Creating a New File

Now, we can create a file inside the directory. First, we have to check to see whether the file exists. WP7 does not support the FileMode.CreateNew option, which is supposed to overwrite a file if it already exists. Trying to do this generates an exception error, even though it works on Windows and Xbox 360. So, we have to delete the file first before creating it again. Usually this is not a problem because savegame data tends to be rather simple for most games. If you are working on a large, complex game, like an RPG, and there’s a lot of data, of course the game might support multiple savegame files, and you’ll have a mini file manager built into the game. But we’re just learning the ropes here, so we’ll do it the simple way to get it working. We use the FileExists() and DeleteFile() methods to get rid of the old save file:

[code]
const string filename = directory + “\savegame.dat”;
if (storage.FileExists(filename))
storage.DeleteFile(filename);
[/code]

Now we’re ready to create a new savegame file and write data to it. This is done with the IsolatedStorageFileStream() class:

[code]
var fstream = new IsolatedStorageFileStream(
filename, FileMode.CreateNew, storage);
[/code]

The FileMode enumeration has these values:

  • CreateNew = 1
  • Create = 2
  • Open = 3
  • OpenOrCreate = 4
  • Truncate = 5
  • Append = 6

Writing Data to the File with Serialization

Although any type of data file can be created, XML is quite easy to use, and an entire class or struct variable (full of game data) can be written to the file with only a couple lines of code. If you want to just write binary or text data to the file, that will work also at this point, but it’s so much easier to use serialization! Here is a simple struct we can use for this example:

[code]
public struct SaveGameData
{
public string Name;
public int Score;
}
[/code]

A new SaveGameData variable is created and the two properties are filled with data. This is where you would store actual game data in the properties in order to restore the game to this gameplay state later when the savegame file is loaded:

[code]
savedata = new SaveGameData();
savedata.Name = “John Doe”;
savedata.Score = rand.Next(500, 5000);
[/code]

Now, to write the data to the file, we have to create an XmlSerializer object, and then write the serialized object out to the file:

[code]
XmlSerializer serializer = new XmlSerializer(typeof(SaveGameData));
serializer.Serialize(fstream, savedata);
[/code]

At this point, the file has been created and data has been written to it that was contained in the savedata struct variable.

Loading a Data File

Loading a serialized XML file is very similar to the writing process. Of course, you may read a simple text or binary file and parse the data if that is more suitable for your needs, but I’m using serialization and XML because it’s so easy and likely to be the approach most game developers take with WP7 savegame data. The same storage object is created, but we don’t need any of the code to create a directory or delete the existing file (obviously), so the code to load the savegame file is much simpler:

[code]
var storage = IsolatedStorageFile.GetUserStoreForApplication();
[/code]

Likewise, the IsolatedStorageFileStream object is created in the same way:

[code]
var fstream = new IsolatedStorageFileStream(
filename, FileMode.CreateNew, storage);
[/code]

There is a second way to create the fstream file object variable: by creating the object in a using statement and then adding code that uses the object in the bracketed code block:
[code]
using (var fstream = new IsolatedStorageFileStream(
filename, FileMode.Open, storage)) { }
[/code]

The XmlSerializer object is created in a similar manner:

[code]
XmlSerializer serializer = new XmlSerializer(typeof(SaveGameData));
[/code]

The only difference really is a call to Deserialize() instead of Serialize(), and this method returns our savegame data as an object:

[code]
data = (SaveGameData)serializer.Deserialize(fstream);
[/code]

Just for curiosity’s sake, here is what the XML file looks like that is created by our code. If you were to serialize a more complex data type, like a Vector4, then the parameters within that class or struct would become sub-items in the XML structure.

[code]
<?xml version=”1.0”?>
<SaveGameData xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
xmlns:xsd=”http://www.w3.org/2001/XMLSchema”>
<Name>John Doe</Name>
<Score>1245</Score>
</SaveGameData>
[/code]

Creating the Storage Demo Example

We will go over a complete program that demonstrates how to save data to a save game file and then load it again, based on some rudimentary user input. Two buttons are created and displayed using our familiar Button class (which inherits from Sprite). This class requires a bitmap file called button.png, so be sure that it exists in the content project.

To verify that the example is working, we will want to run the program, save the data, close the program, and then rerun it and choose the load option to see that the data is still there. So, the example should read and write the data only when the user chooses to, not automatically. When the emulator is being used, exiting the program still preserves it in memory, but closing the emulator will erase all traces of the program and data files.

Closing the WP7 emulator will wipe the storage memory, including data files created by our example here, and any programs previously loaded from Visual Studio. But closing the program and rerunning it will reveal an intact file system. This happens because the emulator creates a new emulation state system when it is run, and that is not saved when it closes.

Figure 19.2 shows the output of the Storage Demo program.

The Storage Demo example shows how to read and write data.
FIGURE 19.2 The Storage Demo example shows how to read and write data.

Button Class

Just for the sake of clarity, Listing 19.1 shows the source code for the Button class. We have seen the code before, but it is required by the Storage Demo and is included again for clarity.

LISTING 19.1 Source Code for the Button Class

[code]
public class Button : Sprite
{
public string text;
private SpriteBatch p_spriteBatch;
private SpriteFont p_font;
public Button(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch)
{
p_spriteBatch = spriteBatch;
p_font = font;
Load(“button”);
text = ““;
color = Color.LightGreen;
}
public void Draw()
{
base.Draw();
Vector2 size = p_font.MeasureString(text);
Vector2 pos = position;
pos.X -= size.X / 2;
pos.Y -= size.Y / 2;
p_spriteBatch.DrawString(p_font, text, pos, color);
}
public bool Tapped(Vector2 pos)
{
Rectangle rect = new Rectangle((int)pos.X, (int)pos.Y, 1, 1);
return Boundary().Intersects(rect);
}
}
[/code]

Storage Demo Source

Here in Listing 19.2, we have the source code for the Storage Demo program, with the definition of the SaveGameData class as well.

LISTING 19.2 Source Code for the Storage Demo Program

[code]
public struct SaveGameData
{
public string Name;
public int Score;
}
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont font;
Random rand;
TouchLocation oldTouch;
Button[] buttons;
int current = -1;
bool loaded = false;
SaveGameData savedata;
const string directory = “StorageDemo”;
const string filename = directory + “\savegame.dat”;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
rand = new Random();
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
//create save button
buttons = new Button[2];
buttons[0] = new Button(Content, spriteBatch, font);
buttons[0].text = “Save”;
buttons[0].position = new Vector2(100, 100);
//create load button
buttons[1] = new Button(Content, spriteBatch, font);
buttons[1].text = “Load”;
buttons[1].position = new Vector2(300, 100);
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
//get state of touch input
TouchCollection touchInput = TouchPanel.GetState();
if (touchInput.Count > 0)
{
TouchLocation touch = touchInput[0];
if (touch.State == TouchLocationState.Pressed)
{
current = -1;
int n = 0;
foreach (Button b in buttons)
{
int x = (int)touch.Position.X;
int y = (int)touch.Position.Y;
if (b.Boundary().Contains(x, y))
{
current = n;
break;
}
n++;
}
}
oldTouch = touch;
}
if (current == 0)
{
savedata = new SaveGameData();
savedata.Name = “John Doe”;
savedata.Score = rand.Next(500, 5000);
SaveData(savedata);
loaded = false;
current = -1;
}
else if (current == 1)
{
savedata = LoadData();
loaded = true;
current = -1;
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin(SpriteSortMode.FrontToBack,
BlendState.AlphaBlend);
print(0, 0, “Storage Demo”, Color.White);
foreach (Button b in buttons)
b.Draw();
if (loaded)
{
print(100, 200, “Loaded data:nn” +
“Name: “ + savedata.Name + “n” +
“Score: “ + savedata.Score.ToString() + “n”,
Color.White);
}
spriteBatch.End();
base.Draw(gameTime);
}
void print(int x, int y, string text, Color color)
{
var pos = new Vector2((float)x, (float)y);
spriteBatch.DrawString(font, text, pos, color);
}
private void SaveData(SaveGameData data)
{
var storage = IsolatedStorageFile.GetUserStoreForApplication();
//create directory for data
if (!storage.DirectoryExists(directory))
storage.CreateDirectory(directory);
//delete any existing file
if (storage.FileExists(filename))
storage.DeleteFile(filename);
//create new savegame file
using (var fstream = new IsolatedStorageFileStream(filename,
FileMode.CreateNew, storage))
{
XmlSerializer serializer = new XmlSerializer(
typeof(SaveGameData));
serializer.Serialize(fstream, data);
}
}
private SaveGameData LoadData()
{
SaveGameData data;
var storage = IsolatedStorageFile.GetUserStoreForApplication();
using (var fstream = new IsolatedStorageFileStream(filename,
FileMode.Open, storage))
{
XmlSerializer serializer = new XmlSerializer(
typeof(SaveGameData));
data = (SaveGameData)serializer.Deserialize(fstream);
}
return data;
}
}
[/code]

We now have the ability to create a savegame file and load it again! This greatly enhances the replay value of a game that would otherwise appear to have been freshly installed every time it is run. Use this feature to store game settings, player names, and high score lists, as well as generated game levels and anything else that needs to be remembered by the game for the next time.

Sound Recorder (Saving Audio Files & Playing Sound Backward)

Sound Recorder enables you to record, manage, and play audio clips. It is named after the Sound Recorder program on Windows and reminiscent of the Voice Memos app on the iPhone. It can come in quite handy when you’re away from a computer and have some thoughts that you don’t want to forget, especially because it enables you to pause in the midst of a single recording.

Control your recording with simple (and large!) record, pause, and stop buttons. Rename or delete previous recordings one-by-one, or bulk-delete unwanted recordings with a check box mechanism matching the one used by the builtin Mail app. When playing a recording, you can adjust the playback speed, pause it, adjust the playback position on an interactive slider, and even reverse the sound!

The idea of adjusting the speed is that you can listen to recorded thoughts or a lecture much faster than the words were originally spoken. Playing the audio back at a faster rate can help you be more productive.

But why would you want to play recorded words backward? Laura Foy from Microsoft’s Channel 9 has theorized that it’s to “find out if you’re secretly sending satanic messages” (see http://bit.ly/laurafoy), but my real motivation is to enable people to play the nerdy game my brother and I used to play as kids. Here’s how you play:

  1. Record yourself saying a word or phrase.
  2. Play it backwards many times, so you can try to memorize what it sounds like backward.
  3. Make a new recording with you mimicking the backward audio.
  4. Play this new recording backward to see how close you can come to replicating the original word or phrase.

We used to play this game with Sound Recorder on Windows (the good version of the program, prior to Windows Vista). Now you can play it anytime and anywhere with Sound Recorder on your Windows Phone! You’ll be surprised by the sounds you have to make to produce a good result!

As far as interaction with the microphone is concerned, Sound Recorder is simpler than Talking Parrot because it doesn’t need to automatically determine when to start and stop collecting data. This app requires a lot more code, however, for managing the audio that it does capture.

Sound Recorder contains three pages in addition to its about page: the main page, which does all the recording; the list page, which shows past recordings; and the details page, which handles playback and editing.

The Main Page

The main page, shown at the beginning of this chapter, has three basic states: stopped, recording, and paused. Figure 36.1 demonstrates all three.

The three possible states of the main page.
FIGURE 36.1 The three possible states of the main page.

The four buttons (shown two at a time) mimic application bar buttons but are significantly bigger. Using real application bar buttons would be fine (and easier to implement), but this makes the buttons a little easier to press when the user is in a hurry.

This page’s user interface, with its photograph and Volume Units (VU) meter, does a bad job of following the design guideline that Windows Phone apps should be “authentically digital.” Showing a digital display similar to the phone’s voice recognition overlay would fit in better. However, sometimes violating guidelines can help your app stand out in a positive way.

The User Interface

Listing 36.1 contains the XAML for the main page. It consists of two images, four custom buttons, a line for the VU meter needle, and a text block for displaying the elapsed time.

LISTING 36.1 MainPage.xaml—The User Interface for Sound Recorder’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”>
<Canvas>
<!– The on-air image –>
<Image Source=”Images/background.png”/>
<!– The off-air image –>
<Image x:Name=”OffAirImage” Source=”Images/offAir.png”/>
<!– The large buttons: 2 in the same left spot, 2 in the same right spot –>
<local:ImageButton x:Name=”RecordButton” Click=”RecordButton_Click”
Text=”record” Canvas.Left=”16” Canvas.Top=”586”
Source=”../../Images/RecordButton.png”
PressedSource=”../../Images/RecordButtonPressed.png”/>
<local:ImageButton x:Name=”PauseButton” Click=”PauseButton_Click”
Text=”pause” Canvas.Left=”16” Canvas.Top=”586”
Source=”../../Images/PauseButton.png”
PressedSource=”../../Images/PauseButtonPressed.png”
Visibility=”Collapsed”/>
<local:ImageButton x:Name=”ListButton” Click=”ListButton_Click”
Text=”list” Canvas.Left=”371” Canvas.Top=”586”
Source=”../../Images/ListButton.png”
PressedSource=”../../Images/ListButtonPressed.png”/>
<local:ImageButton x:Name=”StopButton” Click=”StopButton_Click”
Text=”stop” Canvas.Left=”371” Canvas.Top=”586”
Source=”../../Images/StopButton.png”
PressedSource=”../../Images/StopButtonPressed.png”
Visibility=”Collapsed”/>
<!– The needle for the sound meter –>
<Line Canvas.Left=”240” Canvas.Top=”590” Width=”3” Height=”110” Y2=”110”
Stroke=”Black” StrokeThickness=”3” StrokeStartLineCap=”Triangle”
RenderTransformOrigin=”.5,1”>
<Line.RenderTransform>
<RotateTransform x:Name=”NeedleTransform” Angle=”-55”/>
</Line.RenderTransform>
</Line>
<!– The elapsed time –>
<TextBlock x:Name=”TimerTextBlock” Canvas.Top=”512” Width=”480”
TextAlignment=”Center” Style=”{StaticResource PhoneTextExtraLargeStyle}”
Foreground=”White” Visibility=”Collapsed”/>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

  • The page is portrait-only due to the dimensions of the artwork and the exact layout of the controls surrounding it.
  • The second image, OffAirImage, is shown during the stopped and paused states. It covers the illuminated “on air” sign with one that is off, and it also dims the VU meter (whose needle still moves in response to sound). It accomplishes the dimming with a translucent region in the image, as demonstrated in Figure 36.2.
  • The four buttons are instances of a simple user control called ImageButton that is included with this app’s source code. Rather than doing tricks with opacity masks to get the inverted-colors-when-pressed effect, this control simply asks for two separate image files. It displays PressedSource when pressed; otherwise it displays Source.
  • Because background.png (with fixed colors) fills the page, the text and images in this page all use a hard-coded white color.
  • The VU meter needle is implemented as a Line element whose RotateTransform is manipulated from code behind. Its RenderTransformOrigin of .5,1 rotates it around its bottom edge. It is positioned with Canvas.Left and Canvas.Top rather than solely with its X1 and Y1 properties—and it is given an explicit width and height—to make the transform work more understandably.
The overlay image replaces the photo and dims the sound meter.
FIGURE 36.2 The overlay image replaces the photo and dims the sound meter.

The Code-Behind

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

LISTING 36.2 MainPage.xaml.cs—The Code-Behind for Sound Recorder’s Main Page

[code]

using System;
using System.IO;
using System.Windows;
using System.Windows.Media;
using Microsoft.Phone.Controls;
using Microsoft.Xna.Framework.Audio;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Used for capturing audio from the microphone
byte[] buffer;
MemoryStream stream;
// Needle management
double targetNeedleAngle;
const int MIN_ANGLE = -55;
const int MAX_ANGLE = 55;
const int VELOCITY_FACTOR = 10;
const int DOWNWARD_VELOCITY = -6;
const int SMALL_ANGLE_DELTA = 6;
const int RANGE_FACTOR = 20;
// The current state (Stopped, Recording, or Paused)
AudioState currentState = AudioState.Stopped;
public MainPage()
{
InitializeComponent();
CompositionTarget.Rendering += CompositionTarget_Rendering;
// Required for XNA Microphone API to work
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
// 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.stream = new MemoryStream();
// Listen the whole time so the needle moves even when not recording
Microphone.Default.Start();
}
void Microphone_BufferReady(object sender, EventArgs e)
{
int size = Microphone.Default.GetData(this.buffer);
if (size == 0)
return;
// Calculate the target angle for the volume meter needle
long volume = GetAverageVolume(size);
double range = Math.Min(MAX_ANGLE – MIN_ANGLE, volume / RANGE_FACTOR);
this.targetNeedleAngle = MIN_ANGLE + range;
if (CurrentState == AudioState.Recording)
{
// If recording, write the current buffer to the stream and
// refresh the elapsed time
this.stream.Write(this.buffer, 0, size);
TimeSpan recordingLength = Microphone.Default.GetSampleDuration(
(int)this.stream.Position);
this.TimerTextBlock.Text = String.Format(“{0:00}:{1:00}”,
recordingLength.Minutes, recordingLength.Seconds);
}
}
void CompositionTarget_Rendering(object sender, EventArgs e)
{
// Required for XNA Microphone API to work
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
double newAngle = this.targetNeedleAngle;
double delta = this.targetNeedleAngle – this.NeedleTransform.Angle;
// If the difference is larger than SMALL_ANGLE_DELTA°, gradually move the
// needle rather than directly setting its angle to the target angle
if (Math.Abs(delta) > SMALL_ANGLE_DELTA)
{
// Limit the downward velocity, so it returns to the
// resting position at a constant rate (DOWNWARD_VELOCITY)
newAngle = this.NeedleTransform.Angle +
Math.Max(delta / VELOCITY_FACTOR, DOWNWARD_VELOCITY);
}
// Update the needle’s angle, restricting it
// to a range of MIN_ANGLE° to MAX_ANGLE°
this.NeedleTransform.Angle =
Math.Max(MIN_ANGLE, Math.Min(MAX_ANGLE, newAngle));
}
// Returns the average value among all the values in the buffer
int GetAverageVolume(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(this.buffer, i));
total += value;
}
return (int)(total / (numBytes / 2));
}
AudioState CurrentState
{
get { return this.currentState; }
set
{
this.currentState = value;
// Not pretty code, but shorter than the alternatives
switch (this.currentState)
{
case AudioState.Recording:
RecordButton.Visibility = Visibility.Collapsed;
ListButton.Visibility = Visibility.Collapsed;
OffAirImage.Visibility = Visibility.Collapsed;
PauseButton.Visibility = Visibility.Visible;
StopButton.Visibility = Visibility.Visible;
TimerTextBlock.Text = “”;
TimerTextBlock.Visibility = Visibility.Visible;
break;
case AudioState.Paused:
RecordButton.Visibility = Visibility.Visible;
OffAirImage.Visibility = Visibility.Visible;
PauseButton.Visibility = Visibility.Collapsed;
TimerTextBlock.Text += “ (paused)”;
break;
case AudioState.Stopped:
RecordButton.Visibility = Visibility.Visible;
ListButton.Visibility = Visibility.Visible;
OffAirImage.Visibility = Visibility.Visible;
PauseButton.Visibility = Visibility.Collapsed;
StopButton.Visibility = Visibility.Collapsed;
TimerTextBlock.Visibility = Visibility.Collapsed;
break;
}
}
}
// Button click handlers
void RecordButton_Click(object sender, EventArgs e)
{
CurrentState = AudioState.Recording;
}
void ListButton_Click(object sender, EventArgs e)
{
CurrentState = AudioState.Stopped;
this.NavigationService.Navigate(
new Uri(“/ListPage.xaml”, UriKind.Relative));
}
void PauseButton_Click(object sender, EventArgs e)
{
CurrentState = AudioState.Paused;
}
void StopButton_Click(object sender, EventArgs e)
{
CurrentState = AudioState.Stopped;
// Create a new recording with a unique filename
Recording r = new Recording { Filename = Guid.NewGuid().ToString(),
TimeStamp = DateTimeOffset.Now };
// Save the recording
r.SaveContent(this.stream);
// Ready the stream for another recording
this.stream.Position = 0;
// Add the recording to the persisted list
Settings.RecordingsList.Value.Add(r);
}
}
}

[/code]

  • The structure of this code is pretty similar to the preceding two chapters. In this app, the microphone is started from the constructor to enable the VU meter needle to move at all times, not just while recording is in progress. During development, ensure that your application manifest contains the ID_CAP_MICROPHONE capability, otherwise the call to Microsoft.Default.Start will fail due to Microphone.Default being null.
  • In the microphone’s BufferReady event handler, the buffer is written to the stream, but only if we’re recording. (The three states of the page are indicated by the threevalue AudioState enumeration.) The average volume of the sample is used to determine where the needle should be placed, transforming the value to a range from MIN_ANGLE (-55°) to MAX_ANGLE (55°). The GetAverageVolume method is identical to the one from the preceding two chapters.
  • CompositionTarget_Rendering not only performs the requisite call to FrameworkDispatcher.Update, but it also takes the opportunity to adjust the needle based on the value of targetNeedleAngle calculated in Microphone_BufferReady. If the difference between the current angle and the target angle is small enough, the needle is directly moved to the target angle. Otherwise, the needle is moved a fraction of the necessary distance each time CompositionTarget_Rendering is called to provide a smooth animation. The speed of “downward” motion (decreasing the angle due to softer audio) is limited to DOWNWARD_VELOCITY to provide a more realistic effect of a needle that can jump to a louder volume but always smoothly returns to its resting position. This isn’t meant to be an accurate simulation of VU meter ballistics, but it should look good enough to most people.
  • CurrentState’s property setter updates the user interface the old-fashioned way; by touching several properties on several elements. It would look more satisfactory if the relevant elements were data-bound to a version of CurrentState that is either a dependency property or a property that manually raises change notifications. However, several value converters would be needed to morph the enumeration value into the variety of Visibility values and strings needed. The end result would involve much more code.
  • The code that actually saves the audio data is at the end of the listing in StopButton_Click. To do this, the memory stream holding the data is passed to SaveContent on a custom Recording class shown in the next listing.
  • This app uses two persisted settings, defined as follows in a separate Settings.cs file:

    [code]

    public static class Settings
    {
    // The user’s data
    public static readonly Setting<ObservableCollection<Recording>>
    RecordingsList =
    new Setting<ObservableCollection<Recording>>(“RecordingsList”,
    new ObservableCollection<Recording>());
    // Communicate the selection on the list page to the details page
    public static readonly Setting<int> SelectedRecordingIndex =
    new Setting<int>(“SelectedRecordingIndex”, -1);
    }
    [/code]

LISTING 36.3 Recording.cs—The Object Representing Each Sound File Stored in Isolated Storage

[code]

using System;
using System.ComponentModel;
using System.IO;
using System.IO.IsolatedStorage;
using Microsoft.Xna.Framework.Audio;
namespace WindowsPhoneApp
{
public class Recording : INotifyPropertyChanged
{
// The backing fields
string filename;
string label;
DateTimeOffset timeStamp;
TimeSpan duration;
// The properties, which raise change notifications
public string Filename
{
get { return this.filename; }
set { this.filename = value; OnPropertyChanged(“Filename”); }
}
public string Label
{
get { return this.label; }
set { this.label = value; OnPropertyChanged(“Label”);
// Raise notifications for the readonly properties based on Label
OnPropertyChanged(“Title”); OnPropertyChanged(“ShortTitle”);
OnPropertyChanged(“Subtitle”); }
}
public DateTimeOffset TimeStamp
{
get { return this.timeStamp; }
set { this.timeStamp = value; OnPropertyChanged(“TimeStamp”);
// Raise notifications for the readonly properties based on TimeStamp
OnPropertyChanged(“Title”); OnPropertyChanged(“ShortTitle”);
OnPropertyChanged(“Subtitle”); }
}
public TimeSpan Duration
{
get { return this.duration; }
set { this.duration = value; OnPropertyChanged(“Duration”);
// Raise notifications for the readonly properties based on Duration
OnPropertyChanged(“Title”); OnPropertyChanged(“ShortTitle”);
OnPropertyChanged(“Subtitle”); }
}
// A few computed properties for display purposes
public string Title
{
get {
return String.Format(“{0} ({1:00}:{2:00})”,
this.label ?? this.TimeStamp.LocalDateTime.ToShortTimeString(),
this.Duration.Minutes, Math.Floor(this.Duration.Seconds));
}
}
public string ShortTitle
{
get {
return this.label ?? this.TimeStamp.LocalDateTime.ToShortTimeString();
}
}
public string Subtitle
{
get {
if (this.label != null)
return String.Format(“{0} {1}”,
this.TimeStamp.LocalDateTime.ToShortDateString(),
this.TimeStamp.LocalDateTime.ToShortTimeString());
else
return this.TimeStamp.LocalDateTime.ToShortDateString();
}
}
// Save the stream to isolated storage
public void SaveContent(MemoryStream memoryStream)
{
// Store the duration of the content, used for display purposes
this.Duration = Microphone.Default.GetSampleDuration(
(int)memoryStream.Position);
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream =
userStore.CreateFile(this.Filename))
{
stream.Write(memoryStream.GetBuffer(), 0, (int)memoryStream.Position);
}
}
// Get the raw bytes from the file in isolated storage
byte[] GetBuffer()
{
byte[] buffer =
new byte[Microphone.Default.GetSampleSizeInBytes(this.Duration)];
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream =
userStore.OpenFile(this.Filename, FileMode.Open))
{
stream.Read(buffer, 0, buffer.Length);
}
return buffer;
}
// Create and return a sound effect based on the raw bytes in the file
public SoundEffect GetContent()
{
return new SoundEffect(this.GetBuffer(), Microphone.Default.SampleRate,
AudioChannels.Mono);
}
// Delete the file
public void DeleteContent()
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
userStore.DeleteFile(this.Filename);
}
// Overwrite the file’s contents with the audio data reversed
public void Reverse()
{
byte[] buffer = this.GetBuffer();
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream =
userStore.OpenFile(this.Filename, FileMode.Open, FileAccess.Write))
{
// Reverse each 2-byte chunk (each 16-bit audio sample)
for (int i = buffer.Length – 2; i >= 0; i -= 2)
stream.Write(buffer, i, 2);
}
}
void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
}

[/code]

  • Any changes to Label, TimeStamp, or Duration also raise property-changed notifications for Title, Subtitle, and ShortTitle, three readonly properties whose value is based on these read/write properties. The list page leverages Title and Subtitle in the display for each recording, and the details page leverages ShortTitle.
  • The implementation of SaveContent is a straightforward writing of the passed-in stream’s bytes to the isolated storage file indicated by the Filename property. This method also automatically sets Duration to the length of the recording (revealed by the microphone) so this information can be leveraged in other parts of the app without having to load the audio file. This is especially important for the list page, which displays the duration for every recording simultaneously.
  • When GetBuffer reads in the data from isolated storage, it knows how big the buffer needs to be ahead of time, thanks to the stored duration (and thanks to the microphone’s GetSampleSizeInBytes method). GetBuffer is not public; consumers instead call GetContent, which returns the audio data as a familiar SoundEffect object.
  • DeleteContent deletes the file containing the audio data, just like the same-named method from the Notepad app.
  • The Reverse method does the trick of reversing the audio file. It’s simply a matter of reversing the bytes, except that this needs to be done in 2-byte chunks to keep each 16-bit audio sample intact.

The List Page

The list page, shown in Figure 36.3, contains a list box with recordings that link to the details page. However, this is not a regular list box—it is a custom subclass called CheckableListBox that mimics the Mail app’s mechanism for bulk-selecting items. To enter bulk-selection mode, you can either tap the application bar button or tap the leftmost edge of any item in the list. The latter approach has the advantage of automatically selecting the tapped item. The code to CheckableListBox is not covered in this chapter, but it is included with this chapter’s source code.

The CheckableListBox supports multi-select interaction the same way as the phone’s built-in Mail app.
FIGURE 36.3 The CheckableListBox supports multi-select interaction the same way as the phone’s built-in Mail app.

The only thing you can do with bulk-selected items is delete them. Notice in Figure 36.3 that the application bar changes to show a delete button at the same time that the check boxes appear.

The XAML for the list page is shown in Listing 36.4, and the code-behind is shown in Listing 36.5.

LISTING 36.4 ListPage.xaml—The User Interface for Sound Recorder’s List of Recordings

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.ListPage”
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 one button and one menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”select” Click=”SelectButton_Click”
IconUri=”/Shared/Images/appbar.select.png”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”SOUND RECORDER”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”recordings” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<TextBlock x:Name=”NoItemsTextBlock” Grid.Row=”1” Text=”No recordings”
Visibility=”Collapsed” Margin=”22,17,0,0”
Style=”{StaticResource PhoneTextGroupHeaderStyle}”/>
<!– A list box supporting check boxes for bulk selection –>
<local:CheckableListBox x:Name=”CheckableListBox” Grid.Row=”1”
Margin=”0,18,0,0”
SelectionMode=”Multiple” ItemsSource=”{Binding}”
SelectionChanged=”ListBox_SelectionChanged”>
<local:CheckableListBox.ItemTemplate>
<DataTemplate>
<!– Give each recording two lines: a title and a subtitle –>
<StackPanel>
<TextBlock Text=”{Binding Title}” Margin=”-2,-13,0,0”
Style=”{StaticResource PhoneTextExtraLargeStyle}”/>
<TextBlock Text=”{Binding Subtitle}” Margin=”0,-5,0,28”
Style=”{StaticResource PhoneTextSubtleStyle}”/>
</StackPanel>
</DataTemplate>
</local:CheckableListBox.ItemTemplate>
</local:CheckableListBox>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 36.5 ListPage.xaml.cs—The Code-Behind for Sound Recorder’s List of Recordings

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class ListPage : PhoneApplicationPage
{
bool inSelectMode;
public ListPage()
{
InitializeComponent();
// Assign the data source for the list box
this.DataContext = Settings.RecordingsList.Value;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (Settings.RecordingsList.Value.Count == 0)
ShowListAsEmpty();
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
// The Back button should exit select mode
if (this.inSelectMode)
{
e.Cancel = true;
LeaveSelectMode();
}
}
void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (this.CheckableListBox.SelectedItems.Count == 1 &&
!this.CheckableListBox.AreCheckBoxesShowing)
{
// This is a normal, single selection, so navigate to the details page
Settings.SelectedRecordingIndex.Value =
this.CheckableListBox.SelectedIndex;
this.NavigationService.Navigate(
new Uri(“/DetailsPage.xaml”, UriKind.Relative));
// Clear the selection for next time
this.CheckableListBox.SelectedIndex = -1;
}
else if (this.CheckableListBox.AreCheckBoxesShowing && !this.inSelectMode)
this.EnterSelectMode();
else if (!this.CheckableListBox.AreCheckBoxesShowing && this.inSelectMode)
this.LeaveSelectMode();
if (this.inSelectMode)
(this.ApplicationBar.Buttons[0] as IApplicationBarIconButton).IsEnabled =
(this.CheckableListBox.SelectedItems.Count > 0);
}
void ShowListAsEmpty()
{
NoItemsTextBlock.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = false;
}
void EnterSelectMode()
{
// Show the check boxes
this.CheckableListBox.ShowCheckBoxes();
// Clear the application bar and show a delete button
this.ApplicationBar.Buttons.Clear();
ApplicationBarIconButton deleteButton = new ApplicationBarIconButton(
new Uri(“/Shared/Images/appbar.delete.png”, UriKind.Relative));
deleteButton.Text = “delete”;
deleteButton.IsEnabled = false; // Will be enabled when >=1 item selected
deleteButton.Click += DeleteButton_Click;
this.ApplicationBar.Buttons.Add(deleteButton);
this.inSelectMode = true;
}
void LeaveSelectMode()
{
// Hide the check boxes
if (this.CheckableListBox.AreCheckBoxesShowing)
this.CheckableListBox.HideCheckBoxes();
// Clear the application bar and show a select button
this.ApplicationBar.Buttons.Clear();
ApplicationBarIconButton button = new ApplicationBarIconButton(
new Uri(“/Shared/Images/appbar.select.png”, UriKind.Relative));
button.Text = “select”;
button.Click += SelectButton_Click;
this.ApplicationBar.Buttons.Add(button);
this.inSelectMode = false;
}
// Application bar handlers
void SelectButton_Click(object sender, EventArgs e)
{
EnterSelectMode();
}
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to delete “ +
(this.CheckableListBox.SelectedItems.Count > 1 ? “these recordings” :
“this recording”) + “?”, “Delete recording” +
(this.CheckableListBox.SelectedItems.Count > 1 ? “s” : “”) + “?”,
MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
Recording[] itemsToDelete =
new Recording[this.CheckableListBox.SelectedItems.Count];
this.CheckableListBox.SelectedItems.CopyTo(itemsToDelete, 0);
this.CheckableListBox.SelectedIndex = -1;
this.LeaveSelectMode();
for (int i = 0; i < itemsToDelete.Length; i++)
{
// Remove it from the list
Settings.RecordingsList.Value.Remove(itemsToDelete[i]);
// Delete the audio file in isolated storage
itemsToDelete[i].DeleteContent();
}
if (Settings.RecordingsList.Value.Count == 0)
ShowListAsEmpty();
}
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Sound Recorder”, UriKind.Relative));
}
}
}

[/code]

  • The checkable list box data-binds to the RecordingsList setting and stays up-todate thanks to the observable collection and property-changed notifications from each item. Its SelectionMode property, inherited from the base list box control, is set to Multiple to enable bulk-selection to work.
  • Tapping an item navigates to the details page filled out for that item. This is communicated via the SelectedRecordingIndex setting that is set appropriately before navigating.
  • When an item is deleted, the code makes sure to call DeleteContent in addition to removing the Recording instance from the list. Otherwise, the audio file would be left behind in isolated storage.
  • The rest of the code manages the CheckableListBox. Bulk-selection mode (just called select mode in the listing) can be triggered either by tapping an item’s left margin (handled internally in code for CheckableListBoxItem) or by tapping the “select” button on the application bar. As in the Mail app, bulk-selection mode can be exited either by pressing the hardware Back button or by unchecking all of the check boxes.

The Details Page

The details page contains several features in addition to playing the selected recording.
FIGURE 36.4 The details page contains several features in addition to playing the selected recording.

The details page, shown in Figure 36.4 with its application bar expanded, enables playback, editing, and deletion of the selected sound.

The XAML for this page is shown in Listing 36.6, and the code-behind is shown in Listing 36.7.

LISTING 36.6 DetailsPage.xaml—The User Interface for Sound Recorder’s 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 3 buttons and 2 menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”pause”
IconUri=”/Shared/Images/appbar.pause.png” Click=”PlayPauseButton_Click”/>
<shell:ApplicationBarIconButton Text=”edit name”
IconUri=”/Shared/Images/appbar.edit.png” Click=”EditButton_Click”/>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png” Click=”DeleteButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”reverse”
Click=”ReverseMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock x:Name=”ApplicationTitle” Text=”SOUND RECORDER”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”{Binding ShortTitle}”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<!– The playback slider –>
<TextBlock x:Name=”PlaybackDurationTextBlock” Grid.Row=”1”
Foreground=”{StaticResource PhoneSubtleBrush}” Margin=”12,58,0,0”/>
<Slider x:Name=”PlaybackSlider” SmallChange=”.1” Grid.Row=”1”
Margin=”0,24,0,84”/>
<!– The playback speed slider with its reset button –>
<Grid Grid.Row=”2”>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width=”Auto”/>
</Grid.ColumnDefinitions>
<TextBlock Text=”Playback Speed” Grid.ColumnSpan=”2” Margin=”12,0,0,0”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<Slider x:Name=”SpeedSlider” Grid.Row=”1” SmallChange=”.1” LargeChange=”.1”
Minimum=”-1” Maximum=”1” Margin=”0,18,0,0”
ValueChanged=”SpeedSlider_ValueChanged”/>
<Button Grid.Row=”1” Grid.Column=”1” Content=”reset” Margin=”0,0,0,16”
VerticalAlignment=”Center” local:Tilt.IsEnabled=”True”
Click=”SpeedResetButton_Click”/>
</Grid>
<!– The “edit name” dialog –>
<local:Dialog x:Name=”EditDialog” Grid.RowSpan=”3” Closed=”EditDialog_Closed”>
<local:Dialog.InnerContent>
<StackPanel>
<TextBlock Text=”Choose a name” Margin=”11,5,0,-5”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBox Text=”{Binding Result, Mode=TwoWay}” InputScope=”Text”/>
</StackPanel>
</local:Dialog.InnerContent>
</local:Dialog>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 36.7 DetailsPage.xaml.cs—The Code-Behind for Sound Recorder’s Details Page

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using Microsoft.Xna.Framework.Audio;
namespace WindowsPhoneApp
{
public partial class DetailsPage : PhoneApplicationPage
{
Recording selectedRecording;
SoundEffectInstance soundInstance;
double durationScale = 1;
double elapsedSeconds;
DateTime lastPlayFrame;
SoundState lastSoundState = SoundState.Stopped;
IApplicationBarIconButton playPauseButton;
public DetailsPage()
{
InitializeComponent();
this.playPauseButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// The recording chosen from the list page
this.selectedRecording =
Settings.RecordingsList.Value[Settings.SelectedRecordingIndex.Value];
// The actual sound effect instance to play
this.soundInstance = this.selectedRecording.GetContent().CreateInstance();
// The page title data-binds to the ShortTitle property
this.DataContext = this.selectedRecording;
// Adjust the playback slider based on the recording’s length
PlaybackSlider.Maximum = this.selectedRecording.Duration.TotalSeconds;
// Start playing automatically
Play();
}
void Play()
{
CompositionTarget.Rendering += CompositionTarget_Rendering;
this.playPauseButton.Text = “pause”;
this.playPauseButton.IconUri = new Uri(“/Shared/Images/appbar.pause.png”,
UriKind.Relative);
this.lastPlayFrame = DateTime.Now;
if (this.soundInstance.State == SoundState.Paused)
this.soundInstance.Resume();
else
{
// Play from the beginning
this.soundInstance.Play();
this.elapsedSeconds = 0;
}
}
void Pause()
{
this.playPauseButton.Text = “play”;
this.playPauseButton.IconUri = new Uri(“/Shared/Images/appbar.play.png”,
UriKind.Relative);
this.soundInstance.Pause();
}
void CompositionTarget_Rendering(object sender, EventArgs e)
{
if (this.soundInstance != null)
{
// Keep the playback slider up-to-date with the playing audio
if (this.soundInstance.State == SoundState.Playing ||
this.lastSoundState == SoundState.Playing
/* So remaining time after pausing/stopping is accounted for */)
{
this.elapsedSeconds +=
(DateTime.Now – lastPlayFrame).TotalSeconds / this.durationScale;
this.lastPlayFrame = DateTime.Now;
this.PlaybackSlider.Value = this.elapsedSeconds;
if (this.soundInstance.State == SoundState.Stopped)
this.PlaybackSlider.Value = this.PlaybackSlider.Maximum;
UpdatePlaybackLabel();
}
// Automatically turn the pause button back into a play button when the
// recording has finished playing
if (this.soundInstance.State != SoundState.Playing &&
this.playPauseButton.Text != “play”)
{
this.playPauseButton.Text = “play”;
this.playPauseButton.IconUri =
new Uri(“/Shared/Images/appbar.play.png”, UriKind.Relative);
// Unhook this event since it gets hooked on each play
CompositionTarget.Rendering -= CompositionTarget_Rendering;
}
this.lastSoundState = this.soundInstance.State;
// Required for XNA sound effect to work
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
}
}
void UpdatePlaybackLabel()
{
TimeSpan elapsedTime =
TimeSpan.FromSeconds(elapsedSeconds * this.durationScale);
TimeSpan scaledDuration =
TimeSpan.FromSeconds(PlaybackSlider.Maximum * this.durationScale);
PlaybackDurationTextBlock.Text = String.Format(“{0:00}:{1:00}”,
elapsedTime.Minutes, Math.Floor(elapsedTime.Seconds)) + “ / “ +
String.Format(“{0:00}:{1:00}”,
scaledDuration.Minutes, Math.Floor(scaledDuration.Seconds));
}
// Speed slider handlers
void SpeedSlider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
// Directly apply the -1 to 1 slider value as the pitch
this.soundInstance.Pitch = (float)SpeedSlider.Value;
// The duration scale used by other calculations ranges from
// .5 for double-speed/half-length (+1 pitch) to
// 2 for half-speed/double-length (-1 pitch)
this.durationScale = 1 + Math.Abs(this.soundInstance.Pitch);
if (this.soundInstance.Pitch > 0)
this.durationScale = 1 / this.durationScale;
UpdatePlaybackLabel();
}
void SpeedResetButton_Click(object sender, RoutedEventArgs e)
{
SpeedSlider.Value = 0;
}
// Handlers related to the “edit name” dialog
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
if (EditDialog.Visibility == Visibility.Visible)
{
e.Cancel = true;
EditDialog.Hide(MessageBoxResult.Cancel);
}
}
void EditDialog_Closed(object sender, MessageBoxResultEventArgs e)
{
this.ApplicationBar.IsVisible = true;
if (e.Result == MessageBoxResult.OK)
{
this.selectedRecording.Label = EditDialog.Result.ToString();
}
}
// Application bar handlers
void PlayPauseButton_Click(object sender, EventArgs e)
{
if (this.soundInstance.State == SoundState.Playing)
this.Pause();
else
this.Play();
}
void EditButton_Click(object sender, EventArgs e)
{
EditDialog.Result = this.selectedRecording.Label;
EditDialog.Show();
this.ApplicationBar.IsVisible = false;
}
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to delete this recording?”,
“Delete recording?”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
// Remove it from the list
Settings.RecordingsList.Value.Remove(this.selectedRecording);
// Delete the audio file in isolated storage
this.selectedRecording.DeleteContent();
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
void ReverseMenuItem_Click(object sender, EventArgs e)
{
this.selectedRecording.Reverse();
// We must get the new, reversed sound effect instance
this.soundInstance = this.selectedRecording.GetContent().CreateInstance();
// Re-apply the chosen pitch
this.soundInstance.Pitch = (float)SpeedSlider.Value;
Play();
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Sound Recorder”, UriKind.Relative));
}
}
}

[/code]

  • All three rows in this page’s root grid are given a height of Auto, so the content doesn’t shift when the application bar is hidden during the display of the “edit name” dialog.
  • The Dialog user control used by several apps in this book is leveraged here to enable the user to rename the recording. This name is applied as the Label property on the Recording instance, which impacts the Title, ShortTitle, and Subtitle properties as shown in Listing 36.3.
  • The CompositionTarget_Rendering handler, used only while the audio is playing, keeps the slider and pause/play button in sync with the audio.
  • The speed slider uses the familiar Pitch property on SoundEffectInstance to adjust the audio as it plays. This affects speed and pitch, as there’s unfortunately no builtin way to adjust the speed while maintaining the pitch.
  • The audio reversal is done inside ReverseMenuItem_Click. Because the reversal is done to the file in isolated storage, the sound effect instance must be retrieved again. Invoking the reversal a second time restores the audio file to its original data.

The Finished Product

Sound Recorder (Saving Audio Files & Playing Sound Backward)

Subservient Cat (Video)

Subservient Cat is a sort of “virtual pet” app. Unlike most cats, the subservient cat can actually obey commands! However, users have to figure out what commands the cat will respond to. It’s a bit of a game, because users must keep guessing to see how many commands they can discover.

This app uses video footage of a black cat (whose real name is Boo) as its primary user interface. Therefore, this is the perfect app for learning about MediaElement, the element for playing video inside apps.

Playing Video with MediaElement

If you want to enable users to play, pause, and otherwise control videos, your best option is to use the Media Player launcher. However, if you want to play video inline on your own page, MediaElement enables you to do so.

MediaElement is a UI element that renders the video specified via its Source property, for example:

[code]

<MediaElement Source=”cat.wmv”/>

[/code]

The source URI can point to a file included in your project or to an online video. By default, MediaElement automatically starts playing the video as soon as the element is rendered (or as soon as enough of the video has been downloaded, in the online case), but you can change this by setting its AutoPlay property to false. MediaElement has Play, Pause, and Stop methods that can be used from code-behind. It also has a Position property that reveals the current playback position (as a time span). If the video supports seeking, you can also set Position to move playback to that point in time. MediaElement can be transformed and clipped just like other Silverlight elements, and it can blended amongst other elements.

On the surface, it appears that MediaElement is straightforward to use. It has tons of caveats, however. Several warnings follow that describe some of the biggest caveats.

An app’s frame can only contain one MediaElement!

Attempting to use more than one MediaElement is not supported and fails in various ways.Note that the limitation is more strict than one per page; only one can be attached to the frame at any time. (It doesn’t matter if they’re stopped, paused, or playing.) Therefore, multiple pages can each have a MediaElement only if they never reside on the navigation stack at the same time. Otherwise, if you need to play multiple videos, you’ll either need to reuse the same MediaElement or remove the unused MediaElement from the element tree.

When including a video file in your app, make sure its Build Action is set to Content, not Resource!

This improves the performance of starting the video. When the video is embedded as a resource, it is first extracted and temporarily saved to isolated storage before it is played! (The same warning applies to audio files played by MediaElement, although this is not normally done.)

When MediaElement starts playing, any background audio (such as music playing from Zune) is paused!

This is the main reason that MediaElement should never be used for playing sound effects. Note that this is true even if your video contains no audio.

MediaElement doesn’t work in the emulator under the light theme!

It’s strange, but true.To test your use of a MediaElement on the emulator, you must ensure that it is running under the dark theme.Don’t worry; this bug doesn’t affect real phones.

MediaElement doesn’t render fully opaque!

If any elements exist underneath a MediaElement, you can see them clearly through the video, even when MediaElement’s Opacity property is set to 1 (as it is by default)! This is an anomaly with the composition between the phone’s media player (which internally renders MediaElement’s video) and the rest of Silverlight.

See “Supported Media Codecs forWindows Phone” (http://goo.gl/6NhuD) for details about the video formats supported by MediaElement and the “Recommended Video Encoding Settings” section of http://goo.gl/ttPkO for more details about what encodings work best. If you use Expression Encoder, you can encode videos with a preconfigured profile optimized for Windows Phone (and Zune HD).

The User Interface

The Subservient Cat app uses a single page—MainPage—in addition to its instructions page (not shown in this chapter). The XAML for MainPage is shown in Listing 33.1. The root grid contains three distinct pieces of user interface, all shown in Figure 33.1:

  • The MediaElement that contains the video
  • A simple “intro screen” that introduces each command done by the cat before the corresponding video clip plays
  • A panel with a text box for guessing new commands
The three main components of the user interface on the main page.
FIGURE 33.1 The three main components of the user interface on the main page.

LISTING 33.1 MainPage.xaml—The User Interface for Subservient Cat’s Main Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Landscape” Orientation=”Landscape”>
<!– The application bar–>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.8”>
<shell:ApplicationBarIconButton Text=”command”
IconUri=”/Shared/Images/appbar.command.png”
Click=”CommandButton_Click”/>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBarIconButton Text=”discovered”
IconUri=”/Shared/Images/appbar.1.png”
Click=”DiscoveredButton_Click”/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<!– The video, scaled to fill the page without visible letterboxing –>
<MediaElement x:Name=”MediaElement” Source=”cat.wmv” Stretch=”None”
Volume=”1” MediaOpened=”MediaElement_MediaOpened”
MediaFailed=”MediaElement_MediaFailed”>
<MediaElement.RenderTransform>
<CompositeTransform TranslateY=”-115” TranslateX=”-150”
ScaleX=”1.68” ScaleY=”1.68”/>
</MediaElement.RenderTransform>
</MediaElement>
<!– Something to show on top of the video during seeking –>
<Border x:Name=”IntroScreen” Background=”#6E5962” Visibility=”Collapsed”>
<Grid>
<Image Source=”Images/paw.png” Stretch=”None” HorizontalAlignment=”Left”
VerticalAlignment=”Top” Margin=”100,100,0,0”/>
<Image Source=”Images/paw.png” Stretch=”None” HorizontalAlignment=”Left”
VerticalAlignment=”Top” Margin=”230,50,0,0”/>
<Image Source=”Images/paw.png” Stretch=”None” HorizontalAlignment=”Left”
VerticalAlignment=”Top” Margin=”628,300,0,0”/>
<Image Source=”Images/paw.png” Stretch=”None” HorizontalAlignment=”Left”
VerticalAlignment=”Top” Margin=”528,350,0,0”/>
<TextBlock x:Name=”NextCommandTextBlock” Foreground=”Black” FontSize=”40”
VerticalAlignment=”Center” HorizontalAlignment=”Center”/>
</Grid>
</Border>
<!– The user interface for typing new commands –>
<Grid x:Name=”CommandPanel”
Background=”#A000”
Visibility=”Collapsed” VerticalAlignment=”Bottom”>
<Grid.RowDefinitions>
<RowDefinition/>
<!– A bottom margin, so the auto-scroll while the textbox has focus
doesn’t show extra space below the video –>
<RowDefinition Height=”84”/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”Auto”/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image Source=”/Shared/Images/appbar.command.png” Stretch=”None”
Margin=”70,0,0,0”/>
<Rectangle Grid.Column=”1” Fill=”White” Margin=”12,12,82,12”/>
<TextBox x:Name=”CommandTextBox” Grid.Column=”1” InputScope=”Text”
Margin=”0,0,70,0” TextChanged=”CommandTextBox_TextChanged”
LostFocus=”CommandTextBox_LostFocus”/>
</Grid>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • The video is landscape-oriented, so this is a landscape-only page. However, because a text box is used for guessing new commands, the code-behind temporarily changes SupportedOrientations to allow any orientation while the text box has focus. This way, phones with portrait hardware keyboards still get a good experience.
  • The application bar has three buttons: one for revealing the command input panel, one for navigating to the instructions page, and one whose icon reveals how many commands have been discovered (updated in code-behind). Tapping this last button also tells you whether there are more commands to be discovered, as the total number is a secret. The application bar menu, dynamically filled from code-behind, contains the list of discovered commands and makes the cat perform each one when tapped. This is shown in Figure 33.2.
The application bar menu provides quick access to the list of already-discovered commands, such as “yawn.”
FIGURE 33.2 The application bar menu provides quick access to the list of already-discovered commands, such as “yawn.”
  • Although the app appears to play several different short videos, it actually uses a single longer video (cat.wmv) for performance reasons. The code-behind is responsible for playing the appropriate segments of the video at the appropriate times.
  • The MediaElement is moved and enlarged with a CompositeTransform because the source cat.wmv file has black bars along the top and bottom that we don’t want to see on the screen.
  • MediaElement’s Volume property (a value from 0 to 1) is set to 1 (the loudest possible volume) because the default value is .85. Although the sound can only be as loud as the user’s volume level, this helps ensure that the tiny bit of audio in this app’s video (a short meow) can be heard.

Be sure to give your MediaElement a name!

If you don’t, it’s possible that the marketplace publishing process won’t detect your use of MediaElement, and therefore will not grant your app the “media library” capability that is necessary for your app to work.

The Code-Behind

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

LISTING 33.2 MainPage.xaml.cs—The Code-Behind for Subservient Cat’s Main Page

[code]

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Navigation;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Captures the slice in the big video where each command resides
class VideoClip
{
public TimeSpan Start;
public TimeSpan End;
}
DispatcherTimer videoTimer = new DispatcherTimer();
DispatcherTimer delayTimer = new DispatcherTimer();
VideoClip pendingNewClip;
Dictionary<string, VideoClip> possibleCommands =
new Dictionary<string, VideoClip>();
Dictionary<string, string> aliases = new Dictionary<string, string>();
// Start users off with one known command: yawn
Setting<List<string>> discoveredCommands =
new Setting<List<string>>(“DiscoveredCommands”,
new List<string>(new string[] { “yawn” }));
IApplicationBarIconButton discoveredButton;
public MainPage()
{
InitializeComponent();
this.discoveredButton = this.ApplicationBar.Buttons[2]
as IApplicationBarIconButton;
this.videoTimer.Tick += VideoTimer_Tick;
this.delayTimer.Tick += DelayTimer_Tick;
// All the commands and their positions in the video
this.possibleCommands.Add(“yawn”, new VideoClip {
Start = TimeSpan.FromSeconds(98.36),
End = TimeSpan.FromSeconds(101.28) });
this.possibleCommands.Add(“meow”, new VideoClip {
Start = TimeSpan.FromSeconds(79.4),
End = TimeSpan.FromSeconds(81.863) });

// Permitted variations for each command
this.aliases.Add(“yawn”, “yawn”);
this.aliases.Add(“meow”, “meow”);
this.aliases.Add(“speak”, “meow”);
this.aliases.Add(“talk”, “meow”);
this.aliases.Add(“say your name”, “meow”);

}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Fill the application bar menu with all previously-discovered commands
this.ApplicationBar.MenuItems.Clear();
foreach (string command in this.discoveredCommands.Value)
{
ApplicationBarMenuItem item = new ApplicationBarMenuItem(command);
item.Click += ApplicationBarMenuItem_Click;
this.ApplicationBar.MenuItems.Add(item);
}
// Show how many commands have been discovered on the button imag
this.discoveredButton.IconUri = new Uri(“/Shared/Images/appbar.”
+ this.discoveredCommands.Value.Count + “.png”, UriKind.Relative);
}
void MediaElement_MediaOpened(object sender, RoutedEventArgs e)
{
// Play a short intro clip
this.videoTimer.Interval = TimeSpan.FromSeconds(1.48);
this.videoTimer.Start();
}
void MediaElement_MediaFailed(object sender, ExceptionRoutedEventArgs e)
{
MessageBox.Show(“To see the subservient cat, please disconnect your “ +
“phone from Zune.”, “Please Disconnect”, MessageBoxButton.OK);
}
void PlayClip(string title, TimeSpan beginTime, TimeSpan endTime)
{
// Set up the timer to stop playback approximately when endTime is reached
this.videoTimer.Stop();
this.videoTimer.Interval = endTime – beginTime;
// Hide the video and show the intro screen
this.MediaElement.Pause();
this.NextCommandTextBlock.Text = title;
this.IntroScreen.Visibility = Visibility.Visible;
// Give the intro screen a chance to show before doing the following work
this.Dispatcher.BeginInvoke(delegate()
{
// Delay the reappearance of the video until after the seek completes
this.delayTimer.Interval = TimeSpan.FromSeconds(2);
this.delayTimer.Start();
// Seek to the correct spot in the video
this.MediaElement.Position = beginTime;
});
}
void VideoTimer_Tick(object sender, EventArgs e)
{
// We’ve reached the end of the current clip, so pause the video
this.MediaElement.Pause();
// Prevent the timer from continuing to tick
this.videoTimer.Stop();
}
void DelayTimer_Tick(object sender, EventArgs e)
{
// This timer is used for two reasons, either to delay the execution of a
// new command after typing it in, or to delay the beginning of playing a
// video clip after the intro screen is shown.
if (this.IntroScreen.Visibility == Visibility.Collapsed)
{
// This is the execution of a new command
string text = this.CommandTextBox.Text;
this.CommandTextBox.Foreground = new SolidColorBrush(Colors.Black);
this.CommandTextBox.Text = “”;
this.Focus(); // Closes the command input panel
PlayClip(text.ToLowerInvariant(), this.pendingNewClip.Start,
this.pendingNewClip.End);
}
else
{
// We’re ready to actually play the video clip
this.videoTimer.Start();
this.MediaElement.Play();
this.IntroScreen.Visibility = Visibility.Collapsed;
}
// Prevent the timer from continuing to tick
this.delayTimer.Stop();
}
void CommandTextBox_LostFocus(object sender, RoutedEventArgs e)
{
// Restore the page to landscape-only and hide the command input panel
this.SupportedOrientations = SupportedPageOrientation.Landscape;
this.CommandPanel.Visibility = Visibility.Collapsed;
}
void CommandTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
string text = this.CommandTextBox.Text.Trim().ToLowerInvariant();
// Don’t bother checking when the text is shorter than any valid command
if (text.Length < 3)
return;
if (this.aliases.ContainsKey(text))
{
string commandName = this.aliases[text];
// Only acknowledge the command if it’s not already in the list
if (!this.discoveredCommands.Value.Contains(text))
{
// Signal a successful guess
this.CommandTextBox.Foreground =
Application.Current.Resources[“PhoneAccentBrush”] as Brush;
this.pendingNewClip = this.possibleCommands[commandName];
// Append the new command to the application bar menu
ApplicationBarMenuItem item = new ApplicationBarMenuItem(text);
item.Click += ApplicationBarMenuItem_Click;
this.ApplicationBar.MenuItems.Add(item);
// Record the discovered command
this.discoveredCommands.Value.Add(text);
this.discoveredButton.IconUri = new Uri(“/Shared/Images/appbar.” +
this.discoveredCommands.Value.Count + “.png”, UriKind.Relative);
// Wait a second before hiding the text box and showing the intro screen
this.delayTimer.Interval = TimeSpan.FromSeconds(1);
this.delayTimer.Start();
}
}
}
// Application bar handlers
void ApplicationBarMenuItem_Click(object sender, EventArgs e)
{
IApplicationBarMenuItem item = sender as IApplicationBarMenuItem;
// Grab the right clip based on the menu item’s text
VideoClip command = this.possibleCommands[this.aliases[item.Text]];
this.Focus(); // In case the command input panel is currently showing
PlayClip(item.Text, command.Start, command.End);
}
void CommandButton_Click(object sender, EventArgs e)
{
// Temporarily allow a portrait orientation while the text box is in use
this.SupportedOrientations = SupportedPageOrientation.PortraitOrLandscape;
this.CommandPanel.Visibility = Visibility.Visible;
// Enable automatic deployment of the software keyboard
this.CommandTextBox.Focus();
this.CommandTextBox.SelectAll();
}
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void DiscoveredButton_Click(object sender, EventArgs e)
{
// Pause, otherwise the video will keep playing while the message box is
// shown and the timer Tick handler won’t be raised to stop it!
this.MediaElement.Pause();
if (this.discoveredCommands.Value.Count == this.possibleCommands.Count)
{
MessageBox.Show(“Congratulations! You have discovered all the commands!”,
“Discovered Commands”, MessageBoxButton.OK);
}
else if (this.discoveredCommands.Value.Count == 1)
{
MessageBox.Show(“You have discovered only one command, and it was given” +
“ to you! Keep trying! (You can see and use your command by tapping” +
“ ”…”)”, “Discovered Commands”, MessageBoxButton.OK);
}
else
{
MessageBox.Show(“You have discovered “ +
this.discoveredCommands.Value.Count + “ commands. (You can see them” +
“ and use them by tapping ”…”) There are more to discover!”,
“Discovered Commands”, MessageBoxButton.OK);
}
}
}
}

[/code]

  • The constructor populates possibleCommands with the list of commands along with their starting and ending times in the cat.wmv video. Because guessing each command by its exact name can be incredibly hard, an aliases dictionary is used that enables alternate forms of some commands, such as “speak” instead of “meow.”
  • In OnNavigatedTo, the application bar menu is filled with all previously discovered commands. These are stored in discoveredCommands, which is a Setting so it always gets persisted.
  • In order to show the number of discovered commands in the application bar button’s icon, several distinct images are included in this project: appbar.1.png, appbar.2.png, appbar.3.png, and so on. The right one is selected based on the Count of the discoveredCommands collection.
  • The video starts automatically playing when the page is loaded (because AutoPlay was not set to false in Listing 33.1), but we do not want the entire video to play and reveal all the cat’s actions. Instead, only the first second and a half should play. Therefore, in MediaElement’s MediaOpened event handler (raised when the media has loaded and is ready to start playing), videoTimer is used to pause the video after 1.48 seconds have elapsed. The pausing is done in videoTimer’s Tick event handler, VideoTimer_Tick.

You cannot call Play on a MediaElement until its MediaOpened event is raised!

When a MediaElement’s Source is set (either in XAML or code-behind), you cannot instantly begin interacting with the media. Instead, you must wait for MediaOpened to be raised. If the media cannot be loaded for some reason, a MediaFailed event is raised instead. Subservient Cat avoids the need to manually call Play because it uses the auto-play feature, but if it didn’t use auto-play, then it should call Play inside MediaElement_MediaOpened.

Why doesn’t my video play on a physical phone while it is connected to a computer running Zune?

This is due to the same problem discussed in the preceding chapter.The Zune desktop program locks the media library, which prevents MediaElement from loading its media. Remember, if you need to debug a part of your app that depends on video actually playing, you can use the Windows Phone Connect Tool that ships with the Windows Phone Developer Tools to connect to your phone without Zune running. Subservient Cat detects this situation by handling the MediaFailed event. It assumes the failure is due to the Zune connection, which is a pretty safe assumption because the source video is local to the app.

  • The PlayClip method ensures that the video is paused, seeks to the specified beginTime, and reinitializes videoTimer so it pauses the video at endTime. However, because setting MediaElement’s Position has an annoying effect of seeing the video quickly fastforward or rewind to the desired spot (rather than an instant jump), the intro screen is shown to hide the video during the transition. (We don’t want to reveal yet-to-be-discovered parts of the video!) The setting of Position is done inside a BeginInvoke callback so the showing of the intro screen has a chance to take effect. Without this, you still see the unwanted behavior. Two seconds is chosen as the length of the delay, during which the user can read the command text on the intro screen. We don’t have a way to know when seek has actually finished, but two seconds is long enough.

When you set MediaElement’s Position, the change is not instantaneous!

Instead, you see a little bit of the video before or after the new position, as if you’re watching the video in fast-forward or rewind mode.The workaround used by Subservient Cat is to temporarily obscure the video with other elements.

In the current version of Windows Phone, MediaElement does not support markers.The use of markers to designate when individual clips inside cat.wmv begin and end would have been ideal, and would have greatly reduced the amount of code needed in Listing 33.2. However, using a DispatcherTimer to be notified when the relevant clip has ended is a reasonable workaround.There are two things to be aware of, however:

  • The timer doesn’t give you frame-by-frame accuracy.The video used by this app has a little bit of a buffer at the end of each clip, in case videoTimer’s Tick event gets raised slightly late.
  • If you show a message box, the video will keep playing in the background but the timer’s Tick event handler cannot be called until the message box is dismissed, no matter how much time has elapsed. (MessageBox.Show is a blocking operation.) This is why DiscoveredButton_Click in Listing 33.2 pauses the video first.

When I first wrote Subservient Cat, I called MediaElement’s Stop method inside OnNavigatedFrom because I was worried about the performance impact of unnecessarily playing video while the instructions page is shown and the main page is on the back stack. However, this is unnecessary because MediaElement is automatically paused when a page navigates away. If you don’t want this behavior (perhaps because you want to hear the audio from the video continue while other pages are shown), the MediaElement must be attached to the frame rather than to a specific page.

MediaElement and Files in Isolated Storage

If you want to play a video from isolated storage (presumably because your app downloaded it and then saved it there), you can call MediaElement’s SetSource method that expects a stream rather than a URI. With this, you can pass an appropriate IsolatedStorageFileStream.

The Finished Product

Subservient Cat (Video)

 

TODO List (Pivot & Context Menu)

TODO List enables you to manage tasks in a fast, easy, and attractive way. Mark tasks with colored stars and/or detailed descriptions. Filter them in multiple ways, such as seeing overdue tasks, tasks due today, or tasks with stars. See what you’ve accomplished in a “done” list, with the ability to undo tasks. Trim the filters to only the ones you care about.

The main purpose of TODO List, however, is to demonstrate the pivot control. The pivot is one of the two signature user interface paradigms introduced by Windows Phone 7.

The Pivot Control

A pivot is basically a tabbed user interface in which you can swipe horizontally or tap one of the headers to switch to a different tab. This style of user interface is featured prominently in the built-in Mail, Calendar, and Settings apps, but also used by most of the other built-in apps: Internet Explorer, Maps, Marketplace, Music + Videos, People, and Pictures.

Pivots are designed to provide filtered views over the same set of data (as in the Mail app), distinct views over the same set of data (as in the Calendar app), or to provide easily switchable views over separate sets of data (such as application versus system settings in the Settings app). They are not meant to be used to expose sequential steps in a task, such as a wizard-style user interface. They are meant to occupy the entire page except, perhaps, for an application bar and/or status bar.

Just like the list box and list picker, pivot is an items control. Although the Pivot class exposes an Items collection to which any type of object can be added, only PivotItem objects or data objects should be added.

PivotItem is a simple content control with Content and Header properties of type object. Although these properties can be set to anything, Content is typically set to a panel such as a grid that contains a complex user interface, whereas Header is typically set to a string.

The pivot and panorama controls reside in an assembly that isn’t referenced by default in “Windows Phone Application”projects!

Although their .NET namespace (Microsoft.Phone.Controls) is the same as some commonly used types such as PhoneApplicationPage, the pivot and panorama controls are defined in the Microsoft.Phone.Controls assembly. (Controls such as PhoneApplicationPage are defined in the Microsoft.Phone assembly.) To use these controls, be sure to add a reference to Microsoft.Phone.Controls.dll. If you use Visual Studio’s “Windows Phone Pivot Application” or “Windows Phone Panorama Application”project templates, this assembly is already referenced by default. Similarly, if you add a “Windows Phone Pivot Page”or “Windows Phone Panorama Page” from Visual Studio’s Add New Item dialog, the assembly reference is automatically added to your project.

Here are three pivot design guidelines that apps should—but often don’t—obey:

  • Header text should be lowercase,with the exception of proper names.
  • As already mentioned, do not use one to organize sequential tasks that the user must complete.
  • There should be no more than seven pivot items in a single pivot.

A Pivot without PivotItems

Pivots are unusable with any UI elements other than PivotItem controls.The pivot attempts to render such elements as each item’s header and content.Therefore, attempting to use a different type of UI element throws an exception explaining,“Element is already the child of another element.”This is not a problem, however, because there’s no reason to not use PivotItems.They can contain anything, so you can always wrap your desired content with one.You can also add nonvisual data objects to a pivot and then use its ItemTemplate and HeaderTemplate properties to format them appropriately.

The Main Page

TODO List’s main page is the only one that uses a pivot. It contains five pivot items, all shown in Figure 26.1 in their empty states when first launching the app.

FIGURE 26.1 The five pivot items in their initial states.
FIGURE 26.1 The five pivot items in their initial states.

The User Interface

Listing 26.1 contains the XAML for the main page.

LISTING 26.1 MainPage.xaml—The Main User Interface for TODO List

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:controls=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls”
xmlns:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<!– The application bar, with 3 buttons and 1 menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”new”
IconUri=”/Shared/Images/appbar.add.png” Click=”AddButton_Click”/>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_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>
<phone:PhoneApplicationPage.Resources>
<!– A data template shared by the first four list boxes –>
<DataTemplate x:Key=”DataTemplate”>
<StackPanel Orientation=”Horizontal” local:Tilt.IsEnabled=”True”>
<!– Add a context menu to the item –>
<toolkit:ContextMenuService.ContextMenu>
<toolkit:ContextMenu Opened=”ContextMenu_Opened”
Closed=”ContextMenu_Closed”>
<toolkit:MenuItem Header=”mark as done” Click=”MarkMenuItem_Click”/>
<toolkit:MenuItem Header=”edit” Click=”EditMenuItem_Click”/>
<toolkit:MenuItem Header=”delete” Click=”DeleteMenuItem_Click”/>
</toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>
<!– The star, with the item-specific color –>
<Rectangle Fill=”{Binding Star}” Width=”26” Height=”25” Margin=”0,0,0,10”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/star.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<!– The title –>
<TextBlock Text=”{Binding Title}” Margin=”8,0,0,16”
Style=”{StaticResource PhoneTextExtraLargeStyle}”/>
</StackPanel>
</DataTemplate>
</phone:PhoneApplicationPage.Resources>
<controls:Pivot x:Name=”Pivot” Title=”TODO LIST”>
<!– Make the TODO LIST title match built-in apps better –>
<controls:Pivot.TitleTemplate>
<DataTemplate>
<TextBlock Text=”{Binding}” Margin=”-1,-1,0,-3”
Style=”{StaticResource PhoneTextTitle0Style}”/>
</DataTemplate>
</controls:Pivot.TitleTemplate>
<!– Pivot item #1 –>
<controls:PivotItem Header=”all”>
<Grid>
<TextBlock x:Name=”NoAllTextBlock” Text=”No tasks” Visibility=”Collapsed”
Margin=”22,17,0,0” Style=”{StaticResource PhoneTextGroupHeaderStyle}”/>
<ListBox x:Name=”AllListBox” ItemsSource=”{Binding}”
ItemTemplate=”{StaticResource DataTemplate}”
SelectionChanged=”ListBox_SelectionChanged”/>
</Grid>
</controls:PivotItem>
<!– Pivot item #2 –>
<controls:PivotItem x:Name=”TodayPivotItem” Header=”today”>
<Grid>
<TextBlock x:Name=”NoTodayTextBlock” Text=”Nothing is due today”
Visibility=”Collapsed” Margin=”22,17,0,0”
Style=”{StaticResource PhoneTextGroupHeaderStyle}”/>
<!– Show today’s date underneath the list box –>
<TextBlock x:Name=”TodayTextBlock” Opacity=”.2” Margin=”0,0,0,4”
HorizontalAlignment=”Right” VerticalAlignment=”Bottom” FontWeight=”Bold”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”/>
<ListBox x:Name=”TodayListBox”
ItemTemplate=”{StaticResource DataTemplate}”
SelectionChanged=”ListBox_SelectionChanged”/>
</Grid>
</controls:PivotItem>
<!– Pivot item #3 –>
<controls:PivotItem x:Name=”PastDuePivotItem” Header=”past due”>
<Grid>
<TextBlock x:Name=”NoPastDueTextBlock” Visibility=”Collapsed”
Text=”Nothing is past due. Good job!” Margin=”22,17,0,0”
Style=”{StaticResource PhoneTextGroupHeaderStyle}”/>
<!– Show a clock underneath the list box –>
<Rectangle Opacity=”.2” Margin=”0,0,0,12” VerticalAlignment=”Bottom”
HorizontalAlignment=”Right” Width=”240” Height=”240”
Fill=”{StaticResource PhoneForegroundBrush}”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/clock.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<ListBox x:Name=”PastDueListBox”
ItemTemplate=”{StaticResource DataTemplate}”
SelectionChanged=”ListBox_SelectionChanged”/>
</Grid>
</controls:PivotItem>
<!– Pivot item #4 –>
<controls:PivotItem x:Name=”StarredPivotItem” Header=”starred”>
<Grid>
<TextBlock x:Name=”NoStarredTextBlock” Text=”No starred tasks”
Visibility=”Collapsed” Margin=”22,17,0,0”
Style=”{StaticResource PhoneTextGroupHeaderStyle}”/>
<!– Show a star underneath the list box –>
<Rectangle Opacity=”.2” Margin=”0,0,0,12” VerticalAlignment=”Bottom”
HorizontalAlignment=”Right” Width=”240” Height=”240”
Fill=”{StaticResource PhoneForegroundBrush}”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/bigStar.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<ListBox x:Name=”StarredListBox”
ItemTemplate=”{StaticResource DataTemplate}”
SelectionChanged=”ListBox_SelectionChanged”/>
</Grid>
</controls:PivotItem>
<!– Pivot item #5 –>
<controls:PivotItem x:Name=”DonePivotItem” Header=”done”>
<Grid>
<TextBlock x:Name=”NoDoneTextBlock” Text=”Nothing done. Get to work!”
Visibility=”Collapsed” Margin=”22,17,0,0”
Style=”{StaticResource PhoneTextGroupHeaderStyle}”/>
<!– Show a checkmark underneath the list box –>
<Rectangle Opacity=”.2” Margin=”0,0,0,12” VerticalAlignment=”Bottom”
HorizontalAlignment=”Right” Width=”277” Height=”240”
Fill=”{StaticResource PhoneForegroundBrush}”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/done.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<ListBox x:Name=”DoneListBox” ItemsSource=”{Binding}”
SelectionChanged=”ListBox_SelectionChanged”>
<ListBox.ItemTemplate>
<!– A separate data template specific to the “done” list box –>
<DataTemplate>
<StackPanel Orientation=”Horizontal” Background=”Transparent”
local:Tilt.IsEnabled=”True”>
<!– Add a context menu to the item –>
<toolkit:ContextMenuService.ContextMenu>
<toolkit:ContextMenu Opened=”ContextMenu_Opened”
Closed=”ContextMenu_Closed”>
<toolkit:MenuItem Header=”unmark as done”
Click=”UnmarkMenuItem_Click”/>
<toolkit:MenuItem Header=”edit” Click=”EditMenuItem_Click”/>
<toolkit:MenuItem Header=”delete”
Click=”DeleteMenuItem_Click”/>
</toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>
<!– A checkmark-in-a-circle image –>
<Rectangle Width=”48” Height=”48”
Fill=”{StaticResource PhoneForegroundBrush}”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Shared/Images/normal.done.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<Grid>
<StackPanel Orientation=”Horizontal” Margin=”8,0,0,0”>
<!– The star, with the item-specific color –>
<Rectangle Fill=”{Binding Star}” Width=”26” Height=”25”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/star.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<!– The title –>
<TextBlock Text=”{Binding Title}” Margin=”8,0,0,6”
Style=”{StaticResource PhoneTextExtraLargeStyle}”
HorizontalAlignment=”Left” />
</StackPanel>
<!– A horizontal line on top of the title –>
<Line X1=”-2” X2=”800” Y1=”32” Y2=”32” StrokeThickness=”2”
Stroke=”{StaticResource PhoneForegroundBrush}”/>
</Grid>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</controls:PivotItem>
</controls:Pivot>
</phone:PhoneApplicationPage>

[/code]

  • A separate XML namespace is needed for the unique namespace/assembly combination required by the pivot. The conventional prefix for this XML namespace is controls.
  • Because a pivot is meant to be full-screen, the control includes a Title property that you can use for your app title rather than the typical page header. The control does a decent job of mimicking what the app title should look like, but the position and font weight are slightly wrong. Fortunately, you can customize the title’s appearance however you wish. Title is of type object, so you can set it to an arbitrary tree of UI elements rather than a simple string. Or you can use a TitleTemplate property to customize its appearance. This page leverages TitleTemplate to tweak the appearance of the title string, as shown in Figure 26.2. Future releases of Windows Phone might address this issue, if Silverlight will support the same text kerning done natively by Windows Phone OS. Until such time, applying a custom template is a reasonable workaround.
FIGURE 26.2 The custom title template makes subtle changes to the default appearance of the pivot’s title.
FIGURE 26.2 The custom title template makes subtle changes to the default appearance of the pivot’s title.
  • Pivot also exposes a HeaderTemplate property for customizing the appearance of each pivot item’s header. However, the default headers are perfect for matching the style of the built-in apps, so most apps have no use for this property. If you’re doing something custom, however, such as putting text and an image in each header, this property enables you to do just that.
  • Each pivot item contains a text block (displayed when that item’s list is empty) and a list box in a grid. All but the first item also have a subtle image or text decoration behind the list box.
FIGURE 26.3 The separate item template for the “done” list box adds check marks and a strikethrough effect.
FIGURE 26.3 The separate item template for the “done” list box adds check marks and a strikethrough effect.
  • The first four list boxes share the same item template defined as a resource called DataTemplate. The list box for the “done” pivot item, however, uses its own template that adds a check mark and a strikethrough effect. This is shown in Figure 26.3.
  • Both templates add a context menu to each item, leveraging the ContextMenu element in the Silverlight for Windows Phone Toolkit. To use a context menu, you simply set the ContextMenuService.ContextMenu attachable property on the element that should react to the user’s touch-and-hold gesture. After one second, the menu is shown with the list of menu items you place inside the context menu. This is demonstrated in Figure 26.4.
FIGURE 26.4 The context menu, shown here for “Do the dishes,” exposes three additional actions for each task.
FIGURE 26.4 The context menu, shown here for “Do the dishes,” exposes three additional actions for each task.

On Windows, it’s standard for a context menu to include an item’s default on-click action, and even show it in bold. On Windows Phone, context menus should not include the default on-tap action. Instead, context menu items should be reserved for additional actions that cannot be invoked by any other means on the page. For example, the context menus in Listing 26.1 do not list “view details” as one of the menu items because normal taps on each item already perform that action. Following this guideline not only makes the context menu behavior consistent with the phone’s built-in apps, but it also preserves precious screen real estate.

Although none of them are used by TODO List, pivot exposes several events that are useful for dynamic pivot items:

  • SelectionChanged—Raised when the pivot item currently occupying the screen has changed
  • LoadingPivotItem—Raised right before a pivot item is displayed for the first time
  • LoadedPivotItem—Raised right after a pivot item is displayed for the first time
  • UnloadingPivotItem—Raised right before a pivot item is removed from the pivot’s Items collection
  • UnloadedPivotItem—Raised right after a pivot item is removed from the pivot’s Items collection

Pivot already delay-loads items that are more than one swipe away to improve startup time, but many popular apps use these events to improve performance even further with their own pivotitem virtualization scheme.

The Code-Behind

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

LISTING 26.2 MainPage.xaml.cs—The Code-Behind for TODO List’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
{
bool isNavigatingAway;
bool isContextMenuOpen;
public MainPage()
{
InitializeComponent();
this.Loaded += MainPage_Loaded;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
this.isNavigatingAway = true;
base.OnNavigatedFrom(e);
// Remember the selected item
Settings.SelectedPivotItemName.Value =
(this.Pivot.SelectedItem as PivotItem).Name;
// Workaround for troubles when pivot items are removed and
// cause SelectedIndex > Count-1
this.Pivot.SelectedIndex = 0;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
this.isNavigatingAway = false;
base.OnNavigatedTo(e);
this.TodayTextBlock.Text = DateTime.Now.ToShortDateString();
// If the set of included pivot items were changed
// on the settings page, refresh them
int newPivotItemsCount = 1 + (Settings.IsTodayVisible.Value ? 1 : 0) +
(Settings.IsPastDueVisible.Value ? 1 : 0) +
(Settings.IsStarredVisible.Value ? 1 : 0) +
(Settings.IsDoneVisible.Value ? 1 : 0);
if (this.Pivot.Items.Count != newPivotItemsCount)
{
int insertLocation = 1;
ShowOrHidePivotItem(this.TodayPivotItem,
Settings.IsTodayVisible.Value, ref insertLocation);
ShowOrHidePivotItem(this.PastDuePivotItem,
Settings.IsPastDueVisible.Value, ref insertLocation);
ShowOrHidePivotItem(this.StarredPivotItem,
Settings.IsStarredVisible.Value, ref insertLocation);
ShowOrHidePivotItem(this.DonePivotItem,
Settings.IsDoneVisible.Value, ref insertLocation);
}
}
void ShowOrHidePivotItem(PivotItem item, bool show, ref int insertLocation)
{
// Insert or remove the pivot item, if necessary
if (show && item.Parent == null)
this.Pivot.Items.Insert(insertLocation, item);
else if (!show && item.Parent != null)
this.Pivot.Items.Remove(item);
if (show)
insertLocation++;
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
if (!isNavigatingAway) // Workaround for Loaded-raised-too-often bug
{
// Two of the list boxes use data binding
this.AllListBox.DataContext = Settings.TaskList.Value;
this.DoneListBox.DataContext = Settings.DoneList.Value;
// The rest are manually filled by filtering the task list
RefreshLists();
// Restore the selected item
// (done here because OnNavigatedTo is too early)
PivotItem pivotItem =
this.FindName(Settings.SelectedPivotItemName.Value) as PivotItem;
if (pivotItem != null)
this.Pivot.SelectedItem = pivotItem;
}
}
void RefreshLists()
{
DateTime today = DateTime.Now.Date;
// Fill the three filtered lists
this.TodayListBox.Items.Clear();
this.PastDueListBox.Items.Clear();
this.StarredListBox.Items.Clear();
foreach (Task item in Settings.TaskList.Value)
{
// today
if (item.DueDate.Date == today)
this.TodayListBox.Items.Add(item);
// past due
if (item.DueDate < DateTime.Now)
this.PastDueListBox.Items.Add(item);
// starred
if (item.Star != null && item.Star != “none”)
this.StarredListBox.Items.Add(item);
}
// Show/hide the “no tasks” labels
this.NoAllTextBlock.Visibility = Settings.TaskList.Value.Count == 0 ?
Visibility.Visible : Visibility.Collapsed;
this.NoTodayTextBlock.Visibility = this.TodayListBox.Items.Count == 0 ?
Visibility.Visible : Visibility.Collapsed;
this.NoPastDueTextBlock.Visibility = this.PastDueListBox.Items.Count == 0 ?
Visibility.Visible : Visibility.Collapsed;
this.NoStarredTextBlock.Visibility = this.StarredListBox.Items.Count == 0 ?
Visibility.Visible : Visibility.Collapsed;
this.NoDoneTextBlock.Visibility = Settings.DoneList.Value.Count == 0 ?
Visibility.Visible : Visibility.Collapsed;
}
void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (isContextMenuOpen)
{
// Cancel the selection
(sender as ListBox).SelectedIndex = -1;
return;
}
if (e.AddedItems.Count != 1)
return;
// Communicate the selected item to the details page
Settings.CurrentTask.Value = e.AddedItems[0] as Task;
// Navigate to the details page
this.NavigationService.Navigate(new Uri(“/DetailsPage.xaml”,
UriKind.Relative));
// Undo the selection so the same item can be tapped again upon return
(sender as ListBox).SelectedIndex = -1;
}
// Context menu handlers
void ContextMenu_Opened(object sender, RoutedEventArgs e)
{
this.isContextMenuOpen = true;
}
void ContextMenu_Closed(object sender, RoutedEventArgs e)
{
this.isContextMenuOpen = false;
}
void MarkMenuItem_Click(object sender, RoutedEventArgs e)
{
Task task = (sender as MenuItem).DataContext as Task;
// Move from the task list to the done list
Settings.TaskList.Value.Remove(task);
Settings.DoneList.Value.Add(task);
RefreshLists();
}
void UnmarkMenuItem_Click(object sender, RoutedEventArgs e)
{
Task task = (sender as MenuItem).DataContext as Task;
// Move from the done list to the task list
Settings.DoneList.Value.Remove(task);
Settings.TaskList.Value.Add(task);
RefreshLists();
}
void EditMenuItem_Click(object sender, RoutedEventArgs e)
{
// Communicate the selected item to the add/edit page
Settings.CurrentTask.Value = (sender as MenuItem).DataContext as Task;
// Navigate to the add/edit page
this.NavigationService.Navigate(new Uri(“/AddEditPage.xaml”,
UriKind.Relative));
}
void DeleteMenuItem_Click(object sender, RoutedEventArgs e)
{
if (MessageBox.Show(
“Are you sure you want to permanently delete this task?”, “Delete task”,
MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
// The task is only in one of the two lists, but just try deleting from
// both rather than checking. One call will work, one will be a no-op.
Settings.TaskList.Value.Remove((sender as MenuItem).DataContext as Task);
Settings.DoneList.Value.Remove((sender as MenuItem).DataContext as Task);
RefreshLists();
}
}
// Application bar handlers
void AddButton_Click(object sender, EventArgs e)
{
Settings.CurrentTask.Value = null;
this.NavigationService.Navigate(new Uri(“/AddEditPage.xaml”,
UriKind.Relative));
}
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
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=TODO List”, UriKind.Relative));
}
}
}

[/code]

  • A pivot exposes a SelectedItem (and SelectedIndex) property that represents which of the pivot items is occupying the screen. TODO List stores the selected item as a persisted setting, but it does so by storing the name of the element rather than its index. This is done because this app’s settings page enables the user to hide any of the pivot items except the first one, and the hiding is done by removing the pivot item(s) from the pivot’s collection.

Setting a pivot’s SelectedItem or SelectedIndex property always animates the change in selection!

This is extremely irritating for the main scenarios in which I can imagine these properties being used. For example, when an app is activated and you want to restore a pivot to its previous state (providing the illusion that it was running the whole time), you really want it to appear with the user’s previous selection instantly visible. A workaround to enable this behavior would be to physically shift the order of the pivot items, so the previous selection is always the 0th item and to not write any code that depends on indices.

Setting a pivot item’s visibility has no effect!

Temporarily hiding pivot items would be easy to do if you could simply set its Visibility property to Collapsed.Unfortunately, because this has no effect, the only way to hide a pivot item—and not have it occupy space—is to remove it from the pivot’s Items collection.

According to Windows Phone design guidelines, you should avoid removing empty pivot items if the user has some way to add information to it. Instead, you should show the empty pivot page or perhaps put an explanatory message in it, as done with each pivot item in TODO List’s main page.

  • In OnNavigatedTo (which is called upon return from adjusting the visible set of pivot items on the settings page), pivot items are either added or removed from the pivot based on the current settings.
  • Pivot does not handle the removal of pivot items very gracefully. If pivot items are removed such that the previously selected index is greater than the new last index, an ArgumentOutOfRangeException is thrown. This happens even if pivot’s SelectedIndex property is set to 0 immediately before removing pivot items, presumably due to its animation from the old item to the new item. This is a bug that might be fixed in a future release of Windows Phone.Therefore, Listing 26.2 works around this problem by setting SelectedIndex to 0 in OnNavigatedFrom. That way, even if the user visits the settings page, removes pivot items, and returns to the main page extremely quickly, there is still plenty of time for the pivot’s animation to complete beforehand. Also, the pivot will only remain on the 0th item if the previously selected item has been removed thanks to the logic in Loaded that restores the selected item after OnNavigatedTo has executed.
  • The “all” list box databinds to a TaskList setting and the “done” list box databinds to a DoneList setting. The remaining three list boxes all contain a filtered version of TaskList. They are filled manually inside RefreshLists because there’s no automatic data-binding mechanism that filters a collection.
  • The context menu’s Opened and Closed events are handled simply to enable code to check ondemand whether a context menu is currently open. ListBox_SelectionChanged leverages this to ignore a tap on an item when that tap is actually dismissing an open context menu.
  • Because the same context menu handlers are used for both context menus, the code is written in a way to work for either context. The sender passed to these handlers is the MenuItem that was tapped, so its DataContext property is used to retrieve the source item to which the context menu’s item template is applied.

Supporting Data Types

As seen in the preceding section, TODO List manipulates two collections of tasks exposed as settings. This involves three classes that are important to understand for appreciating how this app works. Listing 26.3 shows the implementation of the Task class used to represent each item shown in any of main page’s list boxes.

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

[code]

using System;
using System.ComponentModel;
namespace WindowsPhoneApp
{
public class Task : INotifyPropertyChanged
{
// The backing fields
string title;
string description;
string star;
DateTimeOffset createdDate;
DateTimeOffset modifiedDate;
DateTimeOffset dueDate;
// The properties, which raise change notifications
public string Title {
get { return this.title; }
set { this.title = value; OnPropertyChanged(“Title”); }
}
public string Description {
get { return this.description; }
set { this.description = value; OnPropertyChanged(“Description”); }
}
public string Star {
get { return this.star; }
set { this.star = value; OnPropertyChanged(“Star”); }
}
public DateTimeOffset CreatedDate {
get { return this.createdDate; }
set { this.createdDate = value; OnPropertyChanged(“CreatedDate”); }
}
public DateTimeOffset ModifiedDate {
get { return this.modifiedDate; }
set { this.modifiedDate = value; OnPropertyChanged(“ModifiedDate”); }
}
public DateTimeOffset DueDate {
get { return this.dueDate; }
set { this.dueDate = value; OnPropertyChanged(“DueDate”); }
}
void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
}

[/code]

  • The item templates on main page leverage the value of each task’s Title and Star properties. All the properties are displayed on the details and add/edit pages, and the DueDate property is also used for sorting the list of tasks.
  • The CreatedDate and ModifiedDate properties are appropriately of type DateTimeOffset rather than DateTime, and the DueDate property is also a DateTimeOffset to match the others.
  • The Star value is a string representing the color (like “red” or “yellow”). This is strange from an API perspective, but it happens to work out nicely because the item templates on the main page and a star on the upcoming details page are able to bind directly to the property without needing a value converter.
  • The property-changed notifications ensure that data-bound user interface elements
    can remain up-to-date. This is leveraged on the main page and the upcoming details
    page. On the main page, it turns out that this is only needed for the “done” list due
    to the way item editing works. This is explained in the upcoming “The Add/Edit
    Page” section.

Listing 26.4 contains the entire set of persisted settings used by TODO List.

LISTING 26.4 Settings.cs—All the Settings Persisted to Isolated Storage

[code]

using System.Collections.ObjectModel;
namespace WindowsPhoneApp
{
public static class Settings
{
// The selected pivot item, stored by name
public static readonly Setting<string> SelectedPivotItemName =
new Setting<string>(“SelectedPivotItemName”, “all”);
// Which pivot items are included in the pivot
public static readonly Setting<bool> IsTodayVisible =
new Setting<bool>(“IsTodayVisible”, true);
public static readonly Setting<bool> IsPastDueVisible =
new Setting<bool>(“IsPastDueVisible”, true);
public static readonly Setting<bool> IsStarredVisible =
new Setting<bool>(“IsStarredVisible”, true);
public static readonly Setting<bool> IsDoneVisible =
new Setting<bool>(“IsDoneVisible”, true);
// The task currently in the details or add/edit page
public static readonly Setting<Task> CurrentTask =
new Setting<Task>(“CurrentTask”, null);
// Sorted in chronological order
public static readonly Setting<SortedTaskCollection> TaskList =
new Setting<SortedTaskCollection>(“TaskList”,
new SortedTaskCollection());
// Kept in the order tasks get done
public static readonly Setting<ObservableCollection<Task>> DoneList =
new Setting<ObservableCollection<Task>>(“DoneList”,
new ObservableCollection<Task>());
}
}

[/code]

  • The first five settings maintain the state of the pivot control on the main page, and the next setting (CurrentTask) tells the details and add/edit pages which item was just selected on the main page.
  • The most important settings are the last two—the list of unfinished tasks and list of done tasks. Notice that these are two different types of collections. DoneList is a basic observable collection of tasks. No sorting is ever done, so the list is always in the order that tasks were finished. (If users want to change the ordering, they would need to “unmark” tasks as done and then mark them again.) TaskList, on the other hand, is an observable collection that automatically sorts its tasks in chronological order based on the value of the DueDate property. This enables every list box on the main page (except the “done” list) to be sorted in this fashion without any sorting code outside of the collection class itself.
  • The fact that both lists are observable collections is important, because the main page relies on the collection-changed notifications for keeping its “all” and “done” lists up-to-date as items are added and removed.

Listing 26.5 shows the implementation of this sorted collection class.

LISTING 26.5 SortedTaskCollection.cs—Adds Automatic Sorting to an Observable Collection of Tasks

[code]

using System;
using System.Collections.ObjectModel;
using System.Runtime.Serialization;
namespace WindowsPhoneApp
{
[CollectionDataContract]
public class SortedTaskCollection : ObservableCollection<Task>
{
protected override void InsertItem(int index, Task item)
{
// Ignore the index. Instead, keep the list sorted in chronological order
int i = 0;
for (i = 0; i < this.Count; i++)
{
DateTimeOffset d = this[i].DueDate;
if (d > item.DueDate)
break;
}
base.InsertItem(i, item);
}
}
}

[/code]

  • All this class needs to do is override ObservableCollection’s protected InsertItem method that ultimately gets called by both Add and Insert. It ignores the passed-in index and instead chooses an index that maintains the desired sorting of the list. The result is confusing for someone trying to call the collection’s Insert method with a specific index (not done by this app), but calling Add works seamlessly.
  • The most subtle part of this collection’s implementation is the CollectionDataContract attribute. This attribute, defined in the System.Runtime.Serialization namespace in the System.Runtime.Serialization assembly (which is not referenced by Windows Phone projects by default), is necessary for this app’s settings to be serialized successfully. The reason why, however, is obscure. Because SortedTaskCollection derives from ObservableCollection<Task>, the two classes have the same data contract name as far as the built-in serialization process is concerned. However, each type serialized must have a unique data contract name, so the CollectionDataContract attribute assigns one to SortedTaskCollection. (You don’t even need to choose an explicit name for this to work!)Without this attribute, an exception with the following message is raised when the app is closing or deactivating, due to automatic attempt to serialize this app’s settings:

    Type ‘System.Collections.ObjectModel.ObservableCollection`1[WindowsPhoneApp.Task]’ cannot be added to list of known types since another type ‘WindowsPhoneApp.SortedTaskCollection’ with the same data contract name ‘http://schemas.datacontract.org/2004/07/WindowsPhoneApp:ArrayOfTask’ is already present.

    Note that if both lists were of type SortedTaskCollection, the serialization would work just fine without the attribute because there would be no conflict. Of course, it’s a good idea to make classes you intend to serialize with such attributes to avoid subtle bugs in the future.

In addition to the CollectionDataContract attribute designed for collection classes, System.Runtime.Serialization exposes a DataContract attribute that can be used on regular (non-collection) classes.

Normally,when data fails to serialize to isolated storage or page state, your only indication is that the data does not exist when you next launch or activate the app.To be able to see detailed exception information for the serialization failure, run your app under Visual Studio’s debugger but tell it to catch all first-chance .NET exceptions.You can do this by checking the “Thrown” checkbox next to “Common Language Runtime Exceptions” on the Exceptions dialog (found under the Debug, Exceptions… menu item). When doing this, you might encounter several additional exceptions within the .NET Framework that are actually harmless and not related to the problems your app is experiencing.You’ll need to continue past those to see the relevant exception.

The Details Page

The details page, shown in Figure 26.5, is a straightforward display of each task’s properties. The task’s title is used as the page title, and the description and date properties are shown below it. If the item has been given a star, it is shown as well. For convenience, the page’s application bar exposes buttons for each of the three actions that the main page exposes as context menu items.

Listing 26.6 contains this page’s XAML and Listing 26.7 contains its code-behind.

FIGURE 26.5 The details page for an item, before and after it is marked with a yellow star.

LISTING 26.6 DetailsPage.xaml—The User Interface for TODO List’s 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 3 buttons and a menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”mark done”
IconUri=”/Images/appbar.markDone.png” Click=”MarkUnmarkButton_Click”/>
<shell:ApplicationBarIconButton Text=”edit”
IconUri=”/Shared/Images/appbar.edit.png” Click=”EditButton_Click”/>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png” Click=”DeleteButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<phone:PhoneApplicationPage.Resources>
<local:DateConverter x:Key=”DateConverter”/>
</phone:PhoneApplicationPage.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”TODO LIST” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”{Binding Title}”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<!– The appropriately-colored star –>
<Rectangle Grid.Row=”2” Width=”240” Height=”240” Fill=”{Binding Star}”
VerticalAlignment=”Bottom” HorizontalAlignment=”Right” Margin=”0,0,0,12”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/bigStar.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<ScrollViewer Grid.Row=”1”>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”Auto”/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”24”/>
</Grid.RowDefinitions>
<!– The description –>
<TextBlock Text=”{Binding Description}”
FontSize=”{StaticResource PhoneFontSizeLarge}”
Grid.ColumnSpan=”2” Margin=”24,0,24,24” TextWrapping=”Wrap”/>
<!– The 2-3 dates –>
<Rectangle x:Name=”AccentRectangle” Grid.Row=”1” Grid.ColumnSpan=”2”
Grid.RowSpan=”4” Fill=”{StaticResource PhoneAccentBrush}”/>
<TextBlock Grid.Row=”1” Text=”Due:” FontWeight=”Bold” Margin=”24,24,24,0”
FontSize=”{StaticResource PhoneFontSizeMedium}”/>
<TextBlock Grid.Row=”2” Text=”Created:” Margin=”24,0”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBlock x:Name=”ModifiedLabelTextBlock” Grid.Row=”3” Text=”Modified:”
Margin=”24,0” Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBlock Grid.Column=”1” Grid.Row=”1”
Text=”{Binding DueDate, Converter={StaticResource DateConverter}}”
Margin=”0,24,0,0” FontWeight=”Bold”
FontSize=”{StaticResource PhoneFontSizeMediumLarge}”/>
<TextBlock Grid.Column=”1” Grid.Row=”2”
Text=”{Binding CreatedDate, Converter={StaticResource DateConverter}}”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBlock x:Name=”ModifiedTextBlock” Grid.Column=”1” Grid.Row=”3”
Text=”{Binding ModifiedDate, Converter={StaticResource DateConverter}}”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
</Grid>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 26.7 DetailsPage.xaml.cs—The Code-Behind for TODO List’s Details Page

[code]

using System;
using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class DetailsPage : PhoneApplicationPage
{
IApplicationBarIconButton markUnmarkButton;
public DetailsPage()
{
InitializeComponent();
this.markUnmarkButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Set the context for the data binding done in XAML
this.DataContext = Settings.CurrentTask.Value;
if (Settings.CurrentTask.Value != null)
{
// Only show the modified date if different from the created date
if (Settings.CurrentTask.Value.CreatedDate ==
Settings.CurrentTask.Value.ModifiedDate)
{
this.ModifiedLabelTextBlock.Visibility = Visibility.Collapsed;
this.ModifiedTextBlock.Visibility = Visibility.Collapsed;
}
else
{
this.ModifiedLabelTextBlock.Visibility = Visibility.Visible;
this.ModifiedTextBlock.Visibility = Visibility.Visible;
}
// Ensure that the application bar button correctly represents whether
// this task is done
if (Settings.DoneList.Value.Contains(Settings.CurrentTask.Value))
{
this.markUnmarkButton.IconUri =
new Uri(“/Images/appbar.unmarkDone.png”, UriKind.Relative);
this.markUnmarkButton.Text = “undo”;
}
}
}
// Application bar handlers
void MarkUnmarkButton_Click(object sender, EventArgs e)
{
if (this.markUnmarkButton.Text == “mark done”)
{
this.markUnmarkButton.IconUri = new Uri(“/Images/appbar.unmarkDone.png”,
UriKind.Relative);
this.markUnmarkButton.Text = “undo”;
// Move the item from the task list to the done list
Settings.TaskList.Value.Remove(Settings.CurrentTask.Value);
Settings.DoneList.Value.Add(Settings.CurrentTask.Value);
}
else
{
this.markUnmarkButton.IconUri = new Uri(“/Images/appbar.markDone.png”,
UriKind.Relative);
this.markUnmarkButton.Text = “mark done”;
// Move the item from the done list to the task list
Settings.DoneList.Value.Remove(Settings.CurrentTask.Value);
Settings.TaskList.Value.Add(Settings.CurrentTask.Value);
}
}
void EditButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/AddEditPage.xaml”,
UriKind.Relative));
}
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(
“Are you sure you want to permanently delete this task?”, “Delete task”,
MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
// The task is only in one of the two lists, but just try deleting from
// both rather than checking. One call will work, one will be a no-op.
Settings.TaskList.Value.Remove(Settings.CurrentTask.Value);
Settings.DoneList.Value.Remove(Settings.CurrentTask.Value);
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=TODO List”, UriKind.Relative));
}
}
}

[/code]

  • The information is placed inside a scroll viewer in case the description is really long. The big star serves as a stationary background that does not scroll with the rest of the content.
  • A few display adjustments are made inside OnNavigatedTo so the page properly updates if the user taps the edit button to navigate to the add/edit page, makes some changes, saves them, and then navigates back. A value converter could have been written and used to avoid toggling the two text blocks’ Visibility property in code-behind, but that seems like overkill.

The Add/Edit Page

The add/edit page acts like two distinct pages—a page for adding a new task and a page for editing an existing task—but due to their enormous similarities, it is implemented as a single page. Figure 26.6 shows this page in both of its roles.

FIGURE 26.6 The add/edit page in its two different modes.
FIGURE 26.6 The add/edit page in its two different modes.

The User Interface

Listing 26.8 contains the XAML for the add/edit page.

LISTING 26.8 AddEditPage.xaml—The User Interface for TODO List’s Add/Edit Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.AddItemPage”
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:sys=”clr-namespace:System;assembly=mscorlib”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<!– The single-button, single-menu-item application bar –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”save”
IconUri=”/Shared/Images/appbar.save.png” Click=”SaveButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”TODO LIST” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock x:Name=”PageTitle”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<StackPanel Margin=”{StaticResource PhoneHorizontalMargin}”>
<!– Title –>
<TextBlock Text=”Title” Margin=”11,0,0,-5”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBox x:Name=”TitleTextBox” InputScope=”Text”
TextChanged=”TitleTextBox_TextChanged” KeyUp=”TitleTextBox_KeyUp”/>
<!– Description –>
<TextBlock Text=”Description” Margin=”11,11,0,-5”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBox x:Name=”DescriptionTextBox” InputScope=”Text” MinHeight=”106”
AcceptsReturn=”True” TextWrapping=”Wrap”/>
<!– Star –>
<toolkit:ListPicker x:Name=”StarListPicker” Header=”Star”>
<toolkit:ListPicker.ItemTemplate>
<DataTemplate>
<StackPanel Orientation=”Horizontal”>
<!– Give each item the colored star next to its text –>
<Rectangle Fill=”{Binding}” Width=”26” Height=”25”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/star.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<TextBlock Text=”{Binding}” Margin=”12 0 0 0”/>
</StackPanel>
</DataTemplate>
</toolkit:ListPicker.ItemTemplate>
<sys:String>none</sys:String>
<sys:String>red</sys:String>
<sys:String>yellow</sys:String>
<sys:String>green</sys:String>
<sys:String>blue</sys:String>
</toolkit:ListPicker>
<!– Due Date –>
<TextBlock Text=”Due Date” Margin=”11,11,0,-5” CacheMode=”BitmapCache”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<toolkit:DatePicker x:Name=”DueDatePicker” CacheMode=”BitmapCache”
ValueChanged=”DateTimePicker_ValueChanged”
local:Tilt.IsEnabled=”True”/>
<!– Due Time –>
<TextBlock Text=”Due Time” Margin=”11,11,0,-5” CacheMode=”BitmapCache”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<toolkit:TimePicker x:Name=”DueTimePicker” CacheMode=”BitmapCache”
ValueChanged=”DateTimePicker_ValueChanged”
local:Tilt.IsEnabled=”True”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • This page exploits three controls from the Silverlight for Windows Phone Toolkit: list picker, date picker, and time picker. The list picker decorates each of its items with an appropriately colored star thanks to data binding that automatically works with the color strings. This is shown in Figure 26.7. For the “none” value, this app leverages the fact that an invalid string causes the binding to fail for that item and leave the rectangle with its default null fill.
FIGURE 26.7 Each item in the list picker visually shows the star next to each color name, as seen when it is expanded.
FIGURE 26.7 Each item in the list picker visually shows the star next to each color name, as seen when it is expanded.

The Code-Behind

Listing 26.9 contains the code-behind for the add/edit page.

LISTING 26.9 AddEditPage.xaml.cs—The Code-Behind for TODO List’s Add/Edit Page

[code]

using System;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class AddItemPage : PhoneApplicationPage
{
IApplicationBarIconButton saveButton;
DateTime? pendingChosenDate;
DateTime? pendingChosenTime;
public AddItemPage()
{
InitializeComponent();
this.saveButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Store the info in page state so it is preserved when temporarily
// navigating away from the page. This is especially important because the
// date picker and time picker navigate to a different page!
this.State[“Title”] = this.TitleTextBox.Text;
this.State[“Description”] = this.DescriptionTextBox.Text;
this.State[“Star”] = this.StarListPicker.SelectedItem;
this.State[“DueDate”] = this.DueDatePicker.Value;
this.State[“DueTime”] = this.DueTimePicker.Value;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// If we’re returning from the date or time picker,
// make sure we apply the chosen value
if (this.pendingChosenDate.HasValue)
this.State[“DueDate”] = this.pendingChosenDate;
if (this.pendingChosenTime.HasValue)
this.State[“DueTime”] = this.pendingChosenTime;
// Initialize the page for either add mode or edit mode
if (Settings.CurrentTask.Value == null)
{
this.PageTitle.Text = “new”;
}
else
{
this.PageTitle.Text = “edit”;
this.TitleTextBox.Text = Settings.CurrentTask.Value.Title;
this.DescriptionTextBox.Text = Settings.CurrentTask.Value.Description;
this.StarListPicker.SelectedItem = Settings.CurrentTask.Value.Star;
this.DueDatePicker.Value =
Settings.CurrentTask.Value.DueDate.LocalDateTime;
this.DueTimePicker.Value =
Settings.CurrentTask.Value.DueDate.LocalDateTime;
}
// Apply any temporary values from page state
if (this.State.ContainsKey(“Title”))
this.TitleTextBox.Text = (string)this.State[“Title”];
if (this.State.ContainsKey(“Description”))
this.DescriptionTextBox.Text = (string)this.State[“Description”];
if (this.State.ContainsKey(“Star”))
this.StarListPicker.SelectedItem = (string)this.State[“Star”];
if (this.State.ContainsKey(“DueDate”))
this.DueDatePicker.Value = (DateTime?)this.State[“DueDate”];
if (this.State.ContainsKey(“DueTime”))
this.DueTimePicker.Value = (DateTime?)this.State[“DueTime”];
// Only allow saving when there’s a title
this.saveButton.IsEnabled = (this.TitleTextBox.Text != null &&
this.TitleTextBox.Text.Trim().Length > 0);
}
void TitleTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
// Only allow saving when there’s a title
this.saveButton.IsEnabled = (this.TitleTextBox.Text != null &&
this.TitleTextBox.Text.Trim().Length > 0);
}
void TitleTextBox_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
this.DescriptionTextBox.Focus();
}
void DateTimePicker_ValueChanged(object sender,
DateTimeValueChangedEventArgs e)
{
// Prevent the values from getting clobbered when navigating back
this.pendingChosenDate = this.DueDatePicker.Value;
this.pendingChosenTime = this.DueTimePicker.Value;
}
// Application bar handlers
void SaveButton_Click(object sender, EventArgs e)
{
// Consolidate the due date and due time into a single DateTime.
// First get just the date (no time) from the date picker’s value
DateTime dueDate = this.DueDatePicker.Value.Value.Date;
// Now add the time to this date
if (this.DueTimePicker.Value.HasValue)
dueDate = dueDate.AddMinutes(
this.DueTimePicker.Value.Value.TimeOfDay.TotalMinutes);
Task item = new Task
{
Title = this.TitleTextBox.Text.Trim(),
Description = this.DescriptionTextBox.Text.Trim(),
Star = (string)this.StarListPicker.SelectedItem,
ModifiedDate = DateTime.Now,
DueDate = dueDate
};
if (Settings.CurrentTask.Value != null)
{
// This is an edit
// Perform the edit differently for the task list versus done list
if (Settings.TaskList.Value.Remove(Settings.CurrentTask.Value))
{
// We removed the old item, and now let’s insert the new item.
// If the due date has changed, this re-sorts the list correctly.
// Be sure to give this new item the original created date
item.CreatedDate = Settings.CurrentTask.Value.CreatedDate;
Settings.TaskList.Value.Add(item);
Settings.CurrentTask.Value = item;
}
else
{
// We don’t want to change the ordering in the done list,
// so just update the item in-place
Settings.CurrentTask.Value.Title = item.Title;
Settings.CurrentTask.Value.Description = item.Description;
Settings.CurrentTask.Value.Star = item.Star;
Settings.CurrentTask.Value.ModifiedDate = item.ModifiedDate;
Settings.CurrentTask.Value.DueDate = item.DueDate;
// Don’t change CreatedDate!
}
}
else
{
// This is a new task
item.CreatedDate = item.ModifiedDate;
Settings.TaskList.Value.Add(item);
}
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=TODO List”, UriKind.Relative));
}
}
}

[/code]

  • This page stores the current value in each control in page state. This is not only a nice touch for when the user gets interrupted while filling out the page, but it is a requirement due to the way that the date and time pickers work. Because these controls navigate away from the current page, failure to save and restore the values would cause the form to get cleared out whenever the date or time picker is used!
  • If an item in the done list is being edited, its values are directly modified. If an item in the task list is being edited, the task is removed and a modified task is added. This is done to keep the task list sorted by due date. If the due date had been changed, editing the existing task in the collection might cause the sorting to be incorrect. This is why Task’s INotifyPropertyChanged implementation is only needed to keep the main page’s “done” list box up-to-date; additions and removals are already reported by observable collections, so the propertychanged notifications are only needed for direct edits.
FIGURE 26.8 The settings page enables the user to hide all but the first pivot item.
FIGURE 26.8 The settings page enables the user to hide all but the first pivot item.

The Settings Page

The settings page, shown in Figure 26.8, enables the user to turn off any of the pivot items except for the “all” item. (The “all” check box is present but always disabled to make it clear that this item can’t be hidden.) Listing 26.10 contains the XAML, and Listing 26.11 contains the code-behind.

LISTING 26.10 SettingsPage.xaml—The User Interface for TODO List’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 header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”SETTINGS” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”todo list” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<!– A check box for each setting –>
<ScrollViewer Grid.Row=”1”>
<StackPanel Margin=”{StaticResource PhoneHorizontalMargin}”>
<TextBlock Margin=”12,12,0,0” Text=”Visible Lists”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<CheckBox Content=”all” IsEnabled=”False” IsChecked=”True”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”
local:Tilt.IsEnabled=”True”/>
<CheckBox x:Name=”TodayCheckBox” Content=”today”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”
local:Tilt.IsEnabled=”True”/>
<CheckBox x:Name=”PastDueCheckBox” Content=”past due”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”
local:Tilt.IsEnabled=”True”/>
<CheckBox x:Name=”StarredCheckBox” Content=”starred”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”
local:Tilt.IsEnabled=”True”/>
<CheckBox x:Name=”DoneCheckBox” Content=”done” local:Tilt.IsEnabled=”True”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 26.11 SettingsPage.xaml.cs—The Code-Behind for TODO List’s Settings Page

[code]

using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
public SettingsPage()
{
InitializeComponent();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Save the settings
Settings.IsTodayVisible.Value = this.TodayCheckBox.IsChecked.Value;
Settings.IsPastDueVisible.Value = this.PastDueCheckBox.IsChecked.Value;
Settings.IsStarredVisible.Value = this.StarredCheckBox.IsChecked.Value;
Settings.IsDoneVisible.Value = this.DoneCheckBox.IsChecked.Value;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the saved settings
this.TodayCheckBox.IsChecked = Settings.IsTodayVisible.Value;
this.PastDueCheckBox.IsChecked = Settings.IsPastDueVisible.Value;
this.StarredCheckBox.IsChecked = Settings.IsStarredVisible.Value;
this.DoneCheckBox.IsChecked = Settings.IsDoneVisible.Value;
}
}
}

[/code]

This page is about as simple as a settings page can get. The hard part is supporting the hiding of pivot items that is done in the main page!

The Finished Product

TODO List (Pivot & Context Menu)

Alarm Clock (Settings,Toggle Switch, Custom Font)

The Alarm Clock app mimics a somewhat-retro digital alarm clock. It pretends to have a fixed display with segments that can be turned on or off as needed. It displays the current time and day of the week, and it enables setting a snoozable alarm. (The alarm only goes off if the app is still running, even if it’s under the lock screen.)

Alarm Clock exposes several user-configurable settings related to the color and formatting of the clock and its alarm vibration behavior. It also persists state such as whether the alarm is on and what time it should go off. All of this is done with the Setting class that has been used in almost every app so far. Now, given the topic of Part III, “Storing & Retrieving Local Data,” it is time to look into how the Setting class works and understand more about storing and retrieving data.

Setting and Isolated Storage

Isolated storage is a special folder provided to each app where arbitrary data files can be stored. The files in this folder are isolated to the designated app. One app can never access the isolated storage for another app. Even if an app wants to share the data in its storage with others, it cannot.

This is a static IsolatedStorageSettings.ApplicationSettings dictionary in the System.IO.IsolatedStorage namespace. You can store any serializable object in this dictionary with a string key. When an application exits (whether closed or deactivated), the contents of the ApplicationSettings dictionary is automatically serialized to a file in isolated storage. And when an application is launched or activated, the dictionary is automatically populated with any previously persisted data. Listing 20.1 contains the implementation of the generic Setting class that wraps the use of the ApplicationSettings dictionary.

LISTING 20.1 Setting.cs—The Setting Class Automatically Persists a Named Value to Isolated Storage

[code]

using System.IO.IsolatedStorage;
namespace WindowsPhoneApp
{
// Encapsulates a key/value pair stored in Isolated Storage ApplicationSettings
public class Setting<T>
{
string name;
T value;
T defaultValue;
bool hasValue;
public Setting(string name, T defaultValue)
{
this.name = name;
this.defaultValue = defaultValue;
}
public T Value
{
get
{
// Check for the cached value
if (!this.hasValue)
{
// Try to get the value from Isolated Storage
if (!IsolatedStorageSettings.ApplicationSettings.TryGetValue(
this.name, out this.value))
{
// It hasn’t been set yet
this.value = this.defaultValue;
IsolatedStorageSettings.ApplicationSettings[this.name] = this.value;
}
this.hasValue = true;
}
return this.value;
}
set
{
// Save the value to Isolated Storage
IsolatedStorageSettings.ApplicationSettings[this.name] = value;
this.value = value;
this.hasValue = true;
}
}
public T DefaultValue
{
get { return this.defaultValue; }
}
// “Clear” cached value:
public void ForceRefresh()
{
this.hasValue = false;
}
}
}

[/code]

Although placing items in the ApplicationSettings dictionary and retrieving them is straightforward, the Setting class cuts down on the amount of code needed by apps compared to using ApplicationSettings directly. This is especially true because users of ApplicationSettings must be prepared for keys that are not in the dictionary (typically for the first run of the app). The Setting class supports the specification of a default value, and it caches its value so it doesn’t need to fetch it from the dictionary every time.

The ForceRefresh method resolves cases where the underlying dictionary entry is modified from external code. This is leveraged by every app that uses this book’s color picker, because the color picker’s page updates ApplicationSettings directly.

The Settings Page

Alarm Clock has four pages: a main page, an alarm page, a settings page, and an instructions page. Because settings pages are one of this chapter’s lessons, we’ll look at Alarm Clock’s settings page first. Shown in Figure 20.1, the page’s XAML is in Listing 20.2.

FIGURE 20.1 Alarm Clock’s settings page exposes several settings.
FIGURE 20.1 Alarm Clock’s settings page exposes several settings.

LISTING 20.2 SettingsPage.xaml—The User Interface for Alarm Clock’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:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
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=”alarm clock”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<Grid Margin=”{StaticResource PhoneMargin}”>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Text=”Foreground color” Margin=”12,7,12,8”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<Rectangle x:Name=”ForegroundColorRectangle” Grid.Row=”1” Height=”90”
Margin=”12,0,12,18” StrokeThickness=”3” local:Tilt.IsEnabled=”True”
Stroke=”{StaticResource PhoneForegroundBrush}”
MouseLeftButtonUp=”ForegroundColorRectangle_MouseLeftButtonUp”/>
<TextBlock Text=”Background color” Grid.Column=”1” Margin=”12,7,12,8”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<Rectangle x:Name=”BackgroundColorRectangle” Grid.Row=”1” Grid.Column=”1”
Margin=”12,0,12,18” Height=”90” StrokeThickness=”3”
Stroke=”{StaticResource PhoneForegroundBrush}”
local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”BackgroundColorRectangle_MouseLeftButtonUp”/>
<toolkit:ToggleSwitch x:Name=”DisableScreenLockToggleSwitch” Grid.Row=”2”
Grid.ColumnSpan=”2” Header=”Disable screen time-out”/>
<toolkit:ToggleSwitch x:Name=”Show24HourToggleSwitch” Grid.Row=”3”
Grid.ColumnSpan=”2” Header=”24-hour clock”/>
<toolkit:ToggleSwitch x:Name=”ShowSecondsToggleSwitch” Grid.Row=”4”
Grid.ColumnSpan=”2” Header=”Show seconds”/>
<toolkit:ToggleSwitch x:Name=”EnableVibrationToggleSwitch” Grid.Row=”5”
Grid.ColumnSpan=”2” Header=”Enable vibration”/>
</Grid>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

This page makes use of several toggle switch controls from the Silverlight for Windows Phone Toolkit. Programmatically, a toggle switch acts like a check box. Visually, its default appearance is an on/off sliding switch.

The Windows Phone team publishes several guidelines for how an app’s settings page should look and behave:

  • The page should have the standard two-text-block header, and it should never scroll, even if the rest of the page does.
  • The text block that typically shows the application name should say “SETTINGS” instead, as if the user got transported to the phone’s Settings app.
  • The text block that typically shows the page title should be the application name, again to match the appearance of pages in the Settings app.
  • When the user changes a setting, it should take effect immediately. There should be no page-level apply/cancel buttons.
  • Specific actions that are irreversible should prompt the user with a message box that gives the opportunity to cancel.
  • Try to keep the settings page as concise as possible.
  • Avoid creating more than one settings page. Making use of scrolling and/or a pivot control (covered in Part IV, “Pivot, Panorama, Charts, & Graphs) is a good way to handle content that won’t fit on the screen.

The page’s code-behind is shown in Listing 20.3. It makes use of the some of the following settings defined in Settings.cs:

[code]

public static class Settings
{
// Persistent user settings from the settings page
public static readonly Setting<Color> ForegroundColor = new Setting<Color>(
“ForegroundColor”, (Color)Application.Current.Resources[“PhoneAccentColor”]);
public static readonly Setting<Color> BackgroundColor = new Setting<Color>(
“BackgroundColor”, Colors.Black);
public static readonly Setting<bool> DisableScreenLock =
new Setting<bool>(“DisableScreenLock”, true);
public static readonly Setting<bool> ShowSeconds =
new Setting<bool>(“ShowSeconds”, true);
public static readonly Setting<bool> Show24Hours =
new Setting<bool>(“Show24Hours”, false);
public static readonly Setting<bool> EnableVibration =
new Setting<bool>(“EnableVibration”, true);
// Persistent user settings from the alarm page
public static readonly Setting<DateTime> AlarmTime = new Setting<DateTime>(
“AlarmTime”, new DateTime(2010, 1, 1, 8, 0, 0));
public static readonly Setting<bool> IsAlarmOn =
new Setting<bool>(“IsAlarmOn”, false);
// Persistent state
public static readonly Setting<SupportedPageOrientation> SupportedOrientations
= new Setting<SupportedPageOrientation>(“SupportedOrientations”,
SupportedPageOrientation.PortraitOrLandscape);
public static readonly Setting<DateTime?> SnoozeTime =
new Setting<DateTime?>(“SnoozeTime”, null);
}

[/code]

LISTING 20.3 SettingsPage.xaml.cs—The Code-Behind for Alarm Clock’s Settings Page

[code]

using System;
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 OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the saved settings
this.ForegroundColorRectangle.Fill =
new SolidColorBrush(Settings.ForegroundColor.Value);
this.BackgroundColorRectangle.Fill =
new SolidColorBrush(Settings.BackgroundColor.Value);
this.DisableScreenLockToggleSwitch.IsChecked =
Settings.DisableScreenLock.Value;
this.Show24HourToggleSwitch.IsChecked = Settings.Show24Hours.Value;
this.ShowSecondsToggleSwitch.IsChecked = Settings.ShowSeconds.Value;
this.EnableVibrationToggleSwitch.IsChecked = Settings.EnableVibration.Value;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Save the settings (except the colors, already saved by the color picker)
Settings.DisableScreenLock.Value =
this.DisableScreenLockToggleSwitch.IsChecked.Value;
Settings.Show24Hours.Value = this.Show24HourToggleSwitch.IsChecked.Value;
Settings.ShowSeconds.Value = this.ShowSecondsToggleSwitch.IsChecked.Value;
Settings.EnableVibration.Value =
this.EnableVibrationToggleSwitch.IsChecked.Value;
}
void ForegroundColorRectangle_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.ForegroundColor.Value.ToString().Substring(1);
string defaultColorString =
Settings.ForegroundColor.DefaultValue.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.ForegroundColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=ForegroundColor”, UriKind.Relative));
}
void BackgroundColorRectangle_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.BackgroundColor.Value.ToString().Substring(1);
string defaultColorString =
Settings.BackgroundColor.DefaultValue.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.BackgroundColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=BackgroundColor”, UriKind.Relative));
}
}
}

[/code]

The color picker page writes the chosen color value directly to the IsolatedStorageSettings. ApplicationSettings dictionary, using the passed-in settingName as the key. This is how it is able to work with the Setting objects used by any app, and why a call to ForceRefresh is required before the Setting is accessed again.

The Alarm Page

The alarm page, shown in Figure 20.2, is basically a second settings page, but dedicated to turning the alarm on and off, and settings its time. It also has a button for testing the alarm volume, so users can make sure it is loud enough to wake them up.

FIGURE 20.2 The alarm page exposes the most important settings in the app.
FIGURE 20.2 The alarm page exposes the most important settings in the app.

Although apps should avoid having more than one settings page, this page is distinctand important-enough to warrant its own page. A user may wish to visit the alarm page every night to ensure the alarm is set correctly, whereas they may never have the need to visit the settings page. Listing 20.4 contains its XAML and Listing 20.5 contains its codebehind.

LISTING 20.4 AlarmPage.xaml—The User Interface for Alarm Clock’s Alarm Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.AlarmPage”
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”
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=”ALARM CLOCK”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”set alarm” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<StackPanel Margin=”{StaticResource PhoneMargin}”>
<toolkit:ToggleSwitch x:Name=”ToggleSwitch” Header=”Alarm”
Checked=”ToggleSwitch_IsCheckedChanged”
Unchecked=”ToggleSwitch_IsCheckedChanged”/>
<toolkit:TimePicker x:Name=”TimePicker”
ValueChanged=”TimePicker_ValueChanged”/>
<ToggleButton x:Name=”TestVolumeButton” Content=”test volume”
Margin=”0,36,0,0” Checked=”TestVolumeButton_Checked”
Unchecked=”TestVolumeButton_Unchecked”
local:Tilt.IsEnabled=”True”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Listing 20.4 leverages two controls from the Silverlight for Windows Phone Toolkit: the toggle switch and the time picker. Toggle switches don’t have an IsCheckedChanged event, so this listing attaches the ToggleSwitch_IsCheckedChanged event handler to two individual events—Checked and Unchecked.

LISTING 20.5 AlarmPage.xaml.cs—The Code-Behind for Alarm Clock’s Alarm Page

[code]

using System;
using System.Windows;
using System.Windows.Navigation;
using System.Windows.Threading;
using Microsoft.Devices;
using Microsoft.Phone.Controls;
using Microsoft.Xna.Framework.Audio; // For SoundEffectInstance
namespace WindowsPhoneApp
{
public partial class AlarmPage : PhoneApplicationPage
{
// For the sound and vibration
SoundEffectInstance alarmSound;
DispatcherTimer timer =
new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
public AlarmPage()
{
InitializeComponent();
this.timer.Tick += Timer_Tick;
// Initialize the alarm sound
this.alarmSound = SoundEffects.Alarm.CreateInstance();
this.alarmSound.IsLooped = true;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the saved settings
this.ToggleSwitch.IsChecked = Settings.IsAlarmOn.Value;
this.TimePicker.Value = Settings.AlarmTime.Value;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Save the settings (except AlarmTime, handled in TimePicker_ValueChanged)
Settings.IsAlarmOn.Value = this.ToggleSwitch.IsChecked.Value;
// Stop the vibration/sound effect if still playing
this.timer.Stop();
this.alarmSound.Stop();
}
void TimePicker_ValueChanged(object sender, DateTimeValueChangedEventArgs e)
{
// To prevent getting clobbered on way back in
Settings.AlarmTime.Value = this.TimePicker.Value.Value;
Settings.SnoozeTime.Value = null;
}
void ToggleSwitch_IsCheckedChanged(object sender, RoutedEventArgs e)
{
// If we’re currently snoozing, cancel it
Settings.SnoozeTime.Value = null;
}
void TestVolumeButton_Checked(object sender, RoutedEventArgs e)
{
// Vibrate, only if its enabled
if (Settings.EnableVibration.Value)
this.timer.Start();
// Play the sound
this.alarmSound.Play();
}
void TestVolumeButton_Unchecked(object sender, RoutedEventArgs e)
{
// Stop the sound and vibration
this.timer.Stop();
this.alarmSound.Stop();
}
void Timer_Tick(object sender, EventArgs e)
{
// Vibrate for half a second
VibrateController.Default.Start(TimeSpan.FromSeconds(.5));
}
}
}

[/code]

Notes:

  • To produce the alarm sound, this app uses sound effect APIs covered in Part V, “Audio & Video.”

The Main Page

The main page is the page that acts like a physical digital alarm clock, with its time display, day-of-the-week display, and alarm information. Its root grid contains many columns, mainly for evenly distributing the seven days of the week. Listing 20.6 contains its XAML, and Figure 20.3 shows this page with its root grid temporarily marked with ShowGridLines=”True”.

FIGURE 20.3 The main page, with grid lines showing.
FIGURE 20.3 The main page, with grid lines showing.

LISTING 20.6 MainPage.xaml—The User Interface for Alarm Clock’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}”
SupportedOrientations=”PortraitOrLandscape”>
<phone:PhoneApplicationPage.Resources>
<!– A style shared by SUN,MON,TUE,WED,THU,FRI,SAT –>
<Style x:Name=”DayOfWeekStyle” TargetType=”TextBlock”>
<Setter Property=”HorizontalAlignment” Value=”Center”/>
<Setter Property=”VerticalAlignment” Value=”Center”/>
<Setter Property=”Grid.Row” Value=”2”/>
</Style>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with three buttons and two menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.95”>
<shell:ApplicationBarIconButton Text=”set alarm”
IconUri=”/Images/appbar.alarm.png” Click=”AlarmButton_Click”/>
<shell:ApplicationBarIconButton Text=”settings”
IconUri=”/Shared/Images/appbar.settings.png” Click=”SettingsButton_Click”/>
<shell:ApplicationBarIconButton Text=”lock screen”
IconUri=”/Shared/Images/appbar.orientationUnlocked.png”
Click=”OrientationLockButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”instructions”
Click=”InstructionsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid x:Name=”Grid”>
<Grid.RowDefinitions>
<RowDefinition/> <!– Top margin –>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”75”/>
<RowDefinition Height=”Auto”/>
<RowDefinition/> <!– Bottom margin –>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name=”LeftMargin” Width=”0”/>
<ColumnDefinition/> <!– SUN –>
<ColumnDefinition/> <!– MON –>
<ColumnDefinition/> <!– TUE –>
<ColumnDefinition/> <!– WED –>
<ColumnDefinition/> <!– THU –>
<ColumnDefinition/> <!– FRI –>
<ColumnDefinition/> <!– SAT –>
<ColumnDefinition x:Name=”RightMargin” Width=”0”/>
</Grid.ColumnDefinitions>
<!– The current time –>
<local:TimeDisplay x:Name=”MainTimeDisplay” Grid.Row=”1” Grid.Column=”1”
Grid.ColumnSpan=”7” HorizontalAlignment=”Center”/>
<!– Two simple labels –>
<TextBlock x:Name=”AlarmOnTextBlock” Grid.Row=”3” Grid.Column=”1”
Grid.ColumnSpan=”3” Text=”ALARM ON” Margin=”0,-24,0,0”
HorizontalAlignment=”Right” VerticalAlignment=”Center”/>
<TextBlock x:Name=”SnoozeTextBlock” Grid.Row=”3” Grid.Column=”1”
Grid.ColumnSpan=”3” Text=”SNOOZING UNTIL” Margin=”0,24,0,0”
HorizontalAlignment=”Right” VerticalAlignment=”Center”/>
<!– The alarm/snooze time –>
<local:TimeDisplay x:Name=”AlarmTimeDisplay” ShowSeconds=”False”
Grid.Row=”3” Grid.Column=”3” Grid.ColumnSpan=”5”
HorizontalAlignment=”Right”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

The XAML uses two instances of a TimeDisplay user control, shown in the next section. The code-behind is in Listing 20.7.

LISTING 20.7 MainPage.xaml.cs—The Code-Behind for Alarm Clock’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using System.Windows.Threading;
using Microsoft.Devices;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using Microsoft.Xna.Framework.Audio; // For SoundEffectInstance
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// A timer, so we can update the display every second
DispatcherTimer timer =
new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
IApplicationBarIconButton orientationLockButton;
TextBlock[] dayOfWeekTextBlocks = new TextBlock[7];
SoundEffectInstance alarmSound;
bool tappedAlarmOff;
public MainPage()
{
InitializeComponent();
this.orientationLockButton =
this.ApplicationBar.Buttons[2] as IApplicationBarIconButton;
this.timer.Tick += Timer_Tick;
this.timer.Start();
// Initialize the alarm sound effect
SoundEffects.Initialize();
this.alarmSound = SoundEffects.Alarm.CreateInstance();
this.alarmSound.IsLooped = true;
// Add the seven day-of-week text blocks here, assigning them to an array
this.dayOfWeekTextBlocks[0] = new TextBlock { Text = “SUN”,
Style = this.DayOfWeekStyle };
Grid.SetColumn(this.dayOfWeekTextBlocks[0], 1);
this.dayOfWeekTextBlocks[1] = new TextBlock { Text = “MON”,
Style = this.DayOfWeekStyle };
Grid.SetColumn(this.dayOfWeekTextBlocks[1], 2);
this.dayOfWeekTextBlocks[2] = new TextBlock { Text = “TUE”,
Style = this.DayOfWeekStyle };
Grid.SetColumn(this.dayOfWeekTextBlocks[2], 3);
this.dayOfWeekTextBlocks[3] = new TextBlock { Text = “WED”,
Style = this.DayOfWeekStyle };
Grid.SetColumn(this.dayOfWeekTextBlocks[3], 4);
this.dayOfWeekTextBlocks[4] = new TextBlock { Text = “THU”,
Style = this.DayOfWeekStyle };
Grid.SetColumn(this.dayOfWeekTextBlocks[4], 5);
this.dayOfWeekTextBlocks[5] = new TextBlock { Text = “FRI”,
Style = this.DayOfWeekStyle };
Grid.SetColumn(this.dayOfWeekTextBlocks[5], 6);
this.dayOfWeekTextBlocks[6] = new TextBlock { Text = “SAT”,
Style = this.DayOfWeekStyle };
Grid.SetColumn(this.dayOfWeekTextBlocks[6], 7);
for (int i = 0; i < this.dayOfWeekTextBlocks.Length; i++)
this.Grid.Children.Add(dayOfWeekTextBlocks[i]);
// Allow the app to run (making the alarm sound and vibration)
// even when the phone is locked.
// Once disabled, you cannot re-enable the default behavior!
PhoneApplicationService.Current.ApplicationIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Restore the ability for the screen to auto-lock when on other pages
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Enabled;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
this.tappedAlarmOff = false;
// Respect the saved settings
this.Foreground = new SolidColorBrush(Settings.ForegroundColor.Value);
this.Grid.Background = new SolidColorBrush(Settings.BackgroundColor.Value);
this.ApplicationBar.ForegroundColor = Settings.ForegroundColor.Value;
this.ApplicationBar.BackgroundColor = Settings.BackgroundColor.Value;
// While on this page, don’t allow the screen to auto-lock
if (Settings.DisableScreenLock.Value)
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Disabled;
// Restore the orientation setting to whatever it was last time
this.SupportedOrientations = Settings.SupportedOrientations.Value;
// If the restored value is not PortraitOrLandscape, then the orientation
// has been locked. Change the state of the application bar button to
// reflect this.
if (this.SupportedOrientations !=
SupportedPageOrientation.PortraitOrLandscape)
{
this.orientationLockButton.Text = “unlock”;
this.orientationLockButton.IconUri = new Uri(
“/Shared/Images/appbar.orientationLocked.png”, UriKind.Relative);
}
RefreshDisplays();
// Don’t wait for the next tick
Timer_Tick(this, EventArgs.Empty);
}
protected override void OnOrientationChanged(OrientationChangedEventArgs e)
{
base.OnOrientationChanged(e);
RefreshDisplays();
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
if (this.alarmSound.State == SoundState.Playing)
{
// Turn the alarm off
this.tappedAlarmOff = true;
this.alarmSound.Stop();
// Set the snooze time to five minutes from now
DateTime currentTimeWithoutSeconds = DateTime.Now;
currentTimeWithoutSeconds =
currentTimeWithoutSeconds.AddSeconds(-currentTimeWithoutSeconds.Second);
Settings.SnoozeTime.Value = currentTimeWithoutSeconds.AddMinutes(5);
RefreshDisplays();
}
else
{
// Toggle the application bar visibility
this.ApplicationBar.IsVisible = !this.ApplicationBar.IsVisible;
}
}
void RefreshDisplays()
{
if (IsMatchingOrientation(PageOrientation.Portrait))
{
// Adjust the margins for portrait
this.LeftMargin.Width = new GridLength(12);
this.RightMargin.Width = new GridLength(12);
// Set the font size accordingly
if (Settings.ShowSeconds.Value)
this.MainTimeDisplay.FontSize = 182;
else
this.MainTimeDisplay.FontSize = 223;
}
else
{
// Adjust the margins for landscape
this.LeftMargin.Width = new GridLength(92);
this.RightMargin.Width = new GridLength(92);
// Set the font size accordingly
if (Settings.ShowSeconds.Value)
this.MainTimeDisplay.FontSize = 251;
else
this.MainTimeDisplay.FontSize = 307;
}
this.AlarmTimeDisplay.FontSize = this.MainTimeDisplay.FontSize / 2;
// Respect the settings in the two time displays
this.MainTimeDisplay.Show24Hours = Settings.Show24Hours.Value;
this.AlarmTimeDisplay.Show24Hours = Settings.Show24Hours.Value;
this.MainTimeDisplay.ShowSeconds = Settings.ShowSeconds.Value;
this.MainTimeDisplay.Initialize();
this.AlarmTimeDisplay.Initialize();
if (Settings.IsAlarmOn.Value)
{
if (Settings.SnoozeTime.Value != null)
{
// Show that we’re snoozing
this.AlarmOnTextBlock.Opacity = .1;
this.SnoozeTextBlock.Opacity = 1;
this.AlarmTimeDisplay.Time = Settings.SnoozeTime.Value;
}
else
{
// Show when the alarm will go off
this.AlarmOnTextBlock.Opacity = 1;
this.SnoozeTextBlock.Opacity = .1;
this.AlarmTimeDisplay.Time = Settings.AlarmTime.Value;
}
}
else
{
// No alarm, no snooze
this.AlarmOnTextBlock.Opacity = .1;
this.SnoozeTextBlock.Opacity = .1;
this.AlarmTimeDisplay.Time = null;
}
}
void Timer_Tick(object sender, EventArgs e)
{
// Refresh the current time
this.MainTimeDisplay.Time = DateTime.Now;
// Keep the day of the week up-to-date
for (int i = 0; i < this.dayOfWeekTextBlocks.Length; i++)
this.dayOfWeekTextBlocks[i].Opacity = .2;
this.dayOfWeekTextBlocks[(int)DateTime.Now.DayOfWeek].Opacity = 1;
// If the alarm sound is playing, accompany it with vibration
// (if that setting is enabled)
if (this.alarmSound.State == SoundState.Playing
&& Settings.EnableVibration.Value)
VibrateController.Default.Start(TimeSpan.FromSeconds(.5));
if (Settings.IsAlarmOn.Value)
{
TimeSpan timeToAlarm =
Settings.AlarmTime.Value.TimeOfDay – DateTime.Now.TimeOfDay;
// Let the alarm go off up to 60 seconds after the designated time
// (in case the app wasn’t running at the beginning of the minute or it
// was on a different page)
if (!this.tappedAlarmOff && this.alarmSound.State != SoundState.Playing
&& timeToAlarm.TotalSeconds <= 0 && timeToAlarm.TotalSeconds > -60)
{
this.alarmSound.Play();
return; // Don’t bother with snooze
}
}
if (Settings.SnoozeTime.Value != null)
{
TimeSpan timeToSnooze =
Settings.SnoozeTime.Value.Value.TimeOfDay – DateTime.Now.TimeOfDay;
// Let the snoozed alarm go off up to 60 seconds after the designated time
// (in case the app wasn’t running at the beginning of the minute or it
// was on a different page)
if (this.alarmSound.State != SoundState.Playing
&& timeToSnooze.TotalSeconds <= 0 && timeToSnooze.TotalSeconds > -60)
{
this.alarmSound.Play();
}
}
}
bool IsMatchingOrientation(PageOrientation orientation)
{
return ((this.Orientation & orientation) == orientation);
}
// Application bar handlers
void AlarmButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/AlarmPage.xaml”,
UriKind.Relative));
}
void SettingsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
// The “orientation lock” feature
void OrientationLockButton_Click(object sender, EventArgs e)
{
// Check the value of SupportedOrientations to see if we’re currently
// “locked” to a value other than PortraitOrLandscape.
if (this.SupportedOrientations !=
SupportedPageOrientation.PortraitOrLandscape)
{
// We are locked, so unlock now
this.SupportedOrientations = SupportedPageOrientation.PortraitOrLandscape;
// Change the state of the application bar button to reflect this
this.orientationLockButton.Text = “lock screen”;
this.orientationLockButton.IconUri = new Uri(
“/Shared/Images/appbar.orientationUnlocked.png”, UriKind.Relative);
}
else
{
// We are unlocked, so lock to the current orientation now
if (IsMatchingOrientation(PageOrientation.Portrait))
this.SupportedOrientations = SupportedPageOrientation.Portrait;
else
this.SupportedOrientations = SupportedPageOrientation.Landscape;
// Change the state of the application bar button to reflect this
this.orientationLockButton.Text = “unlock”;
this.orientationLockButton.IconUri = new Uri(
“/Shared/Images/appbar.orientationLocked.png”, UriKind.Relative);
}
// Remember the new setting after the page has been left
Settings.SupportedOrientations.Value = this.SupportedOrientations;
}
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=Alarm Clock”, UriKind.Relative));
}
}
}

[/code]

Notes:

  • The seven day-of-the-week text blocks are constructed and added to their parent grid in code-behind rather than in XAML. This is done because having them in an array is convenient for the code that needs to illuminate the appropriate one.
  • This page sets ApplicationIdleDetectionMode to Disabled so it runs while the screen is locked (and off). This is the most likely what users would want to do, rather than leaving the screen on all night. However, if they wish to do so, this page also sets UserIdleDetectionMode to Disabled so it won’t automatically lock. If the users want to turn off their screen, they must do it manually.
  • This page does something a bit unique; it applies the chosen foreground and background colors to the application bar in addition to the main display. The chosen background color is applied to the grid rather than the page. Recall that setting a page’s background color has no effect.
  • If the alarm is going off, tapping the screen snoozes it by five minutes. (This is five minutes from the beginning of the current minute, as it would be confusing if the alarm didn’t go off at a minute boundary.) Otherwise, tapping the screen toggles the visibility of the application bar. This provides a way for the user to get a more realistic and minimalistic display.
  • The various text blocks are illuminated with an opacity of 1, and they are “turned off” with an opacity of either .1 or .2. (The variation is there just to give more realism.) The code inside of Timer_Tick leverages the array of day-of-the-week text blocks, using DateTime.Now.DayOfWeek (a number from 0 to 6, inclusive) as the index into the array.

The TimeDisplay User Control

What makes this app special is its time display that uses seven-segment digital numerals. There are a few ways to accomplish such a display. For example, you could create vectorbased shapes and illuminate the appropriate segments at an appropriate time with their fill settings. This app takes a somewhat-easier approach: using a custom font. Listing 20.8 contains the XAML for the TimeDisplay user control that implements the seven-segment display.

Before using a custom font,make sure you have permission!

Using a custom font is easy.Using it legally is another story. Be sure you understand the rules for any specific font you wish to use.The custom font used in Listing 20.8 is called “Pendule Ornamental,” created by Scott Lawrence and available at http://fontstruct.fontshop.com/ fontstructions/show/200136. It is licensed under a Creative Commons Attribution Share Alike license (http://creativecommons.org/licenses/by-sa/3.0/).

LISTING 20.8 TimeDisplay.xaml—The User Interface for the TimeDisplay User Control

[code]

<UserControl x:Class=”WindowsPhoneApp.TimeDisplay”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
<Grid x:Name=”Grid” Background=”Transparent”>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”Auto”/>
<ColumnDefinition Width=”Auto”/>
</Grid.ColumnDefinitions>
<!– The background “off” segments –>
<TextBlock FontFamily=”Fonts/pendule_ornamental.ttf#pendule ornamental”
Opacity=”.1”>
<!– It’s important not to have whitespace between the runs!–>
<Run x:Name=”TimeBackgroundRun”>88:88</Run><Run
x:Name=”SecondsBackgroundRun”>88</Run>
</TextBlock>
<!– The foreground “on” segments –>
<TextBlock FontFamily=”Fonts/pendule_ornamental.ttf#pendule ornamental”>
<!– It’s important not to have whitespace between the runs!–>
<Run x:Name=”TimeRun”/><Run x:Name=”SecondsRun”/>
</TextBlock>
<!– AM / PM –>
<TextBlock x:Name=”AMTextBlock” Grid.Column=”1” Text=”AM”
FontSize=”{StaticResource PhoneFontSizeNormal}”
HorizontalAlignment=”Center” Margin=”4,0,0,0”/>
<TextBlock x:Name=”PMTextBlock” Grid.Column=”1” Text=”PM”
FontSize=”{StaticResource PhoneFontSizeNormal}”
HorizontalAlignment=”Center” Margin=”4,24,0,0”/>
</Grid>
</UserControl>

[/code]

Notes:

  • To use a custom font, simply include the font file in your project (with a Build Action of Content), and then reference it in the FontFamily value on any text block or text box (or element whose font family will be inherited by relevant child elements). The syntax is

    [code] pathAndFilename#fontName [/code]

    You can see the correct value for fontName by opening the font file via Windows Explorer.

  • To give the effect of dim “off” segments in each numeral, this user control actually uses two overlapping text blocks. The one in the back has all segments activated at all times (with a value of 88:88 for the hour/minutes and a value of 88 for the seconds), but is given an opacity of .1. The one in the front displays the actual time at full opacity.
  • Each text block contains two runs, one for the larger hour/minutes and one for the smaller seconds. (The sizes and values are set in code-behind.) A surprising fact about a text block is that its content property is not its Text property, but rather a property called Inlines. Although a type converter enables you to set it to a simple string in XAML, Inlines can be set to a collection of Inline objects. There are two classes that derive from the abstract Inline class: Run and LineBreak.
  • On a run, you can set the same formatting properties available on a text block, such as FontFamily, FontSize, FontStyle, FontWeight, Foreground, and TextDecorations. Runs, therefore, provide a convenient way to create text with mixed formatting all inside a single text block.

LISTING 20.9 TimeDisplay.xaml.cs—The Code-Behind for the TimeDisplay User Control

[code]

using System;
using System.Windows;
using System.Windows.Controls;
namespace WindowsPhoneApp
{
public partial class TimeDisplay : UserControl
{
DateTime? time;
public TimeDisplay()
{
InitializeComponent();
Time = null;
}
public void Initialize()
{
if (!this.ShowSeconds)
{
// Remove the seconds display
this.SecondsRun.Text = null;
this.SecondsBackgroundRun.Text = null;
}
// Hide AM and PM in 24-hour mode
this.AMTextBlock.Visibility =
this.Show24Hours ? Visibility.Collapsed : Visibility.Visible;
this.PMTextBlock.Visibility =
this.Show24Hours ? Visibility.Collapsed : Visibility.Visible;
// The seconds font size is always half of whatever the main font size is
this.SecondsBackgroundRun.FontSize = this.SecondsRun.FontSize =
this.FontSize / 2;
}
public bool ShowSeconds { get; set; }
public bool Show24Hours { get; set; }
public DateTime? Time
{
get { return this.time; }
set
{
this.time = value;
if (this.time == null)
{
// Clear everything
this.TimeRun.Text = null;
this.SecondsRun.Text = null;
this.AMTextBlock.Opacity = .1;
this.PMTextBlock.Opacity = .1;
return;
}
string formatString = this.Show24Hours ? “H:mm” : “h:mm”;
// The hour needs a leading space if it ends up being only one digit
if ((this.Show24Hours && this.time.Value.Hour < 10) ||
(!this.Show24Hours &&
(this.time.Value.Hour % 12 < 10 && this.time.Value.Hour % 12 > 0)))
formatString = “ “ + formatString;
this.TimeRun.Text = this.time.Value.ToString(formatString);
if (this.ShowSeconds)
this.SecondsRun.Text = this.time.Value.ToString(“ss”);
if (!this.Show24Hours)
{
// Show either AM or PM
if (this.time.Value.Hour < 12)
{
this.AMTextBlock.Opacity = 1;
this.PMTextBlock.Opacity = .1;
}
else
{
this.AMTextBlock.Opacity = .1;
this.PMTextBlock.Opacity = 1;
}
}
}
}
}
}

[/code]

The Finished Product

Alarm Clock (Settings,Toggle Switch, Custom Font)