How can you implement and test WNS push notifications for your Windows Store app and how can you use the Windows 8 push notification service without azure?

I was playing with the Push and periodic notifications client-side sample in the Dev Center and thought I would share my simple ASP.Net application files to show you how to complete this sample and see it work.  After reading this you should have a good idea of what you need to do to get WNS push notifications working!

First you need an IIS server that can run ASP.Net.  My Windows 8 machine is on steroids so I simply enabled the Hyper-V client in Windows 8 and installed Windows 8 server on it from the downloadable ISO image.  I then enabled the IIS and ASP.Net roles on it and installed Visual Studio 2012.  You could also simply use an existing IIS install and create the same pages I did on it.  Once you get that set up, you need just a little code on it and you have to follow the steps outlined in this document: Using push notifications.

Details:

Install Windows 8 Server IIS, ASP.Net and Visual Studio if you don’t already have that setup somewhere.  (If you use a different version of ASP.Net you are on your own)

Download the Push and periodic notifications client-side sample (I will use the javaScript version) and open it in Visual Studio 2012 on your Windows 8 machine.

These next steps will allow you to give the sample a unique ID so that your registration process will notify your app and only your app:

Open the package.appxmanifest by double clicking it, click on the Packaging tab and make the following changes:

Change Package Display Name to a different name like MyPushNotificationSample (optional actually)

Change the Publisher to your certificate by clicking on Choose Certificate… and then select from the Configure Certificate… list box and select the same certificate that you would normally use when creating a new app (automatically set for you) or create a test certificate.

Finally add these capabilities so that you can ensure you can hit a local network or remote server for the notification channel you will push out:

1602_Capabilities_2

Now get the secret information we need!

How to authenticate with the Windows Push Notification Service (WNS)

This walks you through getting a Security Identifier (SID) and secret key (client secret) for you app. NOTE: the CN= is used in generating the channel URI later on. It comes from your certificate and package name. If you change the cert and the CN is different, or the package name then you need to do this again!

When you register your application then take the information and use it in your application and web service.  NOTE:  You will need to use the temporary portal.  See Registering your app through the temporary portal  in the remarks section.  (https://manage.dev.live.com/build)

The registration process will notify you after you entered the Package name and publisher and give you information similar to this:

2671_Congrats_1 

So… put the package name in the Push Notification Sample manifest and you will use the other two pieces of info on your ASP.Net application.

I created two ASP.Net pages.  NOTE:  This is a Windows8 server.  If you are using a different version of ASP.NET you may have to modify your code!One to add the notification channel to my web server so I could do it from the sample app (AddNotificationChannel.aspx) and one to fire off a simple notification message ( SendPushNotification.aspx).

NOTE:  This is a really simple implementation.  It does not allow for anything than a simple one line message and does NOT PERSIST THE NotificationChannel.  You would need to persist the Notification Channel in something like a SQL Db so that if your IIS process restarts this is persisted.  You also need to test, test … and oh ya, did I mention TEST, before even thinking about putting his into production.

Here is the code for setting the NotificationChannelURI (called from the sample app):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class AddNotificationChannel : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (Request.HttpMethod != "POST")
        {
            Response.StatusCode = 405;
            Response.StatusDescription = "Incorrect syntax";
            Response.Write("You should use this in a POST to set the channel URI");
            Response.End();
        }
        else
        {
            byte[] aByteReq = new byte[Request.ContentLength];

            Request.InputStream.Read(aByteReq, 0, Request.ContentLength);

            string aStrReq = HttpUtility.UrlDecode(System.Text.UTF8Encoding.UTF8.GetString(aByteReq));
            int i = aStrReq.IndexOf("channelUri=");
            if (i == 0)
            {
                aStrReq = aStrReq.Substring(i + 11);
                //aStrReq = Uri.UnescapeDataString(aStrReq);
                Response.Write("Successfully got channelUri: " + aStrReq);
                if (Application["channelUri"] != null)
                {
                    Application["channelUri"] = aStrReq;
                }
                else
                {
                    Application.Add("channelUri", aStrReq);
                }
            }
            else
            {
                Response.Write("Could not find 'channelUri=' in your posted data.  This is required: " + aStrReq);
            }
            Response.End();

        }
    }
}

Here is the code for Sending the PushNotification (obviously replace the secretInfo region with YOUR info from above):

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class SendPushNotification : System.Web.UI.Page
{

    #region secretInfo
    ///
    /// <secretinfo>
    ///
private string sid = "ms-app://s-1-15-2-2313887820-33830XXX45-1832077996-2334248381-426615021-900790940-2305730494";
    private string secret = "ugd-mJ2QqyehszI1IKlsxxxxHcwJoAb4 ";
    private string accessToken = "";
    /// </secretinfo>
    #endregion

  [DataContract]
    public class OAuthToken
    {
        [DataMember(Name = "access_token")]
        public string AccessToken { get; set; }
        [DataMember(Name = "token_type")]
        public string TokenType { get; set; }
    }

    OAuthToken GetOAuthTokenFromJson(string jsonString)
    {
        using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(jsonString)))
        {
            var ser = new DataContractJsonSerializer(typeof(OAuthToken));
            var oAuthToken = (OAuthToken)ser.ReadObject(ms);
            return oAuthToken;
        }
    }

    protected void getAccessToken()
    {
        var urlEncodedSid = HttpUtility.UrlEncode(String.Format("{0}", this.sid));
        var urlEncodedSecret = HttpUtility.UrlEncode(this.secret);

        var body =
          String.Format("grant_type=client_credentials&client_id={0}&client_secret={1}&scope=notify.windows.com", urlEncodedSid, urlEncodedSecret);

        var client = new WebClient();
        client.Headers.Add("Content-Type", "application/x-www-form-urlencoded");

        string response = client.UploadString("https://login.live.com/accesstoken.srf", body);
        var oAuthToken = GetOAuthTokenFromJson(response);
        this.accessToken = oAuthToken.AccessToken;
    }

    protected string PostToCloud(string uri, string xml, string type = "wns/tile")
    {
        try
        {
            if (accessToken == "")
            {
                getAccessToken();
            }
            byte[] contentInBytes = Encoding.UTF8.GetBytes(xml);

            WebRequest webRequest = HttpWebRequest.Create(uri);
            HttpWebRequest request = webRequest as HttpWebRequest;
            webRequest.Method = "POST";

            webRequest.Headers.Add("X-WNS-Type", type);
            webRequest.Headers.Add("Authorization", String.Format("Bearer {0}", accessToken));

            Stream requestStream = webRequest.GetRequestStream();
            requestStream.Write(contentInBytes, 0, contentInBytes.Length);
            requestStream.Close();

            HttpWebResponse webResponse = (HttpWebResponse)webRequest.GetResponse();
            LogResponse(webResponse);
            return webResponse.StatusCode.ToString();
        }
        catch (WebException webException)
        {
            string exceptionDetails = webException.Response.Headers["WWW-Authenticate"];
            if ((exceptionDetails != null) && exceptionDetails.Contains("Token expired"))
            {
                getAccessToken();
                OutPutInfo("Updated access token");
                return PostToCloud(uri, xml, type);
            }
            else
            {
                LogResponse(webException.Response);
                return "EXCEPTION: " + webException.Message;
            }
        }
        catch (Exception ex)
        {
            return "EXCEPTION: " + ex.Message;
        }
    }

    protected void OutPutInfo(string info)
    {
        //TODO: log information in debug build
    }

    protected void LogResponse(WebResponse info)
    {
        //TODO: log information in debug build
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        if (Application["channelUri"] != null)
        {
            if (Request.QueryString["message"] != null)
            {
                string aStrReq = Application["channelUri"] as string;
                string xml = "<?xml version=\"1.0\" encoding=\"utf-16\"?><tile><visual lang=\"en-US\"><binding template=\"TileWideText01\"><text id=\"1\">" + Request.QueryString["message"] as string + "</text><text id=\"2\"></text><text id=\"3\"></text><text id=\"4\"></text><text id=\"5\"></text></binding></visual></tile>";

                Response.Write("Sending notification message: " + Request.QueryString["message"] + " to 'channelUri='" + aStrReq);
                Response.Write("Result: " + PostToCloud(aStrReq, xml));

            }
            else
            {
                Response.Write("you did not add a ?message='your message' to the query string!");
            }
        }
        else
        {
            Response.Write("Application 'channelUri=' has not been set yet");
        }
        Response.End();

    }
}

Build your ASP.Net app and make sure you can hit these pages.

Putting it to the test!

Now that you have written all this great code, let’s test it.

Fire up the sample app and go to the page, Enter your server name and page you created and hit the ‘Reopen…’ button:

6038_image_thumb_1E785294

Then start listening by changing to scenario 2 and hit the ‘Start listening…’ button:

8508_image_thumb_49DC738E

Finally send the message to your server (use your server name obviously) by posting a URL into IE similar to this (from any computer btw that can hit your server):

http://windows8server/SendPushNotification.aspx?message=Jeff Rocks!

You should get confirmation that the message was sent: Sending notification message: Jeff Rocks! to 'channelUri='https://db3.notify.windows.com/?token=AQQAAADoVfo3q0rtFdvOeAPukHKlabLr9Ml0OOYoC8MWFG2iMJ7ffcA47EgF8XcQxFcadJ%2faQqo086M4a0vYTRfA9MI4NmgVkdnifByFghJFY0G5TKbNRMo%2fMkm8Eec%2fbLmYq9w%3dResult: OK

And the test app will show success!

1667.image_08CDF42A

Let me know if this works for you and if you found it useful!  Comments are welcome, both below and on twitter: #WSDevSol.