Creating a Graphical User Interface

Creating the GUI Controls

A graphical user interface (GUI) is absolutely essential for a game to be successful, even if that means using nothing more than labels and buttons on the screen that the user can click on.

Sprite Class Improvements

Modifying the Sprite Class

To make the GUI controls more effective, the Sprite class must be tweaked just a little.

  1. We need to change the definition of p_content and p_spriteBatch from private to protected so that they will be accessible to classes that inherit from Sprite. This way, we can load assets and draw without creating new reference variables in every subclass. Open the Sprite class and make the change:
    [code]
    protected ContentManager p_content;
    protected SpriteBatch p_spriteBatch;
    [/code]
  2. Just to be sure we are on the same page despite the changes made to this class in the past, here is the Load() method. Ignore past changes and just note this current version, which shows that the size and origin properties have been moved out of the try block:
    [code]
    public virtual bool Load(string assetName)
    {
    try
    {
    image = p_content.Load<Texture2D>(assetName);
    }
    catch (Exception) { return false; }
    size = new Vector2(image.Width, image.Height);
    origin = new Vector2(image.Width / 2, image.Height / 2);
    return true;
    }
    [/code]
  3. Add an error-handling line to the Draw() method so that it won’t crash the program if the image is null. This is a common verification. Since our GUI controls will be using a few images in interesting ways, we just want to ensure that any image that is not loaded correctly won’t crash the program—instead, it will just not show up.
    [code]
    public virtual void Draw()
    {
    if (!visible) return;
    if (image == null) return;
    . . .
    }
    [/code]

GUI Base Class: Control

All the GUI classes will be found in the GUI.cs source code file for the sake of convenience. Within that file, the classes will be wrapped inside the GameLibrary namespace (the same namespace used by Sprite and Animation).

[code]
namespace GameLibrary
{
. . .
}
[/code]

The base GUI class is called Control, and it is primarily used to create a reference to the ContentManager, SpriteBatch, and SpriteFont objects used in a game—all of which are needed by the GUI. Control inherits from Sprite, so it supplies GUI controls (declared as subclasses of Control) with all the features of Sprite, including loading and drawing. Methods are declared as virtual or override so they can be used and overridden in each subclass. There are certainly more services the base class could provide, such as touch input, but it turns out (during development) that most of that code must reside in each individual class. Listing 20.1 contains the source code for the Control class.

LISTING 20.1 Source Code for the Control Class

[code]
public abstract class Control : Sprite
{
protected SpriteFont p_font;
public Control(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch)
{
p_font = font;
}
public override bool Load(string filename)
{
return base.Load(filename);
}
public virtual void Update(TouchLocation touch)
{
}
public override void Draw()

{
base.Draw();
}
}
[/code]

Label Control

A Label is the most fundamental type of GUI control, with the simple task of displaying a text message on the screen. This is more important than it might at first seem, because a Label control can be moved anywhere on the screen without affecting the call to Label.Draw() from the main program. This Label class is rather basic, providing a shadow feature with customizable Color properties for the text and shadow. Two Labels will be used in the sample project later in this hour. Listing 20.2 contains the source code for the Label class.

LISTING 20.2 Source Code for the Label Class

[code]
public class Label : Control
{
public string text;
public Color shadowColor;
public Color textColor;
public bool UseShadow;
public Label(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch, font)
{
text = ““;
color = Color.White;
textColor = Color.White;
shadowColor = Color.Black;
UseShadow = true;
}
public override void Update(TouchLocation touch)
{
base.Update(touch);
}
public override void Draw()
{
if (UseShadow)
{
p_spriteBatch.DrawString(p_font, text,
new Vector2(position.X – 2, position.Y – 2), shadowColor);
}
p_spriteBatch.DrawString(p_font, text, position, textColor);
}
public Vector2 TextSize()
{
return p_font.MeasureString(text);
}
}
[/code]

Button Control

A Button is the second most common type of control needed for a rudimentary GUI system. Our Button class will load a 64×64 bitmap file called button.png (which must be in the content project). The great thing about this is that you can replace the image with one of your own. Due to the way the class works, I recommend using an image with the same dimensions but with your own “skin” theme. The button used in the example this hour is a gray box with a white outline. An important feature for a Button control is to display text and respond to user tap events. Our Button goes further by allowing its background and text colors to be changed independently for a customized look. Listing 20.3 contains the source code for the Button class.

LISTING 20.3 Source Code for the Button Class

[code]
public class Button : Control
{
public string text;
public Color shadowColor;
public Color textColor;
public bool UseShadow;
public bool Tapped;
public Button(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch, font)
{
text = ““;
color = Color.White;
textColor = Color.White;
shadowColor = Color.Black;
UseShadow = true;
Load(“button”);
}
public override void Update(TouchLocation touch)
{
base.Update(touch);
Tapped = false;
if (touch.State == TouchLocationState.Pressed)
{
Rectangle rect = Boundary();
Vector2 pos = touch.Position;
Point point = new Point((int)pos.X, (int)pos.Y);
if (rect.Contains(point))
{
Tapped = true;
}
}
}
public override void Draw()
{
base.Draw();
Vector2 size = TextSize();
Vector2 pos2 = new Vector2(position.X + 2, position.Y + 2);
Vector2 pivot = new Vector2(size.X / 2, size.Y / 2);
p_spriteBatch.DrawString(p_font, text, position, shadowColor,
0.0f, pivot, 1.0f, SpriteEffects.None, zindex);
p_spriteBatch.DrawString(p_font, text, pos2, textColor, 0.0f, pivot,
1.0f, SpriteEffects.None, zindex);
}
public Vector2 TextSize()
{
return p_font.MeasureString(text);
}
}
[/code]

Horizontal Slider Control

A slider control makes it possible to adjust a setting or to control some aspect of a game directly by the user, and resembles a movable sliding lever on the screen. There are two types of slider: horizontal and vertical. Although one common class could be used for both slider orientations, it would be more coding work, so it is more effective to just separate them into HSlider and VSlider controls. This is definitely a complex type of control compared to Label and Button. HSlider loads three images, so these bitmap files must all be found in the content project for the GUI code to run properly:

  • hslider_bar.png
  • hslider_end.png
  • button.png

Remember, when you are creating your own game using these GUI controls, that you can skin the controls to your own liking. The slider button needn’t be a circle at all! It can be any shape, including a custom image or a picture of a dragon—it doesn’t matter, and it’s up to you!

The left and right end images are shared, so if you create a custom skin for the control, be sure that the end images are interchangeable. The middle piece is a line one (1) pixel wide, scaled to the width of the control (set with the HSlider.Limit property). If the limit is 100, the one-pixel-wide image is scaled by 100 times to reach the edge! The scale as well as other properties are borrowed from the base Sprite class embedded in Control, inherited by HSlider. There isn’t much error handling, so if you try to set Limit to a negative number, it just will not work right or will crash. Listing 20.4 contains the source code for the HSlider class.

LISTING 20.4 Source Code for the HSlider Class

[code]
public class HSlider : Control
{
public bool Moving;
public Vector2 start;
private int p_value;
private int p_limit;
Sprite sprLeftEnd, sprRightEnd, sprBar;
public HSlider(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch, font)
{
scale = 1.0f;
start = Vector2.Zero;
Load(“slider_tab”);
sprLeftEnd = new Sprite(content, spriteBatch);
sprLeftEnd.Load(“hslider_end”);
sprLeftEnd.origin = new Vector2(3, 16);
sprRightEnd = new Sprite(content, spriteBatch);
sprRightEnd.Load(“hslider_end”);
sprRightEnd.origin = new Vector2(0, 16);
sprBar = new Sprite(content, spriteBatch);
sprBar.Load(“hslider_bar”);
sprBar.origin = new Vector2(0, 16);
Limit = 100;
}
public int Value
{
get { return p_value; }
set
{
p_value = value;
if (p_value < 0) p_value = 0;
if (p_value > p_limit) p_value = p_limit;
position.X = start.X + p_value;
}
}
public int Limit
{
get { return p_limit; }
set
{
p_limit = value;
sprBar.scaleV = new Vector2((float)
(p_limit + this.image.Width+1), 1.0f);
}
}
public override void Update(TouchLocation touch)
{
base.Update(touch);
Moving = false;
if (touch.State == TouchLocationState.Moved)
{
Rectangle rect = Boundary();
Point point = new Point((int)touch.Position.X,
(int)touch.Position.Y);
if (rect.Contains(point))
{
Vector2 relative = Vector2.Zero;
relative.X = touch.Position.X – position.X;
position.X += relative.X;
Value = (int)(position.X – start.X);
if (position.X < start.X)
position.X = start.X;
else if (p_value > p_limit)
position.X -= relative.X;
Moving = true;
}
}
}
public override void Draw()
{
//draw ends
sprLeftEnd.position = new Vector2(start.X – 16, start.Y);
sprLeftEnd.color = this.color;
sprLeftEnd.Draw();
sprRightEnd.position = new Vector2(start.X + 16 + p_limit, start.Y);
sprRightEnd.color = this.color;
sprRightEnd.Draw();
//draw middle bar
sprBar.position = new Vector2(start.X – 16, start.Y);
sprBar.color = this.color;
sprBar.Draw();
//draw sliding circle
base.Draw();
//draw value text
Vector2 size = p_font.MeasureString(p_value.ToString());
p_spriteBatch.DrawString(p_font, p_value.ToString(), position,
Color.Black, 0.0f, new Vector2(size.X/2, size.Y/2), 0.6f,
SpriteEffects.None, 1.0f);
}
public void SetStartPosition(Vector2 pos)
{
position = pos;
start = pos;
}
}
[/code]

Vertical Slider Control

The Vertical Slider control, or VSlider, shares all the same functionality as HSlider, but calculations are shifted 90 degrees in a vertical orientation. So, all the “X” properties used in the HSlider’s functionality become “Y” properties in VSlider in order for it to work properly. Here are the bitmaps required by the control (and note that button.png is shared):

  • vslider_bar.png
  • vslider_end.png
  • button.png

Listing 20.5 contains the source code for the VSlider class.

LISTING 20.5 Source Code for the VSlider Class

[code]
public class VSlider : Control
{
public bool Moving;
public Vector2 start;
private int p_value;
private int p_limit;
Sprite sprTopEnd, sprBottomEnd, sprBar;
public VSlider(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch, font)
{
scale = 1.0f;
start = Vector2.Zero;
Load(“slider_tab”);
sprTopEnd = new Sprite(content, spriteBatch);
sprTopEnd.Load(“vslider_end”);
sprTopEnd.origin = new Vector2(16, 3);
sprBottomEnd = new Sprite(content, spriteBatch);
sprBottomEnd.Load(“vslider_end”);
sprBottomEnd.origin = new Vector2(16, 0);
sprBar = new Sprite(content, spriteBatch);
sprBar.Load(“vslider_bar”);
sprBar.origin = new Vector2(16, 0);
Limit = 100;
}
public int Value
{
get { return p_value; }
set
{
p_value = value;
if (p_value < 0) p_value = 0;
if (p_value > p_limit) p_value = p_limit;
position.Y = start.Y + p_value;
}
}
public int Limit
{
get { return p_limit; }
set
{
p_limit = value;
sprBar.scaleV = new Vector2(1.0f, (float)
(p_limit + this.image.Height + 1));
}
}
public override void Update(TouchLocation touch)
{
base.Update(touch);
Moving = false;
if (touch.State == TouchLocationState.Moved)
{
Rectangle rect = Boundary();
Point point = new Point((int)touch.Position.X,
(int)touch.Position.Y);
if (rect.Contains(point))
{
Vector2 relative = Vector2.Zero;
relative.Y = touch.Position.Y – position.Y;
position.Y += relative.Y;
Value = (int)(position.Y – start.Y);
if (position.Y < start.Y)
position.Y = start.Y;
else if (p_value > p_limit)
position.Y -= relative.Y;
Moving = true;
}
}
}
public override void Draw()
{
//draw ends
sprTopEnd.position = new Vector2(start.X, start.Y – 16);
sprTopEnd.color = this.color;
sprTopEnd.Draw();
sprBottomEnd.position = new Vector2(start.X, start.Y + 16 + p_limit);
sprBottomEnd.color = this.color;
sprBottomEnd.Draw();
//draw middle bar
sprBar.position = new Vector2(start.X, start.Y – 16);
sprBar.color = this.color;
sprBar.Draw();
//draw sliding circle
base.Draw();
//draw value text
Vector2 size = p_font.MeasureString(p_value.ToString());
p_spriteBatch.DrawString(p_font, p_value.ToString(), position,
Color.Black, 0.0f, new Vector2(size.X / 2, size.Y / 2), 0.6f,
SpriteEffects.None, zindex);
}
public void SetStartPosition(Vector2 pos)
{
position = pos;
start = pos;
}
}
[/code]

Demonstrating the GUI Controls

The initialization code for a GUI demo or a game using GUI controls will always be much more involved and code-intensive than the processing code where the controls are updated and drawn, because there are so many properties involved in creating and customizing a nice-looking, interactive GUI. Our example this hour demonstrates a GUI with Labels, Buttons, HSliders, and VSliders, and is quite functional, as you can see in Figure 20.1. The source code for the GUI Demo program is found in Listing 20.6.

The example demonstrates labels, buttons, and sliders.
FIGURE 20.1 The example demonstrates labels, buttons, and sliders.

On the left is a vertical slider used to adjust the background color. Why? Just to show that the slider works and does something interesting. Maybe in a game a VSlider would be used to adjust the power level of a catapult or an artillery gun. Really, the use for these controls is up to the game’s designer and is just implemented by the programmer (or team). On the right side are three buttons labeled RED, GREEN, and BLUE. Beside each button is a slider.

Clicking a button changes the color component to a random value from 0 to 255, and automatically moves the slider to that location. The slider can also be moved manually, and this in turn will change the button’s color to reflect the change to that color component. The end result of all this color manipulation is seen in the small, unassuming Exit button at the lower-right corner of the screen. Note that the Limit property of both HSlider and VSlider changes its overall size and defines the limits of the sliding button. The three color sliders have a range of 0 to 255, whereas the smaller vertical slider has a range of 0 to 100.

LISTING 20.6 Source Code for the GUI Demo Program

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont font;
Random rand;
TouchLocation oldTouch;
Label lblTitle, lblColor;
Button[] buttons;
HSlider[] hsliders;
VSlider vslider;
Color bgcolor;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
rand = new Random();
bgcolor = Color.CornflowerBlue;
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
lblTitle = new Label(Content, spriteBatch, font);
lblTitle.text = “Graphical User Interface Demo”;
lblTitle.position = new Vector2(400 – lblTitle.TextSize().X / 2, 0);
//create buttons
buttons = new Button[4];
buttons[0] = new Button(Content, spriteBatch, font);
buttons[0].text = “RED”;
buttons[0].position = new Vector2(400, 150);
buttons[0].textColor = Color.Red;
buttons[0].color = Color.DarkRed;
buttons[0].scaleV = new Vector2(1.5f, 1.0f);
buttons[1] = new Button(Content, spriteBatch, font);
buttons[1].text = “GREEN”;
buttons[1].position = new Vector2(400, 230);
buttons[1].textColor = Color.Green;
buttons[1].color = Color.DarkGreen;
buttons[1].scaleV = new Vector2(1.5f, 1.0f);
buttons[2] = new Button(Content, spriteBatch, font);
buttons[2].text = “BLUE”;
buttons[2].position = new Vector2(400, 310);
buttons[2].textColor = Color.Cyan;
buttons[2].color = Color.DarkCyan;
buttons[2].scaleV = new Vector2(1.5f, 1.0f);
buttons[3] = new Button(Content, spriteBatch, font);
buttons[3].text = “Exit”;
buttons[3].position = new Vector2(750, 450);
buttons[3].scaleV = new Vector2(1.2f, 0.8f);
//create horizontal sliders for color editing
hsliders = new HSlider[3];
hsliders[0] = new HSlider(Content, spriteBatch, font);
hsliders[0].SetStartPosition(new Vector2(500, 150));
hsliders[0].color = Color.Red;
hsliders[0].Limit = 255;
hsliders[1] = new HSlider(Content, spriteBatch, font);
hsliders[1].SetStartPosition(new Vector2(500, 230));
hsliders[1].color = Color.LightGreen;
hsliders[1].Limit = 255;
hsliders[2] = new HSlider(Content, spriteBatch, font);
hsliders[2].SetStartPosition(new Vector2(500, 310));
hsliders[2].color = Color.Cyan;
hsliders[2].Limit = 255;
//create vertical slider for bg color editing
vslider = new VSlider(Content, spriteBatch, font);
vslider.SetStartPosition(new Vector2(140, 170));
vslider.color = Color.Yellow;
vslider.Limit = 100;
//create label for slider
lblColor = new Label(Content, spriteBatch, font);
lblColor.text = “Background Color”;
lblColor.position = new Vector2( 140 – lblColor.TextSize().X/2,
100);
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
TouchCollection touchInput = TouchPanel.GetState();
if (touchInput.Count > 0)
{
TouchLocation touch = touchInput[0];
oldTouch = touch;
lblTitle.Update(touch);
UpdateButtons(touch);
UpdateSliders(touch);
vslider.Update(touch);
lblColor.Update(touch);
}
base.Update(gameTime);
}
void UpdateButtons(TouchLocation touch)
{
//update buttons
int tapped = -1;
for (int n = 0; n < buttons.Length; n++)
{
buttons[n].Update(touch);
if (buttons[n].Tapped) tapped = n;
}
//was a button tapped?
int c = rand.Next(256);
switch (tapped)
{
case 0:
buttons[0].color = new Color(c, 0, 0);
hsliders[0].Value = c;
break;
case 1:
buttons[1].color = new Color(0, c, 0);
hsliders[1].Value = c;
break;
case 2:
buttons[2].color = new Color(0, 0, c);
hsliders[2].Value = c;
break;
case 3:
this.Exit();
break;
}
}
void UpdateSliders(TouchLocation touch)
{
//update horizontal sliders
int moving = -1;
for (int n = 0; n < hsliders.Length; n++)
{
hsliders[n].Update(touch);
if (hsliders[n].Moving) moving = n;
}
switch(moving)
{
case 0:
buttons[0].color = new Color(hsliders[0].Value, 0, 0);
break;
case 1:
buttons[1].color = new Color(0, hsliders[1].Value, 0);
break;
case 2:
buttons[2].color = new Color(0, 0, hsliders[2].Value);
break;
}
//colorize Exit button based on colors
buttons[3].color = new Color(hsliders[0].Value,
hsliders[1].Value, hsliders[2].Value);
//update vertical slider
if (vslider.Moving)
{
bgcolor = Color.CornflowerBlue;
bgcolor.R -= (byte)vslider.Value;
bgcolor.G -= (byte)vslider.Value;
bgcolor.B -= (byte)vslider.Value;
}
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(bgcolor);
spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.AlphaBlend);
lblTitle.Draw();
foreach (Button b in buttons)
b.Draw();
foreach (HSlider hs in hsliders)
hs.Draw();
vslider.Draw();
lblColor.Draw();
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);
}
}
[/code]

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.

Passwords & Secrets (Encryption & Observable Collections)

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

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

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

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

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

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

Basic Cryptography

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

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

[code]

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

[/code]

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

Salt in Cryptography

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

The LoginControl User Control

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

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

Listing 21.2 contains the XAML for this control.

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

[code]

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

[/code]

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

Listing 21.3 contains the code-behind.

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

[code]

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

[/code]

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

    [code]

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

    [/code]

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

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

    [code]

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

    [/code]

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

The Change Password Page

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

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

[code]
<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.ChangePasswordPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”PASSWORDS &amp; SECRETS”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”change password”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<!– The user control –>
<local:LoginControl Grid.Row=”1” Closed=”LoginControl_Closed”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

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

[code]

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

[/code]

The Main Page

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

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

The User Interface

Listing 21.6 contains the XAML for the main page.

 

LISTING 21.6 MainPage.xaml—The User Interface for Password & Secrets’Main Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<phone:PhoneApplicationPage.Resources>
<local:DateConverter x:Key=”DateConverter”/>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with 3 menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”show password hint”
Click=”PasswordMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”instructions”
Click=”InstructionsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”more apps”
Click=”MoreAppsMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid Background=”Transparent”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Grid.Row=”0”
Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”PASSWORDS &amp; SECRETS”
Style=”{StaticResource PhoneTextTitle0Style}”/>
</StackPanel>
<!– Show this when there are no notes –>
<TextBlock Name=”NoItemsTextBlock” Grid.Row=”1” Text=”No notes”
Visibility=”Collapsed” Margin=”22,17,0,0”
Style=”{StaticResource PhoneTextGroupHeaderStyle}”/>
<!– The list box containing notes –>
<ListBox x:Name=”ListBox” Grid.Row=”1” ItemsSource=”{Binding}”
SelectionChanged=”ListBox_SelectionChanged”>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<!– The title, in a style matching the note –>
<Border Background=”{Binding ScreenBrush}” Margin=”24,0” Width=”800”
MinHeight=”60” local:Tilt.IsEnabled=”True”>
<TextBlock Text=”{Binding Title}” FontSize=”{Binding TextSize}”
Foreground=”{Binding TextBrush}” Margin=”12”
VerticalAlignment=”Center”/>
</Border>
<!– The modified date –>
<TextBlock Foreground=”{StaticResource PhoneSubtleBrush}”
Text=”{Binding Modified, Converter={StaticResource DateConverter}}”
Margin=”24,0,0,12”/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!– The user control –>
<local:LoginControl x:Name=”LoginControl” Grid.Row=”1”
Closed=”LoginControl_Closed”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

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

Value Converters

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

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

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

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

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

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

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

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

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

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

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

[code]

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

[/code]

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

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

[code]

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

[/code]

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

[code]

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

[/code]

Additional Data for Value Converters

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

[code]

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

[/code]

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

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

[code]

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

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

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

[/code]

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

The Code-Behind

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

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

[code]

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

[/code]

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

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

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

The INotifyPropertyChanged Interface

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

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

[code]

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

[/code]

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

INotifyCollectionChanged

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

The Details Page

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

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

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

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.DetailsPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<!– The application bar, with three buttons –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar IsVisible=”False”>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png”
Click=”DeleteButton_Click”/>
<shell:ApplicationBarIconButton Text=”email”
IconUri=”/Shared/Images/appbar.email.png”
Click=”EmailButton_Click”/>
<shell:ApplicationBarIconButton Text=”settings”
IconUri=”/Shared/Images/appbar.settings.png”
Click=”SettingsButton_Click”/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<phone:PhoneApplicationPage.Resources>
<!– A copy of the text box default style with its border removed and
background applied differently. Compare with the style in Program Files
Microsoft SDKsWindows Phonev7.0DesignSystem.Windows.xaml –>

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

[/code]

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

Listing 21.11 contains the code-behind for this page.

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

[code]

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

[/code]

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

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

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

The Finished Product

Passwords & Secrets (Encryption & Observable Collections)

Pick a Card Magic Trick (3D Transforms)

The Pick a Card Magic Trick app enables you to amaze your friends with a slick magic trick that’s likely to keep them guessing how it’s done even after multiple performances. In this trick, you ask someone in your audience to name any card while the deck of cards is shuffling. After he or she names a card, you press “tap here when ready” to stop the shuffling, shown in Figure 17.1. You then tap the screen to flip over the card, and the card they just named is shown! You can tap the card again to start over.

FIGURE 17.1 The deck of cards shuffles in the background while the “tap here when ready” button is showing.
FIGURE 17.1 The deck of cards shuffles in the background while the “tap here when ready” button is showing.

The magician’s secret is not revealed in this book; you’ll have to run the app (or look at the full source code) that comes with this book in order to see how it is done! (My wife still can’t figure it out; she thinks the app is using speech recognition in order to know what card to show.) What is shown is the main lesson of this chapter—using 3D transforms to flip the playing cards.

3D Transforms

Unlike XNA, Silverlight does not provide a full 3D graphics engine. However, Silverlight enables you to perform the most common 3D effects with perspective transforms. These transforms escape the limitations of the 2D transforms by enabling you to rotate and translate an element in any or all of the three dimensions.

Perspective transforms are normally done with a class called PlaneProjection, which defines RotationX, RotationY, and RotationZ properties. The X and Y dimensions are defined as usual, and the Z dimension extends into and out of the screen, as illustrated in Figure 17.2. X increases from left-to-right, Y increases from topto- bottom, and Z increases from backto- front.

FIGURE 17.2 The three dimensions, relative to the phone screen.
FIGURE 17.2 The three dimensions, relative to the phone screen.

Although plane projections act like render transforms, they are not assigned to an element via the RenderTransform property, but rather a separate property called Projection. The following plane projections are marked on playing card images, producing the result in Figure 17.3:

[code]

<phone:PhoneApplicationPage …>
<StackPanel Orientation=”Horizontal”>
<Image Source=”Images/CardHA.png” Width=”150” Margin=”12”>
<Image.Projection>
<PlaneProjection RotationX=”55”/>
</Image.Projection>
</Image>
<Image Source=”Images/CardH2.png” Width=”150”>
<Image.Projection>
<PlaneProjection RotationY=”55”/>
</Image.Projection>
</Image>
<Image Source=”Images/CardH3.png” Width=”150” Margin=”36”>
<Image.Projection>
<PlaneProjection RotationZ=”55”/>
</Image.Projection>
</Image>
<Image Source=”Images/CardH4.png” Width=”150” Margin=”48”>
<Image.Projection>
<PlaneProjection RotationX=”30” RotationY=”30” RotationZ=”30”/>
</Image.Projection>
</Image>
</StackPanel>
</phone:PhoneApplicationPage>

[/code]

FIGURE 17.3 Using a plane projection to rotate the card around the X,Y, and Z axes and then all three axes.
FIGURE 17.3 Using a plane projection to rotate the card around the X,Y, and Z axes and then all three axes.

Notice that rotating around only the Z axis is like using a 2D RotateTransform, although the direction is reversed.

Although having permanently rotated elements might be interesting for some apps, normally plane projections are used as the target of an animation. Pick a Card leverages a plane projection for its card-flip animation, as well as its card-shuffling animation. Figure 17.4 demonstrates the 3D card flip. After the card back is rotated 90° (to be perpendicular to the screen and therefore temporarily invisible), the image is hidden to reveal the 9 of diamonds card front for the remaining 90° of the animation.

FIGURE 17.4 The 3D card flip is enabled with a plane projection and an animated RotationY property.
FIGURE 17.4 The 3D card flip is enabled with a plane projection and an animated RotationY property.

Much like the 2D transform classes, PlaneProjection defines additional properties for changing the center of rotation: CenterOfRotationX, CenterOfRotationY, and CenterOfRotationZ. The first two properties are relative to the size of the element, on a scale from 0 to 1. The CenterOfRotationZ property is always in terms of absolute pixels, as elements never have any size in the Z dimension to enable a relative specification. Pick a Card leverages CenterOfRotationX in its shuffle animation to make cards flip in from either the left edge of the screen or the right edge of the screen, as demonstrated in Figure 17.5 for the following XAML:

[code]

<phone:PhoneApplicationPage …>
<Grid>
<!– The card on the left –>
<Image Source=”Images/CardBack.png”>
<Image.Projection>
<PlaneProjection RotationY=”62” CenterOfRotationX=”0”/>
</Image.Projection>
</Image>
<!– The card on the right –>
<Image Source=”Images/CardBack.png”>
<Image.Projection>
<PlaneProjection RotationY=”-62” CenterOfRotationX=”1”/>
</Image.Projection>
</Image>
</Grid>
</phone:PhoneApplicationPage>

[/code]

FIGURE 17.5 Two playing cards that would normally overlap are given different centers of rotation, so they appear to flip in from opposite edges of the screen.
FIGURE 17.5 Two playing cards that would normally overlap are given different centers of rotation, so they appear to flip in from opposite edges of the screen.

PlaneProjection defines six properties for translating an element in any or all dimensions. GlobalOffsetX, GlobalOffsetY, and GlobalOffsetZ apply the translation after the rotation, so the offsets are relative to the global screen coordinates. LocalOffsetX, LocalOffsetY, and LocalOffsetZ apply the translation before the rotation, causing the rotation to be relative to the rotated coordinate space.

The Main Page

Pick a Card’s main page doesn’t do much; it’s a main menu that has two modes—an initial one for teaching you how to use the app, and one that hides the secrets once you have learned how to perform the trick. Both modes are shown in Figure 17.6.

FIGURE 17.6 The main page in two different modes.
FIGURE 17.6 The main page in two different modes.

The initial main menu has three buttons: one for instructions, one for performing the trick in a special practice mode, and one for the standard about page. Once the instructions have self-destructed (which can be done by tapping a button on the instructions page), the main menu has a button for performing the trick in its normal mode, and the same about button.

The User Interface

Listing 17.1 contains the XAML for the main page.

LISTING 17.1 MainPage.xaml—The User Interface for Pick a Card’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”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”5*”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<Rectangle Fill=”{StaticResource PhoneForegroundBrush}” Margin=”0,0,0,40”
Width=”158” Height=”200” VerticalAlignment=”Bottom”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/logo.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<TextBlock Text=”pick a card” Margin=”21,16,0,0”
Style=”{StaticResource PhoneTextTitle1Style}”/>
<Button x:Name=”InstructionsButton” Grid.Row=”1”
Content=”instructions for the new magician” Height=”100”
local:Tilt.IsEnabled=”True” Click=”InstructionsButton_Click”/>
<Button x:Name=”BeginButton” Grid.Row=”2”
Content=”practice (FOR YOUR EYES ONLY!)” Height=”100”
local:Tilt.IsEnabled=”True” Click=”BeginButton_Click”/>
<Button Content=”about” Grid.Row=”3” Height=”100” Click=”AboutButton_Click”
local:Tilt.IsEnabled=”True”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Notes:

  • The page is set up for its initial mode. Code-behind transforms it to the other mode.
  • Rather than using an Image element to display the logo, Listing 17.1 uses the logo.png file as an opacity mask for a rectangle filled with the phone theme foreground color. This is done to enable the otherwise-white image to appear black under the light theme, as shown in Figure 17.7.
FIGURE 17.7 The opacity mask enables the image to remain the phone theme’s foreground color, regardless of the theme.
FIGURE 17.7 The opacity mask enables the image to remain the phone theme’s foreground color, regardless of the theme.

Opacity masks are often harmful for performance!

Using an opacity mask with an image brush is a neat trick for enabling nonvector content to respect the phone’s current theme.However, be aware that their use can severely hamper the performance of your app, especially when animations are involved.Opacity masks cause animations to be rasterized on the UI thread, even if they otherwise would have been able to completely run on the compositor thread.Therefore, use extreme caution when applying an opacity mask.You can check whether it is impacting your app by examining the frame rate counter with and without the opacity mask applied.

The Code-Behind

Listing 17.2 contains the code-behind for the main page, which consists of straightforward Click event handlers for each button and code to morph the main menu after the instructions have been hidden. This is based off of a single setting defined in Settings.cs:

[code]

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

LISTING 17.2 MainPage.xaml.cs—The Code-Behind for Pick a Card’s Main Page

[ocde]

using System;
using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (!Settings.PracticeMode.Value)
{
BeginButton.Content = “begin”;
InstructionsButton.Visibility = Visibility.Collapsed;
}
}
void BeginButton_Click(object sender, RoutedEventArgs e)
{
this.NavigationService.Navigate(new Uri(“/TrickPage.xaml”,
UriKind.Relative));
}
void InstructionsButton_Click(object sender, RoutedEventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void AboutButton_Click(object sender, RoutedEventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Pick a Card”, UriKind.Relative));
}
}
}

[/code]

The Trick Page

The “trick page” is used for both phases of the trick—the shuffling and the final card reveal. This same page is used whether the trick is running in “practice mode” or for real.

The User Interface

Listing 17.3 contains the XAML for the trick page.

LISTING 17.3 TrickPage.xaml—The User Interface for Pick a Card’s Trick Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.TrickPage”
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”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait”>
<!– Prevent off-screen visuals from appearing during a page transition –>
<phone:PhoneApplicationPage.Clip>
<RectangleGeometry Rect=”0,0,480,800”/>
</phone:PhoneApplicationPage.Clip>
<!– Add two storyboards to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– The flip –>
<Storyboard x:Name=”FlipStoryboard”
Storyboard.TargetName=”ChosenCardProjection”
Storyboard.TargetProperty=”RotationY”
Completed=”FlipStoryboard_Completed”>
<DoubleAnimation By=”90” Duration=”0:0:.25”/>
</Storyboard>
<!– The shuffle, with separate left and right animations –>
<Storyboard x:Name=”ShuffleStoryboard”
Storyboard.TargetProperty=”RotationY”>
<DoubleAnimation Storyboard.TargetName=”NextCardLeftProjection” From=”120”
To=”0” Duration=”0:0:.2” RepeatBehavior=”Forever”
BeginTime=”0:0:.1”/>
<DoubleAnimation Storyboard.TargetName=”NextCardRightProjection”
From=”-120” To=”0” Duration=”0:0:.2”
RepeatBehavior=”Forever”/>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<Grid Background=”Black”>
<!– The card that flips over –>
<Grid>
<Grid.Projection>
<PlaneProjection x:Name=”ChosenCardProjection”/>
</Grid.Projection>
<Image x:Name=”CardFrontImage” RenderTransformOrigin=”.5,.5”>
<!– Reverse, so it looks correct when flipped over –>
<Image.RenderTransform>
<ScaleTransform ScaleX=”-1”/>
</Image.RenderTransform>
</Image>
<Image x:Name=”CardBackImage” Source=”Images/CardBack.png”/>
</Grid>
<!– More cards, for shuffling –>
<Image x:Name=”NextCardRightImage” Source=”Images/CardBack.png”>
<Image.Projection>
<PlaneProjection x:Name=”NextCardRightProjection” CenterOfRotationX=”1”/>
</Image.Projection>
</Image>
<Image x:Name=”NextCardLeftImage” Source=”Images/CardBack.png”>
<Image.Projection>
<PlaneProjection x:Name=”NextCardLeftProjection” CenterOfRotationX=”-1”/>
</Image.Projection>
</Image>
<!– The “tap here when ready” button and a translucent background–>
<Grid x:Name=”ReadyPanel” Background=”#7000”>
<Button Background=”{StaticResource PhoneBackgroundBrush}”
Content=”tap here when ready”
VerticalAlignment=”Center”/>
</Grid>
<!– Images for practice mode –>
<Image x:Name=”PracticeImage1” Visibility=”Collapsed”
Source=”Images/practice1.png”/>
<Image x:Name=”PracticeImage2” Visibility=”Collapsed”
Source=”Images/practice2.png”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Notes:

  • The grid containing the chosen card has a plane projection (ChosenCardProjection) that is animated by FlipStoryboard to perform the 3D flip. This grid contains the image for the card front (chosen by code-behind) and the image for the card back. The card front image is reversed (with a ScaleTransform) so it appears correctly once the grid is flipped around. The animation only rotates the card 90°, because at that point the card back needs to be hidden so the card front can be seen for the remaining 90°. This is handled by the FlipStoryboard_Completed method in codebehind.
  • ShuffleStoryboard performs the shuffling by animating the plane projections on NextCardRightImage and NextCardLeftImage. These are given centers of rotation that make them flip from the outer edges of the screen, as seen back in Figure 17.1. The left image is given a center of –1 rather than 0 to give a more realistic, asymmetric effect.

The Code-Behind

Listing 17.4 contains the code-behind for the trick page, with 61 lines of code omitted that “magically” set the chosenSuit string to C, D, H, or S and the chosenRank string to A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, or K.

LISTING 17.4 TrickPage.xaml.cs—The Code-Behind for Pick a Card’s Trick Page

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class TrickPage : PhoneApplicationPage
{
string chosenSuit;
string chosenRank;
bool flipPart2;
bool finalPhase;
public TrickPage()
{
InitializeComponent();
this.AddHandler(Page.MouseLeftButtonUpEvent,
new MouseButtonEventHandler(MainPage_MouseLeftButtonUp),
true /* handledEventsToo, so we get the button click */);
InitializeTrick();
}
void InitializeTrick()
{
if (Settings.PracticeMode.Value)
this.PracticeImage1.Visibility = Visibility.Visible;
// Reset everything
this.ReadyPanel.Visibility = Visibility.Visible;
this.CardBackImage.Visibility = Visibility.Visible;
this.NextCardLeftImage.Visibility = Visibility.Visible;
this.NextCardRightImage.Visibility = Visibility.Visible;
this.CardFrontImage.Source = null;
this.flipPart2 = false;
this.ChosenCardProjection.RotationY = 0;
// Start shuffling
this.ShuffleStoryboard.Begin();
}
void MainPage_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (this.ReadyPanel.Visibility == Visibility.Visible)
{
// This is a tap on the “tap here when ready” button
if (Settings.PracticeMode.Value)
{
this.PracticeImage1.Visibility = Visibility.Collapsed;
this.PracticeImage2.Visibility = Visibility.Visible;
}
// Hide ReadyPanel and the shuffling deck,
// leaving the single card back exposed
this.ReadyPanel.Visibility = Visibility.Collapsed;
this.NextCardLeftImage.Visibility = Visibility.Collapsed;
this.NextCardRightImage.Visibility = Visibility.Collapsed;
this.ShuffleStoryboard.Stop();
this.finalPhase = true;
}
else if (this.finalPhase)
{
// This is a tap on the card back to flip it over
if (Settings.PracticeMode.Value)
this.PracticeImage2.Visibility = Visibility.Collapsed;
// Show the chosen card image
this.CardFrontImage.Source = new BitmapImage(new Uri(“Images/Card” +
this.chosenSuit + this.chosenRank + “.png”, UriKind.Relative));
// Perform the first 90° of the flip
this.FlipStoryboard.Begin();
this.finalPhase = false;
}
else if (this.FlipStoryboard.GetCurrentState() != ClockState.Active)
{
// Do it again. (Don’t allow this until the flip animation is finished.)
InitializeTrick();
}
}
void FlipStoryboard_Completed(object sender, EventArgs e)
{
if (!this.flipPart2)
{
// The card is now perpendicular to the screen. It’s time to hide the
// back and run the animation again so the remaining 90° shows the front
this.CardBackImage.Visibility = Visibility.Collapsed;
this.flipPart2 = true;
this.FlipStoryboard.Begin();
}
}
#region Magician’s Secret

#endregion
}
}

[/code]

Notes:

  • A single handler—MainPage_MouseLeftButtonUp—handles the first tap on the “tap here when ready” button, which can actually be anywhere on the screen, the tap on the card back to flip it over, and the tap on the card front to start the trick again. The handler is attached with true passed for handledEventsToo, so the event is received when the button is tapped.
  • When the card back is tapped (indicated by finalPhase being true inside MainPage_MouseLeftButtonUp), the card front image is set to one of 52 images included in the project. These 52 images are shown in Figure 17.8.
  • Inside FlipStoryboard_Completed, the card back is hidden and FlipStoryboad is run again to complete the 180° flip. This works because the animation is marked with By=”90”, so the first run takes it from 0° to 90°, and the second run takes it from 90° to 180°. The card back must be manually hidden because flipping elements over in 3D does not change their Z-order. In other words, unlike in the physical world, the card back remains on top of the card front regardless of the angle of rotation.
FIGURE 17.8 The 52 card images cover every choice except a Joker.
FIGURE 17.8 The 52 card images cover every choice except a Joker.

The Instructions Page

The instructions page, shown in Figure 17.9, contains a button that makes them selfdestruct (turning off practice mode and changing the main menu). Once this is done, the instructions never come back unless the app is uninstalled and reinstalled. This is done to prevent nosy audience members from figuring out the secret to the trick. The XAML for this page isn’t very interesting (other than the fact that its text reveals the secret to the trick), but Listing 17.5 shows the code-behind, which implements the self-destructing behavior.

FIGURE 17.9 The instructions page contains a button that permanently hides the instructions and turns off practice mode.
FIGURE 17.9 The instructions page contains a button that permanently hides the instructions and turns off practice mode.

LISTING 17.5 InstructionsPage.xaml.cs—The Code-Behind for Pick a Card’s Instructions Page

[code]

using System.Windows;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class InstructionsPage : PhoneApplicationPage
{
public InstructionsPage()
{
InitializeComponent();
}
void SelfDestructButton_Click(object sender, RoutedEventArgs e)
{
if (MessageBox.Show(“To protect the secret of this trick, these “ +
“instructions will disappear forever once you turn off practice mode.” +
“ The only way to get them back is to uninstall then reinstall this “ +
“app. Are you ready to destroy the instructions?”,
“These instructions will self-destruct!”, MessageBoxButton.OKCancel)
== MessageBoxResult.OK)
{
Settings.PracticeMode.Value = false;
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
}
}

[/code]

To make the instructions self-destruct, Listing 17.5 changes the PracticeMode persisted setting to false and then navigates back to the main page which hides the instructions button. This setting never changes unless the app is uninstalled because it doesn’t provide the user any way to change it back. Because uninstalling an app removes anything it puts in isolated storage, however, reinstalling it restores PracticeMode’s default value of true.

To implement a behavior that only happens the first time an app is run (or until the user makes some action to turn it off ), simply base it on a value persisted in isolated storage.The Setting object used throughout this book internally uses isolated storage to preserve each value until it is either changed by code or deleted by the app being uninstalled.

The Finished Product

Pick a Card Magic Trick (3D Transforms)

 

 

 

XAML Editor (Dynamic XAML & Popup)

XAML Editor is a text editor for XAML, much like the famous XAMLPad program for the Windows desktop. At first, XAML Editor looks like nothing more than a page with a text box, but it is much more for a number of reasons:

  • It renders the XAML you type as live objects (including any interactivity).
  • It provides XAML-specific auto-completion via a custom text suggestions bar (somewhat like Intellisense).
  • It has a menu of samples, to aid in experimentation.
  • It enables you to email your XAML, in case you come up with something you want to save.
  • It shows error information for invalid XAML in an unobtrusive way.

The custom text suggestions bar is vital for making this app usable, as without it common XAML characters like angle brackets, the forward slash, quotes, and curly braces are buried in inconvenient locations. With this bar, users don’t normally need to leave the first page of keyboard keys unless they are typing numbers.

On the surface, the main lesson for this chapter seems like the mechanism for reading XAML at run-time and producing a dynamic user interface. However, this is accomplished with just one line of code. The main challenge to implementing XAML Editor is providing a custom text suggestions bar. The real text suggestions bar does not support customization, so XAML Editor provides one with a lot of trickery involving an element known as Popup.

The trickery (or, to be honest, hacks) done by this chapter also forms a cautionary tale. In the initial version of Windows Phone 7 (version 7.0.7004.0), the fake suggestions bar was a reasonable replacement for the built-in one, as shown in Figure 11.1. With the addition of the copy/paste feature (starting with version 7.0.7338.0), however, it can no longer act this way. The app had relied on suppressing the real bar by using the default input scope on the text box, but app authors can no longer reliably do this because the bar still appears whenever something can be pasted. Furthermore, there is no way for the fake bar to integrate with the clipboard and provide its own paste button. Therefore, the latest version of XAML Editor treats the fake suggestions bar as a second bar on top of the primary one.

FIGURE 11.1 Because the Windows Phone copy/paste feature did not yet exist, the first version of XAML Editor could reliably place the fake suggestions bar where the real one would be.
FIGURE 11.1 Because the Windows Phone copy/paste feature did not yet exist, the first version of XAML Editor could reliably place the fake suggestions bar where the real one would be.

Popup

A popup is an element that floats on top of other elements. It was designed for temporary pieces of UI, such as tooltips. However, as in this chapter, is often used in hacky ways to produce behavior that is difficult to accomplish otherwise.

A popup doesn’t have any visual appearance by itself, but it can contain a visual element as its single child (and that child could be a complex element containing other elements). By default, a popup docks to the top-left corner of its parent, although you can move it by giving it a margin and/or setting its HorizontalOffset and VerticalOffset properties.

On Top of (Almost) Everything

Figure 11.2 demonstrates the behavior of the popup in the following 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”
SupportedOrientations=”PortraitOrLandscape” Orientation=”Landscape”>
<Grid>
<!– Inner grid with a button-in-popup and a separate button –>
<Grid Background=”Red” Margin=”100”>
<Popup IsOpen=”True”>
<Button Content=”button in popup in grid” Background=”Blue”/>
</Popup>
<Button Content=”button in grid” Canvas.ZIndex=”100”/>
</Grid>
<!– A rectangle that overlaps the inner grid underneath it –>
<Rectangle Width=”200” Height=”200” Fill=”Lime”
HorizontalAlignment=”Left” VerticalAlignment=”Top”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

FIGURE 11.2 The popup’s content doesn’t stretch, stays in the top-left corner of its parent, and stays on top of all other elements.
FIGURE 11.2 The popup’s content doesn’t stretch, stays in the top-left corner of its parent, and stays on top of all other elements.

There are three interesting things to note about Figure 11.2:

  • A popup is only visible when its IsOpen property is set to true.
  • The layout inside a popup is like the layout inside a canvas; a child element is only given the exact amount of space it needs.
  • Popups have a unique power: They can render on top of all other Silverlight elements! Although the sibling button in Figure 11.2 has a larger z-index, and although the lime rectangle is a sibling to the popup’s parent (making it the popup’s uncle?), it appears on top of both of them!

Exempt from Orientation Changes

Besides their topmost rendering, popups have another claim to fame: they are able to ignore orientation changes! This happens when you create and show a popup without attaching it to any parent element. In this case, it is implicitly attached to the root frame, which always acts as if it is in the portrait orientation.

The following empty page demonstrates this behavior:

[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”
SupportedOrientations=”PortraitOrLandscape” Orientation=”Landscape”>
<Grid x:Name=”Grid”/>
</phone:PhoneApplicationPage>

[/code]

In this page’s code-behind, two popups are created in the constructor. The first one is attached to the grid, but the second one is implicitly attached to the frame:

[code]

public MainPage()
{
InitializeComponent();
Popup popup1 = new Popup();
popup1.Child = new Button { Content = “button in popup in grid”, FontSize=40 };
popup1.IsOpen = true;
this.Grid.Children.Add(popup1); // Attach this to the grid
Popup popup2 = new Popup();
popup2.Child = new Button { Content = “button in popup”, FontSize=55,
Foreground = new SolidColorBrush(Colors.Cyan),
BorderBrush = new SolidColorBrush(Colors.Cyan) };
popup2.IsOpen = true; // Show without explicitly attaching it to anything
}

[/code]

This page is shown in Figure 11.3. The cyan button (inside popup2) behaves like the whole screen would behave if it were marked as SupportedOrientations=”Portrait”, whereas the white button (inside popup1) adjusts to remain on the edges of the screen currently acting as the top and the left.

FIGURE 11.3 The popup that isn’t attached to the grid stays docked to the physical top and left of the phone for any orientation.
FIGURE 11.3 The popup that isn’t attached to the grid stays docked to the physical top and left of the phone for any orientation.

Frame-rooted popups also do not move with the rest of the page when the on-screen keyboard automatically pushes the page upward to keep the focused textbox visible. XAML Editor leverages this fact, as the popup containing the text suggestions bar must always be in the exact same spot regardless of what has happened to the page.

The User Interface

Listing 11.1 contains the XAML for this app’s only page, shown at the beginning of this chapter. The page contains a text box on top of a grid used to hold the rendered result from parsing the XAML, and an application bar with four buttons and four menu items.

LISTING 11.1 MainPage.xaml—The User Interface for XAML Editor

[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”
Loaded=”MainPage_Loaded”
SupportedOrientations=”PortraitOrLandscape”>
<!– Application bar with 3-4 buttons and 4 menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”view” Click=”SwitchViewButton_Click”
IconUri=”/Shared/Images/appbar.view.png”/>
<shell:ApplicationBarIconButton Text=”clear” Click=”ClearButton_Click”
IconUri=”/Shared/Images/appbar.cancel.png”/>
<shell:ApplicationBarIconButton Text=”email” Click=”EmailButton_Click”
IconUri=”/Shared/Images/appbar.email.png”/>
<shell:ApplicationBarIconButton Text=”error” Click=”ErrorButton_Click”
IconUri=”/Shared/Images/appbar.error.png”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”simple shapes”
Click=”SampleMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”gradient text”
Click=”SampleMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”clipped image”
Click=”SampleMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”controls”
Click=”SampleMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<!– 1×1 grid containing 2 overlapping child grids –>
<Grid>
<!– where the live XAML goes –>
<Grid x:Name=”ViewPanel”/>
<!– The text editor–>
<Grid x:Name=”EditorPanel” Background=”{StaticResource PhoneBackgroundBrush}”
Opacity=”.9”>
<ScrollViewer x:Name=”ScrollViewer”>
<TextBox x:Name=”XamlTextBox” AcceptsReturn=”True” VerticalAlignment=”Top”
Height=”2048” TextWrapping=”Wrap” InputScope=”Text”
FontFamily=”Courier New” FontSize=”19” FontWeight=”Bold”
SelectionChanged=”XamlTextBox_SelectionChanged”
GotFocus=”XamlTextBox_GotFocus” LostFocus=”XamlTextBox_LostFocus”
TextChanged=”XamlTextBox_TextChanged”/>
</ScrollViewer>
</Grid>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Notes:

  • This page supports all orientations for the sake of text entry.
  • Courier New, the phone’s only built-in fixed-width font, is used to give the text box a code-editor feel.
  • If the text box were to use the default input scope, then the text suggestions bar may or may not appear based on whether there’s something to paste. This would make it impossible to properly place this app’s fake suggestions bar directly above the on-screen keyboard because there’s no way for an app to detect whether there’s currently something on the clipboard. Therefore, the text box is marked with the Text input scope. With the real text suggestions bar always present, the fake one can be reliably placed on top of it. Plus, its standard text suggestions might occasionally be useful while editing XAML.
  • Although the text box supports internal scrolling of its content when the user holds down a finger and drags the caret, it is pretty challenging for users to do this in a satisfactory way. To combat this, the text box is given its maximum supported height and placed in a scroll viewer that enables much more user-friendly scrolling. (It is also marked with word wrapping to avoid the need for horizontal scrolling.) The explicit height is used rather than letting the text box grow on its own because the implementation of the fake suggestions bar requires that part of the text box is always underneath it, and this overlapping would obscure the bottom few lines of text if the text box weren’t longer than its text.Unfortunately, this causes the loss of an important text box feature—the ability to keep the caret visible on the screen while the user is typing. If you knew the current vertical position of the caret, you could scroll the scroll viewer with its ScrollToVerticalOffset method whenever the text changes. Unfortunately, the only caret position exposed by a text box is the character index in the string, and it takes a significant amount of work to calculate coordinates from this.

    Therefore, XAML Editor forces the user to manually scroll the page if the caret goes off-screen or gets hidden under the keyboard.

Elements have a size limitation!

You should avoid making any Silverlight element larger than 2,048 pixels in any dimension, due to system limitations.Otherwise, a variety of behaviors can be seen, such as forced clipping or even the entire screen going blank! The best workaround for a text box would be to virtualize its contents, e.g. only make it contain the on-screen contents (and perhaps a little more) at any single time. Implementing such a scheme while making sure scrolling and typing works as expected can be complex. XAML Editor simply hopes that users don’t type more than approximately 93 lines of XAML!

The Code-Behind

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

LISTING 11.2 MainPage.xaml.cs—The Code-Behind for XAML Editor

[code]

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using Microsoft.Phone.Tasks;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Always remember the text box’s text, caret position and selection
Setting<string> savedXaml = new Setting<string>(“XAML”, Data.SimpleShapes);
Setting<int> savedSelectionStart = new Setting<int>(“SelectionStart”, 0);
Setting<int> savedSelectionLength = new Setting<int>(“SelectionLength”, 0);
// The popup and its content are not attached to the page
internal Popup Popup;
internal TextSuggestionsBar TextSuggestionsBar;
// Named fields for two application bar buttons
IApplicationBarIconButton viewButton;
IApplicationBarIconButton errorButton;
// Remember the current XAML parsing error in case the user wants to see it
string currentError;
// A timer for delaying the update of the view after keystrokes
DispatcherTimer timer =
new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
public MainPage()
{
InitializeComponent();
// Assign the application bar buttons because they can’t be named in XAML
this.viewButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
this.errorButton = this.ApplicationBar.Buttons[3]
as IApplicationBarIconButton;
// Initialize the popup and its content
this.TextSuggestionsBar = new TextSuggestionsBar(this.XamlTextBox);
this.Popup = new Popup();
this.Popup.Child = this.TextSuggestionsBar;
// PopupHelper does the dirty work of positioning & rotating the popup
PopupHelper.Initialize(this);
// When the timer ticks, refresh the view then stop it, so there’s
// only one refresh per timer.Start()
this.timer.Tick += delegate(object sender, EventArgs e)
{
RefreshView();
this.timer.Stop();
};
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Remember the text box’s text, caret position and selection
this.savedXaml.Value = this.XamlTextBox.Text;
this.savedSelectionStart.Value = this.XamlTextBox.SelectionStart;
this.savedSelectionLength.Value = this.XamlTextBox.SelectionLength;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the text box’s text, caret position and selection
this.XamlTextBox.Text = this.savedXaml.Value;
this.XamlTextBox.SelectionStart = this.savedSelectionStart.Value;
this.XamlTextBox.SelectionLength = this.savedSelectionLength.Value;
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
// Make on-screen keyboard instantly appear
this.XamlTextBox.Focus();
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
// Send mouse info to the text suggestions bar, if appropriate
if (PopupHelper.IsOnPopup(e))
this.TextSuggestionsBar.OnMouseDown(PopupHelper.GetPopupRelativePoint(e));
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
// Send mouse info to the text suggestions bar, if appropriate
if (PopupHelper.IsOnPopup(e))
this.TextSuggestionsBar.OnMouseMove(PopupHelper.GetPopupRelativePoint(e));
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
// Send mouse info to the text suggestions bar, in case its appropriate
this.TextSuggestionsBar.OnMouseUp(PopupHelper.IsOnPopup(e));
}
void XamlTextBox_GotFocus(object sender, RoutedEventArgs e)
{
// Show the popup whenever the text box has focus (and is visible)
if (this.EditorPanel.Visibility == Visibility.Visible)
this.Popup.IsOpen = true;
}
void XamlTextBox_LostFocus(object sender, RoutedEventArgs e)
{
// Hide the popup whenever the text box loses focus
this.Popup.IsOpen = false;
}
void XamlTextBox_SelectionChanged(object sender, RoutedEventArgs e)
{
// Update the suggestions based on the text behind the caret location
string text = this.XamlTextBox.Text;
int position = this.XamlTextBox.SelectionStart – 1;
// Initiate the suggestion-picking algorithm on a background thread
BackgroundWorker backgroundWorker = new BackgroundWorker();
backgroundWorker.DoWork += delegate(object s, DoWorkEventArgs args)
{
// This runs on a background thread
args.Result = UpdateTextSuggestions(text, position);
};
backgroundWorker.RunWorkerCompleted +=
delegate(object s, RunWorkerCompletedEventArgs args)
{
// This runs on the UI thread after BackgroundWorker_DoWork is done
// Grab the list created on the background thread
IList<Suggestion> suggestions = args.Result as IList<Suggestion>;
if (suggestions == null)
return;
// Clear the current list
this.TextSuggestionsBar.ClearSuggestions();
// Fill the bar with the new list
foreach (Suggestion suggestion in suggestions)
this.TextSuggestionsBar.AddSuggestion(suggestion);
};
backgroundWorker.RunWorkerAsync();
}
void XamlTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
// Remember the current caret position and selection
int start = this.XamlTextBox.SelectionStart;
int length = this.XamlTextBox.SelectionLength;
// Ensure the text always ends with several newlines so the user
// can easily scroll to see the very bottom of the text
if (!this.XamlTextBox.Text.EndsWith(Constants.NEWLINES))
this.XamlTextBox.Text = this.XamlTextBox.Text.TrimEnd()
+ Constants.NEWLINES;
// Restore the caret position and selection, which gets
// overwritten if the text is updated
this.XamlTextBox.SelectionStart = start;
this.XamlTextBox.SelectionLength = length;
// Cancel any pending refresh
if (this.timer.IsEnabled)
this.timer.Stop();
// Schedule a refresh of the view for one second from now
this.timer.Start();
}
void RefreshView()
{
try
{
// Wrap the user’s text in a page with appropriate namespace definitions
string xaml = @”<phone:PhoneApplicationPage
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””
FontFamily=””{StaticResource PhoneFontFamilyNormal}””
FontSize=””{StaticResource PhoneFontSizeNormal}””
Foreground=””{StaticResource PhoneForegroundBrush}””>”
+ this.XamlTextBox.Text
+ “</phone:PhoneApplicationPage>”;
// Parse the XAML and get the root element (the page)
UIElement root = System.Windows.Markup.XamlReader.Load(xaml) as UIElement;
// Replace ViewPanel’s content with the new elements
this.ViewPanel.Children.Clear();
this.ViewPanel.Children.Add(root);
// An exception wasn’t thrown, so clear any error state
this.XamlTextBox.Foreground = new SolidColorBrush(Colors.Black);
this.ApplicationBar.Buttons.Remove(this.errorButton);
}
catch (Exception ex)
{
// The XAML was invalid, so transition to an error state
this.XamlTextBox.Foreground = new SolidColorBrush(Colors.Red);
if (!this.ApplicationBar.Buttons.Contains(this.errorButton))
this.ApplicationBar.Buttons.Add(this.errorButton);
// Use the exception message as the error message, but remove the line #
this.currentError = ex.Message;
if (this.currentError.Contains(“ [Line:”))
this.currentError = this.currentError.Substring(0,
this.currentError.IndexOf(“ [Line:”));
}
}
IList<Suggestion> UpdateTextSuggestions(string text, int position)
{
// The list of suggestions to report
List<Suggestion> suggestions = new List<Suggestion>();
if (position == -1)
{
// We’re at the beginning of the text box
suggestions.Add(new Suggestion { Text = “<”, InsertionOffset = 0 });
return suggestions;
}
char character = text[position];
if (Char.IsDigit(character))
{
// A number is likely a value to be followed by an end quote, or it could
// be a property like X1 or X2 to be followed by an equals sign
suggestions.Add(new Suggestion { Text = “””, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “=”, InsertionOffset = 0 });
}
else if (!Char.IsLetter(character))
{
// Choose various likely completions based on the special character
switch (character)
{
case ‘<’:
suggestions.Add(new Suggestion { Text = “/”, InsertionOffset = 0 });
break;
case ‘/’:
suggestions.Add(new Suggestion { Text = “>”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “””, InsertionOffset = 0 });
break;
case ‘ ‘:
case ‘r’:
case ‘n’:
suggestions.Add(new Suggestion { Text = “<”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “/”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “>”, InsertionOffset = 0 });
break;
case ‘>’:
suggestions.Add(new Suggestion { Text = “<”, InsertionOffset = 0 });
break;
case ‘=’:
case ‘}’:
suggestions.Add(new Suggestion { Text = “””, InsertionOffset = 0 });
break;
case ‘{‘:
suggestions.Add(
new Suggestion { Text = “Binding “, InsertionOffset = 0 });
suggestions.Add(
new Suggestion { Text = “StaticResource “, InsertionOffset = 0 });
break;
case ‘“‘:
suggestions.Add(new Suggestion { Text = “/”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “>”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “{“, InsertionOffset = 0 });
break;
}
}
else
{
// This is a letter
// First add a few special symbols
suggestions.Add(new Suggestion { Text = “/”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “>”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “=”, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “””, InsertionOffset = 0 });
suggestions.Add(new Suggestion { Text = “}”, InsertionOffset = 0 });
// Keep traversing backwards until we hit a non-letter
string letters = null;
while (position >= 0 && (letters == null ||
Char.IsLetter(text[position])))
letters = text[position–] + letters;
// Add words from our custom dictionary that match the current text as
// as prefix
for (int i = 0; i < Data.Words.Length; i++)
{
// Only include exact matches if the case is different
// (so the user can tap the suggestion to fix their casing)
if (Data.Words[i].StartsWith(letters,
StringComparison.InvariantCultureIgnoreCase) &&
!Data.Words[i].Equals(letters, StringComparison.InvariantCulture))
{
suggestions.Add(new Suggestion { Text = Data.Words[i],
InsertionOffset = -letters.Length });
}
}
}
return suggestions;
}
// Application bar handlers
void ViewButton_Click(object sender, EventArgs e)
{
// Switch between viewing the results and viewing the XAML text box
if (this.EditorPanel.Visibility == Visibility.Visible)
{
this.EditorPanel.Visibility = Visibility.Collapsed;
this.viewButton.IconUri = new Uri(“/Images/appbar.xaml.png”,
UriKind.Relative);
this.viewButton.Text = “xaml”;
}
else
{
this.EditorPanel.Visibility = Visibility.Visible;
this.viewButton.IconUri = new Uri(“/Shared/Images/appbar.view.png”,
UriKind.Relative);
this.viewButton.Text = “view”;
this.XamlTextBox.Focus();
}
}
void ClearButton_Click(object sender, EventArgs e)
{
// Clear the text box if the user agrees
if (MessageBox.Show(“Are you sure you want to clear this XAML?”,
“Clear XAML”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
this.XamlTextBox.Text = “”;
}
void EmailButton_Click(object sender, EventArgs e)
{
// Launch an email with the content of the text box
EmailComposeTask emailLauncher = new EmailComposeTask {
Body = this.XamlTextBox.Text, Subject = “XAML from the XAML Editor app” };
emailLauncher.Show();
}
void ErrorButton_Click(object sender, EventArgs e)
{
// Show whatever the current error is
MessageBox.Show(this.currentError, “XAML Error”, MessageBoxButton.OK);
}
void SampleMenuItem_Click(object sender, EventArgs e)
{
if (this.XamlTextBox.Text.Trim().Length != 0 &&
MessageBox.Show(“Are you sure you want to replace the XAML?”,
“Replace XAML”, MessageBoxButton.OKCancel) != MessageBoxResult.OK)
return;
// Fill the text box with the chosen sample
switch ((sender as IApplicationBarMenuItem).Text)
{
case “simple shapes”:
this.XamlTextBox.Text = Data.SimpleShapes;
break;
case “gradient text”:
this.XamlTextBox.Text = Data.GradientText;
break;
case “clipped image”:
this.XamlTextBox.Text = Data.ClippedImage;
break;
case “controls”:
this.XamlTextBox.Text = Data.Controls;
break;
}
}
}
}

[/code]

Notes:

  • The popup’s child is set to an instance of a TextSuggestionsBar user control, implemented in the next section, which handles the display and interaction of the bar.
  • A fair amount of code is needed to properly position the popup and report where it is being touched, so this is factored into a separate PopupHelper class examined next.
  • In MainPage_Loaded, the on-screen keyboard is automatically deployed (unless a hardware keyboard is active) because there’s no other UI to obscure.
  • Inside the three OnMouse… handlers, the data is being passed along to the text suggestions bar. This highlights the main challenge of implementing this bar—it must never get focus because the on-screen keyboard would go away if the text box loses focus! Therefore, the root of the TextSuggestionsBar user control is marked with IsHitTestVisible=”False”, and the control exposes its own trio of OnMouse… methods, so it can act like it’s being touched when it’s really the text box underneath that is receiving these events.
  • The performance of updating the text suggestions bar is important because it happens on every keystroke (or other movement of the caret). Inside XamlTextBox_SelectionChanged, a background worker is used to execute the time-consuming work—UpdateTextSuggestions. This only works because UpdateTextSuggestions doesn’t interact with any UI or do anything else that requires being run on the UI thread.With a background worker, you can attach a delegate to its DoWork event, which gets raised on a background thread once RunWorkerAsync is called (done at the end of XamlTextBox_ SelectionChanged). When the background work has completed, the RunWorkerCompleted event is raised on the original (UI) thread. This enables user interface updates to occur based on whatever work was done in the background. (Alternatively, the backgroundthread code could call BeginInvoke on the page’s dispatcher to schedule work on the UI thread.) The DoWork handler can pass data to the RunWorkerCompleted handler via a Result property on the event-args parameter.
  • XamlTextBox_TextChanged uses another technique to improve this app’s performance. Rather than instantly re-render the XAML every time it changes, it uses a timer to wait one second. That way, it can cancel a pending update if another change occurs within that second. This technique, as well as the use of a background worker for filling the text suggestions bar, vastly improves the performance when the user holds down a repeatable key (the space bar, backspace, or Enter).
  • RefreshView contains the single line of code needed to turn XAML into a tree of live objects. The static XamlReader.Load method accepts a XAML string as input and returns an object corresponding to the root element in the string. If there’s anything wrong with the XAML, it throws a XamlParseException. RefreshView captures any exception and shows the message to the user if they tap the error button that appears on the application bar. This code strips out any line and position information from the message because (a) the surrounding page element throws off the line number and (b) it’s often not accurate anyway.The XAML string must be selfcontained, so its elements cannot have event handlers assigned, nor can it have unresolved XML namespace prefixes. RefreshView wraps the user’s XAML in a page element with the main namespaces so the user’s XAML doesn’t need to be cluttered with this. (This could have been a grid, and the result would look the same.) Therefore, this code ends up attaching an instance of a page as a child of the ViewPanel grid. It’s weird for a page to contain another page, but it works just fine.
  • UpdateTextSuggestions contains the simple algorithm for providing suggestions based on the text preceding the current caret location. It treats numbers, letters, and symbols differently. Perhaps the most clever thing it does is suggest “Binding “ and “StaticResource “ (with the trailing space included) immediately after a {. It makes use of a simple structure defined in Suggestion.cs as follows:[code]
    namespace WindowsPhoneApp
    {
    public struct Suggestion
    {
    public string Text { get; set; }
    public int InsertionOffset { get; set; }
    }
    }
    [/code]

    The insertion offset captures how much of the suggestion has already been typed before the caret.

  • The custom dictionary of XAML-relevant words (over 300) is a static string array called Words in a static Data class. It contains common element names, property names, and some common property values. The XAML samples accessed via the application bar menu are stored as static fields on the same class. The Data class is not shown in this chapter, but as with all the apps, you can download the complete source code.

PopupHelper

Listing 11.3 contains the implementation of the PopupHelper class used by Listing 11.2. It is directly tied to the main page rather than being any sort of reusable control.

LISTING 11.3 PopupHelper.cs—A Class That Manipulates the Popup Containing the Text Suggestions Bar

[code]

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
internal static class PopupHelper
{
static MainPage page;
static bool textSuggestionsBarSlidDown;
internal static void Initialize(MainPage p)
{
page = p;
page.OrientationChanged += Page_OrientationChanged;
page.TextSuggestionsBar.DownButtonTap += TextSuggestionsBar_DownButtonTap;
AdjustForCurrentOrientation();
}
// Report whether the mouse event occurred within the popup’s bounds
internal static bool IsOnPopup(MouseEventArgs e)
{
if (!page.Popup.IsOpen)
return false;
Point popupRelativePoint = GetPopupRelativePoint(e);
return (popupRelativePoint.Y >= 0 &&
popupRelativePoint.Y < page.TextSuggestionsBar.ActualHeight);
}
// Return the X,Y position of the mouse, relative to the popup
internal static Point GetPopupRelativePoint(MouseEventArgs e)
{
Point popupRelativePoint = new Point();
// We can use the page-relative X as the popup-relative X
Point pageRelativePoint = e.GetPosition(page);
popupRelativePoint.X = pageRelativePoint.X;
// We can’t use the page-relative Y because the page can be automatically
// “pushed” by the on-screen keyboard, whereas the floating popup remains
// still. Therefore, first get the frame-relative Y:
Point frameRelativePoint = e.GetPosition(null /* the frame */);
popupRelativePoint.Y = frameRelativePoint.Y;
// A frame-relative point is always portrait-oriented, so invert
// the value if we’re currently in a landscape orientation
if (IsMatchingOrientation(PageOrientation.Landscape))
popupRelativePoint.Y = frameRelativePoint.X;
// Now adjust the Y to be relative to the top of the popup
// rather than the top of the screen
if (IsMatchingOrientation(PageOrientation.LandscapeLeft))
popupRelativePoint.Y = -(popupRelativePoint.Y+page.Popup.VerticalOffset);
else
popupRelativePoint.Y -= page.Popup.VerticalOffset;
return popupRelativePoint;
}
static void Page_OrientationChanged(object sender,
OrientationChangedEventArgs e)
{
// Clear the slid-down setting on any orientation change
textSuggestionsBarSlidDown = false;
AdjustForCurrentOrientation();
}
static void TextSuggestionsBar_DownButtonTap(object sender, EventArgs e)
{
textSuggestionsBarSlidDown = true;
AdjustForCurrentOrientation();
}
static bool IsMatchingOrientation(PageOrientation orientation)
{
return ((page.Orientation & orientation) == orientation);
}
static void AdjustForCurrentOrientation()
{
page.TextSuggestionsBar.ResetScrollPosition();
if (IsMatchingOrientation(PageOrientation.Portrait))
{
// Adjust the position, size, and rotation for portrait
page.TextSuggestionsBar.MinWidth = Constants.SCREEN_WIDTH;
page.Popup.HorizontalOffset = 0;
page.Popup.VerticalOffset = Constants.SCREEN_HEIGHT –
Constants.APPLICATION_BAR_THICKNESS – Constants.PORTRAIT_KEYBOARD_HEIGHT
– Constants.TEXT_SUGGESTIONS_HEIGHT*2; // 1 for the real bar, 1 for this
page.Popup.RenderTransform = new RotateTransform { Angle = 0 };
if (textSuggestionsBarSlidDown)
page.Popup.VerticalOffset += Constants.PORTRAIT_KEYBOARD_HEIGHT;
}
else
{
// Adjust the position, size, and rotation for landscape
page.TextSuggestionsBar.MinWidth = Constants.SCREEN_HEIGHT –
Constants.APPLICATION_BAR_THICKNESS;
if (IsMatchingOrientation(PageOrientation.LandscapeLeft))
{
page.Popup.RenderTransform = new RotateTransform { Angle = 90 };
page.Popup.HorizontalOffset = 0;
page.Popup.VerticalOffset = -(Constants.LANDSCAPE_KEYBOARD_HEIGHT +
Constants.TEXT_SUGGESTIONS_HEIGHT*2);
// 1 for the real bar, 1 for this
}
else // LandscapeRight
{
page.Popup.RenderTransform = new RotateTransform { Angle = 270 };
page.Popup.Width = Constants.SCREEN_HEIGHT –
Constants.APPLICATION_BAR_THICKNESS;
page.Popup.HorizontalOffset = -page.Popup.Width;
page.Popup.VerticalOffset = Constants.SCREEN_WIDTH –
Constants.LANDSCAPE_KEYBOARD_HEIGHT –
Constants.TEXT_SUGGESTIONS_HEIGHT*2;
// 1 for the real bar, 1 for this
}
if (textSuggestionsBarSlidDown)
page.Popup.VerticalOffset += Constants.LANDSCAPE_KEYBOARD_HEIGHT;
}
}
}
}

[/code]

  • Due to the manual rotation being done to the popup to make it always match the page’s orientation, GetPopupRelativePoint must adjust the page-relative mouse position in a number of ways, depending on the current orientation.
  • This app uses a number of constants. They are defined in Constants.cs as follows:[code]
    public static class Constants
    {
    public const int SCREEN_WIDTH = 480;
    public const int SCREEN_HEIGHT = 800;
    public const int APPLICATION_BAR_THICKNESS = 72;
    // Part of it is 259px tall, but this is the # we need:
    public const int LANDSCAPE_KEYBOARD_HEIGHT = 256;
    public const int PORTRAIT_KEYBOARD_HEIGHT = 339;
    public const int TEXT_SUGGESTIONS_HEIGHT = 62;
    public const int MARGIN = 12;
    public const int TAP_MARGIN = 14;
    public const int MIN_SCROLL_AMOUNT = 10;
    public static readonly string NEWLINES = Environment.NewLine +
    Environment.NewLine + Environment.NewLine + Environment.NewLine +
    Environment.NewLine;
    }
    [/code]
  • This code handles an event on the TextSuggestionsBar called DownButtonTap and moves the position of the popup to the bottom of the screen when this happens. The next section explains what this is about.

The TextSuggestionsBar User Control

The TextSuggestionsBar user control handles the display of the dot-delimited text suggestions and the proper tapping and scrolling interaction. It also contains a workaround for a problem with hardware keyboards.

Ideally, the popup containing this control would automatically position itself above the on-screen keyboard when it is used, but close to the bottom edge of the screen when a hardware keyboard is used instead. Unfortunately, there is no good way to detect when a hardware keyboard is in use, so this app relies on the user to move it. The TextSuggestionsBar has an extra “down” button that is hidden under the on-screen keyboard when it is in use, but revealed when a hardware keyboard is used. The user can tap this button to move the bar to the bottom, just above the real text suggestions bar. Figure 11.4 shows what this looks like. Rather than consuming space with a corresponding “up” button, this app only moves the bar back to its higher position when the phone’s orientation changes.

FIGURE 11.4 The user must manually move the custom text suggestions bar to the appropriate spot when using a hardware keyboard.
FIGURE 11.4 The user must manually move the custom text suggestions bar to the appropriate spot when using a hardware keyboard.

Listing 11.4 contains the XAML for this user control, and Listing 11.5 contains the codebehind.

LISTING 11.4 TextSuggestionsBar.xaml—The User Interface for the TextSuggestionsBar User Control

[code]

<UserControl x:Class=”WindowsPhoneApp.TextSuggestionsBar”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
IsHitTestVisible=”False”>
<StackPanel>
<Canvas Background=”{StaticResource PhoneChromeBrush}” Height=”62”>
<!– The suggestions go in this stack panel –>
<StackPanel x:Name=”StackPanel” Orientation=”Horizontal” Height=”62”/>
</Canvas>
<!– The double-arrow “button” (just a border with a path) –>
<Border Background=”{StaticResource PhoneChromeBrush}” Width=”62”
Height=”62” HorizontalAlignment=”Left”>
<Path Fill=”{StaticResource PhoneForegroundBrush}”
HorizontalAlignment=”Center” VerticalAlignment=”Center”
Data=”M0,2 14,2 7,11z M0,13 14,13 7,22”/>
</Border>
</StackPanel>
</UserControl>

[/code]

LISTING 11.5 TextSuggestionsBar.xaml.cs—The Code-Behind for the TextSuggestionsBar User Control

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace WindowsPhoneApp
{
public partial class TextSuggestionsBar : UserControl
{
// A custom event, raised when the down button is tapped
public event EventHandler DownButtonTap;
TextBox textBox;
double mouseDownX;
double mouseMoveX;
Border pressedSuggestionElement;
int selectionStart;
int selectionLength;
public TextSuggestionsBar(TextBox textBox)
{
InitializeComponent();
this.textBox = textBox;
}
public void OnMouseDown(Point point)
{
// Grab the current position/selection before it changes! The text box
// still has focus, so the tap is likely to change the caret position
this.selectionStart = this.textBox.SelectionStart;
this.selectionLength = this.textBox.SelectionLength;
this.mouseDownX = this.mouseMoveX = point.X;
this.pressedSuggestionElement = FindSuggestionElementAtPoint(point);
if (this.pressedSuggestionElement != null)
{
// Give the pressed suggestion the hover brushes
this.pressedSuggestionElement.Background =
Application.Current.Resources[“PhoneForegroundBrush”] as Brush;
(this.pressedSuggestionElement.Child as TextBlock).Foreground =
Application.Current.Resources[“PhoneBackgroundBrush”] as Brush;
}
else if (point.Y > this.StackPanel.Height)
{
// Treat this as a tap on the down arrow
if (this.DownButtonTap != null)
this.DownButtonTap(this, EventArgs.Empty);
}
}
public void OnMouseMove(Point point)
{
double delta = point.X – this.mouseMoveX;
if (delta == 0)
return;
// Adjust the stack panel’s left margin to simulate scrolling.
// Don’t let it scroll past either its left or right edge.
double newLeft = Math.Min(0, Math.Max(this.ActualWidth –
this.StackPanel.ActualWidth, this.StackPanel.Margin.Left + delta));
this.StackPanel.Margin = new Thickness(newLeft, 0, 0, 0);
// If a suggestion is currently being pressed but we’ve now scrolled a
// certain amount, cancel the tapping action
if (pressedSuggestionElement != null && Math.Abs(this.mouseMoveX
– this.mouseDownX) > Constants.MIN_SCROLL_AMOUNT)
{
// Undo the hover brushes
pressedSuggestionElement.Background = null;
(pressedSuggestionElement.Child as TextBlock).Foreground =
Application.Current.Resources[“PhoneForegroundBrush”] as Brush;
// Stop tracking the element
pressedSuggestionElement = null;
}
this.mouseMoveX = point.X;
}
public void OnMouseUp(bool isInBounds)
{
if (this.pressedSuggestionElement != null)
{
if (isInBounds)
InsertText();
// Undo the hover brushes
pressedSuggestionElement.Background = null;
(pressedSuggestionElement.Child as TextBlock).Foreground =
Application.Current.Resources[“PhoneForegroundBrush”] as Brush;
// Stop tracking the element
pressedSuggestionElement = null;
}
}
public void ResetScrollPosition()
{
this.StackPanel.Margin = new Thickness(0, 0, 0, 0);
}
public void ClearSuggestions()
{
this.StackPanel.Children.Clear();
ResetScrollPosition();
}
// Each suggestion is added to the stack panel as two elements:
// – A border containing a textblock with a • separator
// – A border containing the suggested text
public void AddSuggestion(Suggestion suggestion)
{
// Add the • element to the stack panel
TextBlock textBlock = new TextBlock { Text = “•”, FontSize = 16,
Margin = new Thickness(this.StackPanel.Children.Count == 0 ? 20 : 3, 6, 4,
0), Foreground = Application.Current.Resources[“PhoneForegroundBrush”]
as Brush, VerticalAlignment = VerticalAlignment.Center };
Border border = new Border();
border.Child = textBlock;
this.StackPanel.Children.Add(border);
// Add the suggested-text element to the stack panel
textBlock = new TextBlock { Text = suggestion.Text, FontSize = 28,
Margin = new Thickness(10, 6, 10, 0),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Foreground = Application.Current.Resources[“PhoneForegroundBrush”]
as Brush };
// MinWidth makes single-character suggestions like / easier to tap
// Stuff the insertion offset into the tag for easy retrieval later
border = new Border { MinWidth = 28, Tag = suggestion.InsertionOffset };
border.Child = textBlock;
this.StackPanel.Children.Add(border);
}
void InsertText()
{
string newText = (this.pressedSuggestionElement.Child as TextBlock).Text;
int numCharsToDelete = ((int)this.pressedSuggestionElement.Tag) * -1;
string allText = this.textBox.Text;
// Perform the insertion
allText = allText.Substring(0, this.selectionStart – numCharsToDelete)
+ newText
+ allText.Substring(this.selectionStart + this.selectionLength);
this.textBox.Text = allText;
// Place the caret immediately after the inserted text
this.textBox.SelectionStart = this.selectionStart + newText.Length –
numCharsToDelete;
}
// Find the Border element at the current point
Border FindSuggestionElementAtPoint(Point point)
{
Border border = null;
// Loop through the borders to find the right one (if there is one)
for (int i = 0; i < this.StackPanel.Children.Count; i++)
{
Border b = this.StackPanel.Children[i] as Border;
// Transform the point to be relative to this border
GeneralTransform generalTransform = this.StackPanel.TransformToVisual(b);
Point pt = generalTransform.Transform(point);
pt.X -= this.StackPanel.Margin.Left; // Adjust for scrolling
// See if the point is within the border’s bounds.
// The extra right margin ensures that there are no “holes” in the bar
// where tapping does nothing.
if (pt.X >= 0 && pt.X < b.ActualWidth + Constants.TAP_MARGIN
&& pt.Y <= this.StackPanel.Height)
{
border = b;
// If this is the • element, treat it as part of the next element
// (the actual word), so return that one instead
if ((b.Child as TextBlock).Text == “•”)
border = this.StackPanel.Children[i + 1] as Border;
break;
}
}
return border;
}
}
}

[/code]

Notes:

  • OnMouseDown takes care of highlighting the tapped suggestion, OnMouseMove performs the scrolling of the bar, and OnMouseUp inserts the highlighted suggestion (if there is one).

The Finished Product

XAML Editor (Dynamic XAML & Popup)

Can I write code that interacts with the phone’s copy & paste feature?

No. Copy & paste functionality is automatically supported for any text box, but there is currently no way for a developer to interact with the clipboard, disable the feature, or otherwise influence its behavior.

 

Tip Calculator (Application Lifecycle & Control Templates)

A tip calculator is one of the classic phone apps that people attempt to build, but creating one that works well enough for people to use on a regular basis, and one that embraces the Windows Phone style, takes a lot of care. This app has four different bottom panes for entering data, and the user can switch between them by tapping one of the four buttons on the top left side of the screen.

The primary bottom pane is for entering the amount of money. It uses a custom number pad styled like the one in the built-in Calculator app. Creating this is more complex than using the standard on-screen keyboard, but the result is more useful and attractive—even if the on-screen keyboard were to use the Number or TelephoneNumber input scopes. This app’s custom number pad contains only the keys that are relevant: the 10 digits, a special key for entering two zeros simultaneously, a backspace key, and a button to clear the entire number. (It also enables entering numbers without the use of a text box.)

The three other bottom panes are all list boxes. They enable the user to choose the desired tip percentage, choose to round the tip or total either up or down, and split the total among multiple people to see the correct perperson cost.

Tip Calculator is the first app to behave differently depending on how it is closed and how it is re-opened, so we’ll first examine what is often referred to as the application lifecycle for a Windows Phone app. Later, this chapter also examines some significant new concepts, such as control templates and routed events.

Understanding an App’s Lifecycle

An app can exit in one of two ways: It can be closed, or it can be deactivated. Technically, the app is terminated in both cases, but many users have different expectations for how most apps should behave in one case versus the other.

A closed app is not only permanently closed, but it should appear to be permanently closed as well. This means that the next time the user runs the app, it should appear to be a “fresh” instance without temporary state left over from last time.

The only way for a user to close an app is to press the hardware Back button while on the app’s initial page. A user can only re-run a closed app by tapping its icon or pinned tile.

A deactivated app should appear to be “pushed to the background.” This is the condition for which an app should provide the illusion that it is still actively running (or running in a “paused” state). Logically, the phone maintains a back stack of pages that the user can keep backing into, regardless of which application each page belongs to. When the user backs into a deactivated app’s page, it should appear as if it were there the whole time, waiting patiently for the user’s return.

Because there’s only one way to close an app, every other action deactivates it instead:

  • The user pressing the hardware Start button
  • The screen locking (either user-provoked or due to timeout)
  • The user directly launching another app by tapping a toast notification or answering a phone call that interrupts your app
  • The app itself launching another app (the phone, web browser, and so on) via a launcher or chooser

The user can return to a deactivated app via the hardware Back button, by unlocking the screen, or by completing whatever task was spawned via a launcher or chooser.

States and Events

An app, therefore, can be in one of three states at any time: running, closed, or deactivated. The PhoneApplicationService class defines four events that notify you when four out of the five possible state transitions occur, as illustrated in Figure 10.1:

  • Launching—Raised for a fresh instance of the app.
  • Closing—Raised when the app is closing for good. Despite the name, a handler for this event cannot cancel the action (and there is no corresponding “closed” event).
  • Deactivated—Raised when the app’s pages are logically sent to the back stack.
  • Activated—Raised when one of the app’s pages is popped off the back stack, making the app run again.
FIGURE 10.1 Four events signal all but one of the possible transitions between three states.
FIGURE 10.1 Four events signal all but one of the possible transitions between three states.

From Figure 10.1, you can see that a deactivated app may never be activated, even if the user wants to activate it later. The back stack may be trimmed due to memory constraints. In this case, or if the phone is powered off, the deactivated apps are now considered to be closed, and apps do not get any sort of notification when this happens (as they are not running at the time). Furthermore, if your app has been deactivated but the user later launches it from its icon or pinned tile, this is a launching action rather than a reactivation. In this case, the new instance of your app receives the Launching event—not the Activated event—and the deactivated instance’s pages are silently removed from the back stack. (Some users might not understand the distinction between leaving an app via the Back versus Start buttons, so your app might never receive a Closing event if a user always leaves apps via the Start button!)

When to Distinguish Between States

Several of the apps in previous chapters have indeed provided the illusion that they are running even when they are not. For example, Tally remembers its current count, Stopwatch pretends to advance its timer, and Ruler remembers the scroll position and current measurement. However, these apps have not made the distinction between being closed versus being deactivated. The data gets saved whether the app is closed or deactivated, and the data gets restored whether the app is launched or activated. Although this behavior is acceptable for these apps (and arguable for Ruler), other apps should often make the distinction between being closed/deactivated and launched/activated. Tip Calculator is one such app.

To decide whether to behave specially for deactivation and activation, consider whether your app involves two types of state:

  • User-configurable settings or other data that should be remembered indefinitely
  • Transient state, like a partially filled form for creating a new item that has not yet been saved

The first type of state should always be saved whether the app is closed or deactivated, and restored whether the app is launched or activated. The second type of state, however, should usually only be saved when deactivated and restored when activated. If the user returns to the app after leaving it for a short period of time (such as being interrupted by a phone call or accidentally locking the screen), he or she expects to see the app exactly how it was left. But if the user launches the app several days later, or expects to see a fresh instance by tapping its icon rather than using the hardware Back button, seeing it in the exact same state could be surprising and annoying, depending on the type of app.

Tip Calculator has data that is useful to remember indefinitely—the chosen tip percentage and whether the user rounded the tip or total—because users likely want to reuse these settings every time they dine out. Forcing users to change these settings from their default values every time the app is launched would be annoying. Therefore, the app persists and restores these settings no matter what.

Tip Calculator also has data that is not useful to remember indefinitely—the current amount of the bill and whether it is being split (and with how many people)—as this information should only be relevant for the current meal. So while it absolutely makes sense to remember this information in the face of a short-term interruption like a phone call or a screen lock, it would be annoying if the user launches the app the following day and is forced to clear these values before entering the correct values for the current meal. Similarly, it makes sense for the app to remember which of the four input panels is currently active to provide the illusion of running-whiledeactivated, but when launching a fresh instance, it makes sense for the app to start with the calculator buttons visible. Therefore, the app persists and restores this information only when it is deactivated and activated.

Implementation

You can attach a handler to any of the four lifecycle events by accessing the current PhoneApplicationService instance as follows:

[code]

Microsoft.Phone.Shell.PhoneApplicationService.Current.Activated +=
Application_Activated;

[/code]

However, a handler for each event is already attached inside the App.xaml file generated by Visual Studio:

[code]

<Application …>

<Application.ApplicationLifetimeObjects>
<!–Required object that handles lifetime events for the application–>
<shell:PhoneApplicationService
Launching=”Application_Launching” Closing=”Application_Closing”
Activated=”Application_Activated” Deactivated=”Application_Deactivated”/>
</Application.ApplicationLifetimeObjects>
</Application>

[/code]

These handlers are empty methods inside the generated App.xaml.cs code-behind file:

[code]

// Code to execute when the application is launching (eg, from Start)
// This code will not execute when the application is reactivated
private void Application_Launching(object sender, LaunchingEventArgs e)
{
}
// Code to execute when the application is activated (brought to foreground)
// This code will not execute when the application is first launched
private void Application_Activated(object sender, ActivatedEventArgs e)
{
}
// Code to execute when the application is deactivated (sent to background)
// This code will not execute when the application is closing
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
}
// Code to execute when the application is closing (eg, user hit Back)
// This code will not execute when the application is deactivated
private void Application_Closing(object sender, ClosingEventArgs e)
{
}

[/code]

With these handlers in place, how do you implement them to persist/restore permanent state and transient state?

Permanent state should be persisted to (and restored from) isolated storage, a topic covered in Part III, “Storing & Retrieving Local Data.” The Setting class used by this book’s apps uses isolated storage internally to persist each value, so this class is all you need to handle permanent state.

Transient state can be managed with the same isolated storage mechanism, but there are fortunately separate mechanisms that make working with transient state even easier: application state and page state.

Application state is a dictionary on the PhoneApplicationState class exposed via its State property, and page state is a dictionary exposed on every page, also via a State property. Application state can be used as follows from anywhere within the app:

[code]

// Store a value
PhoneApplicationService.Current.State[“Amount”] = amount;
// Retrieve a value
if (PhoneApplicationService.Current.State.ContainsKey(“Amount”))
amount = (double)PhoneApplicationService.Current.State[“Amount”];
Page state can be used as follows, inside any of a page’s instance members (where this
refers to the page):
// Store a value
this.State[“Amount”] = amount;
// Retrieve a value
if (this.State.ContainsKey(“Amount”))
amount = (double)this.State[“Amount”];

[/code]

But these dictionaries are more than just simple collections of name/value pairs; their contents are automatically persisted when an app is deactivated and automatically restored when an app is activated. Conveniently, these dictionaries are not persisted when an app is closed, and they are left empty when an app is launched, even if it was previously deactivated with data in its dictionaries.

Values used in the application state and page state dictionaries must be serializable!

These dictionaries get persisted to disk when an app is deactivated, so all the data types used must support the automatic serialization mechanism. Primitive data types are serializable, but UI elements, for example, are not. If you place a nonserializable object in one of the dictionaries, an InvalidDataContractException is raised while the app exits. If you use an instance of your own class with serializable members,be sure that it is marked public,otherwise serialization will fail with a SecurityException.

Thanks to this behavior, apps can often behave appropriately without the need to even handle the lifetime events. Inside a page’s familiar OnNavigatedTo and OnNavigatedFrom methods, the isolatedstorage- based mechanism can be used for permanent data and page state can be used for transient data. The Tip Calculator app does this, as you’ll see in its code-behind.

The User Interface

Figure 10.2 displays the four different modes of Tip Calculator’s single page, each with the name of the bottom element currently showing.

FIGURE 10.2 The bottom input area changes based on which button has been tapped.
FIGURE 10.2 The bottom input area changes based on which button has been tapped.

The buttons used by this app are not normal buttons, because they remain highlighted after they are tapped. This behavior is enabled by toggle buttons, which support the notion of being checked or unchecked. (You can think of a toggle button like a check box that happens to look like a button. In fact, the CheckBox class derives from ToggleButton. Its only difference is its visual appearance.)

Tip Calculator doesn’t use ToggleButton elements, however. Instead, it uses RadioButton, a class derived from ToggleButton that adds built-in behavior for mutual exclusion. In other words, rather than writing code to manually ensure that only one toggle button is checked at a time, radio buttons enforce that only one radio button is checked at a time when multiple radio buttons have the same parent element. When one is checked, the others are automatically unchecked.

The behavior of radio buttons is perfect for Tip Calculator, but the visual appearance is not ideal. Figure 10.3 shows what the app would look like if radio buttons were used without any customizations. It gives the impression that you must choose only one of the four options (like a multiple-choice question), which can be confusing.

Fortunately, Silverlight controls can be radically restyled by giving them new control templates. Tip Calculator uses a custom control template to give its radio buttons the appearance of plain toggle buttons. This gives the best of both worlds: the visual behavior of a toggle button combined with the extra logic in a radio button. The upcoming “Control Templates” section explains how this is done.

FIGURE 10.3 What Tip Calculator would look like with plain radio buttons.
FIGURE 10.3 What Tip Calculator would look like with plain radio buttons.

Listing 10.1 contains the XAML for Tip Calculator’s page.

LISTING 10.1 MainPage.xaml—The User Interface for Tip Calculator

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage” x:Name=”Page”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait” shell:SystemTray.IsVisible=”True”
Loaded=”MainPage_Loaded”>
<phone:PhoneApplicationPage.Resources>
<!– Style to make a radio button look like a plain toggle button –>
<Style x:Key=”RadioToggleButtonStyle” TargetType=”RadioButton”>
<!– Override left alignment of RadioButton: –>
<Setter Property=”HorizontalContentAlignment” Value=”Center”/>
<!– Add tilt effect: –>
<Setter Property=”local:Tilt.IsEnabled” Value=”True”/>
<!– The rest is the normal style of a ToggleButton: –>
<Setter Property=”Background” Value=”Transparent”/>
<Setter Property=”BorderBrush”
Value=”{StaticResource PhoneForegroundBrush}”/>
<Setter Property=”Foreground”
Value=”{StaticResource PhoneForegroundBrush}”/>
<Setter Property=”BorderThickness”
Value=”{StaticResource PhoneBorderThickness}”/>
<Setter Property=”FontFamily”
Value=”{StaticResource PhoneFontFamilySemiBold}”/>
<Setter Property=”FontSize”
Value=”{StaticResource PhoneFontSizeMediumLarge}”/>
<Setter Property=”Padding” Value=”8”/>
<Setter Property=”Template”>
<Setter.Value>
<ControlTemplate TargetType=”ToggleButton”>
<Grid Background=”Transparent” >
<VisualStateManager.VisualStateGroups>

</VisualStateManager.VisualStateGroups>
<Border x:Name=”EnabledBackground”
Background=”{TemplateBinding Background}”
BorderBrush=”{TemplateBinding BorderBrush}”
BorderThickness=”{TemplateBinding BorderThickness}”
Margin=”{StaticResource PhoneTouchTargetOverhang}”>
<ContentControl x:Name=”EnabledContent” Foreground=
“{TemplateBinding Foreground}” HorizontalContentAlignment=
“{TemplateBinding HorizontalContentAlignment}”
VerticalContentAlignment=
“{TemplateBinding VerticalContentAlignment}”
Margin=”{TemplateBinding Padding}”
Content=”{TemplateBinding Content}”
ContentTemplate=”{TemplateBinding ContentTemplate}”/>
</Border>
<Border x:Name=”DisabledBackground” IsHitTestVisible=”False”
Background=”Transparent” Visibility=”Collapsed”
BorderBrush=”{StaticResource PhoneDisabledBrush}”
BorderThickness=”{TemplateBinding BorderThickness}”
Margin=”{StaticResource PhoneTouchTargetOverhang}”>
<ContentControl x:Name=”DisabledContent”
Foreground=”{StaticResource PhoneDisabledBrush}”
HorizontalContentAlignment=
“{TemplateBinding HorizontalContentAlignment}”
VerticalContentAlignment=
“{TemplateBinding VerticalContentAlignment}”
Margin=”{TemplateBinding Padding}”
Content=”{TemplateBinding Content}”
ContentTemplate=”{TemplateBinding ContentTemplate}”/>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!– Style for calculator buttons –>
<Style x:Key=”CalculatorButtonStyle” TargetType=”Button”>
<Setter Property=”FontSize” Value=”36”/>
<Setter Property=”FontFamily”
Value=”{StaticResource PhoneFontFamilySemiLight}”/>
<Setter Property=”BorderThickness” Value=”0”/>
<Setter Property=”Width” Value=”132”/>
<Setter Property=”Height” Value=”108”/>
</Style>
<!– Style for list box items –>
<Style x:Key=”ListBoxItemStyle” TargetType=”ListBoxItem”>
<Setter Property=”FontSize”
Value=”{StaticResource PhoneFontSizeExtraLarge}”/>
<Setter Property=”local:Tilt.IsEnabled” Value=”True”/>
<Setter Property=”Padding” Value=”12,8,8,8”/>
</Style>
<!– Style for text blocks –>
<Style x:Key=”TextBlockStyle” TargetType=”TextBlock”>
<Setter Property=”FontSize”
Value=”{StaticResource PhoneFontSizeExtraLarge}”/>
<Setter Property=”Margin” Value=”0,0,12,0”/>
<Setter Property=”HorizontalAlignment” Value=”Right”/>
<Setter Property=”VerticalAlignment” Value=”Center”/>
</Style>
</phone:PhoneApplicationPage.Resources>
<!– The root grid with the header, the area with four buttons
and text blocks, and the bottom input area –>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”TIP CALCULATOR”
Style=”{StaticResource PhoneTextTitle0Style}”/>
</StackPanel>
<!– The area with four buttons and corresponding text blocks –>
<Grid Grid.Row=”1”>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”1.5*”/>
<ColumnDefinition Width=”*”/>
</Grid.ColumnDefinitions>
<!– The four main buttons –>
<RadioButton x:Name=”AmountButton” Grid.Row=”0” Content=”amount”
Style=”{StaticResource RadioToggleButtonStyle}”
Checked=”RadioButton_Checked”
Tag=”{Binding ElementName=AmountPanel}”/>
<RadioButton x:Name=”TipButton” Grid.Row=”1” Content=” “
Style=”{StaticResource RadioToggleButtonStyle}”
Checked=”RadioButton_Checked”
Tag=”{Binding ElementName=TipListBox}”/>
<RadioButton x:Name=”TotalButton” Grid.Row=”2” Content=” “
Style=”{StaticResource RadioToggleButtonStyle}”
Checked=”RadioButton_Checked”
Tag=”{Binding ElementName=TotalListBox}”/>
<RadioButton x:Name=”SplitButton” Grid.Row=”3” Content=” “
Checked=”RadioButton_Checked”
Style=”{StaticResource RadioToggleButtonStyle}”
Tag=”{Binding ElementName=SplitListBox}”/>
<!– The four main text blocks –>
<TextBlock x:Name=”AmountTextBlock” Grid.Column=”1”
Style=”{StaticResource TextBlockStyle}”/>
<TextBlock x:Name=”TipTextBlock” Grid.Row=”1” Grid.Column=”1”
Style=”{StaticResource TextBlockStyle}”/>
<TextBlock x:Name=”TotalTextBlock” Grid.Row=”2” Grid.Column=”1”
FontWeight=”Bold” Style=”{StaticResource TextBlockStyle}”/>
<TextBlock x:Name=”SplitTextBlock” Grid.Row=”3” Grid.Column=”1”
FontWeight=”Bold” Foreground=”{StaticResource PhoneAccentBrush}”
Style=”{StaticResource TextBlockStyle}”/>
</Grid>
<!– The bottom input area, which overlays four children in the same
grid cell –>
<Grid Grid.Row=”2”>
<!– The calculator buttons shown for “amount” –>
<Canvas x:Name=”AmountPanel” Visibility=”Collapsed”>
<Button Style=”{StaticResource CalculatorButtonStyle}”
Background=”{Binding CalculatorMainBrush, ElementName=Page}”
Content=”7” Canvas.Left=”-6” Canvas.Top=”-1”/>
<Button Style=”{StaticResource CalculatorButtonStyle}”
Background=”{Binding CalculatorMainBrush, ElementName=Page}”
Content=”8” Canvas.Left=”114” Canvas.Top=”-1”/>
<Button Style=”{StaticResource CalculatorButtonStyle}”
Background=”{Binding CalculatorMainBrush, ElementName=Page}”
Content=”9” Canvas.Left=”234” Canvas.Top=”-1”/>
<Button Style=”{StaticResource CalculatorButtonStyle}”
Background=”{Binding CalculatorMainBrush, ElementName=Page}”
Content=”4” Canvas.Top=”95” Canvas.Left=”-6”/>
<Button Style=”{StaticResource CalculatorButtonStyle}”
Background=”{Binding CalculatorMainBrush, ElementName=Page}”
Content=”5” Canvas.Top=”95” Canvas.Left=”114”/>
<Button Style=”{StaticResource CalculatorButtonStyle}”
Background=”{Binding CalculatorMainBrush, ElementName=Page}”
Content=”6” Canvas.Top=”95” Canvas.Left=”234”/>
<Button Style=”{StaticResource CalculatorButtonStyle}”
Background=”{Binding CalculatorMainBrush, ElementName=Page}”
Content=”1” Canvas.Top=”191” Canvas.Left=”-6”/>
<Button Style=”{StaticResource CalculatorButtonStyle}”
Background=”{Binding CalculatorMainBrush, ElementName=Page}”
Content=”2” Canvas.Top=”191” Canvas.Left=”114”/>
<Button Style=”{StaticResource CalculatorButtonStyle}”
Background=”{Binding CalculatorMainBrush, ElementName=Page}”
Content=”3” Canvas.Top=”191” Canvas.Left=”234”/>
<Button Style=”{StaticResource CalculatorButtonStyle}”
Background=”{Binding CalculatorMainBrush, ElementName=Page}”
Content=”0” Canvas.Top=”287” Canvas.Left=”-6”/>
<Button Style=”{StaticResource CalculatorButtonStyle}”
Background=”{Binding CalculatorMainBrush, ElementName=Page}”
Content=”00” Width=”252” Canvas.Top=”287” Canvas.Left=”114”/>
<Button Style=”{StaticResource CalculatorButtonStyle}” FontSize=”32”
FontFamily=”{StaticResource PhoneFontFamilySemiBold}”
Background=”{Binding CalculatorSecondaryBrush, ElementName=Page}”
Content=”C” Height=”204” Canvas.Top=”-1” Canvas.Left=”354”/>
<Button x:Name=”BackspaceButton” Height=”204”
Style=”{StaticResource CalculatorButtonStyle}”
Background=”{Binding CalculatorSecondaryBrush, ElementName=Page}”
Canvas.Top=”191” Canvas.Left=”354”>
<!– The “X in an arrow” backspace drawing –>
<Canvas Width=”48” Height=”32”>
<Path x:Name=”BackspaceXPath” Data=”M24,8 39,24 M39,8 24,24”
Stroke=”{StaticResource PhoneForegroundBrush}”
StrokeThickness=”4”/>
<Path x:Name=”BackspaceBorderPath” StrokeThickness=”2”
Data=”M16,0 47,0 47,31 16,31 0,16.5z”
Stroke=”{StaticResource PhoneForegroundBrush}”/>
</Canvas>
</Button>
</Canvas>
<!– The list box shown for “X% tip” –>
<ListBox x:Name=”TipListBox” Visibility=”Collapsed”
SelectionChanged=”TipListBox_SelectionChanged”/>
<!– The list box shown for “total” –>
<ListBox x:Name=”TotalListBox” Visibility=”Collapsed”
SelectionChanged=”TotalListBox_SelectionChanged”>
<ListBoxItem Style=”{StaticResource ListBoxItemStyle}”
Content=”exact” Tag=”NoRounding”/>
<ListBoxItem Style=”{StaticResource ListBoxItemStyle}”
Content=”round tip down” Tag=”RoundTipDown”/>
<ListBoxItem Style=”{StaticResource ListBoxItemStyle}”
Content=”round tip up” Tag=”RoundTipUp”/>
<ListBoxItem Style=”{StaticResource ListBoxItemStyle}”
Content=”round total down” Tag=”RoundTotalDown”/>
<ListBoxItem Style=”{StaticResource ListBoxItemStyle}”
Content=”round total up” Tag=”RoundTotalUp”/>
</ListBox>
<!– The list box shown for “split check” –>
<ListBox x:Name=”SplitListBox” Visibility=”Collapsed”
SelectionChanged=”SplitListBox_SelectionChanged”/>
</Grid>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Notes:

  • The page’s resources collection contains custom styles for the radio buttons (which contains the custom control template), calculator buttons, list box items, and text blocks.
  • The PhoneTitlePanelStyle and PhoneTextTitle0Style styles, the latter of which was introduced in the preceding chapter, are defined in App.xaml (and not shown in this chapter). This app, and the remaining apps in this book, does this with commonly-used styles so they can be easily shared among multiple pages.
  • For convenience, several elements have their Tag property set. For example, the radio buttons set their Tag to the element that should be made visible when each one is checked. The code-behind retrieves the element reference and performs the work to make it visible.
  • Because the content of the last three radio buttons is dynamic, the XAML file leaves them blank to avoid a flicker when the code-behind restores their current values. They are set to a string with a space in it to prevent them from initially being too short.
  • AmountPanel is a canvas with precisely positioned and precisely sized calculator buttons. This could have been done with a grid instead, although each button would have to be given negative margins, because the desired style of the buttons requires overlapping them a bit so the visible space between them is 12 pixels rather than 24. Because this app only supports the portrait orientation, the hardcoded canvas layout works just fine.
  • The built-in Calculator app that this is modeled after uses two different colors of buttons that are similar to but not quite the same as the PhoneChromeBrush resource. Therefore, this page defines two custom brushes as properties in its code-behind file— CalculatorMainBrush for the digit keys and CalculatorSecondaryBrush for the other keys. The calculator buttons use data binding to set each background to the value of the appropriate property. This is why the page is given the name of “Page”— so it can be referenced in the databinding expressions.
    FIGURE 10.4 The custom brushes dynamically change with the current theme, so Tip Calculator’s buttons match the built-in Calculator’s buttons in the light theme.
    FIGURE 10.4 The custom brushes dynamically change with the current theme, so Tip Calculator’s buttons match the built-in Calculator’s buttons in the light theme.

    The reason data binding is used is that these two brushes must change for the light theme versus the dark theme. As shown in Figure 10.4, light-themed buttons that match the built-in Calculator app have different colors. If these two custom brushes did not ever need to change, they could have been defined as simple resources on the page and StaticResource syntax could have been used to set each button’s background.

  • The calculator buttons purposely do not use the tilting effect used on the toggle buttons, because this matches the behavior of the built-in Calculator app. The only thing missing is the sound effect when tapping each button!
  • The graphical content for the backspace button is created with two Path elements. (See Appendix E, “Geometry Reference,” to understand the syntax.) Because the content is vector-based, the codebehind can (and does) easily update its color dynamically to ensure that it remains visible when the button is pressed.
  • Rather than adding text blocks to TotalListBox, this code uses instances of a control called ListBoxItem. List box items are normally the best kind of element to add to a list box because they automatically highlight their content with the theme’s accent color when it is selected (if their content is textual). You can see the automatic highlighting of selected items in Figure 10.2.

Control Templates

A control template can be set directly on an element with its Template property, although this property is usually set inside a style. For demonstration purposes, the following button is directly given a custom control template that makes it look like the red ellipse shown in Figure 10.5:

FIGURE 10.5 A normal button restyled to look like a red ellipse.
FIGURE 10.5 A normal button restyled to look like a red ellipse.

[code]

<Button Content=”ok”>
<Button.Template>
<ControlTemplate TargetType=”Button”>
<Ellipse Fill=”Red” Width=”200” Height=”50”/>
</ControlTemplate>
</Button.Template>
</Button>

[/code]

Despite its custom look, the button still has all the same behaviors, such as a Click event that gets raised when it is tapped. After all, it is still an instance of the Button class!

This is not a good template, however, because it ignores properties on the button. For example, the button in Figure 10.5 has its Content property set to “ok” but that does not get displayed. If you’re creating a control template that’s meant to be shared among multiple controls, you should data-bind to various properties on the control. The following template updates the previous one to respect the button’s content, producing the result in Figure 10.6:

FIGURE 10.6 The button’s control template now shows its “ok” content.
FIGURE 10.6 The button’s control template now shows its “ok” content.

[code]

<Button Content=”ok”>
<Button.Template>
<ControlTemplate TargetType=”Button”>
<Grid Width=”200” Height=”50”>
<Ellipse Fill=”Red”/>
<TextBlock Text=”{TemplateBinding Content}”
HorizontalAlignment=”Center” VerticalAlignment=”Center”/>
</Grid>
</ControlTemplate>
</Button.Template>
</Button>

[/code]

Rather than using normal Binding syntax, the template uses TemplateBinding syntax. This works just like Binding, but the data source is automatically set to the instance of the control being templated, so it’s ideal for use inside control templates. In fact, TemplateBinding can only be used inside control templates and data templates.

Of course, a button can contain nontext content, so using a text block to display it creates an artificial limitation. To ensure that all types of content get displayed properly, you can use a generic content control instead of a text block. It would also be nice to respect several other properties of the button. The following control template, placed in a style shared by several buttons, does this:

[code]

<phone:PhoneApplicationPage …>
<phone:PhoneApplicationPage.Resources>
<Style x:Name=”ButtonStyle” TargetType=”Button”>
<!– Some default property values –>
<Setter Property=”Background” Value=”Red”/>
<Setter Property=”Padding” Value=”12”/>
<!– The custom control template –>
<Setter Property=”Template”>
<Setter.Value>
<ControlTemplate TargetType=”Button”>
<Grid>
<Ellipse Fill=”{TemplateBinding Background}”/>
<ContentControl Content=”{TemplateBinding Content}”
Margin=”{TemplateBinding Padding}”
HorizontalAlignment=”{TemplateBinding HorizontalContentAlignment}”
VerticalAlignment=”{TemplateBinding VerticalContentAlignment}”/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</phone:PhoneApplicationPage.Resources>
<StackPanel>
<!– button 1 –>
<Button Content=”ok” Style=”{StaticResource ButtonStyle}”/>
<!– button 2 –>
<Button Background=”Lime” Style=”{StaticResource ButtonStyle}”>
<Button.Content>
<!– The “X in an arrow” backspace drawing –>
<Canvas Width=”48” Height=”32”>
<Path x:Name=”BackspaceXPath” Data=”M24,8 39,24 M39,8 24,24”
Stroke=”{StaticResource PhoneForegroundBrush}”
StrokeThickness=”4”/>
<Path x:Name=”BackspaceBorderPath” StrokeThickness=”2”
Data=”M16,0 47,0 47,31 16,31 0,16.5z”
Stroke=”{StaticResource PhoneForegroundBrush}”/>
</Canvas>
</Button.Content>
</Button>
<!– button 3 –>
<Button Content=”content alignment and padding”
HorizontalContentAlignment=”Right”
Padding=”50”
Style=”{StaticResource ButtonStyle}”/>
<!– button 4 –>
<Button Content=”5 properties that just work” HorizontalAlignment=”Left”
Height=”100” FontSize=”40” FontStyle=”Italic” Margin=”20”
Style=”{StaticResource ButtonStyle}”/>
</StackPanel>
</phone:PhoneApplicationPage>

[/code]

The result of this XAML is shown in Figure 10.7. By removing the hardcoded width and height from the template, the buttons are automatically given the appropriate size based on their layout properties and the space provided by their parent element. This is why all the buttons now stretch horizontally by default and why the last button is able to get the desired effect when setting its height and alignment. The second button demonstrates that nontext content now works as well as setting a custom background brush. Because the default red brush is moved into the style and the template binds to the current background, the background is now overridable by an individual button while preserving its default appearance. The same is true for the padding, which the third button is able to override. Notice that the five properties (other than Content and Style) set on the last button automatically work without any special treatment needed by the control template.

It might seem counterintuitive at first, but the template maps the control’s padding to the content control’s margin, and it maps the control’s content alignment properties to the content control’s regular alignment properties. This is a common practice, as the definition of padding is the margin around the inner content, and the definition of the content alignment properties is the alignment of the inner content.

FIGURE 10.7 The custom control template respects many properties that are customized on four different buttons.
FIGURE 10.7 The custom control template respects many properties that are customized on four different buttons.

Still, with all this work, the control template used for Figure 10.7 is not complete because it does not respect the various visual states of the buttons. A button should have a different appearance when it is pressed and a different appearance when it is disabled.

The Code-Behind

Listing 10.2 contains the code-behind for Tip Calculator’s page. It makes use of the following enum defined in a separate file (RoundingType.cs):

[code]

public enum RoundingType
{
NoRounding,
RoundTipDown,
RoundTipUp,
RoundTotalDown,
RoundTotalUp
}

[/code]

LISTING 10.2 MainPage.xaml.cs—The Code-Behind for Tip Calculator

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Persistent settings. These are remembered no matter what.
Setting<RoundingType> savedRoundingType =
new Setting<RoundingType>(“RoundingType”, RoundingType.NoRounding);
Setting<double> savedTipPercent = new Setting<double>(“TipPercent”, .15);
// The current values used for the calculation
double amount;
double tipPercent;
double tipAmount;
double totalAmount;
int split = 1;
RoundingType roundingType;
// Which of the four radio buttons is currently checked
RadioButton checkedButton;
// Two theme-specific custom brushes
public Brush CalculatorMainBrush { get; set; }
public Brush CalculatorSecondaryBrush { get; set; }
public MainPage()
{
InitializeComponent();
// A single handler for all calculator button taps
this.AmountPanel.AddHandler(Button.MouseLeftButtonUpEvent,
new MouseButtonEventHandler(CalculatorButton_MouseLeftButtonUp),
true /* handledEventsToo */);
// Handlers to ensure that the backspace button’s vector content changes
// color appropriately when the button is pressed
this.BackspaceButton.AddHandler(Button.MouseLeftButtonDownEvent,
new MouseButtonEventHandler(BackspaceButton_MouseLeftButtonDown),
true /* handledEventsToo */);
this.BackspaceButton.AddHandler(Button.MouseLeftButtonUpEvent,
new MouseButtonEventHandler(BackspaceButton_MouseLeftButtonUp),
true /* handledEventsToo */);
this.BackspaceButton.MouseMove += BackspaceButton_MouseMove;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Remember transient page data that isn’t appropriate to always persist
this.State[“Amount”] = this.amount;
this.State[“Split”] = this.split;
this.State[“CheckedButtonName”] = this.checkedButton.Name;
// Save the persistent settings
this.savedRoundingType.Value = this.roundingType;
this.savedTipPercent.Value = this.tipPercent;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Set the colors of the two custom brushes based on whether
// we’re in the light theme or dark theme
if ((Visibility)Application.Current.Resources[“PhoneLightThemeVisibility”]
== Visibility.Visible)
{
this.CalculatorMainBrush = new SolidColorBrush(
Color.FromArgb(0xFF, 0xEF, 0xEF, 0xEF));
this.CalculatorSecondaryBrush = new SolidColorBrush(
Color.FromArgb(0xFF, 0xDE, 0xDF, 0xDE));
}
else
{
this.CalculatorMainBrush = new SolidColorBrush(
Color.FromArgb(0xFF, 0x18, 0x1C, 0x18));
this.CalculatorSecondaryBrush = new SolidColorBrush(
Color.FromArgb(0xFF, 0x31, 0x30, 0x31));
}
// Restore transient page data, if there is any from last time
if (this.State.ContainsKey(“Amount”))
this.amount = (double)this.State[“Amount”];
if (this.State.ContainsKey(“Split”))
this.split = (int)this.State[“Split”];
// Restore the persisted settings
this.roundingType = this.savedRoundingType.Value;
this.tipPercent = this.savedTipPercent.Value;
RefreshAllCalculations();
// Fill TipListBox and set its selected item correctly
this.TipListBox.Items.Clear();
for (int i = 50; i >= 0; i–)
{
ListBoxItem item = new ListBoxItem { Content = i + “% tip”,
Tag = (double)i / 100,
Style = this.Resources[“ListBoxItemStyle”] as Style };
if ((double)item.Tag == this.tipPercent)
item.IsSelected = true;
this.TipListBox.Items.Add(item);
}
// Fill SplitListBox and set its selected item correctly
this.SplitListBox.Items.Clear();
for (int i = 1; i <= 20; i++)
{
ListBoxItem item = new ListBoxItem {
Content = (i == 1 ? “do not split” : i + “ people”), Tag = i,
Style = this.Resources[“ListBoxItemStyle”] as Style };
if ((int)item.Tag == this.split)
item.IsSelected = true;
this.SplitListBox.Items.Add(item);
}
// TotalListBox is already filled in XAML, but set its selected item
this.TotalListBox.SelectedIndex = (int)this.roundingType;
}
void MainPage_Loaded(object sender, EventArgs e)
{
// Restore one more transient value: which radio button was checked when
// the app was deactivated.
// This is done here instead of inside OnNavigatedTo because the Loaded
// event is raised after the data binding occurs that sets each button’s
// Tag (needed by the handler called when IsChecked is set to true)
if (this.State.ContainsKey(“CheckedButtonName”))
{
RadioButton button =
this.FindName((string)this.State[“CheckedButtonName”]) as RadioButton;
if (button != null)
button.IsChecked = true;
}
else
{
// For a fresh instance of the app, check the amount button
this.AmountButton.IsChecked = true;
}
}
// A single handler for all calculator button taps
void CalculatorButton_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
// Although sender is the canvas, the OriginalSource is the tapped button
Button button = e.OriginalSource as Button;
if (button == null)
return;
string content = button.Content.ToString();
// Determine what to do based on the string content of the tapped button
double digit;
if (content == “00”)
{
// Append two zeros
this.amount *= 100;
}
else if (double.TryParse(content, out digit)) // double so division works
{
// Append the digit
this.amount *= 10;
this.amount += digit / 100;
}
else if (content == “C”)
{
// Clear the amount
this.amount = 0;
}
else // The backspace button
{
// Chop off the last digit.
// The multiplication preserves the first digit after the decimal point
// because the cast to int chops off what’s after it
int temp = (int)(this.amount * 10);
// Shift right by 2 places (1 extra due to the temporary multiplication)
this.amount = (double)temp / 100;
}
RefreshAllCalculations();
}
void TipListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
{
// The item’s Tag has been set to the actual percent value
this.tipPercent = (double)(e.AddedItems[0] as ListBoxItem).Tag;
RefreshAllCalculations();
}
}
void TotalListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
{
// The item’s Tag has been set to a string containg one of the enum’s
// named values. Use Enum.Parse to convert to string to an instance
// of the RoundingType enum.
this.roundingType = (RoundingType)Enum.Parse(typeof(RoundingType),
(e.AddedItems[0] as ListBoxItem).Tag.ToString(), true);
RefreshAllCalculations();
}
}
void SplitListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
{
// The item’s Tag has been set to the split number
this.split = (int)(e.AddedItems[0] as ListBoxItem).Tag;
RefreshSplitTotal();
}
}
void RefreshAllCalculations()
{
RefreshAmount();
RefreshTip();
RefreshTotal();
RefreshSplitTotal();
}
void RefreshAmount()
{
// Use currency string formatting (“C”) to get the proper display
this.AmountTextBlock.Text = this.amount.ToString(“C”);
}
void RefreshTip()
{
// The content of the tip button and text block are impacted by the
// current rounding setting.
string buttonLabel = (this.tipPercent * 100) + “% tip”;
switch (this.roundingType)
{
case RoundingType.RoundTipDown:
this.tipAmount = Math.Floor(this.amount * this.tipPercent);
buttonLabel += “ (rounded)”;
break;
case RoundingType.RoundTipUp:
this.tipAmount = Math.Ceiling(this.amount * this.tipPercent);
buttonLabel += “ (rounded)”;
break;
default:
this.tipAmount = this.amount * this.tipPercent;
break;
}
this.TipTextBlock.Text = this.tipAmount.ToString(“C”); // C == Currency
this.TipButton.Content = buttonLabel;
}
void RefreshTotal()
{
// The content of the total button and text block are impacted by the
// current rounding setting.
string buttonLabel = “total”;
switch (this.roundingType)
{
case RoundingType.RoundTotalDown:
this.totalAmount = Math.Floor(this.amount + this.tipAmount);
buttonLabel += “ (rounded)”;
break;
case RoundingType.RoundTotalUp:
this.totalAmount = Math.Ceiling(this.amount + this.tipAmount);
buttonLabel += “ (rounded)”;
break;
default:
this.totalAmount = this.amount + this.tipAmount;
break;
}
this.TotalTextBlock.Text = this.totalAmount.ToString(“C”); // C == Currency
this.TotalButton.Content = buttonLabel;
}
void RefreshSplitTotal()
{
if (this.split == 1)
{
// Don’t show the value if we’re not splitting the check
this.SplitTextBlock.Text = “”;
this.SplitButton.Content = “split check”;
}
else
{
this.SplitTextBlock.Text = (this.totalAmount / this.split).ToString(“C”);
this.SplitButton.Content = this.split + “ people”;
}
}
// Called when any of the four toggle buttons are tapped
void RadioButton_Checked(object sender, RoutedEventArgs e)
{
// Which button was tapped
this.checkedButton = sender as RadioButton;
// Which bottom element to show (which was stored in Tag in XAML)
UIElement bottomElement = this.checkedButton.Tag as UIElement;
// Hide all bottom elements…
this.AmountPanel.Visibility = Visibility.Collapsed;
this.TipListBox.Visibility = Visibility.Collapsed;
this.TotalListBox.Visibility = Visibility.Collapsed;
this.SplitListBox.Visibility = Visibility.Collapsed;
// …then show the correct one
bottomElement.Visibility = Visibility.Visible;
// If a list box was just shown, ensure its selected item is on-screen.
// This is delayed because a layout pass must first run (as a result of
// setting Visibility) in order for ScrollIntoView to have any effect.
this.Dispatcher.BeginInvoke(delegate()
{
if (sender == this.TipButton)
this.TipListBox.ScrollIntoView(this.TipListBox.SelectedItem);
else if (sender == this.TotalButton)
this.TotalListBox.ScrollIntoView(this.TotalListBox.SelectedItem);
else if (sender == this.SplitButton)
this.SplitListBox.ScrollIntoView(this.SplitListBox.SelectedItem);
});
}
// Change the color of the two paths inside the backspace button when pressed
void BackspaceButton_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
this.BackspaceXPath.Stroke =
Application.Current.Resources[“PhoneBackgroundBrush”] as Brush;
this.BackspaceBorderPath.Stroke =
Application.Current.Resources[“PhoneBackgroundBrush”] as Brush;
}
// Change the color of the two paths back when no longer pressed
void BackspaceButton_MouseLeftButtonUp(object sender, MouseEventArgs e)
{
this.BackspaceXPath.Stroke =
Application.Current.Resources[“PhoneForegroundBrush”] as Brush;
this.BackspaceBorderPath.Stroke =
Application.Current.Resources[“PhoneForegroundBrush”] as Brush;
}
// Workaround for when the finger has not yet been released but the color
// needs to change back because the finger is no longer over the button
void BackspaceButton_MouseMove(object sender, MouseEventArgs e)
{
// this.BackspaceButton.IsMouseOver lies when it has captured the mouse!
// Use GetPosition instead:
Point relativePoint = e.GetPosition(this.BackspaceButton);
// We can get away with this simple check because
// the button is in the bottom-right corner
if (relativePoint.X < 0 || relativePoint.Y < 0)
BackspaceButton_MouseLeftButtonUp(null, null); // Not over the button
else
BackspaceButton_MouseLeftButtonDown(null, null); // Still over the button
}
}
}

[/code]

Notes:

  • In the constructor, three out of four handlers are attached to events using a special AddHandler method that works with a type of event called a routed event. Routed events are discussed later in this section.
  • Inside OnNavigatedFrom and OnNavigatedTo (and MainPage_Loaded), you can see the separate handling of permanent data stored in Setting objects and transient data stored in page state. Although one of the pieces of information to save in page state is the currently-checked radio button, a reference to the radio button itself cannot be placed in page state because it is not serializable. Instead, the radio button’s name is put in the dictionary. When this state is restored inside MainPage_Loaded, the page’s FindName method is called with the saved name in order to retrieve the correct instance of the radio button.
  • Inside OnNavigatedTo, the trick to set the two custom brushes differently for the light versus dark theme is accomplished by checking the value of the PhoneLightThemeVisibility resource.
  • Unlike with TotalListBox, the items for TipListBox and SplitListBox are created in codebehind because they are much longer lists that can easily be created in a loop. These list box items also have their Tag property set so the code that processes the selected item has a reliable way to discover the meaning of the selected item without parsing its string content. List box items have a handy IsSelected property that can be set to select an item rather than using the list box’s SelectedItem or SelectedIndex property. The two loops make use of this to initially select the appropriate values.
  • In the three list box SelectionChanged event handlers, e.AddedItems[0] is used to reference the selected item rather than the list box’s SelectedItem property. This is just done for demonstration purposes, as either approach does the same thing. List boxes can support multiple selections (if their SelectionMode property is set to Multiple), so any time SelectionChanged is raised, you can discover what items have been selected or deselected via the e parameter’s AddedItems and RemovedItems properties. When a list box only supports a single selection, as in this app, AddedItems and RemovedItems can only have zero or one element.
  • The strings created for the text blocks use currency formatting by passing “C” to ToString. For the English (United States) region, this is what prepends the dollar signs to the numeric displays. If you change your phone’s “Region format” to “French (France)” under the phone’s “region & language” settings, the currency formatting automatically adjusts its display, as shown in Figure 10.8.

    FIGURE 10.8 When the region format is French (France), the euro symbol and comma are automatically used instead of a dollar sign and decimal point.
    FIGURE 10.8 When the region format is French (France), the euro symbol and comma are automatically used instead of a dollar sign and decimal point.
  • RadioButton_Checked has logic to ensure the selected item is not scrolled off-screen when the bottom pane switches to a list box. This is accomplished with list box’s ScrollIntoView method. However, it is called inside in asynchronous callback because it doesn’t work when the list box isn’t visible, and the setting of its Visibility property doesn’t take effect instantly. (It happens after the event handler returns and Silverlight updates the layout of the page.) Ideally this logic would have been performed in an event handler for a “visibility changed” event, but no such event exists in Silverlight.
  • Because the background color of buttons invert when pressed, BackspaceButton_MouseLeftButtonUp and BackspaceButton_MouseLeftButtonDown swap the stroke brushes of the paths inside the backspace button to ensure they remain visible. However, doing the work in these two event handlers isn’t quite enough. When the user holds their finger on the button and drags it off without releasing their finger, the button colors revert to normal but the MouseLeftButtonUp event is not yet raised to revert the path strokes in sync.

    To detect this situation, the backspace button’s MouseMove event is also handled. This event continues to get raised even when the finger is moving outside of the button’s bounds because the button “captures” the mouse input when the finger is depressed and doesn’t release it until the finger is released. The MouseMove handler (BackspaceButton_MouseMove) determines whether the finger is outside the bounds of the button, and calls either the MouseLeftButtonUp or MouseLeftButtonDown handler to adjust the strokes accordingly. As a result, the custom graphics in the backspace button behave appropriately in every situation. Figure 10.9 shows the appearance of the backspace button while a finger is pressing it.

    This behavior would be simpler to implement with Visual State Manager animations inside a custom control template for the backspace button. However, it is too early in this book to make use of these features.

Routed Events

FIGURE 10.9 When the backspace button is pressed, you can always see the inner content, thanks to the code that switches its brush.
FIGURE 10.9 When the backspace button is pressed, you can always see the inner content, thanks to the code that switches its brush.

 

Some of the events raised by Silverlight elements, called routed events, have extra behavior in order to work well with a tree of elements. When a routed event is raised, it travels up the element tree from the source element all the way to the root, getting raised on each element along the way. This process is called event bubbling.

Some elements, such as buttons, leverage event bubbling to be able to provide a consistent Click event even if it contents are a complex tree of elements. Even if the user taps an element nested many layers deep, the MouseLeftButtonUp event bubbles up to the button so it can raise Click. Thanks to event bubbling, the button’s code has no idea what its contents actually are, nor does it need to.

Some elements, such as buttons, also halt event bubbling from proceeding any further. Because buttons want their consumers to use their Click event rather than listening to MouseLeftButtonUp and/or MouseLeftButtonDown, it marks these events as handled when it receives them. (This is done via an internal mechanism. Your code doesn’t have a way to halt bubbling.)

Routed Events in Tip Calculator

In Listing 10.2, Tip Calculator leverages event bubbling for convenience. Rather than attaching a Click event handler to each of the 13 buttons individually, it attaches a single MouseLeftButtonUp event handler to their parent canvas using the AddHandler method supported by all UI elements and a static MouseLeftButtonUpEvent field that identifies the routed event:

[code]

// A single handler for all calculator button taps
this.AmountPanel.AddHandler(Button.MouseLeftButtonUpEvent,
new MouseButtonEventHandler(CalculatorButton_MouseLeftButtonUp),
true /* handledEventsToo */);

[/code]

This event is chosen, rather than Click, because MouseLeftButtonUp is a routed event whereas Click is not. Although attaching this handler could be done in XAML with the same syntax used for any event, the attaching is done in C# to enable special behavior. By passing true as the last parameter, we are able to receive the event even though the button has halted its bubbling! Therefore, the halting done by buttons is just an illusion; the bubbling still occurs, but you must go out of your way to see it.

Tip Calculator also leverages this special behavior to add its brush-changing MouseLeftButtonDown and MouseLeftButtonUp handlers to the backspace button. Without attaching these handlers in code with the true third parameter, it would never receive these events. In contrast, it attaches the MouseMove handler with normal += syntax because MouseMove is not a routed event. (Alternatively, it could have attached the MouseMove handler in XAML.)

Determining Which Events Are Routed Events

You can figure out which Silverlight events are routed in one of three ways:

  • Looking for a corresponding static field of type RoutedEvent on the class with the event. For example, all UI elements have a static field called MouseLeftButtonUpEvent, among others.
  • Checking if the second parameter of corresponding event handlers derives from RoutedEventArgs.
  • Reading the documentation (but that’s no fun).

You cannot define your own routed events.

Routed Event Handlers

Handlers for routed events have a signature matching the pattern for general .NET event handlers: The first parameter is an object typically named sender, and the second parameter (typically named e) is a class that derives from EventArgs. For a routed event handler, the sender is always the element to which the handler was attached. The e parameter is (or derives from) RoutedEventArgs, a subclass of EventArgs with an OriginalSource property that exposes the element that originally raised the event.

Handlers typically want to interact with the original source rather than the sender. Because CalculatorButton_MouseLeftButtonUp is attached to AmountPanel in Listing 10.2, it uses OriginalSource to get to the relevant button:

[code]

// A single handler for all calculator button taps
void CalculatorButton_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
// Although sender is the canvas, the OriginalSource is the tapped button
Button button = e.OriginalSource as Button;

}

[/code]

The Finished Product

Tip Calculator