教程:适应于Windows Phone 7的动态区块推送通知(Tile Push Notification)

[原文发表地址] Tutorial: Dynamic Tile Push Notification for Windows Phone 7

[原文发表时间] 3 Jan 2011 11:24 AM

如我在上篇博文中所提到的,我想用动态区块通知更新我的Tip Express应用程序。基本想法很简单: 使用用户所选择的主页背景主题并将用户数据从应用程序推送到固定区块(准确地说,这不是这个应用程序的杀手锏,但是我可以借它为由玩玩通道通知)。

这里有大量关于各种不同的通知类型的概述信息(Raw, Toast, 和区块),所以我在这里不复述了。在我的示例中,我研究并主要关注区块通知。概念本身不复杂但是有很多部件需要追踪。我发现创建一个单页流图很有用:

现在让我们来演示各种不同组件并看看一些源代码(请注意我从上面发布的一些链接中借用了一些代码段来构建我的示例代码)。

步骤一: 在客户端设备建立一个通道

我们的应用程序的第一步是通过使用HttpNotificationChannel类建立一个在设备上有推送客户端运行的通道。该过程中最重要的一块数据是被用来识别网络中的设备会用到的ChannelUri——所有返回到手机上的通信都需要这个值。我还发现为设备创建一个唯一的Guid是很便利的,我的服务可以用它来为每个设备(URI是很长的)建立唯一索引。

一旦通道建立了,你就能在随后的应用程序执行中找到。 利用有效的通道,我们无须保存ChannelUri并添加一些事件处理程序:

   1: public stringChannelName = "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 voidSetupChannel()

   7: {

   8:     Channel = HttpNotificationChannel.Find(ChannelName);

   9:     if (Channel == null)

  10:     {

  11:         Channel = newHttpNotificationChannel(ChannelName);

  12:         Channel.ChannelUriUpdated += newEventHandler<NotificationChannelUriEventArgs>(Channel_ChannelUriUpdated);

  13:         Channel.ErrorOccurred += newEventHandler<NotificationChannelErrorEventArgs>(Channel_ErrorOccurred);

  14:         Channel.Open();

  15:     }

  16:     else

  17:     {

  18:         Channel.ChannelUriUpdated += newEventHandler<NotificationChannelUriEventArgs>(Channel_ChannelUriUpdated);

  19:         Channel.ErrorOccurred += newEventHandler<NotificationChannelErrorEventArgs>(Channel_ErrorOccurred);

  20:         ChannelUriSetup(Channel.ChannelUri);

  21:     }

  22: }

  23: 

  24: private voidChannelUriSetup(Uri uri)

  25: {

  26:     ChannelUri = uri;

  27: }

在我的应用程序中我想生成我自己的背景区块(用我的服务器做宿主)并将其绑定到图片上。我们将使用Channel.BindToShellTile()来实现,将我的域名(“http://www.tipexpress.net/”)作为URI集合传入:

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 staticSystem.Collections.ObjectModel.Collection<Uri> GetTileNotificationUris()

  10: {

  11: #if PUBLIC_HOST

  12:     stringdomainPath = "http://www.tipexpress.net/";

  13: #else

  14:     stringdomainPath = "https://localhost:51046/";

  15: #endif

  16:     var uris = newSystem.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: }

这就是本步骤中全部要做的。现在开始因为通知被传送到设备上,推送客户端会帮我们完成该做的事,包括在应用程序没运行时更新区块。

步骤二: 用我的服务注册

既然我们的设备一切就绪,我们就要让我们的服务知晓并且乐于更新。这可以用普通web服务完成(WCF,asmx等等)。

追踪和通信所需要的关键是步骤一中的ChannelUri。在设备上生成一个唯一Guid也是在服务器上快速识别出设备的一个简易方法(ChannelUri虽然也是唯一的但很长)。你能从Jeff Fansler这里的博客上找到创建唯一Guid并将其存入独立存储区的示例代码。我的应用程序的目的是将用户输入应用程序的金额显示在背景与当前使用的Accent主题匹配的主页上的图块中。我将通过如下服务方法解决所有这些问题:

public void RegisterClientDevice(Guid DeviceId, string ChannelUri, string AccentColor,
            string BillAmount, string TipTotal, string TotalAmount)

AccentColor能很容易地通过下面的当前应用程序设置找到:

Color accent = (Color)Application.Current.Resources["PhoneAccentColor"];

string AccentColor = accent.ToString();

颜色的字符串版本能通过ColorTranslator被很容易地返回至一个Color对象:

Color accent = ColorTranslator.FromHtml(AccentColor);

我现在具备生成区块的一切要素。区块本身必须为PNG格式且大小为173x173像素。因此,比如我的背景现在设置为绿色,最终图块将像这样:

应用程序标题为我们提供另一处可定制的数据,是我们在推送时需要设置的字段(如果你感兴趣的话,我在本文的结尾部分包含了区块生成代码)。

设计服务器有多种不同选择。在我的测试版本中我简单地把DeviceID Guid编入数据结构索引中,并为区块生成一个文件系统中的本地PNG文件。这使的我更容易地为包中的图片返回一个完整的URL。既然测试系统已经就绪,我下个项目的执行将引入一个数据库来追踪所有的事件。我的区块仅4KB大,使用数据库方式将使删除旧的数据,处理锁定等更加容易。 本文中我不会涵盖那些,遵循标准web 服务器设计即可。

区块生成了,我们可以继续下一步,通过推送服务将通知推送回客户端。

步骤三: 将任何有趣事件通知微软推送服务(Microsoft Push Service)

我们的服务现在通过他们直接传送给我们的ChannelUri和首选项追踪客户端。服务的任务现在是在一些时间间隔中找出有趣的数据,并更新客户端(们)。比如说我在写一个邮件客户端,我可能想要时时查看新信息并发送一些没有读取的邮件(像内置应用程序所做的一样)。天气应用程序会报告当前的温度和天气情况等等。我的示例并不真正监控远程数据,目标是允许用户在区块上显示他们自己的数据。考虑到这个,我希望在生成区块后将新区块信息立即推送回去。

我做这个的时候研究了很多示例,发现benwilli的示例代码是最容易采用的。 代码本身就是用一个简单的HTTP POST操作,用XML 载荷(payload)来描述区块的更改。POST被送回到步骤一中所创建的ChannelUri:

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

对我的应用程序来说,我将指定背景区块的URL,并将其与应用程序的标题一起发送(我没有设置数目)

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

最终这将导致下面的(示例,为方便阅读更改了格式)载荷被送至Microsoft推送服务:

<?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>"

发送的代码将添加顶端带“1”值的X-NotificationClass,表示区块更新必须被马上发送。

我的使用示例是让大家能够看到在启动Phone的时候立即看到区块更新。如果我的更新不是那么重要,我能选择另一个让推送服务做更好的安排。

这就是所有的服务代码。现在它会简单地注册新客户端,接收更新,对每个用户发布新区块,并将他们推送回来。

步骤四:Microsoft Push Service与设备的通信

关于这一步最好的消息是我们什么也不用做。 J 推送服务由微软免费提供使用并解决了两个重大问题: (1)服务在世界范围内运行,使更新更有效率;(2)你的应用程序无须为了方便设备接收更新处于活动状态。

更新进到系统后,推送服务会把他们分配到合适的设备上,我们就可以进行下一步了。

步骤五: 推送客户端更新

区块/应用程序

本步中我们也不用做任何操作。推送客户端会在设备上接收和处理更新。

在我的案例中,我将区块绑定到发送的URL上,这样推送服务端会更新固定区块使其包括那个图片。如果我注册为将任何通知都直接接收到我的应用程序(并且它在运行),那么推送客户端也会将这些更新到我的代码。这将允许我把数据汇集到正在执行的应用程序UI上。

最后的结果可以通过运行应用程序看到。在第一步中,我已经用我的默认(透明)应用程序图标把我的应用程序固定到我的主界面上:

用户然后运行应用程序并输入数据:

在我添加的新“区块”透视项中,我让用户可以将当前数据保存为一个区块(我不会显示所有权限相关的代码):

应用程序存在并且区块也更新后,你能在主屏幕上看到生成的区块:

既然我们用强调色来显示生成区块的服务,现在它将适应任何所选的颜色了(微软内置主题和任何可用的功能OEM主题)。在这个案例中,phone必须与我们的服务通信,这样我们才能知道一旦强调色更新就表示应用程序在运行了。如果用户将颜色改为红色然后再运行应用程序做一个“保存到区块”操作,你将得到如下:

就是它了!

勘误表

我在处理这段代码的时候发现一些有趣的事情:

数目—— 指定的<数目>会显示为黑色的泡泡状。 大多数的内置应用程序——比如短信和邮件——有与这些数字不协调的自身定制背景。

我还发现一旦我显示一个<Count>值到区块,它将一直在设备上,我发现能移除它的唯一方法是发送零值到金额:“<wp:Count>0</wp:Count>”

背景区块限制——发布的背景区块必须小于80KB,并需要能在15秒之内下载完(参考MSDN)。背景区块指向一个PNG文件

模拟器和通道URI——至少两次以上我发现推送服务的更新失败,因为各种不同的异常(比如: “412 Precondition Failed”)。我的确发现从头关闭和重启模拟器会解决一些不稳定问题(如果在运行中还有更多代码需要重置客户端,这个问题需要做更多调查)。

区块生成代码——实际PNG生成现在是老调常弹了(很多网站用这个技术在运行时生成图片)。这个示例中比较有技巧的部分是通过系统得到所有探测到的正确数据。不过,我还是想将我的示例代码提供给你们,说不定你们对做类似的东西感兴趣

   1: public classTileBackground

   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, stringAccentColor, 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:  http://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(stringAccentColor, string[] Values)

  94:     {

  95:         if(Values.Length <= 0)

  96:             return null;

  97: 

  98:         // Create the key resources used to render the tile.

  99:         objBmpImage = newBitmap(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: }

更多链接——除了我上一篇博文之外,这里有一些我在处理我的代码的时候发现比较有用的其他链接:

总结

总而言之,Windows Phone中的区块、通知支援是OS相对于其他职能设备的一个很酷的不同之处,所以好好利用它(和Panorama和Pivot之类的其他“Metro”UI)会让你的应用程序更有意思。如我上面提到的,我并不认为这对一个“小费”应用程序来说是一个“杀手锏功能”,它更具有教育意义。但是如果你们在添加自己的区块支持,我希望你们在这篇博客中发现一些有帮助的东西。现在我正在完善我自己的应用程序代码(包括让服务端响应更快更耐用),会很快在程序市场发布一个带这里所概述的新功能的更新版本的Tip Express。

祝您使用得开心!

 


上一篇博客:Visual Studio 2010 SP1中期修复列表