Arduino and Physical Computing

Arduino is an open source electronics platform comprising easy-to-use hardware and software. The microcontroller can receive input from a range of sensors and can send signals to control other devices. It is an environment for bridging computers and the physical world, also called physical computing.

Purchase an Arduino board and a USB cable, and download the software and drivers. Note that you can build your own board and circuitry, but Arduino offers the ease of use of already custom-made boards, especially if this is a new area for you.

Arduino can communicate with ActionScript and AIR via TinkerProxy or Serproxy, which are local proxies between the serial port and AIR via a socket server.

Read Mike Chambers’s blog on how to get started, at http://www.mikechambers.com/ blog/2010/08/04/getting-started-with-flash-and-arduino/.

Mike also developed a speed detector project for gear cars. It uses AIR 2.5 for Android. Two photo resistors are connected to the board that monitors when their values change. Two laser pointers beam on them at all times. When the car breaks one light and then the next, the times are sent to the AIR application that determines the speed. For more information, go to http://www.mikechambers.com/blog/2010/08/11/accelerate-flash-ar duino-based-speedometer/.

Get acquainted with sensors to get ideas. You can detect a variety of things such as light, humidity, temperature, pressure, electric current, sound, and time (for more information, see http://www.arduino.cc/playground/Main/InterfacingWithHardware).

Use of this technology is not limited to mobile devices, but such devices offer freedom of movement and open up new possibilities.

P2P Over a Remote Network

To use networking remotely, you need an RTMFP-capable server, such as Flash Media Server.

If you do not have access to such a server, Adobe provides a beta developer key to use its Cirrus service. Sign up to instantly receive a developer key and a URL, at http://labs .adobe.com/technologies/cirrus/.

The traditional streaming model requires clients to receive all data from a centralized server cluster. Scaling is achieved by adding more servers. Figure 15-2 shows traditional streaming/communication with the Unicast model and RTMFP in Flash Player/Cirrus.

Figure 15-2. Traditional streaming/communication with the Unicast model (left) and RTMFP in Flash Player 10.1/Cirrus 2 (right)
Figure 15-2. Traditional streaming/communication with the Unicast model (left) and RTMFP in Flash Player 10.1/Cirrus 2 (right)

RTMFP, now in its second generation, supports application-level multicast. Multicasting is the process of sending messages as a single transmission from one source to the group where each peer acts as a relay to dispatch the data to the next peer. It reduces the load on the server and there is no need for a streaming server.

The Cirrus service is only for clients communicating directly. It has low latency and good security. It does not support shared objects or custom server-side programming. You could still use shared objects with Flash Media Server, but via the traditional clientserver conduit.

The NetGroup uses ring topology. Its neighborCount property stores the number of peers. Each peer is assigned a peerID, which can be mapped to a group address using Group.convertPeerIDToGroupAddress(connection.nearID). An algorithm is run every few seconds to update ring positions for the group.

When the group is connected, you can obtain statistics such as Quality of Service in bytes per second from NetGroup’s info property:

[code]

function onStatus(event:NetStatusEvent):void {
if (event.info.code == NetGroup.Connect.Success”) {
trace(event.info.group);
// NetGroupInfo object with Quality of Service statistics
}
}

[/code]

The NetStream object is now equipped with new multicast properties. For instance, multicastWindowDuration specifies the duration in seconds of the peer-to-peer multicast reassembly window. A short value reduces latency but also quality.

NetGroup is best used for an application with a many-to-many spectrum. NetStream is for a one-to-many or few-to-many spectrum.

Communication can be done in different ways:

  • Posting is for lots of peers sending small messages.
  • Multicasting is for any size group, but with a small number of the peers being senders, and for continuous/live data which is large over time.
  • Direct routing is for sending messages to specific peers in the group using methods such as sendToAllNeighbors, sendToNeighbor, and sendToNearest.
  • Object replication is for more reliable data delivery whereby information is sent in packets between clients and reassembled.

Matthew Kaufman explains this technology in depth in his MAX 2009 presentation, at http://tv.adobe.com/watch/max-2009-develop/p2p-on-the-flash-platform-with-rtmfp.

Simple Text Chat

This example is very similar to the one we created for P2P over a local network, except for a few minor, yet important, changes.

The connection is made to a remote server using the NetConnection object and RTMFP. If you have the Adobe URL and developer key, use them as demonstrated in the following code:

[code]

const SERVER:String = “rtmfp://” + YOUR_SERVER_ADDRESS;
const KEY:STRING = YOUR_DEVELOPER_KEY;
var connection:NetConnection = new NetConnection();
connection.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
connection.connect(SERVER, KEY);

[/code]

Connecting to a traditional streaming server would still use the URI construct as “rtmfp://server/application/instance” and additional optional parameters to connect, such as a login and password.

The GroupSpecifier now needs serverChannelEnabled set to true to use the Cirrus server, and helps in peer discovery. PostingEnabled is still on to send messages. The IPMulticastAddress property is optional but can help optimize the group topology if the group is large:

[code]

function onStatus(event:NetStatusEvent):void {
if (event.info.code == “NetConnection.Connect.Success”) {
var groupSpec:GroupSpecifier = new GroupSpecifier(“chatGroup”);
groupSpec.postingEnabled = true;
groupSpec.serverChannelEnabled = true;
group = new NetGroup(connection,
groupSpec.groupspecWithAuthorizations());
group.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
}
}

[/code]

The exchange of messages is very similar to the local example. Note that a post method is well suited for many peers sending small messages, as in a chat application that is not time-critical:

[code]

function sendMessage():void {
var object:Object = new Object();
object.user = “Véronique”;
object.message = “This is a chat message”;
object.time = new Date().time;
group.post(object);
}
function onStatus(event:NetStatusEvent):void {
if (event.info.code == “NetGroup.Posting.Notify”) {
trace(event.info.message);
}
}

[/code]

Multicast Streaming

This example demonstrates a video chat between one publisher and many receivers who help redistribute the stream to other receivers.

The application connects in the same way as in the previous example, but instead of a NetGroup, we create a NetStream to transfer video, audio, and messages.

Publisher

This is the code for the publisher sending the stream.

To access the camera, add the permission in your descriptor file:

[code]<uses-permission android:name=”android.permission.CAMERA”/>[/code]

Set the GroupSpecifier and the NetStream. The GroupSpecifier needs to have multicas tEnabled set to true to support streaming:

[code]

import flash.net.NetStream;
var outStream:NetStream;
function onStatus(event:NetStatusEvent):void {
if (event.info.code == “NetConnection.Connect.Success”) {
var groupSpec:GroupSpecifier = new GroupSpecifier(“videoGroup”);
groupSpec.serverChannelEnabled = true;
groupSpec.multicastEnabled = true;
outStream = new NetStream(connection,
groupSpec.groupspecWithAuthorizations());
outStream.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
}
}

[/code]

Once the NetStream is connected, add a reference to the camera and the microphone and attach them to the stream. A Video object displays the camera feed. Finally, call the publish method and pass the name of your choice for the video session:

[code]

function onStatus(event:NetStatusEvent):void {
if (event.info.code == “NetStream.Connect.Success”) {
var camera:Camera = Camera.getCamera();
var video:Video = new Video();
video.attachCamera(camera);
addChild(video);
outStream.attachAudio(Microphone.getMicrophone());
outStream.attachCamera(camera);
outStream.publish(“remote video”);
}
}

[/code]

Recipients

The code for the peers receiving the video is similar, except for the few changes described next.

The incoming NetStream, used for the peers receiving the stream, must be the same GroupSpecifier as the publisher’s stream. The same stream cannot be used for sending and receiving:

[code]

var inStream:NetStream = new NetStream(connection,
groupSpec.groupspecWithAuthorizations());
inStream.addEventListener(NetStatusEvent.NET_STATUS, onStatus);

[/code]

The recipient needs a Video object but no reference to the microphone and the camera. The play method is used to stream the video in:

[code]

var video:Video = new Video();
addChild(video);
inStream.play(“remote video”);

[/code]

Sending and receiving data

Along with streams, NetStream can be used to send data. It is only an option for the publisher:

[code]

var object:Object = new Object();
object.type = “chat”;
object.message = “hello”;
outStream.send(“onReceiveData”, object);

[/code]

To receive data, the incoming stream must assign a NetStream.client for callbacks. Note that the onReceiveData function matches the first parameter passed in the publisher send call:

[code]

inStream.client = this;
function onReceiveData(object:Object):void {
trace(object.type, object.message); // chat, hello
}

[/code]

Closing a stream

Do not forget to remove the stream and its listener after it closes:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetStream.Connect.Closed” :
case “NetStream.Connect.Failed” :
onDisconnect();
break;
}
}
function onDisconnect():void {
stream.removeEventListener(NetStatusEvent.NET_STATUS, onStatus);
stream = null;
}
group.peerToPeerDisabled = false;
group.objectReplicationEnabled = true;

[/code]

End-to-End Stream

Another approach is for the publisher to send a separate stream to each receiver. This limits the number of users, but is the most efficient transmission with the lowest latency. No GroupSpecifier is needed for this mode of communication. In fact, this is no longer a group, but a one-to-one transfer or unidirectional NetStream channel.

Sending a peer-assisted stream

Set the connection parameter to NetStream.DIRECT_CONNECTIONS; the stream now has its bufferTime property set to 0 for maximum speed:

[code]

var outStream:NetStream =
new NetStream(connection, NetStream.DIRECT_CONNECTIONS);
outStream.bufferTime = 0;
outStream.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
var video:Video = new Video();
var camera:Camera = Camera.getCamera();
video.attachCamera(camera);
addChild(video);
outStream.attachAudio(Microphone.getMicrophone());
outStream.attachCamera(camera);
outStream.publish(“privateVideo”);

[/code]

When first connected, every peer is assigned a unique 256-bit peerID. Cirrus uses it to match it to your IP address and port number when other peers want to communicate with you, as in this example. nearID represents you:

[code]

var myPeerID:String
function onStatus(event:NetStatusEvent):void {
if (event.info.code == “NetConnection.Connect.Success) {
myPeerID = connection.nearID;
trace(myPeerID);
// 02024ab55a7284ad9d9d4586dd2dc8d2fa1b207e53118d93a34abc946836fa4
}
}

[/code]

The receivers need the peerID of the publisher to subscribe. The publisher needs a way to communicate the ID to others. In a professional application, you would use a web service or a remote sharedObject, but for web development, or if you know the people you want to communicate with, you can send your peerID in the body of an email:

[code]

var myPeerID:String
function onStatus(event:NetStatusEvent):void {
if (event.info.code == “NetConnection.Connect.Success”) {
myPeerID = connection.nearID;
navigateToURL(new URLRequest(‘mailto:FRIEND_EMAIL?subject=id&body=’+
myPeerID));
}
}

[/code]

The streams are not sent until another endpoint subscribes to the publisher’s stream.

Receiving a stream

In this example, the subscribers get the ID via email and copy its content into the system clipboard. Then they press the giveMe button:

[code]

var giveMe:Sprite = new Sprite();
giveMe.y = 100;
var g:Graphics = giveMe.graphics;
g.beginFill(0x0000FF);
g.drawRect(20, 20, 100, 75);
g.endFill();
giveMe.addEventListener(MouseEvent.CLICK, startStream);

[/code]

The startStream method gets the content of the clipboard and uses it to create the stream. The ID needs to be passed as the second parameter in the stream constructor:

[code]

function startStream():void {
var id:String =
Clipboard.generalClipboard.getData(ClipboardFormats.TEXT_FORMAT) as String;
var inStream:NetStream = new NetStream(connection, id);
inStream.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
var video:Video = new Video();
addChild(video);
inStream.play(“privateVideo”);
video.attachNetStream(inStream);
}

[/code]

The publisher has control, if needed, over accepting or rejecting subscribers. When a subscriber attempts to receive the stream, the onPeerConnect method is invoked. Create an object to capture the call. The way to monitor whom to accept (or not) is completely a function of your application:

[code]

var farPeerID:String;
var outClient:Object = new Object();
outClient.onPeerConnect = onConnect;
outStream.client = outClient;
function onConnect(stream:NetStream):Boolean {
farPeerID = stream.farID;
return true; // accept
OR
return false; // reject
}

[/code]

The publisher stream has a peerStreams property that holds all the subscribers for the publishing stream. Use NetStream.send() to send messages to all the recipients or Net Stream.peerStreams[0].send() for an individual user, here the first one in the list.

NetConnection.maxPeerConnections returns the limit of peer streams, typically set to a maximum of eight.

Directed Routing

Directed routing is for sending data to a specific peer in a group. Peers can send each other messages if they know their counterpart PeerID. This feature only works in a group via NetGroup. It is not available via NetStream.

Sending a message

Individual messages can be sent from one neighbor to another using the NetGroup.send ToNeighbor method:

[code]

var groupSpec:GroupSpecifier = new GroupSpecifier(“videoGroup”);
groupSpec.postingEnabled = true;
groupSpec.serverChannelEnabled = true;
groupSpec.routingEnabled = true;
var netGroup = new NetGroup(connection,
groupSpec.groupspecWithAuthorizations());
netGroup.addEventListener(NetStatusEvent.NET_STATUS, onStatus);

[/code]

The message is an Object. It needs a destination which is the peer receiving the message. Here, PeerID is converted to a group address. It also needs the message itself. Here, we added the time to make each message unique and a type to filter the conversation:

[code]

var message:Object = new Object();
var now:Date = new Date();
message.time = now.getHours() + “” + now.getMinutes()+ “” + now.getSeconds();
message.destination = group.convertPeerIDToGroupAddress(peerID);
message.value = “south”;
message.type = “direction”;
group.sendToNearest(message, message.destination);

[/code]

Receiving a message

The recipient must be in the same group. The message is received at an event with an info.code value of NetGroup.SendTo.Notify. The recipient checks to see if the message is for her by checking if event.info.fromLocal is true, and if it is not, sends it to the next neighbor until its destination is reached:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetGroup.SendTo.Notify” :
trace(event.info.fromLocal);
// if true, recipient is the intended destination
var message:Object = event.info.message;
(if message.type == “direction”) {
trace(message.value); // south
}
break;
}
}

[/code]

Relay

A simple message relay service was introduced in January 2011. It is not intended for ongoing communication, but rather for a few introductory messages, and is a feature for the Cirrus service only. It requires that the sender knows the PeerID of the recipient.

The sender requests a relay:

[code]

connection.call(“relay”, null, “RECIPIENT_ID”, “hello”);

[/code]

The recipient receives and responds to the relay:

[code]

connection.client = this;
function onRelay(senderID:String, message):void {
trace(senderID); // ID of the sender
trace(message); // “hello”
}

[/code]

Treasure Hunt

This treasure hunt game illustrates various aspects of this technology.

Referring to Figure 15-3, imagine the first user on the left walking outdoors looking for a treasure without knowing where it is. She streams a live video as she walks to indicate her progress. The second user from the left knows where the treasure is but is off-site. She guides the first user by pressing keys, representing the cardinal points, to send directions. Other peers (in the two screens toward the right) can watch the live stream and chat among themselves.

Figure 15-3. The Treasure Hunt activity; the panels shown here are (left to right) for the hunter walking, for the guide, and for users viewing the video and chatting over text
Figure 15-3. The Treasure Hunt activity; the panels shown here are (left to right) for the hunter walking, for the guide, and for users viewing the video and chatting over text

Review the sample code provided in this chapter to build such an application. We covered a one-to-many streaming example. We discussed chat in an earlier example. And we just went over sending direct messages.

As a final exercise, you can put all the pieces together to build a treasure hunt application. Good luck, and please post your results.

Other Multiuser Services

If you want to expand your application beyond what this service offers, several other options are available to set up communication between parties remotely, such the Adobe Media Server, Electrotank’s ElectroServer, and gotoAndPlay()’s SmartFox. All of them require server setup and some financing.

ElectroServer was developed for multiplayer games and tools to build a multiplayer lobby system. One installation scales up to tens of thousands of connected game players with message rates of more than 100,000 messages per second. You can try a free 25- user license (see http://www.electrotank.com/). Server-side code requires Java or ActionScript 1. It supports AIR and Android.

SmartFox is a platform for developing massive multiuser games and was designed with simplicity in mind. It is fast and reliable and can handle tens of thousands of concurrent clients with low CPU and memory usage. It is well documented. You can try a full version with a free 100-user license (see http://www.smartfoxserver.com/). Server-side
code requires Java. It supports AIR and Android.

 

 

Silly Eye (Intro to Animation)

Silly Eye is a crowd-pleaser, especially when the crowd contains children. This app displays a large cartoonish eye that animates in a funny, frantic way that can’t be conveyed on paper. Simply hold it up to your right eye and pretend it’s your own silly eye! Figure 12.1 demonstrates how to use this app.

Introducing Animation

When most people think about animation, they think of a cartoon-like mechanism, where movement is simulated by displaying images in rapid succession. In Silverlight, animation has a more specific definition: varying the value of a property over time. This could be related to motion, such as making an element grow by increasing its width, or it could be something completely different like varying an element’s opacity.

There are many ways to change a property’s value over time. The classic approach is to use a timer, much like the DispatcherTimer used in previous chapters, and use a method that is periodically called back based on the frequency of the timer (the Tick event handler). Inside this method, you can manually update the target property (doing a little math to determine the current value based on the elapsed time) until it reaches the final value. At that point, you can stop the timer and/or remove the event handler.

FIGURE 12.1 Give yourself a silly eye by holding the phone up to your right eye.
FIGURE 12.1 Give yourself a silly eye by holding the phone up to your right eye.

However, Silverlight provides an animation mechanism that is much easier to use, more powerful, and performs better than a timer-based approach. It is centered around an object known as a storyboard. Storyboards contain one or more special animation objects and apply them to specific properties on specific elements.

Silly Eye uses three storyboards to perform its animations. To understand what storyboards are and how they work, we’ll examine each one:

  • The pupil storyboard
  • The iris storyboard
  • The eyelid storyboard

The Pupil Storyboard

Here is the storyboard that Silly Eye applies to the pupil to make it appear to repeatedly grow and shrink:

[code]

<Storyboard x:Name=”PupilStoryboard”
Storyboard.TargetName=”Pupil”
Storyboard.TargetProperty=”StrokeThickness”>
<DoubleAnimation From=”100” To=”70” Duration=”0:0:.5”
AutoReverse=”True” RepeatBehavior=”Forever”>
<DoubleAnimation.EasingFunction>
<ElasticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>

[/code]

Notes:

  • The Storyboard.TargetName attachable property indicates that this animation is being applied to an element on the page named Pupil. Pupil is an ellipse defined as follows:

    [code]<Ellipse x:Name=”Pupil” Width=”238” Height=”237” StrokeThickness=”100”
    Fill=”Black”/>[/code]

    (The brush for its stroke is set in code-behind.)

  • The Storyboard.TargetProperty attachable property indicates that Pupil’s StrokeThickness property is being animated.
  • The DoubleAnimation inside the storyboard indicates that StrokeThickness will be animated from 100 to 70 over a duration of half a second. The “Double” in DoubleAnimation represents the type of the target property being animated. (StrokeThickness is a double.)
  • Because AutoReverse is set to true, StrokeThickness will automatically animate back to 100 after reaching the end value of 70. Because RepeatBehavior is set to Forever, this cycle from 100 to 70 to 100 will repeat indefinitely once the animation has started.
  • The EasingFunction property (set to an instance of an ElasticEase) controls how the value of StrokeThickness is interpolated over time. This is discussed in the upcoming “Interpolation” section.

To begin the animation, the storyboard’s Begin method is called as follows:

[code]this.PupilStoryboard.Begin();[/code]

The result of this animation is shown in Figure 12.2 in the context of the entire app. The Pupil ellipse has been given a light blue stroke via code-behind.

FIGURE 12.2 PupilStoryboard makes Pupil’s stroke thickness (seen in blue) shrink from 100 down to 70, causing its black fill to appear to grow.
FIGURE 12.2 PupilStoryboard makes Pupil’s stroke thickness (seen in blue) shrink from 100 down to 70, causing its black fill to appear to grow.

There is a way to trigger a storyboard entirely in XAML so there’s no need for a call to its Begin method in code-behind.You can add an event trigger to an element’s Triggers property.This can look as follows:

[code]

<Grid>
<Grid.Triggers>
<EventTrigger RoutedEvent=”Grid.Loaded”>
<BeginStoryboard>
<Storyboard Storyboard.TargetName=”Pupil”
Storyboard.TargetProperty=”StrokeThickness”>
<DoubleAnimation To=”70” Duration=”0:0:.5” AutoReverse=”True”
RepeatBehavior=”Forever”>
<DoubleAnimation.EasingFunction>
<ElasticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>

</Grid>

[/code]

Thanks to the special BeginStoryboard element, this internally calls Begin on the storyboard in response to the grid’s Loaded event.The Loaded event is the only event supported by event triggers in Silverlight.

Types of Animations

Silverlight provides animation classes to animate four different data types: double, Color, Point, and object. Only double properties are animated in Silly Eye.

If you want to vary the value of an element’s double property over time (such as Width, Height, Opacity, Canvas.Left, and so on), you can use an instance of DoubleAnimation. If you instead want to vary the value of an element’s Point property over time (such as a linear gradient brush’s StartPoint or EndPoint property), you could use an instance of PointAnimation. DoubleAnimation is by far the most commonly used animation class due to large the number of properties of type double that make sense to animate.

Interpolation

It’s important to note that, by default, DoubleAnimation takes care of smoothly changing the double value over time via linear interpolation. In other words, for a one-second animation from 50 to 100, the value is 55 when 0.1 seconds have elapsed (10% progress in both the value and time elapsed), 75 when 0.5 seconds have elapsed (50% progress in both the value and time elapsed), and so on. This is why StrokeThickness is shown with a value of 85 halfway through the animation in Figure 12.2.

Most animations used in Windows Phone apps are not linear, however. Instead, they tend to “spring” from one value to another with a bit of acceleration or deceleration. This makes the animations more lifelike and interesting. You can produce such nonlinear animations by applying an easing function.

An easing function is responsible for doing custom interpolation from the starting value to the ending value. The pupil storyboard uses an easing function called ElasticEase to make its behavior much more “silly” than linear. Figure 12.3 graphs how the interpolation from 100 to 70 differs between the default linear behavior and the elastic ease behavior. In this case, the midpoint value of 85 actually isn’t reached half-way through the animation, but rather right toward the end.

FIGURE 12.3 The ElasticEase easing function drastically alters how a double value changes from 100 to 70.
FIGURE 12.3 The ElasticEase easing function drastically alters how a double value changes from 100 to 70.

Silverlight provides eleven different easing functions, each with three different modes, and several with properties to further customize their behavior. For example, ElasticEase has Oscillations and Springiness properties, both set to 3 by default. Combined with the fact that you can write your own easing functions to plug into animations, the possibilities for custom behaviors are endless. The easing functions used in this app give a wildly different experience than the default linear behavior.

The Iris Storyboard

Silly Eye applies the following storyboard to a canvas called Iris to make the eyeball appear to move left and right:

[code]

<Storyboard x:Name=”IrisStoryboard”
Storyboard.TargetName=”Iris”
Storyboard.TargetProperty=”(Canvas.Left)”>
<DoubleAnimation To=”543” Duration=”0:0:2”
AutoReverse=”True” RepeatBehavior=”Forever”>
<DoubleAnimation.EasingFunction>
<BounceEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>

[/code]

Notes:

  • The syntax for TargetProperty is sometimes more complex than just a property name. When set to an attachable property such as Canvas.Left, it must be surrounded in parentheses.
  • The animation has a different easing function applied that gives the movement a noticeable bounciness. See Appendix D for a graph of BounceEase behavior.
  • The animation is missing a From value! This is okay and often recommended. When no From is specified, the animation starts with the target property’s current value, whatever it may be. Similarly, an animation can specify a From but no To! This animates the property from the value specified in From to whatever its current (pre-animation) value is.

You must specify both From and To if the current property can’t be interpolated!

If you try to animate the width or height of an auto-sized element with a From-less or To-less animation, nothing happens. Elements are auto-sized when their width and height are set to Double.NaN (not-a-number), and the DoubleAnimation can’t interpolate between two values when one of them isn’t even a number. Furthermore, applying the animation to ActualWidth or ActualHeight (which is set to the true width/height rather than NaN) isn’t an option because these properties are read-only and they are not dependency properties. Instead, you must explicitly set the width/height of the target element for such an animation to work.

As with the pupil storyboard, this storyboard’s Begin method is called to make it start:

[code]this.IrisStoryboard.Begin();[/code]

The result of this animation is shown in Figure 12.4. The Iris canvas contains the Pupil ellipse (whose stroke is actually the iris) along with two other ellipses that give the iris its “shine.” Because the position of the parent canvas is animated, all these contents move together.

FIGURE 12.4 IrisStoryboard moves the Iris canvas horizontally from 287 (its initial Canvas.Left value) to 543.
FIGURE 12.4 IrisStoryboard moves the Iris canvas horizontally from 287 (its initial Canvas.Left value) to 543.

Animations also have a By field that can be set instead of the To field.The following animation means “animate the value by adding 256 to its current value”:

[code]<DoubleAnimation By=”256” Duration=”0:0:2”/>[/code]

Negative values are supported for shrinking the current value.

The Eyelid Animation

The final storyboard used by Silly Eye animates two properties on a skin-colored Eyelid ellipse to simulate blinking:

[code]

<Storyboard x:Name=”EyelidStoryboard”
Storyboard.TargetName=”Eyelid”
RepeatBehavior=”Forever” Duration=”0:0:3”>
<DoubleAnimation Storyboard.TargetProperty=”Height”
To=”380” Duration=”0:0:.1” AutoReverse=”True”/>
<DoubleAnimation Storyboard.TargetProperty=”(Canvas.Top)”
To=”50” Duration=”0:0:.1” AutoReverse=”True”/>
</Storyboard>

[/code]

The Eyelid ellipse is defined as follows:

[code]

<Ellipse x:Name=”Eyelid” Canvas.Left=”73” Canvas.Top=”-145”
Width=”897” Height=”770” StrokeThickness=”200”/>

[/code]

As with the Pupil ellipse, the skin-colored brush for its stroke is set in code-behind.

Notes:

  • There’s a reason that Storyboard.TargetName and Storyboard.TargetProperty are attachable properties: They can be set on individual animations to override any storyboard-wide settings. This storyboard targets both the Height and Canvas.Top properties on the target Eyelid ellipse. Therefore, a single target name is marked on the storyboard but separate target properties are marked for each animation.
  • Canvas.Top is animated in sync with Height so the ellipse stays centered as it shrinks vertically.
  • The two animations both use the default linear interpolation behavior. Their motion is so quick that it’s not necessary to try anything more lifelike.
  • A storyboard is more than just a simple container that associates animations with target objects and their properties. This storyboard has its own duration and repeat behavior! The two animations only last .2 seconds (.1 seconds to animate from the current value to 380 and 50, and another .1 seconds to animate back to the original values due to the auto-reverse setting). However, because the storyboard is given a duration of 3 seconds, and because it has the auto-reverse setting rather than its children, the animation remains stationery until the 3 seconds are up. At that point, the .2-second long movement occurs again, and the animation will then be still for another 2.8 seconds. Therefore, this storyboard makes the eyelid blink very quickly, but only once every 3 seconds.

The result of this animation is shown in Figure 12.5 (after calling Begin in C#). Because the Eyelid ellipse is the same color as the background (and intentionally covered on its left side by the black area), you can’t see the ellipse itself. Instead, you see the empty space inside it shrinking to nothing once the height of the ellipse (380) is less than two times its stroke thickness (400).

FIGURE 12.5 EyelidStoryboard compresses the height of the Eyelid ellipse and moves it downward to keep it centered.
FIGURE 12.5 EyelidStoryboard compresses the height of the Eyelid ellipse and moves it downward to keep it centered.

Storyboard and Animation Properties

You’ve already seen the Duration, AutoReverse, and RepeatBehavior properties, which can apply to individual animations or an entire storyboard. In total, there are six properties that can be applied to both storyboards and animations:

  • Duration—The length of the animation or storyboard, set to 1 second by default.
  • BeginTime—A timespan that delays the start of the animation or storyboard by the specified amount of time, set to 0 by default. A storyboard can use custom BeginTime values on its child animations to make them occur in sequence rather than simultaneously.
  • SpeedRatio—A multiplier applied to duration, set to 1 by default. You can set it to any double value greater than 0. A value less than 1 slows down the animation, and a value greater than 1 speeds it up. SpeedRatio does not impact BeginTime.
  • AutoReverse—Can be set to true to make an animation or storyboard “play backward” once it completes. The reversal takes the same amount of time as the forward progress, so SpeedRatio affects the reversal as well. Note that any delay specified via BeginTime does not delay the reversal; it always happens immediately after the normal part of the animation completes.
  • RepeatBehavior—Can be set to a timespan, or a string like “2x” or “3x”, or “Forever”. Therefore, you can use RepeatBehavior to make animations repeat themselves (or cut themselves short) based on a time cutoff, to make animations repeat themselves a certain number of times (even a fractional number of times like “2.5x”), or to make animations repeat themselves forever (as done in this chapter). If AutoReverse is true, the reversal is repeated as well.
  • FillBehavior—Can be set to Stop rather than its default value of HoldEnd, to make the animated properties jump back to their pre-animation values once the relevant animations are complete.

The Main Page

Silly Eye’s main page, whose XAML is in Listing 12.1, contains some vector graphics, an application bar, and the three storyboards just discussed. It also contains an “intro pane” that tells the user to tap the screen to begin, as shown in Figure 12.6. This is done so we can initially show the application bar but then hide it while the app is in use, as the buttons on the screen interfere with the effect. The intro pane informs the user that they can bring the application bar back at any time by tapping the screen.

FIGURE 12.6 The application bar is only visible when the “intro pane” is visible.
FIGURE 12.6 The application bar is only visible when the “intro pane” is visible.

LISTING 12.1 MainPage.xaml—The Main User Interface for Silly Eye

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Landscape” Orientation=”Landscape”>
<!– The application bar, with 2 buttons and 1 menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBarIconButton Text=”settings” Click=”SettingsButton_Click”
IconUri=”/Shared/Images/appbar.settings.png”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<!– Three storyboard resources –>
<phone:PhoneApplicationPage.Resources>
<!– Animate the stroke thickness surrounding the pupil –>
<Storyboard x:Name=”PupilStoryboard” Storyboard.TargetName=”Pupil”
Storyboard.TargetProperty=”StrokeThickness”>
<DoubleAnimation To=”70” Duration=”0:0:.5” AutoReverse=”True”
RepeatBehavior=”Forever”>
<DoubleAnimation.EasingFunction>
<ElasticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Animate the iris so it moves left and right –>
<Storyboard x:Name=”IrisStoryboard” Storyboard.TargetName=”Iris”
Storyboard.TargetProperty=”(Canvas.Left)”>
<DoubleAnimation To=”543” Duration=”0:0:2” AutoReverse=”True”
RepeatBehavior=”Forever”>
<DoubleAnimation.EasingFunction>
<BounceEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Animate the eyelid so it blinks –>
<Storyboard x:Name=”EyelidStoryboard” Storyboard.TargetName=”Eyelid”
RepeatBehavior=”Forever” Duration=”0:0:3”>
<DoubleAnimation Storyboard.TargetProperty=”Height”
To=”380” Duration=”0:0:.1” AutoReverse=”True”/>
<DoubleAnimation Storyboard.TargetProperty=”(Canvas.Top)”
To=”50” Duration=”0:0:.1” AutoReverse=”True”/>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– A 1×1 grid with IntroPanel on top of EyeCanvas –>
<Grid>
<Canvas x:Name=”EyeCanvas”
MouseLeftButtonDown=”EyeCanvas_MouseLeftButtonDown”>
<!– The eyeball –>
<Ellipse Canvas.Left=”270” Canvas.Top=”55” Width=”503” Height=”370”
Fill=”White”/>
<!– Four “bloodshot” curvy/angled paths –>
<Path Data=”M782,252 C648,224 666,270 666,270 L622,212 L604,230” Width=”190”
Height=”70” Canvas.Left=”588” Canvas.Top=”206” Stroke=”Red”
StrokeThickness=”8” Stretch=”Fill” StrokeEndLineCap=”Triangle”/>
<Path Data=”M658,122 C604,176 582,136 582,136 L586,190 L526,204” Width=”144”
Height=”94” Canvas.Left=”541” Canvas.Top=”91” Stretch=”Fill”
Stroke=”Red” StrokeThickness=”8” StrokeEndLineCap=”Triangle”/>
<Path Data=”M348,334 C414,296 386,296 428,314 C470,332 464,302 476,292
C488,282 498,314 500,306” Width=”164” Height=”56” Canvas.Left=”316”
Canvas.Top=”303” Stretch=”Fill” Stroke=”Red” StrokeThickness=”8”/>
<Path Data=”M324,164 C388,210 434,130 444,178 C454,226 464,226 470,224”
Width=”154” Height=”70” Canvas.Left=”322” Canvas.Top=”115”
Stretch=”Fill” Stroke=”Red” StrokeThickness=”8”/>
<!– The complete iris canvas –>
<Canvas x:Name=”Iris” Canvas.Left=”287” Canvas.Top=”124”>
<!– The pupil, whose stroke is the iris –>
<Ellipse x:Name=”Pupil” Width=”238” Height=”237” StrokeThickness=”100”
Fill=”Black”/>
<!– Two “shine” circles –>
<Ellipse Width=”73” Height=”72” Canvas.Left=”134” Canvas.Top=”28”
Fill=”#8DFFFFFF”/>
<Ellipse Width=”110” Height=”107” Canvas.Left=”20” Canvas.Top=”86”
Fill=”#5FFFFFFF”/>
</Canvas>
<!– The skin-colored eyelid –>
<Ellipse x:Name=”Eyelid” StrokeThickness=”200” Width=”897” Height=”770”
Canvas.Left=”73” Canvas.Top=”-145”/>
<!– The black area on the left side that defines the edge of the face –>
<Ellipse Stroke=”Black” StrokeThickness=”300” Width=”1270” Height=”2380”
Canvas.Left=”-105” Canvas.Top=”-1140”/>
</Canvas>
<!– Quick instructions shown at the beginning –>
<Grid x:Name=”IntroPanel” Opacity=”.8”
Background=”{StaticResource PhoneBackgroundBrush}”>
<!– Enable tapping anywhere except very close to the application bar –>
<TextBlock x:Name=”IntroTextBlock” Width=”700” Padding=”170”
MouseLeftButtonDown=”IntroTextBlock_MouseLeftButtonDown”
HorizontalAlignment=”Left” VerticalAlignment=”Stretch”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”>
Tap to begin.<LineBreak/>Later, tap to return.
</TextBlock>
</Grid>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Notes:

  • The application bar contains links to a settings page, an instructions page, and an about page. The first two pages are shown in the next two sections.
  • Notice that the three storyboard resources are given names with x:Name rather than keys with x:Key! This is a handy trick that makes using resources from codebehind much more convenient. When you give a resource a name, it is used as the key in the dictionary and a field with that name is generated for access from C#!
  • The explicit From value has been removed from PupilStoryboard’s animation because it’s not necessary. It was included earlier in the chapter simply to help explain how animations work.
  • IntroTextBlock is the element that listens for taps and hides IntroPanel. It is given a width of 700 rather than the entire width of the page because if it gets too close to the application bar, users might accidentally tap it (and hide the application bar) when actually trying to tap the bar—especially its ellipsis.

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

LISTING 12.2 MainPage.xaml.cs—The Code-Behind for Silly Eye’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
// Start all the storyboards, which animate indefinitely
this.IrisStoryboard.Begin();
this.PupilStoryboard.Begin();
this.EyelidStoryboard.Begin();
// Prevent off-screen parts from being seen when animating to other pages
this.Clip = new RectangleGeometry { Rect = new Rect(0, 0,
Constants.SCREEN_WIDTH, Constants.SCREEN_HEIGHT) };
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Remember the intro panel’s visibility for deactivation/activation
this.State[“IntroPanelVisibility”] = this.IntroPanel.Visibility;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the saved settings for the skin and eye colors
SolidColorBrush skinBrush = new SolidColorBrush(Settings.SkinColor.Value);
this.Eyelid.Stroke = skinBrush;
this.EyeCanvas.Background = skinBrush;
this.Pupil.Stroke = new SolidColorBrush(Settings.EyeColor.Value);
// Restore the intro panel’s visibility if we’re being activated
if (this.State.ContainsKey(“IntroPanelVisibility”))
{
this.IntroPanel.Visibility =
(Visibility)this.State[“IntroPanelVisibility”];
this.ApplicationBar.IsVisible =
(this.IntroPanel.Visibility == Visibility.Visible);
}
}
protected override void OnOrientationChanged(OrientationChangedEventArgs e)
{
base.OnOrientationChanged(e);
// Keep the text block aligned to the opposite side as the application bar,
// to preserve the “dead zone” where tapping doesn’t hide the bar
if (e.Orientation == PageOrientation.LandscapeRight)
this.IntroTextBlock.HorizontalAlignment = HorizontalAlignment.Right;
else
this.IntroTextBlock.HorizontalAlignment = HorizontalAlignment.Left;
}
void IntroTextBlock_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// Hide IntroPanel and application bar when the text block is tapped
this.IntroPanel.Visibility = Visibility.Collapsed;
this.ApplicationBar.IsVisible = false;
}
void EyeCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// Show IntroPanel and application bar when the canvas is tapped
this.IntroPanel.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = true;
}
// Application bar handlers
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void SettingsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Silly Eye”, UriKind.Relative));
}
}
}

[/code]

Notes:

  • The three storyboards are initiated from the constructor by name, thanks to the x:Name markings in XAML.
  • The page’s Clip property is set to a screen-size rectangular region. This is done to prevent the off-screen portions of the vector graphics from being rendered during the animated page-flip transition when navigating to another page. This not only prevents strange visual artifacts, but can be good for performance as well. All UI elements have this Clip property that can be set to an arbitrary geometry.
  • Two persisted settings are used for the skin and eye color, and they are respected in OnNavigatedTo. They do not need to be saved in OnNavigatedFrom because the settings page takes care of this. The settings are defined in a separate Settings.cs file as follows:

    [code]
    public static class Settings
    {
    public static readonly Setting<Color> EyeColor = new Setting<Color>(
    “EyeColor”, (Color)Application.Current.Resources[“PhoneAccentColor”]);
    public static readonly Setting<Color> SkinColor = new Setting<Color>(
    “SkinColor”, /* “Tan” */ Color.FromArgb(0xFF, 0xD2, 0xB4, 0x8C));
    }
    [/code]

  • The visibility of IntroPanel (and the application bar) is placed in page state so the page looks the same if deactivated and later activated.
  • The alignment of IntroTextBlock is adjusted in OnOrientationChanged to keep it on the opposite side of the application bar. Recall that the application bar appears on the left side of the screen for the landscape right orientation, and the right side of the screen for the landscape left orientation.

The Settings Page

Listing 12.3 contains the XAML for this app’s settings page, shown in Figure 12.7. It enables the user to choose different colors for the eye and the skin.

FIGURE 12.7 The settings page enables the user to change both of Silly Eye’s color settings.
FIGURE 12.7 The settings page enables the user to change both of Silly Eye’s color settings.

LISTING 12.3 SettingsPage.xaml—The User Interface for the Settings Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.SettingsPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard settings header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”SETTINGS” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”silly eye” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<!– A rectangle (and text block) for each of the two settings –>
<ScrollViewer Grid.Row=”1”>
<StackPanel Margin=”{StaticResource PhoneMargin}”>
<TextBlock Text=”Eye color” Foreground=”{StaticResource PhoneSubtleBrush}”
Margin=”12,7,12,8”/>
<Rectangle x:Name=”EyeColorRectangle” Margin=”12,0,12,18” Height=”90”
Stroke=”{StaticResource PhoneForegroundBrush}”
StrokeThickness=”3” local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”EyeColorRectangle_MouseLeftButtonUp”/>
<TextBlock Text=”Skin color”
Foreground=”{StaticResource PhoneSubtleBrush}”
Margin=”12,12,12,8”/>
<Rectangle x:Name=”SkinColorRectangle” Height=”90”
Margin=”{StaticResource PhoneHorizontalMargin}”
Stroke=”{StaticResource PhoneForegroundBrush}”
StrokeThickness=”3” local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”SkinColorRectangle_MouseLeftButtonUp”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • This page leverages the custom header styles from in App.xaml.
  • The two clickable regions that display the current colors look like buttons, but they are just rectangles. Their MouseLeftButtonUp event handlers take care of invoking the user interface that enables the user to change each color.
  • The main stack panel is placed in a scroll viewer even though the content completely fits on the screen in all orientations. This is a nice extra touch for users, as they are able to swipe the screen and easily convince themselves that there is no more content.

Listing 12.4 contains the code-behind for this settings page.

LISTING 12.4 SettingsPage.xaml.cs—The Code-Behind for the Settings Page

[code]

using System;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
public SettingsPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the saved settings
this.EyeColorRectangle.Fill = new SolidColorBrush(Settings.EyeColor.Value);
this.SkinColorRectangle.Fill = new SolidColorBrush(Settings.SkinColor.Value);
}
void EyeColorRectangle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
// Get a string representation of the colors we need to pass to the color
// picker, without the leading #
string currentColorString = Settings.EyeColor.Value.ToString().Substring(1);
string defaultColorString =
Settings.EyeColor.DefaultValue.ToString().Substring(1);
// The color picker works with the same isolated storage value that the
// Setting works with, but we have to clear its cached value to pick up
// the value chosen in the color picker
Settings.EyeColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=EyeColor”, UriKind.Relative));
}
void SkinColorRectangle_MouseLeftButtonUp(object sender, MouseButtonEventArgs
e)
{
// Get a string representation of the colors, without the leading #
string currentColorString = Settings.SkinColor.Value.ToString().Substring(1);
string defaultColorString =
Settings.SkinColor.DefaultValue.ToString().Substring(1);
// The color picker works with the same isolated storage value that the
// Setting works with, but we have to clear its cached value to pick up
// the value chosen in the color picker
Settings.SkinColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “showOpacity=false”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=SkinColor”, UriKind.Relative));
}
}
}

[/code]

To enable the user to change each color, this page navigates to a color picker page pictured in Figure 12.8. This feature-filled page, shared by many apps, is included with this book’s source code. It provides a palette of standard colors but it also enables the user to finely customize the hue, saturation, and lightness of the color whether through interactive UI or by simply typing in a hex value (or any string recognized by XAML, such as “red”, “tan”, or “lemonchiffon”). It optionally enables adjusting the color’s opacity.

FIGURE 12.8 The color picker page provides a slick way to select a color.
FIGURE 12.8 The color picker page provides a slick way to select a color.

The color picker page accepts four parameters via its query string:

  • showOpacity—true by default, but can be set to false to hide the opacity slider. This also removes transparent from the palette of colors at the top, and it prevents users from typing in nonopaque colors. Therefore, when you set this to false, you can be sure that an opaque color will be chosen.
  • currentColor—The initial color selected when the page appears. It must be passed as a string that would be valid for XAML. If specified as a hex value, the # must be removed to avoid interfering with the URI.
  • defaultColor—The color that the user gets when they press the reset button on the color picker page. It must be specified in the same string format as currentColor.
  • settingName—A named slot in isolated storage where the chosen color can be found on return from the page. This is the same name used when constructing a Setting instance. The code in Listing 12.4’s OnNavigatedTo method automatically picks up the new value chosen when navigating back from the color picker page, but only because of the ForceRefresh call made before navigating to the color picker.

The Instructions Page

Listing 12.5 contains the XAML for the simple instructions page shown in Figure 12.9. Later chapters won’t bother showing the XAML for their instructions pages unless there’s something noteworthy inside.

FIGURE 12.9 The instructions page used by Silly Eye.
FIGURE 12.9 The instructions page used by Silly Eye.

LISTING 12.5 InstructionsPage.xaml—The User Interface for the Instructions Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.InstructionsPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”SILLY EYE” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”instructions”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<TextBlock Margin=”24 12” TextWrapping=”Wrap”>
Hold up to your right eye, and watch the hilarity ensue!
<LineBreak/><LineBreak/>
Tapping the screen shows/hides the application bar on the side.
<LineBreak/><LineBreak/>
You can customize the eye color and/or skin color on the settings page.
</TextBlock>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • As with the settings page, the main content is placed in a scroll viewer simply to give the user feedback that there is no more content.
  • As with the intro pane on the main page, a single text block makes use of LineBreak elements to format its text.
  • The code-behind file, InstructionsPage.xaml.cs, has nothing more than the call to InitializeComponent in its constructor.

The Finished Product

Silly Eye

P2P Over a Local Network

If your local network supports broadcasting, you can create peer-to-peer direct routing. All the clients need to be on the same subnet, but you do not need to manage them. Verify that your devices have WiFi enabled and are using the same network.

The code to create a peer-to-peer application with RTMFP is quite simple but introduces new concepts. Let’s go over all the steps one at a time.

The connection is established using the flash.net.NetConnection class. Set a listener to receive a NetStatusEvent event. Create the connection by calling the connect function and passing rtmfp as an argument:

[code]

import flash.net.NetConnection;
import flash.events.NetStatusEvent;
var connection:NetConnection = new NetConnection();
connection.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
connection.connect(“rtmfp:”);

[/code]

Wait for the connection to be established. Then several objects need to be created:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetConnection.Connect.Success” :
trace(“I am connected”);
// object creation can now happen
break;
}
}

[/code]

NetGroup is the group of peers. Its capabilities are defined in the GroupSpecifier. The IPMulticastAddress property stores the IPv4 multicast address. It needs to be in the range 224.0.0.0 through 239.255.255.25. The UDP port should be higher than 1024. A group name is passed in its constructor. Try to make it unique. The IPMulticast MemberUpdatesEnabled property must be set to true for clients to receive updates from other clients on a LAN. The postingEnabled property allows clients to send messages to the group:

[code]

import flash.net.GroupSpecifier;
var groupName:String = “com.veronique.simple/”;
var IPMulticastAddress:String = “230.0.0.1:3000″;
var groupSpec:GroupSpecifier = new GroupSpecifier(groupName);
groupSpec.addIPMulticastAddress(IPMulticastAddress);
groupSpec.ipMulticastMemberUpdatesEnabled = true;
groupSpec.postingEnabled = true;

[/code]

Now create the NetGroup. Pass the connection and the GroupSpecifier in its construction. The latter is passed with an authorization property to define the communication allowed: groupspecWithAuthorizations to post and multicast, or groupspecWithout Authorizations to only receive messages. Note that this setting is only relevant if a posting password is set (as defined by your application):

[code]

import flash.net.NetGroup;
var netGroup = new NetGroup
(connection, groupSpec.groupspecWithAuthorizations());
netGroup.addEventListener(NetStatusEvent.NET_STATUS, onStatus);

[/code]

The group is composed of neighbors, you as well as others. Using the same Net StatusEvent event, check for its info.code. Wait to receive the NetGroup.Connect.Suc cess event before using the functionality of NetGroup to avoid getting an error.

When a user joins or leaves the group, the code is as follows:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case ” NetGroup.Connect.Success” :
trace(“I joined the group”);
break;
case “NetGroup.Connect.Rejected” :
case “NetGroup.Connect.Failed” :
trace(“I am not a member”);
break;
}
}

[/code]

Others in the group receive the following events. Note that if the group is large, only a subset of members is informed that a new peer has joined or left the group:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetGroup.Neighbor.Connect” :
trace(“neighbor has arrived”, neighborCount);
break;
case “NetGroup.Neighbor.Disconnect” :
trace(“neighbor has left”);
break;
}
}

[/code]

To send a message, use the NetGroup.post method. It takes an Object as an argument. Messages are serialized in AMF (binary format for serialized ActionScript objects), so a variation of data types can be used, such as Object, Number, Integer, and String types:

[code]

var message:Object = new Object();
message.type = “testing”;
message.body = {name:”Véronique”, greeting:”Bonjour”};
group.post(message);

[/code]

To receive messages, check for an info.code equal to a NetGroup.Posting.Notify event. The message is received as event.info.message. The message is not distributed to the sender:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetGroup.Posting.Notify” :
trace(event.info.message); // [Object]
trace(event.info.message.body.greeting); // Bonjour
break;
}
}

[/code]

Identical messages are not re-sent. To make each message unique, store the current time as a property of the object:

[code]

var now:Date = new Date();
message.time = now.getHours() + “_” + now.getMinutes() +
“_” + now.getSeconds();
group.post(message);

[/code]

If the message only goes in one direction and there will be no overlap between clients, you could use a counter that gets incremented with every new message:

[code]message.count = count++;[/code]

When disconnecting, it is important to remove all objects and their listeners:

[code]

function onStatus(event:NetStatusEvent):void {
switch(event.info.code) {
case “NetConnection.Connect.Rejected” :
case “Connect.AppShutdown” :
trace(“I am not connected”);
onDisconnect();
break;
}
}
function onDisconnect():void {
group = null;
netGroup.removeEventListener(NetStatusEvent.NET_STATUS, onStatus);
netGroup = null;
connection.removeEventListener(NetStatusEvent.NET_STATUS, onStatus);
connection = null;
}

[/code]

Color Exchange

Let’s create a simple example. The hueMe application starts with a shape of a random color. Each client can send a color value to the other client’s application. On the receiving end, the shape changes to the new color (see Figure 15-1).

Figure 15-1. The hueMe application
Figure 15-1. The hueMe application

Draw the initial colored sprite:

[code]

var sprite:Sprite = new Sprite();
var g:Graphics = sprite.graphics;
g.beginFill(Math.round(Math.random()*0xFFFFFF));
g.drawRect(20, 20, 200, 150);
g.endFill();

[/code]

Create the connection for the P2P communication:

[code]

var connection:NetConnection = new NetConnection();
connection.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
connection.connect(“rtmfp:”);

[/code]

Once the connection is established, create the group and check that the user has successfully connected to it:

[code]

function onStatus(event:NetStatusEvent):void {
if (event.info.code == “NetConnection.Connect.Success”) {
var groupSpec:GroupSpecifier = new GroupSpecifier(“colorGroup”);
groupSpec.addIPMulticastAddress(“225.0.0.1:4000”);
groupSepc.postingEnabled = true;
groupSepc.ipMulticastMemberUpdatesEnabled = true;
group = new NetGroup(connection,
groupSpec.groupspecWithAuthorizations());
group.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
} else if (event.info.code == “NetGroup.Connect.Success”) {
trace(“I am part of the group “);
}
}

[/code]

Send a random color value to the group when clicking the sprite:

[code]

g.addEventListener(MouseEvent.CLICK, hueYou);
function hueYou(event:MouseEvent):void {
var randomHue:int = Math.round(Math.random()*0xFFFFFF);
var object:Object = {type:”color”, hue:randomHue};
group.post(object);
}

[/code]

Finally, add the functionality to receive the value from other members of the group and color the sprite:

[code]

import flash.geom.ColorTransform;
function onStatus(event:NetStatusEvent):void {

if (event.info.code == “NetGroup.Posting.Notify”) {
if (event.info.message.type == “color”) {
applyColor(Number(event.info.message.hue));
}
}
}
function applyColor(hue:int):void {
var colorTransform:ColorTransform = new ColorTransform();
colorTransform.color = hue;
sprite.transform.colorTransform = colorTransform;
}

[/code]

Companion AIR Application

To make your application unidirectional, as in a remote control-style application, have one client sending messages and the other receiving messages. Only the networked clients registered for the NetGroup.Posting.Notify event receive data.

Mihai Corlan developed an Android remote control for a desktop MP3 player; read about it at http://corlan.org/2010/07/02/creating-multi-screen-apps-for-android-and -desktop-using-air/.

Tom Krcha created a remote controller to send accelerometer, speed, and brake information to a car racing game (see http://www.flashrealtime.com/game-remote-device-con troller/).

RTMFP UDP

Peer-to-peer (P2P) communication is the real-time transfer of data and media between clients.

Real Time Media Flow Protocol (RTMFP) is an Adobe proprietary protocol. It enables peer-to-peer communication between applications running in Flash Player or the AIR runtime. It is meant to provide a low-latency, secure, peering network experience.

Real Time Messaging Protocol (RTMP) uses Transmission Control Protocol (TCP). RTMFP uses User Datagram Protocol (UDP), which provides better latency, higher security (128-bit AES encryption), and scalability. RTMP is faster than RTMFP, but does not guarantee perfect message ordering and delivery.

RTMP was designed to connect via an RTMFP-capable server, such as the Cirrus service (RTMP uses the Flash Media Server), but it can also be used over a local network using WiFi without the need for a server. Note that Flash Media Server 4.0 speaks RTMFP as well.

 

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

Duped by Fake Documents? Hire a Forgery Attorney

If you are accused of producing fake documents, your best decision would be to hire a specialized and experienced forgery attorney. Forgery allegations can come at either the federal or state level. Know your rights and get protection.The accusation can be for allegedly making a fake document, or changing a document by content or signature. In most cases, to have the knowledge that a document that is in your possession is forged is sufficient to be a crime. Most allegations stem from transactions that include money, contracts, checks and documents either legal or financial. Most of these charges are significant and followed through upon due to the person being accused making a monetary gain because of the alleged forgery.In many situations, an individual who has been charged with alleged forgery involving documents normally is not aware that the suspect documents are forged. This is just one of the reasons why a knowledgeable and experienced forgery attorney is required. With this person’s area of expertise, the forgery attorney can protect a person when they have unknowingly been duped with bad documents.For those who do not understand the gravity of the charges, the maximum penalty by federal standards is 20 years in prison. For the state it is 10 years. Not only could you end up living many years behind bars, your reputation would be in ruin. This could permanently hamper your future employment prospects for years.An experienced forgery attorney will understand how to properly present your case when it comes to unwittingly possessing forged documents. This is a very complicated and tricky area of law. A lawyer that only deals with forgery cases is the preferred choice to deal with all the delicate matters that will have to be properly handled so as to give their clients the best possible outcome.So if you feel that a document in your possession is a forgery or you are accused of forgery, do not hesitate in hiring a qualified forgery attorney as your counsel.

Advantages and Disadvantages of Governmental Student Loan Consolidation

When you decide to consolidate your governmental student loans, you have to take a notice of the advantages, but also of the disadvantages. Are you deciding to consolidate your loans, to make your monthly repayment process easier?First of all, it is nice to hear that you are trying to find the best way for yourself to repay your debts. However, you must ensure that your decision is right for your situation, because it will have a significant impact on your future.Here is a list of the benefits of a governmental student loan consolidation:1. It reduces your federal interest rate
2. Your repayment term can be extended
3. It has different payment plans: Standard, Advanced or Graduate
4. Instead of several loans, you will only repay a single loan
5. There is no fee to pay with a federal loan consolidation
6. There wouldn’t be a credit check or prepaid fees
7. It is easier to apply for federal consolidation than a private consolidationDisadvantages of a governmental loan consolidation:1. It will make special borrowers benefits invalid, if you have a Perkins loan.
2. It will make the six months grace period invalid. This means that you need to pay immediately.
3. If you have collected a large amount of debts, its possible that you end up repaying thousands of dollars more than the original amount of the loan, this is the consequence for the extended repayment term. The longer you are repaying, the more you have to pay.
4. If you have already repaid more than a half of your loans, it would be more smart not to consolidate your loans, you will save more money in the end.

Dangers of Home Equity Debt Consolidation Loans

There are so many debt relief companies out there all vying for your hard-earned dollars. You need to know a little something about the ones that you select before you sign on the dotted line. The dangers of a home-equity debt consolidation loan starts off with the very nature of this form of collateral debt. Make no doubt about it you are incurring another line of credit when you take out a debt consolidation loan even though the salesman made it sound as if your past credit card and debt problems were just that, in the past.Voluntary Theft and AbandonmentWhat is surprising about individuals and homeowners, who readily put their homes up as pieces of collateral against a debt consolidation loan, is that they do this action in the very first place. Right after that, it is not surprising that many professional financial experts consider debt consolidation home equity loans as one of the most dangerous loan issues that exist today in the marketplace.Very Frightening NumbersThe reason why these debt loan professionals feel that home equity loans are nothing but stress and danger comes from the fact of the record of default of this form of loan industry. With the defaulted loan percentages hovering around the 15 to 20% mark, in reality, this means that out of 100 homeowners who took out a debt consolidation loan and used their home as collateral, 15 to 20 of those defaulted and lost their home.Surprisingly IneptThe dangers of a consolidation loan with the residence as collateral is the very fact that you can lose your home if you default on any of the loan terms. What is more distressing is that many of these debt loan contracts come with teaser rates for the first six months of the loan repayment schedule. One can just imagine the shock to the bottom line when individuals who reach that six-month teaser rate, did not realize this little fact and are then slapped with a double-digit variable rate that rises each year.At All CostsPlease be careful and avoid at all costs signing-on to a home equity debt consolidation loans experience since the result may be something out of your darkest nightmares. You do not have to put your home, the place in which you may have been in for decades, as collateral for any debt consolidation loan instrument. There are multiple options and methods, into which you can apply and be accepted, without having to put up the house and home.Debt Consolidation Loans Resources