Category Archives: Windows Phone 7

Back button broken for URL hashes in IE on WP7 Mango

I’ve been on a little twitter rant recently about this, and thought I’d use my blog as another distribution medium.

Feel free to leave comments here, here (connect forum) or here (jQuery Mobile forum).

Here’s the content of my connect forum post on the matter:

Mango seems to have brought along with it a broken browser…

When you link to an anchor on your page using a hash, the back button doesn’t seem to work too well.

For example, go to http://jquerymobile.com/demos/1.0b1/. Those in the know will realise why… To put it simply, jQuery Mobile uses hash changes to navigate between “pages” in a single or multiple page web application. It uses the hash changed events to determine when to use javascript to request the next page, load it into the DOM and perform an animation to display the next page.

Navigating forward on a jQuery Mobile site works perfectly as expected. It’s when you hit the back button that things go wrong. On a desktop browser, going from the landing page to “Into to jQuery Mobile” swipes in the intro page and hitting back swipes it out again to show the landing page. However, on Mango, hitting the back button when on the intro page does nothing. If you were on another site before you went the the demo landing page, then hitting the back button again will take you back to the previous site.

I have been working on an embedded web application recently and this has been driving me crazy! I’m seeing a Navigated event being raised by the WebBrowser control when it goes to the second page in the application, but back buttons (and manually invoking history.back() and history.go(-1) in javascript) just don’t do anything.

This seems to be a breaking change to me and would potentially mean I push to have Windows Phone 7 dropped as a targetted platform for the (relatively high traffic) website I am working on. An embarrassing idea seeing as I pushed for it to be targetted… 😦

UPDATE 14 July 2011 – It’s been noted that the jQuery Mobile docs site mentioned does work in previous builds of WP7, running IE 7 Mobile. This verifies that this issue has been introduced with IE9 Mobile on WP7 Mango.

UPDATE II 14 July 2011 – The issue is a little deeper than I initially realized. If you define a simple page with an anchor at the top of the page referencing a div at the bottom of the page then we see some more exotic behaviour. Clicking the link and hitting back will work the first time. However, you’ll notice that in the address bar the hash is not removed from the URL. This means that clicking the link and hitting back again will get the browser into a tangle. Here is the HTML snippet I used:

<a href="#bottom">bottom</a>
<div style="height: 1000px"></div>
<a id="bottom" href="#" onclick="javascript:history.back();return false;">back</a>

UPDATE III October 2011 – The issue appears to have been fixed in Mango RTM.

Advertisement

A Day in the Life of a Metro-veloper

This is a follow-up post to my Windows Phone 7 presentation last week at the SDDN.

I have uploaded my powerpoint deck to SlideShare.

Some of the sample apps I used are available on MSDN.

And there was a recent video on youtube of an awesome golfing app that really shows the power of the WP7 UX.

push notifications in windows phone 7

Basics

There’s lots of information hiding within about 10 links on MSDN about how push notifications for WP7. The short of it is the phone opens a HttpNotificationChannel which gives it a unique URI for your applications to send notifications to it.

There are three types of notifications – tile, toast and raw. Tile notifications are used for changing the background, count and title of the application when it is pinned to the Start screen. Toast notifications provide unobtrusive notifications to users when they are outside the application, allowing them to step in easily to perform an action. Raw notifications are messages of any format that can be sent to the phone application and received while it is active.

The problem with the information out there is that it is slightly conflicting because of the recent changes to the APIs with different versions of the toolkit. As a result, I spent many hours trying to get this relatively simple piece of functionality working. At one point I decided that whenever I got something working I’d try shrink wrap it and publish it, so here we go…

DISCLAIMER: WOMM…

WARNING: There’s a lot of code here… Please read the Client Side and Server Side sections before believing you are qualified to download the sample. The sample has a thin WPF client implementation over the server side code to make it a little easier to get started. Remember to check the Debug output window for the phone application when you need a Uri to send a notification.

Client Side

My aim was to get the code required for subscribing to notifications to be as simple as possible. I got it down to the following few lines of code:

// in App.ctor
NotificationService notificationService = new NotificationService("some funky channel name");
notificationService.RawNotificationReceived += RawNotificationReceived;
notificationService.ToastNotificationReceived += ToastNotificationReceived;
notificationService.ChannelUriUpdated += ChannelUriUpdated;

// in Application_Launching & Application_Activated
notificationService.Subscribe();

// event handlers
void ToastNotificationReceived(object sender, ToastNotificationReceivedEventArgs e)
{    // example of handling a toast notification within the application
    MessageBox.Show(e.Message, e.Title, MessageBoxButton.OK);
}

void RawNotificationReceived(object sender, RawNotificationRecievedEventArgs e)
{
    MessageBox.Show(e.Message);
}

void ChannelUriUpdated(object sender, NotificationChannelUriEventArgs e)
{
    // call a webservice to report e.ChannelUri
}

There are a couple of things to note. Firstly, the Subscribe() method is called in the Application_Launching and Application_Activated events. This is due to the tombstoning nature of WP7.

Next, there are separate event handlers for Toast and Raw notifications. They are handled differently because toast notifications have a definite payload format that specifies the ability to send to pieces of text (a title and a message). A raw notification can be anything, which is why I have decided to simply surface the string representation of the payload. A toast must also be handled if the application is executing – i.e. it will not be displayed by the OS.

Also, there is no tile notification handler. This is because the OS handles this directly.

Lastly, the ChannelUriUpdated event handler. This is necessary for your phone application to let the server application know that there is a phone waiting for notifications. When this occurs, the phone application should call a web service to register the URI. The NotificationService class will write this information to the Debug console whenever the application is started, so it is not required for debugging purposes.

Server Side

While I was busy working on the client side, I realised that obviously I’d eventually need to have some server side code to send the notifications. Again, I wanted to simplify it as much as possible:

NotificationService service = new NotificationService();

// send a raw notification
service.SendRaw(clientUri, messageText);

// send a toast notification
service.SendToast(clientUri, titleText, messageText);

// send a tile notification
service.SendTile(clientUri, backgroundImageUri, countValue, titleText);

See… simple. Smile

The Real Code

As I mentioned earlier, you can download all this here. Otherwise, feel free to read through this (or just copy/paste it) to get a better understanding of how the HttpNotificationChannel works.

Client side NotificationService implementation:

public class NotificationService
{
    string channelName;
    HttpNotificationChannel channel;

    public NotificationService(string channelName)
    {
        this.channelName = channelName;
    }

    /// <summary>
    /// Subscribes to the notification events on the channel.
    /// If the channel doesn't already exist, it will be created
    /// and bound to shell tile and toasts.
    /// </summary>
    public void Subscribe()
    {
        if (channel == null) BindChannel();
    }

    /// <summary>
    /// Unubscribes from the notification events on the channel.
    /// </summary>
    public void Unsubscribe()
    {
        if (channel != null) UnsubscribeFromChannelEvents();
    }

    /// <summary>
    /// Finds or creates the notification channel and binds the shell tile
    /// and toast notifications as well as events.
    /// </summary>
    private void BindChannel()
    {
        channel = HttpNotificationChannel.Find(channelName);

        if (channel == null || channel.ChannelUri == null)
        {
            if (channel != null) DisposeChannel();

            channel = new HttpNotificationChannel(channelName);
            channel.ChannelUriUpdated += channel_ChannelUriUpdated;
            channel.Open();
        }
        else System.Diagnostics.Debug.WriteLine(channel.ChannelUri.AbsoluteUri);

        SubscribeToChannelEvents();

        if (!channel.IsShellTileBound) channel.BindToShellTile();
        if (!channel.IsShellToastBound) channel.BindToShellToast();
    }

    /// <summary>
    /// Subscribes to the channel's events.
    /// </summary>
    private void SubscribeToChannelEvents()
    {
        channel.ShellToastNotificationReceived += channel_ShellToastNotificationReceived;
        channel.HttpNotificationReceived += channel_HttpNotificationReceived;
        channel.ErrorOccurred += channel_ErrorOccurred;
    }

    /// <summary>
    /// Unsubscribes from the channel's events
    /// </summary>
    private void UnsubscribeFromChannelEvents()
    {
        channel.ShellToastNotificationReceived -= channel_ShellToastNotificationReceived;
        channel.HttpNotificationReceived -= channel_HttpNotificationReceived;
        channel.ErrorOccurred -= channel_ErrorOccurred;
    }

    /// <summary>
    /// Closes the channel and disposes it.
    /// </summary>
    private void DisposeChannel()
    {
        channel.Close();
        channel.Dispose();
        channel = null;
    }

    /// <summary>
    /// Event handler for the ChannelUriUpdate event.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void channel_ChannelUriUpdated(object sender, NotificationChannelUriEventArgs e)
    {
        channel.ChannelUriUpdated -= channel_ChannelUriUpdated;
        System.Diagnostics.Debug.WriteLine(e.ChannelUri.AbsoluteUri);
        OnChannelUriUpdated(e);
    }

    /// <summary>
    /// Raised when the notification channel is given a URI.
    /// </summary>
    /// <remarks>
    /// This is when you would call a web service to tell it that a client is
    /// registered and what the notification URI is.
    /// </remarks>
    public event EventHandler<NotificationChannelUriEventArgs> ChannelUriUpdated;

    /// <summary>
    /// Raises the ChannelUriUpdated event.
    /// </summary>
    /// <param name="e"></param>
    protected virtual void OnChannelUriUpdated(NotificationChannelUriEventArgs e)
    {
        if (ChannelUriUpdated != null) ChannelUriUpdated(this, e);
    }

    /// <summary>
    /// Event handler for the HtppNotificationReceived event.
    /// This is called when a raw notification is received.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void channel_HttpNotificationReceived(object sender, HttpNotificationEventArgs e)
    {
        byte[] bytes;
        using (var stream = e.Notification.Body)
        {
            bytes = new byte[stream.Length];
            stream.Read(bytes, 0, (int)stream.Length);
        }
        var message = Encoding.UTF8.GetString(bytes, 0, bytes.Length);

        OnRawNotificationReceived(message);
    }

    /// <summary>
    /// Occurs when a raw notification is received.
    /// </summary>
    public event EventHandler<RawNotificationRecievedEventArgs> RawNotificationReceived;

    /// <summary>
    /// Raises the RawNotificationReceived event on the UI thread.
    /// </summary>
    /// <param name="message"></param>
    protected virtual void OnRawNotificationReceived(string message)
    {
        Deployment.Current.Dispatcher.BeginInvoke(() =>
        {
            if (RawNotificationReceived != null) 
                RawNotificationReceived(
                    this, 
                    new RawNotificationRecievedEventArgs(message)
                    );
        });
    }

    /// <summary>
    /// Event handler for the ShellToastNotificationReceived event.
    /// This occurs when a toast notification is received on the channel.
    /// </summary>
    /// <remarks>
    /// This must be handled by the application if it is running when a toast is received.
    /// </remarks>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void channel_ShellToastNotificationReceived(object sender, NotificationEventArgs e)
    {
        var title = e.Collection.Values.First();
        var message = e.Collection.Values.Skip(1).FirstOrDefault() ?? string.Empty;
        OnToastNotificationReceived(title, message);
    }

    /// <summary>
    /// Occurs when a toast notification is received.
    /// </summary>
    /// <remarks>
    /// This must be handled by the application if it is running when a toast is received.
    /// </remarks>
    public event EventHandler<ToastNotificationReceivedEventArgs> ToastNotificationReceived;

    /// <summary>
    /// Raises the ToastNotificationReceived event on the UI thread.
    /// </summary>
    /// <param name="title"></param>
    /// <param name="message"></param>
    protected virtual void OnToastNotificationReceived(string title, string message)
    {
        Deployment.Current.Dispatcher.BeginInvoke(() =>
        {
            if (ToastNotificationReceived != null) 
                ToastNotificationReceived(
                    this, 
                    new ToastNotificationReceivedEventArgs(title, message)
                    );
        });
    }

    /// <summary>
    /// Event handler for the ErrorOccurred event.
    /// Handles different events according to ErrorType.
    /// </summary>
    /// <remarks>
    /// Needs more work... ;-(
    /// </remarks>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void channel_ErrorOccurred(object sender, NotificationChannelErrorEventArgs e)
    {
        switch (e.ErrorType)
        {
            // something went severely wrong. lets wait a while before trying again.
            case ChannelErrorType.ChannelOpenFailed:
                DisposeChannel();
                System.Threading.Thread.Sleep(60000);
                BindChannel();
                break;
            // an image uri has been referenced in a notification that was
            // not bound to the shell tile.
            case ChannelErrorType.MessageBadContent:
                break;
            // too many notifications have been received in too short a time span.
            case ChannelErrorType.NotificationRateTooHigh:
                break;
            // a bad payload was received. re-establish the connection to overcome this.
            case ChannelErrorType.PayloadFormatError:
                DisposeChannel();
                BindChannel();
                break;
            // the type notifications we're receiving is going to change.
            case ChannelErrorType.PowerLevelChanged:
                break;
            default:
                break;
        }
    }
}

public class RawNotificationRecievedEventArgs : EventArgs
{
    public string Message { get; private set; }
    public RawNotificationRecievedEventArgs(string message)
    {
        Message = message;
    }
}

public class ToastNotificationReceivedEventArgs : EventArgs
{
    public string Title { get; private set; }
    public string Message { get; private set; }
    public ToastNotificationReceivedEventArgs(string title, string message)
    {
        Title = title;
        Message = message;
    }
}

Server side code:

public interface INotificationService
{
    NotificationResponse SendTile(string uri, string backgroundImageUri, int count, 
                                  string title, [Optional] Guid messageId);
    NotificationResponse SendToast(string uri, string text1, [Optional] string text2, 
                                   [Optional] Guid messageId);
    NotificationResponse SendRaw(string uri, string message, [Optional] Guid messageId);
}

public class NotificationService : INotificationService
{
    const int maxPayloadLength = 1024;

    const string targetHeader = "X-WindowsPhone-Target";
    const string notificationClassHeader = "X-NotificationClass";
    const string messageIdHeader = "X-MessageID";
    const string notificationStatusHeader = "X-NotificationStatus";
    const string subscriptionStatusHeader = "X-SubscriptionStatus";
    const string deviceConnectionStatusHeader = "X-DeviceConnectionStatus";

    const string tileMessageFormat =
        "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
        "<wp:Notification xmlns:wp=\"WPNotification\">" +
            "<wp:Tile>" +
                "<wp:BackgroundImage>{0}</wp:BackgroundImage>" +
                "<wp:Count>{1}</wp:Count>" +
                "<wp:Title>{2}</wp:Title>" +
            "</wp:Tile>" +
        "</wp:Notification>";
    /// <summary>
    /// X-WindowsPhone-Target: token
    /// </summary>
    const string tileTarget = "token";
    /// <summary>
    /// X-NotificationClass: 1
    /// </summary>
    const string tileNotificationClass = "1";

    const string toastMessageFormat =
        "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
        "<wp:Notification xmlns:wp=\"WPNotification\">" +
            "<wp:Toast>" +
                "<wp:Text1>{0}</wp:Text1>" +
                "<wp:Text2>{1}</wp:Text2>" +
            "</wp:Toast>" +
        "</wp:Notification>";
    /// <summary>
    /// X-WindowsPhone-Target: toast
    /// </summary>
    const string toastTarget = "toast";
    /// <summary>
    /// X-NotificationClass: 2
    /// </summary>
    const string toastNotificationClass = "2";

    /// <summary>
    /// X-NotificationClass: 3
    /// </summary>
    const string rawNotificationClass = "3";

    public NotificationResponse SendTile(string uri, string backgroundImageUri, int count, 
                                         string title, [Optional] Guid messageId)
    {
        var str = string.Format(tileMessageFormat, backgroundImageUri, count, title);
        return SendNotification(uri, str, tileNotificationClass, tileTarget, messageId);
    }

    public NotificationResponse SendToast(string uri, string text1, [Optional] string text2, 
                                          [Optional] Guid messageId)
    {
        var str = string.Format(toastMessageFormat, text1, text2);
        return SendNotification(uri, str, toastNotificationClass, toastTarget, messageId);
    }

    public NotificationResponse SendRaw(string uri, string message, 
                                        [Optional] Guid messageId)
    {
        return SendNotification(uri, message, rawNotificationClass, messageId: messageId);
    }

    private NotificationResponse SendNotification(string uri, string message, 
                                                  string notificationClass, 
                                                  [Optional] string target, 
                                                  [Optional] Guid messageId)
    {
        var payload = Encoding.UTF8.GetBytes(message);
        if (payload.Length > maxPayloadLength) 
            throw new ArgumentException(
                "The message provided is longer than the maximum payload length (1024B).",
                message
                );

        var sendNotificationRequest = WebRequest.Create(uri) as HttpWebRequest;
            
        sendNotificationRequest.Method = WebRequestMethods.Http.Post;
        sendNotificationRequest.ContentLength = payload.Length;
        sendNotificationRequest.ContentType = "text/xml";

        // X-NotificationClass: 1, 2, 3
        sendNotificationRequest.Headers.Add(notificationClassHeader, notificationClass);
        // X-WindowsPhone-Target: token, toast
        if (!string.IsNullOrWhiteSpace(target)) 
            sendNotificationRequest.Headers.Add(targetHeader, target);
        // X-MessageId: 00000000-0000-0000-0000-000000000000
        if (messageId != null && messageId != Guid.Empty) 
            sendNotificationRequest.Headers.Add(messageIdHeader, messageId.ToString());

        using (var requestStream = sendNotificationRequest.GetRequestStream())
            requestStream.Write(payload, 0, payload.Length);

        HttpWebResponse response;
        string errorMessage = null;
        try
        {
            response = sendNotificationRequest.GetResponse() as HttpWebResponse;
        }
        catch (WebException ex)
        {
            response = ex.Response as HttpWebResponse;
            errorMessage = ex.Message;
        }
        return new NotificationResponse
        {
            MessageId = response.Headers[messageIdHeader],
            ErrorMessage = errorMessage,
            NotificationStatus = response.Headers[notificationStatusHeader],
            SubscriptionStatus = response.Headers[subscriptionStatusHeader],
            DeviceConnectionStatus = response.Headers[deviceConnectionStatusHeader],
            StatusCode = response.StatusCode
        };
    }
}

public class NotificationResponse
{
    public string ErrorMessage { get; set; }
    public DateTimeOffset Timestamp { get; set; }
    public string MessageId { get; set; }
    public HttpStatusCode StatusCode { get; set; }
    public string NotificationStatus { get; set; }
    public string DeviceConnectionStatus { get; set; }
    public string SubscriptionStatus { get; set; }
}

DOWNLOAD IT NOW!

Remember, to get the Uri just watch the Debug output window when your WP7 application starts up.

Good luck Smile

[Update 2010-08-27] Added extra check to Client.NotificationService.BindChannel() so that a channel that already exists but does not have a URI is disposed and a new one is created.