다음을 통해 공유


Tutorial: Dynamic Tile Push Notification for Windows Phone 7

As I mentioned in my last post, I wanted to update my Tip Express application with dynamic tile notifications.  The basic idea is very simple: push the user data from the application to the pinned tile on the home page using the background theme the user has chosen (this isn’t exactly a killer feature for this application, but it gives me a good excuse to play with channel notifications).

There is plenty of overview information on the various types of notifications (Raw, Toast, and Tile) so I won’t repeat that here.  In my case I care about Tile notifications and will concentrate on that.  The concepts themselves are not that complicated but there are a lot of components to keep track of.  I found it was helpful to create a one page diagram of the flow:

image

Now let’s walk through the various components and look at some source code (note that I have borrowed code snippets from some of the links I posted above to create my sample code).

Step 1:  Establish a Channel on the Client Device

The first step happens in our application by establishing a channel with the Push Client running on the device using the HttpNotificationChannel class.  The most important piece of data from this process is the ChannelUri which will be used to identify the device on the network – all communication back to the phone will require this value.  I also found it handy to create a unique Guid for the device which could be used by my service to index each device uniquely (the URI is quite long). 

Once the channel is created you will be able to Find it on subsequent execution of your application.  With a valid Channel, we’ll save off the ChannelUri and add some event handlers:

    1: public string ChannelName = "MyAppChannel";
    2: public Uri ChannelUri = null;
    3: HttpNotificationChannel Channel = null;
    4:  
    5: // Step 1:  Setup the channel with the push service.  We'll get a URI required for future communications.
    6: private void SetupChannel()
    7: {
    8:     Channel = HttpNotificationChannel.Find(ChannelName);
    9:     if (Channel == null)
   10:     {
   11:         Channel = new HttpNotificationChannel(ChannelName);
   12:         Channel.ChannelUriUpdated += new EventHandler<NotificationChannelUriEventArgs>(Channel_ChannelUriUpdated);
   13:         Channel.ErrorOccurred += new EventHandler<NotificationChannelErrorEventArgs>(Channel_ErrorOccurred);
   14:         Channel.Open();
   15:     }
   16:     else
   17:     {
   18:         Channel.ChannelUriUpdated += new EventHandler<NotificationChannelUriEventArgs>(Channel_ChannelUriUpdated);
   19:         Channel.ErrorOccurred += new EventHandler<NotificationChannelErrorEventArgs>(Channel_ErrorOccurred);
   20:         ChannelUriSetup(Channel.ChannelUri);
   21:     }
   22: }
   23:  
   24: private void ChannelUriSetup(Uri uri)
   25: {
   26:     ChannelUri = uri;
   27: }

In my application I want to generate my own background tile (which I will host on my server) and have the Tile bound to that image.  We’ll use Channel.BindToShellTile() to accomplish this, passing in my domain name (“https://www.tipexpress.net/”) as the URI collection:

    1: // Step 1 (part 2):  Whenever the Channel Uri is updated, save the content and bind.
    2: void Channel_ChannelUriUpdated(object sender, NotificationChannelUriEventArgs e)
    3: {
    4:     var uris = GetTileNotificationUris();
    5:     Channel.BindToShellTile(uris);
    6:     ChannelUriSetup(e.ChannelUri);
    7: }
    8:  
    9: private static System.Collections.ObjectModel.Collection<Uri> GetTileNotificationUris()
   10: {
   11: #if PUBLIC_HOST
   12:     string domainPath = "https://www.tipexpress.net/";
   13: #else
   14:     string domainPath = "https://localhost:51046/";
   15: #endif
   16:     var uris = new System.Collections.ObjectModel.Collection<Uri> { new Uri(domainPath) };
   17:     return uris;
   18: }
   19:  
   20:  
   21: void Channel_ErrorOccurred(object sender, NotificationChannelErrorEventArgs e)
   22: {
   23:     Debug.WriteLine("**** Failed channel registration:  " + e.ToString());
   24: }

That’s all we need to do for this step.  From this point on as notifications are sent to the device, the Push Client will do the right work for us, including updating the Tile when the application is not running.

Step 2:  Register With My Service

Now that our device is ready to go, we need to let our own service know it exists and is interested in updates.  This can be accomplished using ordinary web services (WCF, asmx, etc).  The key thing required to track and communicate with the device is the ChannelUri from Step 1.  Generating a unique Guid on the device can also be a handy way to quickly identify the device on the server (the ChannelUri, while unique, is quite long).  You can find sample code for creating a unique Guid and saving it in isolated storage on Jeff Fansler’s blog here.

The goal for my application is to push the totals the user has entered in the application to the Tile pinned on the main page with a background that matches the Accent Theme currently in use.  I’ll solve all of this by having a service method as follows:

public void RegisterClientDevice(Guid DeviceId, string ChannelUri, string AccentColor,

string BillAmount, string TipTotal, string TotalAmount)

 AccentColor can be found easily through the current application settings as follows:
 Color accent = (Color)Application.Current.Resources["PhoneAccentColor"];
 string AccentColor = accent.ToString();
 This string version of the color can easily be returned into a Color object using ColorTranslator:
 Color accent = ColorTranslator.FromHtml(AccentColor);

I now have everything I need to generate the tile. The tile itself must be a PNG with dimensions of 173x173 pixels. So for example if my background is currently set to Green, I would wind up with a tile like this:

bb20e3e9-866a-4828-baa5-746ca0baedd1

The application Title is a field we will set when we do the push and provides us with another piece of customizable data (I’ve included the Tile generation code below if you are interested).

There are various options for designing the server. For my test version I simply indexed the DeviceId Guid into a data structure and generated a local PNG for the tile to the file system. This allowed me to easily return a full URL for the image in the packet. Now that the test system is up and running my next project will be to introduce a SQL database to track everything. My tiles are only 4KB in size and the database approach will make it easier to prune old data, handle locking, etc. I won’t be covering that in this post; just follow standard web server design.

With the tile generated we can move onto the next step, pushing the notification back to the client through the push service.

Step 3:  Notify the Microsoft Push Service of Any Interesting Events

Our service is now tracking clients through their ChannelUri and preferences that have been sent directly to us.  The job of the service now is to figure out interesting data on some interval and update the client(s).  As an example if I were writing an email client, I would want to check for new messages every so often and send out a count of un-fetched mail (as the built-in application does).  Weather applications push the current temperature and conditions.  Etc.  My sample does not really monitor remote data; the goal is to allow the user to have their data on the tile.  Given this I want to push the new tile information back immediately after I generate the tile.

I played with many examples while doing this but found the sample code from benwilli to be the easiest to adopt.  The code itself is a simple HTTP POST operation with an XML payload describing the tile update.  The POST is sent to the ChannelUri created back in Step 1:

 HttpWebRequest sendNotificationRequest = (HttpWebRequest)WebRequest.Create(ChannelUri);

For my application I will specify the URL to the background tile and send along the application title (I do not have a count):

 notify.SendTileNotification(null, device.UriTilePath, 0, TileTitle);

Ultimately this causes the following (example, formatted by hand for readability) payload to be sent to the Microsoft Push Service:

 "<?xml version=\"1.0\" encoding=\"utf-8\"?>
    <wp:Notification xmlns:wp=\"WPNotification\">
    <wp:Tile>
       <wp:BackgroundImage>https://localhost:51046/UserTiles/005fdf37-d5e4-4497-a4a8-a0252ec8e685.png</wp:BackgroundImage>
       <wp:Count>0</wp:Count>
       <wp:Title>Tip Express</wp:Title>
    </wp:Tile> 
 </wp:Notification>"

The send code will add X-NotificationClass with the value “1” to the header which means the tile update should be sent immediately.  My usage case is for people to be able to see the tile update immediately when they start their phone.  If my updates were less critical I could use one of the other options which gives the push service the option to do better scheduling.

That’s all for the service code.  At this point it will simply register new clients, accept updates, publish new tiles for each user, and push them back.

Step 4:  Microsoft Push Service Communicates with Device

The great news about this step is there is nothing for us to do :)  The Push Service is provided by Microsoft for free and solves solves two key problems:  (1) the service runs at scale world wide making updates more efficient, and (2) your application doesn’t need to be active for the device to receive updates.  As updates come into the system, the Push Service will distribute them to the appropriate device which brings us to the last step.

Step 5:  Push Client Updates Tiles / Application

There is nothing for us to do on this step either.  The Push Client will receive updates and handle them on the device.  In my case I bound the tile to the URL which was sent so the Push Client will update the pinned tile to contain that image.  If I had registered to receive any notifications directly into my application (and it was running), then the Push Client would also dispatch those updates to my code.  This would allow me to funnel data right into my executing application UI.

The final results can be seen by running the application.  In the first step I have my application pinned to the home screen with my default (transparent) application icon:

image

The user then runs the application and enters some data:

image

In a new “tiles” pivot item I have added, I give the user the ability to save their current data as a tile (I’m not showing all of the permissions related code):

image

Now after the application exits and the tile updates, you can see the generated tile on the home screen:

image

Since we give the accent color to the service to generate the tile, it will handle any color currently selected (both the Microsoft built-in themes and any future OEM themes that may be enabled).  In this case the phone must communicate with our service so we know the accent color has been updated which means executing the application.  If the user changes the color to red then runs the application again with a "Save to Tile” operation, you would get the following:

image

That’s it!

Errata

I found a few interesting things while working on this code:

Counts Pushing a Tile notification with <count> specified gives you the small black bubble.  Most of the built-in applications such as Messaging and Email have their own custom drawn backgrounds with the count incorporated into them.

SNAGHTML37412a0e

I also found that once I pushed a <count> value to the Tile, it would stay resident on the device.  The only way I found to remove it was to send the value 0 for the count:   “<wp:Count>0</wp:Count>”

Background Tile Restrictions If you post a background tile, it must be less than 80KB and needs to load in less than 15 seconds (see MSDN reference).  The background tile points to a PNG.

Emulator and Channel URI At least a couple of times I found that my updates to the Push Service would fail with various exceptions (“412 Precondition Failed” for example).  I did find that shutting down and restarting the emulator from scratch would fix some of these transient issues (this one deserves more investigation to determine if there is more code required in the client to reset state on the fly).

Tile Generation Code The actual PNG generation is old school at this point (many web sites use these techniques to generate images on the fly).  The trickier part of this sample was getting all of the right data plumbed through the system.  Nonetheless, I wanted to provide my sample code in case you are interested in doing something similar:

    1: public class TileBackground
    2: {
    3:     static private float MarginSpacing = 15;
    4:     static private float LineMargin = 5;
    5:     static private int TileDimension = 173;
    6:     static private Single FontSize = 28;
    7:     static private float PenWidth = 2;
    8:     static private Color FontColor = Color.White;
    9:  
   10:     float Spacing = 15;
   11:     float MaxWidth = 0;
   12:     Bitmap objBmpImage = null;
   13:     Font objFont = null;
   14:     SolidBrush brushFont = null;
   15:     Graphics objGraphics = null;
   16:     Pen objPen = null;
   17:  
   18:     ~TileBackground()
   19:     {
   20:         try
   21:         {
   22:             if (objGraphics != null)
   23:             {
   24:                 objGraphics.Dispose();
   25:                 objPen.Dispose();
   26:                 brushFont.Dispose();
   27:                 objFont.Dispose();
   28:             }
   29:         }
   30:         catch (Exception e)
   31:         {
   32:             Debug.WriteLine("Failure in ~TileBackground on clenaup: " + e.ToString());
   33:         }
   34:     }
   35:  
   36:     /// <summary>
   37:     /// Generates a tile of the correct dimensions and color with all data included,
   38:     /// then persists the content to a file.
   39:     /// </summary>
   40:     /// <param name="stream">File to save the tile to</param>
   41:     /// <param name="AccentColor">Background color for the tile</param>
   42:     /// <param name="Values">Strings to include in tile</param>
   43:     public void GenerateToFile(System.IO.Stream stream, string AccentColor, string[] Values)
   44:     {
   45:         try
   46:         {
   47:             Bitmap bmp = GenerateBaseLayout(AccentColor, Values);
   48:             SaveToPNG(bmp, stream);
   49:         }
   50:         catch (Exception e)
   51:         {
   52:             System.Diagnostics.Debug.WriteLine("Tile generation failed: ");
   53:             System.Diagnostics.Debug.WriteLine(String.Format("  Path='{0}', AccentColor='{1}', Values='{2}','{3}','{4}'",
   54:                 stream, AccentColor, Values[0], Values[1], Values[2]));
   55:             System.Diagnostics.Debug.WriteLine(e.ToString());
   56:         }
   57:     }
   58:  
   59:     /// <summary>
   60:     /// Saves the given bitmap to a stream with high quality encoding.  Sample code
   61:     /// referenced from:  https://stackoverflow.com/questions/41665/bmp-to-jpg-png-in-c
   62:     /// </summary>
   63:     /// <param name="bmp"></param>
   64:     /// <param name="path"></param>
   65:     private void SaveToPNG(Bitmap bmp, System.IO.Stream stream)
   66:     {
   67:         EncoderParameters encoderParameters = new EncoderParameters(1);
   68:         encoderParameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 100L);
   69:         bmp.Save(stream, GetEncoder(ImageFormat.Png), encoderParameters);
   70:     }
   71:  
   72:     private static ImageCodecInfo GetEncoder(ImageFormat format)
   73:     {
   74:         ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
   75:         foreach (ImageCodecInfo codec in codecs)
   76:         {
   77:             if (codec.FormatID == format.Guid)
   78:             {
   79:                 return codec;
   80:             }
   81:         }
   82:         return null;
   83:     }
   84:  
   85:  
   86:     /// <summary>
   87:     /// Generate a tile of the correct dimensions and color and then add each line of text
   88:     /// to the tile itself.
   89:     /// </summary>
   90:     /// <param name="AccentColor">Background color for the tile</param>
   91:     /// <param name="Values">Strings to display</param>
   92:     /// <returns>Bitmap image of correct color and data</returns>
   93:     public Bitmap GenerateBaseLayout(string AccentColor, string[] Values)
   94:     {
   95:         if (Values.Length <= 0)
   96:             return null;
   97:  
   98:         // Create the key resources used to render the tile.
   99:         objBmpImage = new Bitmap(TileDimension, TileDimension);
  100:         objFont = new Font("Arial", FontSize, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Pixel);
  101:         brushFont = new SolidBrush(FontColor);
  102:         objPen = new Pen(FontColor, PenWidth);
  103:         objGraphics = Graphics.FromImage(objBmpImage);
  104:  
  105:         // Figure out the height of the font and sizing information.
  106:         float FontHeight = (float)objGraphics.MeasureString(Values[0], objFont).Height;
  107:  
  108:         Spacing = FontHeight * 0.8F;
  109:         float yLocation = MarginSpacing;
  110:  
  111:         // Fill the rectangle to the accent color from the user.
  112:         Color accent = ColorTranslator.FromHtml(AccentColor);
  113:         objGraphics.Clear(accent);
  114:  
  115:         objGraphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
  116:         objGraphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
  117:  
  118:         // Figure out the length of the longest string for the summary lines and add some extra pixels.
  119:         float [] LineWidth = GetLineWidths(Values);
  120:         MaxWidth = LineWidth.Max() + LineMargin;
  121:  
  122:         // Add each string to the tile placing a single line after the first two and a double
  123:         // line after all the numbers.
  124:         int strNum = 0;
  125:         AddString(ref yLocation, Values[strNum], LineWidth[strNum]);
  126:  
  127:         ++strNum;
  128:         AddString(ref yLocation, Values[strNum], LineWidth[strNum]);
  129:         DrawLine(ref yLocation, MaxWidth);
  130:  
  131:         ++strNum;
  132:         AddString(ref yLocation, Values[strNum], LineWidth[strNum]);
  133:         DrawLine(ref yLocation, MaxWidth);
  134:         DrawLine(ref yLocation, MaxWidth);
  135:  
  136:         // Finalize the image and return.
  137:         objGraphics.Flush();
  138:         return (objBmpImage);
  139:     }
  140:  
  141:     /// <summary>
  142:     /// Get the width of the given strings.
  143:     /// </summary>
  144:     private float[] GetLineWidths(string[] Values)
  145:     {
  146:         float [] rtn = new float[Values.Length];
  147:         for (int i=0; i < Values.Length; i++)
  148:             rtn[i] = (float)objGraphics.MeasureString(Values[i], objFont).Width;
  149:         return (rtn);
  150:     }
  151:  
  152:     /// <summary>
  153:     /// Adds a line of text to the given image and calculates the location for the next line of text.
  154:     /// </summary>
  155:     private void AddString(ref float yLocation, string value, float Width)
  156:     {
  157:         float x = (float)TileDimension - Width - MarginSpacing;
  158:         objGraphics.DrawString(value, objFont, brushFont, x, yLocation);
  159:         yLocation += Spacing;
  160:     }
  161:  
  162:     /// <summary>
  163:     /// Draws a line at the location then adjusts the next location accordingly.
  164:     /// </summary>
  165:     /// <param name="yLocation">Location on y-axis to display line</param>
  166:     /// <param name="Width">Width of the line</param>
  167:     private void DrawLine(ref float yLocation, float Width)
  168:     {
  169:         yLocation += LineMargin;
  170:         float x2 = (float)TileDimension - MarginSpacing;
  171:         float x = x2 - Width;
  172:         objGraphics.DrawLine(objPen, x, yLocation, x2, yLocation);
  173:     }
  174:  
  175: }

More Links In addition to my last post, here are some additional links that I found helpful while working on my code:

Summary

In summary, the Tile / Notification support in Windows Phone is one of the cool differentiators for the OS from other smart phones so taking advantage of it (and the rest of the “Metro” UI like Panorama and Pivot) are great ways to make your applications more interesting.  As I mentioned above, I don’t consider this a “killer feature” for a tip application, it was more for educational purposes.  But hopefully you’ve found something helpful in this post if you are working on adding your own tile support.  I’m in the process of polishing my own application code now (including making the server more responsive and durable) and will post an updated version of Tip Express to the marketplace soon with the new features outlined here.

Enjoy!

Comments

  • Anonymous
    January 03, 2011
    Thanks for this post Jason. I'm agree with you that the Tile/Notification is a great differentiator feature for Windows Phone. By the way I think that the need to use a backoffice to handle notifications will limit the development of this feature. In your case, there is objectively no reason to need a backoffice just to generate a tile depending of local informations !

  • Anonymous
    January 07, 2011
    Just an FYI, the phone will place the accent color below your image.  If you create a transparent PNG instead, you don't need to worry about the current accent color. Also, for future readers...I had issues using https for the background image uri sent in the tile.  Switched to http and it worked.

  • Anonymous
    January 08, 2011
    @Lionel - there are definitely ways to streamline my example. As you can tell my usage is really kind of contrived.  my hope was it would give enough of the plumbing that you could leverage for your own code that might have more requirements.   Even though the indirect through the push service is an extra step, it does help the device / network scale better.  It's a trade off. @Ryan - good point on trasnparency.  there is more information on transparent backgrounds linked for my last post here: blogs.msdn.com/.../creating-a-tile-using-theme-s-color-as-background-windows-phone-7.aspx

  • Anonymous
    January 13, 2011
    I found this post to be very helpful...  Just wondering...  I noticed in the emulator that I couldn't figure out how to clear the title?  Meaning show the tile pre-push...  So if the tile was pushed and then they opened the application I would want the tile to go back to normal. How would I accomplish this?