Push Notifications with SharePoint 2013-based Windows Phone apps

Setting up push notifications for your SharePoint Windows Phone app is fairly straight-forward. Push notifications with SharePoint uses the same Microsoft Push Notification Service (MSPNS) to create a communications channel from the server to the device. The following code sets up an MSPNS channel:

public void Register(ClientContext context)
{
    _currentContext = context;

    HttpNotificationChannel channel = HttpNotificationChannel.Find(CHANNEL_NAME);

    if (channel == null)
    {
        channel = new HttpNotificationChannel(CHANNEL_NAME);

        channel.ChannelUriUpdated += new EventHandler<NotificationChannelUriEventArgs>(channel_ChannelUriUpdated);
        channel.ErrorOccurred += new EventHandler<NotificationChannelErrorEventArgs>(channel_ErrorOccurred);
        channel.ShellToastNotificationReceived += new EventHandler<NotificationEventArgs>(channel_ShellToastNotificationReceived);

        channel.Open();
        channel.BindToShellToast();
    }
    else
    {
        channel.ChannelUriUpdated += new EventHandler<NotificationChannelUriEventArgs>(channel_ChannelUriUpdated);
        channel.ErrorOccurred += new EventHandler<NotificationChannelErrorEventArgs>(channel_ErrorOccurred);
        channel.ShellToastNotificationReceived += new EventHandler<NotificationEventArgs>(channel_ShellToastNotificationReceived);
    }
}

 

If a channel hasn't yet been created, then we create it, add event handlers for events that we need to handle and then open the channel. The event handler ShellToastNotificationReceived is used so that we can handle the notification while the app is running. If the app is not running, the toast will be handled automatically by calling BindToShellToast().

When MSPNS has created a channel and assigned it a URI, we need to then create a notification subscription in SharePoint:

void channel_ChannelUriUpdated(object sender, NotificationChannelUriEventArgs e)
{
    Web web = _currentContext.Web;
    string channelUri = e.ChannelUri.ToString();

    PushNotificationSubscriber subscriber;

    if (!web.DoesPushNotificationSubscriberExist(App.Settings.AppDeviceID).Value)
    {
        subscriber = web.RegisterPushNotificationSubscriber(App.Settings.AppDeviceID, channelUri);
    }
    else
    {
        subscriber = web.GetPushNotificationSubscriber(App.Settings.AppDeviceID);
        subscriber.ServiceToken = channelUri;
    }

    subscriber.SubscriberType = SUBSCRIBER_TYPE;
    subscriber.Update();

    _currentContext.ExecuteQueryAsync(null,null);
}

 

First we check to see if the notification subscriber already exists. Subscriptions are stored in a list in SharePoint called PushNotificationSubscriptionStore. This is a hidden list that you can't get to through the UI; however, it does show up in the ListData.svc OData service. To see what fields are in the list, review the service's metadata (ex/ http://mobile-apps.fabrikam.com/sites/Team%20Site/_vti_bin/listdata.svc/$metadata). This list contains PushNotificationSubscriptionStoreItem items. The DeviceIdentifier field is what you can use to store the GUID that was generated for the device/app and the ServiceToken field should be used to store the channel URI. Also note that you should probably come up with distinct SubscriberType values. Since all of these notification subscriptions are stored in a single list, this is the simplest way for you to distinguish which notification to send for each application notification. Once these fields are set, we use ClientContext.ExecuteQueryAsync() to save the values.  One other thing to mention, don’t forget to activate the Push Notifications site feature on the site where you’ll be using push notifications (this should actually be the first thing you do).

To handle unregistering for the push notification, you need to unregister the notification subscription from SharePoint:

private void ButtonClear_Click(object sender, RoutedEventArgs e)
{
    App.Settings.ClearSettings();
    PushNotificationRegistration registration = new PushNotificationRegistration();
    registration.Unregister();
}

 

The Unregister method of my PushNotificationRegistration class closes and disposes the channel I created when the user initially registered:

public void Unregister()
{
    HttpNotificationChannel channel = HttpNotificationChannel.Find(CHANNEL_NAME);
    if (channel != null)
    {
        channel.Close();
        channel.Dispose();
    }
}

Once this is ready, then on the server, all you have to do is to find all the notification subscribers that you need to send a notification to and send the notification out using the same code you'd normally use to send push notifications out to other Windows Phone apps. For this app, I decided to handle this in an Event Receiver and send notifications out when an item is added or updated in the Products list:

public class ProductsListEventReceiver : SPItemEventReceiver
{
    /// <summary>
    /// An item is being added.
    /// </summary>
    public override void ItemAdding(SPItemEventProperties properties)
    {
        base.ItemAdding(properties);
        SendNotification("New Product Added", properties); 
    }

    /// <summary>
    /// An item is being updated.
    /// </summary>
    public override void ItemUpdating(SPItemEventProperties properties)
    {
        base.ItemUpdating(properties);
        SendNotification("Product Updated", properties); 
    }

    private void SendNotification(string title, SPItemEventProperties properties)
    {
        SPPushNotificationSubscriberCollection subscribers = properties.Web.PushNotificationSubscribers;
        foreach (SPPushNotificationSubscriber subscriber in subscribers)
        {
            if (!string.IsNullOrEmpty(subscriber.SubscriberType) && subscriber.SubscriberType.Equals(
"ProductAlerts", StringComparison.InvariantCultureIgnoreCase))
            {
                HttpWebRequest sendNotificationRequest = (HttpWebRequest)WebRequest.Create(subscriber.ServiceToken);
                sendNotificationRequest.Method = "POST";
                string toastMessage = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
                "<wp:Notification xmlns:wp=\"WPNotification\">" +
                "<wp:Toast>" +
                "<wp:Text1>" + title + "</wp:Text1>" +
                "<wp:Text2>" + properties.ListItem.Title + "</wp:Text2>" +
                "</wp:Toast> " +
                "</wp:Notification>";
                byte[] notificationMessage = Encoding.Default.GetBytes(toastMessage);
                sendNotificationRequest.ContentLength = notificationMessage.Length;
                sendNotificationRequest.ContentType = "text/xml";
                sendNotificationRequest.Headers.Add("X-WindowsPhone-Target", "toast");
                sendNotificationRequest.Headers.Add("X-NotificationClass", "2");
                try
                {
                    using (Stream requestStream = sendNotificationRequest.GetRequestStream())
                    {
                        requestStream.Write(notificationMessage, 0, notificationMessage.Length);
                    }
                    HttpWebResponse response = (HttpWebResponse)sendNotificationRequest.GetResponse();
                }
                catch
                {
                    //TODO: log error 
                }
            }
        }
    } 

}
 

Note that it's probably not a good idea to do a foreach loop over the SPPushNotificationSubscriberCollectionif there's going to be thousands of items in that list. I did this for the sake of simplicity. Also, LINQ to SharePoint won't work on this list. The only way you might be able to do a more efficient filtering is through a CAML query, though I didn't test this.

Once this feature is installed and the user has registered for push notifications in this app, the results look like this:

toast

One thing to be aware of is that with Windows Phone apps, there's no event notification for when your app is uninstalled. Given this, you probably need some way to determine on the server if a notification subscription should be deleted (if it hasn't been used in a while). Otherwise, you might be flooding MPNS with attempts to send a notification to a channel that is no longer accessible.

If you want the full source code to this, please tweet a link to this post and then send me an email (bart.tubalinal at deviantpoint dot com). I will then email you the source code for both the phone app project and the event receiver project.

Building Windows Phone apps with SharePoint 2013

With SharePoint 2013, Microsoft has included a set of Windows Phone assemblies that will allow you to quickly build native Windows Phone applications that can communicate and interact with data in SharePoint. The two assemblies you need to reference in your WP application are Microsoft.SharePoint.Client.Phone and Microsoft.SharePoint.Client.Phone.Runtime. Both assemblies are located in C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\15\TEMPLATE\LAYOUTS\ClientBin.

Authentication to the SharePoint site is handled using the Authenticator class. Windows Phone apps with SharePoint supports four authentication modes: Windows Authentication (Default), Forms Authentication, Anonymous, and using Microsoft Online. I have not tested the Microsoft Online-based Authentication but I believe it's used in conjunction with the Microsoft.SharePoint.Client.BrowserLogin class (in the Runtime assembly), which is an application page that will load when using this type of authentication.

For Windows Authentication, note that either Forefront UAG must be used or the web application must be configured to use Basic Authentication.

The following is an example of retrieving data from a SharePoint list:

public void LoadData() 
{ 
    currentDispatcher = Application.Current.RootVisual.Dispatcher; 
    //obviously, these shouldn't be hard-coded. 
    ClientContext context = new ClientContext(new Uri("http://url-to-sharepoint-site")); 
    context.Credentials = new Authenticator("Username", "Password", 
    "Domain"); 
    Site site = context.Site; 
    Web web = site.RootWeb; 
    List productsList = web.Lists.GetByTitle("Products"); 
    CamlQuery camlQuery = new CamlQuery(); 
    camlQuery.ViewXml = "<View><RowLimit>100</RowLimit></View>"; 
    productItems = productsList.GetItems(camlQuery); 
    context.Load(productItems); 
    context.ExecuteQueryAsync(ClientRequestSucceededEventHandler, ClientFailedEventHandler); 
    this.IsDataLoaded = true; 
} 

 

If you've done any coding with CSOM or the SharePoint Javascript object model, the above code should be very familiar. It uses the same classes like ClientContext, Site, Web, etc to interact with SharePoint. Like all Windows Phone apps, requests must be done asynchronously to prevent locking the UI thread – which means you need to use ExecuteQueryAsync instead of ExecuteQuery or you will get an Exception.

The important part of the code is that the ClientContext.Credentials is set to an instance of the Authenticator class.

This is what the success handler looks like:

void ClientRequestSucceededEventHandler(object sender, ClientRequestSucceededEventArgs e) 
{ 
    currentDispatcher.BeginInvoke(() => 
    { 
        foreach (var item in productItems) 
        { 
            string trimmedDescription = item["Description"].ToString(); 
            if (trimmedDescription.Length > 150) trimmedDescription = trimmedDescription.Substring(0, 149) + " ..."; 
            this.Items.Add( 
                new ItemViewModel() 
                { 
                    Title = item["Title"].ToString(), 
                    Description = trimmedDescription, 
                    ImageUrl = new Uri((item["Image"] as FieldUrlValue).Url, UriKind.Absolute), 
                    Color = item["Color"].ToString() 
                } 
            ); 
        } 
    }); 
    this.IsDataLoaded = true; 
} 

 

Again, this handler is typical of what you would use with CSOM/JavaScript. When this handler executes, we need to move back to the UI thread to add these items and for it to display in our view. That's what the line currentDispatcher.BeginInvoke(() => {}) is for.

When the code executes, this is what the Phone app looks like:

all-products

This data is being pulled from a SharePoint list:

image

With Windows Phone apps, there is full support for CRUD operations on list data. This also includes external lists. There is also support for push notifications, which I'll cover in the next blog post. I’ll also be sharing the full source code for this little app in the next post.

Cannot modify SharePoint list views with tracing enabled

I was debugging an issue with a heavily customized SharePoint site where users were unable to save any changes they made to SharePoint list views. Since this was a heavily customized site with several custom list definitions, my initial thought was that we may have botched a list definition or two and made these lists and list views non-modifiable. However, I also tested creating an out of the box SharePoint List and found out that its views were not modifiable as well. In order to isolate the issue, I created a second web application with a simple Team Site site collection. In this web app/site collection, the list views were all modifiable so I knew that the issue was definitely related to something in our particular web application.

The next thing I did was check the ULS logs. In the ULS logs, I noticed the following entries immediately posted after attempting to modify a list view:

09/26/2011 10:39:40.52     w3wp.exe (0x1790)                           0x0EB8    SharePoint Foundation             Monitoring                        nasq    Medium      Entering monitored scope (Request (POST:http://test-sp:80/_vti_bin/owssvr.dll?CS=65001))     
09/26/2011 10:39:40.52     w3wp.exe (0x1790)                           0x0EB8    SharePoint Foundation             Logging Correlation Data          xmnv    Medium      Name=Request (POST:http://test-sp:80/_vti_bin/owssvr.dll?CS=65001)    9446a0f0-1577-472e-a8af-a552b1b4cc39
09/26/2011 10:39:40.60     w3wp.exe (0x1790)                           0x173C    SharePoint Foundation             General                           af71    Medium      HTTP Request method: POST    9446a0f0-1577-472e-a8af-a552b1b4cc39
09/26/2011 10:39:40.60     w3wp.exe (0x1790)                           0x173C    SharePoint Foundation             General                           af75    Medium      Overridden HTTP request method: POST    9446a0f0-1577-472e-a8af-a552b1b4cc39
09/26/2011 10:39:40.60     w3wp.exe (0x1790)                           0x173C    SharePoint Foundation             General                           af74    Medium      HTTP request URL: /_vti_bin/owssvr.dll?CS=65001    9446a0f0-1577-472e-a8af-a552b1b4cc39
09/26/2011 10:39:40.69     w3wp.exe (0x1790)                           0x173C    SharePoint Foundation             General                           b9y3    High        Failed to open the file 'C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\Resources\wss.en-US.resx'.    9446a0f0-1577-472e-a8af-a552b1b4cc39
09/26/2011 10:39:40.69     w3wp.exe (0x1790)                           0x173C    SharePoint Foundation             General                           b9y4    High        #20015: Cannot open "": no such file or folder.    9446a0f0-1577-472e-a8af-a552b1b4cc39
09/26/2011 10:39:40.69     w3wp.exe (0x1790)                           0x173C    SharePoint Foundation             General                           b9y4    High        (#2: Cannot open "": no such file or folder.)    9446a0f0-1577-472e-a8af-a552b1b4cc39
09/26/2011 10:39:40.69     w3wp.exe (0x1790)                           0x173C    SharePoint Foundation             General                           b9y9    High        Failed to read resource file "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\Resources\wss.en-US.resx" from feature id "(null)".    9446a0f0-1577-472e-a8af-a552b1b4cc39
09/26/2011 10:39:40.69     w3wp.exe (0x1790)                           0x173C    SharePoint Foundation             General                           8e26    Medium      Failed to open the language resource keyfile wss.    9446a0f0-1577-472e-a8af-a552b1b4cc39
09/26/2011 10:39:40.69     w3wp.exe (0x1790)                           0x173C    SharePoint Foundation             General                           b9y3    High        Failed to open the file 'C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\Resources\wss.resx'.    9446a0f0-1577-472e-a8af-a552b1b4cc39
09/26/2011 10:39:40.69     w3wp.exe (0x1790)                           0x173C    SharePoint Foundation             General                           b9y4    High        #20015: Cannot open "": no such file or folder.    9446a0f0-1577-472e-a8af-a552b1b4cc39
09/26/2011 10:39:40.69     w3wp.exe (0x1790)                           0x173C    SharePoint Foundation             General                           b9y4    High        (#2: Cannot open "": no such file or folder.)    9446a0f0-1577-472e-a8af-a552b1b4cc39
09/26/2011 10:39:40.69     w3wp.exe (0x1790)                           0x173C    SharePoint Foundation             General                           b9y9    High        Failed to read resource file "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\Resources\wss.resx" from feature id "(null)".    9446a0f0-1577-472e-a8af-a552b1b4cc39
09/26/2011 10:39:40.69     w3wp.exe (0x1790)                           0x173C    SharePoint Foundation             General                           8e26    Medium      Failed to open the language resource keyfile wss.    9446a0f0-1577-472e-a8af-a552b1b4cc39
09/26/2011 10:39:40.69     w3wp.exe (0x1790)                           0x173C    SharePoint Foundation             General                           8l3c    Medium      Localized resource for token 'multipages_direction_dir_value%>' could not be found for file with path: "(unavailable)".    9446a0f0-1577-472e-a8af-a552b1b4cc39
09/26/2011 10:39:40.73     w3wp.exe (0x1790)                           0x0EB8    SharePoint Foundation             Monitoring                        b4ly    Medium      Leaving Monitored Scope (Request (POST:http://test-sp:80/_vti_bin/owssvr.dll?CS=65001)). Execution Time=206.33069286739    9446a0f0-1577-472e-a8af-a552b1b4cc39

The key errors I noticed were the fact that, for some reason, certain resource (.resx) files couldn’t be loaded. These errors were quite strange. First, I wasn’t sure why these resource files were being loaded in the first place. Second, I could not understand why SharePoint was trying to load the files from C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\Resources\. These resource files don’t reside there; they are located under the application’s App_GlobalResources directory. But even manually placing the correct resource files in the Resources directory where the application was attempting to load from didn’t resolve the issue.

A little bit of research led me to a post by Ivan Neganov entitled ‘Writing Trace Output to ULS Log in SharePoint 2010’. In the post, Ivan describes a little caveat that enabling ASP.NET tracing caused some SharePoint instability, namely the inability to create a new web part page and the inability to open up a SharePoint site with tracing enabled with SharePoint Designer. Taking that clue, I removed the system.web/tracing element from our application’s web.config and, sure enough, that resolved the issue.

So there seems to be a little laundry of things broken by ASP.NET tracing in SharePoint 2010. Have any of you had any other issues you’ve encountered?

About the author

Bart X. Tubalinal is a Solutions Architect with over 10+ years experience in building enterprise applications. He also considers himself to be, pound for pound, one of the best developers there is.

Archives

Comments

Comment RSS