Sdílet prostřednictvím


Notifying the Mobile Client of New Surveys

patterns & practices Developer Center

On this page: Download:
You Will Learn | Overview of the Solution | Inside the Implementation - Registering for Notifications, Sending Notifications, Notification Payloads Download code samples
Download book as PDF

You Will Learn

  • How to establish a channel to the Microsoft Push Notifications Service.
  • How to register with the service that will send notification messages.
  • How to specify the notification types that the mobile client application will receive.
  • How to send notifications to subscribed Windows Phone devices.

Tailspin wants a way to notify users of new surveys from the user's list of preferred tenants. Tenants are subscribers to the cloud-based Tailspin Surveys application who publish surveys. Users will then be able to synchronize the mobile client application and start using the new surveys.

Hh821030.note(en-us,PandP.10).gifChristine Says:
Christine
                There are two types of notifications that the phone can receive when the application isn't running. The first is a toast notification that the phone displays in an overlay on the user's current screen. The user can click the message to launch the application. The second type of notification is a tile notification that changes the appearance of the front and back of the application's tile in the Quick Launch area of the phone's Start experience. If you change the appearance of a tile by sending a tile notification, and if you want to change the tile back to its original state, you'll have to send another message.<br />You can also use raw notifications to send data directly to the application, but this type of notification requires the application to be running in the foreground. If the application is not running, MPNS discards the notification and notifies the sender. </td>

Overview of the Solution

Tailspin chose to use the Microsoft Push Notifications Service (MPNS) for Windows Phone to deliver information about relevant new surveys to users with Windows Phone devices. This feature allows the cloud-based Surveys application to notify a user about a relevant new survey even when the mobile client application isn't running. In order to receive push notifications, users must first subscribe to them on the AppSettingsView page by turning on the push notifications ToggleSwitch before saving their application settings. In addition, users must then go to the FilterSettingsView page and select their desired tenants before saving their filter settings.

Push notifications for Windows Phone can send notifications to users of your Windows Phone application even if the application isn't running. Toast notifications are ignored if the application is running, unless the ShellToastNotificationReceived event is subscribed to. In this case the application can decide how it wants to respond to toast notifications.

Figure 3 shows, at a high level, how this notification process works for the Tailspin Surveys application.

Hh821030.59707873942334FE38F20908CF05D33C(en-us,PandP.10).png

Figure 3

Push notifications for Windows Phone

Figure 3 shows how an application on the Windows Phone device can register for push notifications from another application—a service running in Windows Azure in this case. After the registration process is complete, the service in Windows Azure can push notifications to the Windows Phone device. The following describes the process illustrated in Figure 3:

  1. The registration process starts when the client application establishes a channel by sending a message to the MPNS.
  2. The MPNS returns a URI that is unique to the instance of the client application on a particular Windows Phone device.
  3. Establishing the channel simply enables the phone to receive messages. The client application must also register with the service that will send the notification messages by sending its unique Uniform Resource Identifier (URI) to the service. In the Surveys application, there is a Registration web service hosted in Windows Azure that the mobile client application can use to register its URI for notifications of new surveys.
  4. Provided that notification registration has succeeded, the mobile client application can also specify which types of notifications it will receive; this part of the registration process sets up a binding that enables the phone to associate a notification with the application and enables the user to launch the mobile client application in response to receiving a message. The Windows Phone Application Certification Requirements specify that you must provide the user with the ability to disable toast and tile notifications. You must run the application at least once to execute the code that establishes the channel before your phone can receive notifications.
  5. Notifications are pushed to the Windows Phone device.

The service can use the unique URI to send messages to the client application. The Surveys service sends a message to a mobile client by sending a message to the endpoint specified by the URI that the client sent when it registered. The MPNS hosts this endpoint and forwards messages from the service on to the correct device.

Hh821030.note(en-us,PandP.10).gifJana Says:
Jana
                Remember that notifications are sent to the phone, not the application. This is because there is no guarantee that the application will be running when the Surveys service sends a message. </td>

For more information about the certification requirements that relate to push notifications, see Section 6.2, "Push Notifications Application," of "Additional Requirements for Specific Application Types" on the MSDN® developer program website.

Note

The sample application uses the free, unauthenticated MPNS that limits you to sending 500 notification requests per channel per day. If you use the free version of MPNS, it also means that your application is vulnerable to spoofing and denial of service attacks if someone impersonates the worker role that is sending notifications.
The authenticated MPNS has no restrictions on the number of notification messages you can send, and it requires the communication between your server component and MPNS to use Secure Sockets Layer (SSL) for sending notification messages.
For more information about the Microsoft Push Notification Service, see "Push Notifications Overview for Windows Phone" on MSDN.

Inside the Implementation

Now is a good time to take a more detailed look at the code that implements push notifications. As you go through this section, you may want to download the Windows Phone Tailspin Surveys application from the Microsoft Download Center.

Registering for Notifications

Before a phone can receive notifications from the Windows Azure service of new surveys, it must obtain its unique URI from the MPNS. This registration process takes place when the user taps the Save button on the AppSettingsView page.

The following code example shows the IRegistrationServiceClient interface in the TailSpin.PhoneClient project that defines the registration operations that the mobile client can perform.

public interface IRegistrationServiceClient
{
  IObservable<TaskSummaryResult> UpdateReceiveNotifications(
    bool receiveNotifications);
  IObservable<Unit> UpdateTenants(
    IEnumerable<TenantItem> tenants);
  IObservable<SurveyFiltersInformation> 
    GetSurveysFilterInformation();
  bool CredentialsAreInvalid();
  void CloseChannel();
  bool IsProcessing { get; }
  event EventHandler IsProcessingChanged;
}

For details of the UpdateTenants and GetSurveysFilterInformation methods, see the section, "Filtering Data," later in this chapter.

The RegistrationServiceClient class implements the UpdateReceiveNotifications method to handle the registration process for the mobile client. The following code example shows how the UpdateReceiveNotifications method handles the registration and unregistration processes in the mobile client application:

  • If the user is enabling notifications from MPNS, the method creates an HttpNotificationChannel object and binds the channel to the toast and tile notifications, and registers the unique URI with the Tailspin Surveys service.
  • If the user is disabling notifications from MPNS, the method unregisters from the Tailspin Surveys service and closes the channel.

The HttpNotificationChannel object stores the unique URI allocated by the MPNS.

private const string ChannelName = "tailspindemo.cloudapp.net";
private const string ServiceName = "TailSpinRegistrationService";
...
public IObservable<TaskSummaryResult> UpdateReceiveNotifications(
  bool receiveNotifications)
{
  if (receiveNotifications)
  {
    httpChannel = new HttpNotificationChannel(ChannelName, ServiceName);
    ...
    // Bind the channel to the toast and tile nofiticaions and register
    // the URI with the Tailspin Surveys service.
    ...
  }
  else
  {
    httpChannel = HttpNotificationChannel.Find(ChannelName);

    if (httpChannel != null && httpChannel.ChannelUri != null)
    {
      return BindChannelAndUpdateDeviceUriInService(
        receiveNotifications, httpChannel.ChannelUri)
        .Select(taskSummary =>
        {
          IsProcessing = false;
          return TaskSummaryResult.Success;
        });
    }
    else
    {
      IsProcessing = false;
      return Observable.Return(TaskSummaryResult.Success);
    }
  }
}

When the user is enabling notifications, the code in the following example from the UpdateReceiveNotifications method creates the channel and registers the URI with the Tailspin Surveys service, and then binds the channel to the toast and tile notifications. It does this by creating the HttpChannel object and then converting the ChannelUriUpdated and ErrorOccurred events of the HttpChannel object into observable sequences using the Observable.FromEvent method, before it opens the channel. Once the observable sequence for the ChannelUriUpdated event has a value, the BindChannelAndUpdateDeviceUriInService method is called. The method also uses a timeout on the ChannelUriUpdated observable sequence, which throws a TimeoutException if the sequence doesn't get a value in 60 seconds. Finally, it returns the observable TaskSummaryResult object from whichever of the two observable sequences gets a value first.

var channelUriUpdated = 
  from evt in httpChannel.ObserveChannelUriUpdatedEvent()
  from result in BindChannelAndUpdateDeviceUriInService(receiveNotifications,
    evt.EventArgs.ChannelUri)
  select result;

var channelUriUpdateFail =
  from o in httpChannel.ObserveErrorOccurredEvent()
  select TaskSummaryResult.UnknownError;

httpChannel.Open();

// If the notification service does not respond in time, it is assumed that the 
// server is unreachable. The first event that happens is returned.

return channelUriUpdated.Timeout(
  TimeSpan.FromSeconds(60)).Amb(channelUriUpdateFail).Take(1)
  .Select(tsr =>
   {
     IsProcessing = false;
     return tsr;
   })
  .Catch<TaskSummaryResult, TimeoutException>(
  (e) => 
  {
    IsProcessing = false;
    return Observable.Return(TaskSummaryResult.UnreachableServer);
  });

Note

The Take(TSource) method returns a specified number of contiguous values from the start of an observable sequence.

The following code example shows how the BindChannelAndUpdateDeviceUriInService method in the RegistrationServiceClient class registers the clients unique URI with the Tailspin Surveys web service by asynchronously invoking a web method and passing it a DeviceDto object that contains the phone's unique URI. In addition, this method also binds the channel to the toast and tile notifications.

private IObservable<TaskSummaryResult> 
  BindChannelAndUpdateDeviceUriInService(
  bool receiveNotifications, Uri channelUri)
{
  var device = new DeviceDto
  {
    Uri = channelUri != null ? channelUri.ToString() : string.Empty,
    RecieveNotifications = receiveNotifications
  };

  var uri = new Uri(serviceUri, "Notifications");

  return httpClient
    .PostJson(new HttpWebRequestAdapter(uri), settingsStore.UserName,
    settingsStore.Password, device)
    .Select(u =>
    {
      BindChannelAndNotify(receiveNotifications);
      return TaskSummaryResult.Success;
    });
}

This method uses an instance of the HttpClient class to post the data transfer object to the web service. Tailspin developed the class to simplify the sending of asynchronous HTTP requests from the mobile client application.

The following code example shows how the BindChannelAndNotify method in the RegistrationServiceClient class configures the phone to respond to toast and tile notifications.

private readonly Uri serviceUri;
private readonly IHttpClient httpClient;
...

private void BindChannelAndNotify(bool receiveNotifications)
{
  if (httpChannel != null)
  {
    if (receiveNotifications)
    {
      if (!httpChannel.IsShellToastBound)
        httpChannel.BindToShellToast();

      if (!httpChannel.IsShellTileBound)
        httpChannel.BindToShellTile();
    }
    else
    {
      if (httpChannel.IsShellToastBound)
        httpChannel.UnbindToShellToast();

      if (httpChannel.IsShellTileBound)
        httpChannel.UnbindToShellTile();
    }
  }
}

The following code example shows the PostJson method from the HttpClient class that uses the Observable.FromAsyncPattern method from the Reactive Extensions (Rx) framework to call the web method asynchronously. This code example shows four steps:

  1. It first creates the IHttpWebRequest object and uses the FromAsyncPattern method to create an asynchronous function that returns an observable Stream object from the IHttpWebRequest object.
  2. It uses the WriteContentToStream method to attach the payload to the request stream.
  3. It then calls the BeginGetResponse and EndGetResponse methods on the request object and returns an IObservable<WebResponse>.
  4. The method returns an IObservable<Unit> instance, which is equivalent to a null in Rx, when it has a complete HTTP response message.
public IObservable<Unit> PostJson<T>(IHttpWebRequest httpWebRequest, 
  string userName, string password, T obj)
{
  var request = GetRequest(httpWebRequest, userName, password);
  request.Method = "POST";
  request.ContentType = "application/json";

  return from requestStream in Observable
    .FromAsyncPattern<Stream>(request.BeginGetRequestStream, 
    request.EndGetRequestStream)()
    from response in WriteContentToStream(requestStream, request, obj)
    select new Unit();
}

private IObservable<WebResponse> WriteContentToStream<T>(Stream requestStream, 
  IHttpWebRequest request, T obj)
{
  using (requestStream)
  {
    var serializer = new DataContractJsonSerializer(typeof(T));
    serializer.WriteObject(requestStream, obj);
  }

  return Observable.FromAsyncPattern<WebResponse>(
  request.BeginGetResponse, request.EndGetResponse)();
}

Figure 4 outlines the chain of observables that are initiated by the AppSettingsViewModel Submit method.

Follow link to expand image

Figure 4

The chain of observables initiated by the AppSettingsViewModel Submit method.

The Submit method calls UpdateReceiveNotifications on the IRegistrationServiceClient instance, observes the result on the UI thread, and subscribes to the result. The call to UpdateReceiveNotifications results in one of four outcomes:

  • TaskSummaryResult.Success is returned. In this case, the settings store is updated and if the intention was to turn off push notifications, then both the IRegistrationServiceClient instance and the subscription are disposed of.
  • TaskSummaryResult.UnreachableServer is returned. This means that the update has timed out.
  • TaskSummaryResult.UnknownError is returned. In this case the ChannelUriUpdateFailed event is raised by the HttpNotificationChannel instance.
  • An exception is thrown. This may be a WebException related to the HTTP post.

These outcomes are handled by the delegates passed into the call to the Subscribe method. This is the only call to the Subscribe method that is required to start the chain of observables. The CloseChannel method in the RegistrationServiceClient class closes and disposes of the HttpChannel object, and is called from the CleanUp method in the AppSettingsViewModel class. For more information see, "Handling Asynchronous Interactions" in Chapter 3, "Using Services on the Phone."

So far, this section has described Tailspin's implementation of the client portion of the registration process. The next part of this section describes how the Surveys service stores the unique URI that the client obtained from the MPNS in Windows Azure storage whenever a Windows Phone device registers for notifications of new surveys.

The following code example shows the implementation of the registration web service that runs in Windows Azure. You can find this class is in the Tailspin.Services.Surveys project.

[AspNetCompatibilityRequirements(RequirementsMode = 
  AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceBehavior(InstanceContextMode = 
  InstanceContextMode.PerCall)]
public class RegistrationService : IRegistrationService
{
  ...
  private readonly IUserDeviceStore userDeviceStore;
  ...
  public void Notifications(DeviceDto device)
  {
    var username = Thread.CurrentPrincipal.Identity.Name;

    bool isWellFormedUriString = Uri.IsWellFormedUriString(
      device.Uri, UriKind.Absolute);
    if (isWellFormedUriString)
    {
      if (device.RecieveNotifications)
      {
        this.userDeviceStore.SetUserDevice(username, new Uri(device.Uri));
      }
      else
      {
        this.userDeviceStore.RemoveUserDevice(new Uri(device.Uri));
      }
    }
  }
}

The Notifications method receives a DeviceDto parameter object that includes the unique URI allocated to the phone by the MPNS and a Boolean flag to specify whether the user is subscribing or unsubscribing to notifications. The service saves the details in the DeviceDto object in the device store in Windows Azure storage.

Note

In the sample application, the service does not protect the data as it reads and writes to Windows Azure storage. When you deploy your application to Windows Azure, you should secure your table, binary large object (blob), and queue endpoints using SSL.

Although phones can explicitly unsubscribe from notifications, the application also removes devices from the device store if it detects that they are no longer registered with the MPNS when it sends a notification.

Sending Notifications

When a survey creator saves a new survey, the Surveys service in Windows Azure retrieves all the URIs for the subscribed Windows Phone devices and then sends the notifications to all the devices that subscribe to notifications for surveys created by that particular survey creator.

The Tailspin Surveys service sends toast notifications of new surveys to subscribed Windows Phone devices.

The following code example from the NewSurveyNotificationCommand class in the TailSpin.Workers.Notifications project shows how the Windows Azure worker role retrieves the list of subscribed phones and sends the notifications. This method uses the filtering service to retrieve the list of devices that should receive notifications about a particular survey. For more information, see the section, "Filtering Data," later in this chapter.

public void Run(NewSurveyMessage message)
{
  var survey = this.surveyStore.GetSurveyByTenantAndSlugName(
    message.Tenant, message.SlugName, false);

  if (survey != null)
  {
    var deviceUris = 
      from user in this.filteringService.GetUsersForSurvey(survey)
      from deviceUri in this.userDeviceStore.GetDevices(user)
      select deviceUri;

    foreach (var deviceUri in deviceUris)
    {
      this.pushNotification.PushToastNotification(deviceUri.ToString(), 
        "New Survey", "tap 'sync' to get it",
         uri => this.userDeviceStore.RemoveUserDevice(new Uri(uri)));
    }
  }
}

The Run method also passes a reference to a callback method that removes from the device store phones that are no longer registered with the MPNS.

Note

For more information about how the Run method is triggered, and about retry policies if the Run method throws an exception, see the section "The Worker Role 'Plumbing' Code" in Chapter 4, "Building a Scalable, Multi-Tenant Application for Windows Azure," of the book, Developing Applications for the Cloud on the Microsoft Windows Azure™ Platform 2nd Edition. This is available on MSDN.

The following code example shows the PushToastNotification method in the PushNotification class, from the TailSpin.Web.Survey.Shared project, which is invoked by the worker role command to send the notification. This method creates the toast notification message to send to the MPNS before it calls the SendMessage method.

public void PushToastNotification(string channelUri, string text1, 
  string text2, DeviceNotFoundInMpns callback)
{
  byte[] payload = ToastNotificationPayloadBuilder.Create(text1, text2);
  string messageId = Guid.NewGuid().ToString();
  this.SendMessage(NotificationType.Toast, channelUri, messageId, payload, 
    callback);
}

The SendMessage method sends messages to the MPNS for forwarding on to the subscribed devices. The following code example shows how the SendMessage method sends the message to the MPNS; the next code example shows how the SendMessage method receives a response from the MPNS.

Hh821030.note(en-us,PandP.10).gifMarkus Says:
Markus The SendMessage method acquires the stream and writes to it asynchronously because the service may be sending messages to hundreds or even thousands of devices, and for every device, it needs to open and write to a stream.
protected void OnNotified(NotificationType notificationType, 
  HttpWebResponse response)
{
  var args = new NotificationArgs(notificationType, response);
  ...
}

private void SendMessage(NotificationType notificationType, 
  string channelUri, string messageId, byte[] payload, 
  DeviceNotFoundInMpns callback)
{
  try
  {
    WebRequest request = WebRequestFactory.CreatePhoneRequest(
      channelUri, payload.Length, notificationType, messageId);
    request.BeginGetRequestStream(
      ar =>
      {
        // Once async call returns get the Stream object
        Stream requestStream = request.EndGetRequestStream(ar);

        // and start to write the payload to the stream 
        // asynchronously. 
        requestStream.BeginWrite(
          payload,
          0,
          payload.Length,
          iar =>
          {
            // When the writing is done, close the stream
            requestStream.EndWrite(iar);
            requestStream.Close();

            // and switch to receiving the response from MPNS

            ...
          },
          null);
    },
    null);
  }
  catch (WebException ex)
  {
    if (ex.Status == WebExceptionStatus.ProtocolError)
    {
      this.OnNotified(notificationType, (HttpWebResponse)ex.Response);
    }
    Trace.TraceError(ex.TraceInformation());
  }
}

After the SendMessage method sends the message to the MPNS, it waits for a response. It must receive the message asynchronously because the MPNS does not return a response until it, in turn, receives a response from the Windows Phone device. The SendMessage method notifies its caller of the response through the OnNotified method call.

...
// Switch to receiving the response from MPNS.
request.BeginGetResponse(
  iarr =>
  {
    try
    {
      using (WebResponse response = request.EndGetResponse(iarr))
      {
        // Notify the caller with the MPNS results.
        this.OnNotified(notificationType, (HttpWebResponse)response);
      }
    }
    catch (WebException ex)
    {
      if (ex.Status == WebExceptionStatus.ProtocolError)
      {
        this.OnNotified(notificationType, (HttpWebResponse)ex.Response);
      }
      if (((HttpWebResponse)ex.Response).StatusCode == HttpStatusCode.NotFound)
      {
          callback(channelUri);
      }
      Trace.TraceError(ex.TraceInformation());
    }
  },
  null);
...

If the SendMessage method receives a "404 Not Found" response code from the MPNS, it removes the stored subscription details from the store because this response indicates that the device is no longer registered with the MPNS.

The following table summarizes the information available from the MPNS in the response.

Item

Description

Message ID

This is a unique identifier for the response message.

Notification Status

This is the status of the notification message. Possible values are Received, Dropped, or QueueFull. You could use this value to determine whether you need to resend the message to this device.

Device Connection Status

This is the connection status of the device. Possible values are Connected, InActive, Disconnected, and TempDisconnected. If it is important that a device receive the message, you can use this value to determine whether you need to resend the message later.

Subscription Status

This is the device's subscription status. Possible values are Active and Expired. If a device's subscription has expired, you can remove its URI from your list of subscribed devices.

For more information about sending push notifications, see "How to: Send a Push Notification for Windows Phone" on MSDN.

Notification Payloads

The elements of a toast notification are:

  • A title string that displays after the application icon.
  • A content string that displays after the title.
  • A parameter value that is not displayed but is passed to the application if the user taps on the toast notification; for example, the page the application should launch to, or name-value pairs to pass to the application.

For information about the elements of a tile notification you should read the section, "Using Live Tiles on the Phone," in Chapter 3, "Using Services on the Phone."

You must make sure that the strings you send on toast notifications fit in the available space.

The following code example from the ToastNotificationPayloadBuilder class in the TailSpin.Workers.Notifications project shows how the Surveys service constructs a toast notification message.

public static byte[] Create(string text1, string text2 = null)
{
  using (var stream = new MemoryStream())
  {
    var settings = new XmlWriterSettings 
    { 
      Indent = true, 
      Encoding = Encoding.UTF8 
    };
    using (XmlWriter writer = XmlWriter.Create(stream, settings))
    {
      if (writer != null)
      {
        writer.WriteStartDocument();
        writer.WriteStartElement("wp", "Notification", 
          "WPNotification");
        writer.WriteStartElement("wp", "Toast", "WPNotification");
        writer.WriteStartElement("wp", "Text1", "WPNotification");
        writer.WriteValue(text1);
        writer.WriteEndElement();
        writer.WriteStartElement("wp", "Text2", "WPNotification");
        writer.WriteValue(text2);
        writer.WriteEndElement();
        writer.WriteEndElement();
        writer.WriteEndDocument();
        writer.Close();
      }

      byte[] payload = stream.ToArray();
      return payload;
    }
  }
}

Next Topic | Previous Topic | Home

Last built: May 25, 2012