Windows Phone

使用 C# 和 Xamarin 构建一款跨平台的移动高尔夫应用程序

Wallace B. McClure

下载代码示例

高尔夫赛季回归会带来很多趣事,其中之一就是参加内含远距赛等赛事的锦标赛。在这些赛事中,球员在指定球洞开球,然后将其开球距离记录下来与锦标赛中的其他球员进行比较。当天最远距离者为获胜者。但是,这类比赛通常不集中记分。如果您第一组参赛,那么在比赛结束前,您都无法得知自己的开球相比他人结果如何。为何不借助手机记录开球的起点和终点并将信息存储在云托管数据库中呢?

用于构建此类应用程序的方法很多,可能会令您不知所措。在本文中,我将演练我是如何使用 Windows Azure 中的后端选项来构建这种应用程序的,以及我是如何解决所遇到的各种问题的。我将介绍使用 Xamarin 编写 Windows Phone 及 iOS 应用程序的代码。

需要符合几个特征。该应用程序需要在多种移动设备和多种设备操作系统上运行。必须是本机应用程序(外观与设备上的其他所有应用程序类似)。后端服务器必须随时可用,对开发者(我)的困扰尽量少。对于跨平台开发部分,云服务必须提供尽可能多的帮助。后端数据库需要提供一定的地理定位功能。

为何选择 C#?

构建跨平台应用程序的方法包括:移动 Web 应用程序;用于 iPhone(和 Android)的 Xamarin C#;放弃跨平台特性,使用供应商主导语言(面向 Windows Phone 的 Microsoft .NET Framework 和面向 iPhone 的 Objective-C)构建应用程序;以及其他一些方法。首先,我适合选择 C#/.NET Framework 作为客户端语言。拥有 C# 背景意味着在了解设备的平台特定功能后,少了一个需要花时间学习的方面。对 iPhone 采用 Xamarin 解决方案,更重要的原因是能够在 Visual Studio 2013 中进行一切开发工作。移动 Web 方法的问题在于,用户希望应用程序尽量深入地与其平台集成。这对移动 Web 解决方案比较困难,但对本机解决方案更易实现。放弃跨平台特性采用供应商主导语言的方案并无意义,原因是:对于每一个平台,您都要学习一门新的语言。

开发工具

构建多平台应用程序通常需要多种开发工具。在 Xamarin.iOS 出现以前,iPhone 的开发工作只能借助 Xamarin Studio(之前称作 MonoDevelop,是开源的 SharpDevelop 的一个移植版本)在 Mac 上进行。虽说使用 Xamarin Studio 也行,但开发者通常希望继续使用自己熟悉的 IDE 进行开发。结合使用 Visual Studio 2013 和 Xamarin.iOS for Visual Studio,您不必放弃自己熟悉和喜欢的 IDE,即可完成 Windows Azure、Windows Phone 和 iPhone 的开发工作。

Windows Azure

移动设备可通过几种方法与 Windows Azure 交互。这些方法包括 Windows Azure 虚拟机(为简便起见,后文以 VM 指代)、Web 角色、Windows Azure 网站和 Windows Azure 移动服务 (WAMS)。

VM 能够提供对所有可变因素的最大控制。您可以更改应用程序及所有的服务器设置。这非常适合需要对底层操作系统设置、要安装的其他应用程序或其他可能的更改进行自定义的应用程序。这称为基础结构即服务 (IaaS)。

Windows Azure 中有一个包含一系列角色的云服务项目类型。在 Visual Studio 中,角色通常映射至项目。从根本上说,Web 角色是捆绑在上传并在 VM 中运行的单一可部署包中的 Web 项目。Web 角色提供 Web UI,内部还可能包含 Web 服务。辅助角色是将在服务器上持续运行的项目。这称为平台即服务 (PaaS)。

从概念上说,Windows Azure 网站与 Web 角色相似。这种解决方案允许应用程序托管 Web 网站或项目。项目可包含 Web 服务。这些 Web 服务可通过 SOAP 或 REST 调用进行调用。当应用程序只需要 IIS 时,Windows Azure 网站是绝佳的解决方案。

前三个方法需要您对 Web 服务有完整的认识 — 如何调用它们,如何在数据库中存储它们,熟悉移动设备与云之间的管道。Microsoft 提供了一个让您迅速、便捷地在云中存储数据、处理推送通知和对用户进行身份验证的解决方案:WAMS。用其他方法构建解决方案后,我认为选择 WAMS 更合理(原因:无需对管道有过多了解,且跨平台支持能力优秀)。有时,WAMS 被描述为“不必构建的后端”。

Windows Azure 移动服务

WAMS 提供了存储、针对多个社交网络的用户身份验证、借助 Node.js 在服务器上创建逻辑的机制、推送通知以及一个用于简化数据创建、读取、更新和删除 (CRUD) 操作的打包客户端代码库,以加快您的移动开发工作。

使用 WAMS 的第一步是创建移动服务和相关的数据库表。数据库表不是必需的。有关设置流程的更多信息,请参阅 Windows Azure 教程 (bit.ly/Nc8rWX)。

服务器脚本 在 WAMS 中,基本的 CRUD 操作通过服务器操作进行。它们是 delete.js、insert.js、read.js 和 update.js 文件,通过服务器上的 Node.js 进行处理。有关 WAMS 中的 Node.js 的更多信息,请参阅 Windows Azure 站点上的文章“在移动服务中使用服务器脚本”(bit.ly/1cHASFA)。

让我们首先了解一下 insert.js 文件(如图 1 所示)。在方法签名中,“item”参数包含所提交的数据。该对象的成员映射至从客户端提交的数据对象。了解从客户端填充数据这部分之后,比较容易理解这个对象的成员。“user”参数包含连接用户的相关信息。在本示例中,用户必须通过身份验证。应用程序使用 Facebook 和 Twitter 进行身份验证,因此,返回的 userId 的格式为“Network:12345678”。该值的“Network”部分包含网络提供商的名称。在本示例中,Facebook 或 Twitter 可用,因此,这两者之一构成了该值的一部分。数字“12345678”仅用于表示 userId。尽管本示例中使用的是 Twitter 和 Facebook,Windows Azure 还可以使用 Microsoft 或 Google 帐户。

图 1 将 Golf Drive 插入云中时所使用的 Insert.js 文件

function insert(item, user, request) {
  if ((!isNaN(item.StartingLat)) && (!isNaN(item.StartingLon)) &&
    (!isNaN(item.EndingLat)) && (!isNaN(item.EndingLon))) {
    var distance1 = 0.0;
    var distance2 = 0.0;
    var sd = item.StartingTime;
    var ed = item.EndingTime;
    var sdate = new Date(sd);
    var edate = new Date(ed);
    var res = user.userId.split(":");
    var provider = res[0].replace("'", "''");
    var userId = res[1].replace("'", "''");
    var insertStartingDate = sdate.getFullYear() + "-" +
       (sdate.getMonth() + 1) + "-" + sdate.getDate() + " " +
      sdate.getHours() + ":" + sdate.getMinutes() + ":" +
      sdate.getSeconds();
    var insertEndingDate = edate.getFullYear() + "-" +
      (edate.getMonth() + 1) + "-" + edate.getDate() + " " +
      edate.getHours() + ":" + edate.getMinutes() + ":" + 
      edate.getSeconds();
    var lat1 = item.StartingLat;
    var lon1 = item.StartingLon;
    var lat2 = item.EndingLat;
    var lon2 = item.EndingLon;
    var sp = "'POINT(" + item.StartingLon + " " + 
      item.StartingLat + ")'";
    var ep = "'POINT(" + item.EndingLon + " " + 
      item.EndingLat + ")'";
    var sql = "select Max(Distance) as LongDrive from Drive";
    mssql.query(sql, [], {
      success: function (results) {
        if ( results.length == 1)
        {
          distance1 = results[0].LongDrive;
        }
      }
    });
    var sqlDis = "select [dbo].[CalculateDistanceViaLatLon](?, ?, ?, ?)";
    var args = [lat1, lon1, lat2, lon2];
    mssql.query(sqlDis, args, {
      success: function (distance) {
        distance2 = distance[0].Column0;
      }
    });
    var queryString = 
      "INSERT INTO DRIVE (STARTINGPOINT, ENDINGPOINT, " +
      "STARTINGTIME, ENDINGTIME, Provider, UserID, " +
      "deviceType, deviceToken, chanelUri) VALUES " +
      "(geography::STPointFromText(" + sp + ", 4326), " +
      " geography::STPointFromText(" + ep + ", 4326), " +
      " '" + insertStartingDate + "', '" +
      insertEndingDate + "', '" + provider + "', " + userId + ", " +
      item.deviceType + ", '" + item.deviceToken.replace("'", "''") +
       "', " + "'" + item.ChannelUri.replace("'", "''") + "')";
    console.log(queryString);
    mssql.query(queryString, [], {
      success: function () {
        if (distance2 > distance1) {
          if (item.deviceType == 0) {
            push.mpns.sendFlipTile(item.ChannelUri, {
              title: "New long drive leader"
            }, {
                  success: function (pushResponse) {
                    console.log("Sent push:", pushResponse);
                  }
               });
            }
          if (item.deviceType == 1) {
            push.apns.send(item.deviceToken, {
              alert: "New Long Drive",
              payload: {
                inAppMessage: "Hey, there is now a new long drive."
              }
            });
          }
        }
      },
      error: function (err) {
        console.log("Error: " + err);
      }
    });
    request.respond(200, {});
  }
}

首先要做的是测试代码以验证输入。我需要验证所提交的纬度和经度是否为有效数字。如果不是,则立即退出插入。下一步是分析所传入的 userId,以获取网络提供商和数字用户标识符。第三步是设置日期,以便将其插入数据库。JavaScript 和 SQL Server 的日期/时间表示不匹配,所以必须进行分析并置为正确格式。

接下来需要完成查询工作。执行 CRUD 语句的 Node.js 命令调用 mssql.query(command, parameters, callbacks)。“command”参数是要执行的 SQL 命令。“parameters”参数是与所设置的命令中指定的参数匹配的 JavaScript 数组。“callbacks”参数包含查询完成后依据成功或出错情况而使用的 JavaScript 回调函数。我会在推送通知部分讨论成功初始查询的内容。

最后是调试问题。您如何知道脚本执行了哪些操作?JavaScript 有一个 console.log(info) 方法。以“info”参数调用该方法时,参数保存在服务的日志文件中,如图 2 所示。请注意屏幕右上角的内置刷新功能。

Log File Information in Visual Studio 2013
图 2 Visual Studio 2013 中的日志文件信息

设置 WAMS 后,可通过 windowsazure.com 门户或 Visual Studio 对其进行管理。

注:在默认设置情况下,从 WAMS 脚本文件调用方法可能会出错,原因在于它们以不同的架构运行。根据具体情况,可能需要授予权限。Jeff Sanders 发表过一篇关于这个问题的博客文章 (bit.ly/1cHQ4Cu)。

缩放

移动应用程序可能会对基础结构施加巨大负载,幸运的是,Windows Azure 有几种方法来处理这一问题。首先,关于消息队列,您有多种替代方法。

在 Windows Azure 中,队列可通过服务总线和 Windows Azure 队列服务实现。借助队列可在不占用应用程序的情况下快速存储数据。在重负载情况下,应用程序可以等待远程数据源响应。也可不直接与数据源交互,而是将数据存储在队列中,从而使应用程序能够继续处理其他工作。根据个人经验,使用队列可轻松提升应用程序的伸缩能力。虽然本应用程序未使用队列,但有必要提一下:在操作负载大或访问系统的移动设备数量多时,可以选择使用队列。幸运的是,服务总线和 Windows Azure 队列服务都提供了可供 WAMS 中的服务器脚本访问的必要 API。

总的说来,队列是数据密集型应用程序的理想解决方案。伸缩工具箱中的另一个工具是自动伸缩。Windows Azure 允许您通过仪表板监测应用程序的运行状况和可用性。您可以设置规则,以便在服务可用性降低时通知应用程序管理员。Windows Azure 允许应用程序根据需求增大或缩小。默认情况下,该功能为禁用状态。当启用时,Windows Azure 会定期检查关于服务的 API 调用数目,如果调用数目达到或超过 API 配额的 90%,就提高配额。每天,Windows Azure 会缩小到设定的最小值。基本规则是设置每日配额以处理预计的每日流量并允许 Windows Azure 根据需要提高配额。至本文撰写时止,可预览运行状况、监测和自动伸缩。

Database(数据库)

数据是所有应用程序的根基,是几乎所有业务的基础。您可以使用托管的第三方数据库、随 VM 运行的数据库服务、Windows Azure SQL 数据库和其他若干可能的选项。对于后端数据库,我选择使用 Windows Azure SQL 数据库而非在 VM 中运行的 SQL Server,原因有如下几个:首先,Windows Azure SQL 数据库的基础产品支持基于位置的服务。其次,Windows Azure SQL 数据库针对性能进行了优化,比在客户端系统上的 SQL Server 基准安装要好。最后,无需对底层系统进行持续管理。

Windows Azure SQL 数据库具有与 SQL Server 相同的点和地理位置数据库类型,因此,可方便地计算两点之间的距离。为方便起见,我编写了两个存储过程来计算这些距离。SQL 函数 Calculate­DistanceViaLatLon 接受浮点类型的经度值和纬度值。它设计为在 WAMS insert.js 脚本中运行,因而可方便地计算传入起终点的距离。可将结果与系统中的当前最大距离进行对比。SQL 函数 CalculateDistance 接受两个地理位置点并计算两者间的距离,如图 3 所示。数据在 Drive 表中存储为 SQL Server 点。

图 3 用于计算两点间距离的 SQL 函数

CREATE FUNCTION [dbo].[CalculateDistanceViaLatLon]
(
  @lat1 float,
  @lon1 float,
  @lat2 float,
  @lon2 float
)
RETURNS float
AS
BEGIN
  declare @g1 sys.geography = sys.geography::Point(@lat1, @lon1, 4326)
  declare @g2 sys.geography = sys.geography::Point(@lat2, @lon2, 4326)
  RETURN @g1.STDistance(@g2)
END
CREATE FUNCTION [dbo].[CalculateDistance]
(
  @param1 [sys].[geography],
  @param2 [sys].[geography]
)
RETURNS INT
AS
BEGIN
  RETURN @param1.STDistance(@param2)
END

图 4 是用于存储起终点数据的表。以“__”为前缀的列是特定于 Windows Azure 的列,因而这里避免使用它们。我们需要关注的列是 StartingPoint、EndingPoint、Distance、deviceToken 和 deviceType。StartingPoint 和 EndingPoint 列存放起、终地理位置点。Distance 列是计算结果列。它是使用 Calculate­Distance SQL 函数算出的浮点值。deviceToken 和 deviceType 列存放标识设备和设备类型(基于 Windows Phone 或 iPhone)的标记。该应用程序目前仅在输入新的起终点成为新的冠军时才向提交数据的设备发送消息。deviceToken 和 deviceType 列可用于推送新任冠军和定期推送关于竞争对手的其他更新。

图 4 存放起终点数据的 SQL 表

CREATE TABLE [MsdnMagGolfLongDrive].[Drive] (
  [id]            NVARCHAR (255)
     CONSTRAINT [DF_Drive_id] 
     DEFAULT (CONVERT([nvarchar](255),newid(),(0))) 
	 NOT NULL,
  [__createdAt]   DATETIMEOFFSET (3) CONSTRAINT
    [DF_Drive___createdAt] DEFAULT (CONVERT([datetimeoffset](3),
    sysutcdatetime(),(0))) NOT NULL,
  [__updatedAt]   DATETIMEOFFSET (3) NULL,
  [__version]     ROWVERSION         NOT NULL,
  [UserID]        BIGINT             NULL,
  [StartingPoint] [sys].[geography]  NULL,
  [EndingPoint]   [sys].[geography]  NULL,
  [DateEntered]   DATETIME           NULL,
  [DateUpdated]   DATETIME           NULL,
  [StartingTime]  DATETIME           NULL,
  [EndingTime]    DATETIME           NULL,
  [Distance]      AS                 ([dbo].[CalculateDistance]
    ([StartingPoint],[EndingPoint])),
  [Provider]      NVARCHAR (20)      NULL,
  [deviceToken]   NVARCHAR (100)     NULL,
  [deviceType] INT NULL,
  PRIMARY KEY NONCLUSTERED ([id] ASC)
);

动态架构

WAMS 的强大功能之一是:默认情况下,数据库表架构是动态的。它依据从移动设备发送给客户端的信息进行修改。从开发阶段转到生产阶段时,应禁用该功能。您最不愿碰到的事就是因为编程错误而造成正在运行的系统出现某种类型架构更改。要禁用该功能很容易,只需转到 Windows Azure 门户的 WAMS 部分中进行配置,将“动态架构”选项切换为禁用。

访问数据

与通过有线连接和延迟相对较低的网络访问数据的方式相比,使用移动设备通过不可靠和延迟相对较高的网络访问数据的方式有很大不同。设备在获取数据时存在两条一般规则。首先,数据访问必须异步完成。移动网络不可靠且延迟较高,以任何方式锁定 UI 线程都不是好主意。用户不明白 UI 为什么会卡住。如果获取数据花费时间过长,设备操作系统会认为应用程序已挂起并结束它。第二条规则是传输的数据必须相对较少。向移动设备发送过多记录会因以下原因而导致问题:移动运营商系统中常见的低速、高延迟网络;与专门用于处理数据的笔记本或台式机 CPU 不同,移动设备 CPU 的优化更倾向于实现低功耗。WAMS 解决了这两个问题。数据访问是异步的,查询通过分页算法自动完成。为了说明这一点,我将介绍两个操作:插入和选择。

使用代理 调用过基于 REST 的服务的开发者都知道 REST 存在缺少内置代理服务的问题。这使得在使用 REST 时多多少少容易出错。该问题可以避免 — 只是比使用 SOAP 要难一些。为方便开发,您可以在本地创建一个代理。图 5 是本示例中的代理。可在服务器脚本中访问对象实例的属性。

图 5 用于 REST 的代理

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
namespace Support
{
  public partial class Drive
  {
    public Drive() {
      DeviceToken = String.Empty;
      ChannelUri = String.Empty;
    }
    [JsonProperty(PropertyName="id")]
    public string Id { get; set; }
    [JsonProperty(PropertyName = "UserID")]
    public Int64 UserID { get; set; }
    [JsonProperty(PropertyName = "Provider")]
    public string Provider { get; set; }
    public double StartingLat { get; set; }
    public double StartingLon { get; set; }
    public double EndingLat { get; set; }
    public double EndingLon { get; set; }
    [JsonProperty(PropertyName = "StartingTime")]
    public DateTime StartingTime { get; set; }
    [JsonProperty(PropertyName = "EndingTime")]
    public DateTime EndingTime { get; set; }
    [JsonProperty(PropertyName = "Distance")]
    public double Distance { get; set; }
    [JsonProperty(PropertyName = "deviceType")]
    public int deviceType { get; set; }
    [JsonProperty(PropertyName = "deviceToken")]
    public string DeviceToken { get; set; }
    [JsonProperty(PropertyName = "ChannelUri")]
    public string ChannelUri { get; set; }
  }
}

查询数据 查询 Windows Azure 中的数据非常简单。数据将通过对 LINQ 查询的调用返回。下面是运行简单查询以返回数据的调用: 

var drives = _app.client.GetTable<Support.Drive>();
var query = drives.OrderByDescending(
  drive => drive.Distance).Skip(startingPoint).Take(PageSize);
var listedDrives = await query.ToListAsync();

在本示例中,我需要按降序排列的距离列表。然后,将之绑定到基于 Windows Phone 的设备和 iPhone 中的一个网格。虽然数据绑定部分不同,但检索数据部分完全一样。

注意:在前一查询中,未调用任何 Where 方法,但这可轻易地完成。另外,使用 Skip 和 Take 方法是为了说明将该应用程序加入分页是多么容易。图 6 显示了基于 Windows Phone 的设备和 iPhone 上的积分榜。

The Scoreboard As Depicted on a Windows Phone-Based Device and an iPhone
图 6 基于 Windows Phone 的设备和 iPhone 上所显示的积分榜

插入数据

在 WAMS 中,插入记录十分容易。只需创建一个数据对象实例,然后对客户端对象调用 InsertAsync 方法。图 7 是基于 Windows Phone 的设备的插入代码。在 Xamarin.iOS 中执行插入的代码与此相似,只是 deviceType、ChannelUri 和 DeviceToken 部分有所不同。

图 7 插入数据(基于 Windows Phone 的设备)

async void PostDrive()
{
  Drive d = new Drive();
  d.StartingLat = first.Latitude;
  d.StartingLon = first.Longitude;
  d.EndingLat = second.Latitude;
  d.EndingLon = second.Longitude;
  d.StartingTime = startingTime;
  d.EndingTime = endingTime;
  d.deviceType = (int)Support.AppConstants.DeviceType.WindowsPhone8;
  d.ChannelUri = _app.CurrentChannel.ChannelUri.ToString();
  try
  {
    await _app.client.GetTable<Support.Drive>().InsertAsync(d);
  }
  catch (System.Exception exc)
  {
    Console.WriteLine(exc.Message);
  }
}

平台间的代码共享

在平台间共享代码是一个重要考量,借助 C# 和 Xamarin 的跨平台功能可通过几种方式完成。所用的机制由具体情况确定。我需要共享非 UI 逻辑。为此,有两个方法:可移植类库 (PCL) 和链接文件。

可移植类库 有许多平台使用 .NET Framework。这些平台包括 Windows、Windows Phone、Xbox、Windows Azure 及 Microsoft 支持的其他平台。当 .NET Framework 初次发布时(以及经过多次迭代后),对于不同的平台,可能需要重新编译 .NET 代码。PCL 能够解决这一问题。借助 PCL 项目,您可以设置一个库,为目标平台提供一组特定的 API 支持。可在类库的项目设置中设置所选择的平台。

伴随 Microsoft 对 PCL 的支持,去年秋,Microsoft 更改了其 PCL 授权,允许在非 Microsoft 平台上提供支持。这使 Xamarin Inc. 能够在 iOS、Android 平台及 OS X 上为所定义的 Microsoft PCL 提供支持。PCL 使用方面的相关文档是现成的。

链接文件 PCL 是进行跨平台开发的理想解决方案。但是,如果某个平台包含另一平台所不具有的功能,可选择通过链接文件共享代码。链接文件安装包含一个基本的 .NET 类库、平台特定类库和平台应用程序项目。.NET 类库包含跨平台共享的通用代码。

平台特定库包含两类文件。它们是来自通用 .NET 类库的链接文件和包含常用 API 但采用不同平台特定实现的代码。其想法是将类库项目的文件通过“添加为链接”的方式添加到平台特定类库中。如图 8 所示。

Using Linked Files
图 8 使用链接文件

其他方法 PCL 和链接文件只是众多代码共享方法中的两个。其他方法包括:部分类、if/def 编译器选项、观察者模式、Xamarin.Mobile(及类似库)、通过 NuGet 提供的其他库(或 Xamarin Component Store)等等。

部分类允许跨共享类库和平台特定类库共享多个类文件。默认情况下,.NET 类库和平台特定库的命名空间不同。使用部分类时,最需要注意的问题是命名空间必须匹配。命名空间不匹配是使用部分类时的常见错误。

Visual Studio 允许通过 if/then 编译器选项指定需要编译哪些代码。利用这一点,可将平台定义为条件编译符号。这些是在项目属性中设置的,如图 9 所示,其中的 #if 指令用于有条件地编译 Windows Phone 代码。

Defining a Platform as a Conditional Compilation Symbol
图 9 将平台定义为条件编译符号

Xamarin.Mobile 是一组包含常用 API 的库。这些库可用于 Windows Phone、iOS 和 Android。Xamarin.Mobile 目前支持位置服务、联系人和照相机。我在应用程序中使用了 Xamarin.Mobile 的地理定位 API。

为确定位置,将平台特定地理定位对象包装为一个地理定位对象。在这里,它使用 C# 5.0 异步式语法。一旦位置确定,调用即进入 .ContinueWith 并执行处理:

geo = new Geolocator();
...
await geo.GetPositionAsync(timeout: 30000).ContinueWith(t =>
  {
    first = t.Result;
    LandingSpot.IsEnabled = true;
  }, TaskScheduler.FromCurrentSynchronizationContext());

注意:设备趋向于提供近似的地理位置。结果就是,不是记录的所有距离都非常精确。

在构建应用程序时,开发者倾向于考虑应用程序较高逻辑层到较低层的调用。例如,用户可以触摸触发位置检测的按钮。但当应用程序的较低逻辑层需要调用进入较高层时,问题出现了。一个简单的解决方案是从较高层向较低层传递引用。很遗憾,这几乎肯定会使较低层的代码无法跨平台共享。这可以通过事件解决。在较低层发出一个在较高层处理的事件。这就是观察者模式的基础。

众多第三方已创建了可跨平台使用的库。您可以通过 NuGet 和 Xamarin Component Store 找到这些库。

推送通知

有时,服务器应用程序需要与移动设备通信。这可通过 WAMS 或 Notification Hub 完成。在发送少量消息时,WAMS 是理想的解决方案。通知中心旨在向大量设备(如“高级客户”或“加利福尼亚州的所有客户”)发送消息。我将讨论 WAMS 方法。

您可以在服务器脚本中调用到移动设备的 WAMS 推送通知。Windows Azure 处理推送通知的许多复杂方面,但它无法将所有差异都抽象到一种(向所有不同平台发送消息的)方式中。幸运的是,这些差异很细微。

mpns 对象用于通过 Microsoft 推送通知服务 (MPNS) 发送消息。mpns 对象包含四个成员。这些成员是 sendFlipTile、sendTile、sendToast 和 sendRaw。这些成员拥有类似的签名。第一个参数是用于通信的通道。第二个参数是 JSON 对象(包含要发送至设备的参数)。第三个参数是在请求成功或失败时执行的一组回调函数。下面使用 mpns 对象的代码用在 Windows Azure 服务器脚本中,可在远距赛中出现新冠军时发送一条消息:

push.mpns.sendFlipTile(item.ChannelUri, {
  title: "New long drive leader"
}, {
    success: function (pushResponse) {
      console.log("Sent push:", pushResponse);
    }

其结果是对磁贴的更新,如图 10 所示。请注意图中的磁贴是如何更新为“New long drive leader”的。

A Push Message Showing a New Long Drive Leader
图 10 显示 New Long Drive Leader 的推送消息

在 WAMS 脚本中,可以使用 apns 对象向 Apple Push Notification Services (APNS) 发送消息。其概念与 mpns 对象类似。最值得注意的成员是发送方法。其签名类似于 mpns 的发送方法。签名包含三个参数:deviceToken(用于唯一标识设备);基于 JSON 的参数对象;最后一个参数是一组回调函数。

下面的代码说明如何使用 apns 对象向 iOS 设备发送“new leader”消息:

push.apns.send(item.deviceToken, {
  alert: "New Long Drive",
  payload: {
    inAppMessage: "Hey, there is now a new long drive."
  }
});

图 11 是要添加到 AppDelegate.cs 文件中的代码(用于处理发送至 iPhone 的消息)。在本示例中,向用户显示一个 UIAlertView。

图 11 在 iPhone 上处理消息

public override void RegisteredForRemoteNotifications(
  UIApplication application, NSData deviceToken)
{
  string trimmedDeviceToken = deviceToken.Description;
  if (!string.IsNullOrWhiteSpace(trimmedDeviceToken))
  {
    trimmedDeviceToken = trimmedDeviceToken.Trim('<');
    trimmedDeviceToken = trimmedDeviceToken.Trim('>');
  }
  DeviceToken = trimmedDeviceToken;
}
public override void ReceivedRemoteNotification(
  UIApplication application, NSDictionary userInfo)
{
  System.Diagnostics.Debug.WriteLine(userInfo.ToString());
  NSObject inAppMessage;
  bool success = userInfo.TryGetValue(
    new NSString("inAppMessage"), out inAppMessage);
  if (success)
  {
    var alert = new UIAlertView("Got push notification",
      inAppMessage.ToString(), null, "OK", null);
    alert.Show();
  }
}

如果需要,可以使用 gcm 对象向 Google Cloud Messaging (GCM) 平台发送消息。

Windows 与 Apple 推送通知(以及 Google 通知)的最大不同之处在于客户端移动系统处理这些消息的方式。随附代码下载中的 AppDelegate.cs 文件内的 Xamarin.iOS 项目包含完整的客户端系统列表。

就是这样。祝您的移动应用程序开发工作和高尔夫比赛一切顺利!

Wallace B. McClure 毕业于佐治亚理工学院 (Georgia Tech),拥有电子工程专业学士学位和硕士学位。他为大公司和小公司做过咨询和开发工作。McClure 撰写过有关使用 Xamarin.iOS 进行 iPhone 编程、使用 Xamarin.Android 进行 Android 编程、应用程序体系结构、ADO.NET 和 SQL Server 以及 AJAX 的书籍。他是 Microsoft MVP、ASPInsider、Xamarin MVP 和 Xamarin Insider。此外,他还是 Scalable Development Inc. 的合伙人。可通过 Learn Now Online 获得其关于 iOS 和 Android 的培训材料。他的博客在 morewally.com,您也可以通过 Twitter twitter.com/wbm 关注他。

衷心感谢以下技术专家对本文的审阅:Kevin Darty(独立签约人)和 Brian Prince (Microsoft)