In This Chapter
• Benefits of push notification
• Toast, tile, and raw notifications
• Specifying notification delivery delay using notification classes
• Shell tile anatomy
• Using local and remote shell tile images
• Updating an application tile using a tile schedule
• Cloud service authentication
• Sending CLR objects with raw notifications
• Building a stock ticker app
Push notification is a server to client, or cloud to phone, messaging system that allows notifications to be sent from the cloud to the phone and displayed to the phone user. Notifications are one-way messages that are normally associated with a particular application and may be delivered both while the application is running and not running. The main purpose of the push notification system is to reduce the power consumption of the phone device, thereby increasing battery life.
This chapter begins with an overview of push notification, starting with the three different types of push notifications: toast, tile, and raw notifications. The chapter examines the various types of channel errors that can occur during push notification and how the phone’s battery level affects delivery of push notifications. Cloud service authentication using X.509 certificates is also discussed as well as how to update an application tile without tile notifications using shell tile schedules.
Finally, the chapter explores a custom stock ticker sample app that allows the user to enter a stock symbol into the phone, after which a cloud service periodically notifies the phone of stock price variations.
Windows Phone supports the following three types of push notifications:
• Toast notification—A systemwide notification that is shown for a few seconds at the top of the phone’s display.
• Tile notification—Displays information using a tile on the Start Experience (home screen).
• Raw notification—Offers a completely flexible manner for delivering notifications to your running app. The OS does not display a raw notification, but rather the notification message is sent directly to your running app.
The Microsoft Push Notification Service (MPNS) is designed to be resilient and to expose a persistent channel for sending notifications to Windows Phone devices.
Apart from some special cases, the Windows Phone platform does not allow multiple apps to be running at the same time.1 The main reason is that, in most cases, it is not efficient to have background apps consuming device resources, such as maintaining an open connection and sending data over the wire. Network activity results in high power consumption and decreased battery life.
1 Background tasks allow an app to run in the background periodically for short periods. For more information, see Chapter 27, “Scheduled Actions.”
By battery life I mean the amount of time a device runs before it requires a recharge. Conversely, battery lifespan means the total amount of time a battery lasts before it is discarded and replaced with a new battery.
Push notification reduces power consumption in several ways; most notably it uses a single shared network connection, eliminating the need for private persistent open connections. If an app needs to be running merely to receive updates from the cloud, a better approach is to use push notification. The push notification model has the following benefits:
• Offline capability—Windows Phone apps should be designed to be tolerant of interruptions as they may be deactivated or terminated at any time. Push notification allows the user to act on some information, even when your app is not running.
• Notification aggregation—Multiple notifications for different apps can be pushed at the same time.
• Notification prioritization—Notifications can be assigned a priority, which affects when the notification is delivered. This control allows a developer to assign lower priorities to those notifications that do not require immediate attention, thereby reducing the device’s network activity.
• Connection pooling—It is more efficient to use a single shared network connection than to have each app owning its own connection.
• Small message size—Notifications are designed to carry a small payload. This forces you to think carefully about what information is really necessary to be conveyed.
• Deterministic behavior—Tile and toast notifications use a well-defined format with a fixed set of data properties, which allows the OS to present them automatically.
• Eliminates polling—Apps that want to receive messages from a cloud service without the use of push notification must periodically poll the cloud service. Push notification eliminates the need for custom network communication code and for private persistent open connections.
Push notification relies on the MPNS to dispatch notifications to the phone. The process works like this:
1. A phone app requests subscription to the MPNS using a notification channel.
2. The MPNS returns a URI, where notifications can be sent.
3. The phone app forwards the URI to any third-party service (such as a web service created by you) that wants to send the phone app notifications (see Figure 13.1).
The Push Notification URI is the address of the MPNS plus an identifier for phone device. Having received the push notification URI, a third-party service is then able to send notifications to the phone, via the MPNS, as shown in Figure 13.2.
The notification channel URI is not guaranteed to remain the same. Therefore, each time a phone app launches, it is expected to send its notification channel URI to its corresponding cloud service.
Notifications from the cloud service use a well-defined protocol for sending notifications to the MPNS. Subsequent sections of the chapter examine the protocol format for the various notification types.
Microsoft does not offer a Service Level Agreement (SLA) for the delivery of push notifications.
The main CLR types used in push notification reside in the Microsoft.Phone.Notification
namespace. To consume push notifications in an application, a reference to the Microsoft. Phone
assembly is required (see Figure 13.3).
To enable an application to use push notification, the ID_CAP_PUSH_NOTIFICATION capability must be present in the WMAppManifest.xml file. While not explicitly required for push notification, the ID_CAP_NETWORKING capability is also required if you want to notify a cloud service of the existence of an MNPS URI. Push notification is useless in most cases without the latter capability.
The following excerpt shows the capabilities needed for push notification in the WMAppManifest.xml file:
<Capabilities>
<Capability Name="ID_CAP_NETWORKING" />
<Capability Name="ID_CAP_PUSH_NOTIFICATION" />
</Capabilities>
For more information on application capabilities, see Chapter 2, “Fundamental Concepts in Silverlight Development for Windows Phone.”
To pass certification for the Windows Phone Marketplace, an app must provide the user with the ability to disable toast and tile notifications. The user must have the ability to perform this task from within the app.
Furthermore, before calling either the HttpNotificationChannel.BindtoShellToast
or HttpNotificationChannel.BindToShellTile
methods for the first time, an application must explicitly ask the user for permission.
These requirements give the user control over if and when push notification is used.
To subscribe to push notification within an app, we first attempt to retrieve an existing HttpNotificationChannel
by name.
channel = HttpNotificationChannel.Find("<a channel name>");
The HttpNotificationChannel.Find
method does not raise an Exception
and returns null
if the channel with the specified name is not found. If so, a new instance must be created.
The Push Notification sample in the downloadable code contains a class called PushNotificationSubscriber
, which manages subscription to the MPNS by wrapping a notification channel. The main benefit of using a wrapper for the notification channel is that if the PushNotificationSubscriber
instance is unable to perform the subscription to the MPNS, due to the absence of a network connection for example, it automatically retries.
If the channel cannot be found, a new channel is created as shown:
if (channel == null)
{
channel = string.IsNullOrEmpty(serviceName)
? new HttpNotificationChannel(channelName)
: new HttpNotificationChannel(channelName, serviceName);
}
The HttpNotificationChannel
constructor has the following two overloads:
public HttpNotificationChannel(string channelName);
public HttpNotificationChannel(string channelName, string serviceName);
The channelName
parameter is the name that the app uses to identify the notification channel instance. This allows an application to retrieve an existing channel by name using the static HttpNotificationChannel.Find
method.
The serviceName
parameter is the name of the cloud service to which the notification channel is associated. The serviceName
parameter may be used to subscribe to an authenticated cloud service. For more information, see the section “Cloud Service Authentication” later in this chapter.
Once you have obtained a channel instance, it can be opened like so:
channel.Open();
For the OS to display toast and tile notifications, the channel must call the BindToShellToast
and BindToShellTile
methods, as demonstrated in the following excerpt:
/* Toast Notifications. */
if (!channel.IsShellToastBound)
{
channel.BindToShellToast();
}
/* Tile Notifications. */
if (!channel.IsShellTileBound)
{
channel.BindToShellTile();
}
To stop receiving toast and tile notifications, the channel provides the UnbindToShellToast
and UnbindToShellTile
methods, respectively.
When creating an HttpNotificationChannel
, avoid keeping it open if not subscribing to its events. It is not necessary for the channel to be kept open for the shell to receive either toast or tile notifications, and the channel itself consumes valuable resources.
HttpNotificationChannel
implements IDisposable
and can be safely disposed after binding it to the shell, as shown in the following excerpt:
using (HttpNotificationChannel channel
= new HttpNotificationChannel(channelName))
{
channel.Open();
channel.BindToShellTile();
channel.BindToShellToast();
}
Calling the Open
method on a channel may immediately raise an Exception
, most commonly an InvalidOperationException
, if the channel fails to establish a connection with the MPNS.
After retrieving or creating a channel instance, you are able to subscribe to its various events.
Be mindful that each Windows Phone device is limited to a total of 15 apps registered for push notifications. If the user installs 15 apps that use push notifications and your app is the 16th one installed, an InvalidOperationException
(channel quota exceeded) is raised if your app calls BindToShellTile
or BindToShellToast
.
The following is a list of the HttpNotificationChannel
events:
• ChannelUriUpdated—This event occurs when the push notification URI changes, which can be at any time. If and when the URI changes, the new URI must be forwarded to any cloud services that send push notifications to your app.
• HttpNotificationReceived—This event occurs when a raw notification is received.
• ShellToastNotificationReceived—This event occurs when a toast notification is received.
• ErrorOccurred—This event provides a handling mechanism for exceptions raised by the channel. The handler for this event receives a NotificationChannelErrorEventArgs
object, which contains an ErrorCode
property, an ErrorType
property, and a Message
property pertaining to the Exception
that was raised.
When a channel error occurs, the NotificationChannelErrorEventArgs.ErrorType
property can be used to determine the nature of the error, as the following excerpt demonstrates:
void HandleChannelErrorOccurred(
object sender, NotificationChannelErrorEventArgs e)
{
switch (e.ErrorType)
{
case ChannelErrorType.ChannelOpenFailed:
// ...
break;
case ChannelErrorType.MessageBadContent:
// ...
break;
case ChannelErrorType.NotificationRateTooHigh:
// ...
break;
case ChannelErrorType.PayloadFormatError:
// ...
break;
case ChannelErrorType.PowerLevelChanged:
// ...
break;
}
}
The following list describes the NotificationChannelErrorEventArgs.ErrorType
enumeration values:
• ChannelOpenFailed—Occurs when the push client is unable to establish a connection with the MPNS. Note that exceptions resulting from, for example, a channel already being open, are raised at the call to the HttpNotificationChannel.Open
method.
• MessageBadContent—Occurs when using tile notifications and the BackgroundImage
URI is pointing to a remote image, despite the HttpNotificationChannel
not being bound to a list of URIs. Tile notifications are discussed in detail later in the chapter.
• NotificationRateTooHigh—Occurs when the push notification client is unable to receive messages because the cloud service is sending too many messages at too high a rate.
• PayloadFormatError—Occurs when the XML payload format, or the HTTP header of the push notification, is syntactically invalid. When this error occurs, the HttpNotificationChannel
is closed and the channel must be reopened.
• PowerLevelChanged—Occurs when the device’s battery level changes significantly enough to trigger a change in the push client’s power policy. This topic is discussed in the next section “Power Management and Push Notification.”
Additional error information can be obtained by examining the NotificationChannelErrorEventArgs.ErrorCode
property, which identifies the HRESULT
of the Exception
. The HRESULT
is a coded numerical value that is assigned to some specific exceptions.
Even though push notification assists in reducing the power consumption of the phone, maintaining any kind of network connection unfortunately expends more power than most other device functions. Therefore, when the phone’s battery level is low, to help reduce power consumption, the phone progressively prevents certain types of notifications from being received by the device (see Figure 13.4).
When an HttpNotificationChannel
raises its ErrorOccured
event with an ErrorType
value of PowerLevelChanged
, it indicates that there is a change to the types of notifications that are to be delivered to the device.
The NotificationChannelErrorEventArgs.ErrorAdditionalData
property can be used to obtain the ChannelPowerLevel
enum value, as shown in the following excerpt:
void HandleChannelErrorOccurred(
object sender, NotificationChannelErrorEventArgs e)
{
switch (e.ErrorType)
{
case ChannelErrorType.PowerLevelChanged:
if (e.ErrorAdditionalData == (int)ChannelPowerLevel.LowPowerLevel
|| e.ErrorAdditionalData
== (int)ChannelPowerLevel.CriticalLowPowerLevel)
{
/* Power level is too low. */
}
break;
}
}
The following list describes each of the ChannelPowerLevel
enum values:
• NormalPowerLevel—The battery level is not low. All push notification types are sent to the device.
• LowPowerLevel—The battery level is low. Only raw notifications are sent to the device.
• CriticalLowPowerLevel—The battery level is critically low. No push notifications of any type are sent to the device.
Sending a toast or tile push notification is done by creating an XML envelope containing the notification information, converting it to a byte array, and then sending it via an HttpWebRequest
to the MPNS.
When sending a push notification, only the HTTP POST method is allowed. Using any other method such as GET, PUT, CREATE, or DELETE results in a 405 MethodNotAllowed response. Moreover, a ProtocolViolationException
results when writing to the request Stream
.
For testing purposes, however, the HTTP GET method can be used, and results in a 200 OK response regardless.
An example of a push notification URL is http://sn1.notify.live.net/throttledthirdparty/01.00/AAGvdLTQzLGqRZ0FRZRVT1GBAgoOs1kPAQAAAAQOMDAwAAAAAAAAAAAAAAA.
The notification URL subscriber identifier, which is located after the final forward slash, does not need to resolve to an active subscription. This prevents a third party from fishing for an active subscription URL.
Raw notifications have more flexibility in their format; however, the process of sending a raw notification remains the same as that of tile and toast notifications. The following sections explore in detail how to send toast, tile, and raw notifications.
When sending a notification, the maximum size of a toast, tile, or raw notification body should not exceed 1KB. If this size is exceeded, a System.Net.WebException
is raised, resulting from a 400 (Bad Request) response code returned from the MPNS.
For a complete list of MPNS response codes, see http://bit.ly/wd6SXy.
Toast notifications are systemwide action requests primarily designed for use with peer-to-peer communications. Toast notifications do not disrupt the user’s workflow. They are, however, distracting and therefore should be used sparingly. Toast notifications are presented on the phone as a clickable overlay on the user’s current screen even if the associated app is not running (see Figure 13.5). Toast notifications are, however, not displayed if the current app is the app that initiated the push notification subscription. If a user clicks on a toast notification, the associated app is launched.
Toast notifications should be time critical and personally relevant to the user.
Assume that toast notifications are not enabled for your app, as they require the user to explicitly opt-in to receive them.
A toast notification consists of the application icon displayed at a reduced size and two text fields, one for a title, the other for the notification message.
The HttpNotificationChannel.ShellToastNotificationReceived
event is used to receive toast notifications within your app, like so:
channel.ShellToastNotificationReceived +=
channel_ShellToastNotificationReceived;
Toast notifications are displayed only when your app is not running in the foreground. If your app is running in the foreground, the toast notification can still be received and handled in code.
When the ShellToastNotificationReceived
event is raised, the handler receives a NotificationEventArgs
object from which the relevant toast notification fields can be extracted, as shown:
void channel_ShellToastNotificationReceived(object sender,
NotificationEventArgs e)
{
string text1 = e.Collection["wp:Text1"];
string text2 = e.Collection["wp:Text2"];
//...
}
Toast notifications require an XML envelope for the notification information. Listing 13.1 demonstrates how the creation of an XML envelope is constructed and sent to the MPNS. An HttpWebRequest
is created using the push notification subscription URL. The notification XML is converted to a byte array, and then it is written to the outgoing request Stream
. After sending the request, the response is examined to ensure that it was correctly received by the MPNS.
For more information on the X-NotificationClass
request header, and setting a notification’s priority level, see the upcoming section “Notification Classes.”
public PushNotificationSendResult SendToastNotification(
string title, string message, string url, PushNotificationPriority priority)
{
string priorityHeader;
if (priority == PushNotificationPriority.RealTime)
{
priorityHeader = "2";
}
else if (priority == PushNotificationPriority.Priority)
{
priorityHeader = "12";
}
else
{
priorityHeader = "22";
}
/* Create the http message that will be sent
* to the Microsoft hosted server. */
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
request.ContentType = "text/xml";
request.Headers = new WebHeaderCollection
{
{ "X-WindowsPhone-Target", "toast" },
{ "X-NotificationClass", priorityHeader }
};
/* The XML envelope contains the notification text. */
string messageFormat = "<?xml version='1.0' encoding='utf-8'?>" +
"<wp:Notification xmlns:wp='WPNotification'>" +
"<wp:Toast>" +
"<wp:Text1>{0}</wp:Text1>" +
"<wp:Text2>{1}</wp:Text2>" +
"</wp:Toast>" +
"</wp:Notification>";
/* Insert the message string. */
string messageFormatted = string.Format(messageFormat, title, message);
/* The message will be sent as a byte array. */
byte[] messageBytes = Encoding.Default.GetBytes(messageFormatted);
request.ContentLength = messageBytes.Length;
using (Stream requestStream = request.GetRequestStream())
{
requestStream.Write(messageBytes, 0, messageBytes.Length);
}
/* Get the response after the message is sent. */
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
string notificationStatus = response.Headers["X-NotificationStatus"];
string deviceConnectionStatus
= response.Headers["X-DeviceConnectionStatus"];
string subscriptionStatus = response.Headers["X-SubscriptionStatus"];
PushNotificationSendResult result = new PushNotificationSendResult(
notificationStatus, deviceConnectionStatus, subscriptionStatus);
Debug.WriteLine(result);
return result;
}
An HttpRequestHeader
named WindowsPhone-Target
with the value toast is added to the request’s Headers
collection, indicating to the MPNS that it is a toast notification.
The XML format for a toast notification is shown in the following fragment:
<?xml version='1.0' encoding='utf-8'?>
<wp:Notification xmlns:wp='WPNotification'>
<wp:Toast>
<wp:Text1>WP7 Unleashed</wp:Text1>
<wp:Text2>Toast example</wp:Text2> " +
</wp:Toast>
</wp:Notification>
When the notification is displayed on the phone, the content of the wp:Text1
element is placed in the title field of the notification overlay, while the content of the wp:Text2
element is placed in the body field (see Figure 13.6).
By default, the app icon is the ApplicationIcon.png file present in the root directory of the Windows Phone project. This can be customized from within the project’s properties editor (see Figure 13.7).
Alternatively, the path to the image for the application icon can be modified manually within the WMAppManifest.xml file at the Deployment/App/IconPath element.
<IconPath IsRelative="true"
IsResource="false">ApplicationIcon.png</IconPath>
As you can see in Figure 13.6, it is wise to tailor the application icon so that it displays clearly at a small size when presented in a toast notification.
A tile is a dynamic visual representation of your app that resides on the phone’s Start Experience. Each app on the phone can be associated with a single application tile and multiple secondary tiles. Tiles serve as a shortcut to an application.
Tile notifications allow the application tile to be modified periodically, using a data driven template model, with a fixed set of data properties. Each property corresponds to a UI element, with each UI element having a fixed position on the tile. A tile notification can be used to communicate information sent from the cloud, such as updates from an email service, a social networking service, or a weather service.
It is at the user’s discretion to pin an application to the Start Experience, and having the app pinned to the Start Experience is essential for receiving tile notifications.
When an application is pinned to the Start Experience, tile notifications are displayed regardless of whether the app is running.
The template for a tile notification consists of three assignable data properties: a title property, a count property (also called a badge), and a background image property (see Figure 13.8). A cloud service may modify the content of an associated tile using tile notifications.
Tile notifications are comprised of three parts: a background, a title, and a count (or badge) field, as shown in Figure 13.9.
The background image of a tile can be assigned a local resource URI or a remote URI to an image on the cloud. By using a remote image, you have the opportunity to dynamically generate it, giving you more control over information presented in the image but at the cost of bandwidth and power consumption.
The tile background image must be in either JPG or PNG format.
The title is a text field located at the bottom of the tile.
The Count
element must be a positive integer value between 0 and 99. This value is displayed on the count property, or badge, region of the tile. If the value is 0 then the badge is not displayed.
Sending a tile notification is done in the same manner as sending a toast notification, the differences being in the request headers and the format of the XML message (see Listing 13.2). As with toast notifications, we create an HttpWebRequest
, build the message, convert the message to a byte array, and then write it to the request Stream
. Finally, we verify that the notification was correctly received by the MPNS.
An HttpRequestHeader
named WindowsPhone-Target
with the value tile is added to the request’s Headers
collection, indicating to the MPNS that it is a tile notification.
public PushNotificationSendResult SendTileNotification(string title, int count,
string imagePath, string url, PushNotificationPriority priority)
{
string priorityHeader;
if (priority == PushNotificationPriority.RealTime)
{
priorityHeader = "1";
}
else if (priority == PushNotificationPriority.Priority)
{
priorityHeader = "11";
}
else
{
priorityHeader = "21";
}
/* Create the http message that will be sent
* to the Microsoft hosted server. */
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
request.ContentType = "text/xml";
request.Headers = new WebHeaderCollection
{
{ "X-WindowsPhone-Target", "token" },
{ "X-NotificationClass", priorityHeader }
};
/* The XML envelope contains the notification text. */
string messageFormat = "<?xml version='1.0' encoding='utf-8'?>" +
"<wp:Notification xmlns:wp='WPNotification'>" +
"<wp:Tile>" +
"<wp:BackgroundImage>{0}</wp:BackgroundImage>" +
"<wp:Count>{1}</wp:Count>" +
"<wp:Title>{2}</wp:Title>" +
"</wp:Tile>" +
"</wp:Notification>";
/* Insert the message string. */
string messageFormatted = string.Format(
messageFormat, imagePath, count, title);
/* The message is sent as a byte array. */
byte[] messageBytes = Encoding.Default.GetBytes(messageFormatted);
request.ContentLength = messageBytes.Length;
using (Stream requestStream = request.GetRequestStream())
{
requestStream.Write(messageBytes, 0, messageBytes.Length);
}
/* Get the response after the message is sent. */
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
string notificationStatus = response.Headers["X-NotificationStatus"];
string subscriptionStatus = response.Headers["X-SubscriptionStatus"];
string deviceConnectionStatus
= response.Headers["X-DeviceConnectionStatus"];
PushNotificationSendResult result = new PushNotificationSendResult(
PushNotificationEnumConverter.ToNotificationStatus(notificationStatus),
PushNotificationEnumConverter.ToDeviceConnectionStatus(
deviceConnectionStatus),
PushNotificationEnumConverter.ToSubscriptionStatus(subscriptionStatus)
);
Debug.WriteLine(result);
return result;
}
The format for a tile notification is shown in the following fragment:
<?xml version='1.0' encoding='utf-8'?>
<wp:Notification xmlns:wp='WPNotification'>
<wp:Tile>
<wp:BackgroundImage>
ProjectSubdirectory/ExampleImage.png
</wp:BackgroundImage>
<wp:Count>
An integer n, with 0 <= n <= 99
</wp:Count>
<wp:Title>Example Title</wp:Title>
</wp:Tile>
</wp:Notification>
When the notification is displayed on the phone, the content of the wp:BackgroundImage
element specifies the URL of the tile’s background image. This value can either be a local path within the project, or a remote URI such as http://www.example.com/ImageName.png.
A tile’s background image size must not exceed 80KB. In addition, the download time for an image must not exceed 1 minute. If the image size is too large, or the download takes too long, then the default image for the tile is used.
The wp:Title
element denotes the title text to be displayed at the bottom of the tile.
For information on how to create shell tiles or modify tile properties directly from your app, see Chapter 27.
Presenting too much information can slow down comprehension of a tile. Therefore, keep the style simple to improve clarity. When designing a background image for a tile, it is important to make different states easily recognizable.
Do not presume that a background image will be placed on a particular screen color. Themes allow the phone’s Start Experience color scheme to be modified. Therefore, when using PNG images as the tile image format, avoid using transparency if possible, as antialiasing may produce unintended discoloration around shape boundaries.
You saw how to specify the background image of an application tile using push notification. There is, however, an alternative approach that lets you set the background image to a remote URI without using push notification, eliminating the need for a cloud service to issue tile notifications.
By using the ShellTileSchedule
class the phone can be directed to periodically download an image from a remote server.
To create a schedule for a tile update, create a new instance of the ShellTileSchedule
class and set the RemoteImageUri
property, as shown in the following example:
string url = "http://www.example.com/ImageName.png";
ShellTileSchedule schedule = new ShellTileSchedule
{
Interval = UpdateInterval.EveryHour,
MaxUpdateCount = 10,
Recurrence = UpdateRecurrence.Interval,
RemoteImageUri = new Uri(url),
StartTime = DateTime.Now
};
schedule.Start();
The following is a list of ShellTileSchedule
properties that affect the download frequency:
• Interval—This property specifies the download frequency rate and is of type Microsoft.Phone.Shell.UpdateInterval
. UpdateInterval
is an enumeration with the following values:
• EveryHour
• EveryDay
• EveryWeek
• EveryMonth
If the shortest frequency, EveryHour
, does not offer a short enough interval, push notification or a background agent should be considered instead.
• MaxUpdateCount—This property defines the number of times the schedule runs. If this value is not set or set to a number less than 1, then the schedule runs indefinitely. This value is ignored if Recurrence
is set to OneTime
.
• Recurrence—This property specifies whether the image is to be fetched periodically, or if retrieval of the image is to occur only once. The available values are UpdateRecurrence.Interval
or UpdateRecurrence.OneTime
.
• RemoteImageUri—This property is the fully qualified URI of the background image. It cannot point to a local image stored on the phone.
• StartTime—This property specifies the duration from the initial ShellTileSchedule.Start
method call until the first time that the image is fetched. This property is used to delay the initial download of an image.
The same restrictions apply to images retrieved using a ShellTileSchedule
as those in tile notifications. In particular, image size must not exceed 80KB, and the download time must not exceed 1 minute. If the image size is too large, or the download takes too long, not only is the default image used for the tile, but the tile schedule is also removed. The only way to reinstate a schedule is to re-run the schedule generation code.
Raw notifications are not displayed by the OS and are delivered directly to your running app, via an event. If your app is not running when a raw notification is received, then the notification is discarded.
Sending a raw notification differs from sending tile and toast notifications in that there is greater flexibility over the contents of the body of the notification.
Listing 13.3 demonstrates sending an object as part of a raw notification, using a DataContractJsonSerializer
. As with tile and toast notifications, we create an HttpWebRequest
, convert the content to a byte array, and then write it to the request Stream
. Finally, we verify that the notification was correctly received by the MPNS.
public PushNotificationSendResult SendRawNotification(
object content, string url, PushNotificationPriority priority)
{
string priorityHeader;
if (priority == PushNotificationPriority.RealTime)
{
priorityHeader = "5";
}
else if (priority == PushNotificationPriority.Priority)
{
priorityHeader = "16";
}
else
{
priorityHeader = "26";
}
/* Create the http message that will be sent
* to the Microsoft hosted server. */
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
/* HTTP POST is the only allowed method to send the notification. */
request.Method = "POST";
request.ContentType = "application/json; charset=utf-8";
request.Headers = new WebHeaderCollection {
{ "X-MessageID", Guid.NewGuid().ToString()},
{ "X-NotificationClass", priorityHeader } };
/* The message will be sent as a byte array. */
byte[] contentBytes;
var serializer = new DataContractJsonSerializer(content.GetType());
using (MemoryStream stream = new MemoryStream())
{
serializer.WriteObject(stream, content);
contentBytes = stream.ToArray();
}
request.ContentLength = contentBytes.Length;
using (Stream requestStream = request.GetRequestStream())
{
requestStream.Write(contentBytes, 0, contentBytes.Length);
}
/* Get the response after the message is sent. */
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
string notificationStatus = response.Headers["X-NotificationStatus"];
string subscriptionStatus = response.Headers["X-SubscriptionStatus"];
string deviceConnectionStatus
= response.Headers["X-DeviceConnectionStatus"];
PushNotificationSendResult result = CreateSendResult(
notificationStatus, deviceConnectionStatus, subscriptionStatus);
return result;
}
The use of the DataContractJsonSerializer
is convenient because it allows you to serialize an entire object on the server in a format that is compatible with Silverlight. With little effort the object can be rehydrated on the client. You could, however, use another payload format, such as a string, as shown in the tile and toast notification examples.
The DataContractJsonSerializer
type, as shown in this example, was chosen over the more commonly used DataContractSerializer
because JSON serialization is more space efficient than XML, producing a payload of much smaller size. This is important because the maximum body size for a toast, tile, or raw notification is 1KB.
The DataContractJsonSerializer
approach relies on the content object’s class being decorated with DataContract
and DataMember
attributes, as shown in the following example:
[DataContract]
public class StockQuote
{
[DataMember]
public string StockSymbol { get; set; }
[DataMember]
public double LastPrice { get; set; }
[DataMember]
public double ChangeInPrice { get; set; }
}
The DataContract
and DataMember
attributes provide the DataContractJsonSerializer
with the information it needs to serialize the StockQuote
object. Neglecting to decorate a property with a DataMember
attribute causes that property to be ignored, and its value to be absent when the object is deserialized in the client application.
To receive a raw notification in an app, subscribe to the HttpNotificationChannel.HttpNotificationReceived
event, as shown:
channel.HttpNotificationReceived += channel_HttpNotificationReceived;
...
void channel_HttpNotificationReceived(object sender,
HttpNotificationEventArgs e)
{
...
}
When the HttpNotificationReceived
event is raised, the handler receives an HttpNotificationEventArgs
object that contains a Notification
property of type HttpNotification
. HttpNotification
contains the following properties:
• Body—The payload for the notification. This is the main property of interest. It is a Stream
containing the raw notification data.
• Channel—The channel to which the notification arrived.
• Headers—The request headers that were supplied during the creation of the notification.
The following excerpt from the PushNotificationViewModel
class in the downloadable sample code demonstrates how raw notification is handled on the phone, and in particular how the content object (in this case, a StockQuote
) is rehydrated:
void subscriber_HttpNotificationReceived(
object sender, HttpNotificationEventArgs e)
{
Message = "Raw notification received.";
Stream bodyStream = e.Notification.Body;
if (bodyStream != null)
{
DataContractJsonSerializer serializer
= new DataContractJsonSerializer(typeof(StockQuote));
StockQuote = (StockQuote)serializer.ReadObject(bodyStream);
}
}
A DataContractJsonSerializer
reverses the serialization process by transforming the stream data back to a StockQuote
object.
Common to all push notification types is an optional custom header named X-MessageID
, which can be added to the HttpWebRequest.Headers
collection. Its purpose is to uniquely identify the notification message, and if present, the same value is able to be retrieved in the notification response. It must be a string that contains a universally unique identifier (UUID), as shown in the following excerpt:
request.Headers.Add("X-MessageID", "<UUID>");
At the time of writing, this header does not serve any purpose, because the resulting HttpWebResponse
is retrieved using the original HttpWebRequest
. It is, therefore, trivial to identify a notification based on an HttpWebResponse
. In the future, however, this setting may be used in conjunction with an MPNS callback, to update the cloud service if a notification is impacted due to, for example, session expiry.
Some kinds of notifications may be more time critical than others. For example, a notification indicating that tomorrow’s weather forecast has changed may be less time critical than a notification about a traffic jam on your planned route home. The MPNS allows notifications to be assigned a batching interval, which works as a guide for the MPNS in determining when to deliver one or more notifications in a batch.
To inform the MPNS of the batching interval of a notification, a header named X-NotificationClass is added to the outgoing request. The batching interval is a string representation of an integer value. The set of valid priority values is determined by the kind of notification, be it a toast, tile, or raw notification.
When notifications are sent to the MPNS, they are placed in queues (see Figure 13.10). Each queue is serviced at different intervals according to its notification class. Accordingly, a queue for a lower notification class may be serviced more frequently than one with a higher notification class. For example, a raw notification with an X-NotificationClass of 3 means that the message is delivered by the MPNS immediately, a value of 13 means that the message is delivered within 450 seconds, and a value of 23 means the message is delivered within 900 seconds.
When sending a push notification, the X-NotificationClass may be omitted, in which case a default regular priority is used. Table 13.1 lists the push notification classes for each notification type.
In the downloadable sample code, the notification class is represented as an enum called PushNotificationPriority
and provides for strongly typed notification class values.
public enum PushNotificationPriority
{
Regular,
Priority,
RealTime
}
The Push Notification protocol enables a cloud service to send a notification to a phone application using a RESTful API. The cloud service can choose to operate as authenticated or unauthenticated by registering an X.509 certificate with the MPNS. An unauthenticated cloud service is limited to sending 500 notification requests a day for each subscription. While in most cases this will suffice, having an authenticated cloud service removes this limitation, and an unlimited number of push notifications can be sent for the life of a subscription.
To authenticate a cloud service, obtain an X.509 certificate issued by a Microsoft trusted root certificate authority. The certificate can then be registered on the Windows Phone Marketplace. The certificate is used to establish a Secure Sockets Layer (SSL) connection between the cloud service and the MPNS.
As mentioned earlier in this chapter, the HttpNotificationChannel
has a constructor that accepts the fully qualified domain name (FQDN) of the cloud service certificate. The FQDN must match the registered certificate’s subject name, which is the CN attribute. An example of an FQDN is www.example.com. Creating a channel using an FQDN is demonstrated in the following excerpt:
using (HttpNotificationChannel channel
= new HttpNotificationChannel(channelName, "www.example.com"))
{
channel.Open();
channel.BindToShellEntryPoint();
channel.BindToShellNotification();
}
The main sample for this app demonstrates the three kinds of push notifications: tile, toast, and raw notifications. It is a stock ticker application that allows the user to enter a stock symbol into the phone, after which a cloud service periodically notifies the phone of stock price variations.
When debugging using a phone device (not the emulator) a local WCF service is unreachable from the app. To allow a device to communicate with a local WCF service, configure Visual Studio to use IIS and use a machine name or external IP address in your WCF client configuration.
The server side component of the sample application consists of a WCF service that allows the phone app to register itself to receive stock price update notifications. The server side implementation retrieves stock price information periodically from Yahoo! Finance (see Figure 13.11).
Input controls for the application include a text box, which allows the user to enter a stock symbol to be monitored, a Register button, and an Unregister button. Clicking on the Register button causes the viewmodel’s RegisterForPushNotification
method to be called. This causes a PushNotificationSubscriber
to be initialized and its events subscribed to, as shown in the following excerpt.
void Subscribe()
{
if (subscriber == null)
{
InitializeSubscriber();
}
subscriber.Subscribe();
}
void InitializeSubscriber()
{
subscriber = new PushNotificationSubscriber(
channelName, null, RegisterWithCloudService);
subscriber.HttpNotificationReceived
+= subscriber_HttpNotificationReceived;
subscriber.ChannelUriUpdated += subscriber_ChannelUriUpdated;
subscriber.ErrorOccurred += subscriber_ErrorOccurred;
}
A delegate is passed to the subscriber, which is used to notify the cloud service of the push notification URI once it is received from the MPNS.
As mentioned earlier in the chapter, the PushNotificationSubscriber
handles the creation and opening of the HttpNotificationChannel
(see Listing 13.4). When the Subscribe
method is called, a channel is retrieved if it already exists; otherwise, a new channel is created. Subscription occurs to the channel’s various events and the channel is opened. The channel then nominates itself to receive tile and toast notifications.
public void Subscribe()
{
try
{
channel = HttpNotificationChannel.Find(channelName);
if (channel == null)
{
channel = string.IsNullOrEmpty(serviceName)
? new HttpNotificationChannel(channelName)
: new HttpNotificationChannel(channelName, serviceName);
}
else
{
channel.UnbindToShellTile();
channel.UnbindToShellToast();
channel.Close();
Subscribe();
return;
}
UnsubscribeFromChannelEvents();
SubscribeToChannelEvents();
try
{
channel.Open();
}
catch (Exception ex)
{
channel.UnbindToShellTile();
channel.UnbindToShellToast();
channel.Close();
WaitAndRetryChannelConnect();
return;
}
/* Toast Notifications. */
if (!channel.IsShellToastBound)
{
channel.BindToShellToast();
}
/* Tile Notifications. */
if (!channel.IsShellTileBound)
{
channel.BindToShellTile();
}
if (channel.ChannelUri != null)
{
RegisterChannel(channel.ChannelUri);
}
}
catch (Exception ex)
{
Debug.WriteLine("Unable to subscribe. " + ex.ToString());
WaitAndRetryChannelConnect();
}
}
If something goes wrong during the subscription phase, the subscriber waits 30 seconds and then tries again, as the following excerpt shows:
void WaitAndRetryChannelConnect()
{
if (retryTimer == null)
{
/* Create a timer and have it fire once. */
retryTimer = new Timer(o => Subscribe(), null, retryDueTimeMs,
Timeout.Infinite);
}
else
{
/* The timer is changed to fire once after the period expires. */
retryTimer.Change(retryDueTimeMs, Timeout.Infinite);
}
}
PushNotificationSubscriber
contains a field of type Action<string>
named registerWithServer
, whose task is to send the URI of the MPNS subscription to the StockQuoteService
cloud service, along with the stock symbol for the stock that the user wants to monitor. By plugging in this behavior you can test the app without relying on the cloud service being present.
The StockQuoteService
implementation is for demonstration purposes only and lacks the capability to support multiple concurrent users. In supporting multiple users, it would be advisable to use a database to record each user’s stock symbols, and to use a worker process to periodically query the database and to send notifications only when a price change occurs.
When the StockQuoteService.RegisterForStockQuoteNotifications
method is called on the server, a Timer
causes tile, toast, and raw notifications to be periodically sent to the subscriber (Listing 13.5).
The StockQuote
result is provided by the StockQuoter
class, discussed next.
[AspNetCompatibilityRequirements(
RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class StockQuoteService : IStockQuoteService
{
Timer timer;
bool notify = true;
int tilePushCount;
string subscriptionUrl;
string stockSymbol;
StockQuote stockQuote;
bool sendingNotifications;
const int notificationPeriodMs = 30000;
public void RegisterForStockQuoteNotifications(
PushNotificationSubscriberId pushNotificationSubscriberId,
string stockSymbol)
{
subscriptionUrl = pushNotificationSubscriberId.PushNotificationUrl;
this.stockSymbol = stockSymbol;
notify = true;
if (timer == null)
{
timer = new Timer(delegate
{
/* An exception must not be raised. */
try
{
NotifyWithStockQuote();
}
catch (Exception ex)
{
Debug.WriteLine("Unable to notify.", ex);
}
}, null,
2000 /* 2000 MS = 5 seconds. */, notificationPeriodMs);
}
}
void NotifyWithStockQuote()
{
if (!notify || sendingNotifications)
{
return;
}
if (tilePushCount > 98)
{
tilePushCount = 0;
}
try
{
sendingNotifications = true;
try
{
StockQuoter stockQuoter = new StockQuoter();
stockQuote = stockQuoter.GetStockQuote(this.stockSymbol);
}
catch (Exception ex)
{
Debug.WriteLine("Unable to retrieve stock quote." + ex);
return;
}
DateTime now = DateTime.Now;
Debug.WriteLine(
"Sending notifications at " + now.ToLongTimeString());
PushNotifier notifier = new PushNotifier();
string background;
if (stockQuote.ChangeInPrice > 0)
{
background = "PushNotification/Images/StockUp.png";
}
else if (stockQuote.ChangeInPrice < 0)
{
background = "PushNotification/Images/StockDown.png";
}
else
{
background = "PushNotification/Images/StockNone.png";
}
try
{
notifier.SendTileNotification(
stockQuote.ToString(),
++tilePushCount,
background,
subscriptionUrl,
PushNotificationPriority.RealTime);
}
catch (Exception ex)
{
Debug.WriteLine("Unable to send tile notifications:" + ex);
}
try
{
notifier.SendToastNotification("Stock Quote",
stockQuote.ToString(),
subscriptionUrl,
PushNotificationPriority.RealTime);
}
catch (Exception ex)
{
Debug.WriteLine("Unable to send toast notification:" + ex);
}
try
{
notifier.SendRawNotification(stockQuote,
subscriptionUrl, PushNotificationPriority.RealTime);
}
catch (Exception ex)
{
Debug.WriteLine("Unable to send raw notification: " + ex);
}
}
finally
{
sendingNotifications = false;
}
}
public void UnregisterForStockQuoteNotifications(
PushNotificationSubscriberId pushNotificationSubscriberId)
{
notify = false;
tilePushCount = 0;
}
}
The GetStockQuote
method creates an HttpWebRequest
that is sent to the Yahoo! Finance stock quote URL. This request produces a CSV (comma-separated values) file containing the stock information of interest, including the price of the stock and the change in price for the day. The following excerpt shows the GetStockQuote
method:
public StockQuote GetStockQuote(string stockSymbol)
{
string queryString = string.Format( "s={0}&f=sl1p2d1t1c1hgvba", stockSymbol);
string url = "http://download.finance.yahoo.com/d/quotes.csv?" + queryString;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
string responseString;
using (HttpWebResponse response
= (HttpWebResponse)request.GetResponse())
{
using (var reader = new StreamReader(
response.GetResponseStream(), Encoding.ASCII))
{
responseString = reader.ReadLine();
}
}
Debug.WriteLine("Stock Quote: " + responseString);
var cells = responseString.Split(',');
if (cells.Length < 3)
{
throw new Exception("Invalid response.");
}
if (cells[1].ToLower() == @"
a")
{
throw new UnknownStockSymbolException(stockSymbol);
}
StockQuote stockQuote = new StockQuote();
string changeInPrice = cells[2].Replace(""", "").Replace("%", "");
stockQuote.ChangeInPrice = double.Parse(changeInPrice,
NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint);
stockQuote.LastPrice = double.Parse(cells[1].Replace(""", ""));
stockQuote.StockSymbol = cells[0].Replace(""", "");
return stockQuote;
}
The information that is sent back from Yahoo! Finance depends on the contents of the query string in the request URL. The format is like so:
http://finance.yahoo.com/d/quotes.csv?s=
<stock symbols separated by +>&f=<field identifiers>
For more information regarding the Yahoo! stock data format, see http://bit.ly/C5MPu.
The StockQuote
class is a container for the information retrieved from Yahoo! Finance. It is decorated with DataContract
and DataMember
attributes so that it can be serialized for raw notifications, as shown in the following excerpt:
[Serializable
[DataContract]
public class StockQuote
{
[DataMember]
public string StockSymbol { get; set; }
[DataMember]
public double LastPrice { get; set; }
[DataMember]
public double ChangeInPrice { get; set; }
public override string ToString()
{
return string.Format("{0} {1} {2}",
StockSymbol, LastPrice, ChangeInPrice);
}
}
The Stock Quoter app uses all three types of push notifications, which are described in the following sections.
Tile notifications are sent periodically from the StockQuoteService
and are displayed as shown in Figure 13.8. The tile notification contains a Text1
data property equal to Stock Quote and a Text2
data property that is the result of calling the ToString
method of the StockQuote
object, which produces a combination of the stock symbol, the stock’s last price, and its change in price.
Toast notifications are sent periodically from the StockQuoteService
and are displayed as shown in Figure 13.5. The sample uses the title property of the tile notification to display the stock symbol, its price, and its change in price for the day. The count property is populated with an incrementing value, which is just for demonstration purposes.
For tile and raw notifications, the sample makes use of three images: StockUp.png, StockNone.png, and StockDown.png, located in the PushNotification/Images directory of the WindowsPhone7Unleashed.Examples project.
The logic for determining the background image for a tile notification is located server-side, as this excerpt from the StockQuoteService
shows:
string background;
if (stockQuote.ChangeInPrice > 0)
{
background = "PushNotification/Images/StockUp.png";
}
else if (stockQuote.ChangeInPrice < 0)
{
background = "PushNotification/Images/StockDown.png";
}
else
{
background = "PushNotification/Images/StockNone.png";
}
Image paths are relative to the application project.
Raw notifications make use of the serialized StockQuote
instance as the following excerpt from the StockQuoteService
class shows:
notifier.SendRawNotification(
stockQuote, subscriptionUrl, PushNotificationPriority.RealTime);
The StockQuote
instance is serialized to a byte
array, which is then used as the content for the raw notification. For more information on the serialization process see the previous section “Receiving a Raw Notification.”
When a raw notification is received by the phone app and the StockQuote
object is rehydrated in the PushNotificationViewModel
class, the object is assigned to the viewmodel’s StockQuote
property.
The view displays the stock information via the viewmodel’s StockQuote
property, as shown in the following excerpt from the PushNotificationView.xaml
file (style attributes have been removed for clarity):
<TextBlock Text="Price:" />
<TextBlock Text="{Binding StockQuote.LastPrice}" />
<TextBlock Text="Change:" />
<TextBlock Text="{Binding StockQuote.ChangeInPrice}" />
The stock direction image is presented using the viewmodel’s StockPriceDirection
property. When the StockQuote
changes in the viewmodel, the StockPriceDirection
property is reevaluated and a different image displayed in the view. The following excerpt shows the StockQuote
property in the viewmodel:
StockQuote stockQuote;
public StockQuote StockQuote
{
get
{
return stockQuote;
}
set
{
Assign("StockQuote", ref stockQuote, value);
/* Set the direction so that the view
* doesn't need to do any evaluation. */
if (stockQuote == null || stockQuote.ChangeInPrice == 0)
{
StockPriceDirection = StockPriceDirection.None;
}
else if (stockQuote.ChangeInPrice > 0)
{
StockPriceDirection = StockPriceDirection.Up;
}
else
{
StockPriceDirection = StockPriceDirection.Down;
}
}
}
The view itself subscribes to the viewmodel’s PropertyChanged
event. When the StockPriceDirection
property changes, it determines what image should be displayed depending on its value.
void viewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "StockPriceDirection")
{
string url;
switch (ViewModel.StockPriceDirection)
{
case StockPriceDirection.Down:
url = "Images/StockDown.png";
break;
case StockPriceDirection.Up:
url = "Images/StockUp.png";
break;
default:
url = "Images/StockNone.png";
break;
}
Image_StockQuote.Source = new BitmapImage(
new Uri(url, UriKind.Relative));
}
}
Updating the UI based on a viewmodel property could be better orchestrated using the page’s VisualStateManager
. In Chapter 16, “Bing Maps,” we discuss a custom VisualStateUtility
class that allows you to bind the visual state of a UI element to a viewmodel property.
The Stock Quoter application allows a user to unregister from receiving stock updates. By clicking the Unregister button, the viewmodel’s UnregisterPushNotification
method is called, which calls the PushNotificationSubscriber
object’s Unsubscribe
method, as shown:
public void UnregisterForPushNotification()
{
try
{
Message = "Unregistering for notifications.";
if (subscriber != null)
{
subscriber.Unsubscribe();
subscriber.Dispose();
subscriber = null;
}
Message = "Unregistered for notifications.";
}
catch (Exception ex)
{
Message = "Problem unregistering: " + ex.ToString();
}
if (channelUri != null)
{
try
{
UnregisterWithServer(channelUri.ToString());
}
catch (Exception ex)
{
Message = "Problem unregistering with server: " + ex.ToString();
}
}
}
When the PushNotificationSubscriber
is disposed, its Timer
and HttpNotificationChannel
are also disposed. The cloud service is then notified that push notifications should no longer be sent.
This chapter looked at the advantages of push notification, and how it can be used to extend the battery life of the device.
The chapter began with an overview of push notification, starting with the three different types of push notifications: toast, tile, and raw notifications. The various types of channel errors that can occur during push notification were examined, and you saw how the phone’s battery level affects delivery of push notifications. The chapter also looked at cloud service authentication using X.509 certificates, and at how to update an application tile without tile notifications using shell tile schedules.
Finally, the chapter explored a stock ticker sample app that allows the user to enter a stock symbol into the phone, after which a cloud service periodically notifies the phone of stock price variations.
3.139.238.226