Xamarin-跨平台应用开发第二版-全-
Xamarin 跨平台应用开发第二版(全)
原文:
zh.annas-archive.org/md5/3cae8b23411785f9b3b7d596129889b4译者:飞龙
前言
Xamarin 为开发 iOS 和 Android 应用程序的 C#构建了三个核心产品:Xamarin Studio、Xamarin.iOS 和 Xamarin.Android。Xamarin 让您直接访问每个平台的本地 API,并能够在平台之间共享 C#代码。与 Java 或 Objective-C 相比,使用 Xamarin 和 C#,您可以获得更高的生产力,同时与 HTML 或 JavaScript 解决方案相比,仍能保持出色的性能。
在本书中,我们将开发一个真实世界的示例应用程序,以展示您可以使用 Xamarin 技术做什么,并基于 iOS 和 Android 的核心平台概念进行构建。我们还将涵盖高级主题,如推送通知、检索联系人、使用相机和 GPS 定位。随着 Xamarin 3 的推出,引入了一个名为 Xamarin.Forms 的新框架。我们将介绍 Xamarin.Forms 的基础知识以及如何将其应用于跨平台开发。最后,我们将指导您了解将应用程序提交到 Apple App Store 和 Google Play 所需的内容。
本书涵盖的内容
第一章,设置 Xamarin,介绍了安装适当的 Xamarin 软件和进行跨平台开发所需的本地 SDK 的过程。
第二章,嘿,平台!,引导您在 iOS 和 Android 上创建第一个“Hello World”应用程序,同时也涵盖了每个平台的一些基本概念。
第三章,iOS 和 Android 之间的代码共享,提供了可以与 Xamarin 一起使用的代码共享技术和项目设置策略。
第四章,XamChat – 一个跨平台应用,介绍了一个我们将全书构建的示例应用程序。在本章中,我们将编写应用程序的所有共享代码,包括单元测试。
第五章,XamChat for iOS,涵盖了实现 XamChat 的 iOS 用户界面以及各种 iOS 开发概念的技术。
第六章,XamChat for Android,涵盖了实现 XamChat 的 Android 版本的技术,并介绍了 Android 特定的开发概念。
第七章,在设备上部署和测试,引导您通过将第一个应用程序部署到设备的痛苦过程。我们还讨论了为什么始终在真实设备上测试您的应用程序很重要。
第八章,带有推送通知的 Web 服务,解释了使用 Azure Mobile Services 实现 XamChat 真实后端 Web 服务的技巧。
第九章, 第三方库,涵盖了使用 Xamarin 的各种第三方库选项,以及您甚至可以利用原生 Java 和 Objective-C 库。
第十章, 联系人、相机和位置,介绍了 Xamarin.Mobile 库作为跨平台访问用户联系人、相机和 GPS 位置的方法。
第十一章, Xamarin.Forms,介绍了 Xamarin 的最新框架,Xamarin.Forms,以及您如何利用它来构建跨平台应用程序。
第十二章, App Store 提交,解释了将您的应用程序提交到苹果 App Store 和谷歌 Play 的过程。
您需要为本书准备的内容
为了使用本书,您需要一个至少运行 OS X 10.7 Lion 的 Mac 计算机。苹果要求 iOS 应用程序必须在 Mac 上编译,因此 Xamarin 也是如此。您还需要拥有 Xamarin.Android 和 Xamarin.iOS 商业版的许可证。还提供免费 30 天试用版。您还可以尝试免费的 Xamarin 入门版,但某些更高级的示例可能无法使用此版本。您可以通过访问 xamarin.com/download 下载适当的软件。
本书面向的对象
本书是为已经熟悉 C# 并想开始使用 Xamarin 进行移动开发的开发者编写的。如果您在 ASP.NET、WPF、WinRT 或 Windows Phone 上工作过,那么使用本书开发原生 iOS 和 Android 应用程序将得心应手。
约定
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和推特用户名应如下所示:"用户将在 UITextField 文本框中输入一些文本,然后点击 UIButton 按钮以开始搜索。"
代码块如下所示:
public override void ViewDidLoad()
{
base.ViewDidLoad();
int count = 0;
button.TouchUpInside += (sender, e) =>
label.Text = string.Format("Count: {0}", ++count);
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
public override void ViewDidLoad()
{
base.ViewDidLoad();
int count = 0;
button.TouchUpInside += (sender, e) =>
label.Text = string.Format("Count: {0}", ++count);
}
任何命令行输入或输出都应如下所示:
keytool -genkey -v -keystore <filename>.keystore -alias <key-name> -keyalg RSA -keysize 2048 -validity 10000
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"点击 下一步 按钮将您带到下一屏幕。"
注意
警告或重要注意事项如下所示。
小贴士
小技巧和技巧看起来像这样。
读者反馈
我们欢迎读者的反馈。请告诉我们您对本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。
要向我们发送一般反馈,请简单地发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。
如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,您已经成为 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从www.packtpub.com下载您购买的所有 Packt 出版物的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
下载本书的颜色图像
我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从以下链接下载此文件:www.packtpub.com/sites/default/files/downloads/7883OS_ImageBundle.pdf。
勘误
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
盗版
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过<copyright@packtpub.com>与我们联系,并提供涉嫌盗版材料的链接。
我们感谢您的帮助,保护我们的作者和我们为您提供有价值内容的能力。
问题
如果您对本书的任何方面有问题,您可以联系我们的邮箱 <questions@packtpub.com>,我们将尽力解决问题。
第一章. 设置 Xamarin
Xamarin 的开发工具使我们能够使用 C#开发原生 iOS、Android 和 Mac 应用程序,C#是最受欢迎的编程语言之一。选择 Xamarin 而不是 Java 和 Objective-C 来开发移动应用程序有许多优点。您可以在两个平台之间共享代码,并通过利用 C#的高级语言特性和.NET 基类库来提高生产率。否则,您将不得不为 Android 和 iOS 分别编写两次应用程序,并且在使用 Objective-C 时失去垃圾回收的好处。
与使用 JavaScript 和 HTML 开发跨平台应用程序的其他技术相比,Xamarin 也有一些独特的优势。C#通常比 JavaScript 性能更好,Xamarin 为开发者提供了直接访问每个平台原生 API 的能力。这使得 Xamarin 应用程序具有原生外观,并且性能与 Java 或 Objective-C 的对应物相似。
Xamarin 的工具通过将您的 C#编译成原生ARM可执行文件来工作,这些文件可以打包成 iOS 或 Android 应用程序。它将 Mono 运行时的精简版与您的应用程序捆绑在一起,只包含您的应用程序使用的基类库的功能。

在本章中,我们将设置您开始使用 Xamarin 开发所需的一切。到本章结束时,我们将安装所有必要的 SDK 和工具,以及提交应用程序商店所需的全部开发者账户。
在本章中,我们将介绍:
-
Xamarin 工具和技术简介
-
安装 Xcode,苹果的 IDE
-
设置所有 Xamarin 工具和软件
-
设置 Android 模拟器
-
加入 iOS 开发者计划
-
注册 Google Play
Xamarin 工具
Xamarin 为开发跨平台应用程序开发了三个核心产品:Xamarin Studio(以前称为 MonoDevelop)、Xamarin.iOS(以前称为 MonoTouch)和Xamarin.Android(以前称为 Mono for Android)。这些工具允许开发者利用 iOS 和 Android 上的原生库,并且建立在 Mono 运行时之上。
Mono,C#和.NET 框架的开源实现,最初由 Novell 开发,用于 Linux 操作系统。由于 iOS 和 Android 在类似程度上基于 Linux,Novell 能够开发 MonoTouch 和 Mono for Android 作为针对新移动平台的产品。在它们发布后不久,另一家公司收购了 Novell,Mono 团队离开了 Novell,成立了一家新公司。不久之后,Xamarin 被创立,专注于完全使用 C#在 iOS 和 Android 上开发这些工具。
准备一台开发机器以用于跨平台应用程序开发可能需要一些时间。更糟糕的是,苹果和谷歌都对它们各自平台上的开发有自己的要求。让我们来看看您的机器上需要安装的内容。
要开始使用 iOS,我们需要安装以下软件:
-
Xcode:这是开发 iOS 和 Mac 应用程序的核心 IDE,使用 Objective-C 语言。
-
Xcode 命令行工具:这些工具安装在 Xcode 中,为开发者提供了常用的命令行工具和脚本语言,如 Subversion、Git、Perl 和 Ruby。
-
Mac 的 Mono 运行时:这是在 OS X 上编译和运行 C#程序所必需的。
-
Xamarin.iOS:这是 Xamarin 针对 iOS 开发的核心产品。
Android 开始使用也需要安装以下软件:
-
Java:这是在 OS X 上运行 Java 应用程序的核心运行时环境。
-
Android SDK:它包含 Google 的标准 SDK、设备驱动程序和用于原生 Android 开发的模拟器。
-
Mac 的 Mono 运行时:这是在 OS X 上编译和运行 C#程序所必需的。
-
Xamarin.Android:这是 Xamarin 针对 Android 开发的核心产品。
每个安装过程都需要一些时间。如果你可以访问快速的网络连接,这将有助于加快安装和设置过程。一切准备就绪后,让我们一步一步地进行,希望我们可以跳过一些你可能遇到的死胡同。
小贴士
需要注意的是,尽管本书没有涉及,Xamarin 也可以在 Windows 和 Visual Studio 上使用。iOS 开发需要 Mac,因此 Windows 开发者必须将 Visual Studio 连接到 Mac 以编译 iOS 应用程序。幸运的是,本书中我们学到的很多东西可以直接应用于在 Windows 上使用 Xamarin。
安装 Xcode
为了使事情进展得更顺利,让我们首先安装 Mac 的 Xcode。除了 Apple 的 IDE,它还会安装 Mac 上最常用的开发者工具。确保你有至少 OS X 10.8(Mountain Lion)的版本,并在 App Store 中找到 Xcode,如图所示:

下载和安装这个过程可能会花费相当长的时间。我建议你花点时间享受一杯美妙的咖啡或着手另一个项目来打发时间。
解决完这些问题后,首次启动 Xcode,并逐步完成初始启动对话框。接下来,导航到Xcode | 首选项…以打开 Xcode 的主设置对话框。
在下载选项卡中,你会注意到可以在 Xcode 中安装的几个附加包。在这里,你可以下载官方 iOS 文档,Xamarin 安装程序将使用这些文档。可选地,你可以安装较旧的 iOS 模拟器,但我们可以使用本书内容的默认模拟器。完成安装后,你的 Xcode 的组件部分应该看起来类似于以下截图:

安装 Xcode 将安装 iOS SDK,这是 iOS 开发的一般要求。由于苹果的限制,iOS SDK 只能在 Mac 上运行。Xamarin 已经尽一切可能确保他们遵循苹果的 iOS 指南,例如限制动态代码生成。Xamarin 的工具也尽可能利用 Xcode 的功能,以避免重新发明轮子。
安装 Xamarin
在安装 Xcode 之后,还需要安装几个其他依赖项,以便在 Xamarin 的工具中进行开发。幸运的是,Xamarin 通过创建一个整洁的一站式安装程序来改善了体验。
通过以下步骤安装免费的 Xamarin 入门版:
-
访问
Xamarin.com,然后点击大型的立即下载按钮。 -
填写一些关于你自己的基本信息。
-
下载
XamarinInstaller.dmg文件并将磁盘镜像挂载。 -
启动
Install Xamarin.app并接受任何出现的 OS X 安全警告。 -
通过安装程序进行操作;默认选项将正常工作。你可以选择安装Xamarin.Mac,但这个主题在本书中没有涉及。
Xamarin 安装程序将下载并安装必要的先决条件,例如 Mono 运行时、Java、Android SDK(包括 Android 模拟器和工具),以及你需要启动和运行所需的一切。
你最终会得到以下截图所示的内容,然后我们可以继续征服跨平台开发中的更大主题:

选择 Xamarin 许可证
Xamarin 的工具对于普通观察者来说可能有点贵,但我倾向于认为这是使用像 C#这样的更高效的语言所能节省的时间。此外,他们的产品通过允许你开发跨平台应用程序而不是在 Java 和 Objective-C 中分别编写两次,可以为你节省相当一部分的开发时间。
Xamarin 有几个版本,因此了解这些版本之间的差异,以便确定你可能需要购买哪种许可证是很好的。版本如下:
-
入门版:仅对个人提供,并且编译用户代码的限制为 64 KB。某些功能不可用,例如 Xamarin.Forms 框架和调用第三方原生库。
-
独立版:仅对个人提供,并且不包括 Visual Studio 支持。
-
商业版:这是为公司提供的;它为 Visual Studio 添加了功能,并包括更好的 Xamarin 产品支持。
-
企业版:这包括 Xamarin 组件存储库中的主要组件免费使用,以及许多其他 Xamarin 支持选项,例如热修复和小于 24 小时的问题响应时间。
设置 Android 模拟器
与在物理设备上开发相比,Android 模拟器在历史上一直以运行缓慢而闻名。为了帮助解决这个问题,Google 开发了一个新的 x86 模拟器,该模拟器在桌面计算机上支持硬件加速。它默认不在 Android 虚拟设备(AVD)管理器中安装,所以让我们设置一下。
通过以下步骤安装 x86 Android 模拟器:
-
打开 Xamarin Studio。
-
导航至 工具 | 打开 Android SDK 管理器…。
-
滚动到 附加组件;安装 Intel x86 模拟器加速器 (HAXM)。这将下载一个安装程序,我们需要运行它。
-
打开 Finder 并按 Command + Shift + G 打开导航弹出窗口。
-
导航至
~/Library/Developer/Xamarin/android-sdk-macosx/extras/intel并安装适当的软件包(根据您的 Mac OS X 版本)。 -
滚动到 Android 4.4.2 (API 19);安装 Intel x86 Atom 系统镜像。
-
可选地,安装您感兴趣的任何其他软件包。作为一个快捷方式,Android SDK 管理器会自动为您选择默认安装的某些软件包。
-
关闭 Android SDK 管理器并切换回 Xamarin Studio。
-
导航至 工具 | 打开 Android 模拟器管理器…。
-
点击 创建…。
-
输入您选择的 AVD 名称,例如
x86 模拟器。 -
选择一个适合您显示器的通用设备,例如具有 4" WVGA 显示屏的设备。
-
在 目标 中,确保您选择了 Intel x86 Atom 系统镜像。
-
创建设备后,继续点击 开始… 以确保模拟器正常运行。
模拟器启动需要一些时间,因此在执行 Android 开发时保持模拟器运行是个好主意。Xamarin 在这里使用标准的 Android 工具,所以您在使用 Java 进行开发时也会遇到同样的问题。如果一切正常启动,您将看到 Android 启动屏幕,然后是一个虚拟的 Android 设备,准备从 Xamarin Studio 部署应用程序,如下面的截图所示:

注册 iOS 开发者计划
要部署到 iOS 设备,Apple 要求您加入其 iOS 开发者计划。会员费为每年 99 美元,并允许您部署 200 台设备进行开发。您还可以访问测试服务器,以实现更高级的 iOS 功能,如应用内购买、推送通知和 iOS 游戏中心。在物理设备上测试您的 Xamarin.iOS 应用程序非常重要,因此我建议在开始 iOS 开发之前获取一个账户。在桌面上的模拟器运行与真实移动设备上的性能有很大不同。还有一些仅在真实设备上运行时才会发生的 Xamarin 特定优化。我们将在后面的章节中全面介绍在设备上测试您的应用程序的原因。
通过以下步骤注册 iOS 开发者计划:
-
点击立即注册。
-
使用现有的 iTunes 账户登录或创建一个新的账户。这之后无法更改,因此请选择一个适合您公司的账户。
-
您可以选择作为个人或公司注册。两者价格均为 99 美元;但是,作为公司注册将需要通过您公司的会计将文件传真给苹果公司。
-
审查开发者协议。
-
填写苹果公司的开发者调查问卷。
-
购买 99 美元的开发者注册费。
-
等待确认电子邮件。
您应该在两个工作日内收到一封看起来类似于以下截图的电子邮件:

从这里,我们可以继续设置您的账户:
-
您可以点击收到的电子邮件中的立即登录,或者前往
itunesconnect.apple.com。 -
使用您之前的 iTunes 账户登录。
-
同意仪表板主页上出现的任何附加协议。
-
从 iTunes Connect 仪表板导航到协议、税务和银行。
-
在本节中,您将看到三列,分别是联系信息、银行信息和税务信息。
-
在所有这些部分中填写您账户的相关信息。对于公司账户,可能需要会计的帮助。
一切完成后,您的合同、税务和银行部分应该看起来类似于以下截图:

在您的 iOS 开发者账户成功注册后,您现在将能够部署到 iOS 设备并将您的应用程序发布到苹果应用商店。
注册为 Google Play 开发者
与 iOS 不同,将您的应用程序部署到 Android 设备是免费的,只需在您的设备设置中进行一些更改。Google Play 开发者账户只需一次性费用 25 美元,并且不需要每年续费。然而,就像 iOS 一样,您将需要一个 Google Play 账户来开发应用内购买、推送通知或 Google Play 游戏服务。如果您不可避免地计划将应用程序提交到 Google Play 或需要实现这些功能之一,我建议您提前设置一个账户。
要注册为 Google Play 的开发者,请执行以下步骤:
-
使用现有的 Google 账户登录或创建一个新的账户。这之后无法更改,因此如果需要,请选择一个适合您公司的账户。
-
接受协议并输入您的信用卡信息。
-
选择一个开发者名称并输入您账户的其他重要信息。再次强调,请选择适合您公司的名称,以便用户在应用商店中看到。
如果一切填写正确,您最终将得到以下 Google Play 开发者控制台:

如果您计划销售付费应用或应用内购买,在此阶段,我建议您设置您的Google 商户账户。这将使 Google 能够通过应用适当的税法在您的国家支付您的应用销售收入。如果您是为公司设置此账户,我建议您寻求您公司会计或簿记员的帮助。
以下是为设置 Google 商户账户的步骤:
-
点击设置商户账户按钮。
-
使用您的 Google 账户再次登录。
-
填写销售应用的适当信息:地址、电话号码、税务信息以及将在客户信用卡账单上显示的显示名称。
完成后,您会看到设置商户账户的帮助提示现在已从开发者控制台中消失,如下面的截图所示:

到此为止,人们可能会认为我们的账户已经完全设置好了,但在能够销售应用之前,还有一个至关重要的步骤:我们必须输入银行信息。
设置您的 Google 商户账户的银行信息可以通过以下步骤完成:
-
返回
play.google.com/apps/publish的 Google Play开发者控制台。 -
点击财务报告部分。
-
点击标题为访问您的商户账户以获取详细信息的小链接。
-
您应该会看到一个警告,表明您尚未设置银行账户。点击指定银行账户链接开始操作。
-
输入您的银行信息。再次提醒,可能需要公司会计的帮助。
-
几天后,查看您的账户中来自 Google 的小额存款。
-
通过访问
checkout.google.com/sell来确认金额。 -
点击设置标签,然后点击财务。
-
接下来,点击验证账户。
-
输入您银行账户上显示的金额,然后点击验证存款。
您的 Google 商户账户也是您取消或退款客户订单的地方。Google Play 与 iOS 应用商店不同,所有客户问题都直接指向开发者。
摘要
在本章中,我们讨论了使用 C#开发 Android 和 iOS 应用程序的 Xamarin 核心产品:Xamarin Studio、Xamarin.iOS 和 Xamarin.Android。我们安装了 Xcode,然后运行了 Xamarin 一站式安装程序,该程序安装了 Java、Android SDK、Xamarin Studio、Xamarin.iOS 和 Xamarin.Android。我们设置了 x86 Android 模拟器,以便在调试应用程序时获得更快速、更流畅的体验。最后,我们为分发我们的应用程序设置了 iOS 和 Google Play 开发者账户。
在本章中,你应该已经掌握了开始使用 Xamarin 构建跨平台应用程序所需的一切。你的开发计算机应该已经准备就绪,并且你应该已经安装了所有原生 SDK,并准备好创建下一个震撼世界的伟大应用。
本章中的概念将为我们准备更高级的主题,这些主题需要安装适当的软件以及拥有苹果和谷歌的开发者账户。我们将部署应用程序到真实设备,并实现更多高级功能,例如推送通知。在下一章中,我们将创建我们的第一个 iOS 和 Android 应用程序,并涵盖每个平台的基础知识。
第二章。你好,平台!
如果您熟悉在 Windows 上使用 Visual Studio 开发应用程序,那么使用 Xamarin Studio 应该非常简单。Xamarin 使用与 Visual Studio 相同的解决方案概念,其中包含一个或多个项目,并为 iOS 和 Android 应用程序创建了几个新的项目类型。还有几个项目模板可以帮助您快速开始开发常见应用程序。
Xamarin Studio 支持多种开箱即用的项目类型,包括标准的.NET 类库和控制台应用程序。您无法在 Mac 上使用 Xamarin Studio 原生开发 Windows 应用程序,但您当然可以在 Xamarin Studio 中开发应用程序的共享代码部分。我们将在后面的章节中关注代码共享,但请记住,Xamarin 使您能够在支持 C#的大多数平台上共享通用的 C#后端。
在本章中,我们将介绍:
-
为 iOS 创建“Hello World”应用程序
-
苹果的 MVC 模式
-
Xcode 和故事板
-
为 Android 创建“Hello World”应用程序
-
Android 活动
-
Xamarin 的 Android 设计器
构建您的第一个 iOS 应用程序
启动 Xamarin Studio 并开始一个新的解决方案。就像在 Visual Studio 中一样,可以从新建解决方案对话框创建许多项目类型。Xamarin Studio(以前称为 MonoDevelop)支持开发许多不同类型的项目,例如针对 Mono 运行时的 C#控制台应用程序、NUnit 测试项目,甚至除了 C#之外的其他语言,如 VB 或 C++。
Xamarin Studio 支持以下 iOS 项目类型:
-
iPhone 或 iPad 项目:这些项目类别使用故事板来布局 UI,并且仅针对 iPad 或 iPhone。
-
通用项目:此类别支持同一 iOS 应用程序中的 iPhone 和 iPad。如果您需要针对这两种类型的设备,则这是首选的项目类型。
-
单视图应用程序:这是基本的项目类型,它设置了一个 iOS 故事板、一个视图和一个控制器。
-
标签应用程序:这是一个自动设置UITabViewController的项目类型,用于具有标签布局的应用程序。
-
WebView 应用程序:此项目类型用于创建部分 HTML 和部分本地的混合应用程序。应用程序已配置为利用 Xamarin Studio 的 Razor 模板功能。
-
iOS 绑定项目:这是一个 iOS 项目,可以为目标 C#库创建 Objective-C 绑定。
-
iOS 单元测试项目:这是一个特殊的 iOS 应用程序项目,可以运行 NUnit 测试。
-
iOS 库项目:这是一个用于其他 iOS 应用程序项目的类库。
要开始,请导航到iOS | iPhone,并在您选择的目录中创建单视图应用程序,如图所示:

你会注意到,从项目模板中自动创建了几个文件和文件夹。这些文件如下:
-
组件:这个文件夹将包含从 Xamarin 组件商店添加的任何组件。有关 Xamarin 组件商店的更多信息,请参阅第九章,第三方库。 -
资源:这个目录将包含你想要直接复制到应用程序包中的任何图像或纯文本文件。请注意,默认情况下,它将包含一个黑色启动画面图像。这确保了你的 iOS 应用程序在 iPhone 5 上全屏运行。 -
AppDelegate.cs:这是苹果处理应用程序级事件的主要类。 -
Entitlements.plist:这是一个苹果用来声明某些 iOS 功能(如推送通知和 iCloud)权限的设置文件。你通常只需要为高级 iOS 功能使用它。 -
*ViewController.cs:这是代表你应用程序第一个屏幕的控制器。它将具有与你的项目相同的名称。 -
Info.plist:这是苹果的manifest文件版本,可以声明你的应用程序的各种设置,如应用程序标题、图标、启动画面和其他常见设置。 -
Main.cs:这个文件包含 C#程序的常规入口点:static void Main()。你很可能不需要修改这个文件。 -
MainStoryboard.storyboard:这是你应用程序的故事板定义文件。它将包含你应用程序中视图的布局、控制器列表以及用于在应用程序中导航的过渡。
现在,让我们运行应用程序,看看从项目模板中默认得到什么。点击 Xamarin Studio 左上角的大号播放按钮。你会看到运行你第一个 iOS 应用程序的模拟器,如下面的截图所示:

到目前为止,你的应用程序只是一个普通的白色屏幕,既不令人兴奋也不实用。在继续前进之前,让我们先对 iOS 开发有一个更深入的了解。
根据你的应用程序的最低 iOS 目标,你还可以在不同的 iOS 模拟器版本上运行应用程序。苹果还提供了 iPad 和市场上所有不同 iOS 设备的模拟器。重要的是要知道,这些是模拟器而不是仿真器。仿真器将运行移动操作系统的封装版本(就像 Android 一样)。仿真器通常表现出较慢的性能,但提供了更接近真实操作系统的复制品。苹果的模拟器在原生 Mac 应用程序中运行,并不是真正的操作系统。好处是,与 Android 仿真器相比,它们运行得非常快。
理解苹果的 MVC 模式
在深入 iOS 开发之前,了解苹果的设计模式以在 iOS 上开发是非常重要的一步。你可能已经使用过模型-视图-控制器(MVC)模式与其他技术如ASP.NET一起使用,但苹果以略不同的方式实现了这个范式。
MVC 设计模式包括以下内容:
-
模型:这是驱动应用程序的后端业务逻辑。这可以是任何代码,例如,向服务器发送网络请求或将数据保存到本地的SQLite数据库。
-
视图:这是屏幕上实际的用户界面。在 iOS 术语中,这是任何从
UIView派生的类。例如,工具栏、按钮以及用户会在屏幕上看到并与之交互的任何内容。 -
控制器:这是 MVC 模式中的工作马。控制器与模型层交互,并使用结果更新视图层。与视图层类似,任何控制器类都将从
UIViewController派生。这是 iOS 应用程序中大部分代码所在的地方。
下图展示了 MVC 设计模式:

为了更好地理解这个模式,让我们通过以下常见场景的例子来引导你:
-
我们有一个 iOS 应用程序,其中包含一个搜索框,需要查询网站以获取工作列表。
-
用户将在
UITextField文本框中输入一些文本,并点击UIButton按钮以开始搜索。这是视图层。 -
一些代码将通过与视图交互来响应按钮,显示一个
UIActivityIndicatorView加载指示器,并调用另一个类中的方法来执行搜索。这是控制器层。 -
在被调用的类中将会发起一个网络请求,并异步返回工作列表。这是模型层。
-
控制器将随后更新视图以显示工作列表并隐藏加载指示器。
注意
关于苹果的 MVC 模式更多信息,请参阅developer.apple.com/library/mac/documentation/general/conceptual/devpedia-cocoacore/MVC.html的文档网站。
需要注意的一点是,在应用程序的模型层,你可以自由地做任何你想做的事情。这就是我们可以使用可以在其他平台如 Android 上重用的纯 C#类的地方。这包括使用 C# 基础类库(BCL)的任何功能,例如处理 Web 服务或数据库。本书后面我们将更深入地探讨跨平台架构和代码共享的概念。
使用 iOS 设计器
由于我们的纯白色应用程序相当无聊,让我们通过添加一些控件来修改应用程序的 View 层。为此,我们将修改 Xamarin Studio 中项目中的MainStoryboard.storyboard文件。可选地,你可以在 Xcode 中打开故事板文件,这是在 Xamarin Studio 中提供设计器之前编辑故事板文件的方法。使用 Xcode 仍然可能很有用,如果 iOS 故事板中有一些功能在 Xamarin 设计器中尚未提供,或者你需要编辑较旧的 iOS 格式,如XIB文件。然而,Xcode 的体验并不那么好,因为 Xcode 中的自定义控件渲染为纯白色方块。Xamarin 的设计器实际上在自定义控件中运行你的绘图代码,这样你就可以准确地看到应用程序在运行时的外观。

让我们通过以下步骤向我们的应用程序添加一些控件:
-
打开本章中创建的项目,在 Xamarin Studio 中打开。
-
双击
MainStoryboard.storyboard文件。 -
iOS 设计器将打开,你将看到应用程序中单个控制器的布局。
-
在右侧的文档大纲选项卡中,你会看到你的控制器在其布局层次结构中包含一个视图。
-
在左上角,你会注意到一个工具箱,其中包含你可以拖放到控制器视图上的几种类型的对象。
-
在搜索框中,搜索
UILabel并将标签拖放到你选择的视图位置。 -
双击标签以编辑标签的文本为任何你想要的。你也可以从底部右角的属性选项卡中填写这个值。
-
同样,搜索
UIButton并将按钮拖放到视图中的某个位置,位于标签上方或下方。你可以使用属性选项卡编辑按钮上的文本。双击按钮将添加一个点击事件处理程序,就像你在为其他平台开发时在 Visual Studio 中熟悉的那样。 -
运行应用程序。
你的应用程序应该看起来更像一个真正的应用程序,如下面的截图所示:

现在,你可能想知道在这个时候如何向应用添加用户交互选项。在 Xcode 的 iOS 设计器中,你可以创建一个outlet,这将使每个视图从 C#中可见。outlet 是 Storyboard 或XIB文件中视图的引用,在运行时会用视图的实例填充。你可以将这个概念与其他技术(如 ASP.NET MVC、WebForms 或Windows Presentation Foundation(WPF)中的控件命名进行比较。幸运的是,Xamarin 的 iOS 设计器在设置 outlet 方面比 Xcode 简单得多。你只需在属性选项卡中填写名称字段,Xamarin Studio 就会在部分类中生成一个属性,这让你可以从控制器访问标签和按钮。此外,你还可以从 Storyboard 文件中连接一个动作,这是一个在事件发生时会被调用的方法。Xamarin Studio 将 iOS 动作作为部分方法暴露出来,以便在类中实现。
让我们按照以下方式向应用添加一些交互:
-
切换回 Xamarin Studio。
-
再次双击
MainStoryboard.storyboard文件。 -
选择你之前创建的标签,转到属性面板并确保已选择小部件选项卡。
-
在名称字段中输入名称
label。 -
对按钮重复此过程,并在其名称字段中输入名称
button。
与 Xcode 中过去的使用体验相比,Xamarin 大大改善了这一体验。对于习惯了 Visual Studio 的用户来说,Xcode 的界面很奇怪。创建 outlet 的方法涉及从控件拖动到 Objective-C 头文件。仅填写名称字段对有 C#背景的开发者来说要简单得多,也更直观。
现在我们已经定义了两个 outlet,从你的控制器中将有两个新的属性可用。展开解决方案中的*ViewController.cs文件并打开*ViewController.designer.cs文件。你会看到你的属性如下定义:
[Outlet]
[GeneratedCode ("iOS Designer", "1.0")]
MonoTouch.UIKit.UILabel label { get; set; }
[Outlet]
[GeneratedCode ("iOS Designer", "1.0")]
MonoTouch.UIKit.UIButton button { get; set; }
由于 Xamarin Studio 可以在设计器或 Xcode 中进行进一步更改时重新构建它,因此修改此文件不是一个好主意。尽管如此,了解幕后实际工作原理是一个好的实践。
打开你的*ViewController.cs文件,并在控制器中的ViewDidLoad方法中输入以下代码:
public override void ViewDidLoad()
{
base.ViewDidLoad();
int count = 0;
button.TouchUpInside += (sender, e) =>
label.Text = string.Format("Count: {0}", ++count);
}
当ViewDidLoad方法被调用时,你的控制器视图将首次加载。这发生在控制器生命周期的某个时刻。我们订阅了TouchUpInside事件,当按钮被点击时,这个事件会被触发;iOS 没有点击事件,这可能是在 Windows 平台上你习惯的。我们还使用了 C#方便的 lambda 表达式语法来在事件触发时更新标签。lambda 表达式是匿名方法的简写,这是自.NET 4.0 以来 C#的一部分特性。
运行你的应用程序,你将能够与按钮交互并增加标签上显示的值,如下面的截图所示:

接下来,我们需要从一个控制器转换到另一个控制器。为此,iOS 有一个称为segue的概念,它基本上是一种从当前控制器切换到下一个控制器的动画。有几种类型的 segue,但最常见的 segue 是从屏幕的右侧或底部滑动到新的控制器。
现在,让我们按照以下步骤向应用程序添加第二个控制器:
-
返回你的项目在 Xamarin Studio 中。
-
双击
MainStoryboard.storyboard文件。 -
从对象库中拖动一个新的控制器,通常位于第一个控制器的左下角。
-
点击控制器以选择它。
-
选择属性面板并确保你处于小部件选项卡。
-
在类字段中为控制器输入一个名称,例如
SecondController。 -
现在,让我们为从第一个控制器到这个控制器的转换添加一个 segue。在点击原始控制器上的按钮到你的新控制器时按住Ctrl键。会出现一条蓝色线条,随后是一个小的弹出菜单。
-
从弹出菜单中选择模态。
-
从 Xamarin Studio 运行应用程序。
由于我们从第一个控制器的按钮设置了一个模态转换,点击第二个控制器时它将出现。然而,目前还没有退出新控制器的方法。如果你返回 Xamarin Studio,你会注意到已经为你自动创建了一个SecondController.cs文件和一个SecondController.designer.cs文件。
让我们在SecondController中添加一个按钮,如下所示:
-
返回 Xamarin Studio。
-
双击
MainStoryboard.storyboard文件。 -
从对象库中将一个按钮拖动到第二个控制器上。
-
导航到属性面板和小部件选项卡。
-
将按钮的名称设置为
close。 -
将按钮的标题设置为
Close。
打开SecondController.cs文件并添加以下方法:
public override void ViewDidLoad()
{
base.ViewDidLoad();
close.TouchUpInside += (sender, e) =>
DismissViewController(true, null);
}
如果你编译并运行你的应用程序,点击按钮将增加标签上的值并显示模态第二个控制器。然后你可以通过点击Close按钮来关闭第二个控制器。注意整洁的滑动动画;iOS 自动应用这些类型的转换效果,并且非常容易在 iOS 上自定义:

由于我们已经介绍了在 Xamarin 的 iOS 设计器中布局控件的基本知识以及与 C#中的输出交互,让我们回顾一下 iOS 应用程序的标准生命周期。处理应用程序级事件的主要位置在AppDelegate类中。
如果你打开你的AppDelegate.cs文件,你可以重写以下方法:
-
FinishedLaunching:这是应用程序的第一个入口点,它应该返回true。 -
DidEnterBackground:这意味着用户点击了他们的设备上的主页按钮,或者另一个应用程序,如电话呼叫,进入了前台。你应该执行任何必要的操作来保存用户的进度或 UI 的状态,因为 iOS 可能会关闭你的应用程序以节省内存,一旦被推送到后台。当你的应用程序在后台时,用户可能正在浏览主屏幕或打开其他应用程序。你的应用程序在内存中实际上是被暂停,直到用户恢复。 -
WillEnterForeground:这意味着用户已从后台重新打开你的应用程序。你可能需要在这里执行其他操作,例如刷新屏幕上的数据等。 -
OnResignActivation:如果操作系统在你的应用程序上方显示系统弹出窗口,就会发生这种情况。例如,这包括日历提醒或用户可以从屏幕顶部向下滑动的菜单。 -
OnActivated:在用户返回你的应用程序后,OnResignActivation方法执行后立即发生。 -
ReceiveMemoryWarning:这是操作系统发出的警告,提示释放应用程序中的内存。由于 C# 的垃圾回收器,Xamarin 中通常不需要这个方法,但如果你的应用程序中存在像图像这样的重对象,这是一个很好的地方来销毁它们。如果无法释放足够的内存,操作系统可以终止你的应用程序。 -
HandleOpenUrl:如果你实现了 URL scheme,这将是 iOS 在桌面平台上的文件扩展名关联的等价物。如果你注册你的应用程序以打开不同类型的文件或 URL,此方法将被调用。
同样,在你的 *ViewController.cs 文件中,你可以覆盖控制器上的以下方法:
-
ViewDidLoad:当与控制器关联的视图被加载时发生。在运行 iOS 6 或更高版本的设备上,它只会发生一次。 -
ViewWillAppear:在视图出现在屏幕上之前发生。如果你在应用程序中导航时需要刷新任何视图,这通常是做这件事的最佳位置。 -
ViewDidAppear:在完成任何过渡动画并显示视图在屏幕上后发生。在某些不常见的情况下,你可能需要在这里执行操作,而不是在ViewWillAppear中。 -
ViewWillDisappear:在视图被隐藏之前调用此方法。你可能需要在这里执行一些清理操作。 -
ViewDidDisappear:在完成显示屏幕上不同控制器的任何过渡动画后发生。就像出现的方法一样,这发生在ViewWillDisappear之后。
有更多方法可供覆盖,但许多在 iOS 的最新版本中已被弃用。熟悉 Apple 的文档网站developer.apple.com/library/ios很有帮助。在尝试理解 Apple 的 API 如何工作时,阅读每个类和方法的文档非常有用。学习如何阅读(不一定是代码)Objective-C 也是一个有用的技能,这样你就可以在开发 iOS 应用程序时将 Objective-C 示例转换为 C#。
构建你的第一个 Android 应用程序
在 Xamarin Studio 中设置 Android 应用程序与 iOS 一样简单,并且与 Visual Studio 中的体验非常相似。Xamarin Studio 包括几个特定于 Android 的项目模板,以帮助你快速开始开发。
Xamarin Studio 包括以下项目模板:
-
Android 应用程序:一个针对您机器上安装的最新 Android SDK 的标准 Android 应用程序。
-
Android Honeycomb 应用程序:一个针对 Android Honeycomb(API(应用程序编程接口)级别 12 及以上)的项目。
-
Android 冰淇淋三明治应用程序:一个针对 Android 冰淇淋三明治(API 级别 15 及以上)的项目。
-
Android 库项目:一个只能由 Android 应用程序项目引用的类库。
-
Android Java 绑定库:一个用于设置从 C#中调用的 Java 库的项目。
-
Android OpenGL 应用程序:一个用于使用低级 OpenGL 进行 3D 或 2D 渲染的项目模板。
-
Android WebView 应用程序:一个用于使用 HTML 进行某些部分的混合应用程序的项目模板。支持 Razor 模板。
-
Android 单元测试项目:一个用于在 Android 上运行 NUnit 测试的项目。
启动 Xamarin Studio 并开始一个新的解决方案。从新建解决方案对话框中,在Android部分下创建一个新的Android 应用程序。
你最终会得到一个类似于以下截图所示的项目解决方案:

你会看到为你创建了以下特定于 Android 的文件和文件夹:
-
Components文件夹:这与 iOS 项目相同;添加 Xamarin Component Store 组件的地方。 -
Assets文件夹:此目录将包含具有AndroidAsset构建操作的文件。此文件夹将包含与 Android 应用程序捆绑的原始文件。 -
Properties/AndroidManifest.xml文件:此文件包含有关您的 Android 应用程序的标准声明,例如应用程序名称、ID 和权限。 -
Resources文件夹:资源包括图像、布局、字符串等,可以通过 Android 的资源系统加载。每个文件都会在Resources.designer.cs中生成一个 ID,你可以使用它来加载资源。 -
Resources/drawable文件夹:你的应用程序使用的任何图像通常都放在这里。 -
Resources/layout文件夹:这个文件夹包含 Android 使用来声明 UI 的任何*.axml(Android XML)文件。布局可以用于整个 activity、fragment、dialog 或 child control 在屏幕上显示。 -
Resources/values文件夹:这个文件夹包含 XML 文件,用于在应用程序中声明字符串(和其他类型)的关键值对。这是在 Android 上设置多语言本地化的常规方法。 -
MainActivity.cs文件:这是MainLauncher动作和你的 Android 应用程序的第一个活动。在 Android 应用程序中没有static void Main函数;执行从设置为true的MainLauncher活动开始。
现在,让我们执行以下步骤来运行应用程序:
-
点击播放按钮来编译和运行应用程序。
-
将会出现一个 选择设备 对话框。
-
选择你想要的模拟器并点击 启动模拟器。如果你在 第一章 中设置了 x86 模拟器,设置 Xamarin,我建议你使用它。
-
等待几秒钟,让模拟器启动。一旦启动,如果你正在处理 Android 项目,让它一直运行是个好主意。这将节省你大量等待的时间。
-
你现在应该能在设备列表中看到启用的模拟器;选择它,然后点击 确定。
-
在第一次将应用程序部署到模拟器或设备时,Xamarin Studio 将必须安装一些东西,例如 Mono 共享运行时和 Android 平台工具。
-
切换到 Android 模拟器,你的应用程序将显示出来。
当一切完成后,你已经部署了你的第一个 Android 应用程序,包含一个单按钮。你的应用将看起来像以下截图所示:

Android 活动
Android 操作系统非常关注活动这个概念。活动是用户可以在其屏幕上执行的任务或工作单元。例如,用户会执行一个电话 activity 来拨号,并执行第二个涉及与他们的地址簿交互以定位号码的活动。每个 Android 应用程序都是由一个或多个活动组成的集合,用户可以启动它们,并在设备上按下硬件的返回键来退出或取消。用户的历史记录保存在 Android back stack 中,你可以在特殊情况下从代码中操作它。当一个新的活动开始时,前一个活动会被暂停并保存在内存中,以供以后使用,除非操作系统内存不足。
活动之间是松散耦合的;在某种程度上,你可以将它们视为在内存中具有完全独立的状态。静态值将持久化应用程序的生命周期,就像.NET 应用程序一样,但常见的做法是通过 Android bundle传递状态。Android bundle 是一组键值对,用于在 Android 对象之间传递数据。这对于传递列表中显示的项目的标识符以在新活动中编辑该项目非常有用。
活动有以下生命周期回调方法,你可以重写它们:
-
OnCreate: 这是创建活动时首先调用的方法。在这里设置你的视图并执行其他加载逻辑。最重要的是,你将在这里调用SetContentView来设置活动的视图。 -
OnResume: 当你的活动视图在屏幕上可见时会被调用。如果活动首次显示,或者用户从另一个活动返回时,都会调用此方法。 -
OnPause: 当用户离开你的活动时会被调用。它可能发生在导航到应用中的新活动之前、锁定屏幕或按 home 按钮时。假设用户可能不会返回,因此你需要在这里保存用户所做的任何更改。 -
OnStart: 这发生在OnResume之前,当活动的视图即将在屏幕上显示时。它发生在活动启动时,以及用户从另一个活动返回时。 -
OnStop: 这发生在OnPause之后,当活动的视图不再显示在屏幕上时。 -
OnRestart: 当用户从先前活动返回到你的活动时,此方法发生。 -
OnActivityResult: 此方法用于与 Android 上其他应用程序中的其他活动进行通信。它与StartActvityForResult一起使用;例如,你将使用此方法与 Facebook 应用程序交互以登录用户。 -
OnDestroy: 当你的活动即将从内存中释放时会被调用。在这里执行任何可能帮助操作系统的额外清理工作,例如处理活动使用过的任何其他重量级对象。
Android 生命周期的流程图如下:

与 iOS 不同,Android 不会对其开发者强制执行任何设计模式。然而,在一定程度上理解 Android 活动生命周期是不可避免的。许多与活动相关的概念与 iOS 中的控制器平行;例如,OnStart相当于ViewWillAppear,而OnResume相当于ViewDidAppear。
其他处理活动的方法如下:
-
StartActivity(Type type): 此方法在应用内启动一个新的活动,并传递没有额外信息给活动。 -
StartActivity(Intent intent):这是一个重载方法,用于使用Intent启动新的活动。这让你能够向新活动传递额外的信息,你也可以在其他应用程序中启动活动。 -
StartActivityForResult:此方法在活动操作完成后预期接收OnActivityResult。 -
Finish:这将关闭当前活动,并在活动完全关闭且不再显示在屏幕上时调用OnDestroy。根据当前后台栈中的内容,用户将返回到先前的活动或主屏幕。 -
SetContentView:此方法设置要显示的活动的主视图。它应该在活动显示在屏幕上之前,在OnCreate方法中调用。 -
FindViewById:这是一个用于定位活动显示的视图的方法。它有一个泛型版本,可以返回适当类型的视图。
你可以将intent视为一个描述从活动到活动过渡的对象。你还可以通过 intent 传递额外的数据以及修改活动的显示和用户的导航历史。
除了活动之外,Android 还有片段的概念。你可以将片段视为在父活动内部显示的微型活动。片段对于在应用程序中重用 UI 的不同部分非常有用,还可以帮助你实现平板电脑上的分屏导航。
Xamarin 的 Android 设计器
Android 项目的默认模板比 iOS 有更多的内置功能。Android 用户界面布局定义在人类可读和可编辑的 XML 文件中。然而,Xamarin Studio 提供了一个优秀的设计工具,允许你拖放控件来定义你的 Android 布局。让我们给你的应用程序添加更多功能并开始使用 Android 设计器。
返回 Xamarin Studio 并执行以下步骤以添加应用程序功能:
-
在 Xamarin Studio 中打开本章中创建的 Android 项目。
-
导航到项目中的资源 | 布局,并打开
Main.axml。 -
你将看到 Android 设计器在 Xamarin Studio 中打开。
-
从右侧的工具箱部分拖动TextView到按钮Hello World, Click Me!上方的布局中。
-
在标签中输入一些默认文本,例如
计数:0。 -
在右侧的属性面板中,你会看到id值设置为
@+id/textView1。让我们将其更改为@+id/myText,以便与按钮保持一致。 -
当我们在这里时,请将按钮上的文本更改为更合适的内容,例如
添加。 -
点击播放按钮来编译并运行应用程序。如果你仍然有安卓模拟器,你可以直接切换到它。否则,你将需要重新启动它。
您的 Android 应用程序现在将看起来与您在设计师中做出的更改完全相同,如下所示:

现在,让我们从代码中与新的标签进行交互。切换回 Xamarin Studio 并打开MainActivity.cs。让我们修改活动以与TextView字段而不是按钮进行交互。我们使用FindViewById方法通过在布局文件中设置的 ID 检索视图。Xamarin Studio 还自动生成一个名为Resource的静态类来引用您的标识符。
因此,让我们通过在OnCreate中放置以下代码来检索TextView字段的实例:
TextView text = FindViewById<TextView>(Resource.Id.myText);
Resource类是一个 Xamarin 设计师将为您填充的静态类。为了将来参考,您可能需要构建您的 Android 项目以显示新 ID 和其他资源在 Xamarin Studio 中的 C#文件中。
接下来,让我们更新按钮上的Click事件:
button.Click += delegate
{
text.Text = string.Format("Count: {0}", ++count);
};
这将重新连接按钮以更新TextView中的文本而不是按钮本身。现在如果我们运行应用程序,我们将得到一个与上一章中 iOS 应用程序功能相同的 Android 应用程序。Android 应用程序的外观如下截图所示:

由于我们在布局中添加了一些自己的视图,让我们添加一个第二个活动来加深我们对 Android 中活动理解的认识。
返回 Xamarin Studio 并执行以下步骤:
-
如果需要,请在 Xamarin Studio 中打开本章中创建的早期 Android 项目。
-
在Android部分的项目中创建一个新的 Android 活动。命名为
SecondActivity.cs。 -
导航到资源 | 布局,并创建一个名为
Second.axml的新 Android 布局。 -
打开
SecondActivity.cs并在OnCreate中添加以下代码:SetContentView(Resource.Layout.Second); -
打开
MainActivity.cs并在按钮的Click事件中添加以下代码行:StartActivity(typeof(SecondActivity)); -
打开
Second.axml并将一个按钮拖入视图。将其文本设置为Finish,例如,并将其 ID 设置为@+id/finish。 -
最后,打开
SecondActivity.cs并在其OnCreate方法中添加以下行:var finish = FindViewById<Button>(Resource.Id.finish); finish.Click += (sender, e) => Finish(); -
构建并运行您的应用程序。
您的应用程序按钮现在除了在标签上增加计数外,还会启动一个新的活动。一旦SecondActivity可见,您就可以点击其按钮来结束活动并返回第一个活动。将来,如果您需要从一个活动传递信息到另一个活动,您将需要创建一个Intent对象并将其传递给StartActivity。您的应用程序的第二个活动如下截图所示:

摘要
在本章中,我们在 Xamarin Studio 中创建了我们的第一个 iOS 应用程序。我们介绍了苹果的 MVC 设计模式,以便更好地理解UIViewController和UIView之间的关系,并介绍了如何在 Xamarin Studio 中使用 iOS 设计器来编辑 storyboard 文件。接下来,我们在 Xamarin Studio 中创建了我们的第一个 Android 应用程序,并学习了 Android 的活动生命周期。我们还使用了 Xamarin 的 Android 设计器来修改 Android XML 布局。
从本章涵盖的主题来看,你应该对使用 Xamarin 的工具开发 iOS 和 Android 的简单应用程序有足够的信心。你应该对原生 SDKs 和设计模式有基本的了解,以便在 iOS 和 Android 上完成任务。
在下一章中,我们将介绍使用 Xamarin Studio 在平台间共享代码的各种技术。我们将探讨不同的跨平台应用程序架构方式,以及如何设置 Xamarin Studio 项目和解决方案。
第三章. iOS 和 Android 之间的代码共享
Xamarin 的工具承诺在尽可能利用每个平台的本地 API 的同时,在 iOS 和 Android 之间共享大量代码。这样做更多的是一项软件工程练习,而不是编程技能或对每个平台的知识。为了构建一个支持代码共享的 Xamarin 应用程序,必须将应用程序分成不同的层。在本章中,我们将介绍这一基本概念,以及在特定情况下需要考虑的特定选项。
在本章中,我们将介绍以下内容:
-
MVVM 设计模式用于代码共享
-
项目和解决方案组织策略
-
可移植类库 (PCLs)
-
平台特定代码的预处理器语句
-
依赖注入 (DI) 简化版
-
控制反转 (IoC)
学习 MVVM 设计模式
模型-视图-视图模型(MVVM)设计模式最初是为使用 XAML 将 UI 与业务逻辑分离并充分利用 数据绑定 的 Windows Presentation Foundation(WPF)应用程序发明的。以这种方式构建的应用程序具有一个独立的 ViewModel 层,该层不依赖于其用户界面。这种架构本身优化了单元测试以及跨平台开发。由于应用程序的 ViewModel 类不依赖于 UI 层,因此可以轻松地将 iOS 用户界面替换为 Android 用户界面,并对 ViewModel 层进行测试。
MVVM 设计模式与之前章节中讨论的 MVC 设计模式也非常相似。
MVVM 设计模式包括以下内容:
-
模型:模型层是驱动应用程序及其相关业务对象的底层业务逻辑。这可以是从服务器发送网络请求到使用后端数据库的任何操作。
-
视图:这一层是屏幕上实际看到的用户界面。在跨平台开发的情况下,它包括驱动应用程序用户界面的任何平台特定代码。在 iOS 上,这包括应用程序中使用的控制器,在 Android 上,则是应用程序的活动。
-
视图模型:这一层在 MVVM 应用程序中充当粘合剂。ViewModel 层协调视图和模型层之间的操作。ViewModel 层将包含视图将获取或设置的属性,以及用户可以对每个视图执行的操作的函数。如果需要,ViewModel 层还将调用模型层的操作。
下图展示了 MVVM 设计模式:

需要注意的是,传统上,视图和视图模型层之间的交互是通过 WPF 的数据绑定创建的。然而,iOS 和 Android 没有内置的数据绑定机制,所以本书中的一般方法将是手动从视图层调用视图模型层。有一些框架提供了数据绑定功能,例如MVVMCross(本书未涉及)和Xamarin.Forms。
在示例中实现 MVVM
为了更好地理解这个模式,让我们实现一个常见的场景。假设我们在屏幕上有一个搜索框和一个搜索按钮。当用户输入一些文本并点击按钮时,将向用户显示产品列表和价格。在我们的示例中,我们使用了 C# 5 中可用的async和await关键字来简化异步编程。
要实现这个功能,我们将从一个简单的model类(也称为业务对象)开始,如下所示:
public class Product
{
public int Id { get; set; } //Just a numeric identifier
public string Name { get; set; } //Name of the product
public float Price { get; set; } //Price of the product
}
接下来,我们将实现我们的模型层,根据搜索词检索产品。这是执行业务逻辑的地方,表示搜索实际上应该如何工作。这体现在以下代码行中:
// An example class, in the real world would talk to a web
// server or database.
public class ProductRepository
{
// a sample list of products to simulate a database
private Product[] products = new[]
{
new Product { Id = 1, Name = "Shoes", Price = 19.99f },
new Product { Id = 2, Name = "Shirt", Price = 15.99f },
new Product { Id = 3, Name = "Hat", Price = 9.99f },
};
public async Task<Product[]> SearchProducts(string searchTerm)
{
// Wait 2 seconds to simulate web request
await Task.Delay(2000);
// Use Linq-to-objects to search, ignoring case
searchTerm = searchTerm.ToLower();
return products.Where(p => p.Name.ToLower().Contains(searchTerm)).ToArray();
}
}
这里需要注意的是,Product和ProductRepository类都被视为跨平台应用程序模型层的一部分。有些人可能会将ProductRepository视为一个服务,它通常是一个自包含的类,用于检索数据。将这个功能分离成两个类是一个好主意。Product类的任务是保存有关产品的信息,而ProductRepository类负责检索产品。这是单一职责原则的基础,该原则指出每个类应该只做一项工作或关注一个方面。
接下来,我们将实现一个ViewModel类,如下所示:
public class ProductViewModel
{
private readonly ProductRepository repository = new ProductRepository();
public string SearchTerm
{
get;
set;
}
public Product[] Products
{
get;
private set;
}
public async Task Search()
{
if (string.IsNullOrEmpty(SearchTerm))
Products = null;
else
Products = await repository.SearchProducts(SearchTerm);
}
}
从这里开始,你的平台特定代码开始了。每个平台将处理管理ViewModel类的实例,设置SearchTerm属性,并在按钮点击时调用Search。当任务完成时,用户界面层将更新屏幕上显示的列表。
小贴士
如果你熟悉在 WPF 中使用的 MVVM 设计模式,你可能会注意到我们没有为数据绑定实现INotifyPropertyChanged。由于 iOS 和 Android 没有数据绑定的概念,我们省略了这一功能。如果你计划为你的移动应用程序创建 WPF 或 Windows 8 版本,或者使用提供数据绑定功能的框架,你应该在需要的地方实现对其的支持。
比较项目组织策略
到目前为止,你可能正在问自己,我如何在 Xamarin Studio 中设置解决方案以处理共享代码,并且还要有特定平台的项目?Xamarin.iOS 应用程序只能引用 Xamarin.iOS 类库,因此设置解决方案可能会出现问题。有几种设置跨平台解决方案的策略,每种策略都有其自身的优缺点。
跨平台解决方案的选项如下:
-
文件链接:对于这个选项,你将从一个包含所有共享代码的普通.NET 4.0 或.NET 4.5 类库开始。然后,你将为想要应用运行的平台创建一个新的项目。每个特定平台的项目将有一个子目录,其中包含从第一个类库链接的所有文件。要设置此环境,将现有文件添加到项目中,并选择添加文件链接选项。任何单元测试都可以针对原始类库运行。文件链接的优点和缺点如下:
-
优点:这种方法非常灵活。你可以选择是否链接某些文件,也可以使用预处理器指令,如
#if IPHONE。你还可以在 Android 和 iOS 之间引用不同的库。 -
缺点:你必须在三个项目中管理一个文件的存在:核心库、iOS 和 Android。如果这是一个大型应用程序或者许多人正在工作,这可能会很麻烦。这个选项也有些过时,因为共享项目的出现。
-
-
克隆项目文件:这与文件链接非常相似。主要区别在于,除了主项目外,你还有每个平台的类库。通过将 iOS 和 Android 项目放置在主项目的同一目录下,可以添加文件而不需要链接。你可以通过右键单击解决方案并导航到显示选项 | 显示所有文件来轻松添加文件。单元测试可以针对原始类库或平台特定版本运行:
-
优点:这种方法与文件链接一样灵活,但你不需要手动链接任何文件。你仍然可以使用预处理器指令并在每个平台上引用不同的库。
-
缺点:你仍然需要在三个项目中管理一个文件的存在。此外,还需要一些手动文件排列来设置此环境。你最终在每个平台上还要管理一个额外的项目。这个选项也有些过时,因为共享项目的出现。
-
-
共享项目:从 Visual Studio 2013 Update 2 开始,微软创建了共享项目的概念,以实现 Windows 8 和 Windows Phone 应用之间的代码共享。Xamarin 也在 Xamarin Studio 中实现了共享项目,作为另一个选项来启用代码共享。共享项目在本质上与文件链接相同,因为向共享项目添加引用实际上是将它的文件添加到你的项目中:
-
优点:这种方法与文件链接相同,但更干净,因为你的共享代码在一个单独的项目中。Xamarin Studio 还提供了一个下拉菜单来切换到每个引用项目,这样你可以看到预处理器语句在代码中的效果。
-
缺点:由于共享项目中的所有文件都会添加到每个平台的主项目中,因此在共享项目中包含特定平台的代码可能会变得很丑陋。如果你有一个大型团队或者团队成员经验不足,预处理器语句可能会迅速失控。共享项目也无法编译成 DLL,因此没有不带源代码共享此类项目的方法。
-
-
可移植类库:这是最佳选项;你开始解决方案时,为所有共享代码创建一个 可移植类库(PCL)项目。这是一个特殊的项目类型,允许多个平台引用同一个项目,让你可以使用每个平台可用的最小 C# 和 .NET 框架子集。每个特定平台的项⽬也将直接引用这个库以及任何单元测试项目:
-
优点:所有共享代码都在一个项目中,所有平台使用相同的库。由于预处理器语句不可用,PCL 库通常有更干净的代码。特定平台的代码通常通过接口或抽象类来抽象化。
-
缺点:你受限于你目标平台数量的 .NET 子集。特定平台的代码需要使用 依赖注入,这可能对于不熟悉它的开发者来说是一个更高级的话题。
-
设置跨平台解决方案
为了完全理解每个选项以及不同情况需要什么,让我们为每个跨平台解决方案定义一个解决方案结构。让我们使用本章前面使用的商品搜索示例,并为每种方法设置一个解决方案。
要设置文件链接,请执行以下步骤:
-
打开 Xamarin Studio 并启动一个新的解决方案。
-
在通用 C# 部分,选择一个新的 库 项目。
-
将项目命名为
ProductSearch.Core,将解决方案命名为ProductSearch。 -
右键单击新创建的项目,并选择 选项。
-
导航到 构建 | 常规,并将 目标框架 选项设置为 .NET Framework 4.5。
-
将
Product、ProductRepository和ProductViewModel类添加到本章前面使用的项目中。你需要在需要的地方添加using System.Threading.Tasks;和using System.Linq;。 -
从顶部菜单导航到 构建 | 构建全部,以确保一切构建正确。
-
现在,让我们通过右键单击解决方案并导航到 添加 | 添加新项目 来创建一个新的 iOS 项目。然后,导航到 iOS | iPhone | 单视图应用程序 并将项目命名为
ProductSearch.iOS。 -
通过右键点击解决方案并导航到添加 | 添加新项目创建一个新的 Android 项目。通过导航到Android | Android 应用创建一个新的项目,并将其命名为
ProductSearch.Droid。 -
在 iOS 和 Android 项目中添加一个名为
Core的新文件夹。 -
右键点击 iOS 项目的新的文件夹,导航到添加 | 从文件夹添加文件。选择
ProductSearch.Core项目的根目录。 -
检查项目根目录下的三个 C# 文件。会出现一个添加文件到文件夹的对话框。
-
选择添加文件链接并确保选中了为所有选定的文件使用相同的操作复选框。
-
对于 Android 项目,重复此过程。
-
从菜单栏的构建 | 构建所有导航到构建,以双重检查一切。你已经成功使用文件链接设置了一个跨平台解决方案。
当一切完成后,你将有一个类似于以下截图的解决方案树:

当你需要在每个平台上引用不同的库时,你应该考虑使用这项技术。如果你正在使用 MonoGame 或其他需要在 iOS 和 Android 上引用不同库的框架,你可能需要考虑使用这个选项。
使用克隆项目文件的方法设置解决方案与文件链接类似,但你将不得不为每个平台创建一个额外的类库。为此,在同一个 ProductSearch.Core 目录下创建一个 Android 库项目和 iOS 库项目。你必须手动创建项目并将它们移动到正确的文件夹,然后重新将它们添加到解决方案中。右键点击解决方案并导航到显示选项 | 显示所有文件,将这些所需的 C# 文件添加到这两个项目中。你的主要 iOS 和 Android 项目可以直接引用这些项目。
你的项目将看起来像以下截图所示,其中 ProductSearch.iOS 引用了 ProductSearch.Core.iOS,而 ProductSearch.Droid 引用了 ProductSearch.Core.Droid:

与可移植类库一起工作
可移植类库(PCL)是一个可以在多个平台上支持,包括 iOS、Android、Windows、Windows Store 应用、Windows Phone、Silverlight 和 Xbox 360 的 C# 库项目。PCLs 是微软为了简化跨不同版本的 .NET 框架的开发而做出的努力。Xamarin 也为 PCLs 添加了对 iOS 和 Android 的支持。许多流行的跨平台框架和开源库开始开发 PCL 版本,例如 Json.NET 和 MVVMCross。
使用 PCLs 在 Xamarin 中
让我们创建我们的第一个可移植类库:
-
打开 Xamarin Studio 并启动一个新的解决方案。
-
在通用 | C#部分下选择一个新的可移植库项目。
-
将项目命名为
ProductSearch.Core,解决方案命名为ProductSearch。 -
将
Product、ProductRepository和ProductViewModel类添加到本章中较早使用的项目中。您需要在需要的地方添加using System.Threading.Tasks;和using System.Linq;。 -
从顶部菜单导航到 构建 | 构建所有,以确保一切构建正确。
-
现在,通过右键单击解决方案并导航到 添加 | 添加新项目 来创建一个新的 iOS 项目。通过导航到 iOS | iPhone | 单视图应用程序 创建一个新项目,并将其命名为
ProductSearch.iOS。 -
通过右键单击解决方案并导航到 添加 | 添加新项目 来创建一个新的 Android 项目。然后,导航到 Android | Android 应用程序 并将项目命名为
ProductSearch.Droid。 -
简单地从 iOS 和 Android 项目添加对便携式类库的引用。
-
从顶部菜单导航到 构建 | 构建所有,你已成功设置了一个包含便携式库的简单解决方案。
每种解决方案类型都有其独特的优缺点。PCLs 通常更好,但在某些情况下它们无法使用。例如,如果您正在使用像 MonoGame 这样的库,这是一个针对每个平台的独立库,那么使用共享项目或文件链接会更好。如果您需要使用像 #if IPHONE 这样的预处理器语句或 iOS 或 Android 上的本地库(如 Facebook SDK),也会出现类似的问题。
小贴士
设置共享项目几乎与设置便携式类库相同。在步骤 2 中,只需在一般的C#部分下选择共享项目,然后完成剩余的步骤。
使用预处理器语句
当使用共享项目、文件链接或克隆的项目文件时,您最强大的工具之一是使用预处理器语句。如果您不熟悉它们,C# 有能力定义预处理器变量,如 #define IPHONE,允许您使用 #if IPHONE 或 #if !IPHONE。
以下是一个使用此技术的简单示例:
#if IPHONE
Console.WriteLine("I am running on iOS");
#elif ANDROID
Console.WriteLine("I am running on Android");
#else
Console.WriteLine("I am running on ???");
#endif
在 Xamarin Studio 中,您可以通过导航到 构建 | 编译器 | 定义符号 来在项目的选项中定义预处理器变量,这些变量用分号分隔。这些变量将应用于整个项目。请注意,您必须为解决方案中的每个配置设置(调试和发布)设置这些变量;这可能会是一个容易忽略的步骤。您还可以通过在 C#文件的顶部声明 #define IPHONE 来定义这些变量,但它们只会在 C#文件内应用。
让我们再举一个例子,假设我们想在每个平台上实现一个打开 URL 的类:
public static class Utility
{
public static void OpenUrl(string url)
{
//Open the url in the native browser
}
}
前面的例子非常适合使用预处理器语句,因为它非常特定于每个平台,并且是一个相当简单的函数。为了在 iOS 和 Android 上实现这个方法,我们需要利用一些本地 API。重构后的类如下所示:
#if IPHONE
//iOS using statements
using MonoTouch.Foundation;
using MonoTouch.UIKit;
#elif ANDROID
//Android using statements
using Android.App;
using Android.Content;
using Android.Net;
#else
//Standard .Net using statement
using System.Diagnostics;
#endif
public static class Utility
{
#if ANDROID
public static void OpenUrl(Activity activity, string url)
#else
public static void OpenUrl(string url)
#endif
{
//Open the url in the native browser
#if IPHONE
UIApplication.SharedApplication.OpenUrl(NSUrl.FromString(url));
#elif ANDROID
var intent = new Intent(Intent.ActionView,Uri.Parse(url));
activity.StartActivity(intent);
#else
Process.Start(url);
#endif
}
}
前面的类支持三种不同类型的项目:Android、iOS 和标准的 Mono 或.NET 框架类库。在 iOS 的情况下,我们可以使用苹果 API 中可用的静态类来执行功能。Android 稍微有些问题,需要Activity对象来原生地启动浏览器。我们通过修改 Android 上的输入参数来解决这个问题。最后,我们有一个普通的.NET 版本,它使用Process.Start()来启动 URL。重要的是要注意,使用第三个选项在 iOS 或 Android 上不会原生工作,这迫使我们使用预处理器语句。
使用预处理器语句通常不是跨平台开发的最佳或最干净的方法。它们通常在狭窄的空间或非常简单的函数中使用得最好。代码很容易失控,并且随着许多#if语句的出现,代码的可读性会变得非常困难,因此总是最好适度使用。当类主要针对特定平台时,使用继承或接口通常是更好的解决方案。
简化依赖注入
依赖注入最初看起来像是一个复杂的话题,但实际上它是一个简单的概念。它是一种设计模式,旨在使你应用程序中的代码更加灵活,以便在需要时可以替换某些功能。这个想法围绕在应用程序中设置类之间的依赖关系,使得每个类只与接口或基类/抽象类交互。这给了你在需要填充本地功能时,在每个平台上自由覆盖不同方法的自由。
这个概念起源于SOLID面向对象设计原则,如果你对软件架构感兴趣,你可能想要研究一下这组规则。如果你想要了解更多关于 SOLID 的信息,维基百科上有一篇很好的文章,(en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29)。在 SOLID 中,我们感兴趣的是D,它代表依赖。具体来说,这个原则声明,一个程序应该依赖于抽象,而不是具体实现(具体类型)。
为了扩展这个概念,让我们通过以下例子来讲解:
-
假设我们需要在一个应用程序中存储一个设置,这个设置用来确定声音是开启还是关闭。
-
现在让我们声明一个简单的设置接口:
interface ISettings { bool IsSoundOn { get; set; } }。 -
在 iOS 上,我们希望使用
NSUserDefaults类来实现这个接口。 -
同样,在 Android 上,我们将使用
SharedPreferences来实现。 -
最后,任何需要与此设置交互的类都只会引用
ISettings,这样就可以在每个平台上替换实现。
提示
下载示例代码
你可以从你购买的所有 Packt 书籍的账户中下载你购买的示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support,并注册以直接将文件通过电子邮件发送给你。
作为参考,此示例的完整实现将如下所示:
public interface ISettings
{
bool IsSoundOn
{
get;
set;
}
}
//On iOS
using MonoTouch.UIKit;
using MonoTouch.Foundation;
public class AppleSettings : ISettings
{
public bool IsSoundOn
{
get
{
return NSUserDefaults.StandardUserDefaults
BoolForKey("IsSoundOn");
}
set
{
var defaults = NSUserDefaults.StandardUserDefaults;
defaults.SetBool(value, "IsSoundOn");
defaults.Synchronize();
}
}
}
//On Android
using Android.Content;
public class DroidSettings : ISettings
{
private readonly ISharedPreferences preferences;
public DroidSettings(Context context)
{
preferences = context.GetSharedPreferences(context.PackageName, FileCreationMode.Private);
}
public bool IsSoundOn
{
get
{
return preferences.GetBoolean("IsSoundOn", true");
}
set
{
using (var editor = preferences.Edit())
{
editor.PutBoolean("IsSoundOn", value);
editor.Commit();
}
}
}
}
现在,你可能会有一个ViewModel类,它将只引用ISettings以遵循 MVVM 模式。以下是一个示例片段:
public class SettingsViewModel
{
private readonly ISettings settings;
public SettingsViewModel(ISettings settings)
{
this.settings = settings;
}
public bool IsSoundOn
{
get;
set;
}
public void Save()
{
settings.IsSoundOn = IsSoundOn;
}
}
对于这样一个简单的示例,使用 ViewModel 层可能不是必需的,但你可以看到,如果你需要执行其他任务,如输入验证,它将非常有用。一个完整的应用程序可能有很多设置,可能需要向用户显示加载指示器。抽象出你的设置实现还有其他好处,这些好处可以为你的应用程序增加灵活性。假设你突然需要用 iCloud 替换 iOS 上的NSUserDefaults;你可以通过实现一个新的ISettings类轻松地做到这一点,而你的其余代码将保持不变。这也有助于你针对新的平台,如 Windows Phone,在那里你可能选择以平台特定的方式实现ISettings。
实现控制反转
你可能会在这个时候问自己,我如何切换不同的类,比如ISettings示例?控制反转(IoC)是一种设计模式,旨在补充依赖注入并解决此问题。基本原理是,你应用程序中创建的许多对象都由一个单独的类管理和创建。而不是使用你的ViewModel或Model类的标准 C#构造函数,服务定位器或工厂类将管理它们在整个应用程序中。
IoC(控制反转)有许多不同的实现和风格,因此让我们实现一个简单的服务定位器类,以便在本书的剩余部分使用,如下所示:
public static class ServiceContainer
{
static readonly Dictionary<Type, Lazy<object>> services = new Dictionary<Type, Lazy<object>>();
public static void Register<T>(Func<T> function)
{
services[typeof(T)] = new Lazy<object>(() => function());
}
public static T Resolve<T>()
{
return (T)Resolve(typeof(T));
}
public static object Resolve(Type type)
{
Lazy<object> service;
if (services.TryGetValue(type, out service)
{
return service.Value;
}
throw new Exception("Service not found!");
}
}
这个类受到了 XNA/MonoGame 的GameServiceContainer类简洁性的启发,并遵循服务定位器模式。主要区别在于大量使用泛型和它是一个静态类。
要使用我们的ServiceContainer类,我们将通过调用Register来声明我们想要在应用程序中使用的ISettings或其他接口的版本,如下面的代码行所示:
//iOS version of ISettings
ServiceContainer.Register<ISettings>(() => new AppleSettings());
//Android version of ISettings
ServiceContainer.Register<ISettings>(() => new DroidSettings());
//You can even register ViewModels
ServiceContainer.Register<SettingsViewMode>(() => new SettingsViewModel());
在 iOS 上,你可以将此注册代码放在你的static void Main()方法中,或者放在你的AppDelegate类的FinishedLaunching方法中。这些方法总是在应用程序启动之前被调用。
在 Android 上,这要复杂一些。您不能将此代码放在充当主启动器的活动OnCreate方法中。在某些情况下,Android 操作系统可能会关闭您的应用程序,但稍后会在另一个活动中重新启动它。这种情况可能会在某个地方引发异常。确保安全的地方是将此代码放在具有在创建应用程序中的任何活动之前被调用的OnCreate方法的自定义 Android Application类中。以下代码行展示了Application类的使用:
[Application]
public class Application : Android.App.Application
{
//This constructor is required
public Application(IntPtr javaReference, JniHandleOwnership transfer): base(javaReference, transfer)
{
}
public override void OnCreate()
{
base.OnCreate();
//IoC Registration here
}
}
要从ServiceContainer类中提取服务,我们可以重写SettingsViewModel类的构造函数,使其类似于以下代码行:
public SettingsViewModel()
{
this.settings = ServiceContainer.Resolve<ISettings>();
}
同样,您将使用通用的Resolve方法来提取您需要在 iOS 控制器或 Android 活动内部调用的任何ViewModel类。这是一种管理应用程序内依赖关系的好方法,简单易行。
当然,还有一些优秀的开源库实现了 C#应用程序的 IoC。如果您需要更高级的功能来处理服务定位,或者只是想升级到一个更复杂的 IoC 容器,您可以考虑切换到其中之一。
这里有一些与 Xamarin 项目一起使用的库:
-
TinyIoC:
github.com/grumpydev/TinyIoC -
Ninject:
www.ninject.org/ -
MvvmCross:
github.com/slodge/MvvmCross包含完整的 MVVM 框架以及 IoC -
Simple Injector:
simpleinjector.codeplex.com -
OpenNETCF.IoC:
ioc.codeplex.com
摘要
在本章中,我们学习了 MVVM 设计模式以及如何用它来更好地构建跨平台应用程序。我们比较了几种项目管理策略,用于管理包含 iOS 和 Android 项目的 Xamarin Studio 解决方案。我们讨论了可移植类库作为共享代码的首选选项,以及如何使用预处理器语句作为实现平台特定代码的快速且简单的方法。
完成本章后,您应该能够使用 Xamarin Studio 中的几种技术来加快 iOS 和 Android 应用程序之间共享代码的速度。使用 MVVM 设计模式将帮助您区分共享代码和平台特定代码。我们还讨论了设置跨平台 Xamarin 解决方案的几个选项。您还应该对使用依赖注入和反转控制来使共享代码访问每个平台的本地 API 有牢固的理解。在我们下一章中,我们将从编写跨平台应用程序开始,深入探讨使用这些技术。
第四章。XamChat – 一个跨平台应用
在我看来,真正学习编程技能的最佳方式是承担一个需要你练习该技能的简单项目。这为新开发者提供了一个项目,他们可以在其中专注于他们试图学习的概念,而无需处理修复错误或遵循客户要求的开销。为了提高我们对 Xamarin 和跨平台开发的理解,让我们为 iOS 和 Android 开发一个名为XamChat的简单应用程序。
在本章中,我们将涵盖以下主题:
-
我们的示例应用程序概念
-
我们应用程序的模型层
-
模拟一个网络服务
-
我们应用程序的视图模型层
-
编写单元测试
开始我们的示例应用程序概念
概念很简单:一个使用标准互联网连接作为发送文本消息替代方案的聊天应用程序。在苹果应用商店中有几个类似的应用程序,这可能是由于短信的成本以及 iPod Touch 或 iPad 等设备的支持。这将是一个整洁的现实世界示例,对用户来说可能很有用,并将涵盖为 iOS 和 Android 开发应用程序的特定主题。
在我们开始开发之前,让我们列出我们将需要的屏幕集:
-
登录/注册:此屏幕将包括用户的标准登录和注册过程
-
对话列表:此屏幕将包括一个按钮来开始新的对话
-
朋友列表:此屏幕将在我们开始新对话时提供添加新朋友的方式
-
对话:此屏幕将显示您和另一个用户之间的消息列表,以及回复的选项
因此,一个快速的应用程序线框布局将帮助你更好地理解应用程序的布局。以下图显示了您应用程序中应包含的屏幕集:

开发我们的模型层
由于我们对应用程序有了很好的了解,下一步是开发这个应用程序的业务对象,即模型层。让我们先定义几个将包含整个应用程序所需数据的类。为了组织上的原因,建议将这些类添加到项目中的Models文件夹中。这是 MVVM 设计模式的底层。
让我们从代表用户的类开始。该类可以创建如下:
public class User
{
public string Id { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
到目前为止,事情相当直接;让我们继续创建代表对话和消息的类的如下:
public class Conversation
{
public string Id { get; set; }
public string UserId { get; set; }
public string Username { get; set; }
}
public class Message
{
public string Id { get; set; }
public string ConversationId { get; set; }
public string UserId { get; set; }
public string Username { get; set; }
public string Text { get; set; }
}
注意,我们正在使用字符串作为各种对象的标识符。这将在后续章节中简化我们与 Azure Mobile Services 的集成。UserId是应用程序将设置的值,用于更改与对象关联的用户。
现在,让我们继续通过以下步骤来设置我们的解决方案:
-
首先创建一个新的解决方案和一个新的可移植库项目。
-
将项目命名为
XamChat.Core,解决方案命名为XamChat。 -
你也可以选择为这个项目使用一个 共享项目,但我选择使用可移植类库,因为它鼓励更好的编程实践。
编写模拟网络服务
在开发移动应用程序时,很多时候你需要在真正的后端网络服务可用之前就开始开发你的应用程序。为了防止开发完全停滞,一个很好的方法就是开发一个服务的模拟版本。这在你需要编写单元测试,或者稍后需要向你的应用程序添加真实后端时也非常有用。
首先,让我们分解我们的应用程序将对网络服务器执行的操作。操作如下:
-
使用用户名和密码登录。
-
注册一个新账户。
-
获取用户的联系人列表。
-
通过用户名添加朋友。
-
获取用户现有对话的列表。
-
获取对话中的消息列表。
-
发送一条消息。
现在,让我们定义一个接口,为每个场景提供一个方法。方法如下:
public interface IWebService
{
Task<User> Login(string username, string password);
Task<User> Register(User user);
Task<User[]> GetFriends(string userId);
Task<User> AddFriend(string userId, string username);
Task<Conversation[]> GetConversations(string userId);
Task<Message[]> GetMessages(string conversationId);
Task<Message> SendMessage(Message message);
}
如你所见,我们通过利用 .NET 基类库中的 任务并行库(TPL)简化了与网络服务的任何异步通信。
由于与网络服务的通信可能是一个漫长的过程,因此始终使用 Task<T> 类进行这些操作是一个好主意。否则,你可能会无意中在用户界面线程上运行一个漫长的任务,这将阻止在操作期间用户输入。Task 对于网络请求绝对是必需的,因为用户可能在 iOS 和 Android 上使用蜂窝互联网连接,它将使我们能够将来使用 async 和 await 关键字。
现在,让我们实现一个实现此接口的模拟服务。将 FakeWebService 类等放在项目的 Fakes 文件夹中。让我们从类声明和接口的第一个方法开始:
public class FakeWebService
{
public int SleepDuration { get; set; }
public FakeWebService()
{
SleepDuration = 1;
}
private Task Sleep()
{
return Task.Delay(SleepDuration);
}
public async Task<User> Login(string username, string password)
{
await Sleep();
return new User { Id = "1", Username = username };
}
}
我们最初使用了一个 SleepDuration 属性来存储一个以毫秒为单位的数字。这用于模拟与网络服务交互,这可能需要一些时间。它也有助于在不同情况下更改 SleepDuration 的值。例如,你可能希望在编写单元测试时将其设置为较小的数字,以便测试可以快速执行。
接下来,我们实现了一个简单的 Sleep 方法,该方法返回一个任务,引入了若干毫秒的延迟。这个方法将在整个模拟服务中用于在每个操作上引入延迟。
最后,Login 方法仅仅是在 Sleep 方法上使用了一个 await 调用,并返回了一个带有适当 Username 的 new User 对象。目前,任何用户名或密码组合都将有效;然而,你可能希望在这里编写一些代码来检查特定的凭据。
现在,让我们实现一些更多的方法,以继续我们的 FakeWebService 类,如下所示:
public async Task<User> Register(User user)
{
await Sleep();
return user;
}
public async Task<User[]> GetFriends(string userId)
{
await Sleep();
return new[]
{
new User { Id = "2", Username = "bobama" },
new User { Id = "3", Username = "bobloblaw" },
new User { Id = "4", Username = "gmichael" },
};
}
public async Task<User> AddFriend(string userId, string username)
{
await Sleep();
return new User { Id = "5", Username = username };
}
对于这些方法中的每一个,我们都使用了与 Login 方法完全相同的模式。每个方法都会延迟并返回一些示例数据。请随意将数据与您自己的值混合。
现在,让我们按照以下方式实现接口所需的 GetConversations 方法:
public async Task<Conversation[]> GetConversations(string userId)
{
await Sleep();
return new[]
{
new Conversation { Id = "1", UserId = "2" },
new Conversation { Id = "2", UserId = "3" },
new Conversation { Id = "3", UserId = "4" },
};
}
基本上,我们只是创建了一个新的 Conversation 对象数组,具有任意 ID。我们还确保将 UserId 值与迄今为止在 User 对象上使用的 ID 匹配。
接下来,让我们实现 GetMessages 以获取消息列表,如下所示:
public async Task<Message[]> GetMessages(int conversationId)
{
await Sleep();
return new[]
{
new Message
{
Id = "1",
ConversationId = conversationId,
UserId = "2",
Text = "Hey",
},
new Message
{
Id = "2",
ConversationId = conversationId,
UserId = "1",
Text = "What's Up?",
},
new Message
{
Id = "3",
ConversationId = conversationId,
UserId = "2",
Text = "Have you seen that new movie?",
},
new Message
{
Id = "4",
ConversationId = conversationId,
UserId = "1",
Text = "It's great!",
},
};
}
再次,我们在这里添加了一些任意数据,并主要确保 UserId 和 ConversationId 与我们迄今为止现有的数据匹配。
最后,我们将编写一个发送消息的方法,如下所示:
public async Task<Message> SendMessage(Message message)
{
await Sleep();
return message;
}
这些方法中的大多数都非常直接。请注意,服务不必完美工作;它只需在延迟后成功完成每个操作即可。每个方法还应该返回某种测试数据,以便在 UI 中显示。这将使我们能够在填充 Web 服务的同时实现我们的 iOS 和 Android 应用程序。
接下来,我们需要实现一个用于持久化应用程序设置的简单接口。让我们定义一个名为 ISettings 的接口,如下所示:
public interface ISettings
{
User User { get; set; }
void Save();
}
我们将 ISettings 设置为同步的,但如果你计划在云中存储设置,你可能希望将 Save 方法设置为异步并返回 Task。对于我们的应用程序,我们并不真正需要这样做,因为我们只会将设置保存在本地。
之后,我们将使用 Android 和 iOS API 在每个平台上实现这个接口。现在,让我们只实现一个将用于稍后编写单元测试的假版本。我们将使用以下代码行实现接口:
public class FakeSettings : ISettings
{
public User User { get; set; }
public void Save() { }
}
注意,假版本实际上不需要做任何事情;我们只需要提供一个实现接口并不会抛出任何意外错误的类。
这就完成了应用程序的模型层。以下是到目前为止我们已实现的最终类图:

编写 ViewModel 层
ViewModels folder within your project:
public class BaseViewModel
{
protected readonly IWebService service = ServiceContainer.Resolve<IWebService>();
protected readonly ISettings settings = ServiceContainer.Resolve<ISettings>();
public event EventHandler IsBusyChanged = delegate { };
private bool isBusy = false;
public bool IsBusy
{
get { return isBusy; }
set
{
isBusy = value;
IsBusyChanged(this, EventArgs.Empty);
}
}
}
BaseViewModel 类是一个放置任何计划在整个应用程序中重用的公共功能的好地方。对于这个应用程序,我们只需要实现一些功能来指示 ViewModel 层是否忙碌。我们提供了一个属性和一个事件,UI 将能够订阅并在屏幕上显示等待指示器。我们还添加了一些需要的服务字段。还可以添加的一个常见功能是对用户输入进行验证;然而,对于这个应用程序,我们并不真正需要它。
实现我们的 LoginViewModel 类
现在我们已经为所有 ViewModel 层创建了一个基类,我们可以为我们的应用程序中的第一个屏幕实现一个 ViewModel 层,即 登录 屏幕。
现在,让我们按照以下方式实现 LoginViewModel 类:
public class LoginViewModel : BaseViewModel
{
public string Username { get; set; }
public string Password { get; set; }
public async Task Login()
{
if (string.IsNullOrEmpty(Username))
throw new Exception("Username is blank.");
if (string.IsNullOrEmpty(Password))
throw new Exception("Password is blank.");
IsBusy = true;
try
{
settings.User = await service.Login(Username, Password);
settings.Save();
}
finally
{
IsBusy = false;
}
}
}
在这个类中,我们实现了以下内容:
-
我们从
BaseViewModel派生类以获取访问IsBusy和包含常见服务的字段 -
我们添加了
Username和Password属性,由视图层进行设置 -
我们添加了一个
User属性,当登录过程完成时进行设置 -
我们实现了一个
Login方法,可以从视图调用,并在Username和Password属性上进行验证 -
在调用
IWebService上的Login方法时,我们设置了IsBusy -
我们通过等待来自网络服务中
Login操作的User属性结果来设置User属性
基本上,这是我们将在应用程序中其余的 ViewModel 层中遵循的模式。我们提供属性供视图层根据用户输入进行设置,并提供用于各种操作的方法。如果是一个可能需要一些时间的方法,例如网络请求,你应该始终返回Task并使用async和await关键字。
小贴士
注意,我们使用了try和finally块来将IsBusy重置为false。这将确保即使在抛出异常的情况下也能正确重置。我们计划在视图层处理错误,以便向用户显示一个包含消息的原生弹出窗口。
实现我们的RegisterViewModel类
由于我们已经完成了我们的ViewModel类以进行登录,我们现在需要创建一个用于用户注册的类。
让我们实现另一个 ViewModel 层来注册新用户:
public class RegisterViewModel : BaseViewModel
{
public string Username { get; set; }
public string Password { get; set; }
public string ConfirmPassword { get; set; }
}
这些属性将处理来自用户的输入。接下来,我们需要添加一个Register方法,如下所示:
public async Task Register()
{
if (string.IsNullOrEmpty(Username))
throw new Exception("Username is blank.");
if (string.IsNullOrEmpty(Password))
throw new Exception("Password is blank.");
if (Password != ConfirmPassword)
throw new Exception("Passwords don't match.");
IsBusy = true;
try
{
settings.User = await service.Register(new User { Username = Username, Password = Password, });
settings.Save();
}
finally
{
IsBusy = false;
}
}
RegisterViewModel类与LoginViewModel类非常相似,但额外有一个用于 UI 设置的ConfirmPassword属性。遵循 ViewModel 层功能分割的良好规则是,当 UI 有新屏幕时,始终创建一个新的类。这有助于你保持代码整洁,并在一定程度上遵循类的单一职责原则(SRP)。SRP指出,一个类应该只有一个目的或职责。我们将尝试遵循这个概念,以保持我们的类小巧且有序,这在跨平台共享代码时可能比通常更重要。
实现我们的FriendViewModel类
接下来是处理用户朋友列表的 ViewModel 层。我们需要一个方法来加载用户的朋友列表并添加一个新朋友。
现在我们来实现FriendViewModel,如下所示:
public class FriendViewModel : BaseViewModel
{
public User[] Friends { get; private set; }
public string Username { get; set; }
}
现在我们需要一个方法来加载朋友。此方法如下所示:
public async Task GetFriends()
{
if (settings.User == null)
throw new Exception("Not logged in.");
IsBusy = true;
try
{
Friends = await service.GetFriends(settings.User.Id);
}
finally
{
IsBusy = false;
}
}
最后,我们需要一个方法来添加一个新朋友并更新本地包含的朋友列表:
public async Task AddFriend()
{
if (settings.User == null)
throw new Exception("Not logged in.");
if (string.IsNullOrEmpty(Username))
throw new Exception("Username is blank.");
IsBusy = true;
try
{
var friend = await service.AddFriend(settings.User.Id, Username);
//Update our local list of friends
var friends = new List<User>();
if (Friends != null)
friends.AddRange(Friends);
friends.Add(friend);
Friends = friends.OrderBy(f => f.Username).ToArray();
}
finally
{
IsBusy = false;
}
}
同样,这个类相当直接。唯一的新增内容是我们添加了一些逻辑来更新朋友列表并在我们的客户端应用程序中对其进行排序,而不是在服务器上。你也可以选择在有充分理由的情况下重新加载朋友列表的完整列表。
实现我们的MessageViewModel类
我们最终需要的 ViewModel 层将处理消息和对话。我们需要创建一种加载对话和消息以及发送新消息的方法。
让我们按照以下步骤开始实现我们的MessageViewModel类:
public class MessageViewModel : BaseViewModel
{
public Conversation[] Conversations { get; private set; }
public Conversation Conversation { get; set; }
public Message[] Messages { get; private set; }
public string Text { get; set; }
}
接下来,让我们实现一个方法来检索对话列表,如下所示:
public async Task GetConversations()
{
if (settings.User == null)
throw new Exception("Not logged in.");
IsBusy = true;
try
{
Conversations = await service.GetConversations(settings.User.Id);
}
finally
{
IsBusy = false;
}
}
同样,我们还需要在对话中检索消息列表。我们需要将对话 ID 传递给服务,如下所示:
public async Task GetMessages()
{
if (Conversation == null)
throw new Exception("No conversation.");
IsBusy = true;
try
{
Messages = await service.GetMessages(Conversation.Id);
}
finally
{
IsBusy = false;
}
}
最后,我们需要编写一些代码来发送消息并更新本地消息列表,如下所示:
public async Task SendMessage()
{
if (settings.User == null)
throw new Exception("Not logged in.");
if (Conversation == null)
throw new Exception("No conversation.");
if (string.IsNullOrEmpty (Text))
throw new Exception("Message is blank.");
IsBusy = true;
try
{
var message = await service.SendMessage(new Message
{
UserId = settings.User.Id,ConversationId = Conversation.Id,
Text = Text
});
//Update our local list of messages
var messages = new List<Message>();
if (Messages != null)
messages.AddRange(Messages);
messages.Add(message);
Messages = messages.ToArray();
}
finally
{IsBusy = false;
}
}
这就完成了我们应用程序的 ViewModel 层以及 iOS 和 Android 上使用的所有共享代码。对于MessageViewModel类,你也可以选择将GetConversations和Conversations属性放在它们自己的类中,因为它们可以被视为一个单独的责任,但这并不是真的必要。
这是我们的 ViewModel 层的最终类图:

编写单元测试
由于我们迄今为止编写的所有代码都不依赖于用户界面,我们可以轻松地为我们的类编写单元测试。这一步骤通常在ViewModel类的首次实现之后进行。测试驱动开发(TDD)的支持者会建议先编写测试,然后再实现功能,所以选择最适合你的方法。在任何情况下,在从视图层使用之前编写针对共享代码的测试都是一个好主意,这样你就可以在它们阻碍你的 UI 开发之前捕捉到错误。
Xamarin 项目利用一个名为NUnit的开源测试框架。它最初是从一个名为JUnit的 Java 测试框架派生出来的,并且是 C#应用程序单元测试的事实标准。Xamarin Studio 提供了几个用于编写 NUnit 测试的项目模板。
设置单元测试的新项目
让我们通过以下步骤设置一个新的单元测试项目:
-
在解决方案中添加一个新的NUnit 库项目,该项目位于C#部分。
-
将项目命名为
XamChat.Tests以保持一致性。 -
接下来,让我们在项目选项下将库设置为 Mono/.NET 4.5 项目,然后导航到构建 | 常规 | 目标框架。
-
右键单击项目引用并选择编辑引用。
-
在项目选项卡下,添加对XamChat.Core的引用。
-
现在,打开
Test.cs文件,你会注意到以下必需的属性,它们构成了使用 NUnit 的单元测试:-
using NUnit.Framework: 这个属性是用于与 NUnit 一起使用的主要语句 -
[测试用例]: 这个装饰器用于表示一个类包含运行测试的方法列表 -
[测试]: 这个装饰器用于表示一个测试方法
-
除了必需的 C#属性之外,还有一些其他属性对于编写测试很有用,如下所示:
-
[TestFixtureSetUp]: 这装饰了一个在测试固定类中所有测试之前运行的方法。 -
[SetUp]: 这装饰了一个在测试固定类中每个测试之前运行的方法。 -
[TearDown]: 这装饰了一个在测试固定类中每个测试之后运行的方法。 -
[TestFixtureTearDown]: 这装饰了一个在测试固定类中所有测试完成后运行的方法。 -
[ExpectedException]: 这装饰了一个旨在抛出异常的方法。对于应该失败的测试用例来说,它非常有用。 -
[Category]: 这装饰了一个测试方法,可以用来组织不同的测试;例如,你可能将它们分类为快速测试和慢速测试。
编写断言
在使用 NUnit 编写测试时,下一个要了解的概念是如何编写断言。断言是一个方法,如果某个值不正确,它将抛出一个异常。它将导致测试失败,并提供关于发生了什么的描述性解释。NUnit 有几个不同的断言 API 集;然而,我们将使用更易读的流式 API 版本。
流式 API 的基本语法是使用Assert.That方法。以下是一个例子:
Assert.That(myVariable, Is.EqualTo(0));
同样,你也可以断言相反的情况:
Assert.That(myVariable, Is.Not.EqualTo(0));
或者以下任何一个:
-
Assert.That(myVariable, Is.GreaterThan(0)); -
Assert.That(myBooleanVariable, Is.True); -
Assert.That(myObject, Is.Not.Null);
随意探索 API。在 Xamarin Studio 中的代码补全功能,你应该能够发现Is类中的有用静态成员或方法,以便在测试中使用。
在我们开始为我们的应用程序编写特定测试之前,让我们编写一个静态类和方法来创建一个全局设置,以便在整个测试中使用。你可以将Test.cs重写如下:
public static class Test
{
public static void SetUp()
{
ServiceContainer.Register<IWebService>(() =>new FakeWebService { SleepDuration = 0 });
ServiceContainer.Register<ISettings>(() =>new FakeSettings());
}
}
我们将在整个测试中使用此方法来设置模型层中的模拟服务。此外,这还替换了现有的服务,以便我们的测试针对这些类的新实例执行。这是单元测试中的一个良好实践,以确保没有从之前的测试中留下旧数据。另外,请注意我们设置了SleepDuration为0。这将使我们的测试运行得非常快。
我们将首先在我们的测试项目中创建一个ViewModels文件夹,并添加一个名为LoginViewModelTests的类,如下所示:
[TestFixture]
public class LoginViewModelTests
{
LoginViewModel loginViewModel;
ISettings settings;
[SetUp]
public void SetUp()
{
Test.SetUp();
settings = ServiceContainer.Resolve<ISettings>();
loginViewModel = new LoginViewModel();
}
[Test]
public async Task LoginSuccessfully()
{
loginViewModel.Username = "testuser";
loginViewModel.Password = "password";
await loginViewModel.Login();
Assert.That(settings.User, Is.Not.Null);
}
}
注意我们使用SetUp方法。我们重新创建每个测试中使用的对象,以确保没有从之前的测试运行中留下旧数据。另一个需要注意的点是,在使用测试方法中的async/await时,你必须返回一个Task。否则,NUnit 将无法知道何时测试完成。
要运行测试,请使用位于 Xamarin Studio 右侧的 NUnit 菜单,默认情况下。点击带有齿轮图标的运行测试按钮来运行测试。你将得到一个类似于以下截图的成功结果:

你还可以查看测试结果面板,如果测试失败,它将显示扩展的详细信息,如下面的截图所示:

要查看测试失败时会发生什么,请修改你的测试以断言一个错误值,如下所示:
//Change Is.Not.Null to Is.Null
Assert.That(settings.User, Is.Null);
你将在测试结果面板中看到一个非常详细的错误,如下面的截图所示:

现在我们来实现LoginViewModel类的另一个测试;确保如果用户名和密码字段为空时,我们得到适当的输出。测试的实现如下:
[Test]
public async Task LoginWithNoUsernameOrPassword()
{
//Throws an exception
await loginViewModel.Login();
}
如果我们按照原样运行测试,将会抛出异常,测试将失败。由于我们预期会发生异常,我们可以装饰该方法,使得测试仅在发生异常时通过,如下所示:
[Test,
ExpectedException(typeof(Exception),
ExpectedMessage = "Username is blank.")]
小贴士
注意,在我们的视图模型中,如果字段为空,将抛出异常类型。你还可以在期望的异常类型不同的情况下更改期望的异常类型。
样本代码以及本书中包含更多测试。建议你对每个ViewModel类上的每个公共操作编写测试。此外,为任何验证或其他重要业务逻辑编写测试。我还建议你对模型层编写测试;然而,在我们的项目中这还不是必需的,因为我们只有模拟实现。
摘要
在本章中,我们探讨了构建一个名为 XamChat 的示例应用程序的概念。我们还实现了应用程序在模型层中的核心业务对象。由于我们还没有服务器来支持这个应用程序,我们实现了一个模拟的 Web 服务。这让你在不需要构建服务器应用程序的情况下继续前进有了灵活性。我们还实现了视图模型层。这一层将以简单的方式向视图层公开操作。最后,我们使用 NUnit 编写了覆盖我们迄今为止所编写代码的测试。在跨平台应用程序中对共享代码编写测试可能非常重要,因为它是多个应用程序的骨架。
完成这一章后,你应该已经完成了我们跨平台应用程序的共享库的全部内容。你应该对我们的应用程序架构及其独特的模型和视图模型层有非常牢固的掌握。你还应该很好地理解如何编写你尚未准备好实现的某些应用程序部分的模拟版本。在下一章中,我们将实现 XamChat 的 iOS 版本。
第五章:XamChat for iOS
在本章中,我们将开发我们的跨平台 XamChat 应用程序的 iOS 部分。由于我们正在使用 MVVM 设计模式,我们将要做的大部分工作将在应用程序的视图层。我们将主要使用原生 iOS API,并了解我们如何利用便携式类库中的共享代码来应用它们。由于 Xamarin.iOS 允许我们直接调用 Apple API,我们的 iOS 应用程序将无法与使用 Objective-C 或 Swift 开发的程序区分开来。
要开始编写 XamChat 的 iOS 版本,请在iOS部分下创建一个新的单视图应用程序。将项目命名为XamChat.iOS或您选择的任何其他适当名称。项目模板将自动创建一个具有不熟悉名称的控制器;请继续删除它。我们将随着工作的进行创建自己的控制器。
在本章中,我们将涵盖以下内容:
-
iOS 应用程序的基本知识
-
使用
UINavigationController -
实现登录界面
-
转场和
UITableView -
添加好友列表
-
添加消息列表
-
消息的编写
理解 iOS 应用程序的基本知识
在我们开始开发我们的应用程序之前,让我们回顾一下应用程序的主要设置。Apple 使用名为Info.plist的文件来存储有关任何 iOS 应用程序的重要信息。这些设置用于当 iOS 应用程序通过 Apple 应用商店安装到设备上时。我们将通过填写此文件中的信息开始任何新的 iOS 应用程序的开发。
Xamarin Studio 提供了一个方便的菜单来修改Info.plist文件中的值,如下面的截图所示:

最重要的设置如下:
-
应用程序名称:这是 iOS 中应用程序图标下的标题。请注意,这与您在 iOS 应用商店中应用程序的官方名称不同。
-
捆绑标识符:这是您的应用程序的捆绑标识符或捆绑 ID。这是一个独特的名称,用于标识您的应用程序。惯例是使用以您的公司名称开始的反向域名命名风格,例如
com.packt.xamchat。 -
版本:这是您的应用程序的版本号,例如
1.0.0。 -
设备:在此字段中,您可以为您的应用程序选择iPhone/iPod、iPad或通用(所有设备)。
-
部署目标:这是您的应用程序运行的最小 iOS 版本。
-
主界面:这是您的应用程序的主要故事板文件,它声明了应用程序的大部分 UI。iOS 将自动加载此文件,并将根控制器作为初始屏幕打开。
-
支持的设备方向:这是您的应用程序能够旋转到的不同位置。
还有其他设置,如应用程序图标、启动画面等。您还可以在高级或源选项卡之间切换,以配置 Xamarin 不提供用户友好菜单的附加设置。
为我们的应用程序配置以下设置:
-
应用程序名称:
XamChat -
捆绑标识符:
com.yourcompanyname.xamchat;请确保您将未来的应用程序命名为以com.yourcompanyname开头 -
版本:可以是您喜欢的任何版本号,但请不要留空
-
设备:iPhone/iPod
-
部署目标:7.0(您也可以选择 8.0,但在这个应用程序中我们没有使用任何 iOS 8 特定的 API)
-
支持的设备方向:仅选择纵向
如果您在项目上右键单击并选择选项,您可以找到一些 Xamarin iOS 应用程序的附加设置。了解 Xamarin Studio 中 iOS 特定项目可用的选项是个好主意。
让我们讨论一些最重要的选项。
-
按照以下截图导航到iOS 构建 | 通用选项卡:
![理解 iOS 应用程序的基本知识]()
在此选项卡下,您有以下选项:
-
SDK 版本:这是编译应用程序的 iOS SDK 版本。通常最好使用默认。
-
链接器行为:Xamarin 实现了一个名为链接的功能。链接器将删除在您的程序集中永远不会被调用的任何代码。这使您的应用程序保持小巧,并允许它们与您的应用程序一起发布核心 Mono 框架的精简版。除了调试构建外,最好使用仅链接 SDK 程序集选项。我们将在下一章中介绍链接。
-
优化 iOS PNG 文件:Apple 使用自定义 PNG 格式来加快应用程序中 PNG 的加载速度。您可以选择关闭此选项以加快构建速度,或者如果您计划自己优化图像。
-
启用调试:开启此选项允许 Xamarin 在应用程序中包含额外的信息,以便从 Xamarin Studio 进行调试。
-
额外的 mtouch 参数:此字段用于向 Xamarin 编译器传递额外的命令行参数。您可以在
iosapi.xamarin.com查看这些参数的完整列表。
-
-
按照以下截图导航到iOS 构建 | 高级选项卡:
![理解 iOS 应用程序的基本知识]()
在此选项卡下,您有以下选项:
-
支持的架构:这里,选项包括ARMv7、ARMv7s和包含两者的FAT版本。这些是不同 iOS 设备处理器支持的指令集。如果您真的关心性能,您可能需要考虑选择支持两者的选项;然而,这将使您的应用程序更大。
-
使用 LLVM 优化编译器:勾选此选项将编译出更小且运行速度更快的代码,但编译时间会更长。LLVM代表低级虚拟机。
-
启用通用值类型共享:这是一个针对 Mono 的特定选项,可以从 C# 通用值类型中获得更好的性能。缺点是会使应用程序略微增大,但我建议您保留此选项。
-
使用 SGen 代际垃圾回收器:这将在您的应用程序中使用新的 Mono 垃圾回收器。如果您确实需要良好的性能并且垃圾回收器(GC)或正在开发需要实时响应的应用程序,例如游戏,我建议您启用此功能。现在默认启用可能是安全的,因为 SGen 垃圾回收器非常稳定。
-
使用引用计数扩展(预览版):这是一个当前处于实验阶段的功能,但可以改善从 C# 访问的本地对象的内存使用。当使用此设置时,这些本地对象的引用由 GC 管理而不是对象上的后备字段。由于它仍在预览中,使用此选项时应谨慎。
-
-
在 iOS 包签名 下,您有以下选项:
-
身份:这是用于将应用程序部署到设备的创建者身份的证书。我们将在后面的章节中详细介绍。
-
配置文件:这是一个将应用程序部署到设备的特定配置文件。它与身份协同工作,但还声明了分发方法和可以安装应用程序的设备。
-
自定义权限:此文件包含应用于配置文件的其他设置,并包含对应用程序的其他特定声明,例如 iCloud 或推送通知。iOS 应用程序的模板项目包括一个默认的
Entitlements.plist文件用于新项目。
-
-
iOS 应用程序:这些设置与您在
Info.plist文件中看到的是相同的。
对于此应用程序,您可以将所有这些选项保留在默认设置。当您自己制作真正的 iOS 应用程序时,您应该考虑根据应用程序的需求更改其中许多设置。
使用 UINavigationController
在 iOS 应用程序中,管理不同控制器之间导航的关键类是 UINavigationController 类。导航控制器是 iOS 上导航的最基本构建块,因此它是大多数 iOS 应用程序的最佳起点。它是一个包含多个子控制器在堆栈中的父控制器。用户可以通过将新控制器放在堆栈顶部或使用内置的返回按钮从堆栈中弹出控制器来前进,从而导航到上一个屏幕。
导航控制器中的方法
以下是在导航控制器中的方法:
-
SetViewControllers:这设置一个子控制器数组。它有一个可选的值来动画过渡。 -
ViewControllers:这是一个获取或设置子控制器数组的属性,没有动画选项。 -
PushViewController:这将在堆栈顶部放置一个新的子控制器,并可以选择显示动画。 -
PopViewControllerAnimated:这将带有动画选项从堆栈顶部的子控制器中弹出。 -
PopToViewController:这将弹出指定的子控制器,移除其上方的所有控制器。它提供了一个动画转换的选项。 -
PopToRootViewController:这将移除除了最底层的控制器之外的所有子控制器。它包括一个显示动画的选项。 -
TopViewController:这是一个属性,它返回当前位于堆栈顶部的子控制器。
小贴士
需要注意的是,如果在使用动画选项的同时尝试修改动画过程中的堆栈,将会导致崩溃。为了解决这个问题,你可以使用SetViewControllers方法并设置整个子控制器列表,或者避免在组合转换中使用动画。
设置导航控制器
执行以下步骤以设置导航控制器:
-
双击
MainStoryboard.storyboard文件以在 Xamarin Studio 中打开它。 -
删除由项目模板创建的控制器。
-
从右侧的工具箱面板中拖动一个导航控制器元素到故事板中。
-
注意,除了视图控制器元素外,还创建了一个导航控制器元素。
-
你将看到一个segue连接了两个控制器。我们将在本章的后面更详细地介绍这个概念。
-
保存故事板文件。
如果你现在运行应用程序,你将拥有一个基本的 iOS 应用程序,顶部有一个状态栏,一个包含默认标题的导航控制器,以及一个完全白色的子控制器,如下面的截图所示:

实现登录屏幕
由于我们应用程序的第一个屏幕将是一个登录屏幕,让我们首先在故事板文件中设置适当的视图。我们将使用 Xamarin Studio 编写 C#代码来实现登录屏幕,并使用 iOS 设计器在我们的故事板文件中创建 iOS 布局。
创建 LoginController 类
返回 Xamarin Studio 中的项目并执行以下步骤:
-
双击
MainStoryboard.storyboard文件以在 iOS 设计器中打开它。 -
选择你的视图控制器,点击属性面板并选择小部件选项卡。
-
在类字段中输入
LoginController。 -
注意,
LoginController类已经为你生成了。如果你愿意,你可以创建一个Controllers文件夹并将文件移入其中。
以下截图显示了在做出更改后,Xamarin Studio 中控制器的设置将看起来是什么样子:

修改控制器的布局
现在让我们通过以下步骤修改控制器的布局:
-
双击
MainStoryboard.storyboard文件第二次,以返回到 iOS 设计器。 -
点击导航栏并编辑标题字段,使其显示为
登录。 -
将两个文本字段拖放到控制器中。适当地定位和调整它们的大小,以适应用户名和密码输入。你可能还想删除默认文本,使字段为空。
-
对于第二个字段,勾选安全文本输入复选框。这将使控件隐藏密码字段的字符。
-
你可能还想分别为
用户名和密码填写占位符字段。 -
将一个按钮拖放到控制器中。设置按钮的标题为
登录。 -
将活动指示器拖放到控制器中。勾选动画和隐藏复选框。
-
接下来,为每个控件创建一个出口,通过填写名称字段。将出口命名为
username、password、login和indicator。 -
保存故事板文件,并查看
LoginController.designer.cs。
你会发现 Xamarin Studio 为每个出口生成了属性:

继续编译应用程序以确保一切正常。在此阶段,我们还需要将XamChat.Core项目添加为引用,该项目是在上一章中创建的。
注册和订阅视图模型和服务
接下来,让我们设置我们的 iOS 应用程序以注册其所有视图模型和其他将在整个应用程序中使用的服务。我们将使用在第四章中创建的ServiceContainer类,XamChat – 一个跨平台应用,来设置我们应用程序中的依赖项。打开AppDelegate.cs并添加以下方法:
public override bool FinishedLaunching(UIApplication application,NSDictionary launchOptions)
{
//View Models
ServiceContainer.Register<LoginViewModel>(() =>new LoginViewModel());
ServiceContainer.Register<FriendViewModel>(() =>new FriendViewModel());
ServiceContainer.Register<RegisterViewModel>(() =>new RegisterViewModel());
ServiceContainer.Register<MessageViewModel>(() =>new MessageViewModel());
//Models
ServiceContainer.Register<ISettings>(() =>new FakeSettings());
ServiceContainer.Register<IWebService>(() =>new FakeWebService());
return true;
}
在以后,我们将用真实的服务替换假的服务。现在,让我们将登录功能添加到LoginController.cs中。首先,将LoginViewModel添加到类顶部的成员变量中,如下所示:
readonly LoginViewModel loginViewModel = ServiceContainer.Resolve<LoginViewModel>();
这将把LoginViewModel的共享实例拖入控制器中的局部变量。这是我们将在整本书中使用的模式,以便将共享视图模型从一个类传递到另一个类。
接下来,重写ViewDidLoad以将视图模型的功能与在出口中设置的视图连接起来,如下所示:
public override void ViewDidLoad()
{
base.ViewDidLoad();
login.TouchUpInside += async(sender, e) =>
{
loginViewModel.Username = username.Text;
loginViewModel.Password = password.Text;
try
{
await loginViewModel.Login();
//TODO: navigate to a new screen
}
catch (Exception exc)
{
new UIAlertView("Oops!", exc.Message, null, "Ok").Show();
}
};
}
我们将在本章的后面添加代码以导航到新屏幕。
接下来,让我们将IsBusyChanged事件连接起来,以便执行以下操作:
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
loginViewModel.IsBusyChanged += OnIsBusyChanged;
}
public override void ViewWillDisappear(bool animated)
{
base.ViewWillDisappear(animated);
loginViewModel.IsBusyChanged -= OnIsBusyChanged;
}
void OnIsBusyChanged(object sender, EventArgs e)
{
username.Enabled =
password.Enabled =
login.Enabled =
indicator.Hidden = !loginViewModel.IsBusy;
}
现在你可能想知道,为什么我们以这种方式订阅事件。问题是LoginViewModel类将贯穿你的应用程序生命周期,而LoginController类则不会。如果我们订阅了ViewDidLoad中的事件,但没有在之后取消订阅,那么我们的应用程序将出现内存泄漏。我们还避免了使用 lambda 表达式来处理事件,因为这会使取消订阅事件变得不可能。请注意,我们在按钮的TouchUpInside事件上没有遇到相同的问题,因为它将像控制器一样在内存中持续存在。这是 C#中事件的一个常见问题,这就是为什么在 iOS 上使用前面提到的模式是一个好主意。
如果你现在运行应用程序,你应该能够输入用户名和密码,如下面的截图所示。当你按下登录时,你应该看到指示器出现,所有控件被禁用。你的应用程序将正确调用共享代码,并且在我们添加真正的 web 服务时应该能够正确运行。

使用 segue 和 UITableView
segue 是从一个控制器到另一个控制器的过渡。同样,一个 storyboard 文件是由 segue 连接在一起的控制器和它们的视图的集合。这反过来又允许你同时看到每个控制器的布局和应用程序的一般流程。
segue 只有几种类型,如下所示:
-
推送:这用于导航控制器内部。它将新的控制器推送到导航控制器堆栈的顶部。推送使用导航控制器的标准动画技术,通常是使用最频繁的 segue。
-
关系:这用于设置另一个控制器的子控制器。例如,导航控制器的根控制器、容器视图或 iPad 应用程序中的分割视图控制器。
-
模式:使用此模式时,一个模态控制器将出现在父控制器之上。它将覆盖整个屏幕,直到被取消。有几种不同的过渡动画可供选择。
-
自定义:这是一个包含自定义类选项的自定义 segue,该类是
UIStoryboardSegue的子类。这让你可以精细控制动画以及下一个控制器的呈现方式。
在执行过程中,segue 还使用以下模式:
-
目标控制器及其视图被创建。
-
创建了一个 segue 对象,它是
UIStoryboardSegue的子类。这通常只对自定义 segue 很重要。 -
在源控制器上调用
PrepareForSegue方法。这是一个在 segue 开始前运行任何自定义代码的好地方。 -
调用 segue 的
Perform方法并开始过渡动画。这是自定义 segue 代码的主要部分。
在 Xamarin.iOS 设计器中,你可以选择从按钮或表格视图行自动触发 segues,或者只是给 segues 一个标识符。在第二种情况下,你可以通过在源控制器上调用其标识符的PerformSegue方法来自己启动 segues。
现在,让我们通过执行以下步骤设置一个新的 segues,通过设置MainStoryboard.storyboard文件的一些方面来设置:
-
双击
MainStoryboard.storyboard文件以在 iOS 设计器中打开它。 -
在故事板中添加一个新的表格视图控制器。
-
选择你的视图控制器,并导航到属性面板和小部件选项卡。
-
在类字段中输入
ConversationsController。 -
在视图控制器部分下方滚动,并输入
Conversations的标题。 -
通过点击并按住Ctrl,从
LoginController拖动蓝色线到另一个控制器来创建从LoginController到ConversationsController的 segues。 -
从出现的弹出菜单中选择推送segues。
-
通过点击并选择 segues,给它一个标识符为
OnLogin。 -
保存故事板文件。
你的故事板将看起来类似于以下截图所示:

打开LoginController.cs,并修改本章前面标记为TODO的代码行如下:
PerformSegue("OnLogin", this);
现在,如果你构建并运行应用程序,你将在成功登录后导航到新的控制器。将执行 segues,你将看到导航控制器提供的内置动画。
接下来,让我们设置第二个控制器上的表格视图。我们在 iOS 上使用一个强大的类UITableView。它在许多情况下都得到使用,并且与其他平台上的列表视图概念非常相似。UITableView类由另一个名为UITableViewSource的类控制。它有你需要覆盖的方法来设置应该有多少行以及这些行应该如何显示在屏幕上。
提示
注意,UITableViewSource是UITableViewDelegate和UITableViewDataSource的组合。我更喜欢使用UITableViewSource以保持简单,因为很多时候需要使用这两个其他类中的任何一个。
在我们开始编码之前,让我们回顾一下在UITableViewSource中最常用的方法,如下所示:
-
RowsInSection:此方法允许你定义一个部分中的行数。所有的表格视图都有部分和行数。默认情况下,只有一个部分;然而,返回部分中的行数是一个要求。 -
NumberOfSections:这是表格视图中部分的数目。 -
GetCell:此方法必须为每一行返回一个单元格,并且应该实现。开发者负责设置单元格的外观;你还可以实现代码,在滚动时回收单元格。回收单元格将在滚动时提供更好的性能。 -
TitleForHeader:如果重写此方法,返回标题字符串的最简单方式。表格视图中的每个部分都有一个标准的头部视图,默认情况下。 -
RowSelected:当用户选择一行时,将调用此方法。
你可以重写额外的方法,但这些都足以应对大多数情况。如果你需要开发具有自定义样式的表格视图,你也可以设置自定义头部和尾部。
现在,让我们打开 ConversationsController.cs 文件,并在 ConversationsController 中创建一个嵌套类,如下所示:
class TableSource : UITableViewSource
{
const string CellName = "ConversationCell";
readonly MessageViewModel messageViewModel = ServiceContainer.Resolve<MessageViewModel>();
public override int RowsInSection(UITableView tableView, int section)
{
return messageViewModel.Conversations == null ?0 : messageViewModel.Conversations.Length;
}
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
var conversation = messageViewModel.Conversations[indexPath.Row];
var cell = tableView.DequeueReusableCell(CellName);
if (cell == null)
{
cell = new UITableViewCell(UITableViewCellStyle.Default, CellName);
cell.Accessory = UITableViewCellAccessory.DisclosureIndicator;
}
cell.TextLabel.Text = conversation.Username;
return cell;
}
}
我们实现了设置表格视图所需的两个方法:RowsInSection 和 GetCell。我们返回了在视图模型中找到的对话数量,并为每一行设置了我们的单元格。我们还使用了 UITableViewCellAccessory.DisclosureIndicator 来为用户提供一个指示器,让他们知道可以点击该行。
注意我们实现的重用单元格。使用带有单元格标识符的 DequeueReusableCell 调用将第一次返回一个 null 单元格。如果为 null,你应该使用相同的单元格标识符创建一个新的单元格。随后的 DequeueReusableCell 调用将返回一个现有的单元格,使你能够重用它。你还可以在故事板文件中定义 TableView 单元格,这对于自定义单元格非常有用。我们这里的单元格非常简单,因此从代码中定义它更容易。在移动平台上重用单元格对于节省内存和为用户提供非常流畅的滚动表格非常重要。
接下来,我们需要在 TableView 上设置 TableView 的源。按照以下步骤修改我们的 ConversationsController 类:
readonly MessageViewModel messageViewModel = ServiceContainer.Resolve<MessageViewModel>();
public override void ViewDidLoad()
{
base.ViewDidLoad();
TableView.Source = new TableSource();
}
public async override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
try
{
await messageViewModel.GetConversations();
TableView.ReloadData();
}
catch(Exception exc)
{
new UIAlertView("Oops!", exc.Message, null, "Ok").Show();
}
}
因此,当视图出现时,我们将加载我们的对话列表。完成此任务后,我们将重新加载表格视图,以便它显示我们的对话列表。如果你运行应用程序,你将在登录后看到一些对话出现在表格视图中,如下面的截图所示。将来,当我们从真实的网络服务加载对话时,一切都将以相同的方式运行。

添加朋友列表屏幕
下一个相当重要的屏幕是我们朋友的列表。当创建新的对话时,应用将加载一个朋友列表以开始对话。我们将遵循一个非常相似的模式来加载我们的对话列表。
首先,我们将创建 UIBarButtonItem,通过执行以下步骤导航到一个名为 FriendsController 的新控制器:
-
双击
MainStoryboard.storyboard文件以在 iOS 设计器中打开它。 -
在故事板中添加一个新的 表格视图控制器。
-
选择你的视图控制器,点击 属性 面板,确保你已经选择了 小部件 选项卡。
-
在 类 字段中输入
FriendsController。 -
滚动到 视图控制器 部分,在 标题 字段中输入
Friends。 -
从 工具箱 面板拖动一个 导航项 到
ConversationsController。 -
创建一个新的栏按钮项元素,并将其放置在新导航栏的右上角。
-
在按钮栏的属性面板中,将其标识符设置为添加。这将使用内置的加号按钮,这在 iOS 应用程序中普遍使用。
-
通过按住Ctrl并从栏按钮拖动蓝色线条到下一个控制器,创建从栏按钮项到
FriendsController的转换。 -
从出现的弹出窗口中选择推送转换。
-
保存故事板文件。
你对故事板的更改应类似于以下截图所示:

你将看到一个 Xamarin Studio 为你生成的新的FriendsController类。如果你编译并运行应用程序,你会看到我们创建的新栏按钮项。点击它将带你到新的控制器。
现在,让我们实现UITableViewSource来显示我们的朋友列表。从在FriendsController内部创建一个新的嵌套类开始,如下所示:
class TableSource : UITableViewSource
{
const string CellName = "FriendCell";
readonly FriendViewModel friendViewModel = ServiceContainer.Resolve<FriendViewModel>();
public override int RowsInSection(UITableView tableView, int section)
{
return friendViewModel.Friends == null ?0 : friendViewModel.Friends.Length;
}
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
var friend = friendViewModel.Friends[indexPath.Row];
var cell = tableView.DequeueReusableCell(CellName);
if (cell == null)
{
cell = new UITableViewCell(UITableViewCellStyle.Default, CellName);
cell.AccessoryView = UIButton.FromType(UIButtonType.ContactAdd);
cell.AccessoryView.UserInteractionEnabled = false;
}
cell.TextLabel.Text = friend.Username;
return cell;
}
}
就像之前一样,我们实现了表格单元格回收,并且只为每个朋友设置了标签上的文本。我们使用cell.AccessoryView来向用户指示每个单元格是可点击的,并开始一个新的对话。我们禁用了按钮的用户交互,以便在用户点击按钮时可以选择行。否则,我们就必须为按钮实现一个点击事件。
接下来,我们需要像修改对话一样修改FriendsController,如下所示:
readonly FriendViewModel friendViewModel = ServiceContainer.Resolve<FriendViewModel>();
public override void ViewDidLoad()
{
base.ViewDidLoad();
TableView.Source = new TableSource();
}
public async override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
try
{
await friendViewModel.GetFriends();
TableView.ReloadData();
}
catch(Exception exc)
{
new UIAlertView("Oops!", exc.Message, null, "Ok").Show();
}
}
这将完全像对话列表一样工作。控制器将异步加载朋友列表并刷新表格视图。如果你编译并运行应用程序,你将能够导航到屏幕并查看我们在第四章中创建的示例朋友列表,如以下截图所示:

添加消息列表
现在,让我们实现查看对话或消息列表的屏幕。我们将尝试模仿 iOS 内置的短信应用程序。为此,我们还将介绍如何创建自定义表格视图单元格的基础知识。
首先,我们需要一个新的MessagesController类来执行以下步骤:
-
双击
MainStoryboard.storyboard文件以在 iOS 设计器中打开它。 -
在故事板中添加一个新的表格视图控制器。
-
选择你的视图控制器,点击属性面板,并确保你已选择小部件选项卡。
-
在类字段中输入
MessagesController。 -
滚动到视图控制器部分,在标题字段中输入
Messages。 -
通过按住Ctrl并将蓝色线条从一个控制器拖动到另一个控制器,从
ConversationsController创建到MessagesController的转换。 -
从出现的弹出窗口中选择推送转换。在属性面板中输入标识符
OnConversation。 -
现在,在
MessagesController中的表格视图中创建两个表格视图单元格。你可以通过默认创建的现有一个来重用。 -
分别将
MyMessageCell和TheirMessageCell输入到每个单元格的类字段中。 -
分别在每个单元格上将标识符设置为
MyCell和TheirCell。 -
保存故事板文件。
Xamarin Studio 将生成三个文件:MessagesController.cs、MyMessageCell.cs和TheirMessageCell.cs。你可以通过创建一个Views文件夹并将单元格移动到其中来保持事物有序。同样,你也可以将控制器移动到Controllers文件夹。
现在让我们为这两个单元格实现一个基类:
public class BaseMessageCell : UITableViewCell
{
public BaseMessageCell(IntPtr handle) : base(handle)
{
}
public virtual void Update(Message message)
{
}
}
我们将在稍后重写Update方法并为每种单元格类型采取适当的行动。我们需要这个类来使与UITableViewSource中的两种单元格类型交互更容易。
现在打开MessagesController.cs并在嵌套类中实现UITableViewSource,如下所示:
class TableSource : UITableViewSource
{
const string MyCellName = "MyCell";
const string TheirCellName = "TheirCell";
readonly MessageViewModel messageViewModel = ServiceContainer.Resolve<MessageViewModel>();
readonly ISettings settings = ServiceContainer.Resolve<ISettings>();
public override int RowsInSection(UITableView tableview, int section)
{
return messageViewModel.Messages == null ?0 : messageViewModel.Messages.Length;
}
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
var message = messageViewModel.Messages [indexPath.Row];
bool isMyMessage = message.UserId == settings.User.Id;
var cell = tableView.DequeueReusableCell(isMyMessage ?MyCellName : TheirCellName) as BaseMessageCell;
cell.Update(message);
return cell;
}
}
我们添加了一些逻辑来检查消息是否来自当前用户,以决定适当的表格视图标识符。由于我们为两个单元格都有一个基类,我们可以将其转换为BaseMessageCell并使用其Update方法。
现在让我们修改MessagesController文件以加载我们的消息列表并显示它们:
readonly MessageViewModel messageViewModel = ServiceContainer.Resolve<MessageViewModel>();
public override void ViewDidLoad()
{
base.ViewDidLoad();
TableView.Source = new TableSource();
}
public async override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
Title = messageViewModel.Conversation.Username;
try
{
await messageViewModel.GetMessages();
TableView.ReloadData();
}
catch (Exception exc)
{
new UIAlertView("Oops!", exc.Message, null, "Ok").Show();
}
}
这里唯一的新内容是我们将Title属性设置为对话的用户名。
为了完成我们的自定义单元格,我们需要在 Xcode 中执行以下步骤进行更多更改:
-
双击
MainStoryboard.storyboard文件以在 iOS 设计器中打开它。 -
将一个新的标签拖放到自定义单元格上。
-
使用一些创意来设计这两个标签。我选择将
MyMessageCell中的文本设置为蓝色,将TheirMessageCell设置为绿色。我在TheirMessageCell中将标签的对齐方式设置为右对齐。 -
对于每个单元格的名称,输入
message。 -
保存故事板文件并返回。
现在将以下Update方法添加到MyMessageCell.cs和TheirMessageCell.cs中:
public partial class MyMessageCell : BaseMessageCell
{
public MyMessageCell (IntPtr handle) : base (handle)
{
}
public override void Update(Message message)
{
this.message.Text = message.Text;
}
}
对于每个单元格重复代码有点奇怪,但这是利用 Xamarin Studio 基于故事板文件生成的输出端口的最简单方法。你也可以选择为两个单元格使用相同的类(即使在 Xcode 中有不同的布局);然而,这样你将失去在每个单元格中拥有不同代码的能力。
如果你现在运行应用程序,你将能够查看消息列表,如下面的截图所示:

消息的编写
对于我们应用程序的最后一部分,我们需要实现一些苹果在他们的 API 中没有提供的自定义功能。我们需要添加一个带有按钮的文本字段,该按钮看起来像是附着在表格视图的底部。这大部分需要编写代码和连接大量事件。
让我们从向我们的MessagesController类添加一些新的成员变量开始,如下所示:
UIToolbar toolbar;
UITextField message;
UIBarButtonItem send;
NSObject willShowObserver, willHideObserver;
我们将文本字段和工具栏按钮放置在工具栏内,如下面的代码所示。NSObject字段将是一个 iOS 事件系统通知的示例。我们很快就会看到这些是如何使用的:
public override void ViewDidLoad()
{
base.ViewDidLoad();
//Text Field
message = new UITextField(new RectangleF(0, 0, 240, 32))
{
BorderStyle = UITextBorderStyle.RoundedRect,ReturnKeyType = UIReturnKeyType.Send,ShouldReturn = _ =>
{
Send();
return false;
},
};
//Bar button item
send = new UIBarButtonItem("Send", UIBarButtonItemStyle.Plain,(sender, e) => Send());
//Toolbar
toolbar = new UIToolbar(new RectangleF(0, TableView.Frame.Height - 44,TableView.Frame.Width, 44));
toolbar.Items = new UIBarButtonItem[]
{
new UIBarButtonItem(message),
send
};
NavigationController.View.AddSubview(toolbar);
TableView.Source = new TableSource();
TableView.TableFooterView = new UIView(new RectangleF(0, 0, TableView.Frame.Width, 44))
{
BackgroundColor = UIColor.Clear,
};
}
这项工作的大部分涉及设置基本的 UI。在这种情况下,我们无法在 Xcode 中完成这项工作,因为它是一个自定义 UI。我们使用 C#创建一个文本字段、工具栏按钮和工具栏,并将它们添加到我们的导航控制器视图中。这将显示在表格视图顶部的工具栏,无论它滚动到何处。我们使用的另一个技巧是将一个页脚视图添加到表格视图中,其高度与工具栏相同。这将简化我们稍后设置的某些动画。
现在,我们需要按照以下方式修改ViewWillAppear:
public async override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
Title = messageViewModel.Conversation.Username;
//Keyboard notifications
willShowObserver = UIKeyboard.Notifications.ObserveWillShow((sender, e) => OnKeyboardNotification(e));
willHideObserver = UIKeyboard.Notifications.ObserveWillHide((sender, e) => OnKeyboardNotification(e));
//IsBusy
messageViewModel.IsBusyChanged += OnIsBusyChanged;
try
{
await messageViewModel.GetMessages();
TableView.ReloadData();
message.BecomeFirstResponder();
}
catch (Exception exc)
{
new UIAlertView("Oops!", exc.Message, null, "Ok").Show();
}
}
这些更改中的大多数都很直接,但请注意我们使用 iOS 通知的方式。Xamarin 提供了一个 C#友好的方式来订阅通知。在提供通知的各种UIKit类中有一个名为Notifications的静态嵌套类。否则,您将不得不使用NSNotificationCenter类,这并不那么容易使用。要取消订阅这些事件,我们只需销毁返回的NSObject。
因此,让我们为ViewWillDisapper添加一个覆盖,以清理这些事件,如下所示:
public override void ViewWillDisappear(bool animated)
{
base.ViewWillDisappear(animated);
//Unsubcribe notifications
if (willShowObserver != null)
{
willShowObserver.Dispose();
willShowObserver = null;
}
if (willHideObserver != null)
{
willHideObserver.Dispose();
willHideObserver = null;
}
//IsBusy
messageViewModel.IsBusyChanged -= OnIsBusyChanged;
}
接下来,让我们设置这些事件的函数,如下所示:
void OnIsBusyChanged (object sender, EventArgs e)
{
message.Enabled = send.Enabled = !messageViewModel.IsBusy;
}
void ScrollToEnd()
{
TableView.ContentOffset = new PointF(0, TableView.ContentSize.Height -TableView.Frame.Height);
}
void OnKeyboardNotification (UIKeyboardEventArgs e)
{
//Check if the keyboard is becoming visible
bool willShow = e.Notification.Name == UIKeyboard.WillShowNotification;
//Start an animation, using values from the keyboard
UIView.BeginAnimations("AnimateForKeyboard");
UIView.SetAnimationDuration(e.AnimationDuration);
UIView.SetAnimationCurve(e.AnimationCurve);
//Calculate keyboard height, etc.
if (willShow)
{
var keyboardFrame = e.FrameEnd;
var frame = TableView.Frame;
frame.Height -= keyboardFrame.Height;
TableView.Frame = frame;
frame = toolbar.Frame;
frame.Y -= keyboardFrame.Height;
toolbar.Frame = frame;
}
else
{
var keyboardFrame = e.FrameBegin;
var frame = TableView.Frame;
frame.Height += keyboardFrame.Height;
TableView.Frame = frame;
frame = toolbar.Frame;
frame.Y += keyboardFrame.Height;
toolbar.Frame = frame;
}
//Commit the animation
UIView.CommitAnimations();
ScrollToEnd();
}
这段代码相当多,但并不太难。OnIsBusyChanged用于在加载时禁用一些视图。ScrollToEnd是一个快速方法,用于将表格视图滚动到末尾。我们需要这样做是为了提高可用性。需要一些数学知识,因为苹果没有提供内置的方法来做这个。
另一方面,OnKeyboardNotification有很多事情要做。我们使用了 iOS 内置的动画系统来设置当键盘出现或隐藏时的动画。我们使用这个来移动视图以适应屏幕键盘。使用动画系统相当简单;调用UIView.BeginAnimations,修改一些视图,然后使用UIView.CommitAnimations完成。我们还使用了键盘的一些其他值来使我们的动画与键盘的动画同步。
最后但同样重要的是,我们需要实现一个函数来发送一条新消息,如下所示:
async void Send()
{
//Just hide the keyboard if they didn't type anything
if (string.IsNullOrEmpty(message.Text))
{
message.ResignFirstResponder();
return;
}
//Set the text, send the message
messageViewModel.Text = message.Text;
await messageViewModel.SendMessage();
//Clear the text field & view model
message.Text = messageViewModel.Text = string.Empty;
//Reload the table
TableView.ReloadData();
//Hide the keyboard
message.ResignFirstResponder();
//Scroll to end, to see the new message
ScrollToEnd();
}
这段代码也很直接。在发送消息后,我们只需重新加载表格,隐藏键盘,然后确保我们滚动到底部以查看新消息,如下面的截图所示。使用async关键字使这变得容易。

摘要
在本章中,我们介绍了 Apple 和 Xamarin 为开发 iOS 应用程序提供的基本设置。这包括 Info.plist 文件和 Xamarin Studio 中的项目选项。我们介绍了 UINavigationController,这是 iOS 应用程序导航的基本构建块,并实现了一个包含用户名和密码字段的登录屏幕。接下来,我们介绍了 iOS 的 segues 和 UITableView 类。我们使用 UITableView 实现了好友列表屏幕,以及消息列表屏幕,同样也是使用 UITableView。最后,我们添加了自定义 UI 功能;一个在消息列表底部浮动的自定义工具栏。
完成本章内容后,你将拥有一个部分功能的 iOS 版本 XamChat。你将对 iOS 平台和工具有更深入的了解,并且具备相当好的知识来应用于构建你自己的 iOS 应用程序。请自行实现本章未涵盖的剩余屏幕。如果你感到困惑,可以随意查阅本书附带的全样本应用程序。在下一章中,我们将使用原生 Android API 开发 XamChat 的 Android UI。我们的大部分步骤将与在 iOS 上所做的工作非常相似,我们将主要与 MVVM 设计模式中的 View 层进行工作。
第六章 XamChat for Android
在本章中,我们将开始开发我们的 XamChat 示例应用程序的 Android UI。我们将直接使用原生 Android API 来创建我们的应用程序,并调用我们的共享便携式类库,类似于我们在 iOS 上所做的那样。同样,我们的 Xamarin.Android 应用程序将无法与用 Java 编写的 Android 应用程序区分开来。
要开始编写 XamChat 的 Android 版本,请打开前几章提供的解决方案,并创建一个新的Android 应用程序项目。将项目命名为XamChat.Droid或您选择的任何其他适当名称。
在本章中,我们将介绍:
-
AndroidManifest
-
为 XamChat 编写登录界面
-
Android 的 ListView 和 BaseAdapter
-
添加好友列表
-
添加消息列表
介绍 AndroidManifest
所有 Android 应用程序都有一个名为 AndroidManifest 的 XML 文件,它声明了关于应用程序的基本信息,例如应用程序版本和名称,并命名为AndroidManifest.xml。这与 iOS 上的Info.plist文件非常相似,但 Android 对其重要性给予了更多的重视。默认项目没有清单,因此让我们通过导航到项目选项 | Android 应用程序并点击添加 Android 清单来创建一个。将为你的应用程序出现几个新的设置。
设置清单
以下截图显示的最重要设置如下:
-
应用程序名称: 这是应用程序的标题,它显示在图标下方。它不同于在 Google Play 上选择的名称。
-
包名: 这与 iOS 上的类似;它是你的应用捆绑标识符或捆绑 ID。这是一个用于标识应用的唯一名称。惯例是在公司名称开头使用反向域名风格;例如,
com.packt.xamchat。它必须以小写字母开头,并且至少包含一个"."字符。 -
应用程序图标: 这是显示在 Android 主屏幕上的应用程序图标。
-
版本号: 这是一个代表应用程序版本的单一数字。提高这个数字表示在 Google Play 上有新版本。
-
版本名称: 这是用户友好的版本字符串,用户将在设置和 Google Play 上看到它;例如,1.0.0。
-
最小 Android 版本: 这是你的应用程序支持的最小 Android 版本。在现代 Android 应用程序中,你通常可以针对 Android 4.0,但这取决于你的应用程序的核心受众。
-
目标 Android 版本: 这是你的应用程序编译所针对的 Android SDK 版本。使用更高的数字可以让你访问新的 API,但是,你可能需要在较新的设备上调用这些 API 时进行一些检查。
-
安装位置: 这定义了你的 Android 应用程序可以安装的不同位置:自动(用户设置)、外部(SD 卡)或内部(设备内部存储)。

常见清单权限
除了这些设置外,还有一个标记为必需权限的复选框集合。在应用程序安装之前,这些权限会在 Google Play 上显示给用户。这是 Android 强制执行一定安全级别的方式,使用户能够看到应用程序将如何访问他们的设备以进行更改。
以下是一些常用的清单权限:
-
相机: 这提供了访问设备相机的权限
-
互联网: 这提供了通过互联网发起网络请求的访问权限
-
读取联系人: 这提供了读取设备联系人库的访问权限
-
读取外部存储: 这提供了读取 SD 卡的访问权限
-
写入联系人: 这提供了修改设备联系人库的访问权限
-
写入外部存储: 这提供了写入 SD 卡的访问权限
除了这些设置外,还需要多次手动更改 Android 清单。在这种情况下,你可以像在 Xamarin Studio 中编辑标准 XML 文件一样编辑清单文件。有关有效 XML 元素和属性的完整列表,请访问developer.android.com/guide/topics/manifest/manifest-intro.html。
现在让我们填写以下应用程序的设置:
-
应用程序名称:
XamChat -
包名:
com.yourcompanyname.xamchat;确保将未来的应用程序命名为以com.yourcompanyname开头 -
版本号: 只需从数字
1开始 -
版本: 这可以是任何字符串,但建议使用类似于版本号的字符串
-
最小 Android 版本: 选择Android 4.0.3 (API Level 15)
-
必需权限: 选择互联网;我们稍后会使用它
在这一点上,我们需要从我们在第四章中创建的可移植类库中引用我们的共享代码,即XamChat – 一个跨平台应用程序。右键单击项目的引用文件夹,然后单击编辑引用...,并添加对XamChat.Core项目的引用。现在你将能够访问在第四章中编写的所有共享代码,即XamChat – 一个跨平台应用程序。
前往Resources目录,然后在values文件夹中打开Strings.xml;这是你 Android 应用程序中所有文本应该存储的地方。这是一个 Android 约定,这将使添加多种语言到你的应用程序变得非常容易。让我们将我们的字符串更改为以下内容:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="ApplicationName">XamChat</string>
<string name="ErrorTitle">Oops!</string>
<string name="Loading">Loading</string>
</resources>
我们将在本章后面使用这些值。如果你在向用户显示文本时需要添加新的值,请随意添加。如果你需要添加更多语言,这非常简单;你可以查看 Android 关于此主题的文档 developer.android.com/guide/topics/resources/localization.html。
创建和实现应用程序类
现在我们来实现我们的主要应用程序类;从 新建文件 对话框中添加一个新的 Activity。在这个文件中,我们不会从 Activity 继承,但这个模板在文件顶部添加了几个 Android using 语句,用于导入代码中要使用的 Android API。创建一个新的 Application 类,我们可以将 ServiceContainer 中的所有内容注册如下:
[Application(Theme = "@android:style/Theme.Holo.Light")]
public class Application : Android.App.Application
{
public Application(IntPtr javaReference, JniHandleOwnership transfer): base(javaReference, transfer)
{
}
public override void OnCreate()
{
base.OnCreate();
//ViewModels
ServiceContainer.Register<LoginViewModel>(() => new LoginViewModel());
ServiceContainer.Register<FriendViewModel>(() => new FriendViewModel());
ServiceContainer.Register<MessageViewModel>(() => new MessageViewModel());
ServiceContainer.Register<RegisterViewModel>(() => new RegisterViewModel());
//Models
ServiceContainer.Register<ISettings>(() => new FakeSettings());
ServiceContainer.Register<IWebService>(() => new FakeWebService());
}
}
我们使用了内置的 Android 主题 Theme.Holo.Light,仅仅因为它是一个整洁的主题,与我们在 iOS 中使用的默认样式相匹配。注意,我们必须为这个类创建一个奇怪的、空的构造函数才能使其正常工作。这是 Xamarin 中自定义 Application 类的当前要求。你可以将其视为样板代码,并且在这种情况下你需要添加它。
现在我们为应用中的所有活动实现一个简单的基类。在 XamChat.Droid 项目中创建一个名为 Activities 的文件夹,并创建一个名为 BaseActivity.cs 的新文件,内容如下:
[Activity]
public class BaseActivity<TViewModel> : Activitywhere TViewModel : BaseViewModel
{
protected readonly TViewModel viewModel;
protected ProgressDialog progress;
public BaseActivity()
{
viewModel = ServiceContainer.Resolve(typeof(TViewModel)) asTViewModel;
}
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
progress = new ProgressDialog(this);
progress.SetCancelable(false);progress.SetTitle(Resource.String.Loading);}
protected override void OnResume()
{
base.OnResume();
viewModel.IsBusyChanged += OnIsBusyChanged;
}
protected override void OnPause()
{
base.OnPause();
viewModel.IsBusyChanged -= OnIsBusyChanged;
}
void OnIsBusyChanged (object sender, EventArgs e)
{
if (viewModel.IsBusy)
progress.Show();
else
progress.Hide();
}
}
我们在这里做了几件事情来简化其他活动的开发。首先,我们使这个类成为泛型,并创建了一个名为 viewModel 的受保护变量来存储特定类型的 ViewModel。请注意,由于平台限制,我们在 iOS 中没有在控制器上使用泛型(有关更多信息,请参阅 Xamarin 文档网站 docs.xamarin.com/guides/ios/advanced_topics/limitations/)。我们还实现了 IsBusyChanged,并使用来自 Strings.xml 文件的 Loading 字符串显示了一个简单的 ProgressDialog 函数,以指示网络活动。
让我们再添加一个方法来向用户显示错误,如下所示:
protected void DisplayError(Exception exc)
{
string error = exc.Message;
new AlertDialog.Builder(this)
.SetTitle(Resource.String.ErrorTitle)
.SetMessage(error)
.SetPositiveButton(Android.Resource.String.Ok,(IDialogInterfaceOnClickListener)null)
.Show();
}
此方法将显示一个弹出对话框,指示出了问题。请注意,我们还使用了 ErrorTitle 和内置的 Android Ok 字符串资源。
这将完成我们 Android 应用的核心设置。从这里,我们可以继续实现应用中各个屏幕的 UI。
添加登录屏幕
在创建 Android 视图之前,了解 Android 中可用的不同布局或视图组类型非常重要。iOS 中没有一些这些的等效功能,因为 iOS 设备的屏幕尺寸变化非常小。由于 Android 几乎有无限的屏幕尺寸和密度,Android SDK 为视图的自动调整大小和布局提供了大量的内置支持。
Android 中的布局和 ViewGroups
以下是一些常见的布局类型:
-
ViewGroup:这是包含子视图集合的视图的基类。你通常不会直接使用这个类。 -
LinearLayout:这是一个将子视图按行或列(但不能同时按行和列)定位的布局。你还可以为每个子视图设置权重,使它们占据不同百分比的可用空间。 -
RelativeLayout:这是一个提供更多对其子视图位置灵活性的布局。你可以将子视图相对于彼此定位,使它们位于彼此之上、之下、左侧或右侧。 -
FrameLayout:这个布局将子视图直接放置在屏幕上的 z 轴 顺序上。这个布局最适合有大型子视图需要其他视图在其上方,并且可能停靠在一边的情况。 -
ListView:这个视图通过一个确定子视图数量的适配器类,以列表形式垂直显示视图。它还支持其子视图被选中。 -
GridView:这个视图在网格中按行和列显示视图。它还需要使用一个适配器类来提供子视图的数量。
在我们开始编写登录界面之前,删除由 Android 项目模板创建的 Main.axml 和 MainActivity.cs 文件,因为它们对这个应用程序没有用。接下来,在项目 Resources 目录下的 layout 文件夹中创建一个名为 Login.axml 的 Android 布局文件。
现在,我们可以按照以下方式开始向我们的 Android 布局添加功能:
-
双击
Login.axml文件以打开 Android 设计器。 -
将两个 Plain Text 视图拖到 Text Fields 部分的布局中。
-
在 Id 字段中,分别输入
@+id/username和@+id/password。这是你将采取的步骤,以便从 C# 代码中与任何控件一起工作。 -
对于密码字段,将其 Input Type 属性设置为
textPassword。 -
将一个 Button 拖到布局中,并设置其 Text 属性为
Login。 -
将按钮的 Id 属性设置为
@+id/login。我们将从代码中使用这个控件。
完成布局后,你的布局看起来会像以下截图所示:

实现登录功能
现在在之前创建的 Activites 文件夹中创建一个新的 Android Activity 文件,命名为 LoginActivity.cs。我们将使用这个文件作为应用程序运行时启动的主要活动。让我们按照以下方式实现登录功能:
[Activity(Label = "@string/ApplicationName", MainLauncher = true)]
public class LoginActivity : BaseActivity<LoginViewModel>
{
EditText username, password;
Button login;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.Login);
username = FindViewById<EditText>(Resource.Id.username);
password = FindViewById<EditText>(Resource.Id.password);
login = FindViewById<Button>(Resource.Id.login);
login.Click += OnLogin;
}
protected override void OnResume()
{
base.OnResume();
username.Text = password.Text = string.Empty;
}
async void OnLogin (object sender, EventArgs e)
{
viewModel.Username = username.Text;
viewModel.Password = password.Text;
try
{
await viewModel.Login();
//TODO: navigate to a new activity
}
catch (Exception exc)
{
DisplayError(exc);
}
}
}
注意,我们将 MainLauncher 设置为 true,使这个活动成为应用程序的第一个活动。在某些应用程序中,一个启动画面被用作第一个活动,所以如果你需要添加启动画面,请记住这一点。我们还利用了本章中早些时候设置的 ApplicationName 值和 BaseActivity 类。我们还重写了 OnResume 方法,以便清除两个 EditText 控件,以便在返回到屏幕时清除值。
现在如果您启动应用程序,您将看到我们刚刚实现的登录屏幕,如下面的截图所示:

使用 ListView 和 BaseAdapter
现在,让我们在 Android 上实现一个对话列表。Android 中UITableView和UITableViewSource iOS 类的对应物是ListView和BaseAdapter。这些 Android 类有并行概念,例如实现抽象方法和在滚动时回收单元格。Android 中使用了几种不同的适配器,例如ArrayAdapter或CursorAdaptor,尽管BaseAdapter通常最适合简单的列表。
实现对话屏幕
让我们实现我们的对话屏幕。让我们首先在您的Activities文件夹中创建一个新的 Android Activity,命名为ConversationsActivity.cs。让我们从对类定义的以下几个更改开始:
[Activity(Label = "Conversations")]
public class ConversationsActivity :BaseActivity<MessageViewModel>
{
//Other code here later
}
执行以下步骤以实现几个 Android 布局:
-
在
Resources目录的layout文件夹中创建一个新的 Android 布局,命名为Conversations.axml。 -
从工具箱中将ListView控件拖放到布局中,并设置其ID为
@+id/conversationsList。 -
在
Resources目录的layout文件夹中创建第二个 Android 布局,命名为ConversationListItem.axml。 -
从工具箱面板中将文本(中号)和文本(小号)控件拖放到布局中。
-
将它们的 ID 设置为
@+id/conversationUsername和@+id/conversationLastMessage。 -
最后,让我们将它们的边距都设置为
3dp,在属性框的布局选项卡中。
这样就会设置好我们将在对话屏幕中使用的所有布局文件。您的ConversationListItem.axml布局将类似于以下截图所示:

现在,我们可以在ConversationsActivity内部实现BaseAdapter作为嵌套类,如下所示:
class Adapter : BaseAdapter<Conversation>
{
readonly MessageViewModel messageViewModel = ServiceContainer.Resolve<MessageViewModel>();
readonly LayoutInflater inflater;
public Adapter(Context context)
{
inflater = (LayoutInflater)context.GetSystemService (Context.LayoutInflaterService);
}
public override long GetItemId(int position)
{
//This is an abstract method, just a simple implementation
return position;
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
if (convertView == null)
{
convertView = inflater.Inflate (Resource.Layout.ConversationListItem, null);
}
var conversation = this [position];
var username = convertView.FindViewById<TextView>(Resource.Id.conversationUsername);
var lastMessage = convertView.FindViewById<TextView>(Resource.Id.conversationLastMessage);
username.Text = conversation.Username;
lastMessage.Text = conversation.LastMessage;
return convertView;
}
public override int Count
{
get { return messageViewModel.Conversations == null ? 0: messageViewModel.Conversations.Length; }
}
public override Conversation this[int index]
{
get { return messageViewModel.Conversations [index]; }
}
}
以下是对适配器内部发生的事情的回顾:
-
我们继承了
BaseAdapter<Conversation>。 -
我们传递了
Context(我们的活动)以便我们可以提取LayoutInflater。这个类使您能够加载 XML 布局资源并将它们填充到视图对象中。 -
我们实现了
GetItemId。这是一个用于识别行的一般方法,但我们现在只是返回位置。 -
我们设置了
GetView,通过仅在新视图为 null 时创建新视图来回收convertView变量。我们还从布局中提取了文本视图来设置它们的文本。 -
我们重写了
Count以返回对话的数量。 -
我们实现了一个索引器,用于返回指定位置的
Conversation对象。
总体来说,这应该与我们之前在 iOS 上所做的相当相似。
设置适配器
现在,让我们通过在ConversationsActivity的主体中添加以下内容来在我们的活动中设置适配器:
ListView listView;
Adapter adapter;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.Conversations);
listView = FindViewById<ListView>(Resource.Id.conversationsList);
listView.Adapter = adapter = new Adapter(this);
}
protected async override void OnResume()
{
base.OnResume();
try
{
await viewModel.GetConversations();
adapter.NotifyDataSetInvalidated();
}
catch (Exception exc)
{
DisplayError(exc);
}
}
此代码将在活动出现在屏幕上时设置适配器并重新加载我们的会话列表。请注意,我们在这里调用了NotifyDataSetInvalidated,这样ListView在会话数量更新后可以重新加载其行。这与我们在 iOS 上通过调用UITableView的ReloadData方法所做的是平行的。
最后但同样重要的是,我们需要修改之前在LoginActivity中设置的OnLogin方法,以便启动我们的新活动,如下所示:
StartActivity(typeof(ConversationsActivity));
现在,如果我们编译并运行我们的应用程序,登录后我们可以导航到会话列表,如下面的截图所示:

实现朋友列表
在我们开始实现朋友列表屏幕之前,我们必须首先在我们的应用程序的ActionBar中添加一个菜单项。让我们首先在我们的项目的Resources文件夹中创建一个新的menu文件夹。接下来,创建一个名为ConversationsMenu.axml的新 Android 布局文件。删除由 XML 创建的默认布局,并用以下内容替换它:
<?xml version="1.0" encoding="utf-8"?>
<menu >
<item android:id="@+id/addFriendMenu"android:icon="@android:drawable/ic_menu_add"android:showAsAction="ifRoom"/>
</menu>
我们设置了一个包含一个菜单项的根菜单。
下面的代码是我们在 XML 中为该项设置的详细说明:
-
android:id:我们稍后将在 C#中使用它来引用菜单项Resource.Id.addFriendMenu。 -
android:icon:这是一个用于显示菜单项的图像资源。我们使用了一个内置的 Android 通用加号图标。 -
android:showAsAction:如果空间允许,这将使菜单项可见。如果由于某种原因设备屏幕太窄,将显示一个溢出菜单来显示菜单项。
现在,我们可以在ConversationsActivity.cs中做一些更改,以显示菜单项,如下所示:
public override bool OnCreateOptionsMenu(IMenu menu)
{
MenuInflater.Inflate(Resource.Menu.ConversationsMenu, menu);
return base.OnCreateOptionsMenu(menu);
}
此代码将把我们的布局应用到活动动作栏顶部的菜单中。接下来,我们可以添加一些代码,以便在菜单项被选中时运行:
public override bool OnOptionsItemSelected(IMenuItem item)
{
if (item.ItemId == Resource.Id.addFriendMenu)
{
//TODO: launch the next activity
}
return base.OnOptionsItemSelected(item);
}
现在,让我们实现下一个活动。让我们首先复制Resources目录中layout文件夹中的Conversations.axml,并将其重命名为Friends.axml。我们在这个文件中要做的唯一更改是将 ListView 的 ID 重命名为@+id/friendsList。
接下来,执行以下步骤以创建一个可用于ListView中列表项的布局:
-
创建一个新的 Android 布局
FriendListItem.axml。 -
打开布局并切换到屏幕底部的源标签。
-
将根
LinearLayoutXML 元素更改为RelativeLayout元素。 -
切换回屏幕底部的内容标签。
-
从工具箱面板拖动一个文本(大号)控件到布局中,并将其Id设置为
@+id/friendName。 -
从工具箱面板拖动一个ImageView控件到布局中;你可以让它保留其默认值或留空。
-
将图像视图的图像更改为
@android:drawable/ic_menu_add。这是我们之前在章节中使用的相同加号图标。您可以从 Framework Resources 选项卡下的 Resources 对话框中选择它。 -
将两个控件的高度和宽度都设置为
wrap_content。这可以在 ViewGroup 部分的 Layout 选项卡下找到。 -
接下来,检查图像视图上的 Align Parent Right 的值。
-
最后,在 Properties 窗口的 Layout 选项卡中,将两个控件的外边距设置为
3dp。
使用 Xamarin 设计器可以非常高效,但一些开发者更喜欢更高层次的控制。你可能考虑自己编写 XML 代码作为替代方案,这相当直接,如下面的代码所示:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView android:text="Large Text"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/friendName"
android:layout_margin="3dp" />
<ImageView
android:src="img/ic_menu_add"
android:layout_alignParentRight="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="3dp" />
</RelativeLayout>
由于我们现在已经有了新屏幕所需的所有布局,让我们在 Activities 文件夹中创建一个名为 FriendsActivity.cs 的 Android Activity。让我们像之前一样创建活动的基本定义,如下所示:
[Activity(Label = "Friends")]
public class FriendsActivity : BaseActivity<FriendViewModel>
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
}
}
现在,让我们实现一个嵌套的 Adapter 类来设置列表视图项,如下所示:
class Adapter : BaseAdapter<User>
{
readonly FriendViewModel friendViewModel = ServiceContainer.Resolve<FriendViewModel>();
readonly LayoutInflater inflater;
public Adapter(Context context)
{
inflater = (LayoutInflater)context.GetSystemService (Context.LayoutInflaterService);
}
public override long GetItemId(int position)
{
return position;
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
if (convertView == null)
{
convertView = inflater.Inflate(Resource.Layout.FriendListItem, null);
}
var friend = this [position];
var friendname = convertView.FindViewById<TextView>(Resource.Id.friendName);
friendname.Text = friend.Username;
return convertView;
}
public override int Count
{
get { return friendViewModel.Friends == null ? 0: friendViewModel.Friends.Length; }
}
public override User this[int index]
{
get { return friendViewModel.Friends [index]; }
}
}
在这个适配器和之前我们为对话屏幕实现的适配器之间,实际上并没有太大的区别。我们只需要设置朋友的名字,并使用 User 对象而不是 Conversation 对象。
要完成设置适配器,我们可以更新 FriendsActivity 类的主体如下:
ListView listView;
Adapter adapter;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.Friends);
listView = FindViewById<ListView>(Resource.Id.friendsList);
listView.Adapter = adapter = new Adapter(this);
}
protected async override void OnResume()
{
base.OnResume();
try
{
await viewModel.GetFriends();
adapter.NotifyDataSetInvalidated();
}
catch (Exception exc)
{
DisplayError(exc);
}
}
最后但同样重要的是,我们可以在 ConversationsActivity 类中更新 OnOptionsItemSelected 如下:
public override bool OnOptionsItemSelected(IMenuItem item)
{
if (item.ItemId == Resource.Id.addFriendMenu)
{
StartActivity(typeof(FriendsActivity));
}
return base.OnOptionsItemSelected(item);
}
因此,如果我们编译并运行应用程序,我们可以导航到一个完全实现的联系人列表屏幕,如下面的截图所示:

消息编写
下一个屏幕稍微复杂一些。我们需要创建一个 ListView,它根据每行的类型使用多个布局文件。我们还需要进行一些布局技巧,以便在 ListView 下方放置一个视图并设置 ListView 以自动滚动。
对于下一个屏幕,让我们首先在 Resources 目录的 layout 文件夹中创建一个新的布局文件名为 Messages.axml,然后执行以下步骤:
-
将一个新的 ListView 拖动到布局中。将其 Id 设置为
@+id/messageList。 -
打开 Stack From Bottom 的复选框,并将 Transcript Mode 设置为
alwaysScroll。这将按从下到上的顺序显示项目。 -
在 LinearLayout 部分的 Layout 选项卡下,将 ListView 的 Weight 值设置为
1。 -
将一个新的 RelativeLayout 拖动到布局中。让其 Id 使用默认值,或者将其移除。
-
将一个新的 Button 拖动到 RelativeLayout 内部。将其 Id 设置为
@+id/sendButton。 -
在 Layout 选项卡中打开 Align Parent Right 的复选框。
-
将 RelativeLayout 中 Text Field 部分的 Plain Text 拖动到按钮的左侧。将其 Id 设置为
@+id/messageText。 -
在 Layout 选项卡中,将 To Left Of 设置为
@+id/sendButton,并将其 Width 设置为match_parent。 -
打开 Center in Parent 复选框以固定垂直居中。
完成后,XML 文件将如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/messageList"
android:layout_weight="1"
android:stackFromBottom="true"
android:transcriptMode="alwaysScroll" />
<RelativeLayout
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button
android:text="Send"
android:layout_alignParentRight="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/sendButton" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/sendButton"
android:layout_centerInParent="true"
android:id="@+id/messageText" />
</RelativeLayout>
</LinearLayout>
接下来,执行以下步骤以创建另外两个 Android 布局:
-
在
Resources目录的layout文件夹中创建一个名为MyMessageListItem.axml的新布局。 -
打开布局并切换到 Source 选项卡。将根 XML 元素更改为
RelativeLayout元素。 -
切换回 Content 选项卡,并将两个 TextView 控件拖放到布局中。
-
在 Id 字段中,分别输入
@+id/myMessageText和@+id/myMessageDate。 -
对于这两个视图,设置 Margin 为
3dp,以及 Width 和 Height 为wrap_content。 -
对于第一个 TextView,在 Style 选项卡下将其 Color 设置为
@android:color/holo_blue_bright。 -
对于第二个 TextView,在 Layout 选项卡下检查 Align Parent Right 复选框。
-
创建一个名为
TheirMessageListItem.axml的新布局,并重复此过程。为新布局中的第一个 TextView 选择不同的颜色。
最后,我们需要为屏幕创建一个新的活动。在 Activities 目录中创建一个名为 MessagesActivity.cs 的新 Android Activity。让我们从设置活动的标准代码开始,如下所示:
[Activity(Label = "Messages")]
public class MessagesActivity : BaseActivity<MessageViewModel>
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
}
}
接下来,让我们实现一个比之前实现的更复杂的适配器,如下所示:
class Adapter : BaseAdapter<Message>
{
readonly MessageViewModel messageViewModel = ServiceContainer.Resolve<MessageViewModel>();
readonly ISettings settings = ServiceContainer.Resolve<ISettings>();
readonly LayoutInflater inflater;
const int MyMessageType = 0, TheirMessageType = 1;
public Adapter (Context context)
{
inflater = (LayoutInflater)context.GetSystemService (Context.LayoutInflaterService);
}
public override long GetItemId(int position)
{
return position;
}
public override int Count
{
get { return messageViewModel.Messages == null ? 0: messageViewModel.Messages.Length; }
}
public override Message this[int index]
{
get { return messageViewModel.Messages [index]; }
}
public override int ViewTypeCount
{
get { return 2; }
}
public override int GetItemViewType(int position)
{
var message = this [position];
return message.UserId == settings.User.Id ?MyMessageType : TheirMessageType;
}
}
这包括除了我们即将实现的 GetView 之外的所有内容。在这里,首先是一些 MyMessageType 和 TheirMessageType 的常量。然后我们实现了 ViewTypeCount 和 GetItemViewType。这是 Android 用于在列表视图中使用两个不同布局的机制。我们为用户的消息使用一种类型的布局,为对话中的另一个用户使用不同的布局。
接下来,让我们按照以下方式实现 GetView:
public override View GetView(int position, View convertView, ViewGroup parent)
{
var message = this [position];
int type = GetItemViewType(position);
if (convertView == null)
{
if (type == MyMessageType)
{
convertView = inflater.Inflate(Resource.Layout.MyMessageListItem, null);
}
else
{
convertView = inflater.Inflate(Resource.Layout.TheirMessageListItem, null);
}
}
TextView messageText, dateText;
if (type == MyMessageType)
{
messageText = convertView.FindViewById<TextView>(Resource.Id.myMessageText);
dateText = convertView.FindViewById<TextView>(Resource.Id.myMessageDate);
}
else
{
messageText = convertView.FindViewById<TextView>(Resource.Id.theirMessageText);
dateText = convertView.FindViewById<TextView>(Resource.Id.theirMessageDate);
}
messageText.Text = message.Text;
dateText.Text = message.Date.ToString("MM/dd/yy HH:mm");
return convertView;
}
让我们通过以下步骤分解我们的实现:
-
我们首先提取了行位置的
message对象。 -
接下来,我们获取视图类型,以确定它是当前用户的消息还是对话中的另一个用户。
-
如果
convertView为null,则根据类型填充适当的布局。 -
接下来,我们从
convertView中提取两个文本视图,messageText和dateText。我们必须使用类型值来确保我们使用正确的资源 ID。 -
我们使用
message对象在两个文本视图中设置适当的文本。 -
我们返回
convertView。
现在,让我们通过设置适配器的其余部分来完成 MessagesActivity。首先,让我们实现一些成员变量和 OnCreate 方法,如下所示:
ListView listView;
EditText messageText;
Button sendButton;
Adapter adapter;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
Title = viewModel.Conversation.Username;
SetContentView(Resource.Layout.Messages);
listView = FindViewById<ListView>(Resource.Id.messageList);
messageText = FindViewById<EditText>(Resource.Id.messageText);
sendButton = FindViewById<Button>(Resource.Id.sendButton);
listView.Adapter = adapter = new Adapter(this);
sendButton.Click += async (sender, e) =>
{
viewModel.Text = messageText.Text;
try
{
await viewModel.SendMessage();
messageText.Text = string.Empty;
adapter.NotifyDataSetInvalidated();
listView.SetSelection(adapter.Count);
}
catch (Exception exc)
{
DisplayError(exc);
}
};
}
与本章中我们之前的活动相比,到目前为止这个活动相当标准。我们还在 OnCreate 中连接了 sendButton 的 Click 事件,以便发送消息并刷新列表。我们还使用了一个技巧,通过将选择设置为最后一个项目来滚动列表视图。
接下来,我们需要实现OnResume来加载消息,使适配器失效,然后将列表视图滚动到末尾,如下所示:
protected async override void OnResume()
{
base.OnResume();
try
{
await viewModel.GetMessages();
adapter.NotifyDataSetInvalidated();
listView.SetSelection(adapter.Count);
}
catch (Exception exc)
{
DisplayError(exc);
}
}
所以最终,如果你编译并运行这个应用,你将能够导航到消息屏幕,并向列表中添加新的消息,如下面的截图所示:

摘要
在本章中,我们首先回顾了 AndroidManifest 文件中的基本设置。接下来,我们实现了一个自定义的Application类来设置我们的ServiceContainer。然后,我们介绍了 Android 的不同布局类型,并使用原生 Android 视图实现了登录屏幕。我们实现了好友列表屏幕,并学习了ListView和适配器的基础知识。最后,我们实现了消息屏幕,并使用了列表视图适配器和布局中更高级的功能。
完成本章学习后,你将拥有一个部分功能的 Android 版 XamChat。你将更深入地理解 Android SDK 和工具。你应该自信地使用 Xamarin 开发自己的 Android 应用。请自行实现本章未涵盖的剩余屏幕。如果你感到困惑,请随时查阅本书附带的全样本应用。在下一章中,我们将介绍如何部署到移动设备,以及为什么在实际设备上测试你的应用非常重要。
第七章。在设备上部署和测试
首次尝试部署到设备时,这既重要又有些麻烦。在设备上测试通常会显示在应用程序的模拟器/仿真器中不存在的性能问题。您还可以测试只能在真实设备上完成的事情,例如 GPS、摄像头、内存限制或蜂窝网络连接。在为 Xamarin 开发时,也存在一些常见的陷阱,这些陷阱只有在真实设备上测试时才会显现。
在本章中,我们将涵盖以下主题:
-
iOS 配置
-
Android 设备调试设置
-
链接器
-
预编译(AOT)
-
与 Xamarin 相关的常见内存陷阱
在我们开始本章之前,重要的是要注意,部署到 iOS 设备需要 iOS 开发者计划会员资格。您可以随时返回到第一章,设置 Xamarin,以了解此过程。
iOS 配置
苹果公司对部署应用程序到 iOS 设备有一套严格的过程。虽然这对开发者来说可能相当复杂,有时甚至痛苦,但苹果可以通过阻止普通用户侧载可能有害的应用程序来提高一定级别的安全性。
部署到 iOS 的先决条件
在我们能够将我们的应用程序部署到 iOS 设备之前,我们需要在iOS 开发中心设置一些事情。我们将首先为您创建 App ID 或包 ID。这是任何 iOS 应用程序的主要标识符。
我们将首先导航到 developer.apple.com 并执行以下步骤:
-
点击iOS 应用图标。
-
使用您的开发者账户登录。
-
点击右侧导航中的证书、标识符和配置文件。
-
点击标识符。
-
点击加号按钮添加新的 iOS App ID。
-
在名称字段中,输入一些有意义的名称,例如
YourCompanyNameWildcard。 -
选择通配符 App ID单选按钮。
-
在包 ID字段中,选择一个反向域名风格的名称,例如
com.yourcompanyname.*。 -
点击继续。
-
检查最终设置并点击提交。
保持此网页打开,因为我们将在本章中一直使用它。
我们刚刚为您注册了一个通配符包 ID;请将此用作所有未来希望与该账户关联的应用程序的名称前缀。稍后,当您准备将应用程序部署到苹果应用商店时,您将创建一个显式 App ID,例如 com.yourcompanyname.yourapp。这允许您将特定应用程序部署到商店,而通配符 ID 最好用于部署到设备进行测试。
接下来,我们需要找到您计划在调试应用程序的每个设备上的唯一标识符。苹果要求每个设备都必须在您的账户下注册,每个开发者的设备数量限制为 200 台。绕过这一要求的唯一方法是注册 iOS 开发者企业计划,该计划每年费用为 299 美元,与标准 99 美元的开发者费用分开。
我们将开始启动 Xcode 并执行以下步骤:
-
在顶部菜单中导航到窗口 | 设备。
-
使用 USB 线将您的目标设备连接上。
-
在左侧导航栏中,您应该看到您的设备名称。点击它以选择它。
-
注意您的设备的标识符值。将其复制到您的剪贴板。
以下截图显示了在 Xcode 中选择您的设备时您的屏幕应该看起来像什么:

返回到developer.apple.com(希望它仍然在章节的早期打开)并执行以下步骤:
-
在左侧导航栏中导航到设备 | 所有。
-
点击页面右上角的加号按钮。
-
为您的设备输入一个有意义的名称,并将剪贴板中的标识符值粘贴到UDID字段中。
-
点击继续。
-
查看您输入的信息并点击注册。
在将来,当您的账户完全设置好时,您只需在 Xcode 中点击用于开发按钮即可跳过第二组步骤。
以下截图显示了完成时您的设备列表应该看起来像什么:

接下来,我们需要生成一个代表您作为账户开发者的证书。在 Xcode 5 之前,您必须使用 Mac 上的密钥链应用程序创建一个证书签名请求。您可以在“应用程序”下找到它,或者使用 OS X 的搜索 spotlight 通过Command + Space。Xcode 的新版本通过将许多此过程集成到 Xcode 中,使事情变得容易得多。
打开 Xcode 并执行以下步骤:
-
在顶部菜单中导航到Xcode | 首选项。
-
选择账户选项卡。
-
点击左下角的加号按钮,然后点击添加 Apple ID。
-
输入您的开发者账户的电子邮件和密码。
-
在创建账户时,点击右下角的查看详情。
-
点击左下角的同步按钮。
-
如果这是一个新账户,Xcode 将显示一个警告,表示尚不存在任何证书。勾选每个框并点击请求以生成证书。
Xcode 现在将自动为您创建一个开发者证书并将其安装到您的 Mac 的密钥链中。
以下截图显示了设置账户后您的屏幕将看起来像什么:

创建配置文件
接下来,我们需要创建一个配置文件。这是允许应用程序安装在 iOS 设备上的最终文件。配置文件包含一个 App ID、设备 ID 列表,最后是开发者的证书。您还必须在 Mac 的密钥链中拥有开发者证书的私钥才能使用配置文件。
以下是一些配置文件类型:
-
开发:这用于调试或发布构建。当您的应用程序处于开发阶段时,您将积极使用此类配置文件。
-
Ad Hoc:这主要用于发布构建。此类证书非常适合 beta 测试或向少量用户分发。使用企业开发者帐户,您可以使用此方法向无限数量的用户分发。
-
App Store:这用于提交到 App Store 的发布构建。您不能使用此证书将应用程序部署到您的设备;它只能用于商店提交。
让我们回到developer.apple.com并按照以下步骤创建一个新的配置文件:
-
导航到左侧面板上的配置文件 | 所有。
-
点击页面右上角的加号按钮。
-
选择iOS 应用开发并点击继续。
-
选择本章中创建的通配符 App ID 并点击继续。
-
选择本章中创建的证书并点击继续。
-
选择您想要部署到的设备并点击继续。
-
输入一个合适的配置文件名称,例如
YourCompanyDev。 -
点击生成,您的配置文件将被创建。
以下截图显示了创建过程中最终将得到的配置文件。不用担心下载文件;我们将使用 Xcode 导入最终配置文件。

要导入配置文件,请返回 Xcode 并执行以下步骤:
-
导航到对话框顶部的菜单中的Xcode | 首选项。
-
选择帐户选项卡。
-
选择您的帐户并点击查看详情。
-
点击左下角的同步按钮。
-
几秒钟后,您的配置文件将出现。
Xcode 将自动包括您在 Apple 开发者网站上创建的任何配置文件。Xcode 还会创建一些自己的配置文件。
在 Xamarin Studio 的最新版本中,您可以查看这些配置文件,但无法同步它们。导航到Xamarin Studio | 首选项 | 开发者帐户以查看 Xamarin Studio 中的配置文件。您还可以在 Xamarin 的文档网站上查看有关 iOS 配置的文档,网址为docs.xamarin.com/guides/ios/getting_started/device_provisioning/。
安卓设备设置
与在 iOS 设备上部署应用的麻烦相比,Android 要容易得多。要将应用部署到设备上,你只需在设备上设置一些设置即可。这是由于 Android 相对于 iOS 的开放性。大多数用户的 Android 设备调试是关闭的,但任何可能想尝试编写 Android 应用的开发者都可以轻松将其打开。
我们将首先打开设置应用。你可能需要通过查看设备上的所有应用来找到它,如下所示:
-
向下滚动并点击标记为开发者选项的部分。
-
在顶部的操作栏中,你可能需要将开关切换到开启位置。这因设备而异。
-
向下滚动并勾选USB 调试。
-
将出现一个警告确认消息。然后,点击确定。
小贴士
注意,一些较新的 Android 设备使得普通用户开启 USB 调试变得稍微困难一些。你必须点击开发者选项项目七次才能打开此选项。
以下截图显示了在过程中你的设备将看起来像什么:

启用此选项后,你只需通过 USB 连接你的设备,并在 Xamarin Studio 中调试 Android 应用即可。你将在选择设备对话框中看到你的设备。请注意,如果你使用 Windows 或非标准设备,你可能需要访问设备制造商的网站来安装驱动程序。大多数三星和 Nexus 设备会自动安装它们的驱动程序。在 Android 4.3 及以上版本中,在开始 USB 调试会话之前,设备上还会出现一个确认对话框。
以下截图显示了在选择设备对话框中你的设备将看起来像三星 Galaxy SII。Xamarin Studio 将显示型号编号,这并不总是你可以识别的名字。你可以在设备的设置中查看此型号编号。

理解链接器
为了使 Xamarin 应用在移动设备上保持小巧和轻量,Xamarin 为其编译器创建了一个名为链接器的功能。其主要目的是从核心 Mono 汇编(如System.dll)和平台特定汇编(如Mono.Android.dll和monotouch.dll)中删除未使用的代码。然而,如果它被设置为在你的自己的汇编上运行,它也可以给你带来同样的好处。不运行链接器,整个 Mono 框架大约有 30 兆字节。这就是为什么默认情况下在设备构建中启用链接的原因,这使你可以保持你的应用小巧。
链接器使用静态分析来处理汇编中的各种代码路径。如果它确定某个方法或类从未使用过,它将从该汇编中删除未使用的代码。这可能是一个耗时的过程,因此默认情况下,在模拟器中运行的构建会跳过此步骤。
Xamarin 应用程序有针对链接器的以下三个主要设置:
-
不链接:在此设置中,跳过链接器编译步骤。这最好用于在模拟器中运行的构建或如果您需要诊断链接器可能存在的问题。
-
仅链接 SDK 程序集:在此设置中,链接器将仅在核心 Mono 程序集(如
System.dll、System.Core.dll和System.Xml.dll)上运行。 -
链接所有程序集:在此设置中,链接器将针对您的应用程序中的所有程序集运行,包括您使用的任何类库或第三方程序集。
这些设置可以在任何 Xamarin.iOS 或 Xamarin.Android 应用程序的项目选项中找到。通常,这些设置不会出现在类库中,因为它通常与将要部署的 iOS 或 Android 应用程序相关联。
链接器也可能在运行时引起潜在问题,因为在某些情况下,其分析错误地确定某些代码未使用。如果您使用的是System.Reflection命名空间中的功能而不是直接访问方法或属性,则可能会发生这种情况。这就是为什么您需要在物理设备上测试您的应用程序很重要的原因,因为设备构建启用了链接。
为了演示这个问题,让我们看一下以下代码示例:
//Just a simple class for holding info
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
//Then somewhere later in your code
var person = new Person { Id = 1, Name = "Chuck Norris" };
var propInfo = person.GetType().GetProperty("Name");
string value = propInfo.GetValue(person) as string;
Console.WriteLine("Name: " + value);
使用不链接或仅链接 SDK 程序集的选项运行前面的代码将正常工作。但是,如果您尝试使用链接所有程序集运行此代码,您将得到一个类似于以下异常:
Unhandled Exception:
System.ArgumentException: Get Method not found for 'Name' at System.Reflection.MonoProperty.GetValue (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] index, System.Globalization.CultureInfo culture) at System.Reflection.PropertyInfo.GetValue (System.Object obj)
由于Name属性的 getter 从未直接从代码中使用,链接器将其从汇编中移除。这导致反射代码在运行时失败。
尽管代码中可能会出现潜在问题,但链接所有程序集的选项仍然非常有用。只有在这种模式下才能执行一些优化,Xamarin 可以将您的应用程序减小到可能的最小大小。如果您的应用程序需要性能或极小的下载大小,您可以尝试此选项。但是,应该进行彻底的测试,以验证链接程序集不会引起任何问题。
为了解决代码中的问题,Xamarin 包含了一套完整的解决方案,以防止您的代码的特定部分被移除。
一些选项包括以下内容:
-
使用
[Preserve]标记类成员。这将强制链接器包含具有该属性的函数、字段或属性。 -
使用
[Preserve(AllMembers=true)]标记整个类。这将保留类中的所有代码。 -
使用
[assembly: Preserve]标记整个程序集。这是一个程序集级别的属性,将保留其中包含的所有代码。 -
通过修改项目选项中的附加 mtouch 参数来跳过一个整个程序集。使用
--linkskip=System来跳过一个整个程序集。这可以用于您没有源代码的程序集。 -
通过 XML 文件进行自定义链接。当你需要跳过没有源代码的特定类或方法的链接时,这是最佳选择。在附加 mtouch 参数中使用
–-xml=YourFile.xml。
以下是一个演示自定义链接的示例 XML 文件:
<linker>
<assembly fullname="mscorlib">
<type fullname="System.Environment">
<field name="mono_corlib_version" />
<method name="get_StackTrace" />
</type>
</assembly>
<assembly fullname="My.Assembly.Name">
<type fullname="MyTypeA" preserve="fields" />
<method name=".ctor" />
</type>
<type fullname="MyTypeB" />
<method signature="System.Void MyFunc(System.Int32 x)" />
<field signature="System.String _myField" />
</type>
</assembly>
</linker>
自定义链接是最复杂的选择,通常是最后的手段。幸运的是,大多数 Xamarin 应用程序不需要解决许多链接器问题。
理解 AOT 编译
Windows 上 Mono 和 .NET 的运行时基于一个即时(JIT)编译器。C# 和其他 .NET 语言被编译成微软中间语言(MSIL)。在运行时,MSIL 被编译成原生代码以在运行应用程序的任何类型的架构上运行。Xamarin.Android 遵循这个模式。然而,由于苹果对动态生成代码的限制,iOS 上不允许使用 JIT 编译器。
为了绕过这个限制,Xamarin 开发了一个新的选项,称为预编译(AOT)编译。除了使 .NET 在 iOS 上成为可能之外,AOT 还具有其他好处,如更短的启动时间和可能更好的性能。
AOT 也有一些限制,通常与 C# 泛型相关。为了在编译前编译程序集,编译器需要对你的代码进行一些静态分析以确定信息类型。泛型给这种情况带来了麻烦。
有一些情况不支持 AOT,但在 C# 中它们是完全有效的。第一个是一个泛型接口,如下所示:
interface MyInterface<T>
{
T GetMyValue();
}
编译器无法在编译前确定可以实现此接口的类,尤其是在涉及多个程序集时。第二个限制与第一个相关。你不能重写包含泛型参数或返回值的虚拟方法。
以下是一个简单的示例:
class MyClass<T>
{
public virtual T GetMyValue()
{
//Some code here
}
}
class MySubClass : MyClass<int>
{
public override int GetMyValue()
{
//Some code here
}
}
再次,编译器的静态分析无法在编译时确定哪些类可以重写此方法。
另一个限制是,你无法在泛型类中使用 DllImport,如下面的代码所示:
class MyGeneric<T>
{
[DllImport('MyImport")]
public static void MyImport();
}
如果你不太熟悉语言功能,DllImport 是从 C# 调用原生 C/C++ 方法的一种方式。在泛型类中使用它们是不支持的。
这些限制是另一个很好的理由,说明为什么在设备上进行测试很重要,因为前面的代码在其他可以运行 C# 代码但不是 Xamarin.iOS 的平台上运行良好。
避免常见的内存陷阱
移动设备上的内存绝对不是无限的商品。因此,你的应用程序中的内存使用可能比桌面应用程序更重要。有时,你可能需要使用内存分析器,或者改进你的代码以更有效地使用内存。
以下是最常见的内存陷阱:
-
垃圾回收器(GC)无法快速收集大对象以跟上你的应用程序
-
你的代码无意中导致内存泄漏
-
一个 C#对象被垃圾收集,但后来被原生代码尝试使用
垃圾收集器
让我们看看第一个问题,即 GC 无法跟上。假设我们有一个 Xamarin.iOS 应用程序,其中有一个按钮用于在 Twitter 上分享图片,如下所示:
twitterShare.TouchUpInside += (sender, e) =>
{
var image = UImage.FromFile("YourLargeImage.png");
//Share to Twitter
};
现在,假设图片是从用户的相册中获取的 10MB 图片。如果用户快速点击按钮并取消 Twitter 帖子,应用程序可能会出现内存不足的情况。iOS 通常会强制关闭使用过多内存的应用程序,您不希望用户在使用您的应用程序时遇到这种情况。
最佳解决方案是在处理完图片后调用Dispose,如下所示:
var image = UImage.FromFile('YourLargeImage.png");
//Share to Twitter
image.Dispose();
更好的方法是将 C#的using语句利用如下:
using(var image = UImage.FromFile('YourLargeImage.png"))
{
//Share to Twitter
}
C#的using语句会在try-finally块中自动调用Dispose,因此即使在抛出异常的情况下,对象也会被销毁。我建议您尽可能利用using语句来处理任何IDisposable类。对于像NSString这样的小对象,这并不总是必要的,但对于更大、更重的UIKit对象来说,这是一个好主意。
小贴士
类似的情况也可能在 Android 的Bitmap类中发生。尽管略有不同,但最好在这个类上调用Dispose和Recycle方法,并使用BitmapFactory.Options设置InPurgeable和InInputShareable。
内存泄漏
内存泄漏是下一个潜在的问题。C#作为一个受管理的、垃圾回收的语言,防止了许多内存泄漏,但并非全部。在 C#中最常见的泄漏是由事件引起的。
假设我们有一个具有事件的静态类,如下所示:
static class MyStatic
{
public static event EventHandler MyEvent;
}
现在,假设我们需要从 iOS 控制器中订阅事件,如下所示:
public override void ViewDidLoad()
{
base.ViewDidLoad();
MyStatic.MyEvent += (sender, e) =>
{
//Do something
};
}
这里的问题在于静态类会保留对控制器的引用,直到事件取消订阅。这是很多开发者可能会忽略的情况。为了在 iOS 上解决这个问题,我会在ViewWillAppear中订阅事件,并在ViewWillDisappear中取消订阅。在 Android 上,使用OnStart和OnStop,或者OnPause和OnResume。
您将正确实现此事件如下:
public override void ViewWillAppear()
{
base.ViewWillAppear();
MyStatic.MyEvent += OnMyEvent;
}
public override void ViewWillDisappear()
{
base.ViewWillDisappear ();
MyStatic.MyEvent -= OnMyEvent;
}
然而,一个事件并不是内存泄漏的必然原因。例如,在ViewDidLoad方法中订阅按钮的TouchUpInside事件是完全可以的。因为按钮的生命周期与控制器一样长,所以所有内容都可以在没有问题的前提下进行垃圾回收。
访问 GC 已处理的对象
对于最后一个问题,垃圾收集器有时会移除 C#对象。后来,一个 Objective-C 对象尝试访问它。
以下是一个将按钮添加到UITableViewCell的示例:
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
var cell = tableView.DequeueReusableCell('MyCell");
//Remaining cell setup here
var button = UIButton.FromType(UIButtonType.InfoDark);
button.TouchUpInside += (sender, e) =>
{
//Do something
};
cell.AccessoryView = button;
return cell;
}
我们将内置的信息按钮作为附件视图添加到单元格中。这里的问题是按钮将被垃圾回收,但它的 Objective-C 对应物将保持使用状态,因为它显示在屏幕上。如果你在一段时间后点击该按钮,你将得到一个类似以下崩溃:
mono-rt: Stacktrace:
mono-rt: at <unknown>
mono-rt: at (wrapper managed-to-native) MonoTouch.UIKit.UIApplication.UIApplicationMain (int,string[],intptr,intptr)
mono-rt: at MonoTouch.UIKit.UIApplication.Main (string[],string,string)
... Continued ...
=================================================================
Got a SIGSEGV while executing native code. This usually indicates
a fatal error in the mono runtime or one of the native libraries
used by your application.
================================================================
这不是最描述性的错误信息,但一般来说,你知道原生 Objective-C 代码中出了些问题。要解决这个问题,创建一个自定义的 UITableViewCell 子类,并为按钮创建一个专门的成员变量,如下所示:
public class MyCell : UITableViewCell
{
UIButton button;
public MyCell()
{
button = UIButton.FromType(UIButtonType.InfoDark);
button.TouchUpInside += (sender, e) =>
{
//Do something
};
AccessoryView = button;
}
}
现在,你的 GetCell 实现看起来可能像以下代码:
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
var cell = tableView.DequeueReusableCell('MyCell") as MyCell;
//Remaining cell setup here
return cell;
}
由于按钮不再是局部变量,它将不会在需要之前被垃圾回收。这样可以避免崩溃,并且从某种意义上说,这段代码更加简洁。类似的情况可能在 Android 上发生,由于 C# 和 Java 之间的交互;然而,这种情况不太可能,因为两者都是垃圾回收语言。
摘要
在本章中,我们开始学习设置 iOS 配置文件的过程,以便部署到 iOS 设备。接下来,我们查看将应用程序部署到 Android 设备所需的设备设置。我们发现了 Xamarin 链接器以及它如何使你的应用程序更小、更注重性能。我们通过了各种设置来解决由你的代码和链接器引起的问题,并解释了 iOS 上的 AOT 编译及其局限性。最后,我们涵盖了 Xamarin 应用程序可能遇到的最常见的内存陷阱。
在移动设备上测试你的 Xamarin 应用程序对于各种原因来说都很重要。由于 Xamarin 必须解决的平台限制,一些错误只会在设备上显示。你的电脑更加强大,所以你会在模拟器上看到不同的性能,而不是在物理设备上。在下一章中,我们将使用 Windows Azure 创建一个真实的 Web 服务来驱动我们的 XamChat 应用程序。我们将使用一个名为 Azure Mobile Services 的功能,并在 iOS 和 Android 上实现推送通知。
第八章. 带有推送通知的 Web 服务
现代移动应用程序由其网络连接性定义。一个不与 Web 服务器交互的移动应用既难找,又不如其他情况下那样互动或社交。在这本书中,我们将使用 Windows Azure 云平台来实现 XamChat 应用程序的服务器端后端。我们将使用一个名为 Azure Mobile Services 的功能,它非常适合我们的应用程序,并且具有内置推送通知的优势。一旦我们完成本章,我们的 XamChat 示例应用程序将更接近成为一个真实的应用程序,并允许其用户相互交互。
在本章中,我们将涵盖以下主题:
-
Windows Azure 提供的服务
-
设置您的 Azure 账户
-
Azure Mobile Services 作为 XamChat 的后端
-
创建表和脚本
-
实现 XamChat 的真实 Web 服务
-
使用 Apple Push 通知服务
-
使用 Google Cloud Messaging 发送通知
学习 Windows Azure
Windows Azure 是微软于 2010 年发布的一个优秀的云平台。Azure 提供了 基础设施即服务(IaaS)和 平台即服务(PaaS),用于构建现代的 Web 应用和服务。这意味着它为您提供了访问直接虚拟机的权限,您可以在其中部署您选择的任何操作系统或软件。这被称为 IaaS。Azure 还提供了多个平台用于构建应用程序,如 Azure Websites 或 SQL Azure。这些平台被称为 PaaS,因为您在较高层次上部署软件,无需直接处理虚拟机或管理软件升级。
让我们来看看 Windows Azure 提供的以下更常见的服务:
-
虚拟机:Azure 为您提供了访问各种大小虚拟机的权限。您可以安装您选择的任何操作系统。在 Azure 的图库中有很多预制的发行版可供选择。
-
网站:您可以部署任何在 Microsoft IIS 中运行的网站类型,从 ASP.NET 网站到 PHP 或 Node.js。
-
SQL Azure:这是 Microsoft SQL Server 的云版本,它是一个功能齐全的 关系数据库管理系统(RDMS),用于存储数据。
-
移动服务:这是一个用于构建移动应用 Web 服务的简单平台。它使用 SQL Azure 作为后端存储,以及基于 Node.js 的简单 JavaScript 脚本系统来添加业务逻辑。在 Azure Mobile Services 的最新版本中,您还可以使用 C# 和 ASP.NET Web API 来开发服务器端代码。
-
存储:Azure 提供了 Blob 存储,一种存储二进制文件的方法,以及 表存储,这是一种用于持久化数据的 NoSQL 解决方案。
-
服务总线: 这是一个基于云的解决方案,用于创建队列以促进其他云服务之间的通信。它还包括通知中心,作为向移动应用程序提供推送通知的简单方式。
-
工作角色: 在云中运行自定义过程的一种简单方式可以是普通的 Windows 可执行文件或.NET 工作角色项目。
-
媒体服务: 提供流式音频或视频到几乎任何设备的机制。它处理编码和交付,并可以扩展以支持大量用户。
-
HDInsight: 在 Windows Azure 上运行的 Apache Hadoop 版本,用于管理极其庞大的数据库,也称为大数据。
-
云服务: 这是由其他服务组合而成的集合。云服务允许您将多个服务捆绑在一起,并创建预生产和生产环境。这是一个出色的部署工具;您可以将更改部署到预生产环境,并交换预生产和生产环境以保持用户的正常运行时间。
除了这些服务之外,还有许多其他服务,并且新服务被定期添加。我们将使用Azure 移动服务,它利用 SQL Azure,来构建 XamChat 的 Web 服务。您可以通过访问windowsazure.com来了解定价和提供的服务详情。
在这本书中,我们选择使用 Windows Azure 作为 XamChat 的 Web 服务后端来展示解决方案,因为它与 Xamarin 应用程序配合使用非常方便,得益于 Xamarin 组件商店中发现的出色库。然而,除了 Azure 之外,还有许多其他选择,您可能想要考虑。使用 Xamarin 的开发平台并不会限制您的应用程序可以交互的 Web 服务类型。
这里还有一些更常见的例子:
-
Parse: 该服务提供与 Azure 移动服务类似的产品,包括数据存储和推送通知。这是许多移动开发者中流行的服务,甚至包括那些不使用 Xamarin 的开发者。您可以在
parse.com获取更多信息。 -
Urban Airship: 该服务为多个平台上的移动应用程序提供推送通知。您可以在
urbanairship.com获取更多信息。 -
亚马逊云服务: 这是一项完整的云解决方案,相当于 Windows Azure。它拥有您部署云应用程序所需的一切,包括全面的支持虚拟机。主要区别在于 Azure 非常专注于 C#,并且是为.NET 开发者构建的。在亚马逊上,PaaS 选项也不如 Azure 多。您可以在
aws.amazon.com获取更多信息。
此外,您还可以使用您选择的编程语言和技术,在自己的本地 Web 服务器或低成本托管服务上开发自己的 Web 服务。
设置您的 Azure 账户
要开始使用 Windows Azure 进行开发,你可以订阅一个免费的一个月试用期,以及价值 200 美元的免费信用额度。如果你有 MSDN 订阅,你还可以获得更多优惠。与此相关,Azure 的许多服务都有免费层,提供较低性能的版本。所以如果你的试用期到期,你可以根据你使用的服务以很少或没有成本继续开发。
让我们从导航到windowsazure.com/en-us/pricing/free-trial并执行以下步骤开始:
-
点击现在尝试链接。
-
使用 Windows Live ID 登录。
-
为了安全起见,通过电话或短信验证你的账户。
-
输入支付信息。这仅在超出你的消费限额时使用。你不会在开发应用程序时意外超出预算——直到真实用户开始与你的服务互动,才比较难意外地花费金钱。
-
点击我同意政策并点击注册。
-
查看最终设置并点击提交。
-
如果所有必要的信息都正确输入,你现在将最终能够访问 Azure 订阅页面。你的订阅页面看起来类似于以下截图:

你可以点击页面右上角的门户链接来访问你的账户。将来,你可以在manage.windowsazure.com管理你的 Azure 服务。
完成 Windows Azure 之旅,快速了解管理门户的功能。然后你可以访问主菜单来创建新的 Azure 服务、虚拟机等。主菜单看起来类似于以下截图:

这就完成了 Windows Azure 的注册过程。与苹果和 Google Play 开发者程序相比,这相当简单。请随意尝试,但不必过于担心花钱。Azure 的大多数服务都有免费版本,并且还免费提供一定量的带宽。你可以在windowsazure.com/en-us/pricing/overview了解更多有关定价的信息。
注意,关于 Windows Azure 昂贵的误解有很多。你可以在免费层上完成所有应用程序的开发,而不需要花费一分钱。当将应用程序投入生产时,你可以轻松地根据需要增加或减少虚拟机实例的数量,以控制成本。一般来说,如果你用户不多,你不会花很多钱。同样,如果你有很多用户,你应该能赚很多收入。
探索 Azure 移动服务
对于 XamChat 的服务器端,我们将使用 Azure 移动服务为应用程序提供后端存储。移动服务是加速提供数据存储和基于REST的 API 的移动应用开发的便捷解决方案,这是一种基于标准的通过 HTTP 与网络服务通信的方式。Azure 移动服务还包括一个.NET 客户端库,用于从 C#与该服务交互。
Azure 移动服务的一些不错特性如下:
-
使用 SQL Azure 或其他 Azure 数据服务(如 Blob 或 Table 存储)在云中存储数据
-
使用 Windows Live ID、Facebook、Google 和 Twitter 进行简单认证
-
支持 iOS、Android 和 Windows 设备的推送通知
-
使用 JavaScript 和 Node.js 或 C#编写服务器端代码
-
一个易于使用的.NET 客户端开发库
-
将 Azure 移动服务扩展以适应大量数据
您可以看到为什么使用 Azure 是简单移动应用的一个好选择。加速开发和它提供的众多功能非常适合我们的 XamChat 示例应用。
让我们导航到manage.windowsazure.com并执行以下步骤以创建移动服务:
-
点击窗口左下角的加号按钮。
-
通过菜单导航到计算 | 移动服务 | 创建。
-
输入您选择的域名 URL,例如
yourname-xamchat。 -
我们目前使用的是免费数据库选项。
-
在区域下拉菜单中选择您所在位置附近的数据中心。
-
对于后端下拉菜单,为了这本书,请将选择保留在JavaScript上。由于我们更关注客户端,设置后端将更加简单。您也可以使用 C#作为替代,但请注意,本书中的示例将使用 JavaScript 编写。
-
现在,点击下一步。
-
使用默认的数据库名称并选择新建 SQL 数据库服务器。
-
输入 SQL 服务器的登录名和密码,并确保将此信息保存在安全的地方。
-
确保区域与您的移动服务相同,以确保移动服务和其数据库之间良好的性能。
-
检查您的最终设置并点击完成按钮。
管理门户将显示进度,创建您的移动服务和 SQL 服务器实例将需要几秒钟。请记住,Azure 正在为您在幕后创建和启动新的虚拟机,因此它确实在为您的要求做大量工作。
完成后,您的账户将除了包含在所有账户中的默认目录外,还将有一个移动服务和一个SQL 数据库,如下面的截图所示:

如果你查看移动服务的 缩放 选项卡,你会注意到它默认运行在 免费 层级。这是一个很好的开发场所。在撰写本书时,它可以容纳 500 个设备。当你将应用程序部署到生产环境时,你可能需要考虑 基本 或 标准 层级,这些层级也为你提供了添加多个实例的选项。
创建表格和脚本
在 Azure 移动服务中实现某项功能的第一步是创建一个新的表格。默认情况下,Azure 移动服务使用其 SQL 数据库的 动态模式 功能。当你从客户端插入一行时,新列会动态地添加到表格中。这可以防止你需要手动创建数据库模式,并且是一种整洁的代码优先的方法来开发你的后端数据库。你始终可以手动连接到 SQL 数据库来微调事物或手动更改模式。
返回管理门户,选择你的移动服务实例,并执行以下步骤:
-
点击 数据 选项卡。
-
点击页面底部中央的 创建 按钮。
-
将表格名称输入为
User。 -
将其他所有设置保留为默认值,然后点击 保存 按钮。
-
重复此过程创建另外三个名为
Friend、Message和Conversation的表格。
现在我们已经有了四个表格,我们需要创建一个脚本来简化我们应用用户的登录过程。Azure 移动服务允许你通过创建在 Node.js 中运行的脚本(Node.js 是一个用于开发带有 JavaScript 的网络服务的框架)来向你的表格添加自定义逻辑。你可以覆盖在 insert、read、update 或 delete 操作期间每个表格所发生的事情。此外,如果你需要其他功能,你还可以创建完全定制的脚本。
点击 User 表格,然后点击 脚本 选项卡。确保你正在查看 insert 操作。默认情况下,你的脚本将非常简单,如下面的代码片段所示:
function insert(item, user, request) {
request.execute();
}
Azure 移动服务中的脚本有三个参数,具体如下:
-
item:此参数是客户端发送到服务的对象。在我们的案例中,它将是我们在前几章中创建的User对象。 -
user:此参数包含有关已认证用户的信息。在我们的示例中我们不会使用它。 -
request:此参数是一个对象,用于运行table操作并向客户端发送响应。调用execute将完成操作并向客户端返回成功响应。
我们需要修改前面的脚本,以确保只有当新用户不存在时才插入新用户。如果用户已存在,我们需要确保密码与用户名匹配。让我们对脚本进行一些修改,如下面的代码行所示:
function insert(item, user, request) {
var users = tables.getTable('user');
users.where({ username : item.Username }).read({
success: function(results) {
if (results.length == 0) {
//This is a new user
request.execute();
}
else {
var user = results[0];
if (item.Password == user.Password) {
request.respond(statusCodes.OK, user);
}
else {
request.respond(statusCodes.UNAUTHORIZED, "Incorrect username or password");
}
}
}
});
}
让我们总结一下前面 JavaScript 中所做的工作:
-
首先,我们获取了
user表。请注意,您可以使用小写来引用表名。 -
接下来,我们运行了一个查询来使用
where函数提取任何现有的用户。我们使用了item.Username,因为这与我们的 C# 中的User对象相匹配。注意这个方法与 C# 中的Linq类似。 -
如果没有结果,我们让请求正常执行。
-
否则,我们比较密码,如果匹配则返回
statusCodes.OK。 -
如果密码不匹配,我们返回
statusCodes.UNAUTHORIZED。这将导致客户端收到错误。 -
要获取可用函数和方法的完整列表,请确保查看 MSDN 上的服务器脚本参考
tinyurl.com/AzureMobileServices。
从这里,只需确保点击 保存 按钮以应用您的更改。Azure 移动服务还提供了通过 Git 提供脚本源代码控制选项。如果您想在本地的首选编辑器中而不是网站编辑器中更改脚本,请充分利用此功能。
然后,我们需要创建一个额外的脚本。本书早期实现的 XamChat 允许用户通过输入朋友的用户名来添加朋友。因此,为了将数据插入到 Friend 表中,我们需要修改 insert 脚本来通过用户名查找用户。
让我们按照以下方式修改 Friends 表的 insert 脚本:
function insert(item, user, request) {
var users = tables.getTable('user');
users.where({ username : item.Username }).read({
success: function(results) {
if (results.length === 0) {
//Could not find the user
request.respond(statusCodes.NOT_FOUND, "User not found");
}
else {
var existingUser = results[0];
item.UserId = existingUser.id;
request.execute();
}
}
});
}
这与之前我们所做的是非常相似的;我们运行了一个简单的查询来根据 Username 值加载 user 表。我们只需在执行请求之前在新的 friend 表上设置 UserId 值。
为 XamChat 添加后端
服务器端更改完成后,下一步是在我们的 XamChat iOS 和 Android 应用程序中实现我们的新服务。幸运的是,因为我们使用了一个名为 IWebService 的接口,所以我们只需实现这个接口就可以在我们的应用程序中使其工作。
现在,我们可以通过执行以下步骤在我们的 iOS 应用程序中开始设置我们的服务:
-
打开我们在 第四章 中创建的
XamChat.Core项目,XamChat – 一个跨平台应用。 -
在项目中创建一个
Azure文件夹。 -
创建一个名为
AzureWebService.cs的新类。 -
创建一个
public类并实现IWebService。 -
在您的代码中右键点击
IWebService并导航到 重构 | 实现接口。 -
将出现一行;按 Enter 插入方法占位符。
当此设置完成时,您的类将类似于以下内容:
public class AzureWebService : IWebService
{
#region IWebService implementation
public Task<User> Login(string username, string password)
{
throw new NotImplementedException();
}
// -- More methods here --
#endregion
}
添加 Azure 移动服务 NuGet 包
为了使使用 Azure 移动服务开发变得更加容易,我们需要添加对 .NET 客户端库的引用。为此,我们将使用 NuGet 来添加库:
-
右键点击
XamChat.Core项目并导航到 添加 | 添加包。 -
使用搜索框搜索
Azure Mobile Services。 -
选择
Azure Mobile Services包,在撰写本书时,该包的版本为 1.2.5。 -
点击添加包。
-
对于
XamChat.iOS和XamChat.Android项目,重复此过程。每个平台都有一些特定平台的设置。
小贴士
如果你喜欢,你也可以从 Xamarin 组件商店获取 Azure 移动服务库。它与使用 NuGet 非常相似。
这将下载库并自动将其添加到你的项目中。NuGet 包管理器可能会发出警告,这些警告可以忽略。NuGet 最初是为 Windows 上的 Visual Studio 开发的,因此包含 PowerShell 脚本或要求许可协议的任何包可能会发出警告。
现在,让我们修改我们的AzureWebService.cs文件。将using Microsoft.WindowsAzure.MobileServices添加到文件顶部,然后进行以下更改:
public class AzureWebService : IWebService
{
MobileServiceClient client = new MobileServiceClient("https://your-service-name.azure-mobile.net/", "your-application-key");
// -- Existing code here --
}
确保你填写了你的移动服务名称和应用密钥。你可以在 Azure 管理门户的仪表板选项卡下的管理密钥部分找到你的密钥。
现在,让我们以下述方式实现我们的第一个方法Login:
public async Task<User> Login(string username, string password)
{
var user = new User
{
Username = username,
Password = password
};
await client.GetTable<User>().InsertAsync(user);
return user;
}
这相当简单,因为这款库的使用非常方便。GetTable<T>方法知道如何根据 C#类名使用名为User的表。在第一次调用时,动态模式功能将根据我们类的 C#属性创建两个新列,分别命名为Username和Password。请注意,InsertAsync方法还将填充用户的Id属性,以便在应用程序中稍后使用,因为我们将在对移动服务的未来调用中需要Id。
接下来,打开AppDelegate.cs文件来设置我们的新服务并添加以下代码:
//Replace this line
ServiceContainer.Register<IWebService>(() => new FakeWebService());
//With this line
ServiceContainer.Register<IWebService>(() => new AzureWebService());
此外,你还需要为 Azure 移动服务添加一些特定平台的设置。将using Microsoft.WindowsAzure.MobileServices添加到文件顶部,并在你的AppDelegate.cs文件中的FinishedLaunching底部添加以下代码行:
CurrentPlatform.Init();
现在,如果你在登录后编译并运行你的应用程序,你的应用程序应该能够成功调用 Azure 移动服务并插入一个新用户。导航到 Azure 管理门户中的 Azure 移动服务的数据选项卡,并选择User表。你将看到你刚刚插入的用户,如下面的截图所示:

注意
通常来说,在数据库中以明文形式存储密码是个坏主意。为了使事情更加安全,可以将它们存储为 MD5 散列。你应该能够在用于在User表上插入密码的自定义 JavaScript 中实现这一更改。有关保护 Windows Azure 应用程序的完整指南,请参阅msdn.microsoft.com/en-us/library/windowsazure/hh696898.aspx。
接下来,让我们创建一个新的类名为 Friend.cs。将其添加到紧挨着其他 Azure 特定类的 Azure 文件夹中,如下所示:
public class Friend
{
public string Id { get; set; }
public string MyId { get; set; }
public string UserId { get; set; }
public string Username { get; set; }
}
我们将使用这个类来存储每个用户的关于朋友的信息。请注意,我们还有一个 Id 属性,并且所有保存在 Azure 移动服务中的类都应该有一个名为 Id 的 string 属性。这将是 SQL 数据库中的表的主键。
接下来,让我们修改 Message 和 Conversation 类,为未来的推送通知做准备。向 Message 类添加一个新属性,如下所示:
public string ToId { get; set; }
然后,向 Conversation.cs 添加以下新属性:
public string MyId { get; set; }
在这里,我们需要为应用程序插入或生成一些测试数据,以便它能够正确运行。最简单的方法是从 C#中插入数据,所以让我们在我们的服务中实现以下简单方法来完成此操作:
public async Task LoadData()
{
var users = client.GetTable<User>();
var friends = client.GetTable<Friend>();
var me = new User
{
Username = "jonathanpeppers",
Password = "password"
};
var friend = new User
{
Username = "chucknorris",
Password = "password"
};
await users.InsertAsync(me);
await users.InsertAsync(friend);
await friends.InsertAsync(new Friend { MyId = me.Id, Username = friend.Username });
await friends.InsertAsync(new Friend { MyId = friend.Id, Username = me.Username });
}
接下来,让我们向 AppDelegate.cs 添加以下方法,并在 FinishedLaunching 中调用它:
private async void LoadData()
{
var service = ServiceContainer.Resolve<IWebService>() as AzureWebService;
await service.LoadData();
}
如果你在此时运行你的应用程序,它将插入两个用户并使他们彼此成为朋友。在这样做之前,让我们向 AzureWebService.cs 中的 LoadData 方法添加一些代码,以便插入对话和消息,如下所示:
var conversations = client.GetTable<Conversation>();
var messages = client.GetTable<Message>();
var conversation = new Conversation
{
MyId = me.Id,
UserId = friend.Id,
Username = friend.Username,
LastMessage = "HEY!"
};
await conversations.InsertAsync(conversation);
await messages.InsertAsync(new Message {
ConversationId = conversation.Id,
ToId = me.Id,
UserId = friend.Id, Username = friend.Username,
Text = "What's up?", Date = DateTime.Now.AddSeconds(-60)
});
await messages.InsertAsync(new Message {
ConversationId = conversation.Id,
ToId = friend.Id,
UserId = me.Id, Username = me.Username,
Text = "Not much", Date = DateTime.Now.AddSeconds(-30)
});
await messages.InsertAsync(new Message {
ConversationId = conversation.Id,
ToId = me.Id,
UserId = friend.Id, Username = friend.Username,
Text = "HEY!", Date = DateTime.Now
});
现在,如果你运行应用程序,它将使用一些良好的数据来初始化数据库。我建议你在第一次成功调用 LoadData 后删除对该方法的调用,也许在开发完成后完全删除该方法。
在继续之前,让我们实现 IWebService 接口的其余部分。它可以如下完成:
public async Task<User> Register(User user)
{
await client.GetTable<User>().InsertAsync(user);
return user;
}
public async Task<User[]> GetFriends(string userId)
{
var list = await client.GetTable<Friend>().Where(f => f.MyId == userId).ToListAsync();
return list.Select(f => new User { Id = f.UserId, Username = f.Username }).ToArray();
}
public async Task<User> AddFriend( string userId, string username)
{
var friend = new Friend { MyId = userId, Username = username };
await client.GetTable<Friend>().InsertAsync(friend);
return new User { Id = friend.UserId, Username = friend.Username };
}
这里每个方法都很简单。Register 与 Login 非常相似,但其他方法的主要复杂之处在于需要将 Friend 对象转换为 User。我们使用了 Azure 库中的 ToListAsync 方法来获取 List<T>;然而,由于我们的接口使用数组,我们很快将列表转换为数组。我们还利用了几个基本的 Linq 操作符,如 Where 和 Select,来完成我们对 IWebService 的实现。
现在,让我们完成与对话和消息相关的其他方法,如下所示:
public async Task<Conversation[]> GetConversations(string userId)
{
var list = await client.GetTable<Conversation>().Where(c => c.MyId == userId).ToListAsync();
return list.ToArray();
}
public async Task<Message[]> GetMessages(string conversationId)
{
var list = await client.GetTable<Message>().Where(m => m.ConversationId == conversationId).ToListAsync();
return list.ToArray();
}
public async Task<Message> SendMessage(Message message)
{
await client.GetTable<Message>().InsertAsync(message);
return message;
}
这就完成了我们对 IWebService 的实现。如果你在这个时候运行应用程序,它将像以前一样工作,唯一的区别是应用程序实际上是在与一个真实的 Web 服务器进行通信。新消息将被持久化到 SQL 数据库中,我们的自定义脚本将处理我们需要的自定义逻辑。请随意尝试我们的实现;你可能会发现一些与 Azure 移动服务配合得很好的功能,这些功能可以应用于你自己的应用程序。
在这一点上,另一个很好的练习是在我们的 Android 应用程序中设置 Azure 移动服务。为此,您只需添加 Azure 移动服务 NuGet 包。之后,您应该能够在Application类中的ServiceContainer.Register调用中替换,并调用CurrentPlatform.Init()。一切都将与 iOS 上完全一样。跨平台开发不是很好吗?
使用 Apple 推送通知服务
在 iOS 上使用 Azure 移动服务实现推送通知非常简单,从 Azure 后端的角度来看。最复杂的部分是处理 Apple 的创建证书和配置文件的过程,以便配置您的 iOS 应用程序。在我们继续之前,请确保您有一个有效的 iOS 开发者计划帐户,因为没有它,您将无法发送推送通知。如果您不熟悉推送通知的概念,请查看 Apple 的文档tinyurl.com/XamarinAPNS。
要发送推送通知,您需要设置以下内容:
-
已在 Apple 注册的显式 App ID
-
针对该 App ID 的配置文件
-
为您的服务器触发推送通知的证书
Apple 提供了开发和生产证书,您可以使用它们从您的服务器发送推送通知。
设置正确的配置文件
让我们从导航到developer.apple.com/account并执行以下步骤开始:
-
点击标识符链接。
-
点击窗口右上角的加号按钮。
-
为捆绑 ID 输入一个描述,例如
XamChat。 -
在显式 App ID部分下输入您的捆绑 ID。这应该与您在
Info.plist文件中设置的捆绑 ID 相匹配,例如,com.yourcompanyname.xamchat。 -
在App Services下,确保您已勾选推送通知。
-
现在,点击继续。
-
检查您的最终设置并点击提交。
这将创建一个类似于以下截图中的显式 App ID,我们可以用它来发送推送通知:

设置您的配置文件
对于推送通知,我们必须使用一个具有显式 App ID 的配置文件,该 App ID 不是开发证书。现在让我们设置一个配置文件:
-
在右侧面板下点击配置文件下的开发链接。
-
点击窗口右上角的加号按钮。
-
选择iOS 应用程序开发并点击继续。
-
选择我们刚刚创建的 App ID,然后点击继续。
-
选择开发者并点击继续。
-
选择您将使用的设备,然后点击继续。
-
为配置文件输入一个名称并点击生成。
-
下载配置文件并安装,或者打开XCode,通过导航到偏好设置 | 帐户来使用同步按钮。
完成后,你应该会到达一个类似以下截图的成功网页:

设置证书签名请求
然后,我们执行以下步骤来设置服务器需要的证书:
-
点击右侧面板中的证书下的开发链接。
-
点击右上角的加号按钮。
-
启用Apple Push Notifications 服务 SSL (沙盒)并点击继续。
-
如前所述,选择你的 App ID,然后点击继续。
-
根据苹果的说明创建一个新的证书签名请求。你也可以参考第七章, 在设备上部署和测试,或者定位到
*.certSigningRequest文件。 -
接下来,点击继续。
-
上传签名请求文件并点击生成。
-
接下来,点击下载。
-
打开文件以将证书导入到钥匙串。
-
在钥匙串中找到证书。它将被命名为Apple Development iOS Push Services,并包含你的 bundle ID。
-
右键单击证书,将其导出到你的文件系统中的某个位置。输入一个你将记住的密码。
这将创建我们需要的证书,以便从 Azure Mobile Services 向我们的用户发送推送通知。剩下的只是返回到 Azure 管理门户,并在Apple Push Notification Settings下的推送选项卡中上传证书,如图所示:

这次上传完成了我们从苹果方面需要的配置。
为推送通知进行客户端更改
接下来,让我们回到 Xamarin Studio 中的我们的XamChat.iOS项目,对客户端进行必要的推送通知更改。我们首先需要向共享代码中添加几个新类。
打开IWebService.cs并添加以下新方法:
Task RegisterPush(string userId, string deviceToken);
接下来,让我们在FakeWebService.cs中实现这个方法(只是为了编译),如下所示:
public async Task RegisterPush(string userId, string deviceToken)
{
await Sleep();
}
现在,让我们在Core/Azure文件夹中添加一个名为Device.cs的新类:
public class Device
{
public string Id { get; set;}
public string UserId { get; set; }
public string DeviceToken { get; set; }
}
最后,我们可以在AzureWebService.cs中实现实际的方法,如下所示:
public async Task RegisterPush( string userId, string deviceToken)
{
await client.GetTable<Device>().InsertAsync(new Device {
UserId = userId,
DeviceToken = deviceToken
});
}
对于 ViewModels,我们需要在LoginViewModel.cs中添加一个额外的新的方法:
public async Task RegisterPush(string deviceToken)
{
if (settings.User == null)
throw new Exception("User is null");
await service.RegisterPush(settings.User.Id, deviceToken);
}
然后,我们需要对MessageViewModel.cs进行一个小修改。在SendMessage方法中创建新的Message对象时,添加以下行:
ToId = Conversation.UserId,
这个修改完成了我们需要添加到共享代码中的内容。当我们添加推送通知到 Android 时,我们将重用这个新功能,所以请花时间在你的XamChat.Droid项目中链接新的Device.cs文件以构建整个解决方案。
现在,让我们添加我们需要的 iOS 平台特定代码。将以下方法添加到你的AppDelegate.cs文件中:
public async override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
var loginViewModel = ServiceContainer.Resolve<LoginViewModel>();
try
{
string token = deviceToken.Description;
token = token.Substring(1, token.Length - 2);
await loginViewModel.RegisterPush(token);
}
catch (Exception exc)
{
Console.WriteLine("Error registering push: " + exc);
}
}
public override void FailedToRegisterForRemoteNotifications(UIApplication application, NSError error)
{
Console.WriteLine("Error registering push: " + error.LocalizedDescription);
}
RegisteredForRemoteNotifications will occur when Apple successfully returns a device token from its servers. It is returned within angle brackets, so we do a little work to trim those off and pass the device token through LoginViewModel to Azure Mobile Services. We also implemented FailedToRegisterForRemoteNotifications just to report any errors that might occur throughout the process.
最后一件要做的事情是实际调用以注册远程通知。打开LoginController.cs文件,并在登录成功调用后直接添加以下代码行:
UIApplication.SharedApplication.RegisterForRemoteNotificationTypes(
UIRemoteNotificationType.Alert |
UIRemoteNotificationType.Badge |
UIRemoteNotificationType.Sound);
您也可以在启动时调用该方法;然而,在我们的情况下,我们需要一个有效的用户 ID 来存储在 Azure 的Device表中。
现在,让我们切换到 Azure 管理门户,并在服务器端使用 JavaScript 进行剩余的更改。在数据选项卡下,创建一个名为Device的新表,使用默认设置。
接下来,我们需要修改insert脚本,以确保不会插入重复的设备令牌:
function insert(item, user, request)
{
var devicesTable = tables.getTable('device');
devicesTable.where({ userId: item.UserId, deviceToken: item.DeviceToken }).read({ success: function (devices)
{
if (devices.length > 0)
{
request.respond(200, devices[0]);
}
else
{
request.execute();
}
}
});
}
最后但同样重要的是,我们需要修改Message表的insert脚本,以便向用户发送推送通知。消息发送如下:
function insert(item, user, request) {
request.execute();
var devicesTable = tables.getTable('device');
devicesTable.where({ userId : item.ToId }).read({
success: function(devices) {
devices.forEach(function(device) {
var text = item.Username + ": " + item.Text;
push.apns.send(device.DeviceToken, {
alert: text,
badge: 1,
payload: {
message: text
}
});
});
}
});
}
执行请求后,我们从数据库中检索设备列表,并为每个设备发送推送通知。为了测试推送通知,部署应用程序,并使用辅助用户(如果使用我们的示例:chucknorris)登录。登录后,您只需使用主页按钮将应用程序置于后台。接下来,在 iOS 模拟器上使用主要用户登录并发送消息。您应该会收到推送通知,如下面的截图所示:

实现 Google Cloud Messaging
由于我们已经在共享代码和 Azure 上设置了所需的所有内容,因此在此阶段设置 Android 的推送通知将变得工作量大大减少。要继续,您需要一个带有验证电子邮件地址的 Google 账户;然而,如果您有的话,我建议您使用注册了Google Play的账户。您可以参考关于Google Cloud Messaging(GCM)的完整文档,网址为developer.android.com/google/gcm。
注意
注意,Google Cloud Messaging 要求在 Android 设备上安装 Google Play,并且 Android 操作系统至少为 2.2 版本。
让我们从导航到cloud.google.com/console并执行以下步骤开始:
-
点击创建项目按钮。
-
输入一个适当的项目名称,例如
XamChat。 -
输入一个项目 ID;您可以使用生成的 ID。我更喜欢使用我的应用程序的 bundle ID,并用连字符替换点。
-
同意服务条款。
-
点击创建按钮。
-
当创建您的第一个项目时,您可能需要验证与您的账户关联的移动电话号码。
-
注意项目编号字段在概览页面。我们稍后会需要这个编号。
以下截图显示了概览选项卡:

现在我们可以继续以下设置:
-
点击左侧面板中的APIs & auth。
-
滚动并点击Google Cloud Messaging for Android。
-
点击顶部的关闭按钮以启用服务。您可能需要接受另一项协议。
-
点击左侧面板中的已注册应用。
-
点击顶部的注册应用按钮。
-
在应用名称字段中输入
XamChat并点击注册。您可以将平台选择保留在默认的Web 应用程序。 -
展开服务器密钥部分,并将API 密钥值复制到您的剪贴板。
-
切换到 Azure 管理门户,并导航到您的 Azure 移动服务实例中的推送选项卡。
-
在google cloud messaging 设置部分粘贴 API 密钥并点击保存。

接下来,让我们修改我们的 insert 脚本以支持 Android,如下所示:
function insert(item, user, request) {
request.execute();
var devicesTable = tables.getTable('device');
devicesTable.where({ userId : item.ToId }).read({
success: function(devices) {
devices.forEach(function(device) {
if (device.DeviceToken.length > 72) {
push.gcm.send(device.DeviceToken, {
title: item.Username,
message: item.Text,
});
}
else {
var text = item.Username + ": " + item.Text;
push.apns.send(device.DeviceToken, {
alert: text,
badge: 1,
payload: {
message: text
}
});
}
});
}
});
}
基本上,我们将超过 72 个字符的任何 deviceToken 值发送到 GCM。这是做这件事的一种简单方法,但您也可以向 Device 表中添加一个值,以指示设备是 Android 还是 iOS。GCM 还支持在通知区域发送自定义值,因此我们发送实际标题和消息。
这就完成了我们在 Azure 侧的设置。在 Android 应用程序中设置下一部分可能有点困难,因此我们将使用名为 PushSharp 的库来简化我们的实现。
首先,导航到 github.com/Redth/PushSharp 并执行以下步骤:
-
下载项目并将其放置在与您的 XamChat 解决方案相同的文件夹中。
-
将
PushSharp.Client.MonoForAndroid.Gcm项目添加到您的解决方案中。您可以在PushSharp.Client子目录中找到该项目。 -
从您的
XamChat.Droid项目引用新的项目。 -
如果尚未安装,您需要为 Android 2.2(API 8)安装Android SDK 平台。您可以从 Xamarin Studio 的工具菜单中启动的 Android SDK 管理器中安装此软件。
接下来,创建一个名为 PushConstants.cs 的新类,如下所示:
public static class PushConstants
{
public const string BundleId = "your-bundle-id";
public const string ProjectNumber = "your-project-number";
}
在 BundleId 值中填写您应用程序的包 ID,并在 ProjectNumber 值中填写在 Google Cloud Console 的概览页面找到的项目编号。
接下来,我们需要设置一些权限以支持我们应用程序中的推送通知。在此文件中的命名空间声明上方添加以下内容:
[assembly: Permission(
Name = XamChat.Droid.PushConstants.BundleId +
".permission.C2D_MESSAGE")]
[assembly: UsesPermission(
Name = XamChat.Droid.PushConstants.BundleId +
".permission.C2D_MESSAGE")]
[assembly: UsesPermission(
Name = "com.google.android.c2dm.permission.RECEIVE")]
[assembly: UsesPermission(
Name = "android.permission.GET_ACCOUNTS")]
[assembly: UsesPermission(
Name = "android.permission.INTERNET")]
[assembly: UsesPermission(
Name = "android.permission.WAKE_LOCK")]
您也可以在我们的 AndroidManifest.xml 文件中做出这些更改;然而,使用 C# 属性可能更好,因为它在键入时提供了代码自动完成的特性。
接下来,创建另一个名为 PushReceiver.cs 的新类,如下所示:
[BroadcastReceiver(
Permission = GCMConstants.PERMISSION_GCM_INTENTS)]
[IntentFilter(
new string[] { GCMConstants.INTENT_FROM_GCM_MESSAGE },
Categories = new string[] { PushConstants.BundleId })]
[IntentFilter(
new string[] {
GCMConstants.INTENT_FROM_GCM_REGISTRATION_CALLBACK },
Categories = new string[] { PushConstants.BundleId })]
[IntentFilter(
new string[] {
GCMConstants.INTENT_FROM_GCM_LIBRARY_RETRY },
Categories = new string[] { PushConstants.BundleId })]
public class PushReceiver :
PushHandlerBroadcastReceiverBase<PushHandlerService>
{ }
PushReceiver.cs 类设置了 BroadcastReceiver,这是 Android 中不同应用程序之间通信的本地方式。有关此主题的更多信息,请参阅 Android 文档中的 developer.android.com/reference/android/content/BroadcastReceiver.html。
接下来,创建一个名为 PushService.cs 的最后一个类,如下所示:
[Service]
public class PushHandlerService : PushHandlerServiceBase
{
public PushHandlerService() : base (PushConstants.ProjectNumber)
{ }
}
现在,右键单击 PushHandlerServiceBase 并导航到 重构 | 实现抽象成员。接下来,让我们逐个实现每个成员:
protected async override void OnRegistered (Context context, string registrationId)
{
Console.WriteLine("Push successfully registered!");
var loginViewModel = ServiceContainer.Resolve<LoginViewModel>();
try
{
await loginViewModel.RegisterPush(registrationId);
}
catch (Exception exc)
{
Console.WriteLine("Error registering push: " + exc);
}
}
上述代码与我们之前在 iOS 上所做的是非常相似的。我们只需将 registrationId 值发送到 loginViewModel。
接下来,当收到消息时,我们必须编写以下代码:
protected override void OnMessage (Context context, Intent intent)
{
//Pull out the notification details
string title = intent.Extras.GetString("title");
string message = intent.Extras.GetString("message");
//Create a new intent
intent = new Intent(this, typeof(ConversationsActivity));
//Create the notification
var notification = new Notification(Android.Resource.Drawable.SymActionEmail, title);
notification.Flags = NotificationFlags.AutoCancel;
notification.SetLatestEventInfo(this,
new Java.Lang.String(title),
new Java.Lang.String(message), PendingIntent.GetActivity(this, 0, intent, 0));
//Send the notification through the NotificationManager
var notificationManager = GetSystemService(Context.NotificationService) as NotificationManager;
notificationManager.Notify(1, notification);
}
这段代码实际上会从通知中提取值,并在 Android 设备的通知中心显示它们。我们使用了内置资源 SymActionEmail 来在通知中显示电子邮件图标。
接下来,我们只需要实现两个更多的抽象方法。目前,我们可以简单地使用 Console.WriteLine 来报告以下事件:
protected override void OnUnRegistered(Context context, string registrationId)
{
Console.WriteLine("Push unregistered!");
}
protected override void OnError (Context context, string errorId)
{
Console.WriteLine("Push error: " + errorId);
}
在将来,当调用 OnUnRegistered 时,你应该考虑从 Azure 的 Device 表中移除注册信息。偶尔,用户的 registrationId 会发生变化,因此这是你的应用程序被通知这一变化的地方。
接下来,打开 Application.cs 并将以下行添加到 OnCreate 的末尾:
PushClient.CheckDevice(this);
PushClient.CheckManifest(this);
接下来,打开 LoginActivity.cs 并在成功登录后添加以下行:
PushClient.Register(this, PushConstants.ProjectNumber);
现在,如果你重复在 iOS 上测试推送通知的步骤,你应该能够向我们的 Android 应用发送推送通知。更好的是,你应该能够跨平台发送推送通知,因为 iOS 用户可以向 Android 用户发送消息。

摘要
在本章中,我们介绍了 Windows Azure 提供的内容:基础设施即服务和平台即服务。我们设置了一个免费的 Windows Azure 账户并设置了一个 Azure 移动服务实例。接下来,我们创建了所有必要的表来存储我们的数据,并编写了一些脚本将业务逻辑添加到 Web 服务中。我们实现了客户端代码以对 Azure 移动服务进行请求。最后,我们使用 Apple 推送通知服务和 Google 云消息传递为 iOS 和 Android 实现了推送通知。
使用 Azure 移动服务,我们能够避免编写大量的服务器端代码——主要是几个简单的脚本。如果不利用 Azure 的功能来实现推送通知,这将是一项相当具有挑战性的工作。在下一章中,我们将探讨如何使用 Xamarin 与第三方库。这包括从 Xamarin 组件商店到使用原生 Objective-C 或 Java 库的所有内容。
第九章. 第三方库
Xamarin 支持.NET 框架的一个子集,但就大部分而言,它包括了您在.NET 基类库中期望的所有标准 API。因此,大量 C#的开源库可以直接在 Xamarin 项目中使用。此外,如果一个开源项目没有 Xamarin 或可移植类库版本,将代码移植到 Xamarin 项目中通常非常直接。Xamarin 还支持调用原生 Objective-C 和 Java 库,因此我们将探讨这些作为重用现有代码的额外手段。
在本章中,我们将介绍以下内容:
-
Xamarin 组件商店
-
NuGet
-
端口现有 C#库
-
Objective-C 绑定
-
Java 绑定
Xamarin 组件商店
向您的项目添加第三方组件的主要和明显方式是通过 Xamarin 组件商店。组件商店与稍后我们将要介绍的NuGet相当类似,但组件商店还包含了一些非免费的付费组件。所有 Xamarin 组件都必须包括完整的示例项目和入门指南,而 NuGet 包本身并不提供文档。
所有Xamarin.iOS和Xamarin.Android项目都包含一个Components文件夹。要开始,只需右键单击文件夹并选择获取更多组件以启动商店对话框,如图下截图所示:

在撰写本书时,有超过 200 个组件可供增强您的 iOS 和 Android 应用程序。这是一个寻找在 Xamarin 应用程序中使用最常见组件的好地方。每个组件都附带完整的艺术作品。在购买高级组件之前,您可能需要演示视频、评论和其他信息。
最常见的组件
最知名和最有用的组件如下:
-
Json.NET: 这是使用 C#解析和序列化 JSON 的事实标准
-
RestSharp: 这是一个常用的简单 REST 客户端,用于.NET
-
SQLite.NET: 这是一个简单的对象关系映射(ORM),用于在移动应用程序中处理本地 SQLite 数据库
-
Facebook SDK: 这是 Facebook 提供的标准 SDK,用于将其服务集成到您的应用程序中
-
Xamarin.Mobile: 这是一个跨平台库,使用通用 API 访问您的设备联系人、GPS、照片库和相机
-
ActionBarSherlock: 这是一个强大的 Android
ActionBar替代品
注意,其中一些库是原生 Java 或 Objective-C 库,而另一些则是纯 C#。Xamarin 从头开始构建以支持调用原生库,因此组件商店提供了许多 Objective-C 或 Java 开发者在开发移动应用程序时可能会使用的常见库。
你还可以将你自己的组件提交到组件商店。如果你有一个有用的开源项目或者只是想赚一些额外的钱,创建一个组件很简单。我们不会在本书中介绍它,但你可以在components.xamarin.com/submit找到有关此主题的完整文档,如下面的截图所示:

端口现有 C#库
尽管 Xamarin 正变得越来越受欢迎,但许多开源.NET 库在支持Xamarin.iOS和Xamarin.Android方面显然还跟不上。然而,在这些情况下,你绝对不会失望。通常,如果库有 Silverlight 或 Windows Phone 版本,你只需创建一个 iOS 或 Android 类库,并添加文件,无需进行代码更改。
为了帮助这个过程,Xamarin 创建了一个在线服务工具,用于扫描你的现有代码并确定库距离可移植性的程度。导航到scan.xamarin.com,上传任何*.exe或*.dll文件以分析其方法进行跨平台开发。扫描过程完成后,你将获得一个端口百分比报告(你的组件/应用程序可以移植到所有平台:Android、iOS、Windows Phone 和 Windows Store)。
以下截图是Lucene .NET 客户端库的示例报告:

如果库的可移植性运行在较高比例,那么将库端口到 Android 或 iOS 应该相对容易。在大多数情况下,将库端口到 Xamarin 甚至可能比 Windows Phone 或 WinRT 更容易。
为了说明这个过程,让我们将一个没有 Xamarin 或可移植类库支持的开放源代码项目进行端口。我选择了一个名为Ninject的依赖注入库,因为它很有用,并且与忍者有关。你可以在www.ninject.org了解更多关于这个库的信息。
让我们开始设置库以与 Xamarin 项目一起使用,如下所示:
-
首先,从
github.com/ninject/Ninject下载 Ninject 的源代码。 -
在 Xamarin Studio 中打开
Ninject.sln。 -
添加一个名为
Ninject.iOS的新iOS 库项目。 -
将
Ninject主项目中的所有文件链接起来。确保你使用添加现有文件夹对话框来加快这个过程。
小贴士
如果你不太熟悉 GitHub,我建议你下载位于mac.github.com的 Mac 桌面客户端。
现在尝试构建Ninject.iOS项目;你将在名为DynamicMethodFactory.cs的文件中遇到几个编译错误,如下面的截图所示:

打开DynamicMethodFactory.cs并注意文件顶部的以下代码:
#if !NO_LCG
#region Using Directivesusing System;
using System.Reflection;
using System.Reflection.Emit;
using Ninject.Components;
#endregion
/// *** File contents here ***
#endif
由于苹果平台限制,无法在 iOS 上使用 System.Reflection.Emit。幸运的是,库编写者创建了一个名为 NO_LCG(代表轻量级代码生成)的预处理器指令,以允许库在不支持 System.Reflection.Emit 的平台上运行。
要修复我们的 iOS 项目,请打开项目选项并导航到构建 | 编译器部分。在配置下拉菜单中,将 NO_LCG 添加到定义符号字段中的调试和发布。点击确定以保存您的更改。注意,现在整个文件在 Xamarin Studio 中以浅灰色突出显示,如以下截图所示。这意味着代码将不会被编译。

如果你现在编译项目,它将成功完成,并创建一个Ninject.iOS.dll文件,你可以从任何Xamarin.iOS项目中引用它。你也可以直接引用Ninject.iOS项目,而不是使用*.dll文件。
在这一点上,你可能希望重复此过程以创建一个Xamarin.Android类库项目。幸运的是,Xamarin.Android支持System.Reflection.Emit,因此如果你愿意,可以跳过添加额外的预处理器指令。
Objective-C 绑定
Xamarin 开发了一个复杂的系统,可以在 iOS 项目中从 C# 调用原生 Objective-C 库。Xamarin.iOS 的核心使用相同的技术调用 UIKit、CoreGraphics 和其他 iOS 框架中的原生 Apple API。开发者可以创建 iOS 绑定项目,通过简单的接口和属性将 Objective-C 类和方法暴露给 C#。
为了帮助创建 Objective-C 绑定,Xamarin 创建了一个名为 Objective Sharpie 的小工具,可以为你处理 Objective-C 头文件,并将有效的 C# 定义导出到绑定项目中。这个工具是大多数绑定的一个很好的起点,它将你的绑定完成到三分之四,在许多情况下,你将需要手动编辑和微调以使其更符合 C#。
例如,我们将为 iOS 的 Google Analytics 库编写一个绑定。这是一个简单而有用的库,可以跟踪你的 iOS 或 Android 应用程序中的用户活动。在撰写本文时,Google Analytics SDK 的版本为 3.10,因此随着新版本的发布,一些这些说明可能会发生变化。
使用 Objective Sharpie
首先,从 tinyurl.com/ObjectiveSharpie 下载并安装 Objective Sharpie,然后执行以下步骤:
-
下载 iOS 可用的最新 Google Analytics SDK,链接为
tinyurl.com/GoogleAnalyticsForiOS。 -
创建一个名为
GoogleAnalytics.iOS的新 iOS 绑定项目。 -
运行 Objective Sharpie。
-
将 目标 SDK 设置为 iOS 7.1 并点击 下一步。
-
添加 Google Analytics SDK 中包含的所有头文件(
*.h);您可以在下载的Library文件夹中找到这些文件。点击下一步。 -
选择一个合适的命名空间,例如
GoogleAnalytics,然后点击生成。 -
将生成的
ApiDefinition.cs文件复制到您的 iOS 绑定项目中。 -
几秒钟后,您的 C#文件将被生成。点击退出。
在整个过程中,您不应该从 Objective Sharpie 收到任何错误消息,完成之后,您的屏幕应该看起来像下面的截图:

小贴士
在编写这本书的时候,Objective Sharpie 与 Xcode 6.0 及以上版本不兼容。如果您遇到这个问题,我建议您下载 Xcode 5.1.1。您可以通过在 Finder 中重命名现有版本并安装第二个版本来并排安装两个版本的 Xcode。您可以在developer.apple.com/downloads/index.action找到旧的 Xcode 下载。
现在如果您回到您的绑定项目中,您会注意到 Objective Sharpie 已经为库的头文件中发现的每个类生成了一个接口定义。它还生成了许多库使用的enum值,并在可能的情况下更改了大小写和命名约定以更接近 C#。
当您阅读绑定时,您会注意到几个 C#属性,它们定义了 Objective-C 库的不同方面,如下所示:
-
BaseType:这声明了一个接口作为 Objective-C 类。将基类(也称为超类)传递给属性。如果没有基类,应使用NSObject。 -
Export:这声明了一个 Objective-C 类上的方法或属性。传递一个字符串,将 Objective-C 名称映射到 C#名称。Objective-C 方法名称通常以下列形式:myMethod:someParam:someOtherParam。 -
Static:这标记一个方法或属性在 C#中为static。 -
Bind:这用于属性,将获取器或设置器映射到不同的 Objective-C 方法。Objective-C 属性可以重命名属性的获取器或设置器。 -
NullAllowed:这允许将null传递给方法或属性。默认情况下,如果发生这种情况,会抛出异常。 -
Field:这声明了一个 Objective-C 字段,在 C#中作为公共变量公开。 -
Model:这标识了一个类,让Xamarin.iOS有可选项覆盖的方法。这通常用于 Objective-C 的代理。 -
Internal:这使用 C#的internal关键字标记生成的成员。它可以用来隐藏您不想公开给外部世界的某些成员。 -
Abstract:这标识了一个 Objective-C 方法为必需的,与Model一起使用。在 C#中,它将生成一个抽象方法。
唯一需要知道的其他规则是如何定义构造函数。由于 C# 接口不支持构造函数,Xamarin 必须发明一个约定。
要定义除了默认构造函数之外的构造函数,请使用以下代码:
[Export("initWithFrame:")]
IntPtr Constructor(RectangleF frame);
这将为类定义一个接受 RectangleF 作为参数的构造函数。方法名 Constructor 和返回类型 IntPtr 会让 Xamarin 编译器生成一个构造函数。
现在,让我们回到我们的绑定项目,完成所有设置。如果你现在编译项目,你会得到一些编译错误。让我们逐一修复它们,如下所示:
-
将项目的默认命名空间更改为
GoogleAnalytics。此设置在项目选项中,通过导航到 General | Main Settings 可以找到。 -
将 SDK 下载中的
libGoogleAnalyticsServices.a添加到项目中。 -
在
ApiDefinition.cs文件顶部添加using语句,包括MonoTouch.Foundation、MonoTouch.UIKit和MonoTouch.ObjCRuntime。 -
删除
GAILogLevel的多重重复声明。你可能还希望将枚举移动到StructsAndEnums.cs文件中。 -
删除
GAIErrorCode的声明。 -
在
GAIDictionaryBuilder的SetAll方法中,将params参数重命名为parameters,因为params是 C# 中的一个保留字。 -
删除
GAILogger、GAITracker、GAITrackedViewController和你找到的任何其他重复类。 -
检查任何
Field声明,并将[Field("Foobar")]改为[Field("Foobar", "__Internal")]。这告诉编译器字段的位置;在这种情况下,它将包含在我们的绑定项目中。 -
删除所有
Verify属性。这些是 Objective Sharpie 在执行操作时不确定的地方。在我们的例子中,它们都很好,所以可以安全地删除它们。
还有一个错误是关于 Objective Sharpie 无法为具有回调的方法生成 C# 委托。导航到 GAI 接口并更改以下方法:
[Export ("dispatchWithCompletionHandler:")]void DispatchWithCompletionHandler (
GAIDispatchResultHandler completionHandler);
你还需要在文件顶部定义以下委托:
public delegate void GAIDispatchResultHandler(
GAIDispatchResult result);
经过这些问题后,你应该能够编译绑定并得到没有错误。你可以阅读 Objective-C 头文件并手动编写定义;然而,使用 Objective Sharpie 通常意味着工作量会少很多。
在这一点上,如果你尝试在一个 iOS 项目中使用库,你会得到如下错误:
Error MT5210: Native linking failed, undefined symbol:
_FooBar. Please verify that all the necessary frameworks
have been referenced and native libraries are properly
linked in.
我们需要定义 Objective-C 库使用的其他框架和库。这与 C# 中的引用工作非常相似。如果我们审查 Google Analytics 文档,它说必须添加 CoreData、SystemConfiguration 和 libz.dylib。此外,你必须添加对 AdSupport 的弱引用。
打开自动创建在 *.a 文件下嵌套的 libGoogleAnalyticsServices.linkwith.cs,并做出以下更改:
[assembly: LinkWith ("libGoogleAnalyticsServices.a",
LinkTarget.ArmV7 | LinkTarget.ArmV7s | LinkTarget.Simulator,
LinkerFlags = "-lz",
Frameworks = "CoreData SystemConfiguration",
WeakFrameworks = "AdSupport",
ForceLoad = true)]
我们以以下方式添加了对框架的引用:
-
框架:将它们添加到
LinkWith属性的Frameworks值中,用空格分隔。 -
弱框架:以相同的方式将它们添加到
LinkWith属性的WeakFrameworks属性中。弱框架是如果找不到可以忽略的库。在这种情况下,AdSupport是在 iOS 6 中添加的;然而,这个库仍然可以在旧版本的 iOS 上工作。 -
动态库:例如
libz.dylib这样的库可以在LinkerFlags中声明。通常,你删除.dylib扩展名,并将lib替换为–l。
实施这些更改后,你将能够从 iOS 项目中成功使用库。有关 Objective-C 绑定的完整文档,请访问 Xamarin 文档网站docs.xamarin.com/ios。
Java 绑定
与 iOS 类似,Xamarin 通过Xamarin.Android提供了对从 C#调用 Java 库的全面支持。原生 Android SDKs 以这种方式运行,开发者可以利用Android Java Bindings项目在 C#中利用其他原生 Java 库。这里的主要区别是,与 Objective-C 绑定相比,手动要做的事情很少。Java 语法与 C#非常相似,因此许多映射是一对一的。此外,Java 在其库中包含元数据信息,Xamarin 使用这些信息自动生成调用 Java 所需的 C#代码。
作为一个例子,让我们为 Google Analytics SDK 的 Android 版本创建一个绑定。在我们开始之前,从tinyurl.com/GoogleAnalyticsForAndroid下载 SDK。在撰写本文时,Android SDK 的版本是 3.01,因此这些说明可能会随着时间的推移而改变。
让我们按照以下步骤开始创建 Java 绑定:
-
在 Xamarin Studio 中启动一个新的
Android Java Bindings Library项目。如果你愿意,可以使用我们为 iOS 所做的相同解决方案。 -
将项目命名为
GoogleAnalytics.Droid。 -
将 Android SDK 中的
libGoogleAnalyticsServices.jar添加到项目的Jars文件夹下。默认情况下,文件的构建操作将是EmbeddedJar。这会将 jar 文件打包到 DLL 中,这是使用最方便的选项。 -
构建项目。你将得到一些错误,我们将在稍后解决。
大部分时间你花费在 Java 绑定上的工作将是修复那些阻止生成的 C#代码编译的小问题。不要担心;许多库在第一次尝试时无需任何更改即可正常工作。一般来说,Java 库越大,你需要做的工作就越多,才能从 C#中使其正常工作。
以下是你可能会遇到的问题类型:
-
Java 混淆:如果库通过混淆工具(如ProGuard)运行,类和方法名称可能不是有效的 C#名称。
-
协变返回类型:Java 在重写虚拟方法中的返回类型规则与 C#不同。因此,你可能需要修改生成的 C#代码的返回类型才能编译。
-
可见性:Java 的可访问性规则与 C#不同;子类中方法的可见性可以改变。有时,你可能需要更改 C#中的可见性才能使其编译。
-
命名冲突:有时,C#代码生成器可能会出错,生成具有相同名称的两个成员或类。
-
Java 泛型:Java 中泛型类的使用往往会在 C#中引起问题。
因此,在我们开始解决 Java 绑定中的这些问题之前,让我们首先清理项目中的命名空间。Java 命名空间默认为com.mycompany.mylibrary,所以让我们将定义更改为更接近 C#。在项目的Transforms目录中,打开Metadata.xml并在根元数据节点内添加以下 XML 标签:
<attr path="/api/package[@name='com.google.analytics.tracking
.android']" name="managedName">GoogleAnalytics.Tracking</attr>
attr节点告诉 Xamarin 编译器需要在 Java 定义中替换什么值。在这种情况下,我们正在将包的managedName替换为GoogleAnalytics.Tracking,因为它在 C#中会更有意义。路径值可能看起来有点奇怪,这是因为它使用了一个名为XPath的 XML 匹配查询语言。一般来说,只需将其视为 XML 的模式匹配查询即可。有关 XPath 语法的完整文档,请查看网上许多资源,例如w3schools.com/xpath。
到目前为止,你可能正在问自己,XPath 表达式是在匹配什么?回到 Xamarin Studio,在顶部的解决方案上右键单击。导航到显示选项 | 显示所有文件。在obj/Debug文件夹下打开api.xml。这是描述 Java 库中所有类型和方法的 Java 定义文件。如果你注意到,这里的 XML 直接关联到我们将要编写的 XPath 表达式。
在我们的下一步中,让我们删除所有我们在这个库中不打算使用的包(或命名空间)。对于大型库来说,这通常是一个好主意,因为你不想浪费时间修复你甚至不会从 C#中调用的库的部分问题。请注意,这实际上并不会删除 Java 代码;它只是阻止从 C#生成任何调用它的 C#声明。
在Metadata.xml中添加以下声明:
<remove-node
path="/api/package[@name='com.google.analytics
.containertag.common']" />
<remove-node
path="/api/package[@name='com.google.analytics
.containertag.proto']" />
<remove-node
path="/api/package[@name='com.google.analytics
.midtier.proto.containertag']" />
<remove-node
path="/api/package[@name='com.google.android
.gms.analytics.internal']" />
<remove-node
path="/api/package[@name='com.google.android
.gms.common.util']" />
<remove-nodepath="/api/package[@name='com.google.tagmanager']" />
<remove-node
path="/api/package[@name='com.google.tagmanager.proto']" />
<remove-node
path="/api/package[@name='com.google.tagmanager.protobuf.nano']" />
现在当你构建库时,我们可以开始解决这些问题。你将收到的第一个错误可能如下所示:
GoogleAnalytics.Tracking.GoogleAnalytics.cs(74,74):
Error CS0234: The type or namespace name 'TrackerHandler'
does not exist in the namespace 'GoogleAnalytics.Tracking'.
Are you missing an assembly reference?
如果我们在api.xml文件中定位到TrackerHandler,我们将看到以下类声明:
<class
abstract="true" deprecated="not deprecated"
extends="java.lang.Object"
extends-generic-aware="java.lang.Object"
final="false" name="TrackerHandler"
static="false" visibility=""/>
那么,你能找到问题吗?我们需要填写visibility XML 属性,但不知何故它是空的。将以下行添加到Metadata.xml中:
<attr
path="/api/package[@name='com.google.analytics
.tracking.android']/class[@name='TrackerHandler']"
name="visibility">public</attr>
这个 XPath 表达式将在com.google.analytics.tracking.android包内找到TrackerHandler类,并将visibility改为public。
如果你现在构建项目,它将成功完成,但有一个警告。在 Java 绑定项目中,修复警告是一个好主意,因为它们通常表明某个类或方法被省略了。注意以下警告:
GoogleAnalytics.Droid: Warning BG8102:
Class GoogleAnalytics.Tracking.CampaignTrackingService has
unknown base type android.app.IntentService (BG8102)
(GoogleAnalytics.Droid)
要解决这个问题,找到api.xml中CampaignTrackingService的类型定义,如下所示:
<class
abstract="false" deprecated="not deprecated"
extends="android.app.IntentService"
extends-generic-aware="android.app.IntentService"
final="false" name="CampaignTrackingService"
static="false" visibility="public">
解决这个问题的方法是将基类改为Xamarin.Android定义的IntentService。将以下代码添加到Metadata.xml中:
<attr
path="/api/package[@name='com.google.analytics
.tracking.android']/class[@name='CampaignTrackingService']"
name="extends">mono.android.app.IntentService</attr>
这将extends属性更改为使用Mono.Android.dll中找到的IntentService。我通过在 Xamarin Studio 的Assembly Browser中打开Mono.Android.dll来找到这个类的 Java 名称。让我们看一下下面的截图中的Register属性:

要检查 Xamarin Studio 中的*.dll文件,你只需打开它们。你还可以在你的项目中的References文件夹中双击任何程序集。
如果你现在构建绑定项目,我们将剩下最后一个错误,如下所示:
GoogleAnalytics.Tracking.CampaignTrackingService.cs(24,24):
Error CS0507:
'CampaignTrackingService.OnHandleIntent(Intent)':
cannot change access modifiers when overriding 'protected'
inherited member
'IntentService.OnHandleIntent(Android.Content.Intent)'
(CS0507) (GoogleAnalytics.Droid)
如果你导航到api.xml文件,你可以看到OnHandleIntent的定义如下:
<method
abstract="false" deprecated="not deprecated" final="false"
name="onHandleIntent" native="false" return="void"
static="false" synchronized="false" visibility="public">
我们可以看到,这个类的 Java 方法是public的,但基类是protected。因此,最好的办法是将 C#版本也改为protected。编写一个匹配这个的 XPath 表达式稍微复杂一些,但幸运的是,Xamarin 有一个简单的方法来检索它。如果你在 Xamarin Studio 的Errors面板中双击错误消息,你将在生成的 C#代码中看到以下注释:
// Metadata.xml XPath method reference:
path="/api/package[@name='com.google.analytics
.tracking.android']/class[@name='CampaignTrackingService']
/method[@name='onHandleIntent' and count(parameter)=1 and
parameter[1][@type='android.content.Intent']]"
将此值复制到path,并在Metadata.xml中添加以下内容:
<attr path="/api/package[@name='com.google.analytics
.tracking.android']/class[@name='CampaignTrackingService']
/method[@name='onHandleIntent' and count(parameter)=1 and
parameter[1][@type='android.content.Intent']]"
name="visibility">protected</attr>
现在,我们可以构建项目,得到零错误和零警告。库现在可以在你的Xamarin.Android项目中使用了。
然而,如果你开始使用这个库,请注意方法参数的名称是p0、p1、p2等等。以下是EasyTracker类的一些方法定义:
public static EasyTracker GetInstance(Context p0);
public static void SetResourcePackageName(string p0);
public virtual void ActivityStart(Activity p0);
public virtual void ActivityStop(Activity p0);
你可以想象,如果不了解正确的参数名称,消费 Java 库会有多困难。参数被这样命名的原因是,Java 库的元数据不包括设置每个参数正确名称的信息。因此,Xamarin.Android做了它能做的最好的事情,并为每个参数按顺序自动命名。
要重命名这个类中的参数,我们可以在Metadata.xml中添加以下内容:
<attr path="/api/package[@name='com.google.analytics
.tracking.android']/class[@name='EasyTracker']
/method[@name='getInstance']/parameter[@name='p0']"
name="name">context</attr>
<attr path="/api/package[@name='com.google.analytics
.tracking.android']/class[@name='EasyTracker']
/method[@name='setResourcePackageName']/parameter[@name='p0']"
name="name">packageName</attr>
<attr path="/api/package[@name='com.google.analytics
.tracking.android']/class[@name='EasyTracker']
/method[@name='activityStart']/parameter[@name='p0']"
name="name">activity</attr>
<attr path="/api/package[@name='com.google.analytics
.tracking.android']/class[@name='EasyTracker']
/method[@name='activityStop']/parameter[@name='p0']"
name="name">activity</attr>
在重建绑定项目时,这将有效地重命名EasyTracker类中这四个方法的参数。此时,我建议您检查您计划在应用程序中使用的类,并重命名参数,这样对您来说会更有意义。您可能需要参考谷歌分析文档以获取正确的命名。幸运的是,SDK 中包含一个javadocs.zip文件,它为库提供了 HTML 参考。
要全面了解实现 Java 绑定的信息,请确保查看 Xamarin 的文档网站docs.xamarin.com/android。在为谷歌分析库创建绑定时,我们遇到的场景肯定比我们遇到的更复杂。
摘要
在本章中,我们将 Xamarin 组件商店的库添加到 Xamarin 项目中,并将现有的 C#库 Ninject 移植到Xamarin.iOS和Xamarin.Android。接下来,我们安装了 Objective Sharpie 并探讨了其使用方法以生成 Objective-C 绑定。最后,我们为 iOS 的谷歌分析 SDK 编写了一个功能性的 Objective-C 绑定,为 Android 的谷歌分析 SDK 编写了一个 Java 绑定。我们还编写了几个 XPath 表达式来清理 Java 绑定。
有几种方法可以从您的Xamarin.iOS和Xamarin.Android应用程序中使用现有的第三方库。我们探讨了从使用 Xamarin 组件商店、移植现有代码到设置 Java 和 Objective-C 库以便从 C#中使用的一切。在下一章中,我们将介绍Xamarin.Mobile库,作为访问用户联系人、相机和 GPS 位置的方式。
第十章. 联系人、相机和位置
当今移动应用程序中使用的许多关键功能都基于我们设备可以收集的新类型数据。例如,GPS 位置和相机是 Instagram 或 Twitter 等现代应用程序的必备功能。开发一个应用程序而不使用这些原生功能是很困难的。因此,让我们探索使用 Xamarin 利用这些功能的方法。
在本章中,我们将做以下事情:
-
介绍 Xamarin.Mobile 库
-
在 Android 和 iOS 上读取通讯录
-
获取我们设备的 GPS 位置
-
从相机和照片库中拉取照片
介绍 Xamarin.Mobile
为了简化跨多个平台开发这些功能,Xamarin 开发了一个名为Xamarin.Mobile的库。它提供了一个单一的 API 来访问 iOS、Android 甚至 Windows 平台上的联系人、GPS 位置、屏幕航向、相机和照片库。它还利用任务并行库(TPL)来提供一个现代的 C# API,这将使我们的开发者比他们的本地替代品更有效率。这使您能够使用 C#中的async和await关键字编写优雅、干净的异步代码。您还可以在 iOS 和 Android 上重用相同的代码,除了 Android 平台所需的少数差异。
要安装 Xamarin.Mobile,请打开Xamarin 组件商店,并在Xamarin Studio中添加Xamarin.Mobile组件,如图所示。您将使用以下功能(组件的功能):

在我们进一步探讨使用 Xamarin.Mobile 之前,让我们回顾一下库中可用的命名空间和功能:
-
Xamarin.Contacts:这个库包含了一些类,使您能够与完整的通讯录进行交互。它包括联系人的照片、电话号码、地址、电子邮件、网站等内容。 -
Xamarin.Geolocation:结合加速度计,它为您提供设备 GPS 位置的信息,包括海拔、航向、经度、纬度和速度。您可以显式跟踪设备的位置,或者监听 GPS 位置随时间的变化。 -
Xamarin.Media:这个库允许访问设备的相机(如果有多个的话)和内置的照片库。这是向任何应用程序添加照片选择功能的一种简单方法。
要获取 Xamarin.Mobile 的完整文档,请访问包含组件商店的 API 文档,网址为componentsapi.xamarin.com。您也可以在 Xamarin Studio 中查看它,通过在查看组件时点击打开 API 文档。
Xamarin.Mobile 也是一个开源项目,拥有标准的 Apache 2.0 许可证。您可以为项目做出贡献或向 GitHub 页面github.com/xamarin/Xamarin.Mobile提交问题。您可以在您的应用程序中使用 Xamarin.Mobile,也可以将其分叉,并根据自己的需求进行修改。
访问联系人
要开始探索 Xamarin.Mobile 提供的内容,让我们在 Xamarin 应用程序中访问地址簿。对于 iOS,第一步是通过导航到iOS | iPhone Storyboard来创建一个单视图应用程序项目。确保您从组件商店将 Xamarin.Mobile 添加到项目中。
现在,让我们实现一个简单的UITableView,其中包含联系人列表:
-
打开
MainStoryboard.storyboard文件。删除由项目模板创建的任何现有控制器。 -
创建一个以
UITableViewController作为其根子控制器的UINavigationController。 -
通过在 iOS 设计器中导航到属性 | 小部件,将
UITableViewController的类设置为ContactsController。 -
保存故事板文件并返回到 Xamarin Studio。
打开自动生成的ContactsController.cs文件,并开始实现表格视图。在文件顶部添加using Xamarin.Contacts;,并对控制器进行以下更改:
public partial class ContactsController :UITableViewController, IUITableViewDataSource
{
public ContactsController (IntPtr handle) : base (handle)
{
Title = "Contacts";
}
}
我们填写了导航栏的标题"Contacts",并将其类设置为实现IUITableViewDataSource。这是 Xamarin 创建的一种新类型接口,用于简化从 C#使用 Objective-C 协议。这与我们在前面的章节中创建继承自UITableViewSource的类完全相同,但您也可以从控制器中完成。Xamarin 在这里做了一些技巧。他们创建了一个具有可选实现方法的接口,这是 C#不支持的功能。这种类型的接口可以通过减少对新类的需求来使您的代码更加简洁,这对于非常简单的控制器来说是非常好的。
接下来,让我们添加一些代码来加载联系人:
Contact[] contacts;
public async override void ViewDidLoad()
{
base.ViewDidLoad();
try
{
var book = new AddressBook();
await book.RequestPermission();
contacts = book.ToArray();
}
catch
{
new UIAlertView("Oops!","Something went wrong, try again later.",null, "Ok").Show();
}
}
要使用 Xamarin.Mobile 加载联系人,您必须首先创建一个AddressBook对象。接下来,我们必须调用RequestPermissions来请求用户允许访问地址簿。这是一个重要的步骤,因为 iOS 设备在应用程序可以访问用户的联系人之前需要这一步骤。这防止了恶意应用程序在用户不知情的情况下检索联系人。另一方面,Android 设备只在安装应用程序之前展示这些权限。
接下来,我们使用了System.Linq扩展方法ToArray来遍历地址簿,并将其存储在名为contacts的成员变量中。根据您的需求,您也可以在AddressBook对象上使用foreach。
如果您现在编译并运行应用程序,您将看到一个标准的 iOS 弹出窗口请求访问联系人,如下面的截图所示:

如果你意外地点击了不允许,你可以通过在设备上导航到设置 | 隐私 | 联系人来更改此设置。在 iOS 模拟器中,你还可以通过关闭应用程序并导航到设置 | 通用 | 重置 | 重置位置与隐私来重置模拟器中的所有隐私提示。这是一个在开发过程中需要重新测试时的好提示。
因此,对于下一步,我们需要实现IUITableViewDataSource接口,以便我们可以处理联系人数组并在屏幕上显示它们。就像添加到UITableViewSource一样,在控制器中添加以下方法:
public override int RowsInSection(UITableView tableview, int section)
{
return contacts != null ? contacts.Length : 0;
}
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
var contact = contacts [indexPath.Row];
var cell = tableView.DequeueReusableCell(CellName);
if (cell == null)
cell = new UITableViewCell(UITableViewCellStyle.Default, CellName);
cell.TextLabel.Text =contact.LastName + ", " + contact.FirstName;
return cell;
}
此外,通过选择一个字符串标识符,如ContactCell,向类中添加一个CellName常量字符串。现在,如果你编译并运行程序,你将能够在设备上看到联系人列表。以下截图显示了 iOS 模拟器中的默认联系人列表:

在 Android 中检索联系人
以非常相似的方式,我们可以使用 Xamarin.Mobile 在 Android 中检索联系人列表。Xamarin.Mobile 中的所有 API 在 Android 中都是相同的,除了需要在几个地方传递Android.Content.Context的要求。这是因为许多原生 Android API 需要当前活动(或到其他上下文,如Application)的引用才能正常工作。首先,通过在 Xamarin Studio 中导航到Android | Android Application来创建一个标准的 Android 应用程序项目。确保您从组件存储库中将 Xamarin.Mobile 添加到项目中。
在并行 iOS 中,让我们创建一个简单的ListView来显示联系人列表,如下所示:
-
从
Resources目录中的layout文件夹打开Main.axml文件。 -
从项目模板中删除默认按钮,并将ListView添加到布局中。
-
将其Id设置为
@+id/contacts。 -
保存文件并打开
MainActivity.cs,以便我们可以对代码进行一些修改。
让我们从删除大部分代码开始;我们不需要来自项目模板的代码。您还需要添加一个using语句用于Xamarin.Contacts。接下来,让我们在MainActivity类中实现一个简单的BaseAdapter<Contact>类,如下所示:
class ContactsAdapter : BaseAdapter<Contact>
{
public Contact[] Contacts { get; set; }
public override long GetItemId(int position)
{
return position;
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
var contact = this [position];
var textView = convertView as TextView;
if (textView == null)
{
textView = new TextView(parent.Context);
}
textView.Text = contact.LastName + ", " + contact.FirstName;
return textView;
}
public override int Count
{
get { return Contacts == null ? 0 : Contacts.Length; }
}
public override Contact this[int index]
{
get { return Contacts [index]; }
}
}
这将在ListView的每一行中显示一个TextView来显示每个联系人。我们在这里为了简化事情所做的另一件事是为联系人数组添加一个属性。这应该相当直接,类似于我们在前面的章节中所做的。
现在,让我们在OnCreate中设置适配器,如下所示:
protected async override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.Main);
var listView = FindViewById<ListView>(Resource.Id.contacts);
var adapter = new ContactsAdapter();
listView.Adapter = adapter;
try
{
var book = new AddressBook(this);
await book.RequestPermission();
adapter.Contacts = book.ToArray();
adapter.NotifyDataSetChanged();
}
catch
{
new AlertDialog.Builder(this).SetTitle("Oops").SetMessage("Something went wrong, try again later.").SetPositiveButton("Ok", delegate { }).Show();
}
}
此代码调用 Xamarin.Mobile,与我们在 iOS 代码中所做的一样,只是在这里,this必须传递给AddressBook构造函数中的 Android Context。我们的代码更改已完成;然而,如果您现在运行应用程序,将会抛出异常。Android 需要在清单文件中请求权限,这将通知用户在从 Google Play 下载时其访问地址簿的权限。
我们必须创建一个AndroidManifest.xml文件并声明一个权限,如下所示:
-
打开 Android 项目的项目选项。
-
在构建下选择Android 应用程序选项卡。
-
点击添加 Android 清单。
-
在必需权限部分下,勾选读取联系人。
-
点击确定保存您的更改。
现在,如果你运行应用程序,你将得到设备上所有联系人的列表,如下面的截图所示:

查找 GPS 位置
使用 Xamarin.Mobile 跟踪用户的 GPS 位置就像访问他们的联系人一样简单。iOS 和 Android 的设置过程类似,但在位置方面,您不需要在代码中请求权限。iOS 会自动显示标准警报请求权限。另一方面,Android 只需在清单中设置一个设置。
例如,让我们创建一个显示 GPS 位置随时间更新的应用程序。让我们从一个 iOS 示例开始,创建一个单视图应用程序项目。这可以通过导航到iOS | iPhone Storyboard并点击Single View Application来完成,就像我们在上一节中所做的那样。确保您从组件存储库中将 Xamarin.Mobile 添加到项目中。
现在,让我们实现一个简单的UITableView来显示 GPS 更新的列表,如下所示:
-
打开
MainStoryboard.storyboard文件。删除由项目模板创建的任何现有控制器。 -
使用
UITableViewController创建UINavigationController作为其根子控制器。 -
通过在 iOS 设计器中导航到属性 | 小部件,将
UITableViewController的类设置为LocationController。 -
保存故事板文件并返回到 Xamarin Studio。
打开LocationController.cs文件,我们首先设置 GPS 以在一段时间内更新表格视图。将using Xamarin.Geolocation;添加到文件顶部。我们可以在控制器的构造函数中设置一些成员变量并创建我们的Geolocator对象,如下所示:
Geolocator locator;
List<string> messages = new List<string>();
public LocationController (IntPtr handle) : base (handle)
{
Title = "GPS";
locator = new Geolocator();
locator.PositionChanged += OnPositionChanged;
locator.PositionError += OnPositionError;
}
接下来,我们可以设置如下的事件处理程序:
void OnPositionChanged (object sender, PositionEventArgs e)
{
messages.Add(string.Format("Long: {0:0.##} Lat: {1:0.##}",e.Position.Longitude, e.Position.Latitude));
TableView.ReloadData();
}
void OnPositionError (object sender, PositionErrorEventArgs e)
{
messages.Add(e.Error.ToString());
TableView.ReloadData();
}
当出现错误或位置变化时,这些会向列表中添加一条消息。我们使用了string.Format来仅显示经纬度到小数点后两位。
接下来,我们必须实际告诉Geolocator开始监听 GPS 更新。我们可以在ViewDidLoad中这样做:
public override void ViewDidLoad()
{
base.ViewDidLoad();
locator.StartListening(1000, 50);
}
在前面的代码中,1000是更新 GPS 位置的最小时间提示,而50是触发位置更新的米数提示。
最后但同样重要的是,我们需要设置表格视图。将LocationController设置为实现IUITableViewDataSource,并将以下方法添加到控制器中:
public override int RowsInSection(UITableView tableview, int section)
{
return messages.Count;
}
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
var cell = tableView.DequeueReusableCell(CellName);
if (cell == null)
cell = new UITableViewCell(UITableViewCellStyle.Default, CellName);
cell.TextLabel.Text = messages [indexPath.Row];
return cell;
}
如果您编译并运行应用程序,您应该会看到 iOS 权限提示,随后在表格视图中随着时间的推移显示经纬度,如下面的截图所示:

在 Android 上实现 GPS 位置
就像在上一节中一样,使用 Xamarin.Mobile 进行 GPS 定位几乎与我们在 iOS 中使用的 API 相同。首先,在我们的 Android 示例中,转到 Xamarin Studio 中的Android | Android Application。确保您从组件商店将 Xamarin.Mobile 添加到项目中。
让我们创建ListView以显示 GPS 位置更新的消息列表,如下所示:
-
在 Android 设计师中,从
Resources目录下的layout文件夹中打开Main.axml文件。 -
从项目模板中删除默认按钮,并将
ListView添加到布局中。 -
将其Id设置为
@+id/locations。 -
保存文件并打开
MainActivity.cs,以便我们可以进行一些代码更改。
与往常一样,删除由项目模板创建的任何额外代码。接下来,添加Xamarin.Geolocation的using语句。然后,将一个简单的BaseAdapter<string>添加到MainActivity类中,如下所示:
class Adapter : BaseAdapter<string>
{
List<string> messages = new List<string>();
public void Add(string message)
{
messages.Add(message);
NotifyDataSetChanged();
}
public override long GetItemId(int position)
{
return position;
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
var textView = convertView as TextView;
if (textView == null)
textView = new TextView(parent.Context);
textView.Text = messages [position];
return textView;
}
public override int Count
{
get { return messages.Count; }
}
public override string this[int index]
{
get { return messages [index]; }
}
}
这与其他我们过去设置的 Android 适配器类似。这里的一个区别是我们创建了一个包含消息List<string>的成员变量和一个将新消息添加到列表中的方法。
现在,让我们向MainActivity类中添加一些方法,以便设置 GPS 位置更新,如下所示:
Geolocator locator;
Adapter adapter;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.Main);
var listView = FindViewById<ListView>(Resource.Id.locations);
listView.Adapter = adapter = new Adapter();
locator = new Geolocator(this);
locator.PositionChanged += OnPositionChanged;
locator.PositionError += OnPositionError;
}
protected override void OnResume()
{
base.OnResume();
locator.StartListening(1000, 50);
}
protected override void OnPause()
{
base.OnPause();
locator.StopListening();
}
void OnPositionChanged (object sender, PositionEventArgs e)
{
adapter.Add(string.Format("Long: {0:0.##} Lat: {1:0.##}",e.Position.Longitude, e.Position.Latitude));
}
void OnPositionError (object sender, PositionErrorEventArgs e)
{
adapter.Add(e.Error.ToString());
}
再次,这看起来与 iOS 的代码相同,只是Geolocator的构造函数不同。如果您此时运行应用程序,它将无错误启动。然而,Geolocator对象不会触发任何事件。我们首先需要从 Android Manifest 文件中添加一个权限来访问位置。在OnResume中启动定位器并在OnPause中停止定位器也是一个好主意。这将通过在活动不在屏幕上时停止 GPS 位置来节省电池。
让我们创建一个AndroidManifest.xml文件,并声明以下两个权限:
-
打开 Android 项目的项目选项。
-
在构建下选择Android Application选项卡。
-
点击添加 Android 清单。
-
在必需权限部分,勾选AccessCoarseLocation和AccessFineLocation。
-
点击确定以保存您的更改。
现在,如果您编译并运行应用程序,您将随着时间的推移获得 GPS 位置更新,如下面的截图所示:

访问照片库和相机
Xamarin.Mobile 的最后一个主要功能是访问照片,以便用户能够将他们自己的内容添加到你的应用程序中。使用名为MediaPicker的类,你可以从设备的相机或照片库中提取照片,并可选择显示自己的 UI 进行操作。
让我们创建一个应用程序,该应用程序在按钮按下时从相机或照片库加载图像,并在屏幕上显示。首先,通过在 Xamarin Studio 中转到iOS | iPhone Storyboard | Single View Application来创建一个单视图应用程序项目。确保从组件商店将 Xamarin.Mobile 添加到项目中。
现在,让我们实现一个包含两个UIButton和一个UIImageView的屏幕,如下所示:
-
打开
MainStoryboard.storyboard文件。删除由项目模板创建的任何现有控制器。 -
创建一个包含一个
UIImageView和两个名为Library和Camera的UIButton的UIViewController。 -
通过在 iOS 设计器中导航到属性 | 小部件,将
UITableViewController的类设置为ContactsController。 -
在名为
imageView、library和camera的控制器中分别为每个视图创建名称字段。 -
保存故事板文件并返回到 Xamarin Studio。
现在,打开PhotoController.cs文件,并在ViewDidLoad中添加以下代码:
MediaPicker picker;
public override void ViewDidLoad()
{
base.ViewDidLoad();
picker = new MediaPicker();
if (!picker.IsCameraAvailable)
camera.Enabled = false;
camera.TouchUpInside += OnCamera;
library.TouchUpInside += OnLibrary;
}
注意,我们必须检查IsCameraAvailable并禁用camera按钮。有些 iOS 设备,如第一代 iPad,可能没有摄像头。除此之外,我们只需要创建一个MediaPicker实例,以便在点击每个按钮时使用。
现在,让我们为每个按钮的TouchUpInside事件添加一个方法,以及几个其他辅助方法,如下所示:
async void OnCamera (object sender, EventArgs e)
{
try
{
var file = await picker.TakePhotoAsync(new StoreCameraMediaOptions());
imageView.Image = ToImage(file);
}
catch
{
ShowError();
}
}
async void OnLibrary (object sender, EventArgs e)
{
try
{
var file = await picker.PickPhotoAsync();
imageView.Image = ToImage(file);
}
catch
{
ShowError();
}
}
UIImage ToImage(MediaFile file)
{
using (var stream = file.GetStream())
{
using (var data = NSData.FromStream(stream))
{
return UIImage.LoadFromData(data);
}
}
}
void ShowError()
{
new UIAlertView("Oops!", "Something went wrong, try again later.", null, "Ok").Show();
}
使用MediaPicker相当简单;你只需调用TakePhotoAsync或PickPhotoAsync来检索MediaFile实例。然后,你可以调用GetStream来对图像数据进行操作。在我们的例子中,我们创建了UIImage以直接在UIImageView中显示。如果发生意外情况或用户取消,还需要使用try-catch块。
你现在应该能够运行应用程序并选择一张要在屏幕上查看的照片。以下截图显示了从照片库中选择的 iOS 模拟器中的默认照片:

在 Android 上访问照片
与 iOS 相比,在 Android 上检索相机或照片库中的照片时,我们必须使用稍微不同的模式。Android 中的一种常见模式是它调用 StartActivityForResult 从另一个应用程序启动活动。当此活动完成时,OnActivityResult 将从您的活动中被调用。正因为如此,Xamarin.Mobile 无法在 Android 上使用与其他平台相同的 API。为了开始我们的示例,请通过在 Xamarin Studio 中转到 Android | Android Application 创建一个 Android 应用程序项目。确保您从组件商店将 Xamarin.Mobile 添加到项目中。
让我们创建两个 按钮 和一个 ImageView 来模拟我们的 iOS UI,如下所示:
-
在 Android 设计器中,从
Resources目录下的layout文件夹中打开Main.axml文件。 -
从项目模板中删除默认按钮,并添加两个新的
Button,分别命名为Library和Camera。 -
将它们的 Id 分别设置为
@+id/library和@+id/camera。 -
创建一个具有 Id 为
@+id/imageView的ImageView。 -
保存文件并打开
MainActivity.cs,以便我们可以修改我们的代码。
如同往常,删除由项目模板创建的任何额外代码。接下来,添加一个 using 语句用于 Xamarin.Media。然后,我们可以添加一个新的 OnCreate 方法以及一些用于我们活动的成员变量,如下所示:
MediaPicker picker;
ImageView imageView;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.Main);
var library = FindViewById<Button>(Resource.Id.library);
var camera = FindViewById<Button>(Resource.Id.camera);
imageView = FindViewById<ImageView>(Resource.Id.imageView);
picker = new MediaPicker(this);
library.Click += OnLibrary;
camera.Click += OnCamera;
if (!picker.IsCameraAvailable)
camera.Enabled = false;
}
我们检索了视图的实例,并通过将活动作为 Context 传递给其构造函数创建了一个新的 MediaPicker。我们连接了一些 Click 事件处理程序,并禁用了 camera 按钮,因为相机不可用。
接下来,让我们实现两个 Click 事件处理程序,如下所示:
void OnLibrary (object sender, EventArgs e)
{
var intent = picker.GetPickPhotoUI();
StartActivityForResult (intent, 1);
}
void OnCamera (object sender, EventArgs e)
{
var intent = picker.GetTakePhotoUI(new StoreCameraMediaOptions
{
Name = "test.jpg", Directory = "PhotoPicker"
});
StartActivityForResult (intent, 1);
}
在每种情况下,我们都会调用 GetPickPhotoUI 或 GetTakePhotoUI 来获取一个 Android Intent 对象的实例。此对象用于在应用程序内启动新的活动。StartActivityForResult 也会启动 Intent 对象,并期望从新活动返回结果。我们还使用 StoreCameraMediaOptions 设置一些值,以指定存储照片的文件名和临时目录。
接下来,我们需要在 Android 中实现 OnActivityResult 以处理新活动完成时会发生的情况:
protected async override void OnActivityResult(
int requestCode, Result resultCode, Intent data)
{
if (resultCode == Result.Canceled)
return;
var file = await data.GetMediaFileExtraAsync(this);
using (var stream = file.GetStream())
{
imageView.SetImageBitmap(await
BitmapFactory.DecodeStreamAsync(stream));
}
}
如果这成功,我们将检索 MediaFile 并使用返回的 Stream 加载一个新的 Bitmap。接下来,所需做的就是调用 SetImageBitmap 在屏幕上显示图像。
让我们创建一个 AndroidManifest.xml 文件并声明两个权限,如下所示:
-
打开 Android 项目的项目选项。
-
在 构建 下的 Android 应用程序 选项卡下选择。
-
点击 添加 Android 清单。
-
在 必需权限 部分下,勾选 Camera 和 WriteExternalStorage。
-
点击 确定 保存您的更改。
您现在应该能够运行应用程序并将照片加载到屏幕上显示,如下面的截图所示:

摘要
在本章中,我们发现了 Xamarin.Mobile 库以及它是如何以跨平台的方式加速常见原生任务的。我们从地址簿中检索了联系人,并设置了随时间推移的 GPS 位置更新。最后,我们从相机和照片库中加载了照片。直接使用原生 API 意味着每个平台上的代码量将是两倍,因此我们看到了 Xamarin.Mobile 库是如何作为一个有用的抽象,可以减少一些开发时间。
完成本章后,你应该对 Xamarin.Mobile 库及其为跨平台开发提供的常用功能有全面的理解。它提供了干净、现代的 API,这些 API 提供了跨 iOS、Android 和 Windows Phone 访问的async/await功能。使用 Xamarin.Mobile 跨平台访问联系人、GPS 和照片非常直接。
在下一章中,我们将介绍如何将应用程序提交到 iOS App Store 和 Google Play 的步骤。这包括如何准备您的应用程序以通过 iOS 指南,以及如何正确注册您的应用程序以供 Google Play 使用。
第十一章。Xamarin.Forms
自从 Xamarin 作为公司开始以来,他们的座右铭一直是直接将 iOS 和 Android 的本地 API 暴露给 C#。这是一个很好的策略,因为使用 Xamarin.iOS 或 Xamarin.Android 构建的应用程序几乎与本地 Objective-C 或 Java 应用程序无法区分。代码共享通常限于非 UI 代码,这可能在 Xamarin 生态系统中留下一个潜在的空白:一个跨平台 UI 抽象。Xamarin.Forms 是解决这个问题的方案,这是一个跨平台 UI 框架,在每个平台上渲染本地控件。对于知道 C#(和 XAML)的人来说,Xamarin.Forms 是一个很好的解决方案,但也可能不想深入了解使用本地 iOS 和 Android API 的细节。
在本章中,我们将涵盖以下内容:
-
在 Xamarin.Forms 中创建“Hello World”
-
讨论 Xamarin.Forms 架构
-
使用 XAML 与 Xamarin.Forms
-
使用 Xamarin.Forms 实现数据绑定和 MVVM
在 Xamarin.Forms 中创建“Hello World”
要了解 Xamarin.Forms 应用程序是如何组合的,让我们先创建一个简单的“Hello World”应用程序。
打开 Xamarin Studio 并执行以下步骤:
-
创建一个新的解决方案。
-
导航到C# | 移动应用部分。
-
创建一个新的空白应用(Xamarin.Forms Portable)解决方案。
-
将你的解决方案命名为合适的名称,例如
HelloForms。
注意成功创建的三个新项目:HelloForms、HelloForms.Android和HelloForms.iOS。在 Xamarin.Forms 应用程序中,你大部分的代码将是共享的,而每个平台特定项目只是启动 Xamarin.Forms 框架的一小部分代码。
让我们检查 Xamarin.Forms 应用程序的最小部分:
-
在
HelloFormsPCL 库中的App.cs。这个类包含 Xamarin.Forms 应用程序的启动页面。一个简单的静态方法GetMainPage()返回应用程序的启动页面。在默认项目模板中, -
创建了一个带有单个标签的
ContentPage,在 iOS 上渲染为UILabel,在 Android 上渲染为TextView。 -
在
HelloForms.AndroidAndroid 项目中,MainActivity.cs。这是 Android 应用程序的主要启动活动。对于 Xamarin.Forms 来说,这里重要的是对Forms.Init(this, bundle)的调用,它初始化了 Xamarin.Forms 框架的 Android 特定部分。接下来是对SetPage(App.GetMainPage())的调用,它显示主 Xamarin.Forms 页面的本地版本。 -
在
HelloForms.iOSiOS 项目中的AppDelegate.cs。这与 Android 非常相似,只是 iOS 应用程序通过UIApplicationDelegate类启动。Forms.Init()将初始化 Xamarin.Forms 的 iOS 特定部分,而App.GetMainPage().CreateViewController()将生成一个可以用于应用程序主窗口的RootViewController的本地控制器。
继续运行 iOS 项目;你应该能看到以下截图类似的内容:

如果您运行 Android 项目,您将得到一个非常类似于 iOS 的 UI,但使用的是原生 Android 控件,如下面的截图所示:

提示
尽管本书没有涉及,但 Xamarin.Forms 也支持 Windows Phone 应用程序。然而,要为 Windows Phone 开发,需要一个运行 Windows 和 Visual Studio 的 PC。如果您能让一个 Xamarin.Forms 应用程序在 iOS 和 Android 上运行,那么让 Windows Phone 版本运行应该易如反掌。
理解 Xamarin.Forms 背后的架构
开始使用 Xamarin.Forms 非常简单,但总是好的,看看幕后发生了什么,以了解幕后发生的事情。在这本书的早期章节中,我们使用原生 iOS 和 Android API 直接创建了一个跨平台应用程序。某些应用程序更适合这种开发方法,因此了解 Xamarin.Forms 应用程序和纯 Xamarin 应用程序之间的区别对于选择最适合您应用程序的框架非常重要。
Xamarin.Forms 是一个在原生 iOS 和 Android API 之上的抽象层,您可以直接从 C# 中调用。因此,Xamarin.Forms 使用与纯 Xamarin 应用程序相同的 API,同时提供了一个框架,允许您以跨平台的方式定义您的 UI。这种抽象层在许多方面是非常好的,因为它让您能够共享驱动 UI 的代码以及任何可能也在标准 Xamarin 应用程序中共享的后端 C# 代码。然而,主要的缺点是性能略有下降,并且受限于 Xamarin.Forms 框架,在可用的控件类型方面有所限制。Xamarin.Forms 还提供了编写 渲染器 的选项,允许您以特定平台的方式覆盖您的 UI。然而,在我看来,渲染器的功能仍然相对有限。
在以下图中查看 Xamarin.Forms 应用程序与传统 Xamarin 应用程序之间的区别:

在这两个应用程序中,应用程序的业务逻辑和后端代码可以共享,但 Xamarin.Forms 通过允许共享 UI 代码为您提供了巨大的好处。
此外,Xamarin.Forms 应用程序有两个项目模板可供选择,因此让我们分别介绍每个选项:
-
Xamarin.Forms 共享:这会创建一个包含所有您的 Xamarin.Forms 代码、iOS 项目和 Android 项目的共享项目。
-
Xamarin.Forms 可移植:这会创建一个包含所有共享 Xamarin.Forms 代码、iOS 项目和 Android 项目的可移植类库。
通常,这两种选项对任何应用程序都适用。共享项目基本上是一组代码文件集合,这些文件会自动添加到引用它的另一个项目中。使用共享项目允许您使用预处理器语句来实现平台特定代码。另一方面,可移植类库项目创建了一个可移植的 .NET 程序集,可以在 iOS、Android 和各种其他平台上使用。PCL 不能使用预处理器语句,因此您通常使用接口或抽象/基类设置平台特定代码。在大多数情况下,我认为可移植类库是一个更好的选择,因为它本质上鼓励更好的编程实践。您可以参考 第三章,iOS 和 Android 之间的代码共享,以了解这两种代码共享技术的优缺点。
使用 XAML 在 Xamarin.Forms 中
除了从 C# 代码中定义 Xamarin.Forms 控件外,Xamarin 还提供了使用 可扩展应用程序标记语言 (XAML) 开发 UI 的工具。XAML 是一种声明性语言,基本上是一组映射到 Xamarin.Forms 框架中特定控件的 XML 元素。使用 XAML 与您认为使用 HTML 定义网页 UI 相似,但区别在于 Xamarin.Forms 中的 XAML 创建代表原生 UI 的 C# 对象。
要理解 XAML 在 Xamarin.Forms 中的工作原理,让我们创建一个带有许多 UI 元素的新页面:
-
通过导航到 C# | 移动应用 | 空白应用 (Xamarin.Forms Portable) 创建一个新的 Xamarin.Forms Portable 解决方案。
-
将项目命名为合适的名称,例如
UIDemo。 -
通过导航到 Forms | Forms ContentPage XAML 项模板添加一个新文件。将页面命名为
UIDemoPage。 -
打开
UIDemoPage.xaml。
现在,让我们编辑 XAML 代码。在 <ContentPage.Content> 标签之间添加以下 XAML 代码:
<StackLayout Orientation="Vertical" Padding="10,20,10,10"> <Label Text="My Label" XAlign="Center" /> <Button Text="My Button" /> <Entry Text="My Entry" /> <Image Source="xamagon.png" /> <Switch IsToggled="true" /> <Stepper Value="10" /> </StackLayout>
在 iOS 和 Android 上运行应用程序。您的应用程序将类似于以下截图:

然后,在 Android 上,Xamarin.Forms 将以相同的方式渲染屏幕,但使用原生 Android 控件:

首先,我们创建了一个 StackLayout 控件,这是一个其他控件的容器。它可以根据 Orientation 值垂直或水平逐个布局控件。我们还为侧面和底部添加了 10 像素的填充,以及从顶部到 iOS 状态栏的 20 像素填充,以调整 iOS 状态栏。如果您熟悉 WPF 或 Silverlight 中的矩形定义语法,您可能对这种语法很熟悉。Xamarin.Forms 使用相同的语法,即由逗号分隔的左、上、右和底部值。
我们还使用了几个内置的 Xamarin.Forms 控件来查看它们的工作方式:
-
Label:我们在本章前面已经使用过这个控件。它仅用于显示文本。在 iOS 上对应于UILabel,在 Android 上对应于TextView。 -
Button:这是一个通用按钮,用户可以点击。这个控件在 iOS 上对应于UIButton,在 Android 上对应于Button。 -
Entry:这个控件是一个单行文本输入。在 iOS 上对应于UITextField,在 Android 上对应于EditText。 -
Image:这是一个简单的控件,用于在屏幕上显示图像,在 iOS 上对应于UIImage,在 Android 上对应于ImageView。我们使用了这个控件的Source属性,从 iOS 的Resources文件夹和 Android 的Resources/drawable文件夹加载图像。你也可以在这个属性上设置 URL,但最好是将图像包含在你的项目中以提高性能。 -
Switch:这是一个开关或切换按钮。在 iOS 上对应于UISwitch,在 Android 上对应于Switch。 -
Stepper:这是一个通用输入,可以通过两个加号和减号按钮输入数字。在 iOS 上,这对应于UIStepper,而在 Android 上,Xamarin.Forms 使用两个Button来实现这个功能。
这只是 Xamarin.Forms 提供的一些控件中的一部分。还有更多复杂的控件,如你期望用于开发移动 UI 的 ListView 和 TableView。
尽管在这个例子中我们使用了 XAML,但你也可以用 C# 实现这个 Xamarin.Forms 页面。下面是一个这样的例子:
public class UIDemoPageFromCode : ContentPage
{
public UIDemoPageFromCode()
{
var layout = new StackLayout
{
Orientation = StackOrientation.Vertical,
Padding = new Thickness(10, 20, 10, 10),
};
layout.Children.Add(new Label
{
Text = "My Label",
XAlign = TextAlignment.Center,
});
layout.Children.Add(new Button
{
Text ="My Button",
});
layout.Children.Add(new Image
{
Source = "xamagon.png",
});
layout.Children.Add(new Switch
{
IsToggled = true,
});
layout.Children.Add(new Stepper
{
Value = 10,
});
Content = layout;
}
}
因此,你可以看到使用 XAML 可以使代码更易读,并且通常在声明 UI 方面表现得更好。然而,使用 C# 来定义你的 UI 仍然是一个可行且直接的方法。
使用数据绑定和 MVVM
到目前为止,你应该已经掌握了 Xamarin.Forms 的基础知识,但你可能想知道 MVVM 设计模式如何融入其中。MVVM 设计模式最初是为了与 XAML 和 XAML 提供的强大数据绑定功能一起使用而构思的,因此它自然是一个与 Xamarin.Forms 一起使用的完美设计模式。
让我们来看看如何使用 Xamarin.Forms 设置数据绑定和 MVVM 的基本知识:
-
你的模型和视图模型层将主要保持与本书前面介绍的 MVVM 模式不变。
-
你的视图模型层应该实现
INotifyPropertyChanged接口,这有助于数据绑定。为了简化 Xamarin.Forms,你可以使用BindableObject基类,并在视图模型中的值发生变化时调用OnPropertyChanged。 -
在 Xamarin.Forms 中,任何页面或控件都有一个
BindingContext属性,它是与之数据绑定的对象。通常,你可以将相应的视图模型设置为每个视图的BindingContext属性。 -
在 XAML 中,你可以使用如下形式的语法
Text="{Binding Name}"来设置数据绑定。这个例子将控件的 Text 属性绑定到BindingContext中对象的 Name 属性。 -
结合数据绑定,事件可以通过
ICommand接口转换为命令。例如,一个按钮的点击事件可以绑定到由 ViewModel 提供的命令。Xamarin.Forms 中有一个内置的Command类来支持这一点。
小贴士
在 Xamarin.Forms 中,也可以通过 Binding 类从 C# 代码设置数据绑定。然而,通常从 XAML 设置绑定要简单得多,因为那里的语法已经被简化了。
现在我们已经涵盖了基础知识,让我们一步一步地来,并将之前在书中讨论的 XamChat 示例应用程序部分转换为使用 Xamarin.Forms。大部分情况下,我们可以重用大部分的 Model 和 ViewModel 层,尽管我们需要进行一些小的修改以支持从 XAML 的数据绑定。
让我们从创建一个名为 XamChat 的 PCL 支持的新 Xamarin.Forms 应用程序开始:
-
首先,在
XamChat项目中创建三个文件夹,分别命名为Views、ViewModels和Models。 -
从之前章节中 XamChat 应用程序中添加适当的
ViewModels和Models类。这些在XamChat.Core项目中可以找到。 -
构建项目并确保所有内容都已保存。你将得到一些编译错误,我们将在稍后解决。
我们需要编辑的第一个类是 BaseViewModel 类。打开它并做出以下更改:
public class BaseViewModel : BindableObject
{
protected readonly IWebService service = DependencyService.Get<IWebService>();
protected readonly ISettings settings = DependencyService.Get<ISettings>();
private bool isBusy = false;
public bool IsBusy
{get {return isBusy;}
set {isBusy = value; OnPropertyChanged();}}
}
首先,我们移除了对 ServiceContainer 类的调用,因为 Xamarin.Forms 提供了自己的 IoC 容器,称为 DependencyService。它的工作方式与我们在上一章中构建的容器非常相似,但它只有一个方法,即 Get<T>,并且注册是通过我们将要设置的程序集属性来设置的。
此外,我们移除了 IsBusyChanged 事件,转而使用支持数据绑定的 INotifyPropertyChanged 接口。从 BindableObject 继承给我们一个辅助方法 OnPropertyChanged,我们使用它来通知绑定在 Xamarin.Forms 中值已更改。请注意,我们没有将包含属性名的 string 传递给 OnPropertyChanged。此方法使用 .NET 4.0 的一个不太为人所知的功能 CallerMemberName,该功能将在运行时自动填充调用属性的名称。
接下来,让我们使用 DependencyService 设置所需的服务。在 PCL 项目的根目录中打开 App.cs 文件,并在命名空间声明之上添加以下两行:
[assembly: Dependency(typeof(XamChat.Core.FakeWebService))]
[assembly: Dependency(typeof(XamChat.Core.FakeSettings))]
DependencyService 将会自动获取这些属性并检查我们声明的类型。这些类型实现的任何接口都将被返回给任何未来调用 DependencyService.Get<T> 的调用者。我通常将所有的 Dependency 声明放在 App.cs 文件中,以便于管理和集中处理。
接下来,让我们通过添加一个新属性来修改 LoginViewModel:
public Command LoginCommand { get; set; }
我们将很快使用这个来绑定按钮的命令。在 ViewModel 层的最后一个小改动是为 MessageViewModel 设置 INotifyPropertyChanged:
Conversation[] conversations;
public Conversation[] Conversations
{get {return conversations; }
set {conversations = value; OnPropertyChanged();}
}
同样,你可以重复这个模式来处理 ViewModel 层中剩余的公共属性,但在这个例子中我们只需要这些。接下来,在 Views 文件夹下创建一个新的 Foms ContentPage Xaml 项目,命名为 LoginPage。在 LoginPage.xaml.cs 代码背后文件中,我们只需做一些修改:
public partial class LoginPage : ContentPage
{
readonly LoginViewModel loginViewModel = new LoginViewModel();
public LoginPage()
{
Title = "XamChat";
BindingContext = loginViewModel;
loginViewModel.LoginCommand = new Command(async () =>
{
try
{
await loginViewModel.Login();
await Navigation.PushAsync(new ConversationsPage());
}
catch (Exception exc)
{
await DisplayAlert("Oops!", exc.Message, "Ok");
}
});
InitializeComponent();
}
}
在这里,我们做了几件重要的事情,包括将 BindingContext 设置为我们的 LoginViewModel。我们设置了 LoginCommand,它基本上调用 Login 方法,如果出现问题则显示消息。如果成功,它还会导航到新页面。我们还设置了标题,这将在应用程序的顶部导航栏中显示。
接下来,打开 LoginPage.xaml,我们将在内容页的内容中添加以下 XAML 代码:
<StackLayout Orientation="Vertical" Padding="10,10,10,10">
<Entry Placeholder="Username" Text="{Binding Username}" />
<Entry Placeholder="Password" Text="{Binding Password}" IsPassword="true" />
<Button Text="Login" Command="{Binding LoginCommand}" />
<ActivityIndicator IsVisible="{Binding IsBusy}" IsRunning="true" />
</StackLayout>
这将设置两个文本字段、一个按钮和一个带有所有绑定以使一切正常工作的旋转按钮的基本设置。由于我们从 LoginPage 代码背后设置了 BindingContext,所以所有属性都绑定到了 LoginViewModel。
接下来,创建一个名为 ConversationsPage 的 XAML 页面,就像我们之前做的那样,并编辑其背后的 ConversationsPage.xaml.cs 代码:
public partial class ConversationsPage : ContentPage
{
readonly MessageViewModel messageViewModel = new MessageViewModel();
public ConversationsPage()
{
Title = "Conversations";
BindingContext = messageViewModel;
InitializeComponent ();
Appearing += async (sender, e) =>
{
try
{
await messageViewModel.GetConversations();
}
catch (Exception exc)
{
await DisplayAlert("Oops!", exc.Message, "Ok");
}
};
}
}
在这种情况下,我们重复了很多相同的步骤。例外的是,我们使用了 Appearing 事件作为加载屏幕上显示的对话的方式。
现在,让我们将以下 XAML 代码添加到 ConversationsPage.xaml 中:
<ListView ItemsSource="{Binding Conversations}">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Username}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
在这个例子中,我们使用了 ListView 来绑定一个项目列表并在屏幕上显示。我们定义了一个 DataTemplate 类,它代表了一个集合,用于表示 ItemsSource 绑定到的列表中的每个项目的单元格。在我们的例子中,为 Conversations 列表中的每个项目创建了一个显示 Username 的 TextCell。
最后但同样重要的是,我们必须回到 App.cs 文件并修改启动页面:
public static Page GetMainPage()
{
return new NavigationPage(new LoginPage());
}
我们在这里使用了 NavigationPage,这样 Xamarin.Forms 就可以在不同的页面之间推送和弹出。这在 iOS 上使用 UINavigationController,这样你就可以看到每个平台上如何使用原生 API。
到目前为止,如果你编译并运行应用程序,你将得到一个功能齐全的 iOS 和 Android 应用程序,它可以登录并查看对话列表:

摘要
在本章中,我们介绍了 Xamarin.Forms 的基础知识,并学习了它如何非常有用,可以用来构建自己的跨平台应用程序。Xamarin.Forms 在某些类型的应用程序中表现出色,但如果需要编写更复杂的 UI 或利用原生绘图 API,可能会受到限制。我们发现了如何使用 XAML 来声明我们的 Xamarin.Forms UI,并理解了 Xamarin.Forms 控件在每个平台上是如何渲染的。我们还深入探讨了数据绑定的概念,并发现了如何使用 MVVM 设计模式与 Xamarin.Forms 结合。最后但同样重要的是,我们开始将书中之前讨论过的 XamChat 应用程序移植到 Xamarin.Forms,并且能够重用大部分后端代码。
第十二章. App Store 提交
现在你已经完成了跨平台应用的开发,下一步显然是将你的应用分发到应用商店。Xamarin 应用与 Java 或 Objective-C 应用以完全相同的方式进行分发;然而,为了成功地将你的应用提交到商店,仍然有很多障碍需要克服。iOS 有一个官方的审批流程,这使得应用商店提交的过程比 Android 长得多。开发者必须等待一周、一个月或更长时间,具体取决于应用被拒绝的次数。与调试你的应用相比,Android 在提交应用到 Google Play 时需要一些额外的步骤,但你仍然可以在几小时内提交你的应用。
在本章中,我们将介绍:
-
App Store 审查指南
-
将 iOS 应用提交到 App Store
-
设置 Android 签名密钥
-
将 Android 应用提交到 Google Play
-
在应用商店取得成功的技巧
遵循 iOS App Store 审查指南
你的应用名称、应用图标、截图和其他方面都在苹果网站上称为 iTunes Connect 的地方声明。销售报告、应用商店拒绝、合同和银行信息以及应用更新都通过 itunesconnect.apple.com 网站进行管理。
苹果的指导方针的主要目的是保持 iOS App Store 安全,无恶意软件。在 iOS App Store 中确实很少发现恶意软件。一般来说,iOS 应用可能对你做的最糟糕的事情就是向你投放广告。在某种程度上,这些指导方针也加强了苹果在应用内支付中的收入分成。遗憾的是,苹果的一些指导方针有争议地消除他们在 iOS 中的一个关键领域的竞争对手。一个例子就是销售电子书的 app,因为它将是 iTunes 和 iBooks 的直接竞争对手。

然而,这里的关键点是让你的应用通过商店审批流程,而不会遇到 App Store 的拒绝。只要你不是故意试图违反规则,大多数应用在获得苹果的批准时不会遇到太多困难。最常见的拒绝与开发者的错误有关,这是一个好事,因为你不想向公众发布一个存在关键问题的应用。
App Store 审查指南相当长,所以让我们将其分解为你可能遇到的最常见的情况。完整的指南列表可以在 developer.apple.com/appstore/resources/approval/guidelines.html 找到。请注意,查看此网站需要有效的 iOS 开发者账户。
一般规则
需要遵循的一些一般规则如下:
-
崩溃、有错误或严重失败的应用将被拒绝
-
不按广告宣传执行或包含隐藏功能的应用程序将被拒绝
-
使用非公开的苹果 API 或从文件系统中的禁止位置读取/写入文件的应用程序将被拒绝
-
提供很少价值或过度开发的应用(如手电筒、打嗝或放屁应用)将被拒绝
-
应用程序不能在没有商标持有人许可的情况下使用商标词作为应用程序名称或关键词
-
应用程序不能非法分发受版权保护的材料
-
可以通过移动友好型网站简单实现的应用程序,如包含大量 HTML 内容但无原生功能的应用程序,可能会被拒绝
这些规则有助于保持 iOS App Store 的整体质量和安全性高于其他情况。由于这些规则中的某些规则,将具有非常少功能的简单应用程序放入商店可能会很困难,所以请确保您的应用程序足够有用和吸引人,以便 App Store 审查团队能够允许它在商店中可用。
不正确和不完整的信息
一些与开发者犯的错误或 iTunes Connect 中的错误标签相关的规则如下:
-
提及其他移动平台(例如 Android)的应用程序或元数据将被拒绝
-
标记有错误或不适当的类别/类型、截图或图标的程序将被拒绝
-
开发者必须为应用程序提供适当的年龄评级和关键词
-
支持服务、隐私政策和营销 URL 必须在应用程序审查时可用
-
开发者不应声明未使用的 iOS 功能;例如,如果你的应用程序实际上没有使用这些功能,请不要声明使用游戏中心或 iCloud
-
在未经用户同意的情况下使用如位置或推送通知等功能的程序将被拒绝
这些错误有时可能是开发者的一时疏忽。只需确保在提交到 iOS App Store 之前,仔细检查您应用程序的所有信息即可。
应用程序中存在的内容
此外,苹果对应用程序中可以包含的内容有以下规定:
-
包含令人反感的内容或可能被视为粗鲁的内容的应用程序将被拒绝
-
设计用来激怒或令用户反感的应用程序将被拒绝
-
包含过多暴力画面的应用程序将被拒绝
-
针对特定政府、种族、文化或公司作为敌人的应用程序将被拒绝
-
图标或截图不符合四岁以上年龄评级的应用程序可能会被拒绝
应用商店向儿童和成人 alike 交付应用程序。苹果还支持对应用程序的17 岁以上年龄限制;然而,这将严重限制您应用程序的潜在用户数量。最好保持应用程序干净且适合尽可能多的年龄段。
苹果的 70/30 收入分成
列出的下一类规则与苹果从 App Store 获得的 70/30 收入分成相关,如下所示:
-
链接到在网站上销售的产品或软件的应用程序可能会被拒绝。
-
使用除 iOS 内购(IAPs)之外的支付机制的应用程序将被拒绝。
-
使用 IAPs 购买实体商品的应用程序将被拒绝。
-
应用程序可以显示在应用程序外部购买的数字内容,只要你无法在应用程序内链接到或购买。所有在应用程序内购买的数字内容都必须使用 IAPs。
只要你不试图规避苹果在 App Store 中的收入分成,这些规则就很容易遵循。始终使用 IAPs 来解锁你应用程序内的数字内容。
一般性建议
最后但同样重要的是,这里有一些与 App Store 拒绝相关的一般性建议:
-
如果你的应用程序需要用户名和密码,确保你在演示账户信息部分包含凭证,以便应用程序审查团队使用。
-
如果你的应用程序包含 IAPs 或其他应用程序审查团队必须明确测试的功能,确保你在审阅笔记中包含说明,以便到达你应用程序中的适当屏幕。
-
提前安排!不要让你的产品应用程序被拒绝破坏了截止日期;至少在你的计划中为应用程序商店的批准预留一个月的时间。
-
当有疑问时,在 iTunes Connect 的审阅笔记部分尽可能详细地描述。
如果你的应用程序被拒绝,大多数情况下都有一个简单的解决方案。如果违反了规则,苹果的审查团队将明确引用指南,并包括相关的崩溃日志和截图。如果你可以在不提交新版本的情况下纠正问题,你可以通过 iTunes Connect 网站上的解决方案中心选项回应应用程序审查团队。如果你上传了新版本,这将使你的应用程序排在队列的末尾等待审查。
iOS 中某些功能的确切和具体规则可能更多,所以如果你在考虑使用 iOS 功能进行创新或跳出常规,请确保查看完整的指南。始终如一,如果你对某个具体指南不确定,最好是就此事寻求专业的法律建议。拨打苹果的支持电话不会对这个主题有任何帮助,因为其支持人员不允许就 App Store 审查指南提供建议。
提交应用程序到 iOS App Store
在我们开始提交我们的应用程序到商店之前,我们需要审查一个简短的清单,以确保你准备好这样做。在过程中达到某个点后意识到你遗漏了某些东西或没有做得很正确是非常痛苦的。此外,还有一些要求需要设计师或营销团队满足,这些不应该完全留给开发者。
在开始提交之前,确保你已经做了以下事情:
-
您应用程序的
Info.plist文件已完全填写。这包括启动画面图像、应用程序图标、应用程序名称和其他需要填写以启用高级功能的设置。请注意,这里的应用程序名称是显示在应用程序图标下的名称。它可以与 App Store 名称不同,并且与 App Store 名称不同,它不需要与商店中的其他所有应用程序都不同。 -
您在 App Store 上至少为您的应用程序选择了三个名称。即使该名称目前在 App Store 上未被占用,它也可能因开发者之前为已从商店中删除的应用程序而不可用。
-
您有一个大型的 1024 x 1024 应用程序图标图像。除非您通过 iTunes(桌面应用程序)分发企业或临时构建,否则不需要将此文件包含在应用程序中。
-
您为您的应用程序针对的每个设备至少有一个截图。这包括针对通用 iOS 应用程序的 iPhone 4 视网膜、iPhone 5、iPhone 6、iPhone 6 Plus 和 iPad 视网膜尺寸的截图。然而,我强烈建议您填写所有五个截图槽。
-
您为 App Store 编写并编辑了良好的描述。
-
您已经选择了一组关键词以改善您应用程序的搜索。
创建分发配置文件
一旦您已经复查了前面的清单,我们就可以开始提交过程。我们的第一步将是为 App Store 分发创建一个配置文件。
让我们从执行以下步骤来创建一个新的配置文件开始:
-
点击右侧导航栏中的证书、标识符和配置文件。
-
点击配置文件。
-
点击窗口右上角的加号按钮。
-
在分发下选择App Store并点击继续。
-
选择您的应用程序 ID。您应该已经在第七章 部署和测试在设备上中创建了一个;点击继续。
-
选择配置文件的证书。通常这里只有一个选项。点击继续。
-
给配置文件一个合适的名称,例如
MyAppAppStore。点击生成。 -
完成后,您可以手动下载和安装配置文件,或者在 Xcode 的首选项 | 帐户下同步您的配置文件,就像我们在书中之前所做的那样。
成功后,您将到达以下屏幕:

将您的应用程序添加到 iTunes Connect
在接下来的步骤中,我们将开始填写您应用程序的详细信息,以便在 Apple App Store 上显示。
我们可以通过以下步骤开始设置您的应用程序在 iTunes Connect 中的配置:
-
导航到
itunesconnect.apple.com并登录。 -
点击我的应用。
-
点击窗口左上角的加号按钮,然后选择新 iOS 应用。
-
输入将在 App Store 上显示的应用名称。
-
输入将在 App Store 上显示的版本号。
-
为你的应用选择一个主要语言。
-
在SKU字段中输入一个值。这用于在报告中识别你的应用。
-
选择你的包标识符。你应该已经在第七章 部署和测试在设备上 中创建了一个;点击继续。
-
这里需要填写大量的信息。如果你遗漏了任何信息,iTunes Connect 在显示警告方面非常有帮助。由于该网站旨在由市场营销专业人士以及开发者使用,因此应该相当用户友好。
-
在进行更改后点击保存。
还有许多可选字段。确保填写审查说明或演示账户信息。如果有任何其他信息,应用审查团队将需要审查你的应用程序。完成时,你将看到你的应用程序状态为准备提交,如下截图所示:

现在我们需要实际将我们的应用上传到 iTunes Connect。你必须上传一个来自 Xcode 或 Application Loader 的构建。两种方法都会产生相同的结果,但有些人如果非开发者提交应用,更喜欢使用 Application Loader。
为 App Store 制作 iOS 二进制文件
我们提交 App Store 的最后一步是提供包含我们应用程序的二进制文件给商店。我们需要创建应用程序的发布版本,使用本章早期创建的发行版配置文件签名。
Xamarin Studio 使这一过程非常简单。我们可以按照以下方式配置构建:
-
在 Xamarin Studio 左上角点击解决方案配置下拉菜单并选择AppStore。
-
默认情况下,Xamarin Studio 将设置你需要提交此构建配置的所有配置选项。
-
接下来,选择你的 iOS 应用程序项目并导航到构建 | 存档。
几分钟后,Xamarin Studio 将打开存档构建菜单,如下截图所示:

此过程创建一个存储在~/Library/Developer/Xcode/Archives的xarchive文件。验证…按钮将检查您的存档在上传过程中可能出现的任何潜在错误,而分发…实际上会将应用程序提交到商店。遗憾的是,在撰写本书时,分发…按钮仅启动应用程序加载器应用程序,该应用程序无法上传xarchive文件。在 Xamarin 解决这个问题之前,您可以通过在存档标签的窗口 | 组织者中导航到这些选项来访问 Xcode 中的存档选项。
继续在 Xcode 中定位存档;如果它没有出现,您可能需要重新启动 Xcode,并执行以下步骤:
-
点击分发…。不用担心,它将在上传之前验证存档。
-
选择提交到 iOS App Store并点击下一步。
-
使用您的 iTunes Connect 凭据登录并点击下一步。
-
选择适用于应用程序的适当配置文件,然后点击提交。
几分钟后,根据您应用程序的大小,您将获得一个确认屏幕,并且您应用程序的状态将变为上传已接收。以下截图显示了确认屏幕的外观:

如果您返回到 iTunes Connect,并导航到预发布标签,您将看到您刚刚上传的构建,状态为处理中:

几分钟后,构建过程将得到处理,并可以添加到 App Store 的发布中。下一步是选择版本标签下的构建,该标签位于构建部分,如下面的截图所示:

点击保存后,您应该能够点击提交审核而不会出现任何剩余警告。接下来,回答关于出口法律、广告标识等问题,并点击提交作为提交您的应用的最后一步。
在此阶段,当您的应用程序等待苹果员工审核时,您无法控制其状态。这可能需要一到两周,具体取决于待审核应用程序的工作量以及年份。更新也将通过相同的过程,但等待时间通常比新应用程序提交短一些。
幸运的是,有一些情况可以加快此过程。如果您导航到developer.apple.com/appstore/contact/?topic=expedite,您可以请求加快应用程序审核。您的问题必须是关键错误修复或与您的应用程序相关的紧急事件。苹果不保证接受加快请求,但在需要时它可能是一个救命稻草。
此外,如果您提交的构建过程中出现问题,您可以通过转到应用详情页面的顶部并选择 从审查中移除此版本 来取消提交。在提交后发现错误的情况下,这允许您上传一个新的构建来替换它。
为您的 Android 应用程序签名
所有 Android 包(apk 文件)都由证书或 keystore 文件签名,以便在设备上安装。当您在调试/开发应用程序时,您的包将自动由 Android SDK 生成的开发证书签名。对于开发或甚至测试版,使用此证书是可以的;然而,它不能用于分发到 Google Play 的应用程序。
要创建生产证书,我们可以使用 Android SDK 中包含的命令行工具 keytool。要创建自己的 keystore 文件,请在终端窗口中运行以下行:
keytool -genkey -v -keystore <filename>.keystore -alias <key-name> -keyalg RSA -keysize 2048 -validity 10000
将 <filename> 和 <key-name> 替换为您应用程序的适当术语。然后,keytool 命令行工具将提示您几个问题,以识别签名的应用程序的当事人。如果您以前曾经使用过 SSL 证书,这个过程非常相似。您还将被提示输入密钥库密码和密钥密码;您可以让它们相同,或者根据您希望密钥有多安全来更改它们。
您的控制台输出将类似于以下截图所示:

完成后,您应将您的 keystore 文件和密码存储在一个非常安全的地方。一旦您使用此 keystore 文件签名为应用程序签名并将其提交到 Google Play,您将无法不使用相同的密钥提交应用程序的更新。没有机制可以恢复丢失的 keystore 文件。如果您真的丢失了它,您唯一的选择是从商店中删除现有的应用程序,并提交一个包含您更新更改的新应用程序。这可能会使您失去很多用户。
要为 Android 包签名,您可以使用 Android SDK 中包含的另一个命令行工具 jarsigner。然而,Xamarin Studio 通过提供一个用户界面来运行您的包,简化了此过程。
在 Xamarin Studio 中打开您的 Android 项目,并按照以下步骤进行操作,以指导您完成签名为 apk 文件的流程:
-
将您的构建配置更改为 发布。
-
选择适当的项目,并导航到 项目 | 发布 Android 应用程序。
-
选择您刚刚创建的
keystore文件。 -
输入您在创建密钥时使用的 密码、别名 和 密钥密码 字段中的值。单击 前进。
-
选择一个目录来部署
apk文件,然后单击 创建。
当操作成功时,Xamarin Studio 将出现一个垫,显示进度。出现的垫看起来就像以下截图所示:

重要的是要注意,Xamarin.Android 在签名 APK 后会自动运行一个名为 zipalign 的第二个工具。此工具将 APK 内的字节对齐,以提高您应用的启动时间。如果您计划从命令行本身运行 jarsigner,则必须运行 zipalign。否则,应用将在启动时崩溃,Google Play 也不会接受该 APK。
提交应用至 Google Play
一旦您签名的 Android 包,将您的应用程序提交到 Google Play 相比 iOS 来说相对简单。所有操作都可以通过浏览器中的 开发者控制台 选项卡完成,无需上传带有 OS X 应用的包。
在开始提交之前,请确保您已经完成了以下清单上的任务:
-
您已声明了一个包含您的应用程序名称、包名称和图标的
AndroidManifest.xml文件。 -
您有一个使用生产密钥签名的
apk文件。 -
您已为 Google Play 选择了一个应用程序名称。这在商店中不是唯一的。
-
您有一个 512 x 512 的高分辨率图标图像用于 Google Play。
-
您有一个为商店编写的经过编辑的描述。
-
您至少有两个截图。然而,我建议您使用包括手机和 7 英寸及 10 英寸平板电脑尺寸在内的所有八个插槽。
在通过清单后,您应该已经完全准备好将您的应用程序提交到 Google Play。添加新应用的选项卡看起来如下截图所示:

首先,导航到 play.google.com/apps/publish 并登录您的账户,然后执行以下步骤:
-
选择 所有应用 选项卡,然后点击 添加新应用。
-
在 Google Play 上输入要显示的应用名称,然后点击 上传 APK。
-
点击 上传您的第一个生产 APK。
-
浏览到您的已签名
apk文件并点击 确定。您将看到 APK 选项卡的勾选标记变为绿色。 -
选择 商店列表 选项卡。
-
填写所有必填字段,包括 描述、高分辨率图标、分类 和 隐私政策(或选择表示您不提交政策的复选框),并至少提供两个截图。
-
点击 保存。您将看到 商店列表 选项卡上的勾选标记变为绿色。
-
选择 定价和分发 选项卡。
-
选择价格和您希望分发的国家。
-
接受 内容指南 和 美国出口法 的协议。
-
点击 保存。您将看到 定价和分发 选项卡上的勾选标记变为绿色。
-
在右上角选择 准备发布 下拉菜单,如图所示,然后选择 发布此应用:

几个小时后,你的应用程序将在 Google Play 上可用。不需要审批流程,并且对应用程序的更新同样痛苦。
Google Play 开发者计划政策
为了提供一个安全的商店环境,Google 会追溯删除违反其政策的应用程序,并且通常会删除整个开发者账户——而不仅仅是应用程序。Google 的政策旨在提高 Google Play 上应用程序的质量,并且与 iOS 上的规则集相比并不那么冗长。尽管如此,以下是对 Google 政策的简要概述:
-
应用程序不能包含色情内容、过度暴力和仇恨言论。
-
应用程序不能侵犯版权材料。
-
应用程序不能具有恶意性质,也不能在用户不知情的情况下捕获用户的私人信息。
-
应用程序在未经用户同意的情况下不能修改用户设备的基本功能(如修改主屏幕)。如果应用程序包含此功能,则必须让用户能够轻松关闭此功能。
-
你应用程序内的所有数字内容都必须使用 Google Play 的应用内计费(或应用内购买)。物理商品不能使用 IAP 购买。
-
应用程序不得滥用可能使用户产生高额账单的蜂窝网络使用。
与 iOS 一样,如果你对某项政策有疑问,最好是获取有关该政策的专业法律建议。要查看政策的完整列表,请访问play.google.com/about/developer-content-policy.html。

制作成功移动应用程序的技巧
从我的个人经验来看,我已经将使用 Xamarin 构建的应用程序提交到 iOS 应用商店和 Google Play 一段时间了。在交付了超过 50 个总下载量达到数百万的应用程序之后,有很多关于什么使一个移动应用程序成功或失败的经验教训要学习。Xamarin 应用程序对于最终用户来说与 Java 或 Objective-C 应用程序无法区分,因此你可以通过遵循与标准 iOS 或 Android 应用程序相同的模式来使你的应用程序成功。
你可以采取很多措施来使你的应用程序更加成功。以下是一些遵循的提示:
-
定价得当:如果你的应用程序几乎对任何人都有吸引力,考虑采用从广告位或应用内购买中获利的免费增值模式。然而,如果你的应用程序相当细分,将应用程序定价为 1.99 美元或更高将更有利。然而,高级应用程序必须保持更高的质量标准。
-
了解你的竞争对手:如果你的应用程序与同一空间中的其他应用程序竞争,请确保你的应用程序比竞争对手更好或提供更广泛的功能集。如果已经有几个与你的应用程序具有相同功能的应用程序,避免这个空间可能也是一个好主意。
-
提示忠实用户进行评论:在用户多次打开您的应用程序后请求评论是一个好主意。这给了真正喜欢您的应用程序的用户一个机会来写好评。
-
支持您的用户:提供一个有效的支持电子邮件地址或 Facebook 页面,以便您能够轻松与用户互动。回应错误报告和负面评论——Google Play 甚至有选项向在您的应用上写评论的用户发送电子邮件。在应用中直接添加反馈按钮也是一个很好的选择。
-
保持您的应用程序小巧:在 iOS 上保持在 100 MB 以下或在 Google Play 上保持在 50 MB 以下将允许用户在他们的蜂窝数据计划下下载您的应用程序。这样做消除了安装应用程序时的摩擦,因为用户会将漫长的下载与运行缓慢的应用程序联系起来。
-
将您的应用提交到审查网站:尽可能在网络上获取尽可能多的评论。苹果提供了发送优惠券码的功能,但对于您应用的安卓版本,您可以发送实际的安卓包。将您的应用发送到审查网站或流行的 YouTube 频道可以是一种很好的免费广告方式。
-
使用应用分析或跟踪服务:报告您应用的用法和崩溃报告对于理解您的用户非常有帮助。修复野外的崩溃和修改用户界面以改善消费行为非常重要。这些例子包括将 Google Analytics 或 Flurry Analytics 添加到您的应用中。
成功的移动应用程序没有银弹。如果您的应用程序引人入胜,满足需求,并且运行快速且正确,您可能会手握下一个热门产品。能够使用 Xamarin 提供一致的跨平台体验也将使您在竞争对手中占据优势。
摘要
在本章中,我们涵盖了您需要了解的一切,以便将您的应用程序提交到 iOS App Store 和 Google Play。我们涵盖了 App Store 审查指南,并将其简化为在审批过程中可能遇到的最常见情况。我们介绍了为您的应用元数据设置流程以及将二进制文件上传到 iTunes Connect 的过程。对于安卓,我们介绍了如何创建生产签名密钥并签名您的安卓包(apk)文件。我们还介绍了如何提交应用程序到 Google Play,并在本章结束时提供了关于如何成功并可能盈利地将应用程序提交到应用商店的技巧。
我希望这本书能让您体验到一个端到端、实用的开发真实世界、跨平台应用程序的流程,使用 Xamarin。与替代品相比,C# 是一种如此出色的语言,您应该非常高效。此外,您将通过共享代码来节省时间,而不会以任何方式限制用户的原生体验。




浙公网安备 33010602011771号