Jaa


打造卓越的磁贴体验(第 2 部分)

本篇博文的第 1 部分中,我们已经了解了应如何设计磁贴更新,以及如何选择模板,从而让动态磁贴与您希望展现的内容相匹配。我们已经为应用程序设置了一个宽形默认磁贴,那么接下来让我们开始更新磁贴。现在,我们将直接深入探讨代码的问题。首先,我们将一同看看如何为 Contoso Food Trucks 应用程序磁贴设置轮询,其中包括我们 Web 服务代码的内容。然后,我们将为应用程序添加一个次级磁贴,并使用 Windows 8 SDK 应用程序磁贴与徽章示例中所提供的 NotificationsExtension 库来更新磁贴。事不宜迟,让我们直奔主题!

选择通知的传递方式

既然我已经了解了我所希望获得的磁贴是什么模样(您可回顾本篇博文的第 1 部分以加深印象),那么接着我需要确定应何时更新这些磁贴。

更新应用程序磁贴共有 4 种方式(请参阅开发中心内选择通知传递方式一文)。应用程序可使用本地通知来更新其磁贴,这对于应用程序正在运行,且信息发生变更的情形十分有效。应用程序可在精确的时间点安排磁贴和 Toast 更新。此外,应用程序可在未运行时使用推送和轮询磁贴通知,进而从云端更新其磁贴。轮询对于低频率的广播内容十分有效。而对于发送需要立即到达目标的 Toast 通知,或面向个人用户的磁贴更新而言,推送的效果将十分显著。在本篇博文中,我们将集中讨论有关轮询更新和本地更新的内容。

轮询附近的移动餐车

我们的应用程序中共有两类不同的信息需要磁贴更新。而其中最重要的信息是用户默认午餐地点附近的移动餐车。用户将在应用程序运行时在其中设置午餐的默认位置。我使用了该默认位置信息来更新磁贴,并让用户了解该位置附近的移动餐车。此处的图像曾在本篇博文的第 1 部分中出现过,其显示了该应用程序的磁贴。现在让我们来看看如何使用轮询来让这些磁贴显示于我们应用程序的磁贴中。

宽形磁贴中显示:[Food Trucks Near You / Nom Nom Barbecue Truck / Sushi Truck / Macaroni Makin' Wagon](您附近的移动餐车/Nom Nom 烧烤餐车/寿司餐车/通心粉餐车)    方形磁贴中显示:[Near You / Nom Nom / Sushi Truck](附近的餐车/Nom Nom/寿司餐车)

移动餐车通常将在同一位置呆上一整天,或至少整个午餐时间段的位置不会发生变化。由于移动餐车的位置并不会经常变化,因此我不需要实时更新磁贴。因此,我无需考虑使用就时间敏感通知而言更为有用的推送通知。但是我希望每天能至少更新一次该数据,因此使用定期通知来轮询 Web 服务以进行变更成为了我的最佳选择。

客户端的实现:轮询附近的移动餐车

用于设置定期通知的客户端实现仅需几行代码。每当用户启动应用程序或切换应用程序时,我们将调用 TileUpdater.startPeriodicUpdate。这将在启动或切换每一应用程序时导致 URI 传递至将被立即轮询的 API 和将被更新的磁贴。而造成这一现象的原因在于 API 调用将立即延伸至云服务 URI 并更新磁贴。这一行为对于调试非常有效,我们无需等待下一轮询间隔即可测试云服务的轮询。

真正的问题在于应选择为 startPeriodicUpdate API 提供哪一 URI。在我们的这个例子中,我希望云服务对磁贴更新关于某特定位置的信息。为了保护我们用户的位置信息,我并不打算将用户的准确位置信息发送至我们的服务。相反,我利用用户在应用程序中所提供的邮政编码来判断其所在位置。

为了将邮政编码传递至 Web 服务,我在轮询 URI 的末端添加了一项查询字符串,从而让我们的云服务了解应在磁贴上显示的位置信息:

https://www.contoso.com/foodtrucks/tile.xml?zipcode=98052

为了响应该 URI 的 HTTP GET,我们的 Web 服务将为 URI 中所提供的邮政编码返回格式化的磁贴通知 XML。以下是用于设置轮询的代码,其已将邮政编码硬编码入 URI 字符串。

JavaScript:

 // update the tile poll URI
var notifications = Windows.UI.Notifications;
var polledUri = new Windows.Foundation.Uri("https://www.contoso.com/foodtrucks/tile.xml?zipcode=98052");
var recurrence = notifications.PeriodicUpdateRecurrence.hour;
var tileUpdater = notifications.TileUpdateManager.createTileUpdaterForApplication();
tileUpdater.startPeriodicUpdate(polledUri, recurrence);

C#:

 // update the tile poll URI
using Windows.UI.Notifications;
Uri polledUri = new Uri("https://www.contoso.com/foodtrucks/tile.xml?zipcode=98052");
PeriodicUpdateRecurrence recurrence = PeriodicUpdateRecurrence.Hour;
TileUpdateManager.CreateTileUpdaterForApplication().StartPeriodicUpdate(polledUri, recurrence);

由于移动餐车的位置在一天之内可能发生变动,因此我希望能以较高的频率来更新磁贴。在这个应用程序之中,我将更新间隔设置为一小时,从而在我们后端服务的负载与磁贴上的信息更新之间形成平衡。

我在调用了一次 startPeriodicUpdate API 后,磁贴将每隔一小时持续更新一次,即使应用程序尚未运行,也是如此。如果我希望变更 URI 来进行轮询,那么我只需使用不同的 URI 再次调用 API。例如,如果用户将其默认位置变更成不同的邮政编码,那么我只需再次调用 startPeriodicUpdate 即可将 URI 更新成正确的邮政编码。如果用户打算从此清除其默认位置或停止磁贴更新,那么应用程序将通过调用 stopPeriodicUpdate API 来停止定期更新。

有关如何使用 startPeriodicUpdate API 的详细信息,请参阅开发中心内推送与定期通知客户端示例如何为磁贴设置定期通知等文章。

服务器的实现:轮询附近的移动餐车

我们希望能在几乎所有的服务技术中为接受轮询的磁贴而实现服务端。以下我将向您展示 PHP 和 ASP.NET 代码的一些示例。

当我们的 Web 服务接受轮询时,其必须使用符合磁贴 XML 架构的 XML 来响应 HTTP GET 请求。由于内容是通过网络发送,因此您同时可使用 HTTPS 来让 Web 服务保护内容。Cookies 尚不受支持。需响应请求的服务的所有信息均必须包含于 URI 之中。基于这一原因,我们的应用程序使用了查询字符串来传递邮政编码。

在我们接下来将看到的 PHP 代码中,我暂未考虑对 Web 服务数据库的访问。而在实际操作的代码中,get_trucks_from_database() 函数所返回的移动餐车变量将包含 Web 服务为获取特定邮政编码而填充磁贴模板所需的所有信息。我对该服务示例进行了简化,重点关注服务将返回的 XML。而现实生活中的 Web 服务部署将考虑性能、可扩展性、安全性和更易于维护的架构等因素。

PHP:

 <?php

//
// set default query string parameters
// process query string and set parameters
//
if($_GET['zipcode']){
  $zipcode = $_GET['zipcode'];
}
else{
  $zipcode = 'default';
}

//
// get item info from our database
// - this is placeholder code that you need to replace in a real implementation
// - the return value is a multidimensional array of the long and short strings
//   to place in the tile template
//
$trucks = get_trucks_from_database($zipcode);

?>
<?php echo '<?xml version="1.0" encoding="utf-8" ?>'?>
<tile>
  <visual>
    <binding template="TileWideText01">
      <text id="1">Food Trucks Near You</text>
      <text id="2"><?php echo $trucks[0][0]?></text>
      <text id="3"><?php echo $trucks[0][1]?></text>
      <text id="4"><?php echo $trucks[0][2]?></text>
    </binding>
    <binding template="TileSquareText03">
      <text id="1">Near You</text>
      <text id="2"><?php echo $trucks[1][0]?></text>
      <text id="3"><?php echo $trucks[1][1]?></text>
    </binding>
  </visual>
</tile>

下一代码段示例与我们刚刚看过的 PHP 代码是等效的。该 ASP.NET 网页示例显示了某一磁贴服务的快速实现。为获得功能完整的 ASP.NET 服务,您可能希望使用新的 ASP.NET Web API。ASP.NET Web API 是专为类似于这样的服务而构建。有关 ASP.NET Web API 的详细信息,请参阅 https://www.asp.net/web-api

ASP.NET:

 @{
  //
  // set default query string parameters  
  // process query string and set parameters
  //
  var zipcode = Request["zipcode"] ?? "default";
  //
  // get item info from our database
  // - this is placeholder code that you need to replace in a real implementation
  // - the return value is a multidimensional array of the long and short strings
  // to place in the tile template
  var trucks = get_trucks_from_database(zipcode);
 
}<?xml version="1.0" encoding="utf-8" ?>'?>
<tile>
  <visual>
    <binding template="TileWideText01">
      <text id="1">Food Trucks Near You</text>
      <text id="2">@trucks[0,0]</text>
      <text id="2">@trucks[0,1]</text>
    </binding>
    <binding template="TileSquareText03">
      <text id="1">Near You</text>
      <text id="2">@trucks[1,0]</text>
      <text id="2">@trucks[1,1]</text>
    </binding>
  </visual>
</tile>

最喜爱的移动餐车

到目前为止,我们已经查看了应用程序显示于主磁贴中的内容。但是有些时候用户希望在 [Start](开始)屏幕中设置一个磁贴来跟踪其最喜爱的移动餐车。在我们的应用程序中,我使用了应用栏来让用户将最喜爱的移动餐车固定至 [Start](开始)之中。这些被固定的磁贴被称为次级磁贴。当用户固定了次级磁贴后,我们将通过向该磁贴发送通知的方式对该其更新有关特定移动餐车的信息。

固定移动餐车磁贴

固定磁贴后,应用程序可让用户从 [Start](开始)屏幕中直接访问应用程序中的特定内容。借助次级磁贴,用户可将应用程序直接启动至有关其固定的移动餐车的部分。

固定的磁贴仅可从应用程序内部创建。用户将有望通过调用应用栏而固定磁贴。应用栏中包含一个标准的推送固定图标,以显示视图中用户可固定的内容。

当用户点击固定按钮时,屏幕中将出现一个弹出窗口,并显示将被固定的磁贴的预览:

弹出窗口中包含 Nom Nom 烧烤餐车的图片和按钮:[Pin to Start] (固定至“开始”)

现在我们需要:

  1. 向应用程序添加一个应用栏,包括用于 [Pin to Start](固定至“开始”)和 [Unpin from Start](从“开始”取消固定)操作的固定图标
  2. 实施一个事件处理程序,以点击应用栏的固定/取消固定按钮
  3. 添加应用程序特定的逻辑,以固定新磁贴响应固定/取消固定操作

由于我们无需关注用于创建应用栏的前两项步骤,因此我们可重点关注固定磁贴自身的内容。有关如何实施应用栏的详细信息,请参考以下链接:

在第 3 步中,我们的应用程序将通过设置几个属性而创建次级磁贴。

JavaScript:

 // Keep track of your secondary tiles with a unique ID   
var nomNomTile = "SecondaryTile.NomNom";

// Set properties on the tile
var logo = new Windows.Foundation.Uri("ms-appx:///images/NomNomTruck-Logo.png");
var smallLogo = new Windows.Foundation.Uri("ms-appx:///images/NomNomTruck-SmallLogo.png");
var wideLogo = new Windows.Foundation.Uri("ms-appx:///images/NomNomTruck-WideLogo.png");
var TileActivationArguments = "TruckName=NomNom";

// Create the secondary tile
var tile = new Windows.UI.StartScreen.SecondaryTile(nomNomTile,
                                                    "Nom Nom",
                                                    "Nom Nom Barbecue Truck",
                                                    TileActivationArguments,
                                                    Windows.UI.StartScreen.TileOptions.sh
owNameOnWideLogo,
                                                    logo,
                                                    wideLogo);

tile.foregroundText = Windows.UI.StartScreen.ForegroundText.light;
tile.smallLogo = smallLogo;

// Request the user’s permission to create the secondary tile
// - we return the promise here, assuming that this code is embedded 
//   in a larger function. 
//   See the Windows 8 SDK Secondary Tiles sample for more info:
//   https://code.msdn.microsoft.com/windowsapps/Secondary-Tiles-Sample-edf2a178
return new WinJS.Promise(function (complete, error, progress) {
  tile.requestCreateAsync().then(function (isCreated) {
    if (isCreated) {
      complete(true);
    } else {
      complete(false);
    }
  });
});

C#:

 // Keep track of your secondary tiles with a unique ID   
const string nomNomTile = "SecondaryTile.NomNom"; 

// Set properties on the tile
Uri logo = new Uri("ms-appx:///images/NomNomTruck-Logo.png");
Uri smallLogo = new Uri("ms-appx:///images/NomNomTruck-SmallLogo.png");
Uri wideLogo = new Uri("ms-appx:///images/NomNomTruck-WideLogo.png");
string tileActivationArguments = "TruckName=NomNom";

// Create the secondary tile object
SecondaryTile secondaryTile = new SecondaryTile(nomNomTile,
                                                "Nom Nom",
                                                "Nom Nom Barbecue Truck",
                                                tileActivationArguments,
                                                Windows.UI.StartScreen.TileOptions.ShowNa
meOnWideLogo,
                                                logo,
                                                wideLogo);

secondaryTile.ForegroundText = Windows.UI.StartScreen.ForegroundText.Light;
secondaryTile.SmallLogo = smallLogo;

// Request the user’s permission to create the secondary tile
// - this code assumes that this code was called within an event handler which has
//   a ‘sender’ parameter.
//   See the Windows 8 SDK Secondary Tiles sample for more info:
//   https://code.msdn.microsoft.com/windowsapps/Secondary-Tiles-Sample-edf2a178
await secondaryTile.RequestCreateForSelectionAsync(MainPage.GetElementRect((FrameworkElement)se
nder), Windows.UI.Popups.Placement.Right);

有关如何固定次级磁贴的详细信息,请参阅次级磁贴的指导原则与核对清单次级磁贴 Consumer Preview 示例等文章。

使用本地通知来更新固定的磁贴

固定磁贴是我们应用程序在 [Start](开始)中进行更新的一个额外磁贴。更新该磁贴与更新应用程序的主磁贴并无差异。在本应用程序中,我使用了本地通知 API,而不是多项云更新机制中的一项来在应用程序运行时更新次级磁贴。我在此处向您展示了本地通知,进而向您展示其工作原理。您可采用类似的方式从云端进行更新。该应用程序同样将拥有易于实现的轮询应用场景。

在本部分的代码中,我使用了包含于 Windows 8 SDK 应用程序磁贴和徽章示例中的 NotificationsExtensions 库。您可将该库包含于应用程序项目中,从而简化本地更新磁贴的过程。库将在 Windows 磁贴更新 API 的顶部提供一个对象模型,从而让您避免从 JavaScript、C# 和 C++ 应用程序内部操作 XML。其同时可通过提供 IntelliSense 而简化开发过程。

利用本地通知 API,我可在应用程序运行的任何时候更新磁贴。而对于固定的移动餐车磁贴,我希望在每次用户启动应用程序时,向磁贴更新有关某特定移动餐车推出的任何优惠信息。

枚举次级磁贴

由于用户可在应用程序未运行时从 [Start](开始)中取消固定次级磁贴,因此应用程序启动时需要进行的首项操作是查找其当前固定的次级磁贴。每个枚举的磁贴都包含一个可辨识自身独特身份的 tileId。由于应用程序将在创建 tileId 时即对其进行设置,因此我们可使用 ID 来了解如何更新所查找到的每一磁贴。以下是具体步骤:

JavaScript:

 // Get secondary tile ids
Windows.UI.StartScreen.SecondaryTile.findAllAsync().done(function (tiles) {
  if (tiles) {
    tiles.forEach(function (tile) {
      switch (tile.tileId) {
        case nomNomTile:
          updateNomNomTruck();
          break;
        // add cases for all the food trucks this app supports
        default:
          break;
      }
    });
  }
});

C#:

 // Get secondary tile ids
IReadOnlyList<SecondaryTile> tilelist = await 
Windows.UI.StartScreen.SecondaryTile.FindAllAsync();

foreach (var tile in tilelist)
{
  switch (tile.TileId)
  {
    case nomNomTile:
      updateNomNomTruck();
      break;
    // add cases for all the food trucks this app supports
    default:
      break;
  }
}

本地更新

我对所有固定的移动餐车磁贴更新了该移动餐车当天的当前信息。如果我使用本地更新来更新主磁贴,那么我们就无需首先枚举次级磁贴,这是因为 API 中的默认操作是更新调用应用程序的主磁贴。以下所显示的是我们在本篇博文第 1 部分中所见到的应用程序的磁贴。

图像中显示了烤架上的肉排、餐车徽标和更新文字:[Nom Nom Barbecue Truck, Washer Ave and 3rd until 3](Nom Nom 烧烤餐车,本月 3 日下午 3 点前位于 Washer Ave)    图像中显示了烤架上的肉排、餐车徽标和更新文字:[Nom Nom @ Washer Ave and 3rd until 3](Nom Nom 3 日 3 点前 @ Washer 大街)

以下是我们在枚举中所调用的函数,该函数实际上将向次级磁贴发送通知。

JavaScript:       

 function updateNomNomTruck() {
  // Business logic for retrieving Nom Nom BBQ truck deals
  // ...
  var result = "Washer Ave and 3rd until 3";

  // We can send a notification only for a tile that is pinned. 
  // Lets make sure the tile is pinned before we try to send the notification.
  if (Windows.UI.StartScreen.SecondaryTile.exists(nomNomTile)) {

    // Construct the wide template
    var wideTile = 
NotificationsExtensions.TileContent.TileContentFactory.createTileWideImageAndText02();
    wideTile.image.src = "https://www.contoso.com/foodtrucks/nomnombbq.png";
    wideTile.textCaption1.text = "Nom Nom Barbecue Truck";
    wideTile.textCaption2.text = result;

    // Construct the square template
    var squareTile = 
NotificationsExtensions.TileContent.TileContentFactory.createTileSquarePeekImageAndText04
();
    squareTile.image.src = "https://www.contoso.com/foodtrucks/nomnombbq.png";
    squareTile.textBodyWrap.text = "Nom Nom @ " + result;

    // Attach the square template to the notification
    wideTile.squareContent = squareTile;

    // send the notification to the secondary tile
    Windows.UI.Notifications.TileUpdateManager.createTileUpdaterForSecondaryTile(nomNomTi
le).update(wideTile.createNotification());
  }
}

C#:    

 private void updateNomNomTruck()
{
  // Business logic for retrieving Nom Nom BBQ truck deals
  // ...
  string result = "Washer Ave and 3rd until 3";

  // We can send a notification only for a tile that is pinned. 
  // Lets make sure the tile is pinned before we try to send the notification.
  if (Windows.UI.StartScreen.SecondaryTile.Exists(nomNomTile))
  {

    // Construct the wide template
    NotificationsExtensions.TileContent.ITileWideImageAndText02 wideTile = 
NotificationsExtensions.TileContent.TileContentFactory.CreateTileWideImageAndText02();
    wideTile.Image.Src = "https://www.contoso.com/foodtrucks/nomnombbq.png";
    wideTile.TextCaption1.Text = "Nom Nom Barbecue Truck";
    wideTile.TextCaption2.Text = result;

    // Construct the square template
    NotificationsExtensions.TileContent.ITileSquarePeekImageAndText04 squareTile = 
NotificationsExtensions.TileContent.TileContentFactory.CreateTileSquarePeekImageAndText04();
    squareTile.Image.Src = "https://www.contoso.com/foodtrucks/nomnombbq.png";
    squareTile.TextBodyWrap.Text = "Nom Nom @ " + result;

    // Attach the square template to the notification
    wideTile.SquareContent = squareTile;

    // Send the notification to the secondary tile
    Windows.UI.Notifications.TileUpdateManager.CreateTileUpdaterForSecondaryTile(nomNomTi
le).Update(wideTile.CreateNotification());
  }
}

由于我所使用的是 NotificationsExtensions 库,因此我不需要在本地代码中操作 XML。相反,我使用了 NotificationsExtensions 提供的对象模型,该模型可让我使用 IntelliSense 来发现每个通知模板中的不同属性。

磁贴更新的 XML 类似于:

 <?xml version="1.0" encoding="utf-8" ?>
<tile>
  <visual>
    <binding template="TileWideImageAndText02">
      <image id="1" src="https://www.contoso.com/foodtrucks/nomnombbq.png"/>
      <text id="1">Nom Nom Barbecue Truck</text>
      <text id="1">Washer Ave and 3rd until 3</text>
    </binding>
    <binding template="TileSquarePeekImageAndText04">
      <image id="1" src=" https://www.contoso.com/foodtrucks/nomnombbq-square.png"/>
      <text id="1">Nom Nom @ Washer Ave and 3rd until 3</text>
    </binding>
  </visual>
</tile>

如果您所使用的是 NotificationsExtensions 库,那么您可使用 IntelliSense 来发现磁贴模板上的属性,进而加速开发本地磁贴更新的过程。我希望 NotificationsExtensions 对您的 JavaScript、C# 以及 C++ 项目有所帮助。

结论

如果用户能在磁贴中看见滑稽、有趣的内容,那么他们启动您的应用程序并了解该应用程序详情的可能性将大大提高。我希望本篇博文能启发您思考如何为您的应用程序添加动态磁贴,进而展现您应用程序的最大优势。如果您希望了解有关通知的更多内容,请参阅开发中心内磁贴与通知综述选择通知传递的方法等文章。

-- Windows 项目经理 Kevin Michael Woley

本篇博文是集体智慧的结晶。非常感谢 Tyler Donahue、Daniel Oliver 以及 Jon Galloway 在本篇博文撰写过程中所给予的大力帮助。