Saturday, September 22, 2012

Windows Azure Mobile Services Preview Walkthrough–Part 3: Pushing Notifications to Windows 8 Users (C#)

Previous members of this series:

Part 3 of 5: The Windows Azure Mobile Services (WAMoS) Preview’s initial release enables application developers targeting Windows 8 to automate the following programming tasks for Windows Store apps:

  1. Creating a Windows Azure SQL Database (WASDB) instance and table to persist data entered in a Windows Store app’s form
  2. Connecting the table to the data entry front end app
  3. Adding and authenticating the application’s users
  4. Pushing notifications to users

image_thumb[1][1]My Windows Azure Mobile Services Preview Walkthrough–Part 1: Windows 8 ToDo Demo Application (C#) post of 9/8/2012 covered tasks 1 and 2; Windows Azure Mobile Services Preview Walkthrough–Part 2: Authenticating Windows 8 App Users (C#) covered task 3.

This walkthrough describes the process for completing task 4 based on the Get started with push notifications and Push notifications to users by using Mobile Services tutorials. The process involves the following steps:

  1. Add push notifications to the app
  2. Update scripts to send push notifications
  3. Insert data to receive notifications
  4. Create the Channel table
  5. Update the app (code updated 9/12/2012 3:45 PM PDT)
  6. Update server scripts
  7. Verify the push notification behavior
  8. Viewing the Push Notification Log (added 9/20/2012 11:15 AM PDT, updated 9/22/2012 10:30 AM)

•• Updated Section “8 – Viewing Push Notification Logs” at the end of this article regarding multiple push notifications and errors on 9/22/2012.

• Updated Section 7 on 9/13/2012 at 1:00 PM PDT with elements marked related to duplicate entries in the Channel table.

Screen captures for each step are added to the two tutorials and code modifications are made to accommodate user authentication.

Prerequisites: Completing the oakleaf-todo C# application in Part 1, as well as the user authentication addition of Part 2, and downloading/installing the Live SDK for Windows and Windows Phone, which provides a set of controls and APIs that enable applications to integrate single sign-on (SSO) with Microsoft accounts and access information from SkyDrive, Hotmail, and Windows Live Messenger on Windows Phone and Windows 8.

If your Visual Studio 2012 version doesn’t include the SQL Server Object Manager feature and you receive multiple notifications when adding new todo items, download SQL Server Management Studio 2012 Express to view and remove duplicate entries in the Channel table, as described in steps 7-3 through 7-10 below.


1 – Add Push Notifications to the App

1-1. Launch your WAMoS app in Visual Studio 2012 for Windows 8 or higher, open the App.xaml.cs file and add the following using statement:

using Windows.Networking.PushNotifications;

1-1 Using PushNotifications

1-2. Add the following code to App.xaml.cs after the OnSuspending() event handler:

public static PushNotificationChannel CurrentChannel { get; private set; }


private async void AcquirePushChannel()
{
        CurrentChannel =  
            await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
}

1-2 PushNotificationChannel

This code acquires and stores a push notification channel.

1-3. At the top of the OnLaunched event handler in App.xaml.cs, add the following call to the new AcquirePushChannel method:

AcquirePushChannel();
1-3 AcquirePushChannel

This guarantees that the CurrentChannel property is initialized each time the application is launched.

1-4. Open the project file MainPage.xaml.cs and add the following new attributed property to the TodoItem class:

[DataMember(Name = "channel")]
 public string Channel { get; set; }

1-4 ChannelDataMember

Note: When dynamic schema is enabled on your mobile service, a new 'channel' column is automatically added to the TodoItem table when a new item that contains this property is inserted.

1-5. Replace the ButtonSave_Click event handler method with the following code:

private void ButtonSave_Click(object sender, RoutedEventArgs e)
    {
        var todoItem = new TodoItem { Text = TextInput.Text, Channel = App.CurrentChannel.Uri };
        InsertTodoItem(todoItem);
    }
1-5 ButtonSave_ClickHandler

This sets the client's current channel value on the item before it is sent to the mobile service.

1-6. In the Management Portal’s Mobile Services Preview section, click the service name (oakleaf-todo for this example), click the Push tab and paste the Package SID value from Windows Azure Mobile Services Preview Walkthrough–Part 2: Authenticating Windows 8 App Users (C#) step 1-3:

1-3 AppRegistered

to the Package SID text box and click the Save button:

Figure6-OakLeaf_ToDo Push Client Credentials


2 – Update Scripts to Send Push Notifications

2-1. In the Management Portal’s Mobile Services Preview section, click the service name (oakleaf-todo for this example) click the Data tab and then click the TodoItem table.

2-2. In the todoitem page, click the Script tab and select the Insert script in the Operation list:

2-2 Original Insert Script

This displays the function that is invoked when an insert occurs in the TodoItem table.

2-3. Replace the insert function with the following code, and then click the Save button:

function insert(item, user, request) {
item.userId = user.userId; request.execute({ success: function() { // Write to the response and then send the notification in the background request.respond(); push.wns.sendToastText04(item.channel, { text1: item.text }, { success: function(pushResponse) { console.log("Sent push:", pushResponse); } }); } }); }

2-3 Updated Insert Script

This registers a new insert script, which sends a push notification (the inserted text) to the channel provided in the insert request. The item.userId = user.userId; instruction is required to add the userId value to the todoitem.


3 – Insert Data to Receive Notifications

3-1. In Visual Studio, press the F5 key to run the app, log in if requested, click OK to confirm the user’s identity and click the Refresh button.

3-2. In the TodoList, type an item in the Insert a TodoItem text box and click the Save button.

3-2 Add Todo Text

3-3. Observe that after the insert completes, the app receives a push notification from WNS.

3-3 Show Push Notification

3-4. In the Management Portal’s Mobile Services Preview section, click the service name (oakleaf-todo for this example) click the Data tab, the TodoItem table, and the Browse tab:

3-4 Browse Channel Values

Notice that the latest TodoItem has the channel value added.


4 - Create the Channel Table

4-1. In the Management Portal’s Mobile Services Preview with the todoitem table active as above, click the Mobile Services node in the navigation pane, click the oakleaf-todo item, and click the Data tab:

4-1 Activate Table Create Button

4-2. Click the Create button to open the Create New Table form, type Channel as the Table name and accept the default permissions:

4-2 CreateNewChannelTable

This creates the Channel table, which stores the channel URIs used to send push notifications separate from item data.

Next, you will modify the push notifications app to store data in this new table instead of in the TodoItem table.

5- Update Your App (Revised 9/12/2012 4:00 PM PDT)

* Steps marked with an asterisk depart from the Push notifications to users by using Mobile Services instructions.

5-1. Reopen the project in Visual Studio 2012, open the MainPage.xaml.cs file, and remove the Channel property from the TodoItem class, which then should appear as follows:

public class TodoItem
{
    public int Id { get; set; }

[DataMember(Name = "text")]
public string Text { get; set; }

[DataMember(Name = "complete")]
public bool Complete { get; set; }

}
5-1 FixTodoItemClass

5-2. Replace the ButtonSave_Click event handler method with the original version of this method, as follows:

private void ButtonSave_Click(object sender, RoutedEventArgs e)
{
    var todoItem = new TodoItem { Text = TextInput.Text };
    InsertTodoItem(todoItem);
}

5-2 FixButtonSaveEventHandler

* 5-3. Add the following using statement at the top of the App.xaml.cs class to support the DataMember decoration:

using Microsoft.WindowsAzure.MobileServices;

5-3 using System.Runtime.Searialization

* 5-4. Add the following code to create a new Channel class to the end of App.xaml.cs class:

public class Channel
{
    public int Id { get; set; }

[DataMember(Name = "uri")]
public string Uri { get; set; }
}

5-5. Replace the App.xaml.cs class’s AcquirePushChannel method with the following code:

private async void AcquirePushChannel()
{
    CurrentChannel = 
        await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();


IMobileServiceTable<Channel> channelTable = App.MobileService.GetTable<Channel>();
var channel = new Channel { Uri = CurrentChannel.Uri };
await channelTable.InsertAsync(channel);
}

image

This code inserts the current channel into the Channel table.


6 – Update Server Scripts

6-1. In the Management Portal select the TodoList database in the Mobile Services’ navigation pane, click the Data tab:

6-1 Channel Table

6-2. Click the Channel node to open its page, click the Script tab and select Insert to display the default script:

6-2 Channel Table Insert Default

This displays the function that is invoked when an insert occurs in the Channel table.

6-3. Replace the default Insert() function with the following code:

function insert(item, user, request) {
    var channelTable = tables.getTable('Channel');
    channelTable
        .where({ uri: item.uri })
        .read({ success: insertChannelIfNotFound });

function insertChannelIfNotFound(existingChannels) {
    if (existingChannels.length > 0) {
        request.respond(200, existingChannels[0]);
    } else {
        request.execute();
    }
}
}

This script checks the Channel table for an existing channel with the same URI. The insert only proceeds if no matching channel was found. This prevents duplicate channel records.

6-3 Channel Table Insert Updated

Note: If you have built and run the app before updating the Insert script with the preceding code, you might have issues with the Channel table. See step 7-3 below, which shows duplicate rows in the Channels table.

6-4. Click the Save button, and then click the TodoItem, click Script and select Insert to display the code added in Part 2.

6-4 Todo Table Insert From Part 2

6-5. Replace the Insert() function with the following code:

function insert(item, user, request) {
item.userId = user.userId; request.execute({ success: function() { request.respond(); sendNotifications(); } }); function sendNotifications() { var channelTable = tables.getTable('Channel'); channelTable.read({ success: function(channels) { channels.forEach(function(channel) { push.wns.sendToastText04(channel.uri, { text1: item.text }, { success: function(pushResponse) { console.log("Sent push:", pushResponse); } }); }); } }); } }

6-5 Todo Table Insert Updated

This insert script sends a push notification (with the text of the inserted item) to all channels stored in the Channel table.

6-6. Click the Save button to save the new code.


7 - Verify the Push Notification Behavior

7-1. In Visual Studio, press F5 to build and run the app, log on, if necessary, and type text in the Insert a TodoItem text box.

7-1 Type Todo Item to Test

7-2. Click the Save button:

7-2 Click Save a Couple of Times

Note that after the insert completes, the app receives multiple push notifications from WNS instead of the single notification expected, apparently due to unwanted duplicate rows in the Channels table having the same uri (identical token values).

7-3. To check for duplicate rows in the Channels table, launch the Management Portal and open the Channel table:

7-3 Duplicate Channel Table Rows

7-4. To remove the extra rows, find the Manage URL (server instance name) by opening the database you created in part 1:

7-4 DatabaseMangageURL

7-5. Open the Channel table in Visual Studio’s SQL Object Explorer or SQL Management Studio 2012 by clicking New Connection and completing the Connect to Server dialog with the Mobile Services’ SQL Server instance name and your administrative credentials:

7-4 SQLServerLoginDialog

7-6 Click connect, choose View, SQL Server Object Explorer if you’re using Visual Studio, expand the server nodes, expand the Tables node right-click the oakleaf_todo.Channel table node, choose View Data to open a grid view of the table rows, and select all but one of the duplicate rows:

7-5 SQLServerObjectExplorerGrid

7-7. Press delete to open a confirmation dialog:

7-6 SQLServerObjectExplorerDelete3

7-8. Click OK to delete the selected rows.

7-9. Repeat steps 1 and 2 a couple of times to verify that you receive only one notification.

7-7 MultipleNotificationsFixed

7-10 (Optional). Run the app on two machines at the same time, and repeat the previous step. In this case, the notification is sent to all running app instances.


•• 8 – Viewing the Push Notification Log 

Despite having removed duplicate rows in the Channel table, as described in the preceding section’s steps 7-3 through 7-9, I again encountered duplicate notifications for single ToDo Items. So I started a Why Do I Receive Three Notifications for a Single ToDo Entry? thread in the Windows Azure Mobile Services forum about this issue.

The Mobile Services team’s Josh Twist made the following observation in a reply to my question:

It is technically possible to receive multiple active (different) channelUrls for the same installed app on the same device. I'd recommend taking a look at our push implementation in the doto client sample which stores an 'installation id' in the channel table to ensure we only have one channel url per actual device installation.

The doto sample is available here: http://code.msdn.microsoft.com/windowsapps/doto-a-simple-social-todo-7e6ba464

If you’ve removed duplicate Channel rows and continue to incur multiple notifications for individual ToDo Item insertions, check the Push Notification Log and solve the problem by following these steps:

8–1. Open the oakleaf-todo WAMoS in the Management Portal, click the Logs tab to display the latest notification messages and select an Info item for a successful notification:

LogForMultipleNotificationsPerTodoItem

The preceding 21 items (7 successful (Info) and 14 failed (Error)) log entries are for inserting a single new ToDo Item.

8-2. Click the Details (i) button to display the Log Entry Details form:

image

8-3. Select an Error item and click the Details button to display the full error message:

image

The MSDN Library’s Push notification service request and response headers topic says the following about notificationstatus = dropped:

image

Josh Twist added the following comment in the forum thread after viewing the log entries in step 8-1:

I can't see the image clearly above but I suspect your app has multiple active channels for the device (you sent three notifications to three different channel urls, but they all point to your device). Take a look at the RegisterDevice method from the doto sample: https://github.com/WindowsAzure/azure-mobile-services/blob/master/samples/doto/C%23/ViewModels/MainViewModel.cs which uses the InstallationId type: https://github.com/WindowsAzure/azure-mobile-services/blob/master/samples/doto/C%23/Common/InstallationId.cs.

Then check the script we have on insert: https://github.com/WindowsAzure/azure-mobile-services/blob/master/samples/doto/ServerScripts/devices.insert.js

Following is the C# code for the InstallationId class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Storage;


namespace Doto
{
    /// <summary>
    /// Generates a persistant unique identifier for this installation that is persisted in
    /// local storage. This is used by Doto to manage channelUrls for push notifications
    /// </summary>
    public static class InstallationId
    {
        private static string _fileName = "installation-id.dat";
        private static string _value = null;
        private static object _lock = new object();


        public static async Task<string> GetInstallationId()
        {
            if (_value != null)
            {
                return _value;
            }


            var file = await ApplicationData.Current.LocalFolder.CreateFileAsync(_fileName, CreationCollisionOption.OpenIfExists);
            _value = await FileIO.ReadTextAsync(file);
            if (string.IsNullOrWhiteSpace(_value))
            {
                _value = Guid.NewGuid().ToString();
                await FileIO.WriteTextAsync(file, _value);
            }


            return _value;
        }
    }
}

And this is the JavaScript code for an insert event handler for an added Devices table, which would replace the Channel table:

function insert(item, user, request) {
    // we don't trust the client, we always set the user on the server
    item.userId = user.userId;
    // require an installationId
    if (!item.installationId || item.installationId.length === 0) {
        request.respond(400, "installationId is required");
        return;
    }
    // find any records that match this device already (user and installationId combo)
    var devices = tables.getTable('devices');
    devices.where({
        userId: item.userId,
        installationId: item.installationId
    }).read({
        success: function (results) {
            if (results.length > 0) {
                // This device already exists, so don't insert the new entry,
                // update the channelUri (if it's different)
                if (item.channelUri === results[0].channelUri) {
                    request.respond(200, results[0]);
                    return;
                }
                // otherwise, update the notification id 
                results[0].channelUri = item.channelUri;
                devices.update(results[0], {
                    success: function () {
                        request.respond(200, results[0]);
                        return;
                    }
                });
            }
            else
            {
                request.execute();
            }
        }
    });
}

Accommodating the added class, as well as a new table and event handler, requires considerable effort to modify the original code. Simply deleting all existing rows of the Channel and, optionally, the TodoItem table(s) solved the problem for me, at least temporarily.

If I encounter multiple notifications for individual ToDo Item entries, I’ll update the source code with the preceding class and event handler.


0 comments: