Sunday, June 3, 2012

Windows 8 push notifications


This post is a brief overview of using push notifications in a Metro app. Push notifications let you create a web service that will be able to independently send tile, toast or raw notifications to your Metro app over Http using a preconfigured communication channel between the app and the service. So your app doesn’t need to poll the service regularly to check for updates, instead, the service can send updates to your app whenever new information is available. This is done through the Microsoft Windows Notification Service or WNS, which is an always-on Microsoft-managed service whose sole purpose is to channel billions of small messages from third-party callers to registered Metro apps in an efficient and dependable manner. Let’s see how this works in a nutshell.

image


1) Your Metro app requests from WNS that a channel be created between itself and WNS. WNS returns a unique channel Uri that represents that channel to the app.

2) The app forwards the channel Uri to your custom service – which you set up ahead of time. Your service stores the uri with the id of the device running the app. When the time comes, your service will use that uri to send a tile or toast notification to the registered devices through WNS.

3) Your service authenticates with WNS (one-time operation) by requesting and storing an authentication token returned by WNS. When the service is ready to send a toast or tile notifications to registered apps, it will send an HTTP  message to WNS including the stored auth token, so that WNS accepts the message.

4) Your service is now ready to send a notification to registered devices : it creates an XML message matching the tile or toast required schema, and sends it to each uris associated with the registered devices. WNS receives each authenticated message and forwards it to the corresponding device. As a result, the app’s tile is updated or a toast is raised on the device.

So first, you request a channel from your Metro app, using the PushNotificationChannelManager class :
   1: channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();

Then you can forward the uri to your service, which has a register operation that accepts a Uri along with some unique identifier for the device running the Metro app :


   1: var data = new PushClientData
   2:             {
   3:                 CustomerId = App.Id,
   4:                 ChannelUri = channel.Uri.ToString()
   5:             };
   6:  
   7: // TODO : insert data into request content
   8:  
   9: //Send request
  10: var response = await client.PostAsync(uri, content);

The service operation may look something like this :


   1: public void RegisterApp(PushClientData app)
   2: {
   3:     //TODO : verify Uri is a valid WNS channel Uri
   4:  
   5:     registeredClientsDictionary[app.Id] = new Uri(app.ChannelUri);
   6: }

When your service runs for the first time, it needs to authenticate with WNS by retrieving an authentication token :


   1: protected void GetAccessToken()
   2: {
   3:     var urlEncodedSid = HttpUtility.UrlEncode(sid);
   4:     var urlEncodedSecret = HttpUtility.UrlEncode(secret);
   5:  
   6:     var body =
   7:       String.Format("grant_type=client_credentials&client_id={0}&client_secret={1}&scope=notify.windows.com",
   8:       urlEncodedSid, urlEncodedSecret);
   9:  
  10:     var client = new WebClient();
  11:     client.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
  12:  
  13:     string response = client.UploadString(new Uri(authUri), body);
  14:     var oAuthToken = GetOAuthTokenFromJson(response);
  15:     this.accessToken = oAuthToken.AccessToken;
  16: }

The sid and secret key can currently be obtained by registering your Metro app in the Windows Push Notifications & Live Connect portal (although this may change later on in the Metro ecosystem). The GetOAuthTokenFromJson helper simply extracts the token from the JSON response returned by WNS.

Now your Metro app is all set up, and your cloud service is authenticated with WNS. The service has stored the channel Uris and Ids of each Metro device running your app and registered with WNS to receive notifications. The service can now send a tile or toast notification to the registered devices through WNS using the stored channel Uris. First, let’s create the notification message the service will send WNS :


   1: private string GetTilePayload(MyDataClass data, string tileId = "tile1")
   2: {
   3:    var wideTemplateName = "TileWidePeekImage01";
   4:  
   5:    tile = string.Format("<tile><visual lang=\"en-US\">" +
   6:                            "<binding template=\"{0}\">" +
   7:                            "<image id=\"1\" src=\"{1}\"/>" +
   8:                            "<text id=\"1\">{2}</text>" +
   9:                            "<text id=\"2\">{3}</text>" +
  10:                            "</binding>", 
  11:                            wideTemplateName, data.Photo, data.Fname + " " + data.Lname, data.Comments);
  12:  
  13:    return tile;
  14: }

Here the service creates the string payload matching the required schema for a tile notification. See this post for more info. If instead, you want the service to send a toast, here’s an example :


   1: private string GetToastPayload(MyDataClass data)
   2: {
   3:     var templateName = "ToastImageAndText02";
   4:     var toast = string.Format("<toast><visual lang=\"en-US\">" +
   5:                             "<binding template=\"{3}\">" +
   6:                             "<image id=\"1\" src=\"{0}\"/>" +
   7:                             "<text id=\"1\">{1}</text>" +
   8:                             "<text id=\"2\">{2}</text>" +
   9:                             "</binding></visual></toast>",
  10:                             data.Photo, data.Fname + " " + data.Lname, data.Comments, templateName);
  11:     return toast;
  12: }

Once your service has built the notification payload, it needs to build the WNS request :


   1: protected string SendNotification(Uri uri, string payload, string type)
   2: {
   3:     byte[] content = Encoding.UTF8.GetBytes(payload);
   4:     string statusCode = "";
   5:  
   6:     var request = HttpWebRequest.Create(uri) as HttpWebRequest;
   7:     request.Method = "POST";
   8:  
   9:     request.Headers.Add("X-WNS-Type", type);
  10:     request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
  11:  
  12:     request.BeginGetRequestStream(result =>
  13:     {
  14:         var requestStream = request.EndGetRequestStream(result);
  15:         requestStream.Write(content, 0, content.Length);
  16:         request.BeginGetResponse(result2 =>
  17:         {
  18:             var response = request.EndGetResponse(result2) as HttpWebResponse;
  19:             statusCode = response.StatusCode.ToString();
  20:         }, null);
  21:     }, null);
  22:  
  23:     return statusCode;
  24: }

As you can see, the service builds a POST request that includes the authentication token in a header and the xml payload we just created in the body. In this example the service creates and sends the request asynchronously for better scalability, although yoy may choose to do so synchronously depending on your scenario.

Note that the authentication token has a time limited validity, so you should be prepared for the case where WNS returns an expired token message and rejects the notification message :


   1: try
   2: {
   3:     var response = request.EndGetResponse(result2) as HttpWebResponse;
   4:     statusCode = response.StatusCode.ToString();
   5: }
   6: catch (WebException ex)
   7: {
   8:     string authHeader = ex.Response.Headers["WWW-Authenticate"];
   9:     if (authHeader.Contains("Token expired"))
  10:     {
  11:         GetAccessToken();
  12:         statusCode = SendNotification(uri, xml, type);
  13:     }
  14:     else
  15:     {
  16:         //return ex.Message;
  17:     }
  18: }

Here I’ve wrapped the code that sends the request and fetches the WNS response in a try-catch block, and if the response indicates an expired token, the code goes out and fetches a new token and then resends the notification to WNS.

So when this method is invoked in your service by some external caller, such as an arbitrary service client, the notification request will get sent out to WNS, and WNS will forward it to all registered devices running your Metro app, which will in turn receive the message and consequently update the app tile or raise a toast on the device.

Note that the app may also want to run some code on the device when a push message is received :


   1: private void InterceptNotification()
   2:         {
   3:             channel.PushNotificationReceived += (s, e) =>
   4:             {
   5:                 switch (e.NotificationType)
   6:                 {
   7:                     case PushNotificationType.Toast:
   8:                         toast = e.ToastNotification; break;
   9:                     case PushNotificationType.Tile:
  10:                         tile = e.TileNotification; break;
  11:                     case PushNotificationType.Badge:
  12:                         badge = e.BadgeNotification; break;
  13:                 }
  14:                 //app-specific code ...
  15:             };
  16:         }



So to recap, you can use push notifications to have a custom cloud service send tile, toast or other kinds of messages to your Metro app. This requires quite a bit of setup involving Microsoft’s Windows Notification Service. You establish communication channels between your Metro app and WNS, between your app and your service, and between your service and WNS. As a result your app can benefit from the push model and from the efficiency and dependability of WNS to receive external notifications without polling or similar actions.

Hope you enjoyed this post!

3 comments:

  1. Yacine, je suis client Pluralsight et tes cours sont vraiment parmis les meilleurs!

    Merci beaucoup pour tout! Sincèrement!

    Tu assures...

    ReplyDelete
    Replies
    1. Merci pour ton feedback positif :)

      Delete
  2. This comment has been removed by a blog administrator.

    ReplyDelete