精通-Xamarin-跨平台开发-全-
精通 Xamarin 跨平台开发(全)
原文:
zh.annas-archive.org/md5/b75a6a1d71b5df38c24115400d32dbdd译者:飞龙
前言
尽管它最初是一个社区努力,旨在将.NET 库和通用语言运行时编译器移植到各种操作系统,但 Xamarin 产品套件很快成为开发 Android 和 iOS 操作系统应用程序的公共平台,使用.NET 框架和最受欢迎的 CLR 语言 C#。Xamarin 开发平台的出现创造了一个新的开发领域,为多种平台提供产品,同时让用户将现有的.NET 开发技能适应这些新平台,并为更广泛的设备和操作系统生成应用程序。多亏了 Xamarin,开发者现在正享受着一个新时代,开发努力不再仅针对单一的应用程序平台,而是跨越多个设备,包括智能手机、平板电脑、个人电脑,甚至可穿戴设备,创建高度高效的本地应用程序。
本书涵盖内容
第一章,使用 Xamarin 进行开发,深入探讨了目标平台上的 Xamarin 框架和架构。它还包括了关于准备 Xamarin 开发环境的入门信息和技巧。
第二章,内存管理,探讨了使用 Xamarin 运行时在 iOS 和 Android 上如何管理内存。在将.NET 平台进行类比的同时,它提供了陷阱、模式和最佳实践的示例。
第三章,异步编程,深入探讨了异步和多线程编程概念。讨论了不同平台上各种线程场景中的特定平台问题。
第四章,本地数据管理,提供了有用的模式和技巧,以高效地在移动设备上使用、管理和漫游数据,使用 Xamarin。
第五章,网络,详细介绍了 Xamarin 应用程序的网络功能以及各种服务集成场景。网络实现通过实际示例进行说明,包括使用本地存储进行数据缓存。
第六章,平台扩展,专注于特定平台的 API 和功能。它解释了可以在 Xamarin 应用程序中使用的某些外围设备。本章还包括了查看原生库以及如何在跨平台 Xamarin 应用程序中包含它们的说明。
第七章,视图元素,提供了关于 UX(用户体验)和设计概念的基础信息,以及 Xamarin 平台上设计原则之间差异和相似性的解释。
第八章,Xamarin.Forms,专注于 Xamarin 的各种特性和可扩展性选项。它还涵盖了表单扩展模块以及如何使用它来在多个平台上生成一致的用户界面。
第九章,可重用 UI 模式,讨论了在跨平台项目中重用视觉资源的策略和模式。还分析了关于 MVC 和 MVVM 模式的一些高级软件架构主题,并进行了演示。
第十章,ALM – 开发人员和 QA,为 Xamarin 跨平台应用程序的 ALM(应用程序生命周期管理)和持续集成方法提供了介绍。作为 ALM 过程中与开发者最相关的部分,讨论并演示了单元测试策略以及自动化的 UI 测试。
第十一章,ALM – 项目和发布管理,解释了版本控制和自动化持续集成工作流程的基本要素。演示了源代码控制选项以及 Xamarin 项目的自动化构建策略。
第十二章,ALM – 应用商店和发布,解释了与应用程序生命周期最后一步相关的应用程序包准备和发布过程。
您需要这本书的内容
为了构建示例项目并使用本书中的代码示例,您将需要一个针对您想要的目标平台(Xamarin.iOS 和/或 Xamarin.Android)的订阅。大多数诊断工具都是作为目标平台开发 SDK 的一部分分发的。作为开发 IDE,如果您使用的是基于 Windows 的开发环境或正在配置开发环境,则需要 Visual Studio 2013(或更高版本)或 Xamarin Studio;否则,只需 Xamarin Studio。对于测试和诊断,可以使用真实移动设备或 SDK 提供的模拟器。
本书面向的对象
本书非常适合那些希望将他们的 Xamarin 移动开发技能从新手或中级水平提升到下一个层次,成为他们组织中的首选人员。为了完全理解所描述的模式和概念,您应该具备合理的知识水平,并理解使用 Xamarin 进行跨平台应用程序开发的核心元素。
惯例
在本书中,您将找到许多用于区分不同类型信息的文本样式。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“例如,Objective-C 类型如NSObject、NSString、NSArray在 C#中暴露,并提供对底层类型的绑定。”
代码块设置如下:
namespace Master.Xamarin.Portable
{
    public class MyPhotoViewer
    {
        private readonly IStorageManager m_StorageManager;
任何命令行输入或输出都按照以下方式编写:
bcdedit /copy {current} /d "No Hyper-V"
bcdedit /set {<identifier from previous command>} hypervisorlaunchtype off
新术语和重要单词以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,在文本中如下所示:“它仍然可以通过 Xamarin Studio 中项目菜单下的运行与 Mono HeapShot菜单项访问。”
注意
警告或重要注意事项以如下框的形式出现。
小贴士
小技巧和技巧如下所示。
读者反馈
我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。
要向我们发送一般反馈,请简单地发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍标题。
如果您在某个主题领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从购买中获得最大收益。
下载示例代码
您可以从www.packtpub.com的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
- 
使用您的电子邮件地址和密码登录或注册我们的网站。 
- 
将鼠标指针悬停在顶部的支持标签上。 
- 
点击代码下载与勘误。 
- 
在搜索框中输入书籍名称。 
- 
选择您想要下载代码文件的书籍。 
- 
从下拉菜单中选择您购买这本书的地方。 
- 
点击代码下载。 
一旦文件下载完成,请确保您使用最新版本的软件解压缩或提取文件夹:
- 
WinRAR / 7-Zip for Windows 
- 
Zipeg / iZip / UnRarX for Mac 
- 
7-Zip / PeaZip for Linux 
勘误
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误表,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分。
盗版
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com> 联系我们,并提供疑似盗版材料的链接。
我们感谢您在保护我们作者和我们提供有价值内容的能力方面的帮助。
询问
如果您对本书的任何方面有问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。
第一章. 使用 Xamarin 进行开发
本章探讨了 Xamarin 框架和架构在不同目标平台上的情况,并确定了差异和相似之处。它还包括有关为 Xamarin 准备开发环境的入门信息和技巧,并涵盖了 Xamarin 开发的一些基本要素。本章分为以下部分:
- 
使用 Xamarin 的跨平台项目 
- 
目标平台 
- 
设置开发环境 
- 
模拟器选项 
- 
典型的 Xamarin 解决方案结构 
- 
跨平台开发的质量 
使用 Xamarin 的跨平台项目
开发者正在享受一个新时代,在这个时代中,开发不再局限于单一的应用程序平台,而是跨越各种媒体,如手机、平板电脑、个人电脑,甚至可穿戴设备。开发项目之间的共享代码和资源提高了工作的优雅性和质量。此外,健壮性、维护多平台应用程序所需的努力以及可重用模块之间存在直接相关性。
通用应用程序是一个以前用来识别针对运行 iOS 操作系统(iPhone 和 iPad)的设备的应用程序的术语。然而,现在这个术语也用来描述 Windows Runtime 应用程序(Windows Store 和 Windows Phone 8.1 - WinRT)以及针对手机和平板电脑的 Android 应用程序。随着 Xamarin 的发布,一个真正的通用应用程序概念诞生了。当考虑 Xamarin 应用程序时,术语“通用”指的是在所有三个平台上运行并适应系统资源的应用程序。
在这个通用应用程序的背景下,开发者现在发现很难获取所有三个平台上的常见任务的必要解决方案。此外,将每个平台视为一个独立的项目进行开发会导致开发者的时间浪费,尽管这种应用程序的主要驱动因素,即数据、业务逻辑和 UI,在所有平台上在概念上几乎是相同的。
Xamarin 平台的开发策略和模式,其中一些在本书的其余部分有所描述,试图解决一些这些问题,并为开发者提供生产跨平台、可管理和高质量产品的工具和策略。
Xamarin 作为一个平台
Xamarin 最初诞生于一个社区努力,旨在将.NET 库和通用语言运行时编译器移植到不同的操作系统。最初的尝试旨在创建一组二进制文件,用于在 Unix 平台上的 C#(.NET 的本土语言)编写应用程序的开发、编译和运行。这个项目,Mono,后来被移植到许多其他操作系统,包括 iOS(Mono-Touch)和 Android(Mono for Android)。
Xamarin 开发平台的兴起创造了一个新的开发细分市场,即在同一时间针对三个不同的平台创建产品,同时允许用户将现有的.NET 开发技能适应这些新平台,并为更广泛的设备和操作系统生成应用程序。
注意
自从早期阶段开始,微软一直是 Xamarin 平台和工具集的强力支持者。正如你将在本章剩余部分看到的那样,Xamarin 工具已完全集成到 Visual Studio 中,并最终包含在 Visual Studio 2015 的设置中。这种合作关系一直持续到 Xamarin 最终被微软收购,这一消息于 2016 年 3 月公开宣布。
Xamarin 为每个提到的平台都提供了编译器,以便在.NET 框架(类似)中编写的代码编译成原生应用程序。这个过程提供了高度高效的应用程序,与解释型移动 HTML 应用程序大相径庭。
除了原生编译,Xamarin 还提供了对强类型平台特定功能的访问。这些功能以健壮的方式使用,在编译时绑定到底层平台。平台特定的执行也可以通过原生调用进行扩展,这是通过互操作库实现的。
作为一款产品,Xamarin
作为一套开发工具,Xamarin 有不同的版本。具有不同知识和经验集合的开发者可以使用这些工具根据他们的需求设置自己的开发环境。Xamarin 开发环境可以在不同的操作系统上配置。然而,目前还不能在同一个操作系统上为所有三个平台进行开发。
对于希望使用熟悉的 Visual Studio 界面并利用现有技能的开发者,Xamarin 的 Visual Studio 扩展提供了合适的选项。一旦安装了扩展,环境就准备好开发 Android 和 Windows Phone 应用程序。这个扩展让开发者能够充分利用 Visual Studio,其中包括这两个平台的设计师。为了开发 iOS 应用程序,你需要通过所谓的 Visual Studio 与 Apple OS X 构建机器的配对过程。构建机器反过来用于在开发环境(Visual Studio)中可视化故事板、编译 iOS 代码和调试应用程序。
第二种选择是使用 Xamarin Studio。Xamarin Studio 是一个完整的 IDE,它具有一些你从 Visual Studio 熟悉的特性,例如智能代码补全(IntelliSense)、代码分析和代码格式化。如果你在 Apple OS X 上运行 Xamarin Studio,你可以使用这个 IDE 为 Android 和 iOS 平台开发。然而,在 Windows 上使用 Xamarin Studio 时,你只能针对 Android 平台。
这个开发套件的一个重要部分是实时监控工具,称为 Xamarin Insights。Xamarin Insights 允许开发者监控他们的实时应用程序,以帮助检测和诊断性能问题和异常,并了解应用程序的使用情况。Xamarin Insights 还可以连接到其他应用程序,例如,应用程序错误可以直接推送到错误跟踪系统。
目标平台
如前所述,Xamarin 创建了一个新的平台,其中开发工作针对多个操作系统和各种设备。最重要的是,编译的应用程序不是运行解释序列,而是拥有原生代码库(如 Xamarin.iOS)或集成的.NET 应用程序运行时(如 Xamarin.Android)。本质上,Xamarin 用编译的二进制文件和执行上下文替换了.NET 应用程序的公共语言运行时(CLR)和 IL,所谓的mono 运行时。
Xamarin on Android
在 Android 应用程序中,mono 运行时直接放置在 Linux 内核之上。这创建了一个与 Android 运行时的并行执行上下文。然后,Xamarin 代码被编译成 IL,并由 mono 运行时访问。另一方面,Android 运行时通过所谓的托管可调用包装器(MCW)访问,这是一个在两个运行时之间的打包包装器。MCW 层负责将托管类型转换为 Android 运行时类型,并在执行时间调用 Android 代码。每当.NET 代码需要调用 Java 代码时,都会使用这个 JNI(Java 互操作)桥。MCW 提供了一系列应用,包括继承 Java 类型、重写方法和实现 Java 接口。
下面的图像显示了 Xamarin.Android 架构:

图 1:Xamarin.Android 架构
Android.*和Java.*命名空间在 MCWs 中用于访问 Android 运行时和 Java API 中特定于设备和平台的功能,例如音频、图形、OpenGL 和电话等功能。
使用互操作库,也可以在 Xamarin.Android 的执行上下文中加载原生库并执行原生代码。在这种情况下,反向回调执行通过Android Callable Wrappers(ACW)处理。ACW 是一个 JNI 桥,允许 Android 运行时访问.NET 域。对于每个直接或间接与 Java 类型(继承自Java.Lang.Object的类型)相关的托管类,在编译时生成一个 ACW。
Xamarin on iOS
在 iOS 应用程序中,根据 iOS SDK 协议,使用集成的并行运行时是不允许的(遗憾的是)。根据 iOS SDK 协议,只有当所有脚本和代码都由 Apple 的 WebKit 框架下载和运行时,才能使用解释代码。
在此限制条件下,开发者仍然可以在.NET 平台上开发应用程序并在其他三个平台上共享代码。在编译时,项目首先被编译成 IL 代码,然后(使用 Mono Touch Ahead-Of-Time 编译器—mtouch)编译成静态原生 iOS 位代码。这意味着使用 Xamarin 开发的 iOS 应用程序是完全的原生应用程序。

图 2:Xamarin.iOS 编译
与 Xamarin.Android 类似,Xamarin.iOS 也包含一个互操作引擎,它将.NET 世界与 Objective-C 世界连接起来。通过这个桥梁,在ObjCRuntime命名空间下,用户能够访问 iOS 基于 C 的 API,以及使用 Foundation 命名空间,并且可以使用和从原生类型派生,以及访问 Objective-C 属性。例如,Objective-C 类型如NSObject、NSString和NSArray在 C#中暴露并提供对底层类型的绑定。这些类型可以作为内存引用或作为强类型对象使用。这提高了开发体验,并增加了类型安全性。
这种静态编译是使用 Windows 平台上的 Xamarin 开发 iOS 应用程序时使用构建机器的主要原因。因此,在 Xamarin.iOS 中没有反向回调功能,即从.NET 代码到原生运行时的调用得到支持,但原生代码回.NET 域的调用则不支持。由于 Xamarin.iOS 应用程序的编译方式,还有其他一些功能被禁用。例如,不允许泛型类型从NSObject继承。另一个重要的限制是,在运行时不允许创建动态类型,这反过来又禁用了在 Xamarin.iOS 应用程序中使用动态关键字。
注意
与其他平台相比,如果 Xamarin.iOS 应用程序包是在调试配置中构建的,它们的大小比它们的发布版本要大得多。这些包被工具化了,但没有被链接器优化。在 Xamarin.iOS 应用程序中不允许对这些包进行性能分析。
与 Xamarin.Android 开发类似,使用 Xamarin.iOS,也可以从托管代码中重用原生代码和库。为此,Xamarin 提供了一个名为绑定库的项目模板。绑定库帮助开发者创建原生 Objective-C 代码的托管包装器。
Windows Runtime 应用程序
尽管 Xamarin 没有将 Windows Runtime 作为目标平台,也没有为它提供专门的工具(除了 Xamarin.Forms 之外),但涉及 Xamarin 的跨平台项目可以,并且通常包括 Windows Runtime 项目。由于.NET 和 C#是 Windows Runtime 的本地语言,大多数共享项目(如可移植库、共享项目和 Xamarin.Forms 项目)可以在 Windows Runtime 中重用,无需进一步修改。
使用 Windows Runtime,开发者可以创建 Windows Phone 8.1 和 Windows Store 应用程序。Windows Phone 8 和 Windows Phone 8.1 Silverlight 也可以作为目标并包含在 PCL 描述中。
设置开发环境
Xamarin 项目可以在各种开发环境中进行。由于此类项目涉及多个平台,操作系统、IDE 选择和配置都是准备过程中的关键部分。
注意
环境设置不仅取决于目标应用平台,还取决于 Xamarin 许可证。不同许可选项和定价信息可以在 Xamarin 网站上找到(store.xamarin.com/)。
选择合适的开发操作系统
Android 应用程序可以使用安装了 Xamarin 扩展的 Xamarin Studio 和 Visual Studio 在 Windows 上进行开发和编译,也可以在安装了 Xamarin Studio for Mac 的 Apple OS X 操作系统上进行。
对于 iOS 应用程序开发,无论是在 Windows 上的 Visual Studio 还是 Apple OS X 上的 Xamarin Studio,都需要一台至少运行 OS X Mountain Lion 的 Apple Macintosh 计算机。构建机器应安装 Xcode 开发工具和 iOS SDK,以及安装了 Xamarin.iOS 套件。
另一方面,Windows Store 应用程序只能在 Windows 平台上进行开发。
| Apple OS X | Microsoft Windows | |
|---|---|---|
| Xamarin Studio | Xamarin Studio | |
| iOS 应用 | 是 | |
| Android 应用 | 是 | 是 | 
| Windows Store 应用 | 
图 3:OS X 和 Windows 上的开发 IDE
在虚拟化方面,开发者也受到限制。根据最终用户协议,OS X 不能安装在非 Apple 品牌的机器上并运行,也不能进行虚拟化。另一方面,你可以在 OS X 开发机器上设置一个虚拟机,用于 Microsoft Windows 和 Visual Studio。然而,在这种情况下,系统应运行嵌套虚拟化以运行 Windows Phone 和 Android 模拟器的 Visual Studio for Windows。尽管 Parallels 和 VMWare Fusion 支持嵌套虚拟化,但 Microsoft 不支持嵌套 Hyper-V,因此此类机器可能不稳定。
Xamarin Studio 设置和配置
Xamarin Studio 可以在 Windows 和 OS X 操作系统上设置。开发者可以从 www.xamarin.com 下载它并遵循安装说明。对于目标平台(例如,Xamarin.iOS、Xamarin.Android 等)以及这些平台的依赖项(例如,Android SDK)应下载并安装到开发机器上。对于 OS X,必须单独安装并配置的一个必需组件是带有 Xcode 开发环境的 iOS SDK。

图 4:Mavericks(OS X 10.9)上的 Xamarin 设置
在 Microsoft Windows 上,重要的是要提到,Xamarin Studio 仅支持 Android 应用程序的开发。Windows Phone 或 iOS 应用程序(即使是远程构建机器)的项目都不能在 Windows 上的 Xamarin Studio 中查看、修改或编译。

图 5:在 OS X 上设置 Xamarin 开发环境
在 OS X 上开发时,与 iOS 和 Android 一起开发 Windows Phone 应用程序的唯一选项是使用 Windows 虚拟机,并使 Visual Studio 与 Xamarin Studio 并行运行。这种设置对于使用 Team Foundation Server 作为源控制的开发者也有帮助,因为他们可以使用 Visual Studio 客户端提供的增强集成,而不是独立的 TFS Everywhere。还可以设置,使操作系统主机机可以与 Visual Studio 配对,成为 iOS 应用程序的构建主机。
Visual Studio 设置和配置
对于 Xamarin 项目,典型的 Windows 开发平台配置包括 Visual Studio 2013 或 2015、Apple OS X 构建主机以及 Hyper-V 和/或 VirtualBox,以便能够使用 Android 和 Windows Phone 模拟器。Xamarin.iOS 应用程序随后在 Apple OS X 构建主机上编译和模拟。

图 6:Windows 平台 Xamarin 开发环境
注意
尽管在 Microsoft Windows 环境中使用虚拟机运行 OS X 在技术上可行,但 Apple 的许可协议不允许这样做:
"2.H. 其他使用限制:本许可证中规定的授予并不允许您,您同意不,在任何非 Apple 品牌的计算机上安装、使用或运行 Apple 软件,或允许他人这样做。"
在 Microsoft Windows 上,Xamarin 的安装与在 Apple OS X 上的 Xamarin Studio 设置类似。所有 Xamarin 开发的先决条件都包含在 Xamarin for Windows 软件包中,以及 Visual Studio 扩展程序。

图 7:Visual Studio 2015 设置
OS X 和 Microsoft Windows 之间的一个主要区别是,Visual Studio 2015 现在包括跨平台开发工具,如 Android SDK、开发套件和 Xamarin 项目模板。因此,Xamarin 的安装仅负责安装请求平台的扩展(即 Xamarin.iOS 和/或 Xamarin.Android)。
为了使用 Visual Studio 开发和测试 iOS 应用,以及可视化并编辑故事板,必须将 Apple OS X 机器连接到 Visual Studio 作为构建主机。Xamarin 4.0 引入了 Xamarin Mac Agent 的概念,这是在 OS X 机器上运行的后台进程,它为 Visual Studio 提供所需的 SSH 连接(通过端口 22 的安全连接)。在 Xamarin 4.0 之前,构建主机机器需要运行所谓的 Mac 构建主机,该主机用于将 Mac 主机与 Visual Studio 配对。Xamarin Mac Agent 的唯一先决条件是在 Windows 工作站和 OS X 构建主机上安装了 Xamarin.iOS,并且构建主机为当前用户启用了远程登录。在 Visual Studio 中,查找 Xamarin Mac Agent 对话框有助于建立远程连接。

图 8:Xamarin.iOS 构建主机
需要记住的是,与 Visual Studio 配对的 Mac 机器必须安装带有 iOS SDK 的 Xcode。还必须在 Xcode 的账户配置部分添加一个开发者账户(无论是否已注册到应用开发者计划)。
注意
如果与 Xcode 关联的账户没有为开发者计划支付订阅,iOS 项目的平台只能设置为模拟器和调试选择中的一个模拟器选项,而不是实际设备。否则,用户将看到一个错误消息,例如,在密钥链中找不到有效的 iOS 签名密钥。
模拟器选项
对于目标平台和开发环境,有许多编译的 Xamarin 项目的模拟器。开发者在使用 Android 平台的模拟器时最具灵活性,而 iOS 和 Windows 商店应用的选项仅限于 SDK 提供的模拟器。
Android 模拟器
Android 应用可以在 Microsoft Windows 和 Apple OS X 操作系统上的多个模拟器上运行和测试。
Android SDK 随带默认的模拟器,该模拟器安装在开发机器上。这种仿真选项在 OS X 和 Windows 操作系统上均可用。

图 9:AVD 和 Genymotion 模拟器
此 Android 模拟器使用 Android 虚拟设备(AVD)来模拟 Linux 内核和 Android 运行时。它不需要任何额外的虚拟化软件来运行,然而,缺乏虚拟化支持使得 AVD 的响应速度大大降低,并且启动时间相对较长。它还提供了广泛的仿真选项供开发者使用,从短信和电话到硬件、外围设备和电源事件。
Genymotion 模拟器(www.genymotion.com/)是 Xamarin 和 Android 开发者最受欢迎的模拟选项之一。尽管它提供免费许可证,但免费版本仅允许 GPS 和摄像头模拟。Genymotion 模拟器在 VirtualBox 虚拟化软件上运行(并与其一起安装)。
小贴士
VirtualBox 与 Hyper-V 一起使用
VirtualBox 软件不能与 Hyper-V 虚拟化软件同时运行,后者是 Windows Phone 开发和 Windows 操作系统上模拟所必需的。为了使用 Windows Phone 模拟器和 Genymotion Android 模拟器,您可以在 Windows 启动时创建一个双启动选项来禁用和启用 Hyper-V。
bcdedit /copy {current} /d "No Hyper-V"
bcdedit /set {<identifier from previous command>} hypervisorlaunchtype off
这将创建一个第二个启动选项,以便在没有 Hyper-V 功能的情况下启动 Windows,这样虚拟化就可以由 VirtualBox 使用。
最后也是最新的 Android 模拟选项是 Visual Studio Android 模拟器。这个 Android 模拟器在 Hyper-V 上运行,并为开发者提供各种设备 API 版本和模拟选项。

图 10:Visual Studio Android 模拟器
Visual Studio Android 模拟器是 Visual Studio 2015 安装的一部分,也可以稍后作为扩展安装。该模拟器提供了与 Windows Phone 模拟器相似的经验,并允许开发者和测试人员使用几乎相同的模拟选项,包括不同的设备配置文件以及不同的 API 级别。
iOS 模拟
iOS 模拟仅适用于 Xcode 工具和 iOS SDK。iOS 模拟器可以直接在开发 Xamarin Studio 时在 Apple OS X 上启动,或者通过将构建机器与在 Microsoft Windows 上运行的 Visual Studio Xamarin 扩展配对来启动。它还可以用来测试 iPhone 和 iPad 应用程序。
典型的 Xamarin 解决方案结构
一个 Xamarin 解决方案可以由不同类型的项目组成。其中一些项目是特定平台的项目,而其他项目是共享项目类型或模块,这使得跨平台重用代码成为可能。

图 11:Visual Studio 和 Xamarin Studio 上的 Xamarin 项目解决方案结构
便携式类库
便携式类库是跨平台项目之间共享代码最常见的方式。PCLs 提供了一套公共参考程序集,使得.NET 库和二进制文件可以在任何基于.NET 的运行时或与 Xamarin 编译器中使用——从手机到客户端,再到服务器和云。PCL 模块设计为仅使用.NET 框架的特定子集,并且可以设置为针对不同的平台。

图 12:便携式类库目标
微软为每个目标组合指定了一个名称,每个配置文件也都有一个 NuGet 目标。通过 NuGet 发布了适用于可移植类库的 .NET 库子集,这是在 Visual Studio 2013 发布时实现的。这使得开发者能够通过 NuGet 包发布他们的工作,针对广泛的移动平台(有关更多信息,请参阅 NuGet 包 部分)。
注意
目前首选的配置文件和 Xamarin 项目的最大公约数子集是所谓的 Profile 259。微软对该配置文件的支持指定为 .NET 可移植子集 (.NET Framework 4.5, Windows 8, Windows Phone 8.1, Windows Phone Silverlight 8),而 NuGet 目标框架配置文件是 portable-net45+netcore45+wpa81+wp8。
在创建 PCL 时,最大的缺点是项目不能包含或引用任何平台特定代码。通常,通过抽象平台特定需求或使用依赖注入或类似方法在平台特定项目中引入实现来解决这个限制。
例如,在下面的设备特定外设示例中,公共可移植类库有一个接受两个单独接口的构造函数,这些接口可以通过依赖注入容器注入,或者可以通过设备特定的实现进行初始化。作为回报,公共库创建了一个业务逻辑实现,如下所示:
namespace Master.Xamarin.Portable
{
    public class MyPhotoViewer
    {
        private readonly IStorageManager m_StorageManager;
        private readonly ICameraManager m_CameraManager;
        public MyPhotoViewer(IStorageManager storageManager, ICameraManager cameraManager)
        {
            m_StorageManager = storageManager;
            m_CameraManager = cameraManager;
        }
        public async Task TakePhotoAsync()
        {
            var photoFileIdentifier = await m_CameraManager.TakePhotoAndStoreAsync();
            var photoData = await m_StorageManager.RetrieveFileAsync(photoFileIdentifier);
            // TODO: Do something with the photo buffer
        }
    }
    /// <summary>
    /// Should be implemented in Platform Specific Library
    /// </summary>
    public interface IStorageManager
    {
        Task<string> StoreFileAsync(byte[] buffer);
        Task<byte[]> RetrieveFileAsync(string fileIdentifier);
    }
    /// <summary>
    /// Should be implemented in Platform Specific Library
    /// </summary>
    public interface ICameraManager
    {
        Task<string> TakePhotoAndStoreAsync();
    }
}
共享项目
“共享项目”这个术语最初是由微软团队在发布适用于 Windows Phone 和 Windows Runtime 的通用应用时提出的(即 Visual Studio 2013)。随着 Xamarin 的到来,共享项目也可以被 Android 和 iOS 项目引用。这类项目本质上是对共享代码和资源文件的包装或容器,这些代码和资源文件链接到多个项目和平台。共享文件资源将在引用项目中稍后包含,并作为这些模块的一部分进行编译。

图 13:共享项目
在使用共享项目时,开发者应小心包含平台特定代码,因为共享元素将被包含在所有引用项目中并编译。可以在共享项目中引入编译器指令(例如,#if __ANDROID__)来表示代码的某些部分仅适用于特定平台。
小贴士
在共享项目中可视化平台特定代码
使用 Visual Studio (2013 或更高版本) 时,可以根据条件编译常量的组合可视化不同的执行路径。

图 14:Visual Studio 共享项目编辑器
Visual Studio 在编辑器窗口的右上角提供了一个下拉菜单,用于确定引用共享项目的平台特定项目。通过选择项目,你可以看到根据目标平台禁用的代码部分。
如果我们使用相同的示例来拍照,我们需要为同一操作创建两个完全不同的实现,如下所示:
private async Task<string> TakePhotoAsync()
{
    string resultingFilePath = "";
    var fileName = String.Format("testPhoto_{0}.jpg", Guid.NewGuid());
#if __ANDROID__
    Intent intent = new Intent(MediaStore.ActionImageCapture);
    var file = new File(m_Directory, fileName);
    intent.PutExtra(MediaStore.ExtraOutput, Net.Uri.FromFile(_file));
    // TODO: Need an event handler with TaskCompletionSource for
    // Intent's result
    m_CurrentActivity.StartActivityForResult(intent, 0);
    resultingFile = file.AbsolutePath;
#elif WINDOWS_PHONE_APP
    ImageEncodingProperties imgFormat = ImageEncodingProperties.CreateJpeg();
    // create storage file in local app storage   
    var file = await LocalStore.CreateFileAsync(fileName);
    resultingFilePath = file.Path;
    // take photo   
    await capture.CapturePhotoToStorageFileAsync(imgFormat, file);
#endif
    return resultingFile;
}
Xamarin.Forms
Xamarin.Forms 是用于创建目标平台 UI 实现的统一库,这些 UI 实现将使用原生控件进行渲染。Xamarin.Forms 项目通常作为 PCL 项目创建,并且可以被 Xamarin.iOS、Xamarin.Android 和 Windows Phone 开发项目引用。Xamarin.Forms 组件也可以包含在共享项目中,并可以利用平台特定的功能。
开发者可以使用这些表单有效地创建常见的 UI 实现,无论是声明式(使用 XAML),还是通过使用提供的 API。这些由 Xamarin.Forms 组件构建的视图,在运行时使用特定平台的控件进行渲染。
开发项目可以通过创建数据访问模型直到 UI 组件的共享实现来实现,从而将平台之间的共享代码量提高到 90% 或更多。
NuGet 包
NuGet 最初是微软的一个开源倡议,旨在在开发者之间共享代码,现在已经成为一个更大的生态系统。虽然 NuGet 服务器可以用作开源库共享平台,但许多开发团队将 NuGet 用作私有公司仓库,用于存储编译后的库。
NuGet 打包是适用于 Xamarin 项目的可行代码共享和重用策略,因为它得到了 Xamarin Studio 和 Visual Studio(在安装 Visual Studio 2012 之后无需进一步安装)的支持。
Xamarin 项目的 NuGet 目标框架名称为 mono,还有进一步的分组,例如 MonoAndroid10,它指的是目标框架为 MonoAndroid 1.0 或更高版本的项目。其他平台目标包括:
- 
MonoAndroid: Xamarin.Android 
- 
Xamarin.iOS: Xamarin.iOS 统一 API(支持 64 位) 
- 
Xamarin.Mac: Xamarin.Mac 的移动配置文件 
- 
MonoTouch: iOS 经典 API 
开发者可以自由地重用公开可用的 NuGet 包或创建自己的仓库来存储编译包,以便包含在 Xamarin 项目中。
小贴士
在 Visual Studio 2015 中创建 NuGet 包
随着 Visual Studio 2015 的发布,有一个新的项目模板可以帮助开发者创建和重用 NuGet 包。

图 15:Visual Studio NuGet 包项目模板
关于创建 NuGet 包和发布的更多信息可以在 NuGet 文档中心找到:(docs.nuget.org/create/creating-and-publishing-a-package)
组件
组件是另一种在 Xamarin 项目中重用编译好的库和模块的方法。组件存储库集成在 Xamarin Studio 和 Visual Studio 中,并且自 2013 年发布以来已经收集了来自开发者的许多可重用提交。组件可以通过使用 Xamarin 组件存储库以与 NuGet 包相同的方式下载并安装到项目中。Xamarin 组件存储库可以在components.xamarin.com找到。
跨平台开发中的质量
一些开发术语有助于开发者在为多个平台开发时创建健壮、可维护、高质量的代码。这些代码描述符帮助开发团队识别架构问题、软件问题和随机错误。
可重用性
"在整个项目中可以重用多少代码?"
可重用性是跨平台开发项目中关键的质量标识之一。Xamarin,特别是随着 Xamarin.Forms 的发布,为开发者提供了丰富的资源来创建平台无关的组件,这可以减少冗余并减少复杂项目中的开发时间。由 Visual Studio 生成的代码质量矩阵和单元测试覆盖率结果可以将这个描述符转化为可衡量的度量。
抽象
"共享组件对平台了解多少?"
在跨平台解决方案中,几乎不可避免地要包含特定平台的代码片段。这些模块抽象化的程度提高了共享组件的鲁棒性,并且与实现逻辑与底层平台耦合的松紧程度密切相关。通过这种方式,共享组件可以很容易地使用模拟或伪造的库进行测试,而无需创建特定平台的测试框架。单元测试代码覆盖率结果有助于确定应用程序的可测试性。
松耦合
"将项目移植到另一个平台有多容易?"
在特定平台的抽象实现之上,一个自主的共享实现层创建了灵活的解决方案,这些解决方案可以轻松地适应其他平台。减少共享逻辑对底层平台的依赖不仅本质上增加了可重用性,也提高了开发项目的敏捷性。共享项目中针对底层平台的条件编译块或if或else循环的数量确定了根据平台执行的代码量。
原生性
"你的应用程序有多少程度与平台融合在一起?"
尽管使用 Xamarin 开发时的最终目标是创建一个可以轻松编译到多个目标的应用程序,但使用 Xamarin 创建的应用程序应该看起来、感觉和表现如同为该特定平台设计。在创建共同基础的同时,应尊重每个平台的 UI 范式和用户交互机制。与上述代码描述相比,原生性更多的是一个名义上的和主观的衡量标准。
摘要
在本章中,我们讨论了 Xamarin 开发套件的一些关键特性,以及在这些先前描述的平台上的开发,并探讨了开发移动应用时 Xamarin 的基本要素。接下来的章节将参考这些关键特性以及平台之间的差异,以识别创建 Xamarin 跨平台应用的有价值模式和策略。
我们还讨论了目标平台的架构概述以及在这些平台上如何开发和编译 Xamarin 应用程序。这些平台之间最重要的区别是,Xamarin.Android(以及 Windows Phone)使用.NET 二进制文件和 Mono(以及.NET)运行时来执行代码,而 Xamarin.iOS 应用程序具有完全不同的设置和双编译(编译时提前)来利用.NET 二进制文件,但不是直接运行它们。
在使用 Xamarin 为 Android 和 iOS 平台开发时,开发者还被迫在不同的操作系统平台和开发 IDE 之间做出选择。开发环境的选取和配置取决于目标平台。IDE 功能、模拟器和仿真器选项在此选择中扮演着重要角色。虽然 OS X 操作系统与 Xamarin Studio 提供了一个熟悉的界面,并允许开发者转移他们的.NET 相关技能和知识,但目前它们对于开发 iOS 应用来说是一个更可行的选择。
另一个重要的更新是 Xamarin 解决方案结构。我们讨论了开发者如何在不同的平台之间共享代码,以及如何重用公共或私有存储库来包含共享模块。共享项目是大多数跨平台开发模式和策略的基础,与可移植类库一起。
总体而言,当使用 Xamarin 规范和功能时,开发者的主要目标应该是创建松散耦合、平台无关的模块,以提高生产力和提升跨平台开发项目的质量。
第二章:内存管理
本章探讨了使用 Xamarin 运行时在 iOS 和 Android 上如何管理内存。虽然与.NET 平台进行类比,但它将提供可能导致泄漏的内存管理问题和问题的示例,并查看有助于开发者节省宝贵资源的有效模式。本章分为以下部分:
- 
应用组件生命周期 
- 
垃圾回收 
- 
平台特定概念 
- 
故障排除和诊断 
- 
模式和最佳实践 
应用组件生命周期
在 Xamarin 生态系统中的每个平台在应用程序的执行生命周期中都有一定的过程和状态。开发者可以实现某些方法并订阅生命周期事件,如应用程序启动、挂起、终止和后台运行,以处理所需的应用程序状态并释放不再需要的资源。
活动生命周期(Android)
在 Android 应用程序中,与传统的应用程序开发模型相反,任何活动都可以作为应用程序的入口点(只要它被指定为这样的入口点)。应用程序中的任何活动都可以在启动时初始化,也可以在应用程序恢复或从崩溃中重新启动时直接恢复。
为了管理活动的生命周期,存在不同的状态和事件,这些可以帮助开发者组织内存资源和程序功能。
活动/运行
当应用程序是焦点应用程序且活动处于前台时,称活动处于活动状态。在此状态下,除非操作系统需要采取特殊措施(例如,系统内存不足或应用程序无响应),否则开发者无需担心内存和资源,因为应用程序具有最高优先级。
在创建周期中,OnCreate是应用程序调用的第一个方法。这是初始化步骤,其中创建视图、引入变量和加载静态数据资源。
OnStart或OnRestart(如果活动在后台后重新启动)是创建周期中的第二个事件方法。如果需要实现特定的数据重新加载过程,则可以重写此方法(们)。这是在活动变得可见之前最后调用的方法。
在活动成功启动后,会调用OnResume方法。此方法表明应用程序已准备好与用户交互。它可以用来(重新)订阅外部事件、显示警报/用户消息以及与设备外围设备通信。
暂停
当设备进入睡眠状态且此活动处于前台,或者活动被另一个对话框或活动部分隐藏时,活动会被暂停。在此状态下,活动仍然是“活跃”的,但不能与用户交互。
OnPause事件方法在活动进入暂停状态之前被调用。这个事件方法是在任何外部事件提供者取消订阅、提交任何未保存的更改以及清理消耗内存资源的对象的理想位置,因为在暂停状态下用户交互是不可能的。当活动再次获得最高优先级时,它将只调用OnResume方法,而不会经历完整的创建周期。
背景
当用户按下主页按钮或使用应用切换器时,活动进入后台状态。在此状态下,不能保证活动会一直存活到用户“重新启动”应用程序。
当应用程序进入后台或停止时,会调用OnStop方法。背景化和停止状态之间的区别在于,当活动正在准备销毁时,它处于停止状态,随后将调用OnDestroy方法,因为应用程序已被关闭且不再被用户使用。如果用户恢复应用程序,活动将调用OnRestart方法,然后跟随完整创建过程。
停止
停止状态代表活动的生命周期结束。当用户按下返回按钮,表示应用程序不再需要时,活动进入此状态。然而,也有可能活动是因为系统内存资源不足,需要关闭处于较低优先级状态(如暂停或后台)的活动而进入此状态。
OnDestroy方法跟随停止状态,是最后被调用的生命周期事件方法。这是应用程序停止可能引起泄漏的长运行进程或清理其他持久资源的最后机会。建议在OnPause和OnStop方法中实现大部分资源清理,因为与由用户触发的OnPause和OnStop方法不同,OnDestroy可能被系统意外调用。
重新启动
当活动在后台之后返回用户交互时,我们称其为“重新启动”。重新启动的活动可以重新加载任何保存的状态信息,并创建一个不间断的用户体验。经过初始化步骤后,应用程序再次进入运行状态。
应用程序生命周期(iOS)
在 iOS 上,应用程序生命周期是通过 UI 应用程序代理来处理的。一旦实现了代理方法并进行了注册,这些方法将由执行上下文调用。
public class Application
{
    static void Main(string[] args)
    {
        UIApplication.Main(args, null, "AppDelegate");
    }
}
[Register("AppDelegate")]
public partial class AppDelegate : UIApplicationDelegate
{
    //Implement required methods
}
在 iOS 上,应用程序事件比 Android 上事件自上而下的执行要复杂一些。开发者可以使用在AppDelegate中实现的状态相关方法将他们的方法插入到传递状态。

图 1:iOS 应用程序状态转换
最重要的状态相关方法是以下这些:
- 
WillFinishLaunching是应用程序在启动时执行代码的第一个机会。它表示应用程序已经开始启动,但状态尚未恢复。
- 
FinishedLaunching在WillFinishLaunching完成后,状态恢复发生时被调用。
- 
OnActivated和OnResignActivation与 Android 平台上的OnPause和OnResume事件方法类似。
- 
当应用程序进入后台状态时,会调用 DidEnterBackground。它类似于 Android 上的OnStop方法,但此方法有时间限制;该方法应在少于 5 秒内执行,并在分配的时间后无通知退出。如果需要更多时间执行某些方法,应用程序可以启动后台任务来完成执行。
- 
WillEnterForeground和WillTerminate可以跟随DidEnterBackground执行。如果调用前者方法,应用程序即将被带回前台和活动状态,否则,应用程序准备被终止,因为系统需要更多内存,或者用户正在关闭后台应用程序。
垃圾回收
垃圾回收(GC)是现代应用程序开发平台中最有效的自动化内存管理技术之一。简单来说,通过自动垃圾回收,为应用程序使用的对象分配内存资源,并回收不再需要的资源。
注意
尽管垃圾回收作为一个自动化过程接管了内存分配的管理负担,但它可能会对性能产生重大影响。这种性能劣势是 iOS 平台上没有垃圾回收机制的主要原因之一。
理论上,GC 负责回收当前执行的应用程序无法访问的运行时元素占用的内存资源。然而,此机制不能总是正确地识别这些不可达资源,并且在清除已识别的内存指针时可能会产生意外的结果。
当应用程序未能识别和/或释放由不可达代码元素占用的资源时,就会发生内存泄漏,这可能导致内存耗尽问题。
当在执行上下文中还存在引用时释放内存区域,就会发生悬挂指针。然后这些引用被移除,内存可以重新分配给其他用途。
当内存区域已经被回收,而应用程序或垃圾收集器试图再次释放这个区域时,会发生双重释放错误。
Xamarin 项目的垃圾回收
在 .NET 框架中,由公共语言运行时定义的托管代码是内存资源由本地垃圾收集器管理的应用程序代码。GC 在初始化时分配内存的一个段来存储和管理内存资源,这被称为“托管堆”。CLR 中的垃圾收集发生在三个不同的代上,不同生命周期的对象分别位于堆中。代之间的提升和对象的存活取决于它们被放置的代以及它们在之前的垃圾收集周期中的存活情况。
SGen 垃圾收集器
SGen 垃圾收集器是大多数 Xamarin 项目(包括 Xamarin.iOS 和 Xamarin.Android)使用的代式垃圾收集器。SGen 在更小的对象集上执行更频繁的垃圾收集,这使得它在保守的 Boehm GC 上更有效率。
SGen 使用三个堆,即幼崽堆、主要堆和大对象空间,根据对象的内存需求分配内存段,当对象在垃圾收集周期中存活时,它们在堆之间进行提升。在这个配置中,幼崽堆类似于 .NET CLR 中的第 0 代,是大多数对象被创建和销毁的地方,大多数的垃圾收集周期都发生在这里以释放内存资源。经过小周期垃圾收集后存活的对象可以被提升到主要堆。主要堆只有在堆本身即将耗尽内存时才会进行主要垃圾收集周期。最后一个堆仅用于具有更高内存需求的大对象,并且不接受来自其他堆的提升。
注意
需要记住的是,在垃圾收集周期中,所有注册到运行时的线程,包括主运行循环线程,都会暂停。对此执行暂停的一个例外是继续运行 iOS 动画的独立进程。
Boehm 垃圾收集器(仅限 iOS)
Boehm GC (也称为 Boehm-Demers-Weiser 垃圾收集器) 是一个开源的垃圾收集器实现,最初是为 C/C++ 语言实现创建的。作为一个保守的垃圾收集器,它仍然有泄漏检测的程序,支持“已终结”语义,并且对代式实现的有限支持使其成为在各种平台上实现和移植的吸引人候选者。
Boehm GC 的实现仅适用于使用 Classic API 的 Xamarin.iOS 应用程序,其中它是默认的垃圾收集器。
平台特定概念
为了理解内存管理技术和陷阱,必须了解一些与平台相关的概念。尽管 Xamarin 提供了几乎与平台无关的开发体验,但 iOS 和 Android 平台在内存分配和引用处理上与 .NET CLR 以及彼此略有不同。
对象引用类型
根据应用需求,引用对象可以被分类。这种分类有助于垃圾收集器决定是否可以释放引用对象的内存分配。
强引用(Strong Reference)保护对象不被“垃圾回收”。当类实例直接由当前执行上下文使用时,引用的对象被称为强引用/可达。
当不需要的引用会干扰垃圾收集时,可以使用弱引用(Weak References)来引用类实例。当引用的对象是弱可达的时,依赖的代码部分在使用引用对象之前必须检查该对象是否仍然存活。根据声明类型实现的释放和终结过程,弱引用在 CLR 中有两种类型:长弱引用和短弱引用。长弱引用是那些可以继续存在并可以被析构函数终结而不是被释放或垃圾回收的类型。
软引用(Soft References)和虚引用(Phantom References)是 Android 运行时的特定内容。简单来说,软引用比弱引用更持久,只有在内存压力下,即使对象不再是强可达的,垃圾收集器才会清除它们。虚引用是 Android 运行时中最弱的引用。它们仅用于实现特殊对象终结方法,并且必须与引用队列关联以进行处理。
自动引用计数(Automatic Reference Counting,简称 ARC)
自动引用计数(Automatic Reference Counting,简称 ARC)是 iOS 5 中引入的编译器功能。它被称为编译器功能,因为它不能被归类为垃圾回收实现。它是一种静态分析实现,其中编译器分析代码执行树,并根据对象持久性要求插入保留和释放消息。
在 ARC 中,不允许在应用程序中插入传统的内存管理调用以分配内存和释放内存地址。
故障排除和诊断
分析(Profiling)是描述在目标应用程序运行时进行的动态系统分析的术语。分析器通常收集有关 CPU 利用率、帧率值以及最重要的是内存分配数据等指标的数据。特别是在 Xamarin 项目中,由于我们处理多个平台,分析成为测试和诊断的重要部分。
有许多工具可以用来分析 Xamarin 项目的内存使用情况,其中 Xamarin Profiler 是唯一一个可以用于 Xamarin.iOS 和 Xamarin.Android 应用程序的工具。
Xamarin Profiler
Xamarin Profiler 是 Xamarin Suite 中的最新成员。与其他平台特定应用程序相比,这个分析器具有优势,因为它可以在 OS X 或 Windows 上运行,针对 Xamarin.Android 或 Xamarin.iOS 应用程序。

图 2:Xamarin Profiler
它被设计用来为开发者提供几乎实时的(取决于采样率)关于 Xamarin 应用程序内存堆的信息。它还可以保存内存分配快照,稍后可以访问和分析。
它可以直接从 Visual Studio 或 Xamarin Studio 启动,并且可以使用模拟器和真实设备构建/运行配置。
目前在初始弹出窗口中可以选择两个工具。
分配工具
第一项工具是分配模板,它提供了关于内存段和分配的详细信息。在这个视图中,开发者可以在摘要选项卡下看到按类名分组的分配的通用列表。调用树选项卡提供了应用程序中线程的列表以及它们与内存对象的关系。分配列表提供了关于对象分配的实时数据,而快照选项卡提供了存储的内存快照的信息。
时间分析器
时间分析器是 Xamarin Profiler 中可以使用的第二项工具。它提供了关于应用程序执行特定方法花费多少时间的宝贵信息。开发者可以查看每个方法的整个堆栈跟踪。
设备监控器(仅限 Android)
Android 设备监控器迄今为止一直是 Android 开发的 主要诊断工具。对于 Xamarin 开发者来说,当安装了 Android SDK 后,可以直接从 Visual Studio 的工具箱项或 Xamarin Studio 的工具菜单中访问设备监控器。
在设备监控器的主页上,有一个树形视图显示可以连接到设备监控器的每个设备或模拟器。
注意
一次只能连接一个调试器到任何设备,因此在使用设备监控器之前,其他调试器必须断开连接。
一旦选择了设备,开发者可以使用图形界面获取分配信息和堆状态。使用设备监控器还可以触发垃圾回收周期。

图 3:Android 设备监控器连接到 Visual Studio 模拟器
工具(仅限 iOS)
Instruments 是一个非常有价值的应用程序,它随 Xcode 工具集一起安装。在这个应用程序中,开发者可以提供一套从能耗、图形资源到内存分配的各种诊断工具。
分配工具的界面与 Xamarin Profiler 非常相似,并提供几乎实时的内存对象数据。

图 4:Instruments 分析 Xamarin 应用程序
Xcode Instruments 工具可以与实际设备或 iOS 模拟器一起使用。它可以直接从 Xamarin Studio 启动。一旦应用程序在 iOS 模拟器或实际设备上启动,它就会在目标选择窗口中可用。

图 5:以 iOS 模拟器为目标设备的 Instruments
注意
如果你使用 Microsoft Windows 开发 Xamarin.iOS 应用程序,并且有一个 OS X 构建机器,你将无法直接从开发站访问 Instruments。一旦应用程序在测试设备或模拟器上,你可以在构建机器上启动 instruments 并选择正确的目标进行分析。
Monotouch Profiler(仅限 iOS)
Monotouch Profiler 是在它被 Xamarin Profiler 取代之前,用于诊断 Xamarin.iOS 应用程序内存问题的 Xamarin 工具。它仍然可以通过在 Xamarin Studio 的 项目 菜单下使用 运行与 Mono HeapShot 菜单项来访问。虽然它提供了有关内存分配和堆的有用信息,但目前它仅限于作为一个轻量级应用程序来获取内存快照。
模式和最佳实践
在处理托管运行时和垃圾回收时,开发者必须注意某些模式和反模式。如果不妥善处理,托管和本地对象都可能产生不可收集的跟踪,这反过来又可能导致内存泄漏和不必要的资源消耗。
可释放对象
垃圾回收器管理的资源通常限于内存分配。其他资源,如网络套接字、数据库句柄、UI 元素和文件/设备描述符需要额外的定义或机制。
在托管运行时,这些对象资源可以通过两种不同的方式清理。第一种,效率较低、不可预测的方式是实现析构器/最终化器。一旦垃圾回收器决定对象不再被强引用,资源如网络套接字可以被释放。然而,可最终化的对象必须等待下一个 GC 周期才能被清理,并且不能由开发者的主动操作来最终化。
清理应用程序资源的另一种方法是,在具有资源引用的类中实现 IDisposable 接口。此接口只需要实现单个 Dispose 方法即可释放托管资源。垃圾回收器还提供了一个方法 (GC.SuppressFinalize) 来避免最终化,因为对象将使用 IDisposable 接口进行处置。
public class DisposableFinalizableClass : IDisposable
{
    private ManagedResource m_Resource; // reference to a resource
    public DisposableFinalizableClass()
    {
        m_Resource = new ManagedResource(); // allocates the resource
    }
    /// <summary>
    /// Destructor for the DisposableFinalizableClass
    /// <remarks>
    /// Note that we are not overriding the object.Finalize method
    /// but providing a destructor for the Finalize method to call
    /// </remarks>
    /// </summary>
    ~DisposableFinalizableClass()
    {
        Dispose(false);
    }
    /// <summary>
    /// Implementing the Dispose method
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        // Letting the GC know that there is no more need for
        // Finalization, the resources are already cleaned-up
        GC.SuppressFinalize(this);
    }
    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (m_Resource != null) m_Resource.Dispose();
        }
        else
        {
            // Called from Finalize
            // Clean-up any unmanaged resources
        }
    }
}
可释放对象可以与 using 块一起使用,这为开发者提供了一种确定性的方法,以便在对象不再需要时立即释放相关资源。
过期监听器问题
与 UI 元素或遗留 API 实现一起使用的最常见模式之一是观察者模式。正如你可能知道的,此模式中有两个利益相关者,观察者和提供者。观察者订阅提供者提供的事件以接收更新。
当观察者模式实现不正确或不完整时,会出现已失效的监听器问题。在这个模式中,订阅后,提供者会保持对观察者的强引用。如果在这个订阅没有被移除之前订阅者离开了上下文,应用程序将无法回收订阅者对象(例如,Android 活动或视图模型),从而导致内存泄漏。
为了演示这个问题,我们将使用斐波那契序列的单例实现,并使用异步方法作为事件提供者。
public event EventHandler<int> CalculationCompleted;
public event EventHandler<List<int>> RangeCalculationCompleted;
/// <summary>
/// Calculates n-th number in Fibonacci sequence
/// </summary>
/// <param name="ordinal">Ordinal of the number to calculate</param>
public void GetFibonacciNumberAsync(int ordinal)
{
    var task = Task.Run(() =>
    {
        var result = GetFibonacciNumberInternal(ordinal);
        if (CalculationCompleted != null) CalculationCompleted(this, result);
    });
    // Avoiding Deadlocks on the UI thread
    task.ConfigureAwait(false);
}
/// <summary>
/// Calculates a range of numbers in Fibonnaci sequence
/// </summary>
/// <param name="firstOrdinal">Ordinal of the first number</param>
/// <param name="lastOrdinal">Ordinal of the last number</param>
public void GetFibonacciRangeAsync(int firstOrdinal, int lastOrdinal)
{
    var task = Task.Run(() =>
    {
        var result = GetFibonacciRangeInternal(firstOrdinal, lastOrdinal);
        if (RangeCalculationCompleted != null) RangeCalculationCompleted(this, result);
    });
    task.ConfigureAwait(false);
}
public static FibonacciSource Instance
{
    get 
    { 
        return m_Instance ?? (m_Instance = new FibonacciSource()); 
    }
}
我们将使用 MvvmCross 实现两个独立的视图模型,并使用关联的视图来调用异步方法,然后使用视图模型上的Close方法返回到主视图。在每个视图模型的构造函数中,我们将订阅FibonacciSource上的相应事件。

图 6:斐波那契计算器应用
为了调查任何内存泄漏,我们在主视图和计算视图之间来回导航。在两个视图(即单个和范围)上迭代几次之后(即,单次和范围),我们在 Xamarin Profiler 上得到了以下结果(仅使用“分配”模板)。

图 7:Xamarin Profiler 结果
你会注意到在垃圾回收(你可以通过GC.Collect()触发 GC 运行)之后,SingleCalculationViewModel的实例都不再存活,然而RangeCalculationViewModel的实例是持久的。这是因为RangeCalculationViewModel的关闭命令中缺少取消订阅的调用。
private MvxCommand m_CloseCommand;
public ICommand CloseCommand
{
    get
    {
        m_CloseCommand = m_CloseCommand ?? new MvxCommand(DoClose);
        return m_CloseCommand;
    }
}
private void DoClose()
{
    // FibonacciSource.Instance.RangeCalculationCompleted -= OnCalculationCompleted;
    Close(this);
}
我们也可以使用这个 Android 应用程序的OnPause事件或任何其他平台上的相关事件,在订阅者或持有订阅的视图组件离开上下文之前取消订阅。
在这种情况下,另一个解决方案是使用TaskCompletionSource将可观察模式转换为可等待模式。包装可观察的斐波那契源将给你更好的订阅控制,并且产生的异步任务更适合移动开发和 MVVM 模式。
private Task<List<int>> CalculateFibonacciRangeAsync(int firstOrdinal, int secondOrdinal)
{
    TaskCompletionSource<List<int>> taskCompletionSource = new TaskCompletionSource<List<int>>();
    EventHandler<List<int>> calcCompletedEventHandler = null;
    calcCompletedEventHandler =
        (sender, e) =>
        {
            FibonacciSource.Instance.RangeCalculationCompleted -= calcCompletedEventHandler;
            taskCompletionSource.SetResult(e);
        };
    FibonacciSource.Instance.RangeCalculationCompleted += calcCompletedEventHandler;
    FibonacciSource.Instance.GetFibonacciRangeAsync(firstOrdinal, secondOrdinal);
    return taskCompletionSource.Task;
}
最后,这个异步任务会通过ContinueWith语句被调用,以在视图模型中设置结果。
private void DoCalculate()
{
    if (!string.IsNullOrWhiteSpace(Input1) && !string.IsNullOrWhiteSpace(Input2))
    {
        int numberOrdinal1, numberOrdinal2;
        if (int.TryParse(Input1, out numberOrdinal1) && int.TryParse(Input2, out numberOrdinal2))
        {
            InfoText = "Calculating";
            var fibonacciTask = CalculateFibonacciRangeAsync(numberOrdinal1, numberOrdinal2)
                .ContinueWith(task =>
                {
                    Result = string.Join(",", task.Result);
                    InfoText = "";
                });
            fibonacciTask.ConfigureAwait(false);
            return;
        }
    }
    InfoText = "Invalid Input";
}
弱引用
弱引用在处理松散耦合的应用层时可以提供很大的帮助。在这些类型的场景中,当对象需要在类域之外进行管理时,可以使用弱引用根据可达性概念从 GC 保护中移除这些实例,因为这些实例对应用的其他层有强引用。
让我们假设在前面的例子中,斐波那契序列项被处理为具有名为FibonacciItem的类的引用值。这个类携带计算出的值和计算的时间。
public class FibonacciItem
{
    public int Value { get; private set; }
    private readonly DateTime m_Calculated;
    public FibonacciItem(int value, DateTime calculatedTime)
    {
        Value = value;
        m_Calculated = calculatedTime;
    }
}
为了减少处理时间,我们现在可以实施一个缓存机制,该机制将强制源根据序号重新计算值,如果它尚未存在于缓存中,或者如果它被废弃以节省内存资源。为此,我们可以使用WeakReference来缓存斐波那契项。
public class FibonacciCache
{
    // Dictionary to contain the cache. 
    private static Dictionary<int, WeakReference> _cache;
    public FibonacciCache()
    {
        _cache = new Dictionary<int, WeakReference>();
    }
    /// <summary>
    /// Accessor to FibonacciItem references
    /// </summary>
    /// <param name="ordinal"></param>
    /// <returns>FibonacciItem if it is still alive</returns>
    public FibonacciItem this[int ordinal]
    {
        get
        {
            if (!_cache.ContainsKey(ordinal)) return null;
            if (_cache[ordinal].IsAlive)
            {
                // Object was obtained with the weak reference.
                FibonacciItem cachedItem = _cache[ordinal].Target as FibonacciItem;
                return cachedItem;
            }
            else
            {
                // Object reference is already disposed of   
                return null;
            }
        }
        set
        {
            _cache[ordinal] = new WeakReference(value);
        }
    }
} 
跨域对象
在 Xamarin 应用程序中,最常见的一个内存问题,跨堆引用,发生在原生运行时和 mono 运行时之间发生交叉时。这个问题源于 mono 运行时几乎被视为一个独立的域,并且仅通过 GC 句柄在原生域的堆中进行管理。
在 Android 场景中,当 Java 对象被托管 C#对象引用或反之亦然时,两个运行时之间的通信变得昂贵。例如,如果我们不使用 ViewModel 模式实现斐波那契计算器,我们希望创建一个数据适配器,将范围计算结果加载到列表视图中。
private void OnFibonacciCalculationCompleted(object sender, List<FibonacciItem> result)
{
    RunOnUiThread(() =>
    {
        var listView = FindViewById<ListView>(Resource.Id.lstResult);
        listView.Adapter = new ArrayAdapter<string>(this, Resource.Layout.ListViewItem, 
            result.Select(val => val.Value.ToString()).ToArray());
    });
}
这种实现具有更高的垃圾收集成本。考虑到语言交叉,它还有性能惩罚,更不用说每个世界中的对象实际上被镜像,这增加了内存分配成本。
这里的解决方案是在托管世界中尽可能多地完成工作,让运行时处理其余部分。因此,我们不是使用本地的ArrayAdapter,而是可以实现一个基适配器,将FibonacciItem实例传递给ListView。
public class FibonacciResultsAdapter : BaseAdapter<FibonacciItem>
{
    List<FibonacciItem> m_ResultItems;
    Activity m_Context;
    public FibonacciResultsAdapter(Activity context, List<FibonacciItem> items)
    {
        m_Context = context;
        m_ResultItems = items;
    }
    public override long GetItemId(int position) { return position; }
    public override FibonacciItem this[int position]
    {
        get { return m_ResultItems[position]; }
    }
    public override int Count
    {
        get { return m_ResultItems.Count; }
    }
    public override View GetView(int position, View convertView, ViewGroup parent)
    {
        View view = convertView;
        if (view == null)
            view = m_Context.LayoutInflater.Inflate(Resource.Layout.ListViewItem, null);
        view.FindViewById<TextView>(Android.Resource.Id.txtOutput).Text = m_ResultItems[position].Value.ToString();
        return view;
    }
}
通过实现适配器,我们移除了 Java 类型ArrayAdapter、ArrayList以及 Java 对FibonacciItem实例的引用。
同样适用于在托管域中继承原生对象的情况。这些所谓的“特殊对象”由垃圾收集器以不同的方式处理。它们必须在每次垃圾回收周期中重新扫描它们携带的所有引用。
循环引用(循环)
在一般情况下,当底层平台使用某种类型的引用计数作为内存管理策略,并且根据对该特定对象实例的引用数量来清理内存时,会发生循环引用。
随着 Microsoft 发布.NET 和引入代际跟踪垃圾回收,Microsoft 放弃了引用计数。在 Android 设备上的 mono 运行时,SGen 也使用某种形式的标记和清除算法。在这两个运行时中,引用都是从所谓的“应用程序根”进行追踪的。这些对象是在垃圾回收周期时“假定”为存活的对象。
根可以是:
- 
对全局对象的引用 
- 
对静态对象的引用 
- 
对静态字段的引用 
- 
对栈上局部对象的引用 
- 
对等待最终化的对象的引用 
- 
对 CPU 寄存器中托管堆上对象的引用 
然而,如前所述,在 iOS 上,为了性能而放弃了垃圾回收,而 ARC(自动引用计数)却无法处理所谓的保留周期。保留周期发生在创建层次结构中的较低元素(即子元素)需要引用父元素时。在这种情况下,当子或父发送release时,由于存在额外的引用保持每个项目存活,dealloc方法永远不会运行。

图 8:保留周期
当托管对象从原生对象(即从NSObect派生的任何对象)派生时,例如 UI 控件,这种本地的 iOS 问题就会成为 Xamarin 应用程序的问题。当托管类从原生对象继承时,为了防止它们被垃圾回收,Xamarin.iOS 创建了一个 GCHandle。这些 GCHandles 以及对象之间的托管引用共同创建了所描述的(间接)保留周期。
如果我们处理的是一个包含子视图数组的父UIView,并且子视图对象保留了对父对象的引用:
public class RetainingParentClass : UIView
{
    public RetainingParentClass()
    {
    }
}
public class RetainingChildClass : UIView
{
    private RetainingParentClass m_Parent;
    public RetainingChildClass(RetainingParentClass parent)
    {
        m_Parent = parent;
    }
}
以下代码片段将创建一个保留周期,并导致应用程序中的内存泄漏:
var parent = new RetainingParentClass();
parent.Add(new RetainingChildClass(parent));
如果我们在视图的构造函数中执行此代码,每次应用程序导航到这个视图时,我们都会创建一个新的父对象,它永远不会被垃圾回收。

图 9:保留对象的工具视图
在这种情况下,最简单的修复方法是,当我们从子对象创建对父对象的引用时使用WeakReference。使用弱引用可以避免保留周期情况,并且不会干扰垃圾回收。
public class RetainingChildClass : UIView
{
    private WeakReference<RetainingParentClass> m_Parent;
    public RetainingChildClass(RetainingParentClass parent)
    {
        m_Parent = new WeakReference<RetainingParentClass>(parent);
    }
}
另一个选项是实现IDisposable接口,通过在 GC 之前将引用设置为 null 来移除对象之间的强链接。
摘要
为了管理应用程序资源,必须对应用程序生命周期有更深入的了解。本章概述的应用程序生命周期事件是 iOS 和 Android 平台上访问底层平台运行时的主要入口点。如果使用得当,两个平台上的事件委托和事件方法可以帮助开发者节省宝贵的资源并避免内存问题。
讨论的其他概念包括垃圾回收、对象引用和自动引用计数。这些概念构成了目标 Xamarin 平台内存管理的基础。
我们还更详细地研究了目标平台的诊断和性能分析工具以及如何有效地使用它们。虽然 iOS 和 Android 平台各自都有一个用于分析内存分配的本地应用程序,但 Xamarin Profiler 为这两个平台提供了一个统一的解决方案。
最后,针对不同记忆相关问题和陷阱,概述了一些有用的模式。为了分析这些模式,分别使用了 Xamarin Profiler 和 Instruments 来分析 Android 和 iOS 应用程序。
在下一章中,我们将探讨异步实现技术,并研究多线程和后台执行的各种模式。
第三章:异步编程
本章深入探讨了异步和多线程编程概念。我们将讨论平台特定的问题,并深入描述在不同平台上如何执行线程场景。本章分为以下部分:
- 
Xamarin 上的多线程 
- 
异步方法 
- 
并行执行 
- 
模式和最佳实践 
- 
后台任务 
Xamarin 上的多线程
Xamarin 平台与 Windows Runtime 遵循单线程公寓模型的基本原则。在这个模型中,简单来说,一个进程被分配一个线程,这个线程作为所有其他可能创建并返回的分支的主干。
虽然开发者仍然有能力创建和消费多个线程,但在 Xamarin 目标平台上的现代应用程序中,这种模型已经通过并发实现进行了扩展,将线程管理的责任委托给运行时,并允许开发者仅定义可能或可能不在单独线程上执行的执行块。
单线程模型
在 Android 和 iOS 中,每个移动应用程序都是在单个线程上启动和运行的,这个线程通常被称为主线程或 UI 线程。大多数的 UI 交互以及进程生命周期事件处理程序和委托都是在该线程上执行的。
在此模型中,开发者主要应关注尽可能长时间地保持主线程对 UI 交互的可用性。如果我们在这个线程上执行一个阻塞调用,它将立即反映给用户,表现为屏幕冻结或应用程序无响应错误,这不可避免地会被底层平台的所谓看门狗实现终止。除了平台特定的限制之外,用户还期望在任何时候都能有一个响应式的 UI,并且无法忍受屏幕冻结哪怕是一瞬间。如果屏幕冻结持续更长时间,他们将尝试强制终止应用程序(参见第七章的反馈部分,视图元素)。
开发者仍然可以在主线程中创建、消费和监控其他线程。使用后台线程并在后台调用长时间运行的过程是可能的。为此,Xamarin.iOS 和 Xamarin.Android 项目上提供了System.Threading命名空间和与线程相关的类。此外,每个平台在底层都有自己的线程实现。
例如,让我们想象我们想要执行一个长时间运行的过程,我们不想这个方法阻塞 UI 线程。使用经典的多线程,实现将类似于:
//var threadStart = new ThreadStart(MyLongRunningProcess);
//(new Thread(threadStart)).Start();
// Or simply
(new Thread(MyLongRunningProcess)).Start();
每个 Thread 都可以提供有关当前执行状态的信息,并且可以被取消、启动、中断,甚至可以被另一个线程加入。开发者可以使用线程模型来限制应用程序的速率和/或更有效地执行代码,而不会犯阻塞 UI 线程的致命错误。
当你在单独的线程上执行的过程需要更新 UI 组件时,可能会变得有些复杂。这将是一个跨线程违规操作。
例如,如果我们想在 Android 活动中从单独的线程更新一个 UI 组件,我们需要在活动上执行以下操作(使用 Activity.RunOnUiThread):
this.RunOnUiThread(() => { UpdateUIComponent(); });
在 iOS 上执行相同的操作看起来类似(使用 NSObject.InvokeOnMainThread):
this.InvokeOnMainThread(() => { UpdateUIComponent(); });
作为参考,在 Windows Runtime 中,同样的执行过程如下所示:
CoreApplication.MainView
    .CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, 
        () => { UpdateUIComponent(); });
当出现异常或操作需要取消时,经典线程模型下的实现变得更加复杂,更不用说线程间的同步和线程安全的数据流完全依赖于开发者或第三方实现。
在 Xamarin 中使用 System.Threading 命名空间和经典线程模型的一个重要错误是,这个命名空间和与线程相关的类不能在 PCLs 中使用。
基于任务的异步模式
自从 .NET 4.0 中引入 Tasks 框架以及后来 Mono 的采用,基于任务的异步模式(TAP)已经成为移动应用程序的主要实现策略。虽然它提供了从线程结构中所需的抽象,但也给了开发团队创建易于阅读、管理和可扩展的应用程序项目的机会。如前所述,由于每个 Xamarin 目标平台都根据底层运行时实现线程,Tasks 框架提供的这种抽象使其成为跨平台项目中异步实现的理想候选,并且是可移植类库中不可或缺的部分。
在这种模式中,每个执行块根据块的返回值被表示为 Task 或 Task<T>(例如,如果块返回 void,则应将其转换为返回 Task;如果块返回 int,则方法签名应为 Task<int>)。任务可以同步或异步执行,可以等待结果或作为完成时的回调承诺执行,可以将任务推送到另一个线程池或在可用时在主线程上执行,占用处理器时间。
任务特别适合于计算密集型操作,因为它们提供了对异步方法何时以及如何执行的良好控制。这些方法上的取消和进度支持使得长时间运行的过程易于管理。
iOS 的并发模型
并发和 iOS 运行时的操作块是苹果对任务框架试图解决的问题的回应。本质上,任务框架和 iOS 上的并发模型是通过创建抽象层来解决创建多任务、健壮且易于扩展的应用程序的解决方案,这样应用程序就不直接管理线程,而是让操作系统决定在哪里以及何时执行操作。
iOS 运行时使用操作或调度队列以先进先出(FIFO)的方式异步调度任务。这种方法提供了自动线程池管理以及简单的编程接口。
虽然 iOS 运行时构造,如NSOperation、NSBlockOperation和NSOperationQueue,在 Xamarin.iOS 框架中实现并可供使用,但它们的实现仅针对 iOS 平台,而任务可以在所有三个平台上使用。
异步方法
任务并行库(TPL)是.NET 框架中并行计算的核心部分,在 Xamarin 运行时中也具有相同的地位。
异步方法执行,结合async和await关键字(自 C# 5.0 引入),可以使应用程序更加响应和高效,并降低实现多线程和同步的复杂性。无需实现参数化线程,启动和推送都委托给后台线程,所谓的“可等待”对象。您可以通过将方法返回类型转换为Task或Task<T>来轻松地将方法转换为异步承诺。作为回报,运行时会在最佳时间执行代码,并将结果返回到您的执行上下文。
例如,使用任务的前一个线程创建示例将非常简单:
Task.Run(() => MyLongRunningProcess());
// Or
Task.Factory.StartNew(MyLongRunningProcess, TaskCreationOptions.LongRunning);
然而,任务框架不仅关乎创建线程或执行非阻塞方法,还关乎以尽可能简单的方式协调和管理这些异步任务。有许多静态辅助方法以及为任务实现的方法,帮助开发者轻松实现一些这些协调场景。
继续阅读
Task类上的ContinueWith函数允许开发者将依赖的任务链在一起,并将它们作为一个整体的任务执行。一旦第一个任务的结果被发送回任务调度器,就会执行延续代理。重要的是要提到,第一个任务和延续方法不一定在同一个线程上执行。代码如下:
Task.Run(() => MyLongRunningProcess())
    .ContinueWith(task => MySecondLongRunningProcess());
如果第二个任务依赖于第一个任务的结果:
Task.Run(() => MyLongRunningProcess())
            .ContinueWith(task => MySecondLongRunningProcess(task.Result));
取消
CancellationToken和CancellationTokenSource用作远程令牌,以控制异步方法、线程或多个线程的执行生命周期以及令牌反映的事件源。
简而言之,CancellationTokenSource负责抛出基于时间或手动的取消事件,这些事件可以通过异步方法上下文中的令牌检索。
您可以使用默认构造函数创建一个取消源,并可以向令牌添加基于时间的取消操作:
m_CancellationSource = new CancellationTokenSource();
var token = m_CancellationSource.Token;
// You can cancel it after a certain amount of time, which would trigger an OperationCanceledException
// m_CancellationSource.CancelAfter(2000);
一旦我们正在执行异步方法,我们可以使用此源中的令牌,或者我们可以将其与TaskFactory关联以创建一个协作的任务列表:
Task.Run(() =>
{
    // Executing my long running method
    if (token.IsCancellationRequested)
    {
        token.ThrowIfCancellationRequested();
    }
}, token);
或者:
var taskFactory = new TaskFactory(m_CancellationSource.Token);
taskFactory.StartNew(() =>
    {
        // Executing my long running method
        if (Task.Factory.CancellationToken != CancellationToken.None && Task.Factory.CancellationToken.IsCancellationRequested)
        {
           Task.Factory.CancellationToken
               .ThrowIfCancellationRequested();
        }
    });
最后,您还可以使用CancellationTokenSource的Cancel或CancelAfter(带有时间延迟)方法取消线程或一组线程。
进度
另一个帮助用户了解后台正在调用的操作的异步控制功能是进度回调实现。就像CancellationToken一样,我们可以向异步任务提供一个事件处理器,用于进度事件,异步方法可以调用该事件处理器将进度信息传递回主线程。
对于简单的进度报告,只需在异步方法中添加一个继承自IProgress<T>接口的额外参数就足够了。
例如,如果我们想在GetFibonacciRangeAsync方法中实现一个进度事件处理器,我们可以使用要计算的值的数量和当前正在计算的序号来报告总进度的百分比:
public async Task<List<int>> GetFibonacciRangeAsync(int firstOrdinal, int lastOrdinal, IProgress<int> progress = null)
{
    var results = new List<int>();
    for (var i = firstOrdinal; i < lastOrdinal; i++)
    {
        results.Add(await GetFibonacciNumberAsync(i));
        decimal currentPercentage = (decimal) lastOrdinal - i/(decimal) lastOrdinal - firstOrdinal;
        if (progress != null)
            progress.Report((int)(currentPercentage * 100);
    }
    return results;
}
为了能够在我们的视图模型中使用进度值,我们可以利用Progress<T>类,它是IProgress<T>的默认实现。代码如下:
Action<int> reportProgress = (value) =>
{
    InfoText = string.Format("{0}% Completed", value);
};
var progress = new Progress<int>(reportProgress);
m_SourceAsync.GetFibonacciRangeAsync(numberOrdinal1, numberOrdinal2, progress)
    .ContinueWith(task =>
    {
        Result = string.Join(",", task.Result.Select(val=>val));
        InfoText = "";
    });
任务批量
在基于任务的异步模式中,除了延续之外,还有其他方法可以批量执行任务,甚至在并行中执行。上一节中的示例是分别等待每个数字计算,并执行下一个调用。然而,内部方法的实现方式使它们彼此独立。因此,实际上没有必要逐个等待它们返回结果。代码如下:
List<Task<int>> calculations = new List<Task<int>>();
Mvx.Trace("Starting Calculations");
for (var i = firstOrdinal; i < lastOrdinal; i++)
{
    var currentOrdinal = i;
    calculations.Add(Task.Factory.StartNew(() => 
        GetFibonacciNumberInternal(currentOrdinal).Value, TaskCreationOptions.LongRunning));
}
Mvx.Trace("Starting When All", DateTime.Now);
int[] results = await Task.WhenAll(calculations);
Mvx.Trace("Calculations Completed", DateTime.Now);
return results.OrderBy(value=>value).ToList();
注意
Mvx静态类和Trace方法由 MvvmCross 库提供。它将在后面的章节中进一步讨论。
现在,序列中的每个斐波那契数都是并行计算的,当序列范围完成时,返回一个结果值数组。最后,我们排序数组并返回值列表。
我们可以通过添加一个带有互斥(线程安全)计数器的进度通知器来扩展此实现:
calculations.Add(Task.Factory.StartNew(() =>
    GetFibonacciNumberInternal(currentOrdinal).Value, TaskCreationOptions.LongRunning)
    .ContinueWith(task =>
    {
        if (progress != null)
        {
            var currentTotal = Interlocked.Increment(ref currentCount);
            decimal currentPercentage = (decimal) currentTotal/(decimal) totalCount;
            progress.Report((int)(currentPercentage * 100));
        }
        return task.Result;
    })); 
上述计算产生的日志跟踪如下:
09-07 21:18:29.232 I/mono-stdout( 3094): mvx:Diagnostic: 40.80 Starting Calculations
09-07 21:18:29.352 I/mono-stdout( 3094): mvx:Diagnostic: 40.92 Starting When All
09-07 21:18:30.432 I/mono-stdout( 3094): mvx:Diagnostic: 42.00 Calculations Completed
计算的总时间约为 1.2 秒。
在每个方法上重复相同的计算将给出以下输出(计算序号 4 直到 11):
09-07 21:26:58.716 I/mono-stdout( 3281): mvx:Diagnostic: 10.60 Starting Calculations
09-07 21:26:58.724 I/mono-stdout( 3281): mvx:Diagnostic: 10.61 Starting calculating ordinal 4
…
09-07 21:27:03.900 I/mono-stdout( 3281): mvx:Diagnostic: 15.78 Starting calculating ordinal 11
09-07 21:27:05.028 I/mono-stdout( 3281): mvx:Diagnostic: 16.91 Calculations Completed
同样的计算总共花费了大约 6.3 秒。
除了WhenAll之外,开发者还配备了Task类上的WhenAny、WaitAll、WaitAny方法以及TaskFactory类上的ContinueWhenAll和ContinueWhenAny方法。
并行执行
在前面的章节中,讨论集中在System.Threading.Tasks命名空间和Task类上。尽管任务是基于异步模型的基础和所谓的任务并行性的基石,但并发集合命名空间构成了异步模型的数据并行性部分,并为开发者提供了执行代码最有效和线程安全的方式的工具。
BlockingCollection<T>是并发集合实现之一,它封装了线程之间的核心同步和协调,并为实现 Xamarin 应用程序中的提供者-消费者模型提供了线程安全的数据存储。
使用BlockingCollection<T>,我们可以轻松实现一个新的方法,该方法利用了前面示例中的并行执行。在这个实现中,我们的视图模型将是消费者,斐波那契源和范围计算任务将是提供者。
如果我们使用阻塞集合重写范围计算方法,我们的方法签名将类似于:
public async Task GetFibonacciRangeAsync(int firstOrdinal, int lastOrdinal, BlockingCollection<int> blockingCollection)
因此,从某种意义上说,消费者将创建阻塞集合,并将其传递给提供者以填充计算值。作为回报,提供者需要使用TryAdd或Add方法将每个计算值从并行任务中推送到集合中。代码如下:
for (var i = firstOrdinal; i < lastOrdinal; i++)
{
    var currentOrdinal = i;
    calculations.Add(Task.Factory.StartNew(() =>
        GetFibonacciNumberInternal(currentOrdinal).Value, TaskCreationOptions.LongRunning)
        .ContinueWith(task =>
        {
            blockingCollection.Add(task.Result);
            return task.Result;
        }));
}
最后,一旦所有计算完成,提供者需要将集合标记为添加完成。代码如下:
//
// Collection is filled completely
await Task.WhenAll(calculations).ContinueWith(task =>
{
    blockingCollection.CompleteAdding();
});
当这些任务在提供者端执行时,我们可以在视图模型中使用while循环创建消费者,检查特定间隔是否有新项目使用TryTake,直到完成。然而,对于此目的,并发集合已经实现了一个方法:GetConsumingEnumerable。使用此方法可以使消费者线程的执行变得像foreach块一样简单。代码如下:
var blockingCollection = new BlockingCollection<int>();
var fibonacciTask = (new FibonacciSourceAsync())
    .GetFibonacciRangeAsync(numberOrdinal1,
        numberOrdinal2, blockingCollection);
fibonacciTask.ConfigureAwait(false);
//
// Starting the Consumer thread
Task.Factory.StartNew(() =>
{
    foreach (var item in blockingCollection.GetConsumingEnumerable())
    {
        var currentItem = item;
        if (Result != string.Empty) Result += ",";
        Result += currentItem;
    }
    InfoText = "Done";
}, TaskCreationOptions.LongRunning);
在此模型中,提供者线程(以及每个正在执行的并行任务)和消费者线程几乎可以即时执行,并且结果几乎立即通过视图模型反映到 UI 上。
在前面的实现中,尽管可能将多个值添加到阻塞集合中,并且阻塞集合支持多个消费者,但foreach循环遵循更线性的执行。我们可以通过添加多个消费者来扩展此模型,使用System.Threading.Tasks.Parallel命名空间中的Parallel.ForEach扩展方法。代码如下:
Task.Factory.StartNew(() =>
{
    var result = Parallel.ForEach(blockingCollection.GetConsumingEnumerable(), item =>
    {
        UpdateUIWithItem(item);
    }).IsCompleted;
    if (result) InfoText = "Done";
}, TaskCreationOptions.LongRunning);
开发者还可以使用和适应其他并发场景的构造和实现模式,例如Partitioner、ActionBlock、ConcurrentScheduler等。然而,这些概念超出了本书的范围。
模式和最佳实践
在实现异步任务时,可以与经典的多线程和事件模式进行类比,甚至进行转换。然而,异步方法必须谨慎实现,以避免死锁和未捕获的异常。
异步模式转换
观察者模式——也称为基于事件的异步模式(EAP)——曾经是长时间运行过程和服务/远程应用程序 API 的主要开发工具。事件和委托在现代应用程序中仍然构成了相当一部分 UI 相关实现。
然而,异步任务和可等待对象提供了一个更方便的方式来处理长时间运行的过程和链式完成方法。
幸运的是,可以将其他异步模式转换为基于任务的实现模式。这类场景涉及使用TaskCompletionSource类。
为了演示这一点,我们将使用之前示例中的简化版斐波那契源实现:
public event EventHandler<int> CalculationCompleted;
public event EventHandler<string> CalculationFailed;
/// <summary>
/// Calculates n-th number in Fibonacci sequence
/// </summary>
/// <param name="ordinal">Ordinal of the number to calculate</param>
public void GetFibonacciNumber(int ordinal)
{
    try
    {
        var result = GetFibonacciNumberInternal(ordinal);
        if (CalculationCompleted != null) CalculationCompleted(this, result);
    }
    catch (Exception ex)
    {
        if (CalculationFailed != null) CalculationFailed(this, ex.Message);
    }
}
在这个例子中,我们有一个成功计算的事件处理程序,另一个是失败计算的事件处理程序(例如,如果序号小于 0,它应该抛出ArgumentOutOfRangeException)。
我们的目的是实现一个异步方法,我们可以执行它并将结果传递给 UI,而无需每次创建新的FibonacciSource时都订阅事件。
为了这个目的,我们可以实现FibonacciSource的新版本,仅暴露异步方法而不是基于事件的方法。代码如下:
public class FibonacciSourceAsync : FibonacciSource
{
    public new Task<int> GetFibonacciNumberAsync(int ordinal)
    {
        var myTaskSource = new TaskCompletionSource<int>();
        EventHandler<FibonacciItem> onCalculationCompleted = null;
        EventHandler<string> onCalculationFailed = null;
        //
        // Subscribe to TaskCompleted: When the CalculationCompleted event is fired, set result.
        onCalculationCompleted = (sender, args) =>
        {
            // Not forgetting to release the event handler
            CalculationCompleted -= onCalculationCompleted;
            myTaskSource.SetResult(args.Value);
        };
        //
        // Subscribe to TaskFailed: If there is an error in the execution, set error.
        onCalculationFailed = (sender, args) =>
        {
            CalculationFailed -= onCalculationFailed;
            myTaskSource.SetException(new Exception(args));
        };
        CalculationCompleted += onCalculationCompleted;
        CalculationFailed += onCalculationFailed;
        // Finally execute the task and return the associated Task promise.
        base.GetFibonacciNumberAsync(ordinal);
        return myTaskSource.Task;
    }
}
现在计算斐波那契数的调用将看起来类似:
public async Task<int> CalculateFibonacciValueAsync(int ordinal)
{
    var fibonacciSource =  new FibonacciSourceAsync();
    try
    {
        return (await fibonacciSource.GetFibonacciNumberAsync(ordinal));
    }
    catch (Exception ex)
    {
        // TODO: Do something with exception 
    }
}
该实现可以通过进度和取消令牌实现进一步扩展。
多线程与任务
关于异步调用,有一点很重要需要认识到,它们不一定在单独的线程上运行。实际上,调用被“安排”在所谓的同步上下文中运行在主线程上,除非它们被指示否则。同步上下文是负责安排需要等待的异步调用的消息队列。一旦异步方法(或大多数情况下是Task)成功执行,结果就会发布回同步上下文(即主 UI 线程)。
为了演示目的,我们将使用之前示例中的异步实现(EAP 转换)以及一些额外的诊断调用,以获取有关正在使用的线程和同步上下文的信息。
注意
这里使用的 TraceThreadInfo 方法及其相关的 ThreadInfo 类是一个通过依赖注入使用的自定义实现。原因是 threading 命名空间只包含 PCL 中的任务相关类,而获取当前线程 ID 的唯一方法是通过平台特定的实现。平台特定的实现模式将在后续章节中详细讨论。
在跟踪方法中,我们将记录当前线程 ID 和当前同步上下文:
public IThreadInfo ThreadInfo
{
    get { return Mvx.GetSingleton<IThreadInfo>(); }
}
private void TraceThreadInfo(string message)
{
    Debug.WriteLine("{0} on Thread '{1}'", message, ThreadInfo.CurrentThreadId);
    Debug.WriteLine("Current Synchronization Context is {0}", SynchronizationContext.Current);
}
附在计算按钮上的计算命令是:
TraceThreadInfo("Begin DoCalculate");
if (!string.IsNullOrWhiteSpace(Input))
{
    int numberOrdinal;
    if (int.TryParse(Input, out numberOrdinal))
    {
        InfoText = "Calculating";
        TraceThreadInfo("Calling GetFibonacciNumberAsync");
        var result = await GetFibonacciNumberAsync(numberOrdinal);
        TraceThreadInfo("Response from GetFibonacciNumberAsync");
        Result = result.ToString();
        InfoText = string.Empty;
        TraceThreadInfo("End DoCalculate");
        return;
    }
}
InfoText = "Invalid Input";
相关的跟踪日志如下所示:

Tasks 的内联执行跟踪
通过查看上述执行堆栈的跟踪消息,可以很容易地看出,尽管我们正在处理异步任务,但整个执行过程都在主线程上完成,除了对源内部方法的实际调用(即它在 Thread 106 上执行)。其余的方法调用具有 Android.App.SyncContext 作为同步上下文,执行顺序与实现的调用序列没有不同。
稍微改变实现并使用 Task 项的 ContinueWith 函数,我们得到稍微不同的结果。代码如下:
TraceThreadInfo("Calling GetFibonacciNumberAsync");
await GetFibonacciNumberAsync(numberOrdinal).ContinueWith(task =>
{
    TraceThreadInfo("Response from GetFibonacciNumberAsync");
    Result = task.Result.ToString();
    InfoText = string.Empty;
});
TraceThreadInfo("End DoCalculate");
此实现的跟踪日志如下所示:

Tasks 的异步执行
如跟踪日志所示,ContinueWith lambda 在单独的线程上执行,但执行仍然是顺序的。
注意
这里的一个重要注意事项是我们正在将结果返回到 ViewModel 的单独线程上。在这个例子中,跨线程调用由 MvvmCross 框架处理。如果我们处理这个赋值,调用将类似于:
await GetFibonacciNumberAsync(numberOrdinal).ContinueWith(task =>
{
    TraceThreadInfo("Response from GetFibonacciNumberAsync");
    this.RunOnUiThread(() =>
    {
        txtResult.Text = task.Result.ToString();
    });
    txtInfo.Text = "";
});
在前面的例子中,一旦执行进入单独的线程,同步上下文就被取消。在 .NET 运行时,未由主同步上下文跟踪的异步任务实际上被分配了一个 TaskScheduler 实例,并通过此上下文执行。在这种情况下,TaskScheduler 负责将成功后消息重定向回主线程,如果任务配置为使用相同的上下文(即 ConfigureAwait(true))。
然而,在 .NET 中同步上下文的工作方式以及配置的任务调用返回到主线程可能会导致死锁,如果异步任务在主线程上以同步方式(即使用 Task.Result 或 Task.Wait())调用。在这种情况下,一旦异步调用执行完成并尝试返回到主上下文,由于主上下文实际上正在等待异步任务本身完成,因此主上下文仍然不可访问。
ConfigureAwait(false)通知调度器不要在任务被调用的相同执行上下文中寻找并返回结果,而是直接在执行上下文中执行并运行到完成。这避免了死锁场景。
这种死锁场景是.NET 运行时的特定问题,因为 Android 上的 Mono 运行时和 Mono.Touch 编译器处理任务执行的方式;在这些平台上目前不会发生死锁。然而,遵循与异步任务和 awaitables 相关的编码约定非常重要,以避免任何意外行为。
为了在单独的线程上执行整个任务,我们可以使用Task.Run(这将任务推送到线程池)或Task.Factory.StartNew。使用StartNew方法可以让你定义即将在任务中执行的方法类型,并让运行时做出使用不同线程的明智决策。代码如下:
var task = Task.Factory.StartNew(async () =>
{
    TraceThreadInfo("Calling GetFibonacciNumberAsync");
    var result = await GetFibonacciNumberAsync(numberOrdinal);
    TraceThreadInfo("Response from GetFibonacciNumberAsync");
    Result = result.ToString();
    InfoText = string.Empty;
}, TaskCreationOptions.LongRunning);
task.ConfigureAwait(false);
在下面的跟踪中,与之前的示例最大的不同是,DoCalculate方法甚至在为计算创建的任务开始执行之前就退出了。这种执行方式非常适合在跨平台移动应用项目中应用 MVVM 模式。它可以避免任何 UI 阻塞问题,并为用户提供连续性的感觉。

启动一个新的异步任务
如果我们想要分析 iOS 应用程序上的相同执行过程(即计算某个序号上的斐波那契数列中的数字),我们可以很容易地使用 Xcode Instruments 的“系统跟踪”模板来识别线程模式。

计算四个不同的斐波那契数
异常处理
如果在多线程实现中不遵循正确的异步路径,处理异常可能会变得繁琐。然而,在大多数情况下,async/await 语言构造减轻了开发者的负担。如果异步链没有被中断,并且调用被正确实现,那么在异步上下文中捕获异常与使用线性代码捕获它们没有区别。
使用我们之前章节中的示例:
try
{
    var result = await GetFibonacciNumberAsync(numberOrdinal);
    Result = result.ToString();
    InfoText = "";
}
catch (Exception ex)
{
    Debug.WriteLine("Error:" + ex.Message);
    InfoText = "EX:" + ex.Message;
}
在这个例子中,如果我们传入的参数是一个负数,它将抛出一个包含消息无法计算负数的斐波那契数的异常,我们将在信息文本框中显示错误消息。
然而,如果我们使用ContinueWith结构来执行相同的代码,结果会有所不同:
try
{
    await GetFibonacciNumberAsync(numberOrdinal).ContinueWith(result =>
    {
        Result = result.Result.ToString();
        InfoText = string.Empty;
    });
}
catch (Exception ex)
{
    Debug.WriteLine("Error:" + ex.Message);
    InfoText = "EX:" + ex.Message;
}
在这个例子中,我们会收到一个或多个错误发生的异常消息。原因是第二个场景中抛出的异常是由于我们创建的异步链而导致的AggregateException。

异步链中的 AggregateException
如果我们在任务本身上使用.Result或.Wait()调用,结果将会相同。
这个实现的重要部分是我们对异步方法调用await。如果不是这样,捕获块永远不会被调用。没有await关键字,try/catch 块只会检查任务的准备是否按预期进行,而不是实际的执行。
另一种无法用 try/catch 块捕获的异步执行类型是返回void而不是Task或Task<T>的异步方法。类似于在事件处理器中抛出异常,它们只能被捕获在AppDomain.UnhandledException或Application.ThreadException事件中。对于异步方法来说,始终是一个更好的实践,让它们返回Task然后返回void。
然而,在ContinueWith实现中,有了手头的Task引用,我们也可以在执行完成之前检查任务的状态。这个赋值实际上导致了异常向上层冒泡。在这种情况下,我们不需要 try/catch 块。代码如下:
await GetFibonacciNumberAsync(numberOrdinal).ContinueWith(result =>
{
    TraceThreadInfo("Response from GetFibonacciNumberAsync");
    if (result.IsFaulted)
    {
        Result = string.Empty;
        InfoText = string.Join("\r\n", result.Exception
            .InnerExceptions.Select(exception => exception.Message));
    }
    else
    {
        Result = result.Result.ToString();
        InfoText = string.Empty;
    }
});
初始化模式
尤其是在涉及服务的场景中,一个常见的需求是拥有一个初始化函数,该函数将准备通信通道并/或执行一个“ping”或认证调用。当开发者面临此类场景时,他们可能犯的最大错误就是在构造函数中使用.Result和/或.Wait()语句调用异步初始化函数(使其成为一个同步调用)。
对于这种场景,让我们假设我们有一个实现了两个简单异步方法实现的接口的服务。
public interface IService
{
    Task<string> AuthenticateAsync(string username, string password);
    Task<int> ServiceMethodAsync(string myParameter);
}
为了能够调用ServiceMethodAsync,我们首先需要调用AuthenticateAsync并从服务接收认证令牌。代码如下:
public MyService(string username, string password)
{
    //
    // Following call would block the constructor
    // IMPORTANT: If it was being called from the main UI thread, it might cause a deadlock
    // Blocking Call Example 1:
    // AuthenticateAsync(username, password).Wait();
    // Blocking Call Example 2:
    m_Token = AuthenticateAsync(username, password).Result;
}
在这个例子中,我们在服务的构造函数中实现了认证调用。尽管在某些情况下实现可能有效,但如果服务构造函数是从主 UI 线程调用的,线程将进入死锁,正如前一个部分所描述的。
最简单的解决方案是公开初始化函数给外部层,或者让服务在每次服务调用之前调用初始化。为此,我们可以将认证调用包装在初始化方法中。代码如下:
public MyService(string username, string password)
{
    m_Username = username;
    m_Password = password;
}
private async Task EnsureInitializationAsync()
{
    if (string.IsNullOrEmpty(m_Token))
    {
        m_Token = await AuthenticateAsync(m_Username, m_Password);
    }
}
服务方法调用看起来可能如下所示:
public async Task<int> ServiceMethodAsync(string myParameter)
{
    await EnsureInitializationAsync();
    try
    {
        int result = await InternalServiceMethodAsync(myParameter);
        return result;
    }
    catch (Exception ex)
    {
        // TODO:
        throw;
    }
}
如前所述,我们也可以通过接口公开初始化:
/// <summary>
/// Describes the service as requiring async initialization
/// </summary>
public interface IAsyncInitialization
{
    /// <summary>
    /// The result of the asynchronous initialization.
    /// </summary>
    Task Initialization { get; }
}
public class MyService : IService, IAsyncInitialization
{
...
    public Task Initialization { get; private set; }
    public MyService(string username, string password)
    {
        m_Username = username;
        m_Password = password;
        Initialization = EnsureInitializationAsync();
    }
    private async Task EnsureInitializationAsync()
    {
        if (string.IsNullOrEmpty(m_Token))
        {
            m_Token = await AuthenticateAsync(m_Username, m_Password);
        }
    }
...
}
在这种情况下,调用方法需要检查服务是否需要异步初始化,并检查所需的任务结果。代码如下:
if (serviceInstance is IAsyncInitialization)
{
    /// Wait for the results of the initialization
    await serviceInstance.Initialization;
}
await serviceInstance.ServiceMethodAsync("param");
信号量
在异步上下文中,同步和节流方法与经典的.NET 运行时实现略有不同。例如,不允许在异步调用中使用锁块,并且您将无法使用Mutex进行同步。Mutex不适用,因为互斥锁只能由单个线程拥有,而异步执行并不保证在它们启动的同一线程上完成。代码如下:
//
// Error: The 'await' operator cannot be used in the body of a lock statement
//lock (m_FibonacciSource)
//{   
//    var result = await GetFibonacciNumberAsync(numberOrdinal);
//}
//
// Warning: Might work but not guaranteed
m_Mutex.WaitOne(200);
await GetFibonacciNumberAsync(numberOrdinal).ContinueWith((task) =>
{
    TraceThreadInfo("Response from GetFibonacciNumberAsync");
    Result = task.Result.ToString();
    InfoText = string.Empty;
});
m_Mutex.ReleaseMutex();
为了处理异步任务的非确定性执行和线程模型,.NET 添加了一个新的结构:信号量。Semaphore(WaitHandle的实现)和SemaphoreSlim(Semaphore的轻量级版本,使用监视器实现)类型在Wait和Release调用上不强制执行线程身份,并且可以异步等待。
例如,让我们执行一系列并行计算,这些计算由允许 3 个访问计数(SemaphoreSlim(3)或SemaphoreSlim(3,3))的信号量编排,如下所示:
var semaphoreSlim = new SemaphoreSlim(3);
int count = 11;
for (var i = 0; i < 7; i++)
{
    Task.Factory.StartNew(() =>
    {
        return semaphoreSlim.WaitAsync().ContinueWith((waitTask) =>
        {
            return Task.Factory.StartNew(() =>
            {
                return GetFibonacciNumberAsync(count = Interlocked.Increment(ref count)).ContinueWith(
                    (calculateTask) =>
                    {
                        TraceThreadInfo(string.Format("Current count on Semaphore: {0}",
                            semaphoreSlim.Release() + 1));
                    });
            }, TaskCreationOptions.LongRunning);
        });
    }, TaskCreationOptions.LongRunning);
这种并行执行可以在 Instruments 系统跟踪模板的平均系统时间视图中轻松找到。如下面的截图所示,每个序号计算都会给出所选计算线程上的确切峰值数):

同步线程的系统时间平均值
后台任务
当需要执行非时间限制的长时间运行过程时,线程和任务解决方案并不是唯一的选择。此外,这两种选项都是用于在您的应用程序处于前台或活动状态时执行代码。当应用程序进入后台或挂起状态时,应用程序可能仍然需要在易失性数据丢失之前执行一些较长时间的方法,或者当应用程序不在交互状态时,可能需要后台运行一个进程。
对于这些类型的场景,iOS 和 Android 都提供了后台和后台操作选项。
iOS 上的后台任务
iOS 上的后台任务是应用程序执行处理任务的最简单方式,无需 UI 线程或响应生命周期代理。
可以执行三种类型的后台任务,以满足不同的需求:
- 
后台安全任务:这些任务可以在过程生命周期的任何阶段执行。它们不会受到应用程序进入后台的影响和/或中断。代码如下: nint taskId = UIApplication.SharedApplication .BeginBackgroundTask(() => { // TODO: Do something if the allotted time runs out }); // TODO: Implement the processing logic if (taskId != UIApplication.BackgroundTaskInvalid) { UIApplication.SharedApplication.EndBackgroundTask(taskId); }
- 
进入后台任务:另一种类型的后台任务用于将状态保存或清理逻辑传递给后台进程。 DidEnterBackground生命周期委托用于初始化这些任务,并在应用程序进入后台状态后继续处理。这些任务的创建与后台安全任务类似。唯一的区别是必须在执行块内部而不是调用线程中调用EndBackgroundTask方法,因为调用进程可能已经返回,不再等待后台任务的执行。代码如下:public override void DidEnterBackground (UIApplication application) { nint taskId = UIApplication.SharedApplication .BeginBackgroundTask(() => { // TODO: Do something if the allotted time runs out }); Task.Run(() => { // TODO: Implement the processing logic UIApplication.SharedApplication.EndBackgroundTask(taskId); }); }
- 
后台传输:这些任务特定于 iOS 7+,提供更长的处理时间(其他后台任务的严格限制为 600 秒,而后台传输可以持续长达 10,000 秒)。后台传输任务用于执行长时间的网络操作,并通过网络上传/下载大文件。 
服务(仅限 Android)
在 Android 平台上,一旦活动进入后台状态,它们就无法执行任务,通常在进入后台后不久就会被停止。服务是引入的应用程序组件,为开发者提供一个接口,以便在后台启动和停止长时间运行的过程。
尽管服务是作为应用程序的一部分创建的,但它们有自己的独立生命周期,即使启动它们的程序和活动被停止或销毁,它们也可以运行。
服务可以采取以下两种形式之一:
- 
启动状态:当活动通过使用意图调用 StartService方法显式启动服务时,服务处于“启动状态”。启动服务通常用作BackgroundWorker,一旦处理操作完成,服务本身或活动会停止它。
- 
绑定状态:一个“绑定”服务通常作为客户端在应用程序的活动或甚至其他应用程序中的提供者。只要另一个组件绑定到它,绑定服务就会保持活跃。 
这两种初始化模式都使用类似的回调方法,但不限于OnStartCommand、OnBind、OnCreate和OnDestroy来启动后台处理并处理其生命周期。
在 Android 命名空间中实现了各种基类,根据需求,可以实现这些基类并启动或绑定。
为了实现一个用于执行一些后台处理任务的启动服务,实现过程的第一步是创建IntentService类:
[Service]
[IntentFilter(new String[] { "com.xamarin.MyDemoService" })]
public class MyDemoService : IntentService
{
    public MyDemoService()
        : base("MyDemoService")
    {
    }
    protected override void OnHandleIntent(Intent intent)
    {
        var myParameter = intent.GetStringExtra("parameter");
        // Invoke long running process
        DoWork(myParameter);
    }
}
IntentService基类已经处理了生命周期事件,例如OnStart,因此我们只需实现OnHandleIntent方法来响应活动发出的意图请求。该类的两个属性Service和IntentFilter被 Xamarin 编译器用于在应用程序清单中生成条目。此实现的调试构建会在应用程序清单中输出以下服务条目。代码如下:
<service android:name="md5d06a1058f86cf8319abb1555c0b54fbf.MyDemoService">
    <intent-filter>
        <action android:name="com.xamarin.MyDemoService" />
    </intent-filter>
</service>
在活动中使用此实现时,可以通过使用意图过滤器条目或使用服务类型来启动意图服务。
//StartService (new Intent (this, typeof(MyDemoService)));
StartService(new Intent("com.xamarin.MyDemoService"));
摘要
总体而言,异步/并发实现模式和后台任务允许开发者将繁重的工作从 UI 线程推离,并在现代移动应用的单一线程范式下创建响应式应用程序。
基于任务的异步模式提供了一种高效且可扩展的方式来轻松实现异步操作。此外,进度、取消和并发集合有助于监控、扩展和管理这些异步块的执行,同时提供了一种相互协作的方式。实现这些块时,开发者无需承担线程、同步以及根据硬件资源扩展线程的负担。
在本书中分析了内存和 CPU 相关主题之后,在下一章中,我们将讨论本地存储以及如何高效地使用它。
第四章:本地数据管理
在本章中,您将找到高效使用、管理和漫游移动设备上数据的模式和技巧。它还探讨了 SQLite 数据库的创建和使用策略。本章分为以下部分:
- 
移动应用程序中的数据 
- 
应用程序数据 
- 
本地文件系统 
- 
数据漫游 
- 
SQLite 
- 
模式和最佳实践 
- 
备份/漫游 
移动应用程序中的数据
在移动应用程序开发中,“数据”一词可以指代不同类型的信息和存储位置。它可以用来描述每次使用应用程序中的视图时创建和销毁的易失状态,或者它可能指的是运行应用程序所需的持久设置和配置信息,甚至可能是存储在本地文件系统中的数据。每种类型的数据在应用程序或应用程序中的视图的生命周期中创建、持久化或销毁。我们可以就这一点讨论四个不同的组。
每种数据类型都存储和访问于不同的位置,每个位置都有其独特的限制和访问模型。

数据类型存储位置
状态
移动应用程序通常是有状态的。用于在 UI 上可视化项目或由应用程序用户创建的数据属于这一类别。状态的目的在于在会话、设备以及/或进程生命周期中保持一致的 app 体验。应用程序设置或视图的当前状态是这一类别的良好例子。
应用数据
应用数据通常指的是执行应用程序所必需的数据。这些数据由应用程序本身创建、存储和管理。它可以是结构化数据存储,也可能是在线应用程序资源的缓存版本。这类数据可以是原始数据,以 SQLite 数据库的形式存在,或者由当前应用程序在当前设备上通过其他设施存储。
应用数据存储在不同的位置可以经历应用程序生命周期的不同阶段。

应用数据生命周期
本地文件
本地文件是存储在本地文件系统中的项目。这些文件通常在应用程序的生命周期和/或作用域之外创建,并且仅由应用程序使用。例如,用户拍摄的照片可以后来被邮件客户端应用程序用作附件项。
外部数据
外部数据可以描述为应用程序在运行时使用的所有其他数据源的组合。这可以包括网络或网络资源。
应用程序数据
应用程序数据构成了 Xamarin 平台和 Windows Runtime 上数据存储的核心。这些数据专属于您的应用程序。它们与它共存,最终消亡,在大多数情况下,它们对运行在同一设备上的其他应用程序甚至使用该应用程序的用户(至少直接)来说不相关或不可访问。
应用程序可以无限制地访问应用程序数据,或所谓的隔离存储,无需请求用户权限或添加声明,并且可以在大多数情况下根据应用程序数据位置的类型写入、读取和查询此存储中的项目。
安装目录
安装目录是可访问数据存储的最内层部分,是应用程序最亲密的位置。应用程序对此位置的访问不受限制,但仅限于读取。iOS、Android 和 Windows Runtime 上的访问模型差异很大。
Android
对于 Xamarin.Android 应用程序,安装目录本质上指的是压缩的 Android 包(即.apk文件),而定义的子目录仅仅是打包并添加到编译时清单中的文件夹的抽象。安装目录和子目录可以通过多种方式访问。

Android 包和项目树
安装目录中对于 Android 应用程序最重要的位置是Resources文件夹。资源可以概括为用于渲染应用程序视图的 UI 相关项。可以包含在应用程序包中的资源类型之一是drawable类型。Drawable 资源是图像资源,可以根据应用程序运行的不同条件和使用设备存在不同的版本(参见第九章,可重用 UI 模式)。为了使编译器将资源包含到应用程序包中,此文件夹中每个项目的构建操作必须设置为AndroidResource。
注意
需要特别指出的是,Android 包不允许文件名包含大写字母,而 Xamarin 开发者可以将这类文件包含到他们的项目中。Xamarin.Android 通过在编译期间重命名资源来处理这个问题(例如,参见drawable文件夹中的XamarinLogo.png文件)。
通过编程方式,可以使用生成的Resource类来获取分配的资源 ID,或者使用提供访问方法的Resources静态类,或者使用android.resource://协议和资源标识符(或者包名与资源名称一起)。然而,在大多数情况下,仅使用分配的 ID 来使用与 UI 控件相关的资源就足够了。代码如下:
var myImageResourceId = Resource.Drawable.XamarinLogo;
var myImageView = (ImageView) FindViewById(Resource.Id.MyImageView);
// Set the Image resource using the id.
myImageView.SetImageResource(myImageResourceId);
// OR:
// Retrieving the resource itself and then assigning it.
Drawable myImageResource = Resources.GetDrawable(myImageResourceId);
myImageView.SetImageDrawable(myImageResource);
在声明式 UI(布局)中,可绘制资源文件夹可以通过别名@drawable访问。同样,字符串资源可以通过@string访问。代码如下:
<ImageView android:src="img/xamarinlogo" 
           android:layout_width="wrap_content" 
           android:layout_height="match_parent" />
安装目录中的另一个重要位置是Assets文件夹。Assets文件夹用于存放您希望与应用程序一起部署的任何原始资源(除了Resources文件夹),并且不会被编译器或运行时处理。可以使用AssetManager类检索资源,并且Activity类中的Assets属性可以用来访问AssetManager类。代码如下:
Task.Run(async () =>
{
    using (var dataPackageStream = Assets.Open("Data.csv"))
    using (var streamReader = new StreamReader(dataPackageStream))
    {
        var content = await streamReader.ReadToEndAsync();
        // TODO: Do something with the comma separated content.
    }
});
安装位置中的其他资源类型,如布局、原始和字符串资源,也可以使用 Android 运行时提供的抽象以这种方式访问。
iOS
iOS 应用程序的构建单元,如可执行代码和相关资源,包含在一个所谓的包中。包是应用程序沙盒的一部分,包的路径在安装期间由操作系统确定。
与 Android 应用程序类似,iOS 应用程序项目也可以包含编译后的图像资源(包资源)。然后,使用运行时提供的抽象层来访问这些项。
例如,为了从包目录中访问图像资源,您需要在UIImage类上调用FromFile方法:
var image = UIImage.FromFile("XamarinLogo.png");
//
// OR making a roundtrip (get the path, read the file, create // image
// Similar to /data/Containers/Bundle/Application/<id>/XamarinMasteriOS.app/XamarinLogo.png
var imagePath = NSBundle.MainBundle.GetUrlForResource("XamarinLogo", "png").Path;
var fileContent = System.IO.File.ReadAllBytes(imagePath);
var secondImage = UIImage.LoadFromData(NSData.FromArray(fileContent));
注意
与 Android 应用程序的访问模型类似,包容器是只读的,不应修改。简单的原因是 iOS 应用程序包由发布者密钥签名,任何对包容器的更改都会使包签名无效。
本地存储
第二部分也有类似的情况。Android 和 iOS 运行时为应用程序数据提供不同的存储设施,无论是结构化数据还是原始内容文件。
Android
在 Android 平台上,Shared Preferences和Internal Storage是两种本地存储选项。这两个选项有不同的访问模型,并且您的应用程序对这些位置有读写访问权限。
在 Android 平台上使用SharedPreferences是存储数据的最基本方式。此类提供了一个简单的持久字典实现,允许应用程序创建、修改和检索原始数据类型(即boolean、float、int、long、string和string_array)及其相关键。这些值的尺寸仅受数据类型本身限制。
提示
如其名所示,SharedPreferences通常用于存储用户选择的配置选项,并且跨用户会话持久化。还有一个基本活动实现PreferenceActivity,可以轻松创建和重用用于用户偏好的视图,该视图利用SharedPreferences来为应用程序提供服务。
SharedPreferences 类的使用模式很简单。为了使用活动的默认首选项或自定义偏好文件,Activity 类提供了专用方法:
// Retrieve an object for accessing private to this activity
ISharedPreferences myPreferences = GetPreferences(FileCreationMode.Private);
// Retrieve and hold the contents of the preference file 'MyCustomPreferences'
ISharedPreferences myCustomPreferences = GetSharedPreferences("MyCustomPreferences", FileCreationMode.Private);
在检索调用之后,如果偏好文件尚未存在,则会根据选择的 FileCreationMode 类创建该文件。要获取偏好条目的值,您可以使用该类提供的获取方法。代码如下:
var myStringValue = myCustomPreferences.GetString("MyStringValue", string.Empty);
var myIntValue = myCustomPreferences.GetInt("MyIntValue", default(int));
要编辑值,可以使用 SharedPreferences 类的 Editor 类。代码如下:
ISharedPreferencesEditor myEditor = myCustomPreferences.Edit();
myEditor.PutString("MyStringValue", myStringValue);
myEditor.PutInt("MyIntValue", myIntValue);
// Apply the current changes from the editor back 
// to the Singleton SharedPreferences class
myEditor.Apply();
// OR
// Commit the changes to the singleton instance 
// AND the disk immediately
myEditor.Commit();
内部存储是应用程序的专用存储。应用程序可以自由创建和检索此目录中的任何类型的文件(和文件夹)。
注意
FileCreationMode 是在 Android 运行时中使用的访问修饰符,用于定义文件的访问类型和权限级别。
- 
追加: 如果文件已存在,则将数据写入现有文件的末尾而不是删除。这应该与 Android.Content.Context.OpenFileOutput一起使用。
- 
启用预写日志记录: 当此数据库的打开标志被设置时,数据库默认启用预写日志记录功能打开。 
- 
多进程: 在 Gingerbread(Android 2.3)及之前版本中是遗留行为,并且当针对这些版本时默认启用。对于针对更高 SDK 版本的程序,必须显式设置。当与 SharedPreferences一起使用时,即使共享首选项实例已经加载到该进程中,也会检查磁盘上的文件是否有修改。当应用程序有多个进程访问同一文件时,这种行为是期望的。
- 
私有: 这是默认的文件创建模式,其中创建的文件只能由调用应用程序(或所有共享相同用户 ID 的应用程序)访问。 
- 
世界可读/世界可写: 两者在 API 级别 17 中已弃用,因为存在安全漏洞,它们可能导致应用程序文件的可访问性。 
此文件夹中的文件,如果没有任何清单声明,可以通过应用程序上下文上的指定方法或使用 Xamarin/Mono 的 IO 方法实现访问。代码如下:
// Creating a file in the application internal storage root
using(var fileStreamInRootPath = this.OpenFileOutput("FileInRootPath", FileCreationMode.Private))
using (var streamWriter = new StreamWriter(fileStreamInRootPath))
{
    streamWriter.Write("Hello World!");
}
//
// Reading the contents of the file
using(var fileStreamInRootPath = this.OpenFileInput("FileInRootPath"))
using (var streamReader = new StreamReader(fileStreamInRootPath))
{
    var stringContent = streamReader.ReadToEnd();
}
// Getting the file path.
// e.g.: /data/data/Xamarin.Master.Android/files/FileInRootPath
var filePath = FilesDir.AbsolutePath + "/" + "FileInRootPath";
// Using the Xamarin (Mono) implementation.
System.IO.File.AppendAllText(filePath, "\r\nAdditional Content");
var allText = System.IO.File.ReadAllText(filePath);
除了基本的 CRUD 操作外,您还可以创建额外的文件夹并枚举文件和文件夹。
iOS
在 iOS 应用程序中最简单的数据存储选项是属性列表(.plist 文件)。这些文件旨在用于相对较小的数据量,这些数据可以用原始数据类型表示。它们可以定义为字典或数组,并以 XML 格式序列化和持久化。
您可以直接使用相关类(NSArray 和 NSDictionary)读取和写入属性列表。例如,一个简单的实现,创建和读取属性列表的代码可能如下所示(带有额外的诊断条目):
myNSDictionary.WriteToFile(dictionaryPath, true);
Debug.WriteLine("File Contents:");
var fileContents = System.IO.File.ReadAllText(dictionaryPath);
Debug.WriteLine(fileContents);
var myNewNSDictionary = NSDictionary.FromFile(dictionaryPath);
Debug.WriteLine("Values read from plist:");
foreach (var key in myNewNSDictionary.Keys)
{
    var keyValue = myNewNSDictionary[key];
    Debug.WriteLine(string.Format("Value for the key '{0}' is '{1}'", key, keyValue));
}
上述实现的结果如下所示:
File Contents:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
 <key>firstKey</key>
 <string>firstValue</string>
 <key>secondKey</key>
 <string>secondValue</string>
 <key>thirdKey</key>
 <integer>8</integer>
</dict>
</plist>
Values read from plist:
Value for the key 'firstKey' is 'firstValue'
Value for the key 'secondKey' is 'secondValue'
Value for the key 'thirdKey' is '8'
当涉及到本地文件存储时,iOS 文件系统为应用程序预留了几个位置;从应用程序的角度来看,这些位置都有特定的用途。
- 
Documents/:Documents库通常用于用户生成的内容。如果文件内容要向用户公开,则应使用此文件夹。此文件夹的内容由 iTunes 备份。
- 
Documents/Inbox:Inbox文件夹是存储应用程序请求打开的文件的地方。应用程序可以读取和删除这些文件;它没有修改这些文档的权限。
- 
Library/:Library文件夹是您不希望向用户公开的文件的根目录。应用程序可以在该目录中创建文件和额外的文件夹。
- 
Library/Application Support:这个库文件夹中的子目录通常用于包含由您的应用程序管理的文件,例如配置文件、模板、保存的数据和购买内容。应将目的地为该文件夹的内容放置在具有应用程序捆绑标识符或公司 ID 的自定义子目录中。
- 
Library/Caches:Caches文件夹用于存储非必需的应用程序创建的文件。
- 
Library/Preferences:应用程序特定的首选项存储在这个文件夹中。然而,访问这个文件夹应通过首选项 API 进行。
- 
tmp/:tmp文件夹是另一个非必需临时文件的位置。
可以使用System.IO命名空间和相关类访问这些库位置。
临时存储
临时存储和/或缓存目录是应用程序不需要任何特定权限的另一个位置。这是应用程序可以保存非必需文件以减少网络或处理时间的地方。这些文件夹的持久性不受操作系统的保证。
在 Android 和 iOS 系统中,指定的缓存和/或临时位置可以通过上下文属性访问,并且可以使用System.IO命名空间和相关类执行 CRUD 操作。
在 Android 上,可以通过上下文的CacheDir属性访问缓存目录:
// Path similar to /data/data/Xamarin.Master.Android/cache
var cacheFilePath = this.CacheDir.AbsolutePath + "/" + "CacheFile";
// Writing to the file
System.IO.File.AppendAllLines(cacheFilePath, new[] { "Cached Content" });
// Reading the file
var cachedContent = System.IO.File.ReadAllText(cacheFilePath);
在 iOS 上,有两个单独的位置用于临时文件(/temp/)和缓存文件(Library/Caches/)。缓存文件比临时数据持久化时间更长,但它们仍然可能被系统删除以释放磁盘空间。代码如下:
// getting the root application sandbox path
var documents = Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments);
// paths to caches and temporary files directories.var cache = Path.Combine(documents, "..", "Library", "Caches");
var tmp = Path.Combine(documents, "..", "tmp");
这两个目录都没有备份或同步到 iCloud。
本地文件系统
在 iOS 上,应用程序无法以编程方式访问应用程序沙盒之外的外部文件(例如,iOS 应用程序无法以编程方式导航到用户的图片目录并选择文件)。本地文件系统和 iOS 应用程序沙盒之间的桥梁仅限于 iOS 8 之前的图片选择控制器。iOS 8 引入了新的文档选择控制器和文档提供者 API。在此交互模型中,实现文档提供者扩展的应用程序创建文档选择 UI,宿主应用程序使用提供的 UI 让用户选择在宿主应用程序执行中使用的文档(类似于 Windows 运行平台上的文件打开选择器和提供者功能)。

UIImagePickerController
对于 Android,除了仅适用于应用程序特定的本地文件存储之外,应用程序还可以访问两个其他位置:公共和私有外部存储(取决于硬件)。在此上下文中,外部存储指的是 SD 卡存储,这在 iOS 系统中不可用。在 Android 运行时,应用程序可以访问根路径(操作系统根路径)并遍历公共文件夹。
让我们看看 Android 文件系统中一些内部和外部路径返回的路径:
Trace.WriteLine(Environment.RootDirectory, "FileSystem");
Trace.WriteLine(Environment.DataDirectory, "FileSystem");
Trace.WriteLine(this.GetExternalFilesDir(Environment.DirectoryDownloads).AbsolutePath, "FileSystem");
Trace.WriteLine(this.GetExternalFilesDir(Environment.DirectoryDocuments).AbsolutePath, "FileSystem");
// Call with GetExternalFilesDir
Trace.WriteLine(this.GetExternalFilesDir(Environment.DirectoryMovies).AbsolutePath, "FileSystem");
Trace.WriteLine(this.GetExternalFilesDir(Environment.DirectoryMusic).AbsolutePath, "FileSystem");
Trace.WriteLine(this.GetExternalFilesDir(Environment.DirectoryPictures).AbsolutePath, "FileSystem");
Trace.WriteLine(Environment.GetExternalStoragePublicDirectory(Environment.DirectoryMovies).AbsolutePath, "FileSystem");
Trace.WriteLine(Environment.GetExternalStoragePublicDirectory(Environment.DirectoryMusic).AbsolutePath, "FileSystem");
Trace.WriteLine(Environment.GetExternalStoragePublicDirectory(Environment.DirectoryPictures).AbsolutePath, "FileSystem");
Trace.WriteLine(Environment.DownloadCacheDirectory, "FileSystem");
Trace.WriteLine(Environment.ExternalStorageDirectory, "FileSystem");
这些调用的输出标识了应用程序特定和公共位置:
I/mono-stdout(10079): FileSystem: /system
I/mono-stdout(10079): FileSystem: /data
I/mono-stdout(10079): FileSystem: /storage/emulated/0/Android/data/Xamarin.Master.Android/files/Download
I/mono-stdout(10079): FileSystem: /storage/emulated/0/Android/data/Xamarin.Master.Android/files/Documents
I/mono-stdout(10079): FileSystem: /storage/emulated/0/Android/data/Xamarin.Master.Android/files/Movies
I/mono-stdout(10079): FileSystem: /storage/emulated/0/Android/data/Xamarin.Master.Android/files/Music
I/mono-stdout(10079): FileSystem: /storage/emulated/0/Android/data/Xamarin.Master.Android/files/Pictures
I/mono-stdout(10079): FileSystem: /storage/emulated/0/Movies
I/mono-stdout(10079): FileSystem: /storage/emulated/0/Music
I/mono-stdout(10079): FileSystem: /storage/emulated/0/Pictures
I/mono-stdout(10079): FileSystem: /cache
I/mono-stdout(10079): FileSystem: /storage/emulated/0
尽管 Android 开发者可以访问大量存储访问方法选项,但他们需要实现自己的文件选择对话框或使用其他已安装应用程序提供的接口(Android 运行时还提供了应用程序之间的提供者-消费者类型的文件共享实现)。

一个示例文件浏览器实现(Xamarin 食谱)
如果有一个默认处理文件对话框的应用程序(可以处理ActionGetContent意图的活动),则可以通过意图调用它,并且可以通过OnActivityResult回调方法访问结果。
SQLite
SQLite 数据库实现为移动应用程序项目提供关系型持久化数据结构。与关系数据库使用的通用服务器/客户端模型不同,SQLite 是一个本地数据库实现,数据存储在应用程序本地存储中。Xamarin.iOS 和 Xamarin.Android 应用程序项目都可以包含 SQLite 数据库及其相关实现。
为了使用 SQLite,开发者需要在跨平台实现 ADO.Net 之间进行选择,其中 SQL 查询应创建并作为纯文本包含,或者使用 SQLite.Net 便携式类库的 linq-2-entities 访问模型。它作为 NuGet 包和组件提供。

SQLite.Net PCL
对于以下演示,我们将使用 SQLite.Net 库的异步版本。
使用 SQLite.Net 实现 SQLite 数据访问层通常遵循代码优先的数据库编程范式。在这个模式中,开发者首先通过创建实体类并使用提供的属性定义数据结构来定义他们的数据模型。代码如下:
public class LocationInfo
{
    [PrimaryKey, AutoIncrement]
    public int LocationInfoId { get; set; }
    public string Name { get; set; }
    public double Latitude { get; set; }
    public double Longitude { get; set; }
}
一旦完成数据模型实现,我们就可以开始创建 SQLite 访问方法。
为了创建一个 SQLite 连接,首先需要为数据库文件定义一个应用程序存储位置。代码如下:
public TravelContext(string sqlitePath, ISQLitePlatform platform)
{
    var connectionString = new SQLiteConnectionString(sqlitePath, false);
    var connectionWithLock = new SQLiteConnectionWithLock(platform, connectionString);
    m_SqliteConnection = new SQLiteAsyncConnection(() => connectionWithLock);
    // OR with non-async connection
    //var connection = new SQLiteConnection(platform, sqlitePath);
}
在这个实现中,ISQLitePlatform提供了平台特定 API 所需的抽象。
当 SQLite 连接准备好使用时,我们可以实现数据表的访问和创建方法。代码如下:
private void InitTablesAsync()
{
    var tasks = new List<Task<CreateTablesResult>>();
    tasks.Add(m_SqliteConnection.CreateTableAsync<LocationInfo>());
    tasks.Add(m_SqliteConnection.CreateTableAsync<City>());
    tasks.Add(m_SqliteConnection.CreateTableAsync<Landmark>());
    tasks.Add(m_SqliteConnection.CreateTableAsync<Comment>());
    // OR
    //var initTask = m_SqliteConnection.CreateTablesAsync<LocationInfo, City, Landmark, Comment>();
    var initTask = Task.WhenAll(tasks);
    initTask.ConfigureAwait(false);
}
我们现在可以通过数据上下文中的公共属性公开表,以便上层可以对这些表执行查询。代码如下:
var dbPath = Path.Combine(this.FilesDir.Path, "myTravelDb.db3");
// TODO: Use Dependency Injection
var platform = new SQLitePlatformAndroid();
var myDbContext = new TravelContext(dbPath, platform);
var landmarksInCityTask = await myDbContext.Landmarks
    .Where(item => item.CityId == cityId).ToListAsync();
可以通过实体关系和级联操作扩展数据模型。还有 SQLite.Net PCL 库的扩展,用于懒加载和子相关操作。
模式和最佳实践
在本节中,我们将探讨两个在移动应用程序中常见的模式,以及如何以平台无关的方式实现这些使用场景。
应用程序偏好设置
应用程序偏好设置是移动应用程序中常见的场景。为了在 iOS 上使用之前描述的属性列表和在 Android 上使用SharedPreferences,通常最合适的做法是使用一个公共的字典接口。然后该接口将在平台特定项目中继承,并可以注入到公共库中。
为了简单演示,我们可以定义一个简单的接口,用于检索和保存字符串值。代码如下:
public interface ISettingsProvider
{
    string this[string key] { get; set; }
}
在 Android 端实现将使用一个简单的字典,通过共享偏好设置实现。代码如下:
public class SettingsProvider : ISettingsProvider
{
    private readonly ISharedPreferences m_SharedPreferences;
    public SettingsProvider(string name = "default")
    {
        // Retrieve and hold the contents of the preference file'
        m_SharedPreferences = Application.Context.GetSharedPreferences(name, FileCreationMode.Private);
    }
    public string this[string key]
    {
        get
        {
            if (m_SharedPreferences.Contains(key))
                m_SharedPreferences.GetString(key, string.Empty);
            return string.Empty;
        }
        set
        {
            var editor = m_SharedPreferences.Edit();
            editor.PutString(key, value);
            editor.Apply();
        }
    }
}
在 iOS 端,实现将使用NSMutableDictionary类来方便用户编辑偏好设置。代码如下:
public string this[string key]
{
    get
    {
        if (m_MyNSMutableDictionary.ContainsKey(new NSString(key)))
        {
            return MyNSMutableDictionary [key].ToString();
        }
        return string.Empty;
    }
    set
    {
        MyNSMutableDictionary [key] = new NSString(value);
        MyNSMutableDictionary.WriteToFile(GetPropertyListPath(), true);
    }
}
现在持久化字典已在两个平台上实现,我们可以将应用程序设置作为一个单例包括进来,以便与依赖注入一起使用。
这个实现可以通过 iOS 平台的设置 API 和 Android 平台的偏好设置视图(PreferencesFragment和PreferencesActivity)来扩展,以创建一个看起来更原生的方式。
文件选择器
在一个跨平台应用程序项目中,如果我们遵循 MVVM 模式,视图模型应该位于共享项目或 PCL 中,这样业务逻辑就可以在应用程序之间共享。然而,如果我们有选择文件进行处理的必要,方法实现应该位于视图本身,因为包含视图的平台特定项目可以访问平台功能。尽管这会将业务逻辑移动到 UI 组件中,但工作必须由视图来完成。
然而,您可以将视图模型的责任委托给视图,而不会损害 MVVM 实现。委托过程可以通过定义文件选择操作的接口的控制反转(IOC)来执行。
为了演示这种用法,我们将使用一个名为IFilePickerService的接口。在这个例子中,我们只想让用户选择一个文件,并将结果文件路径返回给视图模型和可能的数据模型。代码如下:
public interface IFilePickerService
{
    Task<string> PickFileAsync();
}
我们将在视图模型中使用此接口来调用视图执行逻辑。代码如下:
return new MvxCommand(() =>
{
    m_FilePickerService.PickFileAsync()
        .ContinueWith(task =>
        {
            Debug.WriteLine("File Picked:" + task.Result);
        });
});
对于 Android 实现,我们将使用支持相应意图类型的默认文件管理应用程序。我们需要将OnActivityResult类上的意图执行和回调调用转换为异步实现。为了做到这一点,我们将使用任务完成源。代码如下:
private TaskCompletionSource<string> m_PickFileCompletionSource;
每次调用意图时,私有变量将被初始化,并在回调方法中设置结果。考虑到这种模式,接口方法实现将类似于以下内容:
public Task<string> PickFileAsync()
{
    m_PickFileCompletionSource = new TaskCompletionSource<string>();
    Intent intent = new Intent();
    intent.SetType("*/*");
    intent.SetAction(Intent.ActionGetContent);
    intent.AddCategory(Intent.CategoryOpenable);
    try
    {
        StartActivityForResult(intent, 0);
    }
    catch(ActivityNotFoundException ex)
    {
        throw new InvalidOperationException("Could not find a file manager");
    }
    return m_PickFileCompletionSource.Task;
}
最后,回调方法实现只是将结果设置在TaskCompletionSource类上。代码如下:
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
    base.OnActivityResult(requestCode, resultCode, data);
    if (resultCode == Result.Ok)
    {
        m_PickFileCompletionSource.TrySetResult(data.Data.Path);
    }
    else if(resultCode == Result.Canceled)
    {
        m_PickFileCompletionSource.SetCanceled();
    }
}
现在我们已经创建了IFilePickerService接口,至少在 Android 端,我们必须使用我们正在使用的依赖注入提供程序注册类型,然后我们就可以依赖它在视图模型初始化中解析类型。(在这个例子中,我们将使用 MVVMCross 框架。)
代码如下:
public MainView()
{
    Mvx.RegisterType<IFilePickerService>(()=>this);
}
结果应用程序将执行选择文件命令并打开文件浏览器,将文件路径返回给视图模型。如果用户取消文件选择,任务将抛出一个异常,通知操作已被取消。

默认文件浏览器
对于 iOS 部分,我们的工作要简单一些:
public Task<string> PickFileAsync()
{
    var taskCompletionSource = new TaskCompletionSource<string>();
    var documentTypes = new string[] { UTType.PNG, UTType.Image, UTType.BMP };
    var filePicker = new UIDocumentPickerViewController(documentTypes, UIDocumentPickerMode.Open);
    EventHandler<UIDocumentPickedEventArgs> documentPickedHandler = (sender, args) =>
    {
        taskCompletionSource.SetResult(args.Url.Path);
    };
    filePicker.DidPickDocument += documentPickedHandler;
    return taskCompletionSource.Task;
}
完成这些后,我们只需注册类型,我们最终就有一个依赖于依赖注入的平台特定方法的跨平台命令实现。
备份/漫游
Xamarin 目标平台都提供了云同步和备份机制。虽然 Android 备份策略更像是异步后台进程,其中备份和恢复操作必须由调用应用程序启动,但 iOS 和 iCloud 游走策略提供了对文件系统的无缝集成。
Android 和备份 API
Android 备份 API 和 Google 提供的备份传输服务为应用程序开发者提供了一个简单易用的方式,用于将应用程序数据备份和恢复到远程云存储。使用 BackupManager 提供的 API,在工厂重置后或一个设备到另一个设备之间恢复数据是可能的。
备份操作由 Android 运行时的 BackupManager 执行,与应用程序数据相关的操作委托给在应用程序清单中注册的 BackupAgent。记住这一点很重要,即您的应用程序必须在 Android 备份服务中注册。在包清单中包含您从注册中获得的备份服务密钥至关重要。
为了创建 BackupAgent,您必须实现 BackupAgent 抽象类的 OnBackup 和 OnRestore 方法。在这些方法中,您的数据的老旧和新旧状态以 ParcelFileDescriptor 的形式提供(可以用来访问实际文件的文件元数据)。在恢复方法中,您还会收到应用程序版本,如果数据结构在应用程序更新之间发生变化,这可能很有帮助。
创建代理的另一种方式是使用现有的代理模板(BackupAgentHelper)并使用现有的辅助类来备份和恢复应用程序数据的一定子集。
例如,SharedPreferencesBackupHelper 类是用于您的应用程序的 SharedPreferences 文件上的备份操作的一般实现。可以将应用程序的首选项组传递给辅助器,辅助器类可以处理备份逻辑实现。
另一个辅助类是 FileBackupHelper 类,它可以用来备份和恢复应用程序文件。
为了演示备份 API 和一个常见的备份场景,我们可以创建一个备份代理,它会跟踪备份事件和方法执行。实现类应该从 BackupAgentHelper 类派生:
public class PreferencesBackupService : BackupAgentHelper
{
    // TODO: Override the methods we might need
}
要将此备份代理包含到我们的应用程序中,我们可以编辑应用程序清单或使用程序集信息中的 ApplicationAttribute 属性。AssemblyInfo.cs 和 AndroidManifest.xml 都可以在 Properties 项目文件夹下找到。

应用程序清单和 AssemblyInfo
使用 ApplicationManifest.xml 文件,让我们添加备份代理和备份服务密钥:
<application android:label="Xamarin.Master.Android" 
             android:icon="@drawable/Icon" 
             android:backupAgent="PreferencesBackupService">
  <meta-data android:name="com.google.android.backup.api-key"  
             android:value="..." />
</application>
如果我们处理的是 Java 类库而不是 Xamarin 和 JNI 桥接,前面的应用程序清单条目将看起来是这样。实际上,一旦收到备份请求,此注册就会抛出错误。代码如下:
09-22 18:28:33.647 E/ActivityThread(32153): Agent threw during creation: java.lang.ClassNotFoundException: Didn't find class "Xamarin.Master.Android.PreferencesBackupService" on path: DexPathList[[zip file "/data/app/Xamarin.Master.Android-1.apk"],nativeLibraryDirectories=[/data/app-lib/Xamarin.Master.Android-1, /system/lib]]
要将 PreferencesBackupService 类注册到 Android 运行时,我们需要为该类型本身添加一个标识符。由于我们不在清单声明中使用命名空间限定符,我们可以在应用程序默认命名空间中注册该类:
[Register("Xamarin.Master.Android.PreferencesBackupService")]
public class PreferencesBackupService : BackupAgentHelper 
如果我们不使用应用程序清单条目来注册备份代理而使用 Application 属性,则属性将类似于以下使用 AssemblyInfo.cs 文件:
[assembly: Application(AllowBackup = true, BackupAgent = typeof(PreferencesBackupService))]
[assembly: MetaData("com.google.android.backup.api_key", Value = "...")]
在这种情况下,Android 可调用包装器(ACW)使用我们备份代理的默认命名约定创建,并插入到应用程序清单中,因此我们不需要额外注册我们的类。应用程序清单中生成的条目包含对命名空间和包含的程序的 MD5 哈希:
md5d06a1058f86cf8319abb1555c0b54fbf.PreferencesBackupService
提示
如果你使用 Visual Studio 进行开发并在模拟器上运行应用程序,你可以在 <projectdir>\obj\<buildconfig>\android\src 目录中看到为 Android 暴露的类生成的 MD5 值。

Android 源目录
注册完成后,我们可以在代理类中重写几个方法以获取跟踪信息。代码如下:
public override void OnCreate()
{
    var preferencesHelper = new SharedPreferencesBackupHelper(this, "ApplicationSettings");
    AddHelper("ApplicationPreferences", preferencesHelper);
    Debug.WriteLine("PreferencesBackupService was created", "BackUp");
    base.OnCreate();
}
你现在可以打开 Android Adb 控制台并使用以下命令来触发备份请求:
adb shell bmgr enable true
adb shell bmgr run
一旦数据段发生变化,你可以使用 BackupManager 类的 DataChanged 方法并使用它来请求恢复操作。(在正常情况下,恢复操作由 Android 备份服务安排和执行,因此应用程序不需要显式调用它。)
代码如下:
BackupManager backupManager = new BackupManager(this);
// Notifying the backup manager about data changes
backupManager.DataChanged();
// Using an implementations of RestoreObserver class to request restore
backupManager.RequestRestore(new MyRestoreObserver());
iOS 和通用存储
为了在 iOS 应用程序中使用 iCloud 功能,它们必须在 Apple 配置门户 和项目清单中进行配置。
在配置门户中,创建 App ID 时,必须将 iCloud 选择为启用服务之一。然后,使用 <TeamID>.<BundleID> 格式,将容器标识符插入到 Entitlements.plist 文件中。需要编辑的键如下:
com.apple.developer.ubiquity-kvstore-identifier
com.apple.developer.ubiquity-container-identifiers
在 iOS 上,提供的最简单的同步机制是以键/值对的形式对原始数据类型进行同步。这用于简单的用户首选项或需要在不同客户端之间同步的应用程序所需值。键/值对的总大小不能超过 64 千字节,最大值大小为 64 kB,键大小为 64 字节。
可以通过 NSUbiquitousKeyValueStore 类访问同步上下文。代码如下:
/// <summary>
/// Synchronizes local values to the cloud
/// </summary>
private void SyncUpSettings()
{
    var store = NSUbiquitousKeyValueStore.DefaultStore;
    //
    // Can use designated set functions for different value types
    // string, bool, NSData, NSDictionary, NSObject[], long, double
    store.SetString("myStringValue", "New String Value");
    store.SetLong("myLongValue", 1234);
    store.SetBool("myBoolValue", true);
    store.Synchronize();
}
使用相同的存储,你可以访问值:
/// <summary>
/// Gets the values from synchronized local storage
/// </summary>
/// <returns></returns>
private Dictionary<string,object> GetValues()
{
    var results = new Dictionary<string,object>();
    var store = NSUbiquitousKeyValueStore.DefaultStore;
    //
    // Getting the synchronized LOCAL values
    results.Add("myStringValue",store.GetString("myStringValue"));
    results.Add("myLongValue", store.GetLong("myLongValue"));
    results.Add("myBoolValue", store.GetBool("myBoolValue"));
    return results;
}
同步过程并不是在调用同步方法后立即发生。该过程根据 iCloud 自己的计划启动;上同步通常在 5 秒内发生,而确切知道下同步何时发生的方法是向NSUbiquitousKeyValueStore事件添加Observer代理。
代码如下:
NSNotificationCenter.DefaultCenter.AddObserver(
    NSUbiquitousKeyValueStore.DidChangeExternallyNotification, (notification) =>
    {
        NSDictionary userInfo = notification.UserInfo;
        // NInt: 0-ServerChange, 1-InitialSyncChange, 
        // 2-QuotaViolationChange
        NSNumber reasonNumber = (NSNumber) userInfo.ObjectForKey(NSUbiquitousKeyValueStore.ChangeReasonKey);
        // NSString[] You can used the changed items list to sync only those values
        NSArray changedKeys = (NSArray) userInfo.ObjectForKey(NSUbiquitousKeyValueStore.ChangedKeysKey);
        // OR get the latest values from synchronized local storage
        var latestValues = GetValues();
    });
对于同步文件,实现稍微复杂一些。虽然备份和恢复场景由 iOS 应用程序和 iTunes 自动处理,但为了保持同步文件存储,开发者需要实现UIDocument类以准备需要在设备之间同步的文档类型。
UbiquityContainer目录由所谓的守护进程管理,以协调 iCloud 上下文中文件的同步和修改。为了避免引起并发问题并干扰守护进程处理,需要访问和修改相关文件时,必须使用NSFilePresenter和NSFileCoordinator类。
使用演示者和协调器进行文件操作的最简单方法是实现UIDocument基类。需要实现两个虚拟方法来读取数据并将数据写入文档。
假设我们想要为我们的应用程序保持序列化实体数据的同步上下文。首先,我们需要将我们的类声明为继承并实现从UIDocument类所需的构造函数。代码如下:
public class EntityDocument<T> : UIDocument
{
    public EntityDocument(NSUrl url)
        : base(url)
    {
        m_Type = typeof(T);
    }
然后,我们需要实现两个虚拟方法。以下加载方法仅从云中反序列化数据到泛型类类型定义中定义的实体。代码如下:
/// <summary>
/// Content down-sync'd from the cloud
/// </summary>
public override bool LoadFromContents(NSObject contents, string typeName, out NSError outError)
{
    // TODO: Implement a try/catch block to return (if any) errors as well as negative result (i.e. return false).
    outError = null;
    if (contents != null)
    {
        var serializedData = NSString.FromData((NSData)contents, NSStringEncoding.UTF8);
        m_Entity = JsonConvert.DeserializeObject<T>(serializedData);
    }
    // LoadFromContents called when an update occurs
    NSNotificationCenter.DefaultCenter.PostNotificationName(string.Format("{0}DocumentModified",m_Type.Name), this);
    return true;
}
最后,我们可以实现保存方法,该方法将序列化对象并将流保存到无处不在的容器中。代码如下:
/// <summary>
/// Content to up-sync to the cloud
/// </summary>
public override NSObject ContentsForType(string typeName, out NSError outError)
{
    // TODO: Implement a try/catch block to return (if any) errors as well as negative result (i.e. return false).
    outError = null;
    if (m_Entity != null)
    {
        var serializedData = JsonConvert.SerializeObject(m_Entity);
        NSData docData = new NSString(serializedData).Encode(NSStringEncoding.UTF8);
        return docData;
    }
    return null;
}
为了能够使用名为LocationInfo的示例类实现此实现,我们首先可以实施一个加载文件程序(我们正在为每个加载的位置使用单个文件查询,但这可以通过使用ENDSWITH或CONTAINS之类的查询进行扩展)。代码如下:
private void GetLocationsInfo(string locationName)
{
    var locationDataQuery = new NSMetadataQuery();
    locationDataQuery.SearchScopes = new NSObject[] {NSMetadataQuery.UbiquitousDocumentsScope};
    locationDataQuery.Predicate = NSPredicate.FromFormat(string.Format("{0} == %@",
        NSMetadataQuery.ItemFSNameKey), new NSString(locationName + "Data.txt"));
    NSNotificationCenter.DefaultCenter.AddObserver(this, new Selector("locationLoaded:"),
      NSMetadataQuery.DidFinishGatheringNotification, locationDataQuery);
    locationDataQuery.StartQuery();
}
一旦查询返回,我们就可以将对象扩展到所需的数据。代码如下:
[Export("locationLoaded:")]
private void DidFinishGatheringHandler(NSNotification notification)
{
    var locationQuery = (NSMetadataQuery) notification.Object;
    locationQuery.DisableUpdates();
    locationQuery.StopQuery();
    NSNotificationCenter.DefaultCenter.RemoveObserver(this, NSMetadataQuery.DidFinishGatheringNotification, locationQuery);
    LoadLocationInfo(locationQuery);
    // listen for notifications that the document was modified via the // server 
    NSNotificationCenter.DefaultCenter.AddObserver(this, new Selector("itemReloaded:"),
      new NSString("LocationInfoDocumentModified"),
      null);
}
示例中的LoadLocationInfo函数将简单地尝试打开文件并处理加载的数据。代码如下:
private void LoadLocationInfo(NSMetadataQuery locationDataQuery)
{
    if (locationDataQuery.ResultCount == 1)
    {
        NSMetadataItem item = (NSMetadataItem) locationDataQuery.ResultAtIndex(0);
        var url = (NSUrl)item.ValueForAttribute(NSMetadataQuery.ItemURLKey);
        m_LocationData = new EntityDocument<LocationInfo>(url);
        m_LocationData.Open((success) =>
        {
            if (success)
            {
                var info = m_LocationData.Entity;
                // TODO: Do something with the location info loaded
            }
            else
                Console.WriteLine("failed to open iCloud           document");
        });
    }
}
注意,我们还在使用在EntityDocument<T>类中定义的通知名称(string.Format("{0}DocumentModified", m_Type.Name)订阅数据更改事件。重新加载的实现只是简单地从通知本身收集对象。代码如下:
[Export("itemReloaded:")]
private void DataReloadedHandler(NSNotification notification)
{
    var locationData = (EntityDocument<LocationInfo>) notification.Object;
    var entityData = locationData.Entity;
    // TODO: Do something with the location info loaded.
}
对于保存和同步数据,我们只需在UIDocument类上分配新数据并更新更改计数即可。代码如下:
private void SyncLocationDataChanges(LocationInfo info)
{
    m_LocationData.Entity = info;
    m_LocationData.UpdateChangeCount(UIDocumentChangeKind.Done); 
}
这个主题将在第五章网络中进一步讨论。
摘要
在本章中,我们讨论了一些本地存储容器和访问策略。在 Xamarin 的两个平台中,通过额外的将数据备份和同步到云端的选项,开发者可以创建一致的用户界面以及具有状态感的移动应用程序。
在下一章中,我们将讨论网络连接选项以及如何使用目标 Xamarin 平台提供的本地存储选项与连接数据一起使用。
第五章:网络通信
在本章中,我们将详细探讨 Xamarin 应用程序的网络功能以及各种服务集成场景。本章还包括了在连接应用程序场景中如何使用本地存储进行数据缓存的实际示例。它分为以下部分:
- 
已连接的应用 
- 
网络服务 
- 
推送通知 
- 
SignalR 
- 
模式和最佳实践 
- 
平台特定概念 
- 
云集成 
已连接的应用
根据定义,移动应用程序应该尽可能轻量级和资源高效。你不能期望将媒体和其他内容打包到应用程序中,然后分发应用程序或为用户数据创建过大的存储空间,尤其是对于那些主要目的是提供用户访问相关内容或存储和操作数据的程序。
例如,在处理跨平台项目时,创建统一业务逻辑和存储的最简单方法之一是创建一个网络服务层,并将责任和逻辑委托给这一层。在这种情况下,应用程序(s)将仅负责提供由服务层提供的内容,或将用户输入传达给服务层。
这种方法不仅提高了应用程序的效率,还在逻辑实现和表示之间创建了一个抽象层。这允许开发者从存储和执行的技术选择上摆脱平台限制。
还需要提到的是,应用程序对外部资源的依赖不是一个选择问题,而已经变成了一个必要性,因为应用程序越来越依赖于第三方网络服务 API 和社交媒体网络。
网络服务
网络服务通常定义为通过网络(网络)进行的可互操作机器到机器通信。在跨平台应用程序的上下文中,这个定义中最重要的术语将是“可互操作”。使用不同框架或语言编写的网络服务,在运行于不同类型的运行时和硬件上,符合相同的标准,其中大部分可以被运行在各种平台上的应用程序消费,包括 Xamarin 目标平台。
Xamarin 目标平台,即 iOS 和 Android,以及 Windows Runtime,可以使用 TCP/IP(传输控制协议/互联网协议的简称)堆栈通过安全或非安全的 HTTP(超文本传输协议的简称)传输层访问无状态网络服务。尽管可以通过网络服务消费各种数据表示,但 JSON 和 XML 是最常见的基于文本的表示法。
注意事项
在定义或访问网络服务时,有三个基本元素需要考虑。我们可以将这些称为网络服务的 A-B-C:地址、绑定和合约。地址是远程访问服务的位置,绑定定义了传输和安全协议,合约定义了服务使用的数据类型和方法。
虽然在 Web 服务合约中定义的方法和数据类型非常特定于情况,但 Xamarin 应用程序可以通用的传输和序列化协议。
在 Web 服务场景中,如果消费者是 Xamarin 目标平台,您应该始终坚持使用异步实现进行客户端实现。如前所述,Web 服务客户端的异步实现可以减少阻塞主线程的机会,并保护应用程序免受网络短缺相关错误和崩溃的影响。
运输
对于 iOS 和 Android 平台上的 Xamarin 应用程序,主要的通信协议是 HTTP。HTTP 传输可以通过证书或凭据在客户端和/或消息级别进行加密。
在 iOS 和 Xamarin.Android 应用程序的其他版本中,消息级别的安全是可选的。在 iOS 9 中,应用传输安全(ATS)功能强制执行对网络资源的加密连接。尽管可以将某些域添加到排除列表中,或者完全关闭目标应用程序的 ATS,但强烈建议您为 Xamarin.iOS 应用程序使用安全的 HTTP(或 HTTPS)传输。
尽管在 Xamarin 平台上完全或部分支持 TCP、UDP 或 HTTP 上的 WebSocket 通信协议,但根据当前的服务基础设施实现,这些通信通道不能与网络服务一起使用。
消息
服务的消息规范定义了在 HTTP 传输层传输数据时应使用哪种格式。
在处理网络服务的 Xamarin 应用程序中,消息应根据服务要求构建为 SOAP(简单对象访问协议)、POX(代表纯文本 XML)或 JSON。

简单 SOAP 消息示例
消息结构对于客户端和服务器实现之间请求和响应对的序列化和反序列化非常重要。因此,可以采用其他类型的数据通信模型,这将需要客户端和服务器进行额外的自定义实现。
SOAP/XML 服务
SOAP 网络服务使用 SOAP 定义的架构封装的 XML 数据对象。Windows Communication Foundation(WCF)服务和 ASP.Net 旧版服务(ASMX)都是 SOAP 服务,并符合 SOAP 协议。
SOAP Web 服务合约是在 Web 服务描述语言(WSDL)中定义的,WSDL 文档,连同其他 XML 数据模式(例如,XSD 文件),通常可以通过 Web 服务 URL 访问。使用此文档,无论底层语言如何,都可以以一致的方式定义 Web 服务,并且可以被各种客户端接口和消费。

SOAP 1.1 服务的服务 WSDL
在 Xamarin 应用程序中,创建所谓的代理(服务消费者)的一种可能方式是使用 Silverlight SDK 生成访问代码。使用 Silverlight SDK 的主要原因在于 Windows Communication Foundation 客户端基础设施并未完全包含在 Xamarin 核心中,只能使用与 Silverlight 框架非常相似的客户端功能子集来访问 Web 服务。
为了生成客户端,您只需使用命令行工具执行以下命令:
slsvcutil http://localhost/ ReferenceService.svc /d:c:\bin\
提示
SLSvcUtil 可在各种 SDK 中找到,包括 Windows Phone 7、Windows Phone 8、Windows Phone 8.1(Silverlight)以及实际的 Silverlight SDK 目录:
- 
C:\Program Files (x86)\Microsoft SDKs\Windows Phone\v7.0\Tools\SlSvcUtil.exe
- 
C:\Program Files (x86)\Microsoft SDKs\Windows Phone\v8.0\Tools\SlSvcUtil.exe
- 
C:\Program Files (x86)\Microsoft SDKs\Windows Phone\v8.1\Tools\SlSvcUtil.exe
- 
C:\Program Files (x86)\Microsoft SDKs\Silverlight\v5.0\Tools\SlSvcUtil.exe
前面的命令将生成一个可以与支持 SOAP 1.1 配置的任何 Web 服务通信的 WCF 客户端。如果我们想要消费 WCF 服务,支持的绑定配置将是 BasicHttpBinding 和 WebHttpBinding(本质上是一个 REST 绑定)。WSHttpBinding 和类似配置使用其他 SOAP 配置来封装数据请求和响应。

生成 Silverlight 代理
生成的客户端将具有基于事件和异步编程模型(APM)的异步方法来访问客户端。
[OperationContract (AsyncPattern=true, Action="master.xamarin.com/ReferenceService/GetRegions", ReplyAction="master.xamarin.com/ReferenceService/GetRegionsResponse")]
IAsyncResult BeginGetRegions(Xamarin.Master.TravelTrace.Data.Region filter, AsyncCallback callback, object asyncState)
List<Xamarin.Master.TravelTrace.Data.Region> EndGetRegions(IAsyncResult result)
public void GetRegionsAsync(Xamarin.Master.TravelTrace.Data.Region filter)
另一种方法是在 Visual Studio 或 Xamarin Studio 中创建 Web 引用。Web 引用只能用于与实现 WS-I Basic Profile 1.1(换句话说,SOAP 1.1)的服务进行通信。由 Web 引用生成的客户端使用 ASMX 通信堆栈(.NET 2.0 服务技术),而不是服务引用所使用的 WCF 客户端基础设施。

添加 Web 引用对话框(Visual Studio)
如果我们要比较由 Web 引用和 Silverlight SDK 生成的客户端,我们可以轻松地识别出底层技术。
// Web Service Generated Client.
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Web.Services.WebServiceBindingAttribute(Name="BasicHttpBinding_ReferenceService", Namespace="master.xamarin.com")]
[GeneratedCodeAttribute("System.Web.Services", "4.6.79.0")]
public partial class AsmxReferenceServiceClient : System.Web.Services.Protocols.SoapHttpClientProtocol
// WCF Generated Client
[GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
public partial class ReferenceServiceClient : System.ServiceModel.ClientBase<ReferenceService>, ReferenceService
通过查看生成的两个代理的类图,我们可以对方法执行策略有更多的了解:

生成的代理比较
在跨平台项目中集成生成的代理的理想方式是将服务引用添加到可移植类库中,以便用于特定平台的项目。为了在 Visual Studio 中向 PCL 项目添加服务引用,您必须将 Windows Phone 8.1 作为目标之一删除,或者添加对System.ServiceModel命名空间的引用(Visual Studio 将自动从目标列表中删除 Windows Phone 8.1)。Windows Phone 8.1 平台不包含 Windows Communication Foundation 客户端程序集。完成此步骤后,添加服务引用选项将出现在项目上下文菜单中。
对于涉及 Windows Phone 8.1 的场景,更合适的解决方案是使用 RESTful 服务和客户端。
RESTful 服务
与 SOAP 服务相比,RESTful 服务没有 SOAP 协议的额外开销或请求/响应对的封装。本质上,SOAP 方法调用引起的网络流量与 REST 调用的请求/响应对相同。表示状态转移(REST)模型的简单性提高了性能和可维护性。RESTful 服务的无状态和可缓存方法使它们成为 Xamarin 目标平台的最佳解决方案。
REST 服务本质上可以描述为静态 HTTP 端点。用于访问这些端点的 HTTP 动词(GET、PUT、POST 和 DELETE)定义了在服务层上要调用的方法类型(PUT 用于更新,POST 用于创建,DELETE 用于删除操作)。消息结构可以从 JSON 到 XML,甚至到 ATOM。
在 Xamarin 目标平台上,有各种现成的选项和附加组件可用于基于 REST 的 Web 服务。这些选项中的任何一个都可以用来执行 Web 请求,并且请求/响应对可以根据要求和选择的消息媒体类型进行序列化/反序列化。
由于我们正在对 REST 端点进行常规的 Web 请求,最简单的实现将涉及HttpClient,它包含在System.Net.Http命名空间中。
例如,如果我们想要实现一个基类来处理上一节中使用的 RESTful Web 服务的 CRUD(创建、读取、更新和删除)方法(TravelTrace.ReferenceDataService),我们可以在内部 HTTP 客户端层周围实现一个针对每个调用的包装器。
public BaseClient(string baseAddress, string securityToken)
{
    if (string.IsNullOrEmpty(baseAddress)) throw new ArgumentNullException("baseAddress");
    BaseAddress = new Uri(baseAddress);
  // Storing the security token in a class property of type string
    SecurityToken = securityToken.StartsWith("Bearer") ? securityToken.Substring(7) : securityToken;
    m_HttpClient = CreateHttpClient();
}
您会注意到我们正在使用基础地址作为服务器地址,并且如果有的话,使用安全令牌来初始化我们的客户端。在这个实现中,创建方法将简单地创建 HTTP 客户端,并使用身份验证令牌作为默认头。另一个重要要求是将“Accept”头设置为通知客户端期望从服务器接收哪种类型的内容(在这个例子中是 JSON)。
private HttpClient CreateHttpClient()
{
    var httpClient = new HttpClient();
    httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    if (string.IsNullOrEmpty(SecurityToken))
    {
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", SecurityToken);
    }
    return httpClient;
}
在 HTTP 管道准备好执行请求后,我们可以开始实现 REST 服务的基方法。
protected async Task<string> GetStringAsync(string path)
{
    // if we are using the BaseClient multiple times
    // we can create a new transport with each method
    //HttpClient httpClient = CreateHttpClient();
    try
    {
        // Get the response from the server url and REST path for the data
        var response = await m_HttpClient.GetAsync(new Uri(BaseAddress, path));
        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            throw new UnauthorizedAccessException("Access Denied");
        }
        if (response.IsSuccessStatusCode)
        {
            return await response.Content.ReadAsStringAsync();
        }
        throw new WebException(response.ReasonPhrase);
    }
    catch (Exception ex)
    {
        // TODO:
        throw ex;
    }
}
现在,GetRegions 方法看起来是这样的:
var regions = await GetStringAsync("regions");
这个请求的结果可以在调试屏幕中可视化:

来自 Web API 的 JSON 数据
然而,这仅仅是服务数据的字符串表示,我们需要扩展我们的实现以包括一个 JSON 序列化器。有多个序列化选项可用,包括通过 Microsoft BCL 包提供的标准库:System.Xml 和 System.Json 命名空间。NewtonSoft Json.NET 仍然是最受欢迎的 JSON 库之一,并且可以通过 NuGet 获取。
public async Task<List<Region>> GetRegionsAsync(Region filter = null)
{
    var result = new List<Region>();
    var regions = await GetStringAsync("regions");
    var resultingList = JToken.Parse(regions);
    await Task.Run(() =>
    {
        result.AddRange(resultingList["value"]
            .Select(item => item.ToObject<Region>()));
    });
    return result;
}
使用这个实现,我们可以在基类实现中创建通用方法,并将序列化责任推到这一层。
protected async Task<List<T>> GetListAsync<T>(string path)
{
    List<T> result = new List<T>();
    try
    {
        var response = await GetStringAsync(path);
        var resultingList = JToken.Parse(response);
        await Task.Run(() =>
        {
            result.AddRange(resultingList["value"]
                .Select(item => item.ToObject<T>()));
        });
    }
    catch (Exception ex)
    {
        // TODO:
        throw ex;
    }
    return result;
}
我们可以扩展这个通用实现以用于其他 Web 方法,并为我们的 RESTful 客户端创建基础。认证场景将在下一节中进一步讨论。
对于 Xamarin 开发者来说,还有许多更多的 REST 消费者实现可用,这些模块可以通过组件和 NuGet 包(如 RestSharp、Hammock 等)包含到跨平台项目中。
OData 和 OAuth
OData 和 OAuth 是两种广泛接受的用于 RESTful 通信场景的标准/协议。Xamarin 移动应用程序处理外部资源,尤其是第三方 Web 服务 API 时,通常实现这些协议。
OData
与作为通信协议的 SOAP 不同,REST 只是一种针对 Web 服务实现的架构方法。RESTful 服务不需要符合某些规范,可能差异很大。为了识别 RESTful 服务的需求并为客户端应用程序和服务器之间交换的数据创建统一的结构,微软在 2007 年启动了 OData。OData 现在是一个国际上认可的协议,由 OASIS 维护,并被各种应用程序、平台和公司(例如,微软 Azure 移动服务、微软 Office 365 Web 访问、Salesforce、SAP Netweaver Gateway Solution、IBM WebSphere 等)支持/使用。
在 OData 协议中,每个对象集都由一个符合 REST 原则的端点定义。对于 GET 请求,这些实体集端点可以接受对象标识符,从而得到该特定实体实例的详细信息,或者可以使用 OData 过滤器和其他查询选项查询列表中的实体。
与 SOAP/XML 服务中的 WSDL 类似,可访问的端点(实体集和函数)以及服务合同中使用的类型通常通过带有 CSDL(OData 公共架构定义语言)文件的元数据端点提供服务。
要访问整个元素列表,请访问 http://localhost/Xamarin.Master.TravelTrace.Service.Api/odata/regions。
要访问实体集端点中的单个元素,请访问 http://localhost/Xamarin.Master.TravelTrace.Service.Api/odata/regions(guid'90222c18-66fa-441a-b069-0115faa1e0f1')。
要查询具有筛选条件的元素列表,请访问 http://localhost/Xamarin.Master.TravelTrace.Service.Api/odata/regions?$filter=Continent eq 'Europe'。
使用 OData 协议,也可以进行涉及额外属性展开、lambda 操作符和函数的高级 OData 查询;然而,这些主题超出了本书的范围。
可以下载的 NuGet 包和组件有多种,这些组件作为开源和/或免费软件,有助于为 OData 服务生成客户端。
OAuth
OAuth 是一个开放标准,通常由服务提供商用于授权。OAuth 的一般用例可能是使用第三方身份提供者,如 Live ID(微软)、Google、Facebook 或 Twitter,在移动或网络应用程序中进行身份验证和授权。
经典的 OAuth 2.0 实现场景通常是一个两步过程。第一步涉及用户通过提供者的网络界面授予客户端应用程序访问权限。第二步是使用从提供者网络界面接收到的授权代码来获取访问令牌,以访问提供者的资源。

Facebook 作为身份验证提供者
在网络应用程序上的授权过程的第一步通常是显示提供者授权页面的 iframe。在 Xamarin 应用程序中,这一步是通过使用网页视图控件或更专业的实现(WebAuthenticationBroker 是 Windows Phone 8.1 上的现成控件)来执行的。考虑到提供者页面会向客户端应用程序页面发出带有授权令牌的回调请求,而客户端应用程序负责从回调 URL 或内容体中解析和提取此令牌,实现两步验证过程可能会变得相当繁琐。

Xamarin.Auth 组件
为了提供对 OAuth API 的访问并简化实现,开发者可以利用可用的 Xamarin OAuth 组件:Xamarin.Auth(可在 Xamarin.iOS 和 Xamarin.Android 平台上使用)。还有一个配套的组件用于社交媒体提供者 API:Xamarin.Social。
使用 Xamarin.Auth 实现,使用 Facebook API 进行身份验证可以简单到只需几行代码。
var authenticationBroker = new OAuth2Authenticator(
    clientId: "<App ID from https://developers.facebook.com/apps>",
    scope: "",
    authorizeUrl: new Uri("https://m.facebook.com/dialog/oauth/"),
    redirectUrl: new Uri("http://www.facebook.com/connect/login_success.html"));
authenticationBroker.Completed += (sender, eventArgs) =>
{
    DismissViewController(true, null);
    if (eventArgs.IsAuthenticated)
    {
        // TODO: eventArgs.Account contains the authenticated user info
    }
    else
    {
        // TODO: Possibly the user denied access to the account or
        // the user could not authenticate with the provider
    }
};
// The GetUI method returns UINavigationControllers on iOS, and Intents on Android
PresentViewController(authenticationBroker.GetUI(), true, null);
SignalR
ASP.NET SignalR 是一种网络服务器端技术,允许开发者为他们的应用程序传递实时更新。SignalR 的工作方式与 WCF 双向通道类似,其中服务器端通过主要服务合同访问,服务器到客户端的通信通过回调合同进行。虽然 WCF 双向通道提供与 SignalR 相同的场景支持,但双向通道实现目前在任何 Xamarin 目标平台上都不受支持。另一方面,所有 Xamarin 目标平台都可用 SignalR 组件。

SignalR 组件
SignalR 利用 WebSockets,这使得在 HTTP 传输上实现双向通信。本质上,WebSockets 几乎以与 TCP Sockets 相同的方式工作;然而,连接是在 HTTP 传输层上建立的。
使用 SignalR,需要实时数据的应用程序可以实现,无需求助于轮询或监听通道实现,这在移动平台上既不可扩展也不高效。
SignalR 通常在服务器端实现为一个 Hub 应用程序,它创建不同的事件接收器供不同的应用程序订阅。每个订阅特定频道的客户端在这些频道上以字符串格式或已反序列化为复杂类型的方式接收事件通知和数据,在正常广播场景下。
// Connect to the server
var hubConnection = new HubConnection("http://xamarin.traveltrace.com/");
// Create a proxy to the 'MainHub' on the SignalR server
var myHubProxy = hubConnection.CreateHubProxy("MainHub");
// Subscribe to message from the server
myHubProxy.On<string>("ServerStringCall", message =>
{
    // TODO: use the message update from the channel
});
// Subscribe to message with a complex type
myHubProxy.On<Region>("ServerComplexCall", message =>
{
    // TODO: use the message update from the channel
});
// Start the connection
await hubConnection.Start();
一般而言,SignalR 服务器实现可以替换 RESTful 服务操作。这些双向 Hub 可以提供由消费者调用的功能,以及从服务器到监听客户端的更新调用。
虽然可以使用不同的消息格式来交换数据,但大多数实现使用 JSON 格式来序列化和反序列化数据,并且 Json.NET 是 SignalR 组件使用的默认序列化库。
await myHubProxy.Invoke("MySimpleServerMethod", "myParameter");
await myHubProxy.Invoke<Region>("MyComplexServerMethod", new Region{Continent = Continent.Europe});
在服务器端调用的事件之上,SignalR 频道还提供生命周期事件:
- 
接收: 当连接上接收到任何数据时触发。提供接收到的数据。 
- 
连接缓慢: 当客户端检测到缓慢或频繁断开连接时触发。 
- 
重新连接: 当底层传输开始重新连接时触发。 
- 
重新连接: 当底层传输重新连接时触发。 
- 
状态改变: 当连接状态改变时触发。提供旧状态和新状态。 
- 
关闭: 当连接断开时触发。 
SignalR 支持使用 SSL 传输安全,并且能够与网络服务器和移动应用程序已经使用的现有身份验证和授权提供者集成。
模式和最佳实践
在移动应用程序中,开发者在开发项目中使用网络服务和其它通信渠道时,通常会使用某些可重用的设计模式。这些模式旨在提高效率,并增加代码在不同平台以及跨平台移动应用程序的各个执行域之间的共享。
异步转换
为 WCF 和/或 SOAP/XML 服务生成的代理通常包括基于事件的异步实现或带有 begin 和 end 方法的异步调用模式。这两种实现都可以转换为基于任务的异步模式。
为了将基于事件的异步服务方法转换为基于任务的,我们可以使用 TaskCompletionSource<T> 并返回产生的任务(参考第三章,异步编程)。
public Task<List<Region>> GetRegionsAsync(Region filter = null)
{
    var taskAwaiter = new TaskCompletionSource<List<Region>>();
    var client = CreateServiceClient();
    EventHandler<GetRegionsCompletedEventArgs> completedDelegate = null;
    completedDelegate = (sender, args) =>
        {
            if (args.Error != null)
            {
                taskAwaiter.SetException(args.Error);
            }
            taskAwaiter.SetResult(args.Result);
            client.GetRegionsCompleted -= completedDelegate;
        };
    client.GetRegionsCompleted += completedDelegate;
    client.GetRegionsAsync(new Region { Continent = Continent.Europe });
    return taskAwaiter.Task;
}
对于异步调用模式,我们可以使用 TaskFactory 的指定方法。TaskFactory 的 FromAsync 方法使用 begin 和 end 方法以及异步状态对象(例如,可以用于取消令牌或进度回调)来创建一个可等待的任务。
public Task<List<Region>> GetRegionsAsync(Region filter = null)
{
    var client = (ReferenceService.ReferenceService)CreateServiceClient();
    var task = Task<List<Region>>.Factory
        .FromAsync(
        (callback, o) => client.BeginGetRegions(filter, callback, o),
        result => client.EndGetRegions(result),
        null);
    return task;
}
数据模型抽象
遵循之前提出的质量标识符,在服务相关场景中,创建一个可以被跨平台应用程序的不同分支使用的数据库模型抽象层非常重要。
使用前几节中的旅行者指南应用程序示例,我们可以分析共享策略。在这个例子中,作为一个开发团队或单个开发者,我们负责:
- 
实现负责访问数据库和连接外部 API 的服务层,如果需要的话 
- 
实现将被 Xamarin 应用程序使用的共享通用逻辑 
- 
实现 Xamarin.iOS 和 Xamarin.Android 应用程序 
- 
实现 Windows Phone 8.1 应用程序 
- 
实现将使用 Silverlight 组件(可选)的 Web 界面 
为了简单起见,我们将只实现一个数据类型和一个 GET 方法。
对于合约和数据对象,我们可以创建一个针对 Xamarin 平台以及 .NET 4.5 的可移植库。我们包括 .NET 配置文件的原因是因为我们将在服务层实现中使用数据模型。
实现开始于创建数据传输模型对象。这些对象通常是服务层上使用的数据库表的反映。然而,DTO(数据传输对象)和 DBO(数据访问对象,Entity Framework 项目)之间的一对一映射并不是绝对必要的,因为 DTO 抽象层的唯一目的是在我们将要处理的实际数据存储之上创建一个抽象层。
public class Region
{
    [JsonProperty("id")]
    public Guid Id { get; set; }
    [JsonProperty("name")]
    public string Name { get; set; }
    [JsonProperty("continent")]
    public Continent Continent { get; set; }
}
注意
注意,我们包括 Json.NET 属性来定义类属性。它们用于在序列化/反序列化期间格式化 JSON 对象属性为驼峰式(例如,camelCase),这是 JavaScript 约定,而不是属性名称的.NET 约定大驼峰式(例如,PascalCase)。这些属性定义可以与 RESTful 客户端和 Web 服务实现一起使用。这不会干扰其他服务或客户端层用例。
在我们创建模型之后,我们可以定义将被 Web 服务和相关客户端使用的接口。我们将在服务层定义两个接口用于同步实现,并在客户端侧进行异步消费。
namespace Xamarin.Master.TravelTrace.Common.Infrastructure
{
    public interface IReferenceService
    {
        List<Region> GetRegions(Region filter = null);
        List<Country> GetCountries(Country filter = null);
        List<City> GetCities(City filter = null);
    }
    public interface IReferenceServiceAsync
    {
        Task<List<Region>> GetRegionsAsync(Region filter = null);
        Task<List<Country>> GetCountriesAsync(Country filter = null);
        Task<List<City>> GetCitiesAsync(City filter = null);
    }
} 
服务实现策略通常是使用 RESTful 层。为了演示目的,让我们在一个单独的项目中实现 WCF 服务,重用之前定义的数据模型和接口。

解决方案结构
在这个实现中,每个服务方法都将调用数据存储库(Entity Framework/MSSQL),并且存储库将通过转换数据库层实体来返回 DTO 对象。
我们需要实现的项目下一个部分是服务数据消费者层。我们将为此层创建一个新的可移植库,并使用生成的 WCF 客户端。在创建项目并添加对System.ServiceModel命名空间和包含 DTO 模型的通用可移植库的引用后,一个重要的细节是要确保生成的代理重用引用的库。

服务引用属性
注意
如果你使用 Silverlight SDK 生成客户端,包含现有库以重用类型会稍微复杂一些。为了做到这一点,你可以使用“引用”开关(或者简单地说,/r:)并将实用程序指向包含已实现类型的程序集。
slsvcutil http://localhost/ReferenceService.svc 
 /d:c:\bin\ /r:C:\Local\Xamarin.Master.TravelTrace.Common.dll
在创建代理之后,我们有一个结构,其中数据模型和合约被应用程序的不同层共享,包括服务、数据访问层、服务代理,最后是应用程序。

共享服务结构
然而,实现应该进一步扩展,将服务代理转换为基于任务的异步实现。另一个有用的改进是实现本地数据库缓存和离线存储。对于这个缓存层,可以重用相同的 DTO 实现。
如果我们要在这个跨平台项目中包含一个 Windows Phone 8.1 客户端,解决 WCF 基础设施不足的唯一方法就是用 RESTful 实现来替换 WCF 服务。
服务缓存
在处理网络场景时,重要的是要记住,移动设备并不总是拥有良好的网络连接或根本就没有网络。为了使 Xamarin 连接应用程序即使在离线场景中也可用,可以实现一个缓存层来存储和返回不经常更改的数据项。
例如,在旅行指南应用程序中,用户将想要访问指南,甚至可能需要地图,即使他们处于漫游连接中,或者更糟糕的是,没有任何连接。为了便于离线存储,我们可以实现一个 SQLite 数据库,该数据库使用现有的数据传输对象作为存储项,并在有互联网连接时在特定间隔更新数据。
实现的第一步将是修订我们的 DTO 层类,并在需要时添加 SQLite 属性。这将创建对服务层 SQLite 程序集的依赖;另一种选择是使用服务层和客户端库之间的链接代码文件,或者为 SQLite 数据存储重新创建 DTO 对象。
public class Region
{
    public Region()
    {
        Countries = new List<Country>();
    }
    [PrimaryKey]
    [JsonProperty("id")]
    public Guid Id { get; set; }
    [JsonProperty("name")]
    public string Name { get; set; }
    [JsonProperty("continent")]
    public Continent Continent { get; set; }
    [OneToMany(CascadeOperations = CascadeOperation.CascadeInsert | CascadeOperation.CascadeRead)]
    [JsonProperty("countries")]
    public List<Country> Countries { get; set; } 
}
在此场景中,为了创建一个数据上下文,如果可用则使用在线存储,如果互联网连接有限则使用本地数据存储,我们可以实现与之前示例中为服务代理创建的相同数据接口,并为数据同步上下文创建一个父处理程序。

应用层的数据抽象
在同步上下文中,对于 GET 方法,服务调用将仅用于更新本地存储,实际结果将从本地存储返回。对于 PATCH、POST 和 PUT 调用,根据在线连接性,我们将要么在本地保存数据,要么将增量和新对象实例推送到服务,并使用更新来更新本地数据。
public class DataSyncContext : IReferenceServiceAsync
{
    public IReferenceServiceAsync LocalDataService { get; set; }
    public IReferenceServiceAsync RemoteDataService { get; set; }
...
    public async Task<List<Region>> GetRegionsAsync(Region filter = null)
    {
        try
        {
            // Getting the online results
            var results = await RemoteDataService.GetRegionsAsync(filter);
            // If there were any online changes.
            SyncToLocal(results);
        }
        catch (Exception ex)
        {
            // TODO:
        }
        // Returning the local storage results (with or without updates)
        return await LocalDataService.GetRegionsAsync(filter);
    }
...
}
为了提高此实现的性能,当我们为某些可视化加载数据时,我们可以首先调用本地数据提供程序,然后继续进行 UI 更新,接着调用 Web 服务方法以及相同的延续委托。
Action<List<Region>> onRegionsLoaded = regions =>
{
    // Update the view-model data or the UI.
};
DataContext.LocalDataService.GetRegionsAsync()
    .ContinueWith((task) =>
    {
        onRegionsLoaded(task.Result);
    });
DataContext.GetRegionsAsync()
    .ContinueWith((task) =>
    {
        onRegionsLoaded(task.Result);
    });
平台特定概念
在 Xamarin 平台上,还有其他由原生运行时提供并由 Xamarin 支持的概念和网络通信方法。
权限
为了让 Android 或 Windows Phone 应用程序访问互联网,应用程序清单应该声明该应用程序将需要使用网络来访问资源。
在 Android 系统中,权限声明使用 XML 文件的清单节点中的 uses-permission 标签:
<uses-permission android:name="android.permission.INTERNET" />
虽然这种声明在大多数使用场景中足够使用,但为了访问当前的网络状态或 Wi-Fi 状态,还必须声明网络状态权限:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
对于 Windows Phone,可声明的应用程序能力为 ID_CAP_NETWORKING。
两个平台的应用程序清单都可以通过应用程序项目属性中的指定配置部分进行编辑。

Android Manifest
除了之前提到的 App Transport Security (ATS)之外,iOS 不强制执行任何清单设置或权限,以允许应用程序使用网络连接。
NSUrlConnection/NSUrlSession(仅限 iOS)
除了可用于 Xamarin 目标平台的不同的客户端库之外,还可以使用一些原生实现来调用和接收外部网络数据。Xamarin.iOS 平台上的一个可用选项是NSUrlConnection。借助NSUrlConnection,开发者可以发出网络请求并使用响应。
一个简单的网络请求,用于从之前在 iOS 上演示的静态数据端点检索数据,看起来可能如下所示:
public Task<List<Region>>  GetRegionsAsync(Region filter = null)
{
    var nsUrlRequest = new NSUrlRequest(new NSUrl(myServiceEndpoint));
    var taskSource = new TaskCompletionSource<List<Region>>();
    var nsUrlConnection = new NSUrlConnection(nsUrlRequest,
        new ConnectionSerializingHandler<List<Region>>(taskSource));
    nsUrlConnection.Start();
    return taskSource.Task;
}
连接委托的实现将涉及数据的反序列化并将结果分配给TaskCompletionSource,以便完成方法执行。
public class ConnectionSerializingHandler<T> : 
        NSUrlConnectionDataDelegate where T:class,new()
{
    private StringBuilder m_ResponseStore;
    private TaskCompletionSource<T> m_TaskCompletion; 
    public bool IsFinishedLoading { get; set; }
    public string ResponseContent { get; set; }
    public ConnectionSerializingHandler(TaskCompletionSource<T> taskCompletionSource)
        : base()
    {
        m_ResponseStore = new StringBuilder();
        m_TaskCompletion = taskCompletionSource;
    }
    public override void ReceivedData(NSUrlConnection connection, NSData data)
    {
        if (data != null)
        {
            m_ResponseStore.Append(data);
        }
    }
    public override void FinishedLoading(NSUrlConnection connection)
    {
        IsFinishedLoading = true;
        ResponseContent = m_ResponseStore.ToString();
        // TODO: implement deserialization and 
        m_TaskCompletion.SetResult(result);
    }
}
尽管此实现在 iOS 平台上是可能的,但考虑到将 Mono 传递到 iOS 桥接器(同样适用于 Android 和 JNC 桥接器)的成本,应避免此类实现,并且应仅使用原生或 Mono 运行时代码通过网络进行通信。
以类似的方式,我们可以实现 iOS 中新的NSUrlSession类的使用场景。然而,NSUrlSession也可以用于后台下载场景。因此,我们将在下一节中讨论它。
背景下载
当应用程序需要的网络资源比客户端 UI 可以等待的更多时,在 Xamarin 移动应用程序中,我们可以求助于后台下载。iOS 和 Android 平台都提供了后台下载的实现,这些策略可以在 Xamarin 运行时执行。
对于 Xamarin.Android 应用程序开发者来说,执行后台下载的最简单方法是从 API 级别 9 开始提供的下载管理器 API 服务/应用程序。下载管理器可以用请求初始化,并且应用程序可以订阅有关下载状态的事件通知。
首先,我们需要创建一个请求并将其传递给DownloadManager:
global::Android.Net.Uri downloadUri = global::Android.Net.Uri.Parse("<URL to Download>");
DownloadManager.Request request = new DownloadManager.Request(downloadUri);
// Types of networks on which this download will be executed.
request.SetAllowedNetworkTypes(DownloadNetwork.Wifi);
// Allowed on Roaming connection?
request.SetAllowedOverRoaming(false);
// Allowed on Metered Connection?
request.SetAllowedOverMetered(false);
//Set the title of this downloaded
request.SetTitle("My Background Download");
//Set the description of this downloaded
request.SetDescription("Xamarin.Android download using DownloadManager");
//Set the local destination for the downloaded file
request.SetDestinationInExternalFilesDir(this, global::Android.OS.Environment.DirectoryDownloads, "MyDownloadedData.xml");
// or use the request.SetDestinationUri()
一旦请求准备就绪可以执行,我们可以获取DownloadManager实例并将下载请求排队:
m_DownloadManager = (DownloadManager)GetSystemService(DownloadService);
// Enqueue the request
// The download reference will be used to retrieve the status
m_CurrentDownloadReference = m_DownloadManager.Enqueue(request);
可以使用下载引用来获取排队下载的当前状态信息或取消正在进行的后台下载。
要获取下载的当前状态或取消它,我们可以使用DownloadManager实例上的相应方法。
// Removing the queued request from the DownloadManager queue.
m_DownloadManager.Remove(m_CurrentDownloadReference);
//
// Retrieving the current status of the download queue
// Create a query to retrieve the download status(s)
DownloadManager.Query myDownloadQuery = new DownloadManager.Query();
myDownloadQuery.SetFilterById(m_CurrentDownloadReference);
// Request the queued download items as a data table.
var cursor = m_DownloadManager.InvokeQuery(myDownloadQuery);
var statusColumn = cursor.GetColumnIndex(DownloadManager.ColumnStatus);
var status = (DownloadStatus)cursor.GetInt(statusColumn);
此实现可以通过使用BroadcastReceiver类从DownloadManager应用程序接收到的通知(复数)进行扩展。
public class DownloadBroadcastReceiver : BroadcastReceiver
{
    public override void OnReceive(Context context, Intent intent)
    {
        // Get the download reference from the intent broadcast
        long referenceId = intent.GetLongExtra(DownloadManager.ExtraDownloadId, -1);
        // TODO: Implement the delegated execution
    }
}
我们现在可以将广播接收器注册到DownloadManager实例,并使用可能的委托实现来更新 UI。
//set filter to only when download is complete and register broadcast receiver
IntentFilter filter = new IntentFilter(DownloadManager.ActionDownloadComplete);
// TODO: We can extend the DownloadBroadcastReceiver with delegates
RegisterReceiver(new DownloadBroadcastReceiver(), filter);
在广播机制之上,下载管理器应用 UI 也可以在 Xamarin 应用程序中调用,以提供关于正在进行或已完成传输的统一 UI。
在 iOS 平台(至少是 iOS 7 之后),后台传输(包括下载和上传操作)可以通过 NSUrlSession 实现。NSUrlSession 提供了一个易于实现的接口,允许开发者创建高效且可靠的传输过程。
NSUrlSession 的实现策略最初涉及实现一个 NSUrlSessionDelegate,它将成为传输过程的负责“处理程序”。与传输的健康和状态相关的基方法通过此代理公开,可以实施以提供传输所需的信息或向应用程序用户提供实时更新。
- 
当后台会话完成时,会调用 DidFinishEventsForBackgroundSession。
- 
当服务器请求凭证时,会调用 DidReceiveChallenge。
- 
当会话出现问题时,会调用 DidBecomeInvalid。
NSUrlSessionDelegate 为更专业的传输代理提供了基实现:NSUrlSessionDownloadDelegate 用于下载操作,NSUrlSessionTaskDelegate 用于上传操作。这些代理类公开了与传输任务相关的附加状态方法(例如,下载代理提供了检索下载进度通知的方法)。
例如,如果我们使用与 Xamarin.Android 中相同的示例,并且实现 BroadcastReceiver,则 NSUrlSessionDownloadDelegate 的实现需要三个基本方法来完成、错误和进度。
public class DownloadTaskDelegate : NSUrlSessionDownloadDelegate
{
    public override void DidFinishDownloading(NSUrlSession session, NSUrlSessionDownloadTask downloadTask, NSUrl location)
    {
        // TODO: Implement the delegate for download finished
    }
    public override void DidBecomeInvalid(NSUrlSession session, NSError error)
    {
        //base.DidBecomeInvalid(session, error);
        // TODO: Implement the delegate for error
    }
    public override void DidWriteData(NSUrlSession session, NSUrlSessionDownloadTask downloadTask, long bytesWritten, long totalBytesWritten,
        long totalBytesExpectedToWrite)
    {
        //base.DidWriteData(session, downloadTask, bytesWritten, 
        //  totalBytesWritten, totalBytesExpectedToWrite);
        // TODO: Implement the delegate for download progress
    }
} 
在代表实现完成后,我们可以使用 NSUrlSession 创建会话并开始下载操作。
NSUrlSessionConfiguration downloadSessionConfiguration = NSUrlSessionConfiguration.BackgroundSessionConfiguration ("com.TravelTravel.BackgroundTransfer");
m_DownloadSession = NSUrlSession
    .FromConfiguration(downloadSessionConfiguration, 
    new DownloadTaskDelegate(), 
    new NSOperationQueue());
NSUrl url = NSUrl.FromString("<URL to Download>");
NSUrlRequest request = NSUrlRequest.FromUrl(url);
m_DownloadTask = m_DownloadSession.CreateDownloadTask(request);
在处理程序实现的基础上,iOS 应用可以被唤醒以执行某些代码,例如本地移动通知,通知用户会话已完成。对于任务完成事件,需要使用 iOS 应用程序代理(参考第二章,内存管理),以 DidFinishEventsForBackgroundSession。
移动通知(也称为远程场景中的推送通知)是在操作系统级别执行的用户通知,用于通知用户有关应用程序相关的更新。它们可以通过本地触发或使用远程服务器触发。
推送通知
推送通知是微妙的 UI 消息,可以帮助应用程序向用户提供有关服务层正在执行的非同步任务的信息,或者有关与应用程序实例本身相关的外部事件(例如,来自社交网络的消息、旅行预订的批准等)。
在 Xamarin 平台和 Windows Phone 上都可以创建和接收推送通知。这些通知由一个二级服务器/应用程序(例如,服务层)触发,由平台相应的消息基础设施提供商进行代理,并在目标客户端的应用程序中显示。对于 Android 平台,消息提供者是 Google Cloud Messaging (GCM),而对于 iOS,则是 Apple Notification Push Service (APNS)。这两个服务提供商都要求您的应用程序注册以接收推送通知,并且服务器应用程序需要有凭证以便能够与通知服务进行身份验证。同样,Windows Notification Services (WNS) 采用联合身份验证机制。
GCM 和 APNS 都使用订阅模型,其中特定设备上的客户端应用程序订阅/注册推送通知,并创建一个地址令牌。该地址令牌随后被服务器用于向消息代理服务(例如,GCM)发送推送通知,并将队列中的消息递送到特定的客户端。

推送通知
在经典的短信模型之上,GCM 还支持基于主题和基于群组的消息,接收者不限于单一设备/应用程序对。使用 GCM 还可以创建一个双向通道,客户端能够将消息发送回服务器层。
在这些平台上,推送通知可以用来触发各种任务,其中最常见的是导航到某个视图并继续由通知初始化的业务流程。
虽然在客户端订阅推送通知相对简单,但在跨平台场景中,需要复杂的实现来引入一个单一的服务器环境,为 GCM 和 APNS 提供消息。然而,这两个平台都有平台无关的实现。Microsoft Azure 平台和通知中心就是这些解决方案之一,其中与 GCM 和 APNS 的通信都是通过使用相同的业务逻辑实现来支持的。
云集成
尽管有多个云服务提供商作为移动应用程序后端开发平台,但考虑到其与 .NET 平台以及随后的 Xamarin 的固有自然联系,Microsoft Azure 在竞争对手中脱颖而出。Azure 支持的大多数功能都有针对 Xamarin 目标平台的特定实现。
Azure 移动服务
Azure 移动服务是一个可扩展的云开发平台,它帮助开发者轻松地将功能添加到他们的移动应用程序中。本章中描述的与网络服务相关的模式和功能,如 OData 服务、离线数据存储、推送通知和 OAuth 认证提供者,已经包含在移动服务 SDK 中,并且可以通过 Azure 管理控制台进行配置。
为了演示上述功能,我们可以将它们集成到我们的演示应用程序中。
首步是在 Azure 管理控制台中创建一个移动服务。为此,我们将选择一个计算服务并创建移动服务。

创建计算服务
然后,我们将设置移动服务端点并创建 SQL 数据库以存储在线数据。

移动服务设置
一旦设置完成,就可以下载“个性化”的服务层项目,以便将移动服务集成到应用程序项目中。

将移动服务连接到现有的 Xamarin 应用
在服务层项目中,您会注意到只创建了一个控制器以方便您使用。我们将通过添加额外的控制器并将对自定义 DTO 数据模型的引用添加到我们从 Azure 门户下载的新服务项目中来扩展项目。之所以添加引用的文件,是因为服务项目中的数据对象必须从 EntityData 类派生。我们需要进行的另一个更改是将类定义转换为部分并移除 SQLite 引用,例如,您可以注释掉 SQLite 属性描述符或使用条件编译。
在此示例中,我们使用 AZURE 作为 Azure 服务的构建常量。
public partial class Region
{
    public Region()
    {
        Countries = new List<Country>();
    }
#if !AZURE
    [PrimaryKey]
    [JsonProperty("id")]
    public Guid Id { get; set; }
#endif
#if !AZURE
    [JsonProperty("name")]
#endif
    public string Name { get; set; }
#if !AZURE
    [JsonProperty("continent")]
#endif
    public Continent Continent { get; set; }
#if !AZURE
    [OneToMany(CascadeOperations = CascadeOperation.CascadeInsert | CascadeOperation.CascadeRead)]
    [JsonProperty("countries")]
#endif
    public List<Country> Countries { get; set; } 
}
最后,使用 partial 声明为 Region 类创建数据对象定义:
public partial class Region : EntityData
{
}
在此步骤之后,您可以直接使用现有的控制器项目模板来添加专门的数据端点(Microsoft Azure 移动服务表控制器)。

Microsoft Azure 移动服务表控制器
这将为数据对象创建一个控制器并将类型插入到数据上下文中。
一旦项目发布并且移动服务正在运行,SQL 数据库表将自动迁移。此迁移也适用于数据表列更改或 DTO 模型未来的添加。
现在,我们可以在客户端应用程序中添加 NuGet 包或组件,并添加必要的初始化代码,如 Azure 管理控制台移动服务部分起始页面所述。
在主活动中,我们创建了以下移动服务实例:
public static MobileServiceClient MobileService = new MobileServiceClient(
    "https://traveltrace.azure-mobile.net/",
    "<Removed for security reasons>"
    );
将以下内容添加到事件处理程序或 OnCreate 函数中:
// Intialization the mobile services on the mobile platform
CurrentPlatform.Init();
// Adding a region item to the database
var item = new Region {Continent = Continent.Europe, Name = "Balkan"};
MobileService.GetTable<Region>().InsertAsync(item).ContinueWith((result) =>
{
    System.Diagnostics.Debug.Write(result.Status);
});
代码成功执行后,可以使用 SQL Management Studio 或 Visual Studio SQL Server 工具观察 Azure 数据库中的数据。

Azure 数据示例
现在我们已经有一个可以与之通信的工作服务层和客户端,我们可以看看本地同步。
Azure 离线数据
对于本地数据缓存和离线场景,Azure Mobile Services SDK 已经实现了一个同步框架,其中本地数据存储在 SQLite 数据库中,同步由拉取和推送命令(推送请求将本地更改上传到云存储,而拉取请求从服务器下载最新更改)处理,使用默认的冲突处理器。每个拉取请求都会自动发出一个推送请求,其中本地数据被推送到云存储。冲突根据创建和更新的字段解决,这些字段是使用 EntityData 基类定义的每个对象类型的成员。
注意
在开始实现之前,我们需要下载并安装 Azure Mobile Services SQLiteStore NuGet 包。
为了初始化默认的本地数据存储,我们将使用 MobileServicesSQLiteStore 实现。可以通过 IMobileServiceLocalStore 接口集成自定义本地存储实现。
private async Task InitLocalStoreAsync()
{
    // new code to initialize the SQLite store
    string path = Path.Combine(
        Environment.GetFolderPath(
        Environment.SpecialFolder.Personal), "traveltrace.db");
    if (!File.Exists(path))
    {
        File.Create(path).Dispose();
    }
    var store = new MobileServiceSQLiteStore(path);
    store.DefineTable<Region>();
    // Uses the default conflict handler, which fails on conflict
    await MobileService.SyncContext.InitializeAsync(store);
}
在本地存储初始化和同步上下文创建之后,我们可以实现每次应用程序启动时都可以调用的同步方法。
private async Task SyncAsync()
{
    // IMobileServiceSyncTable<Region> RegionsTable = MobileService.GetSyncTable<Region>();
    await MobileService.SyncContext.PushAsync();
    await RegionsTable.PullAsync("AllRegions", RegionsTable.CreateQuery());
}
PushAsync 和 PullAsync 方法还接受过滤表达式,因此可以限制同步到某些实体。
在此实现中,一旦同步上下文就绪,如果服务连接不可用,IMobileServiceSyncTable<T> 接口实现将处理离线数据,并且数据将保存在本地存储中,直到下一次推送操作。
Azure 身份验证
Azure 平台为 Xamarin 移动应用程序提供各种身份验证机制。每种身份验证机制都可以通过 NuGet 包和/或组件集成到现有的具有服务后端的移动应用程序中。
作为多租户、基于云的目录和身份管理服务,Azure Active Directory (Azure AD) 为应用程序开发者提供了一种简单的方法,在大量云 SaaS 应用程序上创建单点登录体验。还可以将现有的 Windows Server Active Directory 集成到应用程序中,并利用现有的本地身份存储。这些功能使 Azure AD 成为 LOB 应用程序的理想候选者。
Azure 移动服务的另一种身份验证策略是配置现有的身份验证提供者,如 Facebook、Google、Twitter 或 Microsoft,并使用 Azure 移动 SDK 来保护服务请求。为了注册身份验证提供者,第一步是在目标平台上创建一个消费者应用程序。
例如,如果我们要在我们的身份验证场景中使用 Live ID,我们需要使用 Live Connect 应用程序管理网站(account.live.com/developers/applications/index)。同样,对于 Twitter,我们需要在 Twitter 应用程序管理控制台(apps.twitter.com/)上创建一个 Twitter 消费者应用程序。

Live Connect 应用程序管理网站
一旦设置好应用程序,就可以使用 Azure 管理控制台来更新移动服务配置。

移动服务身份验证配置
在设置好移动服务的身份提供者之后,只需通过添加 Authorize 属性,就可以轻松保护 Web 服务项目。
[AuthorizeLevel(AuthorizationLevel.User)]
public class RegionController : TableController<Region>
在客户端应用程序中,通过在 Azure 移动服务 SDK 客户端上使用正确的身份验证提供者的LoginAsync方法,可以简单地处理身份验证。
MobileService.LoginAsync(this, MobileServiceAuthenticationProvider.MicrosoftAccount).ContinueWith((task) =>
{
    System.Diagnostics.Debug.WriteLine("Currently authenticated user's ID is {0}", task.Result.UserId);
});
结果是使用 Xamarin.Auth 组件接收到的相同的身份验证屏幕。

代理身份验证
Azure 云集成场景远不止这里所描述的。这个基于云的开发平台所包含的功能可以帮助开发者轻松且可扩展地增强他们的 Xamarin 应用程序。
摘要
本章概述了在 Xamarin 应用程序中创建连接应用程序时可以使用的各种网络通道。
由于现有 Web 服务协议的互操作性(无论是 SOAP/XML 还是 REST/JSON),Web 服务无疑是现代移动应用程序的基本列表之一。不幸的是,由于 Windows Communication Foundation 客户端基础设施不包括在 Windows Phone 运行时中,XML 服务与 Windows Phone 8.1 运行时的集成稍微困难一些(尽管它们仍然由 Windows Phone Silverlight 运行时支持)。然而,相同的 RESTful 服务代理可以在每个 Xamarin 目标平台和 Windows Phone 上使用。
讨论了如移动服务和 Azure Active Directory 之类的云集成选项,并提供了演示示例。这些技术中的每一个都为 Xamarin 移动应用程序提供了额外的连接性和集成机会。SignalR 是另一种网络技术,通过客户端应用程序和服务器之间的双向通信,为移动应用程序提供了额外的通信能力。
使用 TravelTrace 应用程序范围演示了几个常见的服务和 Web 实现模式,这些模式将在本书剩余部分的各种场景中使用。每个模式最初都针对不同的质量标识符。
最后,我们讨论了一些特定平台的网络选项。
第六章。平台扩展
本章专注于特定平台的 API 和功能。它解释了可以在 Xamarin 应用程序中使用的某些外设。我们还将讨论原生库以及如何在跨平台 Xamarin 应用程序中包含它们。以下主题将被讨论:
- 
内容共享 
- 
外设 
- 
位置数据 
- 
原生库 
内容共享
每个 Xamarin 目标平台都实现了一种在应用程序之间共享格式化内容的策略。共享实现通过允许用户在任何其他应用程序中打开来自您的应用程序的文件来提高您应用程序的可见性。此外,这些类型的实现从原生角度为您的跨平台项目提供了额外的价值。
应用程序间的共享是通过底层运行时作为共享源和目标应用程序之间的经纪人来实现的。在 iOS 和 Windows Store 应用程序中,共享以抽象文件元素的形式进行。然而,Android 应用程序可以更进一步,通过共享可以被接收应用程序操作格式化数据来共享,这本质上允许源应用程序几乎充当数据存储库。
注意
在 Windows Store 中,应用程序可以主动共享内容,如媒体元素、URI、文本内容和其他类型的数据。然而,在这个实现策略中,即共享实现策略中,源应用程序必须启动共享过程。本书中描述的内容共享场景是关于目标应用程序通过源应用程序访问内容。
在 Windows Runtime 中,应用程序通过所谓的应用程序合约的用法相互交互或与操作系统交互。借助合约,应用程序可以沉浸到运行时中,并更接近成为运行时的一部分。
通过在 Android 上实现基本 ContentProviders 和在 iOS 平台上实现文档提供程序扩展,实现了相同的功能。
文件选择器和合约(Windows Store 应用程序)
Windows Runtime 中最常用的合约之一是文件打开选择器合约。在这个合约实现中,源应用程序必须实现当它被调用以为目标应用程序提供文件内容时的激活策略。当目标应用程序需要某种类型的文件时,运行时会列出所有可能声明这种类型在其应用程序清单中的源应用程序(例如,在 Windows Phone 上,当您想在邮件客户端中附加文档并附带一张图片时,OneDrive 应用程序会显示为可能来源之一)。
用户随后选择他们想要在当前应用程序中使用的文件,提供程序应用程序负责为目标应用程序创建或提供文件。

Windows Runtime 中的文件选择器合约
在这种方法中,文件不一定是实际的文档项,但它可以是概念性的。例如,如果我们要在 TravelTrace 应用程序中实现文件打开选择器,我们不需要在文件打开选择器中使用实际的文档来提供内容。共享的内容项可以是用户跟踪的之前的旅行,所选的旅行可以提供一个生成的剪贴簿或图像的拼贴,根据消费者应用程序请求的文档类型,可以是图像格式或 PDF 文档。
文档提供者扩展(iOS)
文档提供者扩展(在 iOS 8 中引入)允许应用程序(即消费应用程序)访问其应用程序沙盒之外的文档。文档提供者扩展有两方面。文档选择器视图控制器扩展为操作系统提供 UI 实现,以便在源应用程序被选为文档选择器视图中的文档源时显示。然而,文件提供者扩展负责提供文档级别的操作。
为了创建一个提供者扩展,我们可以在 Xamarin Studio 中使用现有的项目模板。

文档选择器扩展项目模板
一旦创建项目,我们就负责在故事板中创建视图并实现DocumentPickerViewController,以便当我们的应用程序被选中提供文件时,可用的文件在 UI 上列出。DocumentPickerViewController最初有两个需要我们注意的方法。PrepareForPresentation方法接收选择器模式(Import、Open、ExportToService或MoveToService),以便用户界面可以根据请求的操作进行准备。OpenDocument方法仅为了我们的方便而实现,以证明一旦用户选择了一个文档,我们应该准备相应的文件 URL,并使用DismissGrantingAccess方法将其传递给运行时。
需要记住的是,我们文档选择器扩展提供的 URL 应该已经指向一个实际文件,或者我们应该继续实现文档文件提供者扩展,该扩展将在消费应用程序显示文档选择器并用户选择文件,或者消费应用程序直接使用缓存的 URL 打开文件时提供文件。
在文档文件提供者扩展项目中,关键实现位于StartProvidingItemAtUrl方法。此方法使用提供的FileCoordinator类在目标 URL 处创建文件(例如,生成文件或从远程位置下载)。
public override void StartProvidingItemAtUrl (NSUrl url, Action<NSError> completionHandler)
{
  NSError error, fileError = null;
  NSData fileData;
  // TODO: get the file data for file at <url> from model
  fileData = new NSData ();
  FileCoordinator.CoordinateWrite (url, 0, out error, (newUrl) => fileData.Save (newUrl, 0, out fileError));
  if (error != null)
    completionHandler (error);
  else
    completionHandler (fileError);
}
扩展实现完成后,我们必须准备项目元数据条目。每个项目(包括扩展和容器应用程序)都需要使用App Groups功能。这个功能需要在Entitlements选项列表中设置。其他设置包括基本文档存储 URL、文档选择器支持的操作类型等。然而,这些配置值是在Info.plist选项列表中插入的。

文档提供者扩展的权限
为了将扩展添加到包含的应用程序中,我们只需要将它们作为引用添加到主项目中。如果你查看主项目文件,你会注意到引用是通过将IsExtension标志设置为 true 来添加的。
ContentProvider 和 ContentResolver(Android)
安卓平台上的内容提供者充当数据存储库。这些存储库通过结构化端点描述(类似于网络服务上的 REST 端点)暴露给消费应用程序。使用提供的元数据,消费应用程序通过实现ContentResolvers来解析提供者的内容。使用内容提供者,应用程序可以公开已知的数据类型,如联系人列表照片或日历事件,以及自定义数据类型和格式化数据。
在此基础设施的消费端,Android 运行时已经默认实现了许多内容提供者,例如Contacts、MediaStore、UserDictionary等。这些提供者可以通过实现基类如ContentResolver和CursorAdapter来访问。CursorAdapter用于将ContentResolver检索到的数据馈送到 UI 列表视图控件。ContentProvider API 操作可能涉及列表查询和对单个记录的 CRUD 操作。
提供者应用程序负责注册一个对应用程序唯一的权限。权限条目可以描述为特定应用程序的基本内容 URI。它既可以添加到清单文件中,也可以在实现ContentProvider的类上使用属性条目。
[ContentProvider(new string[] { "com.xamarin.master.traveltrace.TripProvider" })]
public class TripDataProvider : ContentProvider
内容提供者还需要提供另一项重要的元数据,即 Mime-Type 信息。为了便于在消费应用程序中使用CursorAdapter,内容提供者需要为项目列表(以vnd.android.cursor.dir开头)以及单个项目(以vnd.android.cursor.item开头)提供 Mime-Type。
最后,内容提供者需要公开其他应用程序可用的数据的数据列。这是通过从基本抽象类中隐藏InterfaceConstants嵌套类来实现的。
public new static class InterfaceConsts
{
    public const string Id = "Id";
    public const string Name = "Name";
    public const string Description = "Description";
    public const string Date = "Date";
    public const string Location = "Location";
    public const string ContentPath = "ContentPath";
}
另一种可选的实现方法是创建一个UriMatcher类,这可以简化查询方法的实现过程。
private UriMatcher GetUriMatcher()
{
    var matcher = new UriMatcher(UriMatcher.NoMatch);
    // to get data...
    matcher.AddURI(Authority, _basePath, (int) QueryType.List);
    matcher.AddURI(Authority, _basePath + "/#", (int) QueryType.Single);
    return matcher;
}
最终实现与查询、更新、插入和删除方法相关。这些方法中的每一个都需要根据定义的抽象类返回ICursor实现。
public override global::Android.Database.ICursor Query(global::Android.Net.Uri uri, string[] projection, string selection, string[] selectionArgs, string sortOrder)
{
    switch ((QueryType) m_UriMatcher.Match(uri))
    {
        case QueryType.List:
            // TODO:
        case QueryType.Single:
            // TODO:
        default:
            throw new Java.Lang.IllegalArgumentException("Unknown Uri: " + uri);
    }
}
总体而言,虽然 Android 提供了更多内容共享的灵活性,但它使得其他应用程序消费源应用程序提供的数据变得稍微困难一些。在 Xamarin.Android 应用程序上,内容提供程序实现提供的数据无法在没有专门实现的情况下被其他应用程序消费。
外设
在本节中,我们将讨论几个通信协议,这些协议使应用程序能够与其他平台和其他设备进行通信。
蓝牙
蓝牙通信协议已成为移动设备上的一个宝贵功能。特别是随着与物联网(IoT)相关的技术的兴起,以及我们日常生活中使用的各种配件,我们对移动平台上的蓝牙堆栈的依赖性有所增加。
虽然 Xamarin.Android 应用程序和 Windows Runtime 应用程序都可以使用 GATT(蓝牙低功耗)和 RFCOMM(蓝牙串行),但 iOS 应用程序只能通过蓝牙低功耗协议进行通信。这种差异的主要原因是 Android 和 Windows Runtime 根据共享规范实现串行通信端口。然而,苹果使用一个专有的通信堆栈,并使用加密系统。这不幸地限制了串行通信仅限于苹果生产或符合标准的配件或设备之间。
对于 Xamarin.Android,蓝牙 API 位于Android.Bluetooth命名空间中。使用提供的类,开发者可以增强他们的应用程序,添加如下功能:
- 
扫描可发现蓝牙设备(包括 LE 协议) 
- 
获取本地蓝牙适配器和配对设备的信息 
- 
使用 RFCOMM 协议创建串行通信套接字 
- 
同时作为 GATT 客户端或 GATT 服务器 
只有在用户权限清单条目中声明蓝牙的情况下,才能访问蓝牙协议。
<manifest ... >
  <uses-permission android:name="android.permission.BLUETOOTH" />
  ...
</manifest> 
在 Windows Runtime 上,蓝牙相关功能在Windows.Devices.Bluetooth命名空间中实现。与 Android 中的功能集类似,Windows Runtime 蓝牙堆栈要求应用程序在应用程序清单中声明适配器访问要求和要使用的协议(对于某些特定设备和协议,蓝牙能力声明必须手动插入到清单中)。该平台的一个重要特性是,可以通过后台任务促进并保持蓝牙连接,使设备能够在后台或挂起状态下继续其操作。
对于 Xamarin.iOS,蓝牙低功耗(BLE)相关的实现需要使用CoreBluetooth框架。
目前在 Xamarin 商店中,用于跨平台外围设备集成的关键组件是 Monkey.Robotics 项目。在实现蓝牙 LE 和 Wi-Fi 的基本 API 时,可以使用此组件与一些其他供应商特定的外围设备,例如健康监测设备和智能手表。
Wi-Fi Direct
Wi-Fi Direct 是另一种通信协议,允许 Wi-Fi 设备创建点对点(P2P)网络,并使用 Wi-Fi 适配器在不使用公共提供者网络连接的情况下交换信息。
在本书中描述的 Xamarin 目标平台中,只有 Android 平台支持此协议。Windows 10 平台将支持 Wi-Fi Direct;然而,此实现将仅针对基于 Windows 的设备。
在 Android 设备上,随着 Wi-Fi P2P 的引入,开发者可以创建能够以更高的速度和更长的距离进行通信的应用程序,这比蓝牙适配器要远得多。Wi-Fi P2P 功能是在 Android 4.0(API 级别 14)中引入的,并且符合 Wi-Fi 联盟的 Wi-Fi Direct 标准。
为了能够使用此功能,应用程序清单应包含ACCESS_WIFI_STATE、CHANGE_WIFI_STATE和INTERNET权限。
这些服务的访问是通过位于 Android.Net.Wifi.P2P 命名空间中的 WifiP2pManager 提供的。使用此管理器,应用程序可以广播、创建组、请求对等设备,并且开发者可以创建通过 Wi-Fi Direct 在 P2P 套接字上进行通信的应用程序。
近场通信
近场通信(NFC)协议为配对和广告场景(例如,NFC 标签)提供了一种易于使用的蓝牙替代方案。使用 NFC,可以在彼此靠近的移动设备之间创建套接字并传输数据。
不幸的是,NFC 协议是 iOS 设备上另一个不受支持的通信协议。(报告表明 iPhone 6 在技术上具有使用此协议的能力;然而,此 API 并未向开发者提供。)
然而,Windows Phone 和 Android 设备上的 NFC 堆栈实现了大多数相同的配置文件。本质上,在技术上可以在靠近的 Windows 和 Android 设备之间通过 NFC 进行通信(默认情况下,轻触发送功能作为一个跨平台功能工作)。尽管 Windows 设备使用专有消息方案(Windows:),但仍有第三方框架支持 NDEF。NDEF 是一个跨平台的消息方案,目前是 Android 的默认方案。
位置数据
现在,地理上下文(位置感知)对于应用程序变得越来越重要,尤其是运行在移动平台上的应用程序。例如,搜索引擎根据从客户端平台收集的位置信息优化结果,社交媒体和照片应用程序为帖子和中媒体项添加地理标签,还有许多其他关于应用程序运行的方式或平台,而是位置的数据用例。
在 Xamarin 平台上,位置信息是通过使用几个不同的来源提供的。其中最准确的是GPS(全球定位系统)。此选项消耗最多的电量,并且通常仅适用于前台应用程序。其他可以提供相对不太准确数据的选项是网络提供者,如 Wi-Fi 或蜂窝数据。iBeacon 是苹果公司引入的另一种技术,适用于 iOS 7+设备。iBeacon 兼容设备使用蓝牙 LE 协议传输位置信息,然后这种传输被手机和平板电脑上的蓝牙适配器使用。
在 Xamarin 目标平台上,可以通过系统事件和触发器主动访问位置信息。在每个平台上,对位置的访问都受到隐私设置的限制,并且始终由用户决定某个(或每个)应用程序是否可以访问位置服务。
Android 位置和 Google Play 服务
在 Android 运行时的早期版本中,android.location API 是框架指定的模块,用于向应用程序添加位置感知。然而,在 Google Play Services SDK(兼容 Android v2.2、API 级别 8 或更高版本)发布后,Google 提供的位置 API 成为在 Android 平台上访问位置数据的首选方式。
LocationManager,一个LocationServices实现,是一个全局服务,可以在 Xamarin.Android 应用程序中通过应用程序上下文访问。为了获取位置信息,应用程序必须通过ILocationListener的实现订阅位置更新。
m_LocationService = GetSystemService(LocationService) as LocationManager;
if (m_LocationService != null)
{
    if (m_LocationService.IsProviderEnabled(LocationManager.GpsProvider))
    {
        // Get updates in min every 5 seconds for every minimum 2m change
        m_LocationService.RequestLocationUpdates(LocationManager.GpsProvider, 5000, 2, m_LocationListener);
    }
    else if (m_LocationService.IsProviderEnabled(LocationManager.NetworkProvider))
    {
        // Get updates in min every 10 seconds for every minimum // 10m change
        m_LocationService.RequestLocationUpdates(LocationManager.NetworkProvider, 10000, 10, m_LocationListener);
    }
}
在位置监听器接口中,有几个事件可以被利用。除了位置变化信息外,开发者还提供了与不同位置提供者状态变化相关的更新。
在上一个示例中使用的简单位置监听器实现将类似于以下内容:
public class LocationListener : Java.Lang.Object, ILocationListener
{
    public void OnLocationChanged(Location location)
    {
        Trace.WriteLine(string.Format("Lat:{0}, Long {1}", location.Latitude, location.Longitude), "OnLocationChanged");
    }
    public void OnProviderDisabled(string provider)
    {
        Trace.WriteLine(string.Format("Location Provider '{0}' is disabled", provider), "OnProviderDisabled");
    }
    public void OnProviderEnabled(string provider)
    {
        Trace.WriteLine(string.Format("Location Provider '{0}' is enabled", provider), "OnProviderEnabled");
    }
    public void OnStatusChanged(string provider, Availability status, Bundle extras)
    {
        Trace.WriteLine(string.Format("Location Provider '{0}' status changed to {1}", provider, status), "OnStatusChanged");
    }
}
监听器接口可以在当前的Activity本身或任何其他JavaObject类实现上实现。使用在第三章中定义的背景化技术,即异步编程,监听器接口也可以在自定义启动的服务上实现,并且应用程序可以通过服务数据直接(绑定场景)或通过服务持久化的信息接收位置变化的背景更新。
小贴士
在移动应用程序上测试位置信息可能很困难。为了便于 GPS 数据测试和诊断,Android SDK 和 Visual Studio Android 模拟器都配备了位置模拟功能。

模拟在路线上行驶的汽车
Visual Studio Android 模拟器还提供了模拟在路线或根据定义的标记和定义的间隔更改 GPS 位置上行驶的汽车或其他交通工具的功能。
在位置信息的基础上,使用位置提供者状态信息,可以更高效、更可靠地收集位置信息(例如,根据连接性和对精度的要求在 GPS 和网络提供的信息之间切换)。为了获取当前适用于应用程序范围的最佳提供者,可以使用GetBestProvider方法,并指定所需的精度标准(粗略或精确位置信息)和功耗标准(高、中、低)。
这种智能地在位置数据提供者之间切换是使用 Fused Location Provider(Google Play Services SDK)和 Google 位置服务而不是默认位置 API 的主要优势。

Google Play 服务 Xamarin 组件
可供 Xamarin.Android v4.8+开发者作为组件使用的 Xamarin 绑定库到 Google Play Services SDK,提供了一种简单的方法将各种服务,包括位置 API,集成到 Xamarin.Android 应用程序中。这些组件实现了 Java 绑定项目,并负责 Google 提供的 Android 库的繁琐实现和编译。
在安装 Google Play 服务的位置组件后,在尝试构建 Xamarin.Android 应用程序时,您可能会收到类似于以下这样的编译错误:
"没有找到与给定名称匹配的资源(在'value'处,值为'@integer/google_play_services_version')."
这种错误的原因是 Xamarin 组件依赖于 Google Play Services SDK,并且 SDK 模块应该通过 Android SDK Manager 手动安装。

Android Google Play SDK
安装 SDK 模块后,Xamarin.Android 应用程序可以无错误地构建。
一旦设置和配置完成,就可以在 Xamarin 应用程序中初始化并使用GoogleApiClient类。GoogleApiClient需要实现两个接口以收集有关客户端连接状态的信息:GoogleApiClient.IConnectionCallbacks和GoogleApiClient.IOnConnectionFailedListener。
如果您正在实现的应用程序不依赖于位置数据的持续更新,而是只需要当前的位置,您可以使用GoogleApiClient上提供的GetLastLocation方法。此方法提供了一次性读取选项。
m_GoogleClient = new GoogleApiClient.Builder(this)
        .AddApi(Gms.Location.LocationServices.API)
        .AddConnectionCallbacks(this)
        .AddOnConnectionFailedListener(this)
        .Build();
m_GoogleClient.Connect();
为了接收融合位置提供者的实时更新,您必须为 Google 位置服务 API 实现ILocationListener接口。此监听器与默认的不同;它只包含一个用于位置变化的事件处理器实现。与数据提供者相关的事件不需要实现,因为融合位置提供者本身负责在位置数据提供者之间智能切换。
虽然提供者类型和提供者状态变化对我们使用融合位置提供者来说并不相关,但仍然可以通知融合提供者我们的应用程序范围需要哪种类型的准确性和优先级。为此,我们可以在订阅位置更新时使用SetPriority方法,并在LocationRequest上使用适当的标志。
- 
高精度(100):请求最精细的位置 
- 
平衡电源/精度(102)(默认):请求 block级别的精度(约 100 米精度)
- 
低电源(104):请求 city级别的精度(约 10 公里精度)
- 
无电源(105):将位置更新设置为使用被动模式;等待发送到其他客户端应用程序的位置更新(也称为骑乘) 
除了优先级之外,融合位置提供者还允许开发者设置其他重要的位置更新划分,例如最小间隔、最小位移和过期时间。
private async Task RequestLocationUpdates(GoogleApiClient apiClient)
{
    // Describe our location request
    var locationRequest = new Gms.Location.LocationRequest()
        .SetInterval(5000) // Setting the interval to 5 seconds
        .SetSmallestDisplacement(5) // Setting the smallest update delta to 5 meters
        .SetPriority(Gms.Location.LocationRequest.PriorityHighAccuracy)
        // Setting the priority to Fine and High Power
        .SetExpirationDuration(20000); // Stopping the location updates after 20 seconds.
    // Request updates
    await Gms.Location.LocationServices.FusedLocationApi
        .RequestLocationUpdates(apiClient, locationRequest, m_LocationListener);
}
不幸的是,Google Play 服务仅在 Android SDK 模拟器上预安装,而对于其他模拟器,必须在模拟器映像上下载并安装 Google 应用程序包。
iOS 上的位置服务
在 iOS 平台上,位置数据通过CoreLocation库访问,类似于 Android 位置 API,位置变化通过事件代理的帮助发送给订阅的应用程序。CLLocationManager类使得从移动设备获取位置数据更新变得非常简单。
Xamarin.iOS 位置数据访问实现从创建所需的Info.plist条目开始,这将解释为什么应用程序需要访问用户的地理位置。为了做到这一点,我们必须编辑 Info.plist 文件,添加以下条目之一或两个:

位置信息的 Info.plist 条目
除了 Info.plist 条目外,还应记住,从 iOS 8 开始,应用程序必须明确请求使用位置数据的权限。为了获得用户的同意,位置管理器公开了两种方法:一种用于授权应用程序进行持续本地数据更新,另一种仅当应用程序处于前台时使用。
_LocationManager = new CLLocationManager();
_LocationManager.RequestWhenInUseAuthorization();
_LocationManager.RequestAlwaysAuthorization();
最后,我们可以订阅LocationsUpdated事件以接收最新的位置更新信息。
if (CLLocationManager.LocationServicesEnabled)
{
    _LocationManager.LocationsUpdated += (sender, eventArgs) =>
    {
        Debug.WriteLine(
            string.Format("Lat:{0}, Long {1}",
            eventArgs.Locations[0].Coordinate.Latitude,
            eventArgs.Locations[0].Coordinate.Longitude), "OnLocationChanged");
    };
    // Every ~500m an update
    _LocationManager.StartMonitoringSignificantLocationChanges();
    // Every 10m send an update event
    _LocationManager.DistanceFilter = 10;
    _LocationManager.StartUpdatingLocation();
}
可以使用公开的准则属性和方法进一步优化应用程序范围内的位置信息。也可以检索其他类型的信息,例如航向方向。然而,首先检查服务是否可用,并根据系统状态信息请求更新是很重要的。
if (CLLocationManager.HeadingAvailable)
{
    // update the heading
    _LocationManager.StartUpdatingHeading();
    _LocationManager.UpdatedHeading += (sender, eventArgs) =>
    {
        Debug.WriteLine("New Heading: X: {0} Y: {1} Z: {2}", 
            eventArgs.NewHeading.X, 
            eventArgs.NewHeading.Y,
            eventArgs.NewHeading.Z);
    };
}
Windows Runtime 上的位置数据
在 Windows Runtime(Windows 商店应用程序)中,Windows.Device.Geolocation命名空间专门用于跟踪设备随时间变化的位置。Geolocator类替代了先前平台中的主要访问点,可以通过事件提供按需数据和信息更新。
与 iOS 访问请求类似,应用程序可以使用RequestAccessAsync方法请求应用程序用户的同意,并根据响应,可以通过Geolocator类访问方法或事件。
var accessStatus = await Geolocator.RequestAccessAsync();
if(accessStatus == GeolocationAccessStatus.Allowed)
{
    // Give update in every 5 meters
    Geolocator geolocator = new Geolocator { DesiredAccuracyInMeters = 5 };
    // Use StatusChanged event for Geolocator status change
    geolocator.StatusChanged += OnStatusChanged;
    // Use PositionChanged event for Geolocator status change
    geolocator.PositionChanged += (sender, eventArgs) =>
    {
        UpdateLocationData(eventArgs.Position);
    }
    // Get the current position
    Geoposition pos = await geolocator.GetGeopositionAsync();
    UpdateLocationData(pos);
}
地理围栏
地理围栏是一个抽象的边界,可以通过位置服务定义,以便创建地理围栏的应用程序在用户进入或离开此边界时从移动设备接收更新。这消除了轮询位置信息的需求,并为移动应用程序提供了不同的实现机会。
地理围栏的使用案例从简单的位置提醒到根据当前区域显示某些图像或信息创建的虚拟导游不等。
所有 Xamarin 目标平台都支持创建和使用地理围栏。例如,要在 iOS 平台上创建地理围栏,我们需要使用CLCircularRegion和CoreLocation库的位置监控功能。当移动设备进入和离开该区域时,会触发两个感兴趣的事件。
var region = new CLCircularRegion(
new CLLocationCoordinate2D(43.8592, 018.4315), 600, 
"Old Town");
if (CLLocationManager.IsMonitoringAvailable(typeof (CLCircularRegion)))
{
    _LocationManager.DidStartMonitoringForRegion += (sender, eventArgs) =>
    {
        Debug.WriteLine(string.Format("Starting region monitoring for {0}",
       eventArgs.Region.Identifier));
    };
    _LocationManager.RegionEntered += (sender, eventArgs) =>
    {
        CreateLocalNotification("Welcome to Old Town", 
            "Don't forget to take stroll down the Bascarsija and visit the historic national library!");
    };
    _LocationManager.RegionLeft += (sender, eventArgs) =>
    {
        Debug.WriteLine(string.Format("User left {0}",
        eventArgs.Region.Identifier));
    };
    _LocationManager.StartMonitoring(region);
}
此实现创建了一个围绕描述区域(由坐标定义的中心和半径为 600 米的圆)的地理围栏,并在指定的围栏被进入时发送通知,提供有关位置的信息。

老城区地理围栏
在 Android 平台上,相同的实现会结合使用LocationServices和GeofenceBuilder类来创建IGeofence类型的边界,并将它们添加到观察列表中。在 Android 平台上,一个重要的区别是事件通过代理处理,通常由一个意图服务实现。
实现开始于创建GoogleApiClient,就像之前的示例一样,一旦 API 客户端连接成功,我们就可以继续创建地理围栏(geofence(s))和将要处理回调的意图服务(intent service)。
public void OnConnected(Bundle connectionHint)
{
    var intent = new Intent(this, typeof(GeofenceListenerService));
    var pendingIntent = PendingIntent.GetService(this, 0, intent, PendingIntentFlags.UpdateCurrent);
    var geoFence =
        new GeofenceBuilder().SetRequestId("OldTown")
            .SetTransitionTypes(Geofence.GeofenceTransitionEnter | Geofence.GeofenceTransitionExit)
            .SetCircularRegion(43.8592, 018.4315, 600)
            .SetExpirationDuration(200000) // Expiration Duration is obligatory
            .Build();
    var geofenceRequest = (new GeofencingRequest.Builder()).AddGeofence(geoFence).Build();
    //
    // The async version can be used instead
    // await LocationServices.GeofencingApi.AddGeofencesAsync(m_GoogleClient, geofenceRequest, pendingIntent);
    LocationServices.GeofencingApi.AddGeofences(m_GoogleClient, geofenceRequest, pendingIntent);
}
用于在位置更新时发送本地 toast 通知的意图服务实现看起来会类似于以下:
[Service(Exported = false)]
public class GeofenceListenerService : IntentService
{
    public GeofenceListenerService() : base("GeoFenceListenerService")
    {
    }
    protected override void OnHandleIntent(Intent intent)
    {
        var geofencingEvent = GeofencingEvent.FromIntent(intent);
        if (geofencingEvent.HasError)
        {
            int errorCode = geofencingEvent.ErrorCode;
            // TODO: Log Error
        }
        else
        {
            var requestId = geofencingEvent.TriggeringGeofences[0].RequestId;
            switch (geofencingEvent.GeofenceTransition)
            {
                case Geofence.GeofenceTransitionEnter:
                    if (requestId == "OldTown")
                    {
                        Toast.MakeText(Application.Context,
                            "Don't forget to take stroll down the Bascarsija and visit the historic national library!",
                            ToastLength.Short);
                    }
                    break;
                case Geofence.GeofenceTransitionExit:
                    Debug.WriteLine(string.Format("User left {0}", requestId));
                    break;
            }
        }
    }
}
Windows Store 应用用于地理围栏的类是GeofenceMonitor和Geofence/GeoCircle描述类。一个简单的Geofence类将包括一个Geocircle类和相关的 ID。
string fenceId = "OldTown";
// Define fence properties
BasicGeoposition position;
position.Latitude = 43.8592;
position.Longitude = 018.4315;
position.Altitude = 0.0;
double radius = 600; // in meters
Geocircle geocircle = new Geocircle(position, radius);
// Create the geofence
Geofence geofence = new Geofence(fenceId, geocircle);
一旦地理围栏初始化,我们可以使用GeofenceMonitor类添加地理围栏并订阅事件。
GeofenceMonitor.Current.GeofenceStateChanged += OnGeofenceStateChanged;
GeofenceMonitor.Current.StatusChanged += OnGeofenceStatusChanged;
还可以使用地理围栏状态更改事件作为触发后台任务的触发器,这样注册的应用程序就不需要在前台或甚至运行状态。
本地库
尽管 Xamarin 框架和.NET 核心在 Xamarin.Android 和 Xamarin.iOS 平台上的实现提供了大量的功能,但在某些情况下,在跨平台实现中包含本地代码是不可避免的。幸运的是,在这两个平台上都可以绑定或链接本地库。
管理可调用包装器(Android)
如前几章所述,管理可调用包装器是生成的管理代码库,它提供了一种通过 JNI 桥与 Java 运行时交互的方法,以执行某些 Java 库中的代码。
Java 库通常打包在 Java 归档文件(JAR 文件)中,使用编译后的 Java 库项目,可以创建一个绑定库,该库可以包含在 Xamarin.Android 应用中。
为了演示这种用法,我们将创建一个用于简单 JSON 解析库的 MCW。创建我们的绑定库的第一步是使用内置的项目模板创建我们的绑定项目。

绑定库项目
一旦创建绑定项目,我们可以将 JAR 文件复制到绑定项目中的创建的 Jars 文件夹。复制完成后,一个重要的步骤是检查 JAR 资源的构建动作配置。复制的 JAVA 库文件可以使用两种方式:
- 
输入 Jar:这是一个将要用于生成管理包装器的 Java 库归档。 
- 
引用 Jar:这是一个仅用作参考而不用于生成包装器的 Java 库归档。 

绑定库结构和构建动作
在将构建动作字段设置为InputJar(这个简单的库没有任何依赖项)之后,我们可以构建库项目。一旦构建成功,你可以在<项目目录>\obj\Debug\generated\src目录中看到生成的管理文件。
namespace Org.Json.Simple.Parser {
  // Metadata.xml XPath class reference: path="/api/package[@name='org.json.simple.parser']/class[@name='JSONParser']"
    [global::Android.Runtime.Register (
                "org/json/simple/parser/JSONParser", 
                DoNotGenerateAcw=true)]
  public partial class JSONParser : global::Java.Lang.Object {
        ...
查看主解析文件,你会注意到一个类的定义由一个 Android 运行时注册和一个从 Java 对象派生的类组成。关于类或类成员的元数据也有一个元数据注释,它定义了 Java 库包中项的路径。
如果我们想要更改命名空间(默认情况下,它们是从api.xml文件中定义的包名生成的),或者类或类的任何成员的名称,我们可以使用位于绑定项目中的Metadata.xml文件。Metadata.xml文件包含对从 jar 文件生成的 api.xml 文档的转换。这个 API 描述文档包含类定义和组件,其格式类似于 mono 编译器使用的 GAPI。通过包含在Metadata.xml中的转换,我们可以重新定义为生成的 C#项指定的托管名称。
例如,为了更改命名空间,我们会使用类似以下的描述:
<attr path="/api/package[@name='org.json.simple']" name="managedName">Json.Simple</attr>
对于更改类名,语法相当类似:
<attr path="/api/package[@name='org.json.simple']/class[@name='JSONParser']" name="managedName">JsonParser</attr>
最后,生成的类声明将类似于以下内容:
namespace Json.Simple.Parser {
  // Metadata.xml XPath class reference: path="/api/package[@name='org.json.simple.parser']/class[@name='JSONParser']"
  [global::Android.Runtime.Register ("org/json/simple/parser/JSONParser", DoNotGenerateAcw=true)]
  public partial class JsonParser : global::Java.Lang.Object {
链接与绑定(iOS)
在 Xamarin.iOS 平台上处理本地代码时,开发者可以使用几种选项。
如果我们处理的是简单的 C 或 Objective-C 静态实用库,则可以创建所谓的胖二进制,然后在编译时链接它们。在 Xamarin 运行时(记住 iOS 中没有 Xamarin 运行时,所有内容都编译成静态代码),可以使用 Xamarin 框架中的 P/Invoke 功能调用本地库中的方法。
另一个选项,它允许用户通过创建与 Objective-C 类和方法绑定的方式,与本地库建立更强的“桥梁”(以性能为代价),是创建到 Objective-C 类和方法的绑定。使用这种方法,类似于 Android 运行时的托管调用包装器,我们需要在 Objective-C 框架库上创建一个 C#包装器,并使用托管包装器来访问本地实现。尽管这种方法为原生代码提供了一个更直观和可管理的访问点,因为托管包装器本质上是一个高级实现,而 mono 编译器实际上生成 P/Invoke 和 Imports 来访问原生功能,所以它比本地链接稍微昂贵一些。
两种实现都需要从创建胖二进制文件作为起点。胖二进制是口语化的术语,用来描述包含所有可能的 CPU 架构(模拟器为 i386,设备为 ARMv7/ARM64)的本地二进制编译的二进制包。为了创建适用于所有 iOS 开发目标的通用二进制文件,需要使用 Mac OS X 中的命令行工具。
lipo -create -output libFatBinary.a libThinBinary-i386.a libThinBinary-arm64.a libThinBinary-armv7.a
在创建通用的二进制文件后,现在您可以将通用包复制到 Xamarin.iOS 应用程序的项目文件夹中,将构建操作设置为 None,并指示 mtouch 编译器在编译时链接二进制文件。对于构建说明,您需要在项目属性中的构建参数部分使用 gcc 标志。
-gcc_flags "-L${ProjectDir} -lFatBinary -force_load ${ProjectDir}/libFatBinary.a
根据使用的框架或二进制文件中是否包含 C++ 代码(例如,C++ 代码的 –cxx 标志),可能需要包含额外的参数。
另一个选项是在 Objective-C 绑定项目中创建一个 LinkWith 声明(在大多数情况下,这是自动创建的)。代码如下:
[assembly: LinkWith ("libFatBinary.a",     
LinkTarget.ArmV7|LinkTarget.ArmV7s|LinkTarget.Simulator|LinkTarget.Simulator64|LinkTarget.Arm64, 
ForceLoad = true, 
Frameworks = "CoreFoundation CoreData CoreLocation", 
LinkerFlags = "-lz -lsqlite3", 
IsCxx = true)]
在 Objective-C 绑定项目中,您必须首先熟悉原生库中的类型、方法和其他构造,以便能够开始实现绑定库中的相应托管类型。
小贴士
Objective Sharpie 是一个用于创建 Objective-C 库托管包装的有用工具。最初,它是 Xamarin 团队内部使用的一个工具,很快就被公开发布。尽管实现并不完整,并且它作为一个官方产品并不完全受支持,但它可以帮助加速对原生库的实现。
摘要
在本章中,我们讨论了一些与跨应用通信、外围设备和位置数据相关的特定平台特性。
使用特定平台的特性可以使您的应用程序更吸引平台用户,通过提供他们熟悉的场景并增强应用程序的本地外观和感觉。
与不同通信协议(如蓝牙、NFC 和 Wi-Direct)相关的特定平台特性可以用于各种场景。然而,这里描述的大多数协议和配置文件都针对 Android 和 Windows Phone。Xamarin.iOS 应用程序只能从蓝牙 LE 配置文件中受益。
位置感知是另一个所有移动应用程序都可以从中受益的特定平台实现。通过将位置上下文添加到应用程序的业务逻辑中,开发者可以为用户提供更个性化的体验。
最后,如果需要,Xamarin 为 Android 和 iOS 平台提供了绑定和链接原生库的重要功能,将复杂的移植任务简化为仅仅导入。
在下一章中,我们将讨论不同平台上的用户界面组件以及它们之间的相互关系。
第七章. 视图元素
在本章中,您将找到关于用户体验(UX)设计概念的简介信息,以及 Xamarin 平台上设计原则的异同解释。将通过实例说明 UI 元素之间的关联,并使用实际案例展示有用的设计模式,以在平台间创建一致的用户体验,同时不牺牲原生外观和感觉。本章分为以下部分:
- 
设计理念 
- 
设计元素 
- 
用户交互 
设计理念
在为跨平台应用设计时,最大的陷阱之一是将一个操作系统的设计模式强加给另一个操作系统。在移动世界中,每个平台及其用户都对应用程序有一定的期望。这些期望可能微不足道,比如一个常见功能访问按钮上的图标(例如,iOS 和 Android 上的分享按钮),或者非常重要,比如视图布局(例如,iOS 和 Windows Phone 上视图底部和顶部的标签按钮)。在这个范式下,设计师的责任变得更加复杂,因为设计在创建应用程序品牌的同时,还需要对平台用户有吸引力。
用户期望
移动平台用户是习惯性动物。一个移动应用程序采用率的关键决定因素之一是它使用起来有多容易,以及平台用户发现功能有多容易。重要的是要记住,当用户熟悉了特定的平台时,他们会在与该设备交互时期望某些模式和行为。试图改变这些习惯并强迫用户进入他们不习惯的使用模式可能会代价高昂。
平台要求
iOS 和 Windows Runtime 都有经过多年微软和苹果在各个软件平台上的经验而精炼的明确设计指南。Android 作为一个开源开发平台,自早期版本以来一直在寻找自己的身份,并且最初的一般实现原则是在 iOS 上设计,然后将设计移植到 Android。然而,随着谷歌发布 Material Design 指南,Android 平台和应用程序开发者似乎终于找到了一个可以遵循的方案,并在不同的应用程序上为 Android 平台创建统一体验。
随着极简主义和平面设计模式在软件设计中的出现,微软是第一个发布微软设计语言(现代 UI,代号 Metro)的先驱。现代 UI 设计严重依赖于字体和几何形状。这种设计模式的理念是“内容优于装饰”,并鼓励应用程序开发者使用内容本身来提供交互性,并移除任何对内容或功能不重要的不必要的装饰。

Windows Phone 7 的全景视图
随着 iOS 7 的发布,苹果通过对其用户界面的全面改革加入了极简主义运动,这一改革被设计高级副总裁乔纳森·艾夫(Jonathan Ive)描述为将复杂化为秩序。透明度、字体和分层是这种新设计的亮点。这是苹果设计方向的一次重大转变,当时它以其在各种应用程序和平台上的拟物化设计而闻名。

iOS 7 主页和 Android 对话框
谷歌对扁平化设计原则的看法,即 Material Design(代号量子纸),试图通过将设计元素简化到其基本要素,并重新创建具有类似纸张和墨水本质的强烈字体的交互表面来解决相同类型的设计问题。
硬件依赖性
与网络应用程序类似,在 Xamarin 目标平台上,尤其是在 Windows 和 Android 上,Xamarin 应用程序将要运行或显示的硬件差异很大。为特定平台设计的应用程序可以在具有中等分辨率的低端触摸屏设备上使用,也可以在具有高清显示的平板电脑上使用,无论是横向还是纵向旋转。这种硬件依赖性在设计移动应用程序的用户界面时应是主要关注点之一。
例如,在 Android 3.0 之前的手机通常有硬件按钮,这些按钮有助于在应用程序和操作系统本身中进行导航。这些按钮包括返回、菜单、搜索和主页按钮。尽管后来的设备将硬件按钮替换为底部的系统导航栏(软件按钮),但这一特性在仍然具有返回、Windows 和搜索硬件按钮的 Windows Phone 设备上得到了延续。在 iOS 上,导航层次结构的实现完全取决于应用程序,通常由放置在顶部导航栏上的返回按钮处理。
Android 上的设计指标
对于不同的分辨率,为了创建自适应的用户界面,每个平台使用不同的方法。然而,在每个平台上,重要的指标单位是像素密度。像素密度可以定义为在一英寸长度内可以容纳的像素数量。根据像素密度(PPI 或每英寸像素数),独立于屏幕的总物理宽度和高度,开发者可以在各种移动设备上创建一致的视图。换句话说,总屏幕分辨率(像素密度乘以屏幕尺寸)是设计跨设备/平台应用程序时考虑的一个下降特性。
在 Android 平台上,为了在不同像素密度上创建统一的使用体验,开发者和设计师被鼓励使用密度无关像素(dp)单位来表示 UI 控件的各个尺寸和测量值。密度无关像素是通过将 160 像素密度作为标准来计算的,并使用归一化像素值来计算显示尺寸。
查看以下表格以获取有关 Android 密度无关像素的更多信息:
| 屏幕密度 | 密度桶 | 屏幕分辨率(像素) | 屏幕分辨率(dp) | 
|---|---|---|---|
| 120 | LDPI | 360 x 480 | 480 x 640 | 
| 160 | MDPI | 480 x 640 | 480 x 640 | 
| 240 | HDPI | 720 x 960 | 480 x 640 | 
| 320 | XHDPI | 960 x 1280 | 480 x 640 | 
Android 密度无关像素(dp)
为了演示缩放和密度无关像素,我们可以比较不同设备上的以下视图。使用像素设计的内容将在不同设备上以不同的方式可视化:

使用像素设计 UI
然而,如果我们使用 dp 作为测量单位来使用相同的设计元素,UI 将会更加统一。

使用 dp 设计 UI
与 dp 类似,Android 上另一个密度无关的度量单位是 "sp",或可缩放像素。dp 和 sp 之间的主要区别在于 sp 是根据用户的字体设置进行缩放的,通常与文本内容相关联,而 dp 由操作系统管理,用户通常无法对其进行任何控制。
对于媒体资源(例如,图像)和布局,Android 解决方案结构支持创建专门的设计元素。可以使用正确的密度桶标识符作为 drawable 文件夹的后缀来提供不同大小和分辨率的图标和其他图形(例如,drawable-xhdpi 用于超高密度)。同样,如果需要,可以根据布局文件夹中的屏幕尺寸组提供多个替代布局(例如,layout-xlarge 或 layout-xlarge-land 用于超大屏幕上的纵向和横向显示)。
iOS 上的设计指标
在 iOS 生态系统中,只有少数设备和屏幕分辨率。在这个平台上,显示缩放的标识符是点(pt)表示法。点在非视网膜显示器上显示为一个物理像素。在视网膜显示器和更高配置(iPhone 6 Plus)上,缩放因子分别计算为 2x 和 3x,而点测量值保持不变。
注意
iPhone 6 Plus 的缩放因子为 3,屏幕分辨率为 414 x 736 点。这相当于 1242 x 2208 像素分辨率。然而,该设备上支持的物理分辨率是 1080 x 1920。因此,使用 3x 缩放因子渲染的图像在此设备上以 1:1.15 的比例进行下采样。
Windows Runtime 上的设计度量
在 Windows Runtime 上,应用视图的缩放由缩放算法负责,这些算法将控件、字体和其他 UI 元素的大小标准化。这个过程发生在运行时,开发者通常不需要处理它。当设计 Windows Runtime 应用程序时,度量单位是像素。然而,像素被称为有效像素。有效像素是操作系统的标准化尺寸单位。

Windows Runtime 上的有效像素
对于有效像素的一个常见例子是考虑一个 24px 大小的字体。使用这个字体可视化的文本在用户距离手机 5-10 厘米处和距离几米远的 surface hub 上显示方式相同。
设计元素
为了在各个平台上创建一致的布局,同时符合平台要求,开发者和设计师需要熟悉每个平台,并在这些平台的布局和 UI 控件之间建立联系。我们将在下一章中讨论 Xamarin.Forms 范围内的这个问题。这些平台之间的联系构成了 Xamarin.Forms 框架的基础。
基本布局
在所有三个平台上,主要的布局元素彼此非常相似。然而,这些元素的位置根据平台的不同而有很大差异。

用户界面布局
在每个平台上,显示系统信息的状态栏都位于屏幕顶部(如前一张截图中所标记的“1”所示)。这部分是设计适用于 Xamarin 目标平台的应用程序时应牢记的常量元素之一。
注意
在 Windows 10 操作系统上,系统栏可以根据用户的主动操作来扩展,以提供有关系统的详细信息。这种扩展会导致应用画布上的元素被隐藏。然而,这并不会导致元素偏移,扩展发生在屏幕的不同层上。
在所有三个平台上,第二个元素通常是导航栏(在屏幕截图上标记为“2”)。此元素仅用于显示有关当前视图的信息。然而,在 iOS 和特别是 Android 上,导航栏具有额外的功能。iOS 应用程序中的导航栏可用于分层导航项。然而,在 Android 平台上,所谓的应用栏包含与上下文相关的命令和导航项。在 Android 应用程序的主要应用栏(右侧的导航面板)上显示的上下文菜单以及显示不适用于主应用栏(右侧的导航面板)的额外上下文相关命令的上下文菜单和显示左侧导航面板的导航抽屉是 Android 应用程序主要应用栏的功能和结构元素。在 Windows Phone 应用程序的标题区域放置与应用程序和内容相关的按钮或链接已被不推荐。然而,在 Windows 10 上,类似于 Android 平台上的导航抽屉,开发者可以实现应用程序级别的切换。
在 Windows 平台上,与上下文相关的应用程序命令和显示在上下文菜单中的附加项通常位于应用程序栏底部(标记为“5”)。尽管可以在屏幕顶部创建应用程序栏,但这通常适用于使用对等/水平导航模式的应用程序(参考下一节,导航)。
系统导航栏(标记为“4”)位于 Android 平台屏幕底部。此栏包含三个按钮,即返回、主页和历史记录。在 Android 4.0 之前,这些按钮曾是硬件按钮。
与 Windows Phone 上的底部应用栏和 Android 系统导航栏不同,在 iOS 上,这个区域通常由标签栏(标记为“3”)占据。标签栏提供了 iOS 应用程序的主要导航功能,并且应该在应用程序的每个屏幕上可用(类似于 Windows Phone 上的对等导航应用栏)。
导航
在应用程序设计中,导航策略应该是首要决策事项之一。根据应用程序的要求或要关注的元素,开发者可以采用不同的导航策略。
在构建导航树和准备应用程序的流程图时,你可以使用两种类型的遍历:分层(垂直)和对等(水平)。当用户想要在导航树同一级别的页面之间导航时,会发生水平导航。分层导航可以在垂直路径的任一方向上。一般来说,随着用户导航的深入,屏幕上类似类型的对象数量减少,单个对象的细节增加。换句话说,在应用程序导航层次结构的子树的低层节点中很少看到列表视图。

导航层次结构
在传统的导航方法之上,可以在不同级别和子树之间的页面之间使用跳转链接,以便轻松访问这些节点(例如,一个从层次结构底部导航回主页的“首页”链接)。
为了展示导航设计,我们将为 TravelTrace 应用程序创建一个界面,该界面在上一章中作为功能实现的示例。
水平导航
在同级或兄弟之间进行导航可以提供一种轻松切换上下文的方法,以便在顶级项目之间切换。在这种情况下,同级将代表应用程序的主要功能,这些功能通常应始终对用户可用。在这一级别,导航可以通过标签控件或 Android 平台上的导航抽屉等应用程序级导航提供者来实现。主页应具有清晰的设计和焦点;它应该说明您的应用程序旨在做什么。
例如,如果我们想用我们的旅行应用程序来展示顶级同级项,我们首先需要确定应用程序必须提供的主要功能。
这款旅行伴侣应用程序的可能功能包括:
- 
获取附近景点的详细信息 
- 
允许用户规划他们的旅行 
- 
创建并分享旅行纪念品(照片、笔记、提示等) 
应用程序的功能可能包括:
- 
创建一个社交平台来分享和重用旅行体验 
- 
在旅行和文化访问前后协助用户 
总体而言,我们希望强调社交方面,并在用户访问期间提供个人协助。鉴于这个“决定”,我们可以开始为我们的应用程序设计初始概念。

主屏幕示例
在 Windows Phone 平台上,主屏幕可以是中心视图或枢轴视图。尽管每个视图都有类似的导航属性,但枢轴控件通常用于显示具有相似特性的内容分组。
因此,通常更倾向于使用中心视图作为主页,以便使应用程序的不同顶级部分可用,并且子节点易于发现。

中心视图(Windows Phone)
当考虑 Windows Phone 和HubView时,在层次结构中的顶级项之间导航的唯一可能方式是滑动手势,而在其他平台上则可以点击标签栏按钮。
在导航不同类别或内容项的过滤视图时,也可能发生另一种类型的水平导航。在 Android 平台上,主应用栏可以托管一个过滤器下拉菜单,以选择显示内容项的正确类别。在 iOS 上,导航栏或次要底部工具栏可以用来创建一个按钮,显示选择器(也称为旋转按钮),以选择导航树上的正确兄弟节点。iOS 上另一个可能的水平导航提供者控件是SegmentedView控件,它可以用来显示同一类型内容的不同视角(例如,与未来计划相比的过往旅行或最近的指南和专辑)。

iOS 上的 SegmentedView 控件
在 Windows 平台上,对于具有“几个”以上类别且可能类别始终可见并显示在内容区域旁边的用例,选择主/详细类型的实现通常是一个更好的主意。也可以在附加到命令栏按钮之一的飞出菜单中使用下拉菜单。如果类别数量有限,可以在视图实现中使用PivotView控件。

Windows Phone 上的命令栏飞出
在所有平台上,也可以包含内容内的选择控件,帮助用户在类别之间导航(下拉菜单、选择器、旋转按钮、超链接、按钮等)。
例如,我们的旅行应用程序的目录视图允许用户自由浏览上传的内容,需要将不同大陆的国家项目进行分类。

Android 上的主应用栏过滤器
最后,iOS 和 Android 顶部导航栏和主应用栏上使用的 Next/Previous 按钮,以及 Windows Phone 上的左右滑动手势,可以在在兄弟节点和/或集合项之间导航时创造一个愉快的体验。这种类型的导航通常用于层次导航树底部或子树底部。
垂直导航
具有父子关系的元素(例如,父页面可以是国家视图,子视图可以是城市详情)可以使用导航树的垂直遍历。最简单和最常见的方式是当用户点击项目时导航到内容元素的细节视图。
与细节概念相关的一个常见错误是将它变成一个两步过程,其中用户首先需要选择项目,然后点击一个细节命令按钮。在现代应用程序中,通过使用内容元素本身作为交互元素,使 UI 直观至关重要。
一旦用户处于详细信息视图,向上导航到导航树中的上一级可以通过在主应用栏(在 Android 上)和导航栏(在 iOS 上)上的返回按钮,或者使用 Windows Phone 上的硬件返回按钮和系统栏上的软返回按钮来实现。不建议在 Windows Phone 平台上使用额外的返回按钮,因为设计空间已经有限,并且可以使用硬件按钮实现相同的功能,这与桌面版本不同,桌面版本没有硬件按钮,设计画布相对宽敞。

Windows Phone 上的语义缩放
在 Windows Phone 平台上,在不实现二级视图的情况下,创建对内容元素的不同视角的另一种方法是使用SemanticZoom控件。SemanticZoom控件提供了同一列表内容元素的两个视图,其中第一个通常是一个分类视图,元素数量较少,第二个是包含内容项额外详细信息的完整列表视图。两个视图之间的导航通常通过捏入和捏出手势来实现(有关详细信息,请参阅手势部分)。
跳转导航
当应用程序在未遵守导航层次结构的情况下在不同节点之间导航时,就会发生跳转或交叉导航(例如,可以导航到位于 Windows Phone 应用程序顶层中心页面的第三级项的详细信息视图)。
这种类型的导航通常与非常特定的功能一起使用,这些功能与应用程序的一般轮廓无关。导航命令可以包含在导航栏中,或者作为嵌入到内容中的超链接。使用命令栏创建与项目相关的导航链接也很常见。

Android 上的导航抽屉
在 Android 上,创建用于轻松切换上下文的导航访问点的另一种可能方式是使用导航抽屉类型的功能。在 iOS 上,可以通过持续标签栏获得类似体验。如前所述,随着 Windows 10 的发布,类似的功能被添加到了 Windows Phone 平台。
内容元素
每个 Xamarin 目标平台都提出了一些策略和指南来可视化内容。尽管开发人员被赋予了创建吸引人和创新的设计块的自由,尤其是在 Android 和 Windows Phone 平台上,但必须遵守严格的指南。我们可以将这些内容块和控制项分为几个类别。
收藏视图
集合视图提供了一种高效的方式来显示基于集合的内容元素。在大多数实现用例中,集合元素是交互式的,并使用文本和图像控件显示内容项的属性。在内容项本身上添加与项目相关的命令或标志也很常见,形式为标记(例如,将项目添加到收藏夹的命令、显示状态图标等)。
UITableView (iOS)
在 iOS 平台上,UITableView 提供了一种灵活的方式来在可自定义的布局上显示集合数据。在表格视图中,每个单元格都可以自定义以显示来自内容项的一批属性,并且开发者可以自由使用内置的事件和命令来实现额外的命令逻辑(例如,行操作)。

分组表格视图和带有详细信息的表格视图
UITableView 和相关控制器 (UITableViewSource) 的另一个开箱即用的功能是内容元素所谓的索引。索引的工作方式与 Windows 平台上的跳转列表类似,提供了一种轻松的方式来对内容项进行编目,并使用户能够轻松跳转到正确的部分或组。
搜索显示控制器也可以与 UITableView 关联,在项目集合上创建标准的 iOS 搜索体验。
默认情况下可以包含在表格视图单元格中的可能的艺术品如下:
|  | 复选标记 | 表示行被选中 | 
|---|---|---|
|  | 揭示指示器 | 表示与行关联的另一个表格 | 
|  | 详细信息揭示指示器 | 标识用户可以点击以查看当前行的详细信息(例如,弹出视图) | 
|  | 行重排 | 标识行可以被拖动以重新排序 | 
|  | 行插入 | 向表格中添加新行 | 
|  | 删除视图/隐藏 | 显示或隐藏当前行的删除按钮 | 
|  | 删除按钮 | 删除行 | 
表格视图艺术品
UICollectionView (iOS)
UICollectionView 用于在 iOS 平台上创建类似网格的布局。集合视图也可以通过内置属性和基类进行自定义。与本质上受表格结构和包含单元格限制的表格视图相比,集合视图在本质上更灵活。
集合视图也由可以在多种布局中显示的单元格组成。默认布局可以使用 UICollectionViewFlowLayout 进行自定义。流布局可以定义行之间的最小行间距、项目之间的最小临时间距、项目大小和部分内边距(分配给集合中各部分的边距)。
以下代码示例创建了一个简单的流布局结构:
UICollectionViewFlowLayout flowLayout = 
                          new UICollectionViewFlowLayout();
flowLayout.MinimumLineSpacing = 20f;
flowLayout.MinimumInteritemSpacing = 4f;
flowLayout.SectionInset = new UIEdgeInset(4f, 4f, 4f, 4f);
flowLayout.ItemSize = new SizeF(20f, 20f);
myCollectionView.CollectionViewLayout = flowLayout;
另一种自定义集合视图布局的方法是继承 UICollectionViewLayout 类并实现自定义布局。在自定义布局实现中,该类负责根据集合大小和可用布局区域提供布局属性,如单元格的大小和位置。
UICollectionViewController 用于标准化要呈现的数据,并作为集合和单元格级别事件(如单元格选择和上下文菜单)的代理。
此外,SupplementaryView 和 DecorationView 类通过提供与部分相关的详细信息和集合视图层的 UI 自定义来提供额外的自定义。
ListView (Android)
ListView 是 Android 平台上使用最频繁的组件之一。虽然它可以用来显示相对较小的菜单项列表,但通过适配器,它也可以用来可视化来自其他应用程序和服务的数据。可以将 ListView 控件与 iOS 平台上的 UITableView 控件和数据提供者接口进行比较。Android 上的适配器可以与 iOS 上的 UITableViewSource 相比较。
默认情况下,ListView 包含 12 个内置视图,可以通过 Android.Resource.Layout 类访问。这些布局从简单的单行文本到可展开的分组类别视图不等。每个布局都使用几个控件引用,如 Text1、Text2 和 Icon,这些应由适配器通过将值分配给内容字段来填充。通过创建 AXML 标记文件并在适配器中引用该标记,也可以实现自定义布局。
一个示例自定义布局实现可能如下所示:
<RelativeLayout 
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
 android:background="@drawable/CustomSelector"
    android:padding="8dp">
    <LinearLayout
        android:id="@+id/Text"
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="10dip">
        <TextView
            android:id="@+id/Title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20dip" />
        <TextView
            android:id="@+id/Description"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14dip" />
    </LinearLayout>
    <ImageView
        android:id="@+id/Image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:padding="5dp"
        android:src="img/icon"
        android:layout_alignParentRight="true" />
</RelativeLayout>
我们还可以通过添加视觉状态选择器来扩展样式(参见前一个示例中的背景颜色分配)。
自定义视觉状态选择器的实现可以定义为以下内容:
<?xml version="1.0" encoding="utf-8"?>
<selector >
  <item android:state_pressed="false"
        android:state_selected="false"
        android:drawable="@color/blue" />
  <item android:state_pressed="true"
        android:state_selected="false"
        android:drawable="@color/red" />
</selector>
最后,列表适配器的实现可能如下所示:
    public class CountriesDataAdapter : BaseAdapter<Country>
    {
        private List<Country> m_Items;
        private Activity m_Context;
        public CountriesDataAdapter(Activity context, List<Country> items)
        {
            m_Context = context;
            m_Items = items;
        }
        public override long GetItemId(int position)
        {
            return position;
        }
        public override View GetView(int position, View convertView, ViewGroup parent)
        {
            var item = m_Items[position];
            View view = convertView ?? 
m_Context.LayoutInflater
.Inflate(Resource.Layout.CustomRowView, null);
            view.FindViewById<TextView>(Resource.Id.Title).Text = item.Name;
            view.FindViewById<TextView>(Resource.Id.Description).Text = string.Format("In {0} region of {1}", item.Region.Name, item.Region.Continent);
            view.FindViewById<ImageView>(Resource.Id.Image).SetImageResource(Resource.Drawable.Icon);
            return view;
        }
        public override int Count
        {
            get { return m_Items.Count; }
        }
        public override Country this[int position]
        {
            get { return m_Items[position]; }
        }
    }
上述代码应生成类似于以下截图的视图:

带有自定义布局的列表视图
GridView (Android)
除了ListView控件外,在 Android 平台上,集合可以通过ViewGroup进行可视化。视图组用于捆绑不同的视觉树,并在可滚动的视图元素中显示项目。ViewGroup最常见实现是GridView小部件。GridView是一个可滚动的网格控件,其中内容项再次提供了ListAdapter实现。
GridView通常与一组同质的内容项一起使用。这些内容项由一组文本内容和相关的图像项组成。内容项通常被称为磁贴,它们还可以包括几个与内容相关的命令。
磁贴在概念上类似于 Windows 应用程序现代 UI 设计的动态磁贴块。它们由主要内容和次要内容组成。主要内容填充整个单元格(例如,相册应用中的专辑封面),而次要内容则由图标或文本表示。主要操作通常是垂直下降的导航命令(导航到详细视图)。与内容项相关联的上下文操作通常被认为是磁贴上的次要内容。
如果内容项上的操作量或内容不是同质的,建议考虑在网格视图中使用卡片而不是磁贴。
CardView(Android)
CardView控件是在 Android 5.0 中引入的,它可以被描述为一个自包含的内容单元。这里的自包含是指卡片通常包括多个操作和多种与内容相关的项目。用户通常不需要求助于二级操作(选择然后使用上下文菜单)来与这些内容项交互。

标准卡片布局
当没有直接比较集合元素的需求和可能性,且内容由各种类型的数据组成时,通常使用卡片。卡片可以通过使用操作按钮或在某些情况下,使用内容内的输入控件来交互。它们可以是可展开的,并且通常具有固定的宽度。
CardView作为一个FrameLayout小部件实现,可以与ListView或GridView一起使用来表示内容元素。
ListView 和 ListBox(Windows Phone)
ListView和ListBox是 Windows Phone 平台上的主要集合可视化控件。ListView是ListBox的一个更专业的实现,主要用于显示基于文本的内容。它的对应物ListBox高度可定制,可以用于显示由多种数据类型组成的内容。
这两个容器都可以用于项目级别的上下文操作。然而,ListBox类似于 Android 平台上的CardViews,用于创建可能包括操作和输入控件的可交互内容元素。
这两个控件都支持双向数据绑定,并且可以使用行为、项目模板和/或控件样式来对项目进行样式化和自定义。默认情况下,这两个控件的方向都是垂直的,但如果希望内容项目在水平线上显示,则可以将方向设置为水平。
如果需要在模板级别进行更多自定义以及如何布局项目,开发者还可以使用 ItemsControl,这是 Windows Phone 平台上大多数集合视图的基础实现。
为了自定义在 ListView 上显示的项目,我们首先需要创建一个 DataTemplate,它将是用于 ListViewItems 的模板。
一个示例 DataTemplate 声明可能如下所示:
<DataTemplate x:Key="SampleItemTemplate">
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <Border Margin="0,10,0,0">
      <Image Source="{Binding ImageUrl}" Height="80" Width="80" />
    </Border>
    <StackPanel Grid.Column="1" Margin="15,0,0,0">
      <TextBlock Text="{Binding Title}"
        Style="{ThemeResource ListViewItemTextBlockStyle}" />
      <TextBlock Text="{Binding Subtitle}"
        Style="{ThemeResource ListViewItemSubheaderTextBlockStyle}" />
    </StackPanel>
  </Grid>
</DataTemplate>
一旦我们的模板准备就绪,我们可以将模板分配给我们的 ListView,同时附带集合数据源,这是一个包含简单 SampleItem 对象的列表,这些对象的属性在 DataTemplate 中进行了描述。
代码如下:
<Grid>
  <ListView
    ItemTemplate="{StaticResource SampleItemTemplate}"
    ItemsSource="{Binding MyItems}" />
</Grid>
现在,内容项目在 ListView 中以两列样式显示,包括图片、标题和描述文本。
GridView (Windows Phone)
GridView 是 Windows Phone 平台上 ItemsControl 的另一种实现,它允许开发者以流布局创建集合视图。在处理媒体元素时,通常应首选 GridView 而不是 ListBox 或 ListView。

ListView 与 GridView 的比较
与之前定义的元素类似,GridView 支持双向数据绑定,并且可以使用标准方法进行自定义。
虚拟化面板(Windows Phone)
重要的是要认识到移动平台的性能不如桌面或平板设备。特别是当处理大量数据时,尽管应用程序在桌面工作站上可以很好地进行视觉表现,但内存资源可能会在移动设备上导致 UI 闪烁、延迟,甚至阻塞。为了通过仅加载所需数据来减少内存使用并提高性能,Windows Runtime 提供了虚拟化面板控件(例如,VirtualizingStackPanel)。ItemsControl 是大多数在此描述中提到的集合视图的基础,它支持数据和 UI 虚拟化。

Windows Runtime 上的 UI 虚拟化
UI 虚拟化处理的是在应用程序视图中渲染的控件。当应用程序的列表视图绑定到大量项目时,在这种情况下,不需要在运行时内存中渲染和保持控件,而只需处理视口中的控件。在这个范例中,通过滚动操作从屏幕上移除的控件,如果用户滚动回来看,需要被销毁并重新绘制。
数据虚拟化处理分页数据源。例如,对于“可虚拟化”的数据源(实现ISupportIncrementalLoading的集合),只有当前视口所需的数据被加载到应用程序中,当 UI 控件需要显示更多项目时,会从数据源请求额外的批次。随机访问虚拟化允许开发者在任何随机序号上检索数据子集。对于这种类型的数据虚拟化,数据源需要实现INotifyCollectionChanged和IObservableVector。
模态视图
模态视图是临时视图组件,可以提供交互式界面以获取用户对特定任务的输入或决定工作流程的执行路径。使用警告对话框通知用户关于对应用程序执行至关重要的关键信息也是常见的。
弹出视图和警告(iOS)
iOS 平台提供了各种模态对话框,用于在不同场景下显示、编辑和操作数据。这些对话框类型的外观各不相同,但共同点是它们总是获得焦点,并显示在屏幕的最高层,而对话框下的内容则通过半透明覆盖层隐藏。
操作表是使用频率最高的模态对话框之一。这种对话框类型通常用于在开始任务或取消任务之前给用户一个选项。它通常以按钮列表的形式显示;最后一个按钮通常是屏幕底部的“取消”按钮。

iOS 上的操作表显示
可以使用UIAlertController并指定UIAlertControllerStyleActionSheet来初始化操作表。如果屏幕尺寸允许(在水平规则的环境中),操作表将以弹出视图的形式显示。
警告对话框是 iOS 上另一种类型的模态对话框。警告通常用于通知或请求用户同意影响应用程序执行的问题。与操作表不同,警告对话框可以包含描述性文本、标题,甚至文本输入字段。

带输入字段且只有描述和标题的警告对话框
可以使用UIAlertController和指定UIAlertControllerStyleAlert来调用警告对话框。警告对话框应避免任何形式的冗余、非正式和负面内容。如果标题提供了足够的信息让用户继续执行,则可以省略描述性文本。
弹出视图是 iOS 平台上的另一种临时上下文视图。然而,弹出视图仅在水平规则的环境中显示(在 iPad 的纵向和横向,以及 iPhone 6 Plus 的横向旋转中)。在水平紧凑的环境中,它们作为全屏模态对话框显示。
为了初始化弹出视图,可以使用UIPopoverPresentationController。
模态对话框是 iOS 上使用的另一种类型的临时视图显示。模态对话框可用于需要执行非常特定任务或工作流程的自包含和紧凑视图的场景。

页面表单样式的模态对话框
模态对话框可以使用 UIPresentationController 创建,具有各种模态呈现样式(全屏、页面表单、表单表单和当前上下文)。然而,与模态对话框关联的呈现样式在水平紧凑环境中几乎表现相同(所有 iPhone 模型,除了横向模式下的 iPhone 6 Plus)。
弹出菜单、弹出和菜单(Windows Phone)
弹出菜单是 Windows Phone 平台上的主要模态对话框。它们可用于各种场景,包括显示上下文菜单、显示项目的附加详细信息或获取用户的同意。不同类型弹出菜单的共同行为是它们总是以最高的 z-index 在屏幕上显示,并且下面的元素通过半透明覆盖被禁用。弹出菜单默认具有轻触关闭机制。换句话说,如果用户在弹出控件边界外任何地方轻触,它们可以被关闭。
弹出菜单通常与当前视图上的另一个控件相关联,要么通过使用附加属性,要么通过使用 Flyout 类的 ShowAt 函数。Flyout 类的内容属性用于分配一个 UIElement 以在屏幕上显示。
    var flyout = new Flyout();
    var stackPanel = new StackPanel { Orientation = Orientation.Vertical, Margin = new Thickness(5)};
    var textBlock = new TextBlock { Text = "Flyout Text Content", FontSize = 20 };
    var textInput = new TextBox { PlaceholderText = "Input Value", FontSize = 18 };
    var button = new Button { Content = "Apply", FontSize = 18 };
    stackPanel.Children.Add(textBlock);
    stackPanel.Children.Add(textInput);
    stackPanel.Children.Add(button);
    flyout.Content = stackPanel;
    flyout.ShowAt(TextBlock);
上述示例代码将创建一个包含文本内容、输入字段和按钮的弹出菜单:

Windows Phone 上的简单弹出菜单
注意
尽管弹出菜单始终附加到一个 UIElement(无论是使用 XAML 还是代码)并且对话框应该显示在相关元素附近,但在 Windows Phone 上,弹出菜单的行为类似于显示在屏幕顶部的消息对话框。
在 Windows 运行时,可以使用弹出菜单的派生类型来处理特定场景。MenuFlyout、TimePickerFlyout 和 DateTimePickerFlyout 是这些实现的示例。

菜单弹出菜单使用
除了弹出菜单之外,弹出控制还可以用来显示临时视图或内容项的详细信息。弹出菜单通常是独立的控件,可以直接包含在视图 XAML 中。它们可以选择使用轻触关闭,并可以使用 IsOpen 属性来显示或隐藏。
对于警报对话框或关键输入要求,MessageDialog类为开发者提供了一个熟悉的实现工具。MessageDialog是一个简单的对话框,用于显示文本内容和众多 UI 命令。UICommand类代表一个按钮及其相关的动作(如果有),用于在对话框上显示动作,并在用户选择后为对话框提供一个结果。以下实现创建了一个带有文本字段和两个命令的消息对话框:
MessageDialog dialog = new MessageDialog("You are about to delete an important item", "Important Deletion");
dialog.Commands.Add(new UICommand("Sure"));
dialog.Commands.Add(new UICommand("Not Really"));
dialog.ShowAsync();
这在 UI 上显示的方式类似于飞出视图的视觉表示:

Windows Phone 上的 MessageDialog 示例
对话框(Android)
在 Android 上的对话框可以简单实现,如警报对话框或全屏对话框,用于检索所需表单数据以继续当前任务。对话框的行为与其他平台上的模态对话框实现相同;它们中断当前任务,并显示在底层之上。底层内容通过半透明的灰色覆盖层隐藏。
简单的警报对话框,类似于其他平台上的并行实现,由标题、描述性内容和确认操作组成。它们在用户输入对执行至关重要的情况下被调用。

Android 警报对话框
在描述性内容和动作按钮内容中避免任何歧义是很重要的。
在 Android 应用程序中使用的另一种流行对话框是上下文菜单对话框。这种类型的对话框在选择列表中的项目后不需要任何确认。它们也有轻量级消失的行为。如果对话框有关于选择项的附加信息以及可能的其他操作,它们被称为简单对话框。这些对话框上的选择也不需要确认。

Android 对话框
如果对话框实现需要用户明确确认所做的选择,这些对话框通常被称为确认对话框。在对话框屏幕底部通常有一个“取消”按钮,以便保留之前选定的选项。
文本视图
在所有三个平台上,随着极简设计倾向的出现,排版和文本内容项成为了用户体验设计的焦点。每个平台都有针对不同场景的字体大小和字体的明确指南。更重要的是,这些平台中的每一个都有专门的方式来显示和编辑富文本格式。
- 
Windows: 在 Windows Phone 平台上,Run 元素用于定义应用特定格式化的文本部分。Run 元素可以包含在 TextBlock元素或RichTextBlock控件中。除了 Runs,RichTextBlocks还可以与类似 html 的样式元素(例如,粗体、span、斜体等)结合使用。使用RichTextBlocks和RichTextBlockOverflow作为容器,Windows Phone 应用程序可以支持任何形状和样式的文本显示。
- 
Android: 在 Android 平台上,使用所谓的 spans 实现文本格式化。有大量的预定义样式 span 实现,例如 RelativeSizeSpan、ForegroundColorSpan和ClickableSpan。这些 span 实现用于设置SpannableString中某些部分的描述样式。有一个SpannableStringBuilder类可以用来创建带样式的段落/文本内容。一旦SpannableString完成,它就可以用作TextView控件的内容。
- 
iOS: 在 iOS 平台上,文本相关功能和控件由 Core Text 库引入。 UITextView控件是该库中的可视化元素。通过使用NSMutableAttributedText类实现文本格式化。对于带属性的文本内容,可以设置不同的文本范围以使用某些属性,例如NSUnderlineStyleAttribute、NSBackgroundColorAttribute等。在显示带属性的文本块时,可以使用NSTextContainer来描述一个形状,其中文本应该以行片段的形式显示。
网页视图
在 Xamarin 目标平台上,网页视图控件用于显示丰富的 HTML 内容。这些网页视图控件构建自己的导航堆栈,独立于应用程序运行时。在 Android 和 Windows Phone 上,还可以将 JavaScript 注入到控件上显示的 HTML 内容中。
在所有平台上,不仅可以从远程加载,还可以从应用程序资源中加载本地网络应用程序。
反馈
现代应用程序设计的支柱之一是确保用户始终了解应用程序正在执行的操作以及这些任务的进度。即使应用程序正在处理阻塞调用(在完成任务之前无法继续执行),显示进度环也能营造出应用程序仍然响应的错觉。
进度指示器可以分为两组:不确定的和确定的。
不确定进度
不确定的任务及其相关的进度指示器与那些应用程序无法提供估计完成时间或进度信息的操作相关。这些操作可能依赖于多个子程序的完成,可能与整个应用程序或单个 UI 元素相关。
对于不确定过程,我们首先需要决定该过程对于应用程序有多重要。如果应用程序无法在没有完成当前过程的情况下继续,这将是一个应用程序级阻塞调用。在涉及阻塞调用(包括单步或多步)的情况下,使用主内容区域上的进度环是一个好主意。这种场景的一个好例子是主客户端试图从服务器检索电子邮件消息,而不知道服务器上有多少条消息。如果此过程中涉及多个步骤,你还可以在进度环附近或上方显示信息文本。
在 Android 上,可以使用ProgressDialog类实现此实现。实例化此控件提供了一个模态对话框,可以包含描述性文本。在将其显示在 UI 上之前,将不确定标志设置为 true 是很重要的。

Android、iOS 和 Windows Phone 上的进度环
在 iOS 上,使用UIActivityIndicatorView可以实现相同的可视化。你可以修改其行为以实现动画和颜色变化。
在 Windows Phone 上,ProgressRing类提供了相同类型的功能。
在不确定的场景中,如果正在执行的过程不会阻止用户继续与应用程序交互,最好给出关于过程和执行中涉及的控件更微妙的指示。这可以通过使用位于或覆盖过程开始处的进度环或进度条来实现。在 iOS 上,进度条和环之间的唯一区别是过程是确定性的还是不确定性的。然而,在 Android 和 Windows Phone 上,进度条也可以作为不确定任务指示器。在 Windows Phone 平台上,如果过程是应用程序级任务,通常也会在屏幕顶部显示不确定进度条,但与应用程序的交互可以继续,无需等待此过程的完成结果。
确定进度
确定任务及其相关指示器与应用程序可以提供当前状态信息给用户的过程相关。在 Xamarin 目标平台上,首选的确定进度指示器是进度条。进度条在提供过程当前完成状态的视觉指示的同时,还可以包括一个标签,给出任务当前状态的文本描述。
如果过程相对较长,提供取消方法(例如,进度条附近的取消按钮)也很重要。

Android 确定进度条显示
在 Android 平台上,除了进度指示外,进度条上还可以显示缓冲百分比。
用户交互
在跨平台开发项目中,应用程序的用户交互模式集合是另一个重要元素。已经在其他平台上使用应用程序的用户希望在其他平台运行的客户端上找到相同的交互模式。当涉及到特定平台的交互模式时,这个决策过程变得更加复杂,因为应用程序应该为平台用户提供一个熟悉的界面。在这种情况下,实现平台原生性和应用程序身份之间的平衡妥协非常重要,并找到最佳解决方案。
使用交互模式进行品牌的一个好例子是 iOS 应用程序中使用的“下拉刷新”交互模式。大多数处理信息流(例如,Facebook、Twitter 等)的应用程序提供商都在他们的 iOS 应用程序中使用了这种实现。尽管这不是 Android 和 Windows Phone 上的原生交互模式,但类似的方案在这些平台上迅速流行起来;因此,大多数开发者和用户现在在各种平台上都采用了这种用例。
交互控件
在大多数情况下,为 Xamarin 目标平台构建的应用程序需要输入和其他交互控件来收集用户必要的信息。通过交互,我们指的是几乎可以在 Xamarin 应用程序中使用的所有 UI 控件。在这种情况下,甚至一个简单的筛选下拉控件,用于选择不同的视图视角,也会是一个交互控件,请求用户显示适当的数据或视角。
文本输入
文本输入字段是使用最频繁的一种输入字段类型。文本字段可以设计为单行文本或多行文本。文本字段的一个重要方面是,一旦在触摸设备上选中文本输入字段,虚拟键盘就会出现在屏幕上。在设计用户界面并在之后实现时,牢记这一点通常是一个好主意。
在 iOS 上,虽然UITextField提供了单行文本输入的机制,但UITextView可以用来创建可编辑的富文本内容。这两个控件都提供了诸如大写和纠错等选项。

UITextView 编辑和只读视图
此外,UITextView提供了可以将互联网地址转换为链接、地址转换为地图链接、电话号码转换为拨打电话的深度链接以及日期/时间值转换为日历事件项的检测器。
Android 文本输入字段与 iOS 平台上的类似。主要区别在于,在 Android 上,除了两个不同的控件外,只有EditText控件用于多行和单行文本输入。这是通过设置控件的InputType属性(或在 AXML 中的inputType属性)来实现的。除了文本格式之外,还可以设置其他输入范围,例如邮政地址、首字母大写、自动更正和句子开头大写。请注意,这些范围参数是位组合。另一个提供自动建议的专用控件是AutoCompleteTextView,开发者可以将ArrayAdapter分配为建议的源。
在 Windows Phone 上,TextBox是最常用的文本输入控件。它可以高度定制以满足之前提到的要求。此外,输入范围字段允许开发者控制用于输入值的虚拟键盘。例如,将范围设置为电话号码将显示仅包含数字的键盘。"AutoSuggestBox"、"PasswordBox"和"RichEditBox"是其他可以用于更专业场景的控件。
下拉选择
在每个平台上,可以使用专门的控件来使用下拉元素。在 iOS 上使用UIPickerView,而在 Android 上通过所谓的旋转器实现相同的功能。旋转器与其他内容驱动的控件非常相似,它们通过SpinnerAdapter进行填充。

iOS、Android 和 Windows Phone 上的下拉控件
除了旋转器控件外,简单的菜单对话框也可以用于用户的输入。Windows Runtime 为不同的选择用例提供了额外的专用控件,即ComboBox和ListView。
选项选择
与 HTML 表单中的单选按钮或复选框类似,每个平台都提供了相关的 UI 元素选项。在 Android 上,针对此场景的专用控件有复选框、单选按钮和切换按钮。从 Android 4.0(API 14)开始,也可以使用开关控制。除了这些控件之间的视觉差异外,行为是相同的。在 iOS 上,布尔数据类型的主要切换控件是开关。类似于 Android,Windows Phone 提供复选框、单选按钮和切换开关控件,以及选项选择和布尔类型。
注意
每个平台都有许多其他控件,每个控件都为不同的 UI 交互场景提供了特定的用途。Windows Runtime 和 Material Design 的用户体验指南是相应平台上的优秀资源。尽管苹果的人机界面设计文档没有像其他平台那样提供广泛的用户体验指南,但它们仍然是了解用户控件用例的宝贵资源。
手势
当为 Xamarin 目标平台开发时,你应该始终记住,将要运行应用程序的设备很可能具有触摸屏。
触摸屏设备除了经典的指针式手势(例如,点击、双击、滚动等)外,还提供了各种交互手势,这些手势有助于开发者创建能够以更自然方式与用户交互的界面。
|  | 点击 | 在大多数场景中,点击手势与指针设备的单次点击类似。它主要用于选择控件。 | 
|---|---|---|
| 长按 | 长按或点击并保持用于在 Windows Phone 上访问上下文菜单。在 Android 上,它用于项目选择。 | |
| 双击 | 双击通常用于放大/缩小控件。 | |
| 向下滑动 | 向下滑动或平移向下用于垂直滚动场景。此外,列表控件在 Windows 上支持滑动向下进行选择。它也常与“下拉刷新”实现一起使用。 | |
| 向右滑动 | 与向下滑动类似,向右滑动用于垂直滚动场景和同级导航场景。如果手势快速,则称为“轻扫”。 | |
| 向左滑动 | 这与其他平移手势相同。它也可以用于在 iOS 和 Windows Phone 10 上删除列表项。 | |
| 向上滑动 | 这是一种另一种平移手势。它还可以用于在 Android 应用程序上显示底部面板。 | |
| 点击与拖动 | 这通常用作与可拖动组件交互的主动手势。 | |
| 向外捏合 | 这用于活动画布应用模式。它用于放大视图。在 Windows 上,语义缩放控件就利用了这一点。 | |
| 向内捏合 | 这与向外捏合手势类似,用于缩小应用程序屏幕活动内容区域(例如,放大照片)。 | |
| 旋转 | 这是另一种在活动画布应用程序(例如,地图客户端)上使用的手势。它用于旋转当前视口。 | 
常见手势
虽然这些手势中的一些已经在 Xamarin 平台上的内置控件中实现,但可能存在需要使用它们来在应用程序中创建新的交互用例的场景。对于这类需求,可以在相应的框架中找到专门的实现。
在 iOS 平台上,手势识别器实现的起点是抽象类UIGestureRecognizer。UIKit中有许多手势识别器的实现,它们可以与代理实现结合使用。
在 Android 上,可以使用GestureDetector类和IOnGestureListener接口来提供各种手势事件和用户动作的实现。经典的交互事件,如平移手势和点击动作,可以通过任何Activity实现的OnTouchEvent方法访问。
在 Windows Phone 平台上,大多数默认控件都提供了与指针或触摸事件进行交互的功能,用于经典操作场景。然而,对于更复杂的手势,可以使用Windows.UI.Input命名空间中可用的GestureRecognizer类。
摘要
本章概述了 Xamarin 目标平台的设计理念以及设计模式。设计元素部分概述了设计师和开发者可用的主要控件和布局,同时提供了各种内容显示策略。还有关于交互式和现代用户界面设计的附加部分。
尽管每个平台都提供了自己的 UI 设计模式和指南,但在跨平台应用程序的设计工作中,主要目标是找到一个在本地外观和感觉与应用程序品牌设计之间的最佳折衷方案。
在下一章中,我们将讨论 Xamarin.Forms 框架,并利用这里概述的设计元素之间的相关性。
第八章. Xamarin.Forms
Xamarin.Forms 是 Xamarin 编译器技术的扩展模块;在目标平台原生 UI 组件之上的抽象层。本章将重点介绍 Xamarin.Forms 的各种功能和扩展选项,这些功能和选项帮助开发者创建可以编译成 Xamarin 项目的跨平台应用程序用户界面,从而提高代码共享的质量标准,并使跨平台应用程序开发项目更加易于管理和统一。本章分为以下几部分:
- 
内部机制 
- 
组件 
- 
扩展表单 
- 
模式和最佳实践 
内部机制
如前所述,Xamarin 作为一个跨平台开发框架,为开发者提供了创建依赖于并使用相同代码库的应用程序的工具集。在这些类型的实现中,共享的代码量与可管理性成正比。
在 Android 和 iOS 平台上,Xamarin.Forms 在 Mono 运行时之上和预编译.NET 堆栈之上添加了一个抽象层。这个抽象层的唯一责任是向 Xamarin 编译器提供必要的指令,以标准化 GUI 元素的代码或标记,以便在 Xamarin 应用程序中渲染原生控件。由于 Xamarin 的平台语言是 C#,可扩展应用程序标记语言(XAML)是首选的设计标记语言。Xamarin.Forms 为 Windows Store 应用程序提供了相同的抽象作为运行时库。
Xamarin.Forms 提供的抽象层利用了之前章节中(见第七章,视图元素)所展示的类似 UI 元素和布局模式。在这种情况下,Xamarin.Forms 只为所有三个平台提供通用的控件和视图,并省略了特定平台的 UI 元素。重要的是要理解,Xamarin.Forms 不是原生用户界面实现的替代品,而更多是在创建跨平台应用程序时构建的基础。

图 1:Xamarin.Forms 抽象层
Xamarin.Forms 不仅提供了一个统一的本地 UI 开发框架,还提供了与松散耦合 UI 开发相关的一些附加功能,例如数据绑定、依赖注入和信使基础设施。在某种程度上,这些功能使得在各个移动应用程序项目中使用的第三方 MVVM 库变得过时。
Xamarin.Forms 结构
Xamarin.Forms 库通过 NuGet 包进行分发,可以自由地包含在跨平台开发项目中。
虽然 iOS 的 NuGet 包没有显示任何依赖项,但 Android 和 Windows Phone 版本依赖于几个支持库(即 Windows Phone 的 WPToolKit;以及 Android 的几个设计和兼容性包)。
Xamarin.Forms.Core 库包含 UI 元素和必要的 XAML 声明,以及与数据绑定和类似操作相关的附加功能。此程序集可以包含在提供视图实现的平台特定项目中可移植类库项目中。作为回报,原生客户端项目应引用 Xamarin.Forms.Core 和 Xamarin.Forms 的平台特定程序集(例如,Xamarin.Forms.Platform.iOS)。Xamarin.Forms 平台库包含所谓的渲染器实现,负责使用原生控件渲染 Xamarin.Forms 元素。换句话说,这些平台程序集提供了原生元素与其 Xamarin.Forms 对应元素之间的映射。
项目结构
为了创建一个针对 iOS、Android 和/或 Windows Phone 8 的 Xamarin.Forms 应用程序项目,可以使用 跨平台 部分中的任何一个项目模板。虽然可移植库项目模板使用 PCL 创建 Xamarin.Forms 应用程序样板,但共享项目模板创建了一个共享项目,其中文件引用链接到原生客户端应用程序项目。

图 2:Xamarin.Forms 项目模板
小贴士
在较旧版本的 Xamarin 中,项目模板可以在 移动应用 部分找到。
一旦项目初始化,通过选择 Blank App (Xamarin.Forms Portable) 项目模板,创建的解决方案将包括四个项目,一个项目与输入的项目名称相同,另外三个具有平台后缀的平台特定项目。

图 3:Xamarin.Forms 解决方案主视图和项目范围
使用此项目模板的 Xamarin.Forms 的一项注意事项是,实际上由该框架支持的其他平台(例如,Windows Phone 8.1 和 Windows 10)并未包含在此多项目模板中。这些项目可以手动创建,并且可以使用 NuGet 包管理器添加 Xamarin.Forms 的 NuGet 包。还重要的是要提到,项目模板中引用的 NuGet 包可能不是 Xamarin.Forms 的最新版本,因此可以使用 NuGet 包管理器进行更新。

图 4:Xamarin.Forms 的最新 NuGet 包
如果查看可移植库中生成的代码 App.cs 和平台特定项目,实现模式立即变得明显。
Xamarin.Forms 的实现包含作为根节点的应用程序类实现。此应用程序由平台特定项目中生成的代码初始化和调用(类似于以下从 Xamarin.Forms iOS 应用程序示例中摘录的代码):
[Register("AppDelegate")]
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
        global::Xamarin.Forms.Forms.Init();
        LoadApplication(new App());
        return base.FinishedLaunching(app, options);
    }
}
模板样板中的应用程序初始化代码创建了一个包含单个标签的StackLayout元素的页面,并将此视图指定为主要页面:
public App()
{
    // The root page of your application
    MainPage = new ContentPage
    {
        Content = new StackLayout
        {
            VerticalOptions = LayoutOptions.Center,
            Children = {
                new Label {
                    XAlign = TextAlignment.Center,
                    Text = "Welcome to Xamarin Forms!"
                }
            }
        }
    };
}
如您所见,Xamarin.Forms 应用程序结构由不同布局配置中的控件组成,这些配置通过各种页面类型呈现。
组件
根据它们在视图层次结构中的位置和它们的用途,Xamarin.Forms 组件可以分为三个主要组。
页面
从概念上讲,页面是导航元素。它们提供了视图元素的通用层次组织,同时充当布局的容器。有各种可以继承和实现或使用 XAML 标记设计的页面类型。
标签页
在上一章讨论顶级导航页面时,我们提到了几个可以在顶级页面中提供水平导航的控件。使用 Xamarin.Forms,TabbedPage允许开发人员创建这些水平导航视图元素。TabbedPage在 Android 上生成标签操作栏和相关活动。在 Windows Phone 上,生成的视图包含枢轴控件。最后,在 iOS 上,生成的视图包含标签栏和相关视图。
TabbedPage包含其子导航页面(即,Children属性接受不同的页面实现),并且子元素的页面标题用作导航链接。
将上一章中的标签视图示例实现到我们的 TravelTrace 应用程序中看起来会类似于以下片段:
var tabbedPage = new TabbedPage();
tabbedPage.Children.Add(new ContentPage
{
    Title = "Recent",
    Content = new StackLayout
    {
        VerticalOptions = LayoutOptions.Center,
        Children = {
            new Label {
                HorizontalTextAlignment = TextAlignment.Center,
                Text = "Recent uploads page"
            }
        }
    }
});
// ...
// TODO: Add the other tab nav items
MainPage = tabbedPage;
同样的实现可以使用 XAML 和创建TabbedPage实现来完成:
<TabbedPage 
  x:Class="Xamarin.Master.TravelTrace.Views.MainTabView">
  <ContentPage Title="Recent" Icon="social.png">
    <StackLayout VerticalOptions="Center">
      <Label Text="Recent uploads page" HorizontalTextAlignment="Center"></Label>
    </StackLayout>
  </ContentPage>
  <ContentPage Title="Local" Icon="map.png">
    <StackLayout VerticalOptions="Center">
      <Label Text="Local landmarks page" HorizontalTextAlignment="Center"></Label>
    </StackLayout>
  </ContentPage>
  <ContentPage Title="Friends" Icon="people.png">
    <StackLayout VerticalOptions="Center">
      <Label Text="Friends related page" HorizontalTextAlignment="Center"></Label>
    </StackLayout>
  </ContentPage>
</TabbedPage>
将新创建的MainTabView类实例分配给App.cs中的MainPage会导致与代码实现相同的视图。

图 5:TabbedPage 视图
在这里提到这一点很重要,为TabbedPage实现中单个对等项提供的Icon属性仅适用于 iOS 平台。Xamarin 不支持标签和枢轴视图中的图标,并且这不是 Android 和 Windows Phone 接受的设计方法。
主详细页面
具有标签视图的示例满足我们设计的水平导航要求,但我们也需要为我们的 Android 应用程序提供一个导航抽屉和相关的菜单导航项。
MasterDetailPage提供了一个结构,其中主页面选择菜单可以在详细页面中发起导航请求。此外,如果Detail页面的内容封装在NavigationPage中,生成的视图将被添加到导航堆栈中,这样以前显示的页面就可以通过事件方法轻松地拉入主视图。为了包含一个额外的导航层和全局菜单,我们现在可以使用MasterDetailPage类来创建所需的导航结构。
实现的第一步是创建我们的主视图。在这种情况下,主视图将包括一个简单的列表视图,其中包含菜单和一个作为列表标题的配置文件显示。当列表视图内容项被选中时,我们可以将事件冒泡到 MasterDetailPage,或者将父页面作为参数传递给我们正在实现的菜单页面。
public NavigationMenuView(Page root)
{
    Icon = "toggle.png";
    InitializeComponent();
    ListViewMenu.ItemsSource = m_MenuItems = new List<Tuple<string, string, string>
    {
        new Tuple<string, string, string>("Profile", "profile", "profileicon.png"),
        new Tuple<string, string, string>("Map", "map", "mapicon.png"),
        new Tuple<string, string, string>("Settings", "settings", "settingsicon.png")
    };
    ListViewMenu.SelectedItem = m_MenuItems[0];
    ListViewMenu.ItemSelected += async (sender, e) => 
    {
        if(ListViewMenu.SelectedItem == null)
            return;
        // TODO: Implement the navigation strategy 
        Debug.WriteLine("Item selected {0}", 
          ((Tuple<string, string, string>)e.SelectedItem).Item2);
    };
}
在这个实现中,我们使用一个包含标签、标记和菜单项图标的三个参数的 Tuple。当然,实现一个包含这些数据值的类会更好。
现在我们可以通过设置 Master 和 Detail 属性来构建我们的 MasterDetailPage:
var masterDetailPage = new MasterDetailPage();
// Can select any of the behaviors: 
// Default, Popover, Split, SplitOnLandscape, SplitOnPortrait
masterDetailPage.MasterBehavior = MasterBehavior.Popover;
masterDetailPage.Master = new NavigationMenuView(masterDetailPage);
masterDetailPage.Detail = new NavigationPage(new ContentPage
{
    Title = "Detail Page",
    Content = new StackLayout
    {
        VerticalOptions = LayoutOptions.Center,
        Children = {
            new Label {
                HorizontalTextAlignment = TextAlignment.Center,
                Text = "Here is the Detail"
            }
        }
    }
});
MainPage = masterDetailPage;
MasterBehavior 可以根据平台进行调整。在这个例子中,我们将使用弹出行为,该行为在 Android 上显示一个弹出窗口和一个切换按钮在主应用栏中,并在其他平台上创建一个导航命令图标以打开弹出窗口。

图 6:Android 和 Windows Phone 上的导航弹出窗口
当使用 MasterDetailPage 时,重要的是要预测在 Xamarin.Forms 标记中做出的设计决策的结果,以确保针对目标平台的最终应用程序仍然遵循设计指南。
NavigationPage
NavigationPage 是 Page 类的最抽象实现。使用 NavigationPage 的主要目的是在应用程序上下文中创建一个导航堆栈。这种导航上下文在 Windows Phone 上是原生支持的。然而,其他平台不会为之前查看的页面创建堆栈。使用 NavigationPage,可以利用导航历史记录中的项目,并使用推送和弹出方法来操作堆栈。
CarouselPage
CarouselPage 是另一种用户可以使用滑动或轻扫手势在同级页面之间导航的水平导航实现。CarouselPage 与 Windows Phone 7 平台上的全景视图和枢轴控件非常相似,除了 CarouselPage 有严格的快照点(即,当自由滚动视图快照到控件或页面的边缘时)并且它没有无限循环的项目,与枢轴控件相比,而是有更线性的导航。在行为上,它类似于并使用与 Windows Runtime 中的 FlipView 控件相似的导航策略。
为了启动一个轮播式导航结构,可以使用 XAML 或代码隐藏。一个简单的包含三个内容页面实现的轮播视图如下所示:
<CarouselPage 
  x:Class="Xamarin.Master.TravelTrace.Views.GuidesView">
  <ContentPage Title="First Peer">
    <StackLayout  HeightRequest="50" VerticalOptions="Center" BackgroundColor="Silver">
      <Label Text="Content for the First Peer" HorizontalTextAlignment="Center"></Label>
    </StackLayout>
  </ContentPage>
  <ContentPage Title="Second Per">
    <StackLayout HeightRequest="50" VerticalOptions="Center" BackgroundColor="Gray">
      <Label Text="Content for the Second Peer" HorizontalTextAlignment="Center"></Label>
    </StackLayout>
  </ContentPage>
  <ContentPage Title="Third Peer">
    <StackLayout HeightRequest="50" VerticalOptions="Center" BackgroundColor="Silver">
      <Label Text="Content for the Third Peer" HorizontalTextAlignment="Center"></Label>
    </StackLayout>
  </ContentPage>
</CarouselPage>
结果视图将是一个容器,用于在同级之间通过触摸启动的水平导航。

图 7:轮播视图
ContentPage
ContentPage 是一种简单的页面实现,通常与之前描述的页面结构一起使用。它可以描述为实际的内容呈现器。在其他导航实现中的子视图通常由 ContentPage 实现。
为了设置用户界面中要可视化的内容,您可以使用Content属性,该属性接受一个视图对象的列表。布局元素通常用作ContentPage和其他用户控制的直接子元素,其他用户控制附加到这个视觉树中。
布局
布局是结构化设计元素,允许开发者使用各种策略组织 UI 控件。我们可以根据它们的类继承层次结构将布局分为两组:单视图和多视图。

图 8:布局类
单视图布局是基本布局实现的直接后代,并且它们只能显示单个视图项(它们也可以是视觉树的一个分支)。这个类别的例子包括ContentView、Frame和ScrollView。ContentView和Frame元素很少使用,在处理较少的内容元素和/或具有活动屏幕模式的应用程序(例如,绘图应用程序会使用一个带有绝对定位的单个画布实现;绘制的几何项将是画布的子项)时可能很有帮助。
另一方面,ScrollView是最受欢迎的控件之一,可以与另一个布局元素(如StackLayout)一起使用。当与StackLayout一起使用时,如果StackLayout的计算高度大于客户端区域,父控件ScrollView使子控件能够改变视口。尽管这并不常见,但ScrollView仍然可以与简单的控件(如Label或Image)一起使用。
例如,如果我们要在上一节创建的TabbedPage中实现主要内容,我们可以使用ScrollView来显示显示从 TravelTrace 服务器最近上传的项目。这个实现的标记将类似于以下片段:
  <ScrollView>
    <StackLayout x:Name="StackLayout">
      <Grid Padding="10" ColumnSpacing="4">…</Grid>
      <Grid Padding="10" ColumnSpacing="4">…</Grid>
      <!-- Omitted for clarity -->
    </StackLayout>
  </ScrollView>
它几乎会像滚动ListView一样显示:

图 9:ScrollView 可视化
注意
在正常情况下,处理大量数据项的长列表时,ListView应该是要使用的主要控件。此实现仅用于演示目的。
多页面布局类别包括AbsoluteLayout、Grid、RelativeLayout以及,如前例所示,StackLayout。每个布局都用于满足各种设计相关要求的具体场景。
Grid类似于 Windows Presentation Foundation 中的Grid,用于在网格结构中组织子元素。创建网格的第一步是定义ColumnDefinitions和RowDefinitions,它们描述了将要用于渲染元素的单元格。在此步骤之后,可以使用Grid的附加属性(如Grid.Row、Grid.Column、Grid.RowSpan和Grid.ColumnSpan)将视图元素添加到网格中。
使用前一个实现中的示例单元格,我们可以有一个经典的单元格视图,其中有两行文本和位于单元格最右侧部分的图像:
<Grid Padding="10" ColumnSpacing="4">
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="40" />
  </Grid.RowDefinitions>
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="*" />
    <ColumnDefinition Width="60" />
  </Grid.ColumnDefinitions>
  <Image Grid.RowSpan="2" Grid.Column="1" Source="mapicon.png" HeightRequest="40" WidthRequest="40"/>
  <Label Grid.Row="0" Grid.Column="0" Text="Item Title" FontSize="16"/>
  <Label Grid.Row="1" Grid.Column="0" Text="{Binding LongTextPlaceholder}" FontSize="14" />
</Grid>
AbsoluteLayout提供了一种渲染机制,其中子元素以浮动矩形的形式组织。放置几何形状(即LayoutBounds属性)定义了元素的 X 和 Y 坐标以及边界矩形的尺寸。LayoutBounds属性可以接受设备单位或比例单位。用于比例单位的观念类似于 HTML 布局中使用的"%"系统。这些值必须在 0-1 的范围内,以指定屏幕区域上的元素。AbsoluteLayoutFlags枚举可以用来定义遵循比例单位系统或其他方式(例如,PositionProportional、HeightProportional、SizeProportional或All)的边界矩形值。
RelativeLayout在概念上类似于 Android 和 Windows 10 平台上的相对布局。它也使用与 iOS 自动布局实现类似的约束机制。在相对布局中,元素可以定位在一个类似于绝对布局的边界矩形内。然而,这个边界矩形的值是以父元素(RelativeToParent)或视觉树中的另一个控件(RelativeToView)为参考定义的。开发者还被允许使用常数而不参考另一个控件。
在相对布局中,如果布局是在代码背后创建的,则使用 lambda 表达式或匿名函数定义约束。例如,为了将一个图像元素添加到页面中心,大小为(100,100),我们会使用RelativeToParent约束:
relativeLayout.Children.Add(image,
    Constraint.RelativeToParent(parent => parent.Width/2 - 50),
    Constraint.RelativeToParent(parent => parent.Height/2 - 50),
    Constraint.Constant(100), Constraint.Constant(100));
如果我们在图片下方 10 个单位处插入一个标签,它看起来将如下所示:
relativeLayout.Children.Add(label,
    Constraint.RelativeToParent(parent => parent.Width/2 - 100),
    Constraint.RelativeToView(image, (parent, view) =>
    {
        // Here view is referring to the other relative control
        return view.Y + view.Height + 10;
    }),
    Constraint.Constant(200),
    Constraint.Constant(100));
结果将如下所示:

图 10:相对布局
可以使用 XAML 中的标记扩展进行类似的实现。尽管约束表达式选项受因子和常数值的限制(即使用相对布局,因子乘以所选属性的值,常量用于偏移值),但在数据绑定场景中它可能非常有用。
<RelativeLayout x:Name="relativeLayout">
  <Image x:Name="Image" Source="icon.png" HeightRequest="100" WidthRequest="100"
          RelativeLayout.XConstraint=
          "{ConstraintExpression Type=RelativeToParent, 
                                  Property=Width, 
                                  Factor=0.5, 
                                  Constant=-50}"
          RelativeLayout.YConstraint=
          "{ConstraintExpression Type=RelativeToParent, 
                                  Property=Height, 
                                  Factor=0.5, 
                                  Constant=-50}" />
  <Label Text="Hello Relative Layout!" HeightRequest="100" WidthRequest="200" HorizontalTextAlignment="Center"
          RelativeLayout.XConstraint=
          "{ConstraintExpression Type=RelativeToParent, 
                                  Property=Width, 
                                  Factor=0.5, 
                                  Constant=-100}"
          RelativeLayout.YConstraint=
          "{ConstraintExpression Type=RelativeToView,
                                  ElementName=Image,
                                  Property=Y, 
                                  Constant=110}"/>
</RelativeLayout>
最后,StackLayout与 Windows 平台上的StackPanel和 Android 平台上的LinearLayout类似,提供了一种流式布局,其中子视图(即控件)会根据设置的朝向和元素的计算或请求的尺寸自动排列。
视图
在 Xamarin.Forms 中,用户界面控件被称为视图。视图是 Xamarin 目标平台上的控件或小部件的抽象,每个视图都在相应的平台上使用本地控件进行渲染。
对于文本相关场景,有三个控件:Editor、Entry 和 Label。Editor 和 Entry 视图分别向用户界面提供多行和单行编辑功能。另一方面,标签视图可以用作只读控件,适用于任何场景。
对于下拉相关场景,可以使用 Picker 视图。更专业的拾取器实现包括 TimePicker 和 DatePicker。Stepper 和 Slider 是其他可以提供约束值的视图,例如一定范围内的整数。对于选项场景,唯一可用的控件是 Switch 视图。Switch 视图在 Android 和 iOS 上渲染为 Switch 控件,在 Windows 上渲染为 ToggleButton。
对于进程反馈实现,有两个视图可供选择,即 ProgressBar 和 ActivityIndicator。ProgressBar 提供一个确定性的进度指示器,而 ActivityIndicator 在目标平台上渲染为一个不确定性的进度环。
对于与网络资源相关的场景,可以使用 WebView。与目标平台上的嵌入式网页原生控件类似,WebView 可以用来显示本地(即,由应用程序资源或文本值构建的网页元素)或远程网页。它提供了对显示的网页文档的导航堆栈和导航事件的访问。
对于集合视图,Xamarin.Forms 中有两个主要控件:ListView 和 TableView。ListView 无疑是最专业的控件,用于显示内容项的集合。它支持数据绑定场景以及更专业的操作,如下拉刷新、上下文相关命令和选择。另一方面,TableView 用于内容项更异质化的场景,而不是数据绑定源,需要固定 UI 元素声明。它可以用于选择菜单显示、配置值或作为输入表单。
ListView 和 TableView 都由单元格组成。单元格是用于在这些集合视图中渲染内容元素的视觉模板。虽然 TableView 通常与默认模板(如 SwitchCell 和 EntryCell)相关联,这些模板用于在表格中创建表单元素,但 ListView 通常使用 ViewCell 的模板实现。对于更简单的实现场景,也可以使用内置的单元格实现,如 TextCell 和 ImageCell,与 ListView 控件一起使用。
小贴士
对于 TableView 集合控件,iOS 平台目前不支持 HasUnevenRows 属性和单元格的自动布局。这是一个已知的平台限制,最近已针对 ListView 控件进行了修复。开发者可以选择为 TableView 定义一个固定的 RowHeight,或者为每个单元格定义一个 Height 值。
为了演示ListView的使用,我们可以利用之前的实现,其中我们使用了StackLayout和ScrollView。在前一个场景中,我们创建了硬编码的 UI 元素,这些元素被定义为Grid项。在这个实现中,让我们假设我们有一个可以设置为ListView数据提供者的数据源:
RecentUploadsList.ItemsSource = new List<Tuple<string, string, string>>
{
    new Tuple<string, string, string>("Sarajevo trip on 04.10", longText, "profileicon.png"),
    new Tuple<string, string, string>("Istanbul trip on 23.09", longText, "mapicon.png"),
    new Tuple<string, string, string>("Rome trip on 12.09", longText, "settingsicon.png"),
    new Tuple<string, string, string>("Sarajevo trip on 04.10", longText, "profileicon.png"),
    new Tuple<string, string, string>("Istanbul trip on 23.09", longText, "mapicon.png"),
    new Tuple<string, string, string>("Rome trip on 12.09", longText, "settingsicon.png"),
    new Tuple<string, string, string>("Sarajevo trip on 04.10", longText, "profileicon.png"),
    new Tuple<string, string, string>("Istanbul trip on 23.09", longText, "mapicon.png"),
    new Tuple<string, string, string>("Rome trip on 12.09", longText, "settingsicon.png")
};
在此提供者中,我们使用一个包含三个值的Tuple,为内容条目提供显示名称、描述和图像值。
注意
Tuple值通过 Item1、Item2…属性进行访问。
ListView可以包含三个视觉模板,分别定义集合视图的相应部分:HeaderTemplate、FooterTemplate和ItemTemplate。也可以直接使用视图元素来设置标题和页脚:
<ListView BackgroundColor="Gray" SeparatorColor="Black" HasUnevenRows="true" x:Name="RecentUploadsList" >
  <ListView.Header>
    <Label TranslationX="10" Text="Recent Uploads"></Label>
  </ListView.Header>
  <!--<ListView.ItemTemplate> TODO: Insert DataTemplate </ListView.ItemTemplate>-->
</ListView>
ItemTemplate定义了内容元素在集合视图中如何渲染。如果未定义ItemTemplate,列表渲染器将尝试将内容元素转换为字符串,并以TextCells的形式显示它们。从先前的示例中重用网格实现,我们可以为ListView的ItemTemplate属性定义DataTemplate:
<ListView.ItemTemplate>
  <DataTemplate>
    <ViewCell>
        <Grid Padding="10" ColumnSpacing="4">
          <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="40" />
          </Grid.RowDefinitions>
          <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="60" />
          </Grid.ColumnDefinitions>
          <Image Grid.RowSpan="2" Grid.Column="1" Source="{Binding Item3}" 
                  HeightRequest="40" WidthRequest="60"/>
          <Label Grid.Row="0" Grid.Column="0" 
                  Text="{Binding Item1}" FontSize="16" />
          <Label Grid.Row="1" Grid.Column="0" 
                  Text="{Binding Item2}" FontSize="14" />
        </Grid>
    </ViewCell>
  </DataTemplate>
</ListView.ItemTemplate>
此实现将在一个可滚动的列表容器中显示,类似于以下截图:

图 11:具有项目源的ListView
为了实现与上下文相关的功能,可以编辑项目数据模板、视图单元格,以包含上下文菜单元素。也可以在代码隐藏文件中修改视图单元格。
以下 XAML 代码片段可以用来创建两个上下文菜单操作:Favourite和Remove:
<ViewCell.ContextActions>
  <MenuItem Text="Favourite" Clicked="OnFavouriteClicked" CommandParameter="{Binding .}">
  </MenuItem>
  <MenuItem Text="Remove" IsDestructive="True" Clicked="OnRemoveClicked" CommandParameter="{Binding .}">
  </MenuItem>
</ViewCell.ContextActions>
注意,Remove命令被标记为破坏性操作。IsDestructive标志用于在 iOS 上创建滑动删除行为。在其他平台上,破坏性行为的表现与其他命令类似。

图 12:上下文菜单操作
ListView还有一个名为IsPullToRefreshEnabled的标志。此属性可用于支持下拉刷新行为。RefreshCommand可用于绑定刷新列表所需的操作。
扩展形式
尽管 Xamarin.Forms 框架提供了一套广泛的可自定义 UI 元素,但在某些场景中,您可能希望更改某些控件的外观或行为。此外,有时提供跨应用的自定义方案可以提供一致性并减少冗余。Xamarin.Forms 中使用的 XAML 标记基础设施提供了各种自定义实现场景。
样式
在实现某些 UI 模式时,视图元素必须相互独立声明,同时它们必须携带相同的设计属性,如字体、布局属性、颜色等。在这种情况下,可以使用样式来组织和重用元素属性。
使用 ListView,唯一定义的视图容器将是项目数据模板,从数据源加载的内容项将使用相同的模板进行渲染。然而,如果视图需求是使用 Grid、StackLayout 或 TableView,则每个视图项都必须单独定义。
例如,使用 TableView 控件为 Xamarin.Forms 应用程序创建设置视图可能会变得相当繁琐。在此实现中,如果我们不能使用标准单元格视图,如 EntryCell 或 SwitchCell,因为要求,由于要求,标记变得更加冗余,每个控件都必须声明类似的字体和颜色,这些字体和颜色构成了应用程序的主题。

图 13:TableView 用于设置视图
在此实现中,自定义单元格视图被用来为每个设置创建描述元素。如果我们查看标记文件,我们可以看到每个文本元素的重复样式:
<TableView Intent="Settings" HasUnevenRows="True">
  <TableRoot>
    <TableSection Title="Location">
      <ViewCell>
        <StackLayout Orientation="Vertical" Padding="10">
          <StackLayout Orientation="Horizontal">
            <Label TextColor="White" FontSize="24"
                   VerticalTextAlignment="Center" 
                   HorizontalOptions="StartAndExpand" 
                   Text="Use Location" />
            <Switch IsToggled="True"></Switch>
          </StackLayout>
          <Label TextColor="Silver" FontSize="20"
                 VerticalTextAlignment="Center" 
                 HorizontalOptions="StartAndExpand"  
                 Text="Use location to tag photos and notes. Location can be used for notifications as well">
          </Label>
        </StackLayout>
      </ViewCell>
      <ViewCell>
        <StackLayout Orientation="Vertical" Padding="10">
          <StackLayout Orientation="Horizontal">
            <Label TextColor="White" FontSize="24"
                   VerticalTextAlignment="Center" 
                   HorizontalOptions="StartAndExpand" 
                   Text="Create Geofences" />
            <Switch IsToggled="True"></Switch>
          </StackLayout>
          <Label TextColor="Silver" FontSize="20"
                  VerticalTextAlignment="Center" 
                  HorizontalOptions="StartAndExpand" 
                  Text="Create geofences to give notifications about landmarks.">
          </Label>
        </StackLayout>
      </ViewCell>
    </TableSection>
    <!-- Additional sections were removed for simplicity -->
  </TableRoot>
</TableView>
在这个例子中,每个标签至少定义了 TextColor、FontSize、VerticalTextAlignment 和 HorizontalOptions。设置标签和描述元素有一个模式,然而,垂直和水平对齐选项适用于所有文本元素。
初始时,我们可以通过创建一个将应用于所有 Label 元素的隐式样式来简化标记。隐式样式不定义资源键,因此它们应用于所有目标控件,例如 TargetType:
<ContentPage.Resources>
  <ResourceDictionary>
    <Style TargetType="Label">
      <Setter Property="HorizontalOptions" Value="StartAndExpand" />
      <Setter Property="VerticalTextAlignment" Value="Center" />
    </Style>
  </ResourceDictionary>
</ContentPage.Resources>
我们现在可以创建额外的样式来设置项目标签和描述:
<Style x:Key="SettingLabel" TargetType="Label">
  <Setter Property="FontSize" Value="24"></Setter>
  <Setter Property="TextColor" Value="White"></Setter>
</Style>
<Style x:Key="SettingDescription" TargetType="Label">
  <Setter Property="FontSize" Value="20"></Setter>
  <Setter Property="TextColor" Value="Silver"></Setter>
</Style>
然而,这并没有像我们预期的那样工作。结果表明,隐式样式被更具体的样式描述覆盖了。重要的是要认识到,为同一目标控件定义的样式之间没有隐式级联。XAML 不是 HTML/CSS。

图 14:隐式样式被指定的样式覆盖
为了创建级联方案,我们需要将 SettingLabel 和 SettingDescription 样式基于初始隐式样式。为此,我们需要为我们的基本样式定义一个键,并在派生样式声明中引用此基本样式:
<ContentPage.Resources>
  <ResourceDictionary>
    <Style x:Key="BaseLabelStyle" TargetType="Label">
      <Setter Property="HorizontalOptions" Value="StartAndExpand" />
      <Setter Property="VerticalTextAlignment" Value="Center" />
    </Style>
 <Style x:Key="SettingLabel" BaseResourceKey="BaseLabelStyle" TargetType="Label">
      <Setter Property="FontSize" Value="24"></Setter>
      <Setter Property="TextColor" Value="White"></Setter>
    </Style>
 <Style x:Key="SettingDescription" BasedOn="{StaticResource BaseLabelStyle}" TargetType="Label">
      <Setter Property="FontSize" Value="20"></Setter>
      <Setter Property="TextColor" Value="Silver"></Setter>
    </Style>
  </ResourceDictionary>
</ContentPage.Resources>
注意,SettingDescription 样式使用 BasedOn 声明(类似于 WPF 实现),而 SettingLabel 使用 BaseResourceKey 属性。这两个引用都可以在 Xamarin.Forms 实现中使用。
触发器和行为
有时,实现需要根据相同或任何其他控件属性或数据的变化,以及某些事件(例如,根据数据输入值的变化禁用某个控件)来更改控件的风格或行为。在正常情况下,实现利用数据绑定,其中数据更改事件被路由到表示者,表示者更改视图,提供了一个简单的解决方案。然而,如果 UI 事件应该触发另一个 UI 更改,数据绑定的成本将是一个开销。相反,Xamarin.Forms 标记提供了触发器和行为,这些触发器和行为为内置控件增加了复杂性。
例如,我们为我们的应用程序之前创建的设置视图需要某些业务规则。第一个设置值,UserLocation,是 UseGeofences 设置的依赖项。换句话说,在技术上,不使用位置服务就无法创建地理围栏。对于这个特定场景,我们可以从IsToggled值创建一个数据绑定:
<Switch x:Name="SwitchUseGeofences" IsToggled="True"
              IsEnabled="{Binding Source={x:Reference SwitchUserLocation}, Path=IsToggled}">
前面的实现按预期工作,因为IsToggled和IsEnabled的值都使用Boolean作为值类型。如果我们需要更改目标 UI 元素的任何其他属性,我们就必须实现一个值转换器。此外,多个属性更改需要多个绑定。
触发器为这类场景提供了一个简单的解决方案。有四种类型的触发器可以用来启动设置器动作或自定义触发器动作的实现。属性触发器用于根据相同控件属性的值在用户控件上创建一个视觉状态。数据触发器以类似的方式使用,但在这个情况下,触发的原因由数据绑定定义。事件触发器绑定到用户控件事件,多触发器可以包含并调用依赖于多个条件的动作。
在这个情况下,可以从上一个示例中的相同场景使用DataTrigger来实现。在迭代场景时,实现可以设置关联描述标签的启用和文本颜色属性:
<ViewCell>
  <StackLayout Orientation="Vertical" Padding="10">
    <StackLayout Orientation="Horizontal">
      <Label Text="Create Geofences" Style="{StaticResource SettingLabel}" />
      <Switch x:Name="SwitchUseGeofences" IsToggled="True"
              IsEnabled="{Binding Source={x:Reference SwitchUserLocation}, Path=IsToggled}">
        <Switch.Triggers>
 <DataTrigger TargetType="Switch" Binding="{Binding Source={x:Reference SwitchUserLocation}, Path=IsToggled}" Value="True">
 <Setter Property="IsEnabled" Value="False"></Setter>
 </DataTrigger>
        </Switch.Triggers>
      </Switch>
    </StackLayout>
    <Label Text="Create geofences to give notifications about landmarks."
            Style="{StaticResource SettingDescription}">
      <Label.Triggers>
 <DataTrigger TargetType="Label" Binding="{Binding Source={x:Reference SwitchUserLocation}, Path=IsToggled}" Value="False">
 <Setter Property="IsEnabled" Value="False"></Setter>
 <Setter Property="TextColor" Value="Transparent"></Setter>
 </DataTrigger>
 </Label.Triggers>
    </Label>
  </StackLayout>
</ViewCell>
让我们再实现一个当主控件禁用时触发的通知,警告用户其他设置已被禁用。为此实现,我们需要一个事件触发器和触发器动作实现。触发器动作实现包括实现TriggerAction<T>类和虚拟的Invoke方法:(请参阅依赖注入部分以了解INotificationService的实现)
public class WarningTriggerAction : TriggerAction<Switch>
{
    public string Message { get; set; }
    protected override void Invoke(Switch sender)
    {
        if(!sender.IsToggled) 
            DependencyService.Get<INotificationService>()
                .Notify(Message);
    }
}
然后,我们需要在页面标记的根节点中声明包含实现的命名空间:
最后,我们可以在主设置控件上添加事件触发器:
<Switch x:Name="SwitchUserLocation" IsToggled="True">
  <Switch.Triggers>
    <EventTrigger Event="Toggled">
      <components:WarningTriggerAction Message="Disabling this setting will disable other values" />
    </EventTrigger>
  </Switch.Triggers>
</Switch>

图 15:使用 EventTrigger 触发的通知
如果我们希望这个触发器应用于多个控件(例如,示例中的通知设置部分),我们可以为主要的设置值创建一个新的样式,并将触发器添加到样式声明中:
<Style x:Key="SectionToggleSwitch" TargetType="Switch">
  <Style.Triggers>
    <EventTrigger Event="Toggled">
      <components:WarningTriggerAction Message="Disabling this setting will disable other values" />
    </EventTrigger>
  </Style.Triggers>
</Style>
使用Switch控件的实现同样可以达到相同的结果。行为是一种更通用的扩展机制,允许开发者扩展现有的用户控件,而无需创建这些控件的派生类。
例如,如果我们使用相同的场景(即,当开关控制被切换关闭时,应向用户显示一个通知窗口),我们需要为Switch视图实现一个带有类型参数的基类Behavior:
public class SectionSwitchAlertBehavior : Behavior<Switch>
{
    public string Message { get; set; }
    protected override void OnAttachedTo(Switch control)
    {
        control.Toggled += ControlOnToggled;
        base.OnAttachedTo(control);
    }
    protected override void OnDetachingFrom(Switch control)
    {
        control.Toggled -= ControlOnToggled;
        base.OnDetachingFrom(control);
    }
    private void ControlOnToggled(object sender, ToggledEventArgs toggledEventArgs)
    {
        if (!toggledEventArgs.Value && !string.IsNullOrWhiteSpace(Message))
        {
            DependencyService.Get<INotificationService>().Notify(Message);
        }
    }
}
在自定义行为实现类中,OnAttachedTo方法用作初始化函数,其中可以自定义控件。同样,OnDetachingFrom用于清理自定义设置以及可能附加到控件上的任何现有事件处理器。尽管技术上可行,但使用行为修改绑定上下文并不建议。
自定义行为可以包含在针对同一类型控件的样式中,或者通过在特定控件中添加内联标记元素来实现:
<Style x:Key="SectionToggleSwitch" TargetType="Switch">
  <Style.Behaviors>
 <components:SectionSwitchAlertBehavior Message="Disabling this setting will disable other values" />
  </Style.Behaviors>
</Style>
自定义渲染器
Xamarin.Forms 为开发者提供了一个统一的标记和实现框架,用于创建所有 Xamarin 目标平台的本地 UI 视图。提供的 UI 元素的抽象被框架用来渲染本地控件。类似于 Xamarin.Forms 解决方案的解剖结构,Xamarin.Forms 平台中的每个视图/控件都是一个复合实现。虽然抽象的控制逻辑的行为可以在便携式类库中实现和派生,但与各个平台相关的控件渲染器是由特定平台库实现的。

图 16:自定义渲染器实现
为了自定义一个控件,必须首先为抽象控件创建一个派生类。在此实现之后,自定义控件可以通过clr-namespace声明(类似于TriggerAction和Behaviors)进行引用,并在视图标记中使用。
在这个阶段,控件的自定义实现将使用基类的默认渲染器。为了改变特定平台上本地控件渲染的方式,我们需要提供一个自定义渲染器实现,并使用同一平台的ExportRenderer属性进行注册。
自定义渲染器提供了一种强大的方式,可以自定义 Xamarin.Forms 中常见视图实现如何在特定平台视图中呈现。
模式和最佳实践
在本节中,我们将讨论开发者在开发 Xamarin.Forms 应用程序时通常会采用的几种实现模式和工具。消息和依赖注入功能将在第九章可重用 UI 模式中进一步讨论。
消息基础设施
在模型-视图-视图模型(MVVM)或模型-视图-演示者(MVP)模式的理想实现中,每个屏幕都是自包含的;视图、模型和缓解组件的屏幕模块通过各种通信渠道相互通信。
然而,在复杂的应用程序中,有时需要在这些自包含元素之间建立通信渠道,因为某个屏幕上的操作结果应该传播到应用程序的其他无关部分,这些部分对这一操作的结果有共同兴趣。作为解决这个问题的一种方法,在 MVVM 框架如 MVVMCross、Prism 或 MVVM Light 中,通常可以看到事件聚合器模式的实现,提供了一种松散耦合、多播启用发布者/订阅者消息基础设施。事件聚合器可以描述为事件中心,它接收多种类型的强类型消息并将这些消息传递给多个订阅者。
在 Xamarin.Forms 中,事件聚合器被称为MessagingCenter。它公开了三组方法:Subscribe、Unsubscribe和Send。Subscribe和Unsubscribe方法由事件观察者使用,而Send方法由发布者使用。
在这个范例中,订阅者负责提供发送者的实例和/或类型以及预期的消息类型(即定义消息的简单文本参数)。消息类型或名称是消息的标识符,与消息签名(发送者类型和参数类型)一起,构成了订阅者的决策标准。最后,提供的最后一个参数是回调委托,它可以有发送者,以及可能的事件参数:
MessagingCenter.Subscribe<MyViewModel, MyMessageContract>(this, "MyMessageName",
    (sender, data) =>
    {
        // TODO: Use the provided data and the sender
    });
// or
//MessagingCenter.Subscribe(this, "MyMessageName", (sender) => { }, myViewModelInstance);
发布者负责提供具有相同消息名称和签名的消息。在发布者一侧,消息签名由消息名称和消息参数组成:
MessagingCenter.Send(this, "MyMessageName", new MyMessageContract
{
    // TODO: Pass on the required data.
});
MessagingCenter 可以证明是非常有用的,为 Xamarin.Forms 应用程序中的架构问题(特别是涉及关注点分离的场景)提供简单的解决方案/折衷方案,并在组件之间创建解耦的通信渠道。
依赖注入
如前所述,使用可移植类库(PCLs)实现通用跨平台库的最大缺点之一是平台特定功能无法直接访问,因为这些平台依赖的模块无法由这些库引用。
解决这个问题的最有效和优雅的解决方案之一是使用依赖注入(也称为 IoC - 控制反转)。使用依赖注入,特定平台的函数应该被抽象到隔离的接口中,这些接口可以后来用于访问通过提供的依赖容器注入的实现模块。
DependencyService在 Xamarin.Forms 中允许应用程序通过抽象接口使用特定平台的实现。
在一个常见的场景中,第一步是定义将要被通用应用程序层使用的抽象(在通用可移植表单库中)。
为了演示,让我们实现一个模块,该模块使用原生消息方法来显示用户的通知:
public interface INotificationService
{
    void Notify(string message);
}
现在我们可以在特定平台的项目中实现这个接口。在 Xamarin.Android 项目中,我们可以使用 toast 通知来实现:
[assembly:Xamarin.Forms.Dependency(typeof(NotificationService))]
namespace Xamarin.Master.TravelTrace.Droid.Modules
{
    public class NotificationService : INotificationService
    {
        public void Notify(string message)
        {
            var toast = Toast.MakeText(Application.Context, message, ToastLength.Long);
            toast.Show();
        }
    }
}
对于 iOS 平台,我们可以创建一个本地通知消息,并使用共享应用程序基础设施来展示它。然而,前台应用程序的本地通知会自动消失(只有 UI 级别还可以实现一个事件代理来处理接收到的通知事件并显示一个警告)。因此,我们将使用UIAlertController类,并通过当前窗口来展示:
[assembly: Xamarin.Forms.Dependency(typeof(NotificationService))]
namespace Xamarin.Master.TravelTrace.iOS.Modules
{
    public class NotificationService : INotificationService
    {
        public void Notify(string message)
        {
            //
            // This will not fire for the foreground application
            //UILocalNotification localNotification = new 
            // UILocalNotification();
            // localNotification.FireDate = 
            // NSDate.Now.AddSeconds(2);
            //localNotification.AlertBody = message;
            //localNotification.TimeZone = 
            // NSTimeZone.SystemTimeZone;
            // UIApplication.SharedApplication
            // .PresentLocalNotificationNow(localNotification);
            // UIApplication.SharedApplication
            // .ScheduleLocalNotification(localNotification);
            //Create Alert
            var okAlertController = UIAlertController.Create ("Notification", message, UIAlertControllerStyle.Alert);
            //Add Action
            okAlertController.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
            if (UIApplication.SharedApplication.KeyWindow != null)
                UIApplication.SharedApplication.KeyWindow.RootViewController.PresentViewController(okAlertController, true, null);
        }
    }
}
最后,对于 Windows Phone 平台,我们只能使用本地 toast 通知与 Windows Phone 8.1 和 Windows 10 移动设备上当前运行的应用程序。对于其他版本,类似于 iOS 场景,前台应用程序不允许使用本地 toast 通知。因此,我们可以使用MessageBox类实现一个更简单的通知对话框:
[assembly: Xamarin.Forms.Dependency(typeof(NotificationService))]
namespace Xamarin.Master.TravelTrace.WinPhone.Modules
{
    public class NotificationService : INotificationService
    {
        public void Notify(string message)
        {
            MessageBox.Show(message);
        }
    }
}
为了在实现 Xamarin.Forms 应用程序的可移植类库中使用INotificationService接口,我们需要解析接口以创建一个平台适当实现实例:
DependencyService.Get<INotificationService>().Notify("Hello Xamarin.Forms!");
重要的是要注意,在这个示例实现中,使用了Dependency程序集属性来注册平台相关的实现类。也可以使用DependencyService的Register方法来创建依赖容器:
Xamarin.Forms.DependencyService.Register<INotificationService, NotificationService>();
Register方法必须在 Xamarin.Forms 初始化(即Forms.Init方法)之后以及任何依赖模块加载之前调用。
共享项目与可移植项目
Xamarin.Forms 扩展引入了两种类型的跨项目解决方案模板。每个模板都包含特定平台的项目以及一个通用项目,用于实现这些原生应用程序的平台无关组件。
在之前的例子中,我们使用了 PCL 项目模板,该模板创建三个特定平台的项目,每个项目都引用一个跨平台的便携式类库。特定平台的项目将应用程序初始化委托给便携式类库,该类库初始化 Xamarin.Forms 并渲染使用 Xamarin.Forms 实现的页面。
第二个项目模板创建一个共享项目,该项目被包含并编译到特定平台的项目中。在这种情况下,由于我们实际上并没有处理一个无平台实现(即,共享项目中的实现直接编译到引用的项目中),因此开发者可以自由使用平台特定功能,前提是编译条件用于适当的平台。
展示两种方法之间差异的最简单方法是将上一节中的通知服务重新实现,不使用依赖注入。在之前的例子中,我们需要创建一个用于在常见视图中使用的通知功能抽象,并在运行时从特定平台的项目中注入实现。在共享项目的情况下,我们可以使用条件编译来实现相同的功能:
public class NotificationService
{
    public void Notify(string message)
    {
        if (!string.IsNullOrWhiteSpace(message))
#if __IOS__
    var okAlertController = UIAlertController.Create("Notification", message, UIAlertControllerStyle.Alert);
    okAlertController.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
    if (UIApplication.SharedApplication.KeyWindow != null)
        UIApplication.SharedApplication.KeyWindow.RootViewController
            .PresentViewController(okAlertController, true, null);
#elif __ANDROID__
    var toast = Toast.MakeText(Application.Context, message, ToastLength.Long);
    toast.Show();
#elif WINDOWS_PHONE
            MessageBox.Show(message);
#endif
    }
}
在这种情况下,每个平台编译都使用函数的特定部分。我们还可以使用其他类型的抽象和部分类或方法,根据场景需求创建优雅的实现。
平台特定微调
尽管如此,或者正因为 Xamarin.Forms 试图提供一个统一的实现层并将其转换为原生控件,有时开发者会面临为特定平台实现修改的挑战。这些修改从小到字体大小(因为设备和平台依赖的像素度量)或背景颜色等小改动,到更系统性的问题,如 iOS 平台上TableViews没有自动布局实现。处理这类情况有各种方法,而Device类通常是这些解决方案的访问点。
当处理常见的排版控件,如Label或Entry字段时,最简单的方法是使用Device.Styles类中可用的内置样式来符合特定设备的布局或无障碍性要求。有几个样式元素,如BodyStyle、SubtitleStyle和CaptionStyle,可以用来解决常见的实现问题。这个类中的样式元素在运行时为当前平台/设备计算,因此当处理标记而不是代码时,必须通过DynamicResource XAML 标记扩展来引用。
使用TitleStyle实现的简单标签如下:
var mylabel = new Label
{
    Text = "Text for my Title",
    Style = Device.Styles.TitleStyle
};
这也可以在标记文件中声明如下:
<Label x:Name="MyLabel" Text="Text for my Title" Style="{DynamicResource TitleStyle}" />
另一个有用的特定平台字体相关实用工具是 NamedSize 枚举。NamedSize 枚举可以与 Device.GetNamedSize 方法一起使用,以在目标平台中选择文本字段最合适的字体大小。枚举提供了四种内置选项,适用于不同场景:
var mylabel = new Label {Text = "Text for my Title"};
// A Large font size, for titles or other important text elements
mylabel.FontSize = Device.GetNamedSize(NamedSize.Large, typeof (Label));
内置转换器也可以用于在 XAML 标记中包含字体大小:
<Label x:Name="MyLabel" Text="Text for my Title" FontSize="Large" />
对于更通用的实现需求,Device.Idiom 和 Device.OS 分别提供了有关设备类型(桌面、手机、平板电脑等)和设备操作系统(Android、iOS、Windows 或 Windows Phone)的有价值的目标平台信息。
注意
目前,无法使用 Device.OS 属性区分 Windows Phone 8.1 和 Windows Phone Silverlight 版本。可以使用条件编译作为这种区分的替代方案。
此外,Device.OnPlatform 函数及其 XAML 扩展对应函数可以帮助开发者实现特定平台样式。OnPlatform 函数为每个平台使用三个值,并根据 Device.OS 属性返回适当的值。
使用 OnPlatform 函数可视化标签将类似于以下代码片段:
var mylabel = new Label {Text = "Text for my Title"};
mylabel.FontSize = Device.OnPlatform<double>(
     Android: 24, WinPhone: 24, iOS: 18);
或者,使用 XAML 标记扩展,它将看起来像这样:
<Label x:Name="MyLabel" Text="Text for my Title">
  <Label.FontSize>
    <OnPlatform x:TypeArguments="x:Double" Android="24" WinPhone="24" iOS="16"/>
  </Label.FontSize>
</Label>
Device.OnPlatform 函数还有一个重载,可以根据当前操作系统执行某些操作。
摘要
简而言之,Xamarin.Forms 提供了一套工具集,用于增加特定平台项目之间的代码共享,并为开发者提供在为这些项目开发 UI 组件时的统一体验。总的来说,Xamarin.Forms 框架证明是不可或缺的,尤其是在跨平台实现中,平台依赖性功能需求最小。
这个统一的抽象层负责渲染特定平台的 UI 控件,并为用户提供原生体验。此层还可以通过各种功能和模式进行扩展,其中一些在本章中已讨论。
在下一章中,我们将重点关注更多可重用视图元素和实现模式。Xamarin.Forms 将再次在此背景下被引用。
第九章:可重用 UI 模式
在本章中,我们将讨论在跨平台项目中重用视觉资源(即文本和媒体资源)的策略和模式。此外,将从本地化的角度迭代解释可重用资源。最后,将分析和演示关于模型-视图-控制器和模型-视图-视图模型模式的高级软件架构主题。本章分为以下部分:
- 
视觉资源 
- 
本地化 
- 
架构模式 
视觉资源
我们可以在编译时对项目中的任何资源进行分类,这些资源被用户界面使用,作为视觉资源。视觉资源可以从简单的文本元素到媒体项目(例如图像、动画、视频等),用于创建用户界面的视觉元素。每个 Xamarin 目标平台都提供不同的机制来存储和分发这些资源。
在 Android 和 iOS 上,资源和它们的本地化表示形式保存在指定的Resources文件夹和子结构中。在 Windows Phone(包括 Silverlight 和 Windows Runtime)上,可以使用嵌入的资源文件(即resw或resx)来管理资源。
文本资源
每个 Xamarin 目标平台使用各种策略来过滤掉静态文本资源,例如消息对话框或标签的内容,从视图实现中。这样做有助于开发人员将可读性资源与代码库分离,创建符合关注点分离原则的项目结构。
Xamarin.Android
在 Android 平台上,文本资源可以存储在strings.xml文件中,通过代码检索或用于声明性标记(即 AXML 文件)。包含字符串资源的 XML 文件可以在Resources\values目录中找到或创建。文件名与资源检索后的相关性不大。
资源 XML 文件具有简单的格式,其中每个字符串都定义为具有关联名称属性的 XML 节点:
<resources>
  <string name="ApplicationName">Fibonnaci Calculator</string>
  <string name="SingleCalculation">Single Calculation</string>
  <string name="RangeCalculation">Range Calculation</string>
  <string name="GCCollect">GC Collect</string>
</resources>
字符串值可以在标记中使用,也可以在 Android 声明性属性中使用@string/<ResourceName>的表示法:
<Button
    android:text="@string/SingleCalculation"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    local:MvxBind="Click NavigateToSingleCalculationCommand" />
<Button
    android:text="@string/RangeCalculation"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    local:MvxBind="Click NavigateToRangeCalculationCommand" />
为了为视图添加活动标签,可以直接使用@string表示法包含ApplicationName字符串:
[Activity(Label = "@string/ApplicationName")]
在经典字符串资源的基础上,还可以在资源 XML 文件中包含一组字符串资源和数量字符串。数量字符串是具有不同计数引用定义的资源字符串,适用于各种场景,并遵循正确的复数规则。
例如,对于一个以英语为默认语言的应用程序,复数数量字符串将类似于以下内容(例如,一个单词表示一个,复数形式表示其他):
<plurals name="CalculationsCompleted">
  <item quantity="one">%d calculation was completed.</item>
  <item quantity="other">%d calculations were completed.</item>
</plurals>
而对于土耳其语,它将类似于以下内容(相同的规则适用于所有可数名词):
<plurals name="CalculationsCompleted">
  <item quantity="other">%d islem tamamlandi.</item>
</plurals>
这种用法的示例可以扩展到斯拉夫语系(例如俄语、波兰语和捷克语),在这些语言中,对于少量项目或以特定数字结尾的数字有不同的用法。数量可能的切换值有zero、one、two、few、many和other。这些切换值的运用遵循 Unicode 通用区域数据仓库中定义的语言复数规则(更多信息请参见unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html)。例如,英语不需要对少量或零个项目的特定处理,因此为这些情况定义的任何规则都将被运行时忽略。
一旦修改了资源 XML 文件,你将看到Resource.Designer.cs文件在每次编译时都会(重新)生成。此文件包含不同类型资源的关联 ID 值,并可用于使用Resources实用工具类检索资源项。

图 1:生成的资源常量
使用Resources实用工具,可以通过以下方式检索文本资源:
// Getting a single text value
var singleStringValue = Resources.GetText(Resource.String.ApplicationName);
// Getting a string array
var stringArrayValue = Resources.GetTextArray(Resource.Array.MyStringArray);
// Getting a pluralized version for 2 items
var quantity = Resources.GetQuantityString(Resource.Plurals.CalculationsCompleted, 2, 2);
此外,其他原始数据类型(例如整数、布尔值等),以及用于样式定义中的单位或structs(例如尺寸和颜色)也可以包含在资源 XML 文件中。
Xamarin.iOS
在 iOS 平台上,将文本资源与其他项目内容分离的最简单方法就是创建.strings文件(例如,Localizable.strings),这些文件遵循简单的类似 JSON 的模式,包含键/值对:
"GCCollect" = "GC Collect";
"RangeCalculation" = "Range Calculation";
"SingleCalculation"= "Single Calculation";
这些字符串值,编译成资源包后,可以使用NSBundle.MainBundle.LocalizedString方法进行访问:
var localizedString = NSBundle.MainBundle.LocalizedString ("RangeCalculation", "");
本地化字符串值可以用作 UI 控件的标签,在静态文本内容和实际运行时组件之间创建松散耦合的关系。这个过程在 iOS 生态系统中被称为国际化。国际化的控件和元素可以轻松地本地化为不同的语言。Strings文件可以创建在Resources文件夹中,或者可以放置在Resources目录内的Base.lproj文件夹中,这构成了 iOS 项目的基本地化项目文件夹(默认/回退资源)。
对于故事板,国际化过程可能稍微复杂一些。Xcode 中每个故事板中的 UI 元素都被分配了一个唯一的标识符,称为 Object ID,而在 Xamarin Storyboard Designer 中则被称为 Localization ID。为了将文本内容分配给故事板上的特定项,开发者需要为每个故事板创建字符串文件(例如,对于名为 Main.storyboard 的故事板,你需要创建一个 Main.strings 文件),并使用特定控件的本地化 ID 和文本属性的名称:
/* Class = "UIViewController"; title = "Single Calculation"; ObjectID = "138"; */
"138.title" = "Single Calculation";
/* Class = "UILabel"; text = "Ordinal"; ObjectID = "153"; */
"153.text" = "Ordinal";
/* Class = "UIButton"; normalTitle = "Calculate"; ObjectID = "156"; */
"156.normalTitle" = "Calculate";
如所见,属性名称和大小写与 UI 控件的实际类型属性(例如,text 对于 UILabel,normalTitle 对于 UIButton)明显不同。iOS 国际化指南可以提供有关故事板属性的详细信息。
创建故事板的基础国际化文件的另一种方法是使用 Xcode 生成 string 文件。为了使用 Xcode 修改 Xamarin.iOS 项目,可以使用 打开方式 右键菜单项选择 Xcode Interface Builder 以访问故事板和主项目窗口的项目属性。

图 2:Xcode Interface Builder
在 Xcode 界面中,国际化设置位于项目设置页面。如果事先创建了基础国际化文件夹,项目设置国际化部分中的 基础国际化 选项将已经选中。

图 3:Xcode 项目配置
任何额外的语言选择都会生成特定语言的 .lproj 文件夹和目标故事板及字符串文件的 .strings 文件。一旦关闭 Xcode 窗口,这些更改将在 Xamarin.iOS 项目结构中反映出来。
Windows Phone
在 Windows Phone (Silverlight) 项目中,资源通过传统的 resx 文件管理(.NET 框架的遗留)。默认语言资源由项目模板生成并存储在 Resources 文件夹下的 AppResources.resx 文件中。

图 4:Windows Phone 资源
可以嵌入到资源文件中的其他内容类型包括图像、图标、音频和其他类型的文件。这些文件可以通过代码和标记来访问,使用生成的 AppResources 类。另一个生成的类 LocalizedStrings 提供了对存储在嵌入的资源文件中的资源的访问:
<StackPanel>
    <Button x:Name="SingleCalculation"
            Content="{Binding LocalizedResources.SingleCalculation, Source={StaticResource LocalizedStrings}}"
            Style="{StaticResource NavigationButtons}"></Button>
    <Button x:Name="RangeCalculation"
            Content="{Binding LocalizedResources.RangeCalculation, Source={StaticResource LocalizedStrings}}"
            Style="{StaticResource NavigationButtons}"></Button>
    <Button x:Name="GCCollect"
            Content="{Binding LocalizedResources.GCCollect, Source={StaticResource LocalizedStrings}}"
            Style="{StaticResource NavigationButtons}"></Button>
</StackPanel>
在 Windows Phone 8.1(即 Windows Runtime)和 Windows 10 中,应用程序使用 resw 文件(称为 PRIResource,指代编译方法)。尽管 resx 和 resw 文件的格式相同,但 resw 文件只能包含原始值(即字符串值或可以表示为字符串的值)。使用 resw 文件,开发者可以直接使用控件的 Uid 值将样式或其他属性值分配给用户控件,类似于 iOS 上故事板的国际化。

图 5:Windows Runtime PRI 资源
除了目标资源外,开发者仍然可以自由使用简单的资源字符串。这些资源可以通过 ResourceLoader 类和 GetString 方法访问。
图像资源
移动应用程序项目可以包含来自外部来源的媒体资源以及应用程序包。在每一个目标平台上,媒体资源可以通过不同的方式包含在内。
虽然 iOS 和 Windows Phone 没有规定媒体资源在项目树中的特定位置,但在 Android 项目中,开发者有义务将图像文档包含在 Resources 目录下的 drawable 文件夹中。

图 6:项目结构
与 iOS 平台上的文本资源结构类似,如果你计划在后续迭代中进行本地化,建议将语言中立的图像元素(针对默认语言)放置在 Base.lproj 位置。此外,还可以使用资产目录来简化图像及其不同分辨率下的像素完美替代物的管理(参见 自适应视觉资源 部分)。
自适应视觉资源
针对 Xamarin 平台的自适应 UI 模式有时迫使开发者包含不同分辨率和像素密度的媒体资源变体。尽管图像资源根据上述自适应 UI 指标进行了缩放,但缩放后的图像并不总是能呈现出令人满意的效果(例如,将图像大小调整为原始大小的两倍,以在不同设备上具有相同的物理屏幕尺寸,其显示效果可能并不如预期)。
Android 平台使用设备兼容性配置限定符来处理图像和文本资源文件夹(即drawables和values),以及其他类型的资源,例如布局。在这些项目中,兼容性限定符作为后缀连接到资源文件夹(例如,drawables-xhdpi文件夹可以用来提供针对大约 320 dpi 的超高密度设备显示的特定图像)并且向此文件夹添加各种默认资源。兼容性配置不仅处理像素密度,还提供与移动网络相关开关的选择器(即MCC(移动国家代码)和MNC(移动网络代码)),语言,和地区(见本地化部分),布局方向(即从左到右或从右到左),与屏幕尺寸相关的各种选项,屏幕方向,UI 模式(与显示应用程序的平台相关——汽车、桌面、电视、家电或手表),夜间模式(即白天或夜晚),与输入类型相关的配置,最后是平台 API 级别/版本。
在 iOS 平台上,可以通过添加不同的后缀为图像资产提供不同分辨率和设备风格的同一图像版本(即 iPhone、iPod 和 iPad)。设备风格值(即设备修饰符)与波浪号(~)字符一起使用,可以使用~iphone后缀识别 iPhone 和 iPod 的资源,使用~ipad后缀识别 iPad 的资源。@2x后缀,应出现在设备修饰符之前,用于识别高分辨率图像变体。
在 Windows Phone 8.1 引入之前,Windows Phone 操作系统仅支持四种变体:WVGA(480 x 800,仅用于 WP 7.1)、WXGA(768 x 1280)、720p(720 x 1280)和 1080p(1080 x 1920)。区分这些分辨率的唯一方法是使用App.Current.Host.ScaleFactor设备配置属性(例如,100 的缩放因子表示 WVGA,150 表示 HD)。Windows Store 应用程序(包括 Windows Phone 8.1)提供了一种类似于 iOS 和 Android 的自动缩放机制。在 Windows Phone 8.1 平台上,每个资源文件和/或文件夹都可以添加各种限定符以支持多个显示比例、语言和地区、对比度等,以针对不同的设备配置定制外观和感觉。如果限定符应用于特定文件,则每个限定符/值对应通过下划线分隔,并添加在文件名和扩展名之间(即filename.qualifiername-value_otherqualifier-value.fileextension)。如果限定符应用于完整文件夹,则对于每个限定符/值,应创建一个子文件夹(即resourcefolder/qualifier-value/otherqualifier-value/)。
例如,查看以下项目路径:
Images/en-US/config-designer/myImage.scale-140_layoutdir-LTR.png
这可以通过Images/myImage.png资源路径访问。
可重用资产
在跨平台项目中管理媒体资产,尤其是如果你为不同的设备配置提供变体时,可能会变得相当困难。为了在多个平台上重用这些资产,可以利用链接文件引用(添加 | 现有项 | 添加为链接)。

图 7:将资源作为链接添加
使用这种策略,图像文档可以包含在所有特定平台项目的公共位置(例如,公共便携式库),并且只需将链接文件引用添加到特定平台项目中。
这样,图像文档不会被复制到多个位置,而只是编译到不同的特定平台项目中。

图 8:Windows Phone 和 Android 项目中正常和高清晰度的链接资源
在跨平台项目中,文本资源在不同平台之间差异不大,尤其是如果涉及的资源是简单的字符串值,而不是针对 UI 控件的特定属性(例如,在故事板中指定的标签或按钮的文本内容)。另一个观察结果是,大多数文本资源值都以 XML 格式(针对 Windows Phone 和 Android)或简单的 JavaScript-like 表示法(在 iOS 中)作为键/值对处理。基于这些假设,我们可以创建一个自动化的过程,该过程评估一个通用的资源文件,并为目标平台创建/生成资源字符串。
考虑到我们将使用一个共享的项目库或便携式类库,该库将包含针对特定平台项目的共享代码,这个通用项目将是存储通用资源字符串的最合适位置。我们可以使用此项目创建resx格式的通用资源包。
如前所述,这些嵌入式资源文件是简单的 XML 文件,其中字符串资源对存储在具有name属性作为键的<data>节点中,以及<value>文本节点作为值(文件的其余部分包含用于代码生成的 XSD 架构和元数据值)。

图 9:Resx 文件 XML 结构
Android 字符串资源结构与复杂性较低,节点名称不同(即,资源值由具有name属性的<string>文本节点表示)。在 Visual Studio 中使用 XSL 转换在两个 XML 文件之间进行转换相对简单。
注意
XSL 是可扩展样式表语言的缩写,用于将 XML 文档从一种格式转换为另一种格式。XSLT 文件可以使用模板、XPath 查询和其他 XSL 函数来处理 XML 文档内容。更多信息可以在www.w3schools.com/xsl/default.asp找到。
要将资源文件转换为 Android 格式,我们将在通用项目中的 AppResources.resx 文件相同的文件夹中创建一个 XSLT 文件。为了创建 Android XML 资源文件,我们需要从 <root> 节点中选择每个 <data> 元素,并在 <resources> 根节点内创建具有适当文本内容和属性的 <string> 节点:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" 
     exclude-result-prefixes="msxsl">
  <xsl:output method="xml" indent="yes"/>
  <xsl:template match="/">
    <resources>
      <xsl:for-each select="/root/data">
        <string>
          <xsl:attribute name="name">
            <xsl:value-of select="@name"/>
          </xsl:attribute>
          <xsl:value-of select="value"/>
        </string>
      </xsl:for-each>
    </resources>
  </xsl:template>
</xsl:stylesheet>
现在,完成此步骤后,我们可以使用 XML 菜单通过 resx 文件来调试 XSLT 文件:

图 10:Visual Studio 中的 XSL 转换调试会话
确认转换按预期工作后,我们现在可以自动化此过程,以便每次重建通用项目时都重新生成字符串文件。为此自动化,我们可以使用第三方 XML 转换命令行应用程序,并通过项目设置将控制台命令作为预构建事件命令行参数添加。另一个选项是使用内置的 MSBuild 任务 XslTransformation 来添加 BeforeBuild 目标。
小贴士
为了添加新的构建目标,需要在 Visual Studio 中修改 csproj 文件。为此,首先需要使用项目上下文菜单中的 卸载项目 选项卸载通用项目,然后可以从相同的上下文菜单中选择 编辑 <项目文件名> 选项来编辑项目文件。
XslTransformation 任务是一个简单的构建任务,有三个基本参数用于需要转换的 XML 文件(即 XmlInputPath),用于转换的 XSL 文件(即 XslInputPath),以及最终的输出路径(即 OutputPaths):
<Target Name="BeforeBuild">
  <XslTransformation 
    XslInputPath="Resources\AndroidTransform.xslt" 
    XmlInputPaths="Resources\AppResources.resx" 
    OutputPaths="..\Xamarin.Master.Fibonacci.Android\Resources\values\strings.xml" />
</Target>
通过此修改,每次构建通用项目(默认设置下,通用项目应在 Android 项目之前构建)时,strings.xml 文件将被生成并放置在 Android 项目的 values 文件夹中。
相同的转换方法也适用于 iOS 本地化的 strings 文件。在 iOS 特定的转换中,输出应设置为文本,转换样式表应创建键/值对。为了为嵌入的资源文件中的每个数据元素创建文本行,可以利用 concat 函数:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" 
   exclude-result-prefixes="msxsl">
  <xsl:output method="text" encoding="utf-8" indent="no" omit-xml-declaration="no"/>
  <xsl:template match="/">
    <xsl:for-each select="/root/data">
      <xsl:value-of select="concat('"', @name, '" = "', value, '";', '
')" />
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>
在此样式表中,需要注意的是,文本元素(符号),如双引号和换行符(即行结束),是 HTML 编码的。
一旦确认转换结果,我们可以在项目文件中添加另一个 XslTransformation 任务作为 BeforeBuild 目标来创建本地化的 strings 文件:
<XslTransformation 
  XslInputPath="Resources\IOSTransform.xslt" 
  XmlInputPaths="Resources\AppResources.resx" 
  OutputPaths="..\Xamarin.Master.Fibonacci.iOS\Resources\Base.lproj\Localizable.strings" />
使用相同的实现,包含 resx 文件的翻译值可以转换并用于为目标平台生成本地化资源。除了 XSL 转换之外,T4 模板也可以用来生成文本资源文件。由于某些构建任务(包括 XslTransformation)尚未被 xBuild 和 Xamarin Studio 支持,如果您的开发环境主要是 Mac OS 且主要开发 IDE 是 Xamarin Studio,则 T4 模板可以提供一种替代方案。使用 T4 模板,还可以遍历通用资源中的每个文件并生成平台特定项目中匹配的本地化文件。
下一个部分将总结在 Xamarin 目标平台上的本地化策略。
本地化
本地化和全球化是移动应用的两个基本概念。在前几节中,我们讨论了将视觉内容与应用程序其余部分分离的不同方法。这个过程本质上是为移动应用本地化做准备,通常是全球化阶段的一部分。全球化应用应独立于它们正在执行的 culture 或 locale 运行,功能相同。在本地化过程中,开发者应创建特定语言的资源并将它们集成到全球化应用程序中。
区域设置和文化
区域设置可以定义为包括特定设备(或在某些情况下特定应用程序)上所有区域配置的通用术语。区域设置不仅代表用户界面语言,还包括用于显示日期、时间、数字和货币值的格式。
作为全球化工作的一部分,在 Xamarin 目标平台上,开发者首先需要确定哪些语言将作为本地化工作的一部分得到支持。一旦用户发布并安装了移动应用,它应显示支持的语言,以便用户界面可以使用操作系统指定的区域设置(如果支持)或应用程序的默认/后备语言进行渲染。
支持的语言清单是根据提供的资源(Android)或预先声明的清单或项目条目(iOS 和 Windows Phone)计算得出的值。
Windows Phone
在 Windows Phone Silverlight 应用程序项目中,可以根据命名约定使用资源包提供不同语言的资源。然后应在 WMAppManifest.xml 文件中引用提供的包。为 Windows Phone 应用程序添加额外的语言支持的最简单方法是通过项目属性来识别支持的文化。

图 11:Windows Phone Silverlight 应用程序的项目属性
保存项目配置修改后,Visual Studio 会自动创建相关的 resx 文件(例如,为波斯尼亚语创建 AppResources.bs.resx,为土耳其语创建 AppResources.tr-TR.resx)并更新应用程序清单。默认语言可以从包清单(即 package.appxmanifest)或应用程序清单(WMAppManifest.xml)设计器中进行修改。
Windows Store 应用程序(即 Windows Phone 8.1)使用以支持的语言命名的文件夹进行全球化,其中包含 resw 资源文件。例如,为了创建一个针对与上一个示例相同文化的应用程序,我们需要创建一个类似以下的文件夹结构和特定文化资源文件:

图 12:Windows Store 应用支持的文化和应用程序包
一旦创建完应用程序包,你会注意到,除了单个应用程序包外,还会创建一个应用程序包,并且每个支持的文化都关联一个存储应用程序包在该包中。
注意
应用程序包在 Windows Store 应用中使用,以减少用户为特定 CPU 架构(ARM、x86 或 x64)、显示硬件(图像和其他媒体资产,针对不同分辨率进行优化)或区域设置下载的应用程序包的大小。可以在创建应用程序包时选择打包策略,但如果放弃打包,开发者需要为计划支持其应用程序的每个 CPU 架构创建不同的上传包。
Xamarin.iOS
如前所述,对于 Xamarin.iOS,一旦在 Xcode 开发环境中为项目选择了附加语言,生成的本地化文件夹和文件将自动添加到 Xamarin.iOS 项目中。生成的故事板字符串文件最初包含可能的本地化字段和从故事板分配的值。其他字符串包资源文件从 Base.lproj 文件夹复制相同的值。

图 13:本地化的 Xamarin.iOS 项目
当使用文本资源文件进行本地化时,MainBundle 属性的 LocalizedString 函数要么返回与当前用户语言选择匹配的值,要么返回在 Base.lproj 目录中定义的默认值。
小贴士
当使用 Visual Studio 创建和编辑 strings 文件时,使用 选项 对话框和 文本编辑器 | 文件扩展名 部分将 strings 扩展名映射到 JavaScript 编辑器是一个好主意。
为了加载与当前首选语言(或语言)配置不匹配的语言特定资源,你需要使用本地化包路径,并使用此包上的相同函数检索本地化资源:
var path = NSBundle.MainBundle.PathForResource("tr", "lproj");
NSBundle languageBundle = NSBundle.FromPath(path);
var localizedString = languageBundle.LocalizedString ("RangeCalculation", "");
原生开发语言目录(即,Base.lproj),以及特定语言的文件夹,也可以用来存储其他类型的包资源,例如图像资源、故事板、XIB 文件,甚至是特定语言的Info.plist文件。(语言目录中的InfoPlist.strings文件可以用来覆盖应用程序Info.plist文件中的值,例如应用程序名称。)
向信息清单中添加支持的语言至关重要。对于本地化,有两个相关的键。第一个相关项是本地化原生开发区域(即,CFBundleDevelopmentRegion),第二个键是本地化(即,CFBundleLocalizations)。虽然原生开发区域定义了与Base.lproj位置关联的语言,但本地化条目提供了有关其他支持本地化的信息。
Xamarin.Android
在 Xamarin.Android 项目中,本地化与 Windows Phone 8.1 项目的文件夹结构类似,通过特定的文件夹结构实现,语言代码附加到本地化资源项中(例如,drawable-tr或values-en)。

图 14:Android 本地化文件夹结构
在运行时,通过一个简单的消除算法选择合适的资源,该算法根据地区、显示密度、显示大小、触摸支持和其他标准选择正确的资源文件。
Xamarin.Forms
Xamarin.Forms 便携式类库项目模板提供了一个理想的文本资源共享环境。在这个设置中,与 Windows Phone Silverlight 项目类似的过程,可以使用resx文件创建资源包,这些资源包可以用来本地化使用 Xamarin.Forms 框架创建的跨平台视图。

图 15:本地化的 Xamarin.Forms 资源
一旦将嵌入的资源文件及其翻译对应的文件添加到公共 PCL 项目中,就可以使用生成的静态类来访问资源条目。为了使生成的类可以从特定平台实现中访问,资源文件的自定义工具属性必须设置为PublicResXFileCodeGenerator,而构建操作属性必须设置为Embedded Resource。
小贴士
使用 Xamarin Studio 或 Visual Studio,可以通过文件属性窗口设置资源访问器的正确访问修饰符。在 Visual Studio 中,资源编辑器也可以用来修正资源项的访问修饰符(即,使用资源设计器选择访问修饰符 | Public)。
在 Windows Phone 运行时,根据当前线程的文化,正确加载资源文件,因此先前的实现会自动选择合适的嵌入式资源。然而,支持的语言仍然需要通过应用程序清单进行配置。在 Xamarin.iOS 中,根据用户的语言偏好(而不是当前 UI 语言)加载正确的资源,并且应使用 CFLocalizations 条目将支持的语言包含在 Info.plist 文件中。对于 Android 平台,UI 语言选择被视为资源的标识符。
以下实现将本地化前一章中的选项卡页实现:
var tabbedPage = new TabbedPage();
tabbedPage.Children.Add(new ContentPage
{
    Title = TextResources.TabItemRecent,
    Content = new StackLayout
    {
        // Omitted for clarity
    },
    Icon = "social.png"
});
在先前的示例中,高亮显示的代码行将访问器属性设置为特定的资源元素。当使用 XAML 进行相同的实现时,我们可以求助于使用 TextResources 生成的类进行静态绑定属性:
<TabbedPage 
    x:Class="Xamarin.Master.TravelTrace.Views.MainTabView"
    >
  <ContentPage 
        Title="{x:Static resources:TextResources.TabItemRecent}" 
        Icon="social.png">
包含生成的资源访问器的 CLR 命名空间是很重要的。
架构模式
应用程序的用户界面可以描述为对下面所有移动部分的封装。随着应用程序变得更加复杂,用户界面的责任也在增加,封装下面的产品变得更加困难。抛开 UI 的静态部分(即本章前几节中描述的资产),它是应用程序中最易变的部分。为了对抗整个应用程序生命周期中积累的熵,解决重复的问题模式,以及重用模块,开发者通常会在他们的开发工作中利用某些设计模式。特别是在跨平台项目中,这些架构设计模式的重要性已经被证明是无可辩驳的。
为了演示目的,让我们使用一个简单的表单提交场景。在这个实现中,用户将看到一个需要填写的表单。一旦用户填写了所有必需的文本字段,他/她将通过提交按钮提交内容。然后验证数据并存储。用户应该随后通过一个只读屏幕被告知提交情况,在该屏幕上他/她可以看到提交和存储的数据。

图 16:经典的 n 层场景
在 n 层实现中,表示层将负责可视化数据并持有 API 门面实例。API 门面将实现业务逻辑以验证信息并将其提交给数据层实例。数据层将仅负责与持久存储进行通信(可能通过服务层)。
事件订阅(即文本字段更改或提交按钮点击)将在表示层中实现。在成功提交后,表示层会将当前的 API 对象传递给一个新的表示容器,并可视化提交的数据。尽管这种方法在层之间提供了清晰的分离,但在层次结构中(即表示层持有 API 的强类型引用,API 要么重用或创建数据模型的新实例)仍然存在强烈的联系。应用程序层还创建了一个不必要的庞大且复杂的结构,该结构应提供所有容器和相关场景在表示层中所需的方法。在事件驱动实现方面,表示层仍然承担着最大的责任。如果我们把这个实现转移到 Xamarin 跨平台项目中,我们就能在各个平台上重用完整的应用程序层和数据层。然而,对于其他平台项目,仍然需要对表示层进行相当多的重新实现,因为这一层负责使用 API。这种模式的另一个缺点是,除了门面之外,实现单元测试并不容易(即在表示层上有多个事件订阅)。
MVP(模型-视图-演示者)和MVVM(模型-视图-视图模型),都是MVC(模型-视图-控制器)的衍生模式,试图解决经典n层实现中的一些问题。这两种模式本质上都使用一个被动的表示层,并将主要责任委托给监督或中介组件;这样做的主要原因是因为对视图进行单元测试通常是不切实际的,因此视图应尽可能不包含逻辑。演示者与数据层进行主动通信,并负责视图的视觉呈现。在这个范例中,视图与中介通信的唯一方式是通过路由事件(关注点分离)。还应注意,在这些架构实现中,应用程序被划分为自给自足的三元组(即模型、视图和演示者),它们构成了应用程序中的不同用例和视图。门面通常仅在模型组件中使用。
MVC
MVC 模式最初被引入到 Smalltalk 中,后来随着其在 Web 应用程序和框架中的(过度)使用而流行起来。在经典的 MVC 实现中,模型不仅提供对数据存储的访问,还实现了任何所需的企业逻辑。模型可以描述为问题域的核心实现,与用户界面无关。
控制器通常代表从视图中剥离的逻辑;它可以向模型以及视图发送命令,并从视图接收路由的事件。状态的变化(即在模型中),无论是否有控制器的干预,都会在视图上反映出来(经典的 MVC 允许模型和视图之间的主动或隐式交互)。
iOS 应用架构
在 iOS 应用程序中,迄今为止主要开发语言一直是 Objective-C。用于 Mac OS 和 iOS 应用程序开发的 Cocoa 和 Cocoa Touch 框架也主要用 Objective-C 开发。考虑到 Objective-C 和 SmallTalk 之间的紧密联系,iOS 开发工具包采用和强制执行的主要开发模式是 MVC 也就不足为奇了。
在 Cocoa 版本的 MVC 实现中,由于移动应用程序开发环境的技术要求,视图和模型之间的直接通信完全放弃(并被禁止),并且为了提高模型和视图组件的可重用性。在这个模式中,控制器(有时也称为调解者)被赋予控制视图和模型之间数据流的主要责任。从这个角度来看,Cocoa 对 MVC 的实现无疑类似于 MVP 模式:

图 17:Cocoa MVC
在这个实现方案中,鼓励开发人员将三联组件相互解耦,并通过定义的抽象来实现它们之间的通信。
视图和控制器之间的分离通常通过命令、出口和绑定来实现。命令提供可执行的组合,可以从一层传递到另一层,出口是某些 UI 元素的扩展,以便控制器可以订阅事件并根据状态控制 UI 的呈现方式。
当视图元素使用 XIBs 或故事板(即在编译时使用故事板生成 XIBs)设计时,出口被定义为视图-控制器的访问点。视图-控制器不直接依赖于视图,视图也没有任何关于控制器的知识。这种设置符合关注点分离原则,并提供了松散耦合的结构,正如建议的那样。
如果我们要实现前一个示例中的场景,我们将在提交表单中暴露两个文本输入字段的出口和一个提交按钮的出口。这些出口将反过来被分配给视图的控制器用于订阅某些事件,以验证和提交数据。视图-控制器(即UIController)还负责更改控件显示方式(例如,验证可以改变文本输入字段的颜色)以及将用户操作(例如,提交数据)与模型进行通信。在这种情况下,导航到另一个视图也是控制器的责任。

图 18:iOS 表单上的 MVC 演示
当新视图的控制器存在于调用控制器中,或者两个视图都使用相同的 UI 控制器(即,在先前的示例中,相同的控制器可以用于提交和只读视图)时,视图之间的 segue 导航是另一种可能的导航策略。
MVVM
MVVM(模型-视图-视图模型),MVP 模式的衍生品,为 UI、业务逻辑和数据之间提供了明确的边界。自从其出现以来,它几乎立即成为了WPF(Windows 演示基础)应用程序的主要实现模式。WPF 框架提供的数据绑定功能构成了这种中介模式的基础。
注意
数据绑定是用来描述将 UI 层(即控件)中的数据可视化元素与其他层(例如,其他控件或数据对象)连接起来的机制的术语。绑定双方之间的同步通过各种事件(例如,使用INotifyPropertyChanged接口来传播值更改事件)来维护。
在这个模式中,ViewModel 是主要演员,其责任是控制视图和模型之间的数据流。在这个架构中,出口由 ViewModel 暴露并由视图实现使用(与 iOS MVC 架构相反)。ViewModel 以可以与 UI 控件属性或状态关联的数据元素的形式提供这些出口,以及作为视图控件可以用来响应用户输入的通用命令。
Windows Runtime
Windows Phone 应用程序以及 Windows Store 应用程序原生支持 UI 控件的数据绑定。这一特性使得 Windows Phone 应用程序成为这种架构的理想候选者。然而,架构元素仍应由开发者根据特定项目的需求来实现。有多个(开源或商业)库可以作为 NuGet 包包含在开发项目中,包括 Prism(一个跨平台 MVVM 库,最初是微软模式和实践团队的宠物项目,但现在由社区维护)和 MVVMCross(一个跨平台开源 MVVM 框架)。
在 MVVM 模式和数据绑定的核心,我们可以找到可绑定基类的实现。可绑定基类提供了INotifyPropertyChanged接口的实现,使得识别和实现将参与数据绑定的数据元素变得更加容易。此接口用于将数据项及其属性的价值更改事件路由到 UI 元素。
一个简单的可绑定基类实现看起来会类似于以下这样:
public abstract class BindableBase : INotifyPropertyChanged
{
    protected virtual void SetProperty<T>(ref T property, T value, 
      [CallerMemberName] string propertyName = null)
    {
        if (Equals(property, value)) return;
        property = value;
        OnPropertyChanged(propertyName);
    }
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}
这个类的实现可以与模型数据项一起使用,以便任何更改都可以在 UI 上反映出来:
public class ModelData : BindableBase
{
    private string m_Property1BackingField = string.Empty;
    public string Property1
    {
        get
        {
            return m_Property1BackingField;
        }
        set
        {
            SetProperty(ref m_Property1BackingField, value);
        }
    }
}
现在,ModelData类已经可以用作 ViewModel,并且其绑定可以提供给Property1:
public MainPage()
{
    this.InitializeComponent();
    this.DataContext = new ModelData {Property1 = "Hello MVVM"};
}
主页上的输入控件的数据绑定看起来会类似于以下这样:
<TextBox Text="{Binding Property1, Mode=TwoWay}">
在这种绑定场景中,我们将绑定模式设置为TwoWay。这种绑定类型意味着,无论属性值的变化是在 ViewModel 还是用户界面(即用户输入)上,都会传播到 UI 元素,反之亦然。
注意
数据绑定可以使用不同的模式来维护。OneTime绑定用于在数据源更改时使用源属性更新目标属性。这种类型的绑定通常用于只读控件。OneWay绑定仅在源属性值更改时更新目标属性,而TwoWay模式用于双向同步。最后,OneWayToSource仅在目标属性有更改时更新源属性。
数据绑定不仅限于从 ViewModel 属性到值和从值到 ViewModel 属性的绑定。用户控件的可绑定属性也可以在这个模式中桥接。此外,用户控件的可绑定属性包括行为和样式属性(例如,TextBox用户控件上的IsEnabled属性)。还可以通过附加和/或依赖属性为内建或派生用户控件提供额外的可绑定属性。
命令绑定是另一个概念,它提供了一种解耦的方式来将用户操作控件(例如,Button)与数据上下文中的可执行元素(即,ViewModel)关联起来。为了使用户控件绑定到命令,用户控件应实现可绑定命令属性,而 ViewModel 应提供特定操作的ICommand实现。ICommand接口是一个简单的接口,包含一个CanExecute属性,一个相关的CanExecuteChanged事件(通常绑定到用户控件的IsEnabled属性),以及Execute方法。
一个简单的命令实现,将验证前一个示例中的数据模型并执行,其实现方式如下(请注意,MVVM 框架通常提供一个通用的Command类,该类接受用于Execute和CanExecute方法的委托和/或 lambda 表达式):
public class SubmitCommand : ICommand
{
    private readonly ModelData m_DataContext;
    public SubmitCommand(ModelData dataContext)
    {
        m_DataContext = dataContext;
        m_DataContext.PropertyChanged += (sender, args) =>
        {
            if(args.PropertyName == "Property1" && 
                CanExecuteChanged !=null) 
                CanExecuteChanged(this, null);
        };
    }
    public bool CanExecute(object parameter)
    {
        return m_DataContext.Property1.Length > 5;
    }
    public void Execute(object parameter) {
        // TODO:
    }
    public event EventHandler CanExecuteChanged;
}
使用此实现(无论是公共的还是之前定义的数据模型嵌套类),我们可以在初始化新的ModelData类时初始化和公开命令:
public ModelData()
{
    Submit = new SubmitCommand(this);
}
public ICommand Submit { get; set; }
最后,此命令在 XAML 标记中的绑定看起来如下:
<Button Content="Submit" Command="{Binding Submit}"></Button>
如果我们要使用 MVVM 模式实现前一个表单提交场景,我们可以观察到数据和命令绑定的实现。我们可以实现一个 ViewModel 类,该类负责加载和提交可绑定数据项。视图将绑定到 ViewModel 属性和命令,以及绑定到数据项本身。

图 19:表单提交场景的 MVVM 实现
在此设计中,SubmitCommand既用于将用户输入提交到模型,也用于验证表单本身(使用CanExecute方法)。ViewModel 的IsReadOnly属性绑定到文本字段的IsReadOnly属性以及提交按钮的Visibility属性(在只读模式下,而不是提交按钮,应显示提交标签),可能还使用IValueConverter(在数据绑定场景中用于绑定属性之间的双向转换的接口)。
注意
值转换器实现了IValueConverter接口,以在绑定过程中应用自定义逻辑。它们通常用作目标属性和源属性的 CLR 类型(例如,如果数据模型属性类型是定义某种颜色的string,我们需要将此值转换/解析为SolidColorBrush或类似类型以分配给视觉元素的属性)的适配器。
除了通过使用 MVVM 实现的松散耦合和模块化之外,ViewModel 提供的伪有限自动机允许开发者轻松地重新创建视图使用的数据状态,并轻松实现单元测试。
在 Xamarin.iOS 和 Xamarin.Android 上实现 MVVM
在 Xamarin 项目中,为了在不同平台的应用程序之间创建统一的结构并最大化代码共享,使用 MVVM 模式作为全局实现原则是一种广泛接受的做法。由于 iOS 和 Android 原生不支持数据绑定和命令模式实现,使用支持 Xamarin 跨平台开发的 MVVM 框架可以是一个解决方案。
注意
需要指出的是,iOS 和 Cocoa 有键值观察的概念,并且可以在一定程度上应用类似绑定的实现。
在 Xamarin.iOS 和 Xamarin.Android 上,绑定通常通过UIViewController(在 iOS 上)和Activities(在 Android 上)的扩展提供。在 iOS 中,这种实现策略将 MVC 架构中的视图和控制器转换为仅有的视图实现,而 ViewModel 在概念上取代了模型实现。对 ViewModel 的绑定是在UIViewControllers和Activities的应用程序生命周期事件中初始化的。
使用 Xamarin.Forms 的 MVVM
Xamarin.Forms 的数据绑定功能是对 WPF 数据绑定的实现/端口,因此支持 XAML 绑定用于数据和命令。Xamarin.Forms 与 Windows Runtime 的主要区别在于,在 Windows Store 应用程序中,用户控件或容器的绑定上下文是通过DataContext属性配置的,而在 Xamarin.Forms 中,使用BindingContext属性用于相同的目的。Xamarin.Forms 还提供了通用的命令实现类(即Command和Command<T>),允许开发者无需在嵌套类中实现ICommand接口即可暴露命令。
摘要
在跨平台项目中,无论是否使用 Xamarin.Forms,建议尽可能保持视图元素薄,不包含静态和/或可共享元素。正如本章所讨论的,每个 Xamarin 目标平台都支持特定的资源和管理方式。这些方法可以通过使用链接资源和使用特殊的构建技术来扩展,以便在特定平台的项目之间共享静态资源。
架构模式,无论是平台强制的还是其他原因,可以在项目开始时或通过迭代项目成熟时采用。MVC、MVVM 以及 MVP 模式有助于减少视图上的可共享逻辑组件,从而创建一个更松散耦合的项目结构(参见第一章中的质量标识符,使用 Xamarin 进行开发)。
在介绍完 Xamarin 框架和 UI 相关概念的不同方面之后,本书的下一部分将讨论与应用程序生命周期管理(ALM)相关的话题,以创建针对处理 Xamarin 项目的个人或团队的效率开发流程。
第十章。ALM – 开发者和 QA
本章介绍了在 Xamarin 跨平台应用程序中应用生命周期管理(ALM)和持续集成方法。作为 ALM 过程中对开发者最相关的部分,本章将讨论并演示单元测试策略,以及自动化的 UI 测试。本章分为以下几部分:
- 
开发流程 
- 
故障排除 
- 
单元测试 
- 
UI 测试 
开发流程
开发流程可以被描述为一条虚拟的生产线,它将一个项目从仅仅是一堆业务需求引导到消费者手中。这个流程中的利益相关者包括但不限于业务代理、开发者、质量保证团队、发布和配置团队,以及最终的用户。在这个生产线上,每个利益相关者都承担着不同的责任,并且他们应该协同工作。因此,拥有一个高效、健康且最好是自动化的流程,该流程能够提供单元之间的沟通和交付成果的传递,对于项目的成功至关重要。
在敏捷项目管理框架中,开发流程是循环的,而不是线性的交付队列。在应用程序生命周期中,需求持续地插入到待办事项列表中。待办事项列表导致规划和开发阶段,随后是测试和质量保证。一旦生产就绪的应用程序发布,消费者可以通过实时应用程序遥测仪器成为这个周期的一部分。

图 1:应用生命周期管理
在 Xamarin 跨平台应用程序项目中,开发团队有幸拥有各种工具和框架,这些工具和框架可以简化 ALM 策略的执行。从可用于早期原型设计和设计的草图和模拟工具,到构成 ALM 骨干的源代码控制和项目管理工具,Xamarin 项目可以利用各种工具来自动化和系统地分析项目时间线。
本章的以下部分主要集中讨论在将任务分配给开发者到任务或错误完成/解决并检查到源代码控制仓库之间的时间线上,保护 Xamarin 跨平台项目健康和稳定性的防御线。
故障排除和诊断
与 Xamarin 目标平台和开发 IDE 关联的 SDK 配备了全面的分析工具。利用这些工具,开发者可以识别导致应用冻结、崩溃、响应时间慢以及其他与资源相关的问题(例如,过度使用电池)的原因。
使用 XCode Instruments 工具集分析 Xamarin.iOS 应用程序。在此工具集中,有多个分析模板,每个模板用于分析应用程序执行的某个特定方面(例如,在第二章中使用的分配模板,用于内存分析)。可以在运行在 iOS 模拟器或实际设备上的应用程序上执行工具模板。

图 2:XCode Instruments
类似地,可以使用 Android SDK 提供的设备监控器分析 Android 应用程序。使用 Android Monitor,还可以分析内存配置文件、CPU/GPU 利用率和网络使用情况,并收集应用程序提供的诊断信息。Android 调试桥接器(ADB)是一个命令行工具,允许执行各种手动或自动的设备相关操作。
对于 Windows Phone 应用程序,Visual Studio 提供了多种分析工具,用于分析 CPU 使用率、能耗、内存使用率和 XAML UI 响应性。特别是 XAML 诊断会话可以提供有关视图实现中问题部分的宝贵信息,并确定可能的视觉和性能问题:

图 3:Visual Studio XAML 分析
最后,Xamarin Profiler 作为成熟的应用程序(目前处于预览发布状态),可以帮助分析内存分配和执行时间。Xamarin Profiler 可以与 iOS 和 Android 应用程序一起使用。
单元测试
测试驱动开发(TDD)模式规定,业务需求和由这些需求定义的细粒度用例应首先反映在单元测试用例中。这允许移动应用程序在定义的断言单元测试模型范围内增长/发展。无论遵循 TDD 策略还是实施测试以确保开发管道的稳定性,单元测试都是开发项目的根本组成部分。

图 4:单元测试项目模板
Xamarin Studio 和 Visual Studio 都提供了一系列针对跨平台项目不同领域的测试项目模板。在 Xamarin 跨平台项目中,单元测试可以分为两组:平台无关和平台特定测试。
平台无关单元测试
平台无关的组件,例如包含 Xamarin 应用程序共享逻辑的可移植类库,可以使用针对.NET 框架的通用单元测试项目进行测试。可以根据选择的开发环境使用 Visual Studio 测试工具或 NUnit 测试框架。还应注意,用于为 Xamarin 项目创建共享逻辑容器的共享项目不能使用.NET 单元测试固定装置进行测试。对于共享项目和引用特定平台的项目,应准备特定平台的单元测试固定装置。
在遵循 MVVM 模式时,视图模型是单元测试固定装置的重点,因为,如前所述,视图模型可以被视为一个有限状态机,其中可绑定属性用于创建一个特定的状态,在该状态下执行命令,模拟要测试的特定用例。这种方法是测试 Xamarin 应用程序 UI 行为最方便的方法,而无需实现和配置自动化的 UI 测试。
在为这类项目实现单元测试时,通常使用模拟框架来替换业务逻辑中依赖平台的部分。松散耦合这些依赖组件(参见第八章,Xamarin.Forms),使得开发者更容易注入模拟接口实现,并增加了这些模块的可测试性。最流行的单元测试模拟框架是 Moq 和 RhinoMocks。
注意
Moq 和 RhinoMocks 都使用反射,特别是 Reflection.Emit 命名空间,用于在运行时生成类型、方法、事件和其他资源。之前提到的 iOS 代码生成限制使得这些库在特定平台测试中不适用,但它们仍然可以包含在针对.NET 框架的单元测试固定装置中。对于特定平台的实现,True Fakes 库提供了编译时代码生成和模拟功能。
根据实现的具体细节(如使用的命名空间、网络通信、多线程等),在某些场景下,测试特定平台上的通用逻辑实现是强制性的。例如,某些多线程和并行任务实现在 Windows Runtime、Xamarin.Android 和 Xamarin.iOS 上给出不同的结果。这些变化通常是由于底层平台的机制或.NET 和 Mono 实现逻辑之间的细微差异造成的。为了确保这些组件的完整性,可以将通用单元测试固定装置作为链接/引用文件添加到特定平台的测试项目中,并在测试平台上执行。
特定平台单元测试
在 Xamarin 项目中,无法使用 Visual Studio 测试套件和 NUnit 框架中提供的传统单元测试运行器对平台相关的功能进行单元测试。平台相关的测试是在空白的特定平台项目上执行的,这些项目作为该特定平台单元测试的框架。
可以使用 Visual Studio 测试套件测试 Windows 运行时应用程序项目。然而,对于 Android 和 iOS,应使用 NUnit 测试框架,因为 Visual Studio 测试工具在 Xamarin.Android 和 Xamarin.iOS 平台上不可用。

图 5:测试框架
Windows Phone(Silverlight)和 Windows Phone 8.1 应用程序的单元测试运行器使用与 Visual Studio 测试资源管理器集成的测试框架。可以在 Visual Studio 内部执行和调试单元测试。
Xamarin.Android 和 Xamarin.iOS 测试项目模板分别使用 NUnitLite 实现对应平台的功能。为了运行这些测试,测试应用程序应该部署在模拟器(或测试设备)上,并且必须手动执行应用程序。
提示
通过仪器化,可以在 Android 和 iOS 平台上自动化单元测试;然而,这些方法将在下一章中讨论。
在每个 Xamarin 目标平台上,初始应用程序生命周期事件用于添加必要的单元测试:
[Activity(Label = "Xamarin.Master.Fibonacci.Android.Tests", MainLauncher = true, Icon = "@drawable/icon")]
public class MainActivity : TestSuiteActivity
{
    protected override void OnCreate(Bundle bundle)
    {
        // tests can be inside the main assembly
        //AddTest(Assembly.GetExecutingAssembly());
        // or in any reference assemblies
        AddTest(typeof(Fibonacci.Android.Tests.TestsSample).Assembly);
        // Once you called base.OnCreate(), you cannot add more assemblies.
        base.OnCreate(bundle);
    }
}
在 Xamarin.Android 实现中,MainActivity 类继承自 TestSuiteActivity,该类实现了运行单元测试所需的基础设施以及用于可视化测试结果的 UI 元素。在 Xamarin.iOS 平台上,测试应用程序使用默认的 UIApplicationDelegate,通常使用 FinishedLaunching 事件代理来创建单元测试运行固定程序的 ViewController:
public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
    // Override point for customization after application launch.
    // If not required for your application you can safely delete this method
    var window = new UIWindow(UIScreen.MainScreen.Bounds);
    var touchRunner = new TouchRunner(window);
    touchRunner.Add(System.Reflection.Assembly.GetExecutingAssembly());
    window.RootViewController = new UINavigationController(touchRunner.GetViewController());
    window.MakeKeyAndVisible();
    return true;
}
以这种方式执行单元测试的主要缺点是难以生成代码覆盖率报告并归档测试结果。
这两种测试方法都不提供测试 UI 层的能力。它们只是用来测试平台相关的实现。为了测试交互层,需要实现特定平台或跨平台(Xamarin.Forms)的编码 UI 测试。
UI 测试
一般而言,单元测试的代码覆盖率与共享代码的数量直接相关,这至少相当于普通 Xamarin 项目代码库的 70-80%。正如前几章所述,架构模式的主要驱动因素之一是减少视图层中的逻辑和代码量,以便使用传统单元测试的项目可测试性达到令人满意的水平。编码 UI(或自动 UI 接受)测试用于测试跨平台解决方案的最高层:视图。
Xamarin.UITests 和 Xamarin Test Cloud
用于 Xamarin 项目的 UI 测试主要框架是 Xamarin.UITests 测试框架。此测试组件可用于各种平台特定项目,从原生移动应用程序到 Xamarin.Forms 实现,但 Windows Phone 平台和应用程序除外。Xamarin.UITests 是基于 Calabash 框架的实现,Calabash 框架是一个针对移动应用程序的自动化 UI 验收测试框架。
Xamarin.UITests 是通过公开可用的 NuGet 包引入到 Xamarin.iOS 或 Xamarin.Android 应用程序中的。包含的框架组件用于提供进入原生应用程序的入口点。入口点是 Xamarin 测试云代理,它在编译过程中嵌入到原生应用程序中。云代理类似于本地服务器,允许 Xamarin 测试云或测试运行器与应用程序基础设施通信并模拟与应用程序的用户交互。
注意
Xamarin 测试云是一个基于订阅的服务,允许使用通过 Xamarin.UITests 实现的 UI 测试在真实移动设备上进行测试。Xamarin 测试云不仅为 Xamarin.iOS 和 Xamarin.Android 应用程序提供了强大的测试基础设施和大量的移动设备,还可以集成到持续集成工作流程中。
安装适当的 NuGet 包后,可以在特定设备上的特定应用程序中初始化 UI 测试。为了初始化应用程序的交互适配器,需要配置应用程序包和设备。在 Android 上,可以使用 APK 包路径和设备序列号进行初始化:
IApp app = ConfigureApp.Android.ApkFile("<APK Path>/MyApplication.apk")
                   .DeviceSerial("<DeviceID>")
                   .StartApp();
对于 iOS 应用程序,流程类似:
IApp app = ConfigureApp.iOS.AppBundle("<App Bundle Path>/MyApplication.app")
    .DeviceIdentifier("<DeviceID of Simulator")
    .StartApp();
一旦创建了App句柄,使用 NUnit 编写的每个测试都应该首先创建测试的前置条件,模拟交互,并最终测试结果。
IApp接口提供了一套方法来选择视觉树上的元素并模拟某些交互,例如文本输入和点击。除了主要的测试功能外,还可以拍摄截图以记录测试步骤和可能的错误。
Visual Studio 和 Xamarin Studio 都为 Xamarin.UITests 提供了项目模板。
Xamarin 测试记录器
Xamarin 测试记录器是一个可以简化自动化 UI 测试创建的应用程序。它目前处于预览版本,并且仅适用于 Mac OS 平台。

图 6:Xamarin 测试记录器
使用此应用程序,开发者可以选择需要测试的应用程序以及将要运行应用程序的设备/模拟器。一旦开始录制会话,屏幕上的每个交互都会作为单独屏幕上的执行步骤被记录下来,这些步骤可以用来生成 Xamarin.UITests 实现的准备或测试步骤。
编码 UI 测试(Windows Phone)
编码 UI 测试用于在 Windows Phone 平台上进行自动化 UI 测试。Windows Phone 和 Windows Store 应用程序的编码 UI 测试与其他 .NET 平台(如 Windows Forms、WPF 或 ASP.Net)的对应版本没有区别。还应注意,只有 XAML 应用程序支持编码 UI 测试。
编码 UI 测试是在模拟器上生成的,基于 Automation ID 原则编写的。Automation ID 属性是 Windows Phone 应用程序(仅在 XAML 中)以及应用程序中使用的 UI 控件的自动生成或手动配置的标识符。编码 UI 测试依赖于为特定屏幕上的每个控件使用 Automation IDs 创建的 UIMap。在创建 UIMap 时,可以使用准星工具选择模拟器屏幕上的应用程序和控件来定义交互元素:

图 7:生成编码 UI 访问器和测试
一旦创建了 UIMap 并生成了设计器文件,就可以使用手势和生成的 XAML 访问器来创建测试前提条件和断言。
对于编码 UI 测试,可以在单个断言上使用和测试多个特定场景的输入值。使用 DataRow 属性,单元测试可以扩展以测试多个数据驱动场景。下面的代码片段使用多个输入值来测试不同的错误输入值:
[DataRow(0,"Zero Value")]
[DataRow(-2, "Negative Value")]
[TestMethod]
public void FibonnaciCalculateTest_IncorrectOrdinal(int ordinalInput)
{
    // TODO: Check if bad values are handled correctly
}
自动测试可以在可用的模拟器和/或真实设备上运行。它们也可以包含在 CI 构建工作流程中,并成为自动化开发流程的一部分。
Calabash
Calabash 是一个用于执行 Cucumber 测试的自动化 UI 接受测试框架。Cucumber 测试提供了一种类似于编码 UI 测试的断言策略,但更广泛且以行为为导向。Cucumber 测试框架支持用 Gherkin 语言(一种用于行为定义的人类可读编程语法描述)编写的测试。Calabash 提供了必要的基础设施,以在各个平台和应用运行时上执行这些测试。
使用数据驱动模型在 Coded UI 上先前测试的功能和场景的简单声明将类似于以下摘录。在此功能中仅声明了两个可能的测试场景以供演示;功能可以扩展:
Feature: Calculate Single Fibonacci number.
Ordinal entry should greater than 0.
Scenario: Ordinal is lower than 0.
    Given I use the native keyboard to enter "-2" into text field Ordinal
    And I touch the "Calculate" button
    Then I see the text "Ordinal cannot be a negative number."
Scenario: Ordinal is 0.
    Given I use the native keyboard to enter "0" into text field Ordinal
    And I touch the "Calculate" button
    Then I see the text "Cannot calculate the number for the 0th ordinal."
由于 Calabash 框架公开的 Ruby API 与嵌入在具有 NuGet 包的 Xamarin 应用程序中的 Xamarin Test Cloud Agent 具有双向通信线路,因此 Calabash 测试执行在 Xamarin 目标平台上是可能的。
Calabash/Cucumber 测试可以在 Xamarin Test Cloud 上针对真实设备执行,因为应用程序运行时与 Calabash 框架之间的通信由 Xamarin Test Cloud Agent 维护,这与 Xamarin.UI 测试相同。
摘要
Xamarin 项目可以从一个正确建立的开发管道和 ALM 原则的使用中受益。这种类型的做法使得团队更容易分担责任,并以迭代的方式解决业务需求。
在 ALM 时间线中,开发阶段是大多数具体实现发生的主要领域。为了使开发团队能够提供能够在 ALM 周期中存活的优质代码,强烈建议使用 Xamarin 开发 IDE 中的可用工具对原生应用程序进行分析和测试。
在 Xamarin 项目中,针对目标平台的通用代码库可以使用传统的单元测试来处理和测试,如同.NET 实现一样,但平台特定的实现需要更特别的处理。应用程序的平台特定部分需要在相应的平台模拟器或设备上的空壳应用程序,即测试框架中测试。
为了测试视图,可以使用如 Coded UI tests(适用于 Windows Phone)和 Xamarin.UITests(适用于 Xamarin.Android 和 Xamarin.iOS)等可用的框架,以增加测试代码覆盖率并为交付管道创建一个稳定的基石。
本章中讨论的大多数测试和分析工具都可以集成到自动化的持续集成过程中。用于源代码控制和持续集成构建及测试过程的基础设施将是下一章的主题。
第十一章. ALM – 项目和发布管理
本章解释了版本控制和自动化持续集成工作流程的基本知识。将为 Xamarin 项目演示版本控制选项以及自动化构建策略。还将涵盖其他主题,如实时遥测收集和测试应用分发中心。本章分为以下几节:
- 
版本控制 
- 
持续集成 
- 
自动化测试 
- 
测试版部署 
- 
实时遥测 
版本控制
无论作为团队还是个人工作,版本控制或版本管理始终是软件开发项目开发流程的基本要素。源代码仓库是描述处理代码库版本控制和合并的代码管理存储的术语。源代码仓库的附加功能可能包括但不限于分支、审查、暂存和类似的生产力相关功能。然而,这些项目适用于任何类型的软件开发项目,并且超出本书的范围。
对于 Xamarin 项目,开发者可以利用几种类型的仓库。仓库的选择通常取决于所选的环境设置(即操作系统、开发 IDE 等)。
TFVC
团队基础版版本控制(TFVC)是 Team Foundation Server 及其基于云的对应产品 Visual Studio Team Services(以前称为 Visual Studio Online)提供的本地仓库的名称。TFVC 是一个集中式版本控制系统,其中版本历史记录保存在集中式服务器仓库中,客户端每个文件只有一个版本(即工作区版本)。
TFVC 为习惯于 Microsoft 开发堆栈的 Xamarin 开发者提供了一个非常熟悉的源代码管理工具集。对于使用 Windows 和 Visual Studio 的 Xamarin 开发者来说,TFVC 是一个理想的选择,因为它与 Visual Studio 具有原生集成。源代码管理是在“签入”和“签出”操作的基础上实现的。每次代码签入还可以包括对项目元数据工件(如任务、功能、错误等)的引用。将更改集(即要签入的源代码文件集合)与项目元数据关联为团队中的开发者提供了一个理想的发展管道。
对于在 Windows 或 Mac OS 上使用 Xamarin Studio 的开发者,使用 TFVC 的唯一选项是安装 Team Explorer Everywhere。Team Explorer Everywhere 是一个 Eclipse 插件,可以安装在 Mac OS 上,并用于签入和签出源代码项。在 Windows 上使用 Xamarin Studio 的开发者仍然可以安装并使用免费版的 Visual Studio 来访问 TFS 服务器。
Git
Git与 TFVC 不同,是一个分布式版本控制系统,其中每个开发者都有一个整个源仓库的克隆,每个克隆都本地管理,直到更改发布到中央服务器。开发者也可以自由创建私有本地分支,并在不同的分支之间轻松切换。根据需要,分支可以被合并、发布或关闭。
Xamarin Studio 原生支持 Git 以及如pull、clone、commit和push等开发者命令,可以在 IDE 内执行。这种原生支持使得 Git 仓库成为使用基于 Mac 的开发环境的开发者的理想选择。
Visual Studio 也支持 Git 仓库和经典的 Pull-Commit-Push 流程。除了 Visual Studio 支持外,随着 Visual Studio Team Services 的最新更新,现在可以使用 Git 仓库创建团队项目。版本控制类型的选取不会干扰其他项目相关选项或构建设置。然而,目前无法使用与项目相关的功能(例如,将更改集与任务项关联)使用 Git 仓库。

图 1:带有 Git 的团队基金会服务器
目前也可以设置一个使用多种类型仓库的团队项目。这些仓库目前只能使用最新版本的 Visual Studio(即 Visual Studio 2015 Update 1)访问。
下一个部分提供了涉及 TFS 和 Git 仓库的更多集成选项。
TFS/Git 场景
在某些场景中,开发者可以选择或被迫使用 Git 仓库与集中式的 TFVC 仓库一起使用(例如,具有 Mac OS 开发设置的开发者没有直接与 TFVC 集成)。在这种情况下,有几种可用的实用工具和实现模式可以帮助团队准备他们的开发基础设施。
Git 桥接
可以采用的一种集成路径是微软在 CodePlex 上维护的 Git-TF 工具。Git-TF 工具是一个用 Java 编写的平台无关的工具。它利用 TFVC API 来允许开发者使用 TFS 仓库与本地 Git 仓库一起使用。
在这个集成路径中,Xamarin 开发团队的单一成员或多个成员可以使用 Git-TF 工具与中央仓库同步的本地或共享 Git 仓库。
对于个人开发者使用与 TFVC 同步的本地 Git 仓库的设置,首先需要将 TFS 仓库克隆到本地机器:
git tf clone http://myserver:8080/tfs $/TeamProjectA/Main
克隆后,可以在本地机器上使用 Git 仓库继续开发。本地提交的执行不会反映在中央仓库上。在此期间,可以使用 Git-TF 的pull命令将中央仓库与本地仓库合并:
git tf pull --rebase
一旦开发任务完成,可以使用checkin命令将代码签入 TFS(而不是git push):
git tf checkin --associate=123,124 –-message="Additional items for Task 123"
Git-TF 提供了将 TFS 上的工作项关联/解决并包含类似于标准代码签入的签入注释的选项。

图 2:Git 与 TFVC 仓库
另一种可能性是为几个团队成员配置一个共享的 Git 仓库,这样每个开发者都可以将其克隆到本地环境中并用作分支。在这种配置中,中央仓库(TFVC)和共享 Git 仓库之间的代码合并和同步必须由管理员处理。
类似地,GitHub 上维护的 Git-TFS 工具是一个用.NET 编写的开源项目,它提供了 TFS 和 Git 仓库之间的双向集成。然而,这个工具目前还没有适用于 Mac OS 的版本。Git-TFS 为与工作空间处理和 shelvesets 相关的一些高级 TFS 场景提供了支持。
NuGet 包
如前所述,NuGet 包是使用 Xamarin 的跨平台项目中的一种代码共享策略。NuGet 包还可以用来创建 TFVC 和 Git 之间的桥梁,可能通过为 Xamarin 目标平台提供 PCL 库来实现。
例如,我们可以考虑一个场景,其中 Windows Store 应用程序和 Xamarin.iOS 之间的共享项目是在基于 Windows 的开发环境中实现的,而 Xamarin.iOS 开发团队成员使用带有 Xamarin Studio 的 Mac OS 开发设置。在这个例子中,团队项目可以包括 TFVC 仓库(用于共享代码和 Windows Store 应用程序实现)和一个 Git 仓库(用于 Xamarin.iOS 开发)。两个服务器之间的同步可以通过 NuGet 包来处理。
可以使用 TFS 的内置构建任务定义来构建和部署 NuGet 包,使用持续集成(CI)构建过程,使 NuGet 过程成为开发管道和持续集成的一部分。

图 3:NuGet 包的自动化构建
对于 NuGet 包的分发,除了商业产品(例如,Artifactory 服务器)之外,Visual Studio Team Services 还可以用来创建 NuGet 源馈送并将它们私下发布给开发团队成员。
Subversion (SVN)
Subversion 是另一种源代码存储库类型,通常称为 SVN 或 Apache Subversion。使用 XCode 开发工具,可以在 Mac OS 开发环境中轻松创建 Subversion 存储库。Xamarin Studio 原生支持 SVN(版本 1.6 或更高)。对于偏好基于 Mac 的开发环境的个人 Xamarin 开发者来说,Subversion 可以是一个简单的解决方案。尽管有公开可用的 Visual Studio 扩展和集成工具,可以在 Windows 环境中使用 SVN,但通常还是更倾向于使用原生支持的 Git 和 TFVC,而不是 SVN。
持续集成
持续集成(CI)是指涉及上述源代码管理策略的软件实践,包括自动化的构建/部署和测试阶段。如今,CI 通常指的是应用程序生命周期管理(ALM)的自动化构建/部署和测试阶段。
对于 Xamarin 项目,软件工程师可以自由使用大量 CI 管理工具,这些工具既有商业版也有免费增值许可(即免费使用有限功能)。
Visual Studio Team Services
Visual Studio Team Services(VSTS)是 Team Foundation Server 的云版本,为 Xamarin 开发者提供了方便的功能。目前作为免费增值订阅服务提供,团队可以免费管理有限数量的项目,每个团队成员数量也有限。
在 VSTS 团队项目中,可以管理、规划、自动构建、测试 Git 和 TFVC 开发存储库,并且可能部署(有关 VSTS 集成的 Beta 部署部分请参阅)。

图 4:TFS 自动化的 Xamarin 构建
包含 Xamarin.iOS 和 Xamarin.Android 项目的开箱即用的构建模板可以在托管构建代理上执行。虽然后者构建模板可以使用共享托管构建代理执行,但 Xamarin.iOS 需要一个具有 Xamarin.iOS 功能的专用构建主机,以便与团队项目关联。

图 5:VSTS 托管 Android 构建代理
对于 Xamarin.Android 构建模板,开发者需要插入 Xamarin 许可证详情。然而,构建代理在构建期间不会占用许可证座位。构建定义模板包括一个激活步骤,其中构建代理被注册为 Xamarin 许可证的占用者,并在构建完成后进行另一个步骤以移除许可证。
在 VSTS 中,还可以集成 Xamarin Test Cloud,使用默认构建模板执行自动验收测试。
TeamCity
TeamCity(JetBrains)是另一个 CI 服务器,它为各种平台提供了自动构建和大量的集成场景。TeamCity 可以下载并安装到多个操作系统上(包括 OS X 和 Windows),并且作为一个免费增值产品(带有有限的免费构建代理安装和构建配置)。
对于 Xamarin 开发团队来说,TeamCity 最大的优势是它可以安装在 Mac OS 上。一旦构建服务器配置完成(它可以在运行 TeamCity 服务器的同一台机器上),就可以在 Xamarin.Android 和 Xamarin.iOS 上触发各种动作的构建,例如仓库更改和计划。

图 6:TeamCity 与 Xamarin 项目
在 Xamarin 项目中,关于可能的集成场景和构建步骤,还包括应用程序包的 Calabash 仪器化和 Xamarin Test Cloud 提交。
其他
Xamarin 开发团队有使用许多其他在线/云基础 SaaS(软件即服务)提供商的奢侈,这些提供商适用于内部和开源开发。其中最受欢迎的服务是 GitHub,它提供基于订阅的私有和公共仓库。CI 构建提供商如 AppVeyor 和 Travis CI 与 GitHub 有原生集成,可以用于各种平台特定的构建配置。
最后,Jenkins 是另一个免费和商业安装都适用的 CI 服务器。Jenkins 可以与各种仓库集成,并可以配置为构建和测试 Xamarin 项目。
自动化测试
自动化测试,换句话说,就是运行作为开发工作一部分建立的单元测试或代码 UI 测试,是大多数开发项目中持续集成周期的一个基本部分。
为了准备 Xamarin 项目的测试固定装置,开发者可以使用各种框架,如 Visual Studio 测试套件、nUnit 和 xUnit。此外,Xamarin 开发团队有从可用的源控制仓库和 CI 平台列表中进行选择的自由。幸运的是,由于上述测试框架为各种配置提供了测试适配器(除了本地的 Visual Studio 测试框架),因此这些 CI 管道的各个方面都可以轻松集成。
例如,让我们考虑一个托管在 Visual Studio Team Services 上 TFVC 仓库的 Xamarin 项目,其中单元测试固定装置是利用 xUnit 框架编写的。作为第一步,为了让 TFS 构建代理帮助 xUnit 适配器运行单元测试固定装置,测试适配器必须作为解决方案的 NuGet 包安装。

图 7:xUnit 测试适配器
在适配器包部署到源代码控制仓库后,团队构建现在可以包括使用自定义适配器的测试步骤。
在 Visual Studio 团队构建中,如果未定义自定义适配器,则测试将使用默认适配器运行。在这种情况下,构建步骤将报告找不到测试。

图 8:xUnit 测试适配器设置
在此配置中,解决方案中 NuGet 包的packages文件夹被用作源目录(例如,$(Build.SourcesDirectory)\Xamarin.Master.Fibonacci\packages\<path>)。也可以使用测试项目的binaries文件夹来访问适配器二进制文件。还应注意,在执行实际的测试用例之前,测试项目在 Visual Studio 测试任务之前的 MSBuild 任务是必不可少的。
Beta 部署
Beta 测试是 Xamarin 开发流程的一个关键部分。通过使用 Beta 测试分发中心,如 HockeyApp、Crashlytics 或 Testflight,可以将应用程序包发送给测试用户/测试者。对于 Windows Phone 8.1 和 Android,也可以使用简单的网络方法来分发应用程序包(例如,使用共享网络位置、下载链接等)。
HockeyApp
HockeyApp 作为唯一支持所有 Xamarin 目标平台(包括 Windows Runtime)的 Beta 分发中心,具有各种 CI 配置的集成能力。
初始时,Stuttgart 公司是一家为 iOS 和 Android 提供 Beta 测试平台的公司,后来扩展了其 SDK 以支持 Microsoft 移动开发平台。HockeyApp 最终被 Microsoft 收购。然而,它继续支持包括 Mac OS 在内的各种移动平台。

图 9:HockeyApp 应用仪表板
可以直接从 Web 界面上传 HockeyApp 分发中心的应用程序包。团队成员和/或测试者应将 HockeyApp 应用程序下载到他们的移动设备上,以便从服务器下载最新的包。
除了手动发布外,HockeyApp 还提供了两个公共 API:一个用于客户端,一个用于开发者。客户端 API 用于与服务器通信,以提供应用程序运行时相关的分析,而开发者 API 为开发者提供了上传和分发应用程序包所需的功能。
对于 Visual Studio Team Services(Visual Studio Online)和 Jenkins,有集成模块可以使将应用程序作为 CI 构建的一部分发布成为可能。

图 10:HockeyApp TFS 构建步骤
HockeyApp 套件还包括可以集成到 Visual Studio Team Services、Assembla、BaseCamp、BitBucket 等缺陷跟踪系统中的崩溃分析功能。
HockeyApp 提供免费和企业许可选项。
Crashlytics
Crashlytics 是另一个测试分析平台,为 Xamarin.iOS 和 Xamarin.Android 应用提供分发和崩溃报告功能。
Crashlytics 提供了与其他协作工具的集成,例如 PivotalTracker、JIRA、GitHub 和 BitBucket。它还提供了一个公开的 API,为各种集成场景提供服务钩子。
Crashlytics 最近被 Twitter 收购,并继续支持两个 Xamarin 目标平台应用。Crashlytics 目前是 Twitter fabric 开发平台的一部分,并提供免费服务。
TestFlight
TestFlight 最初是 iOS 和 Android 应用的测试平台,在被苹果收购后立即取消了 Android 应用的支持。现在它是苹果开发者计划的一部分,并且只能通过 iTunes Connect 访问。
提交给 TestFlight 的内容与实际苹果商店应用包没有区别。最终的可分发包(.ipa)应准备提交,并使用 Application Loader 上传到苹果服务器(有关更多信息,请参阅第十二章,ALM – 应用商店和发布)。
很遗憾,这个过程目前无法自动化,因为没有构建集成选项,也没有公开的 API。
软件包分发
与 iOS 设备相比,Android 和 Windows Phone 设备都可以安装和运行通过互联网或移动存储分发的应用包。
对于 Windows Phone 8 和 8.1,测试设备应使用 Windows Phone SDK 配置为开发者设备。为了开发者能够解锁 Windows Phone 设备,需要一个 Windows 开发者账户(这是一个免费订阅):

图 11:Windows Phone 开发者解锁
在注册步骤之后,开发者可以使用 SDK 工具安装应用包,或者如果有硬件支持,可以使用 SD 卡和默认的商店应用。
对于 Android 平台,有可用的免费工具可以用来安装.apk包。默认的包管理器也可以用来安装作为网络资源共享的自定义应用包。
实时遥测
实时遥测是指用于定义从目标受众或测试人员使用中的应用程序收集的分析信息。这些分析值对于功能丰富的移动应用来说是无价的,因为 Xamarin 应用可能针对运行 iOS、Android 或 Windows Phone 且具有各种硬件配置和外围设备的设备。
通过遥测,开发团队能够收集有关不同场景中用户输入模式、应用程序利用流程和平台障碍/优势的信息。虽然此类统计信息对于 UX 设计至关重要,但有关实际使用场景的崩溃/异常详细信息、网络连接、内存消耗和其他诊断数据等值可以作为应用程序的健康指标。
对于 Xamarin 目标平台,有大量的遥测提供者和框架。这些框架可以通过绑定包(例如,Android 应用程序的 Google Analytics)包含在 Xamarin 应用程序中,并且针对 Xamarin 应用程序的遥测平台(如 Xamarin Insights 和/或 Microsoft Application Insights)可以包含在 Xamarin 实现中。
Xamarin Insights
Xamarin Insights 是专为 Xamarin 目标平台构建的分析和崩溃报告平台。Xamarin Insights 的实现可以用于每个 Xamarin 平台项目,包括 Xamarin.Forms 应用程序和 Windows Runtime。这是一个基于订阅的服务,可以在基于网络的仪表板上看到实时遥测数据。
为了在跨平台应用程序解决方案中开始使用 Xamarin Insights,应在特定平台的项目中包含 Xamarin Insights NuGet 包。在引入框架客户端程序集后,可以使用订阅密钥初始化 Xamarin.Insights 运行时。
例如,如果我们使用 MVVMCross 实现将 Xamarin Insights 模块包含并初始化到 Xamarin.Android 应用程序中,初始化可以包含在应用程序设置中:
public class Setup : MvxAndroidSetup
{
    public Setup(Context applicationContext) : base(applicationContext)
    {
        Insights.Initialize("<API Key>", applicationContext);
        // Identifying the specific user, and follow the usage pattern in the rest of the execution
        var traits = new Dictionary<string, string> {
            {Insights.Traits.Email, "john.smith@contoso.com"},
            {Insights.Traits.Name, "John Smith"}
        };
        Insights.Identify("john.smith@contoso.com ", traits);
    }
}
在此实现中,Identify 方法是一个可选调用。它用于识别用户特定的特征,而不是通用使用模式。
注意
无论哪个平台运行着 Xamarin Insights 内容,应用程序都应该启用使用互联网连接(即,应用程序清单)。还建议在 Xamarin.Android 应用程序上启用诸如 BATTERY_STATS、READ_LOGS、ACCESS_WIFI_STATE 等权限,以收集更多信息。同样,在 Windows Phone 8 上,必须在记录遥测时添加 ID_CAP_IDENTIFY_DEVICE 功能来识别特定设备。
一旦初始化了 Xamarin Insights 上下文,就可以在共享库(例如,ViewModel 实现)上执行额外的报告调用。
Application Insights
Application Insights 是另一个可以与 Xamarin 应用程序一起使用的基于订阅的服务/平台。这个基于云的套件最初由微软为 Web 应用程序发布,但逐渐进入了移动应用程序领域。Application Insights 的 NuGet 包可以用于 Xamarin.Android(API 级别 15 及以上)和 Xamarin.iOS(版本 6 及以上)应用程序。功能有限的 Application Insights 可以免费用于无限数量的设备,并有限地处理数据。
Application Insights 的使用场景本质上与 Xamarin Insights 非常相似。第一步是使用平台特定的初始化器来启动遥测会话。一旦创建了遥测上下文,就可以使用TelemetryClient实例来启动自动诊断记录或向见解服务器发送手动数据:
var telemetryClient = new TelemetryClient();
 // User Action Event
 telemetryClient.TrackEvent("Calculation Completed");
 // Send a metric:
 telemetryClient.TrackMetric("Calculation Range", (ordinal2 - ordinal1));
 // Nominal values by which you can filter events:
 var nominalValues = new Dictionary<string,string> { {"calculation", "rangeCalculation"}};
 // Metrics associated with an event:
 var metrics = new Dictionary<string,int> 
         {
             {"ordinal1", ordinal1},
              {"ordinal2", ordinal2}
          };
 telemetryClient.TrackEvent("Calculation Completed", nominalValues, metrics);
与 HockeyApp 提供的崩溃分析相结合,使用 Application Insights 移动应用程序的用法统计和服务器端数据(如果有),实时遥测可以提供关于 Xamarin 应用程序的宝贵见解。
注意
Application Insights 正在逐渐被 HockeyApp 取代。这一转变最初在 2015 年 11 月的 Connect()会议上宣布。截至 2016 年 4 月,微软将停止接受 Xamarin 应用程序以及 Windows Store 和 Windows Phone 应用程序的新提交。2016 年 6 月,移动应用程序的应用程序见解数据将完全迁移到 HockeyApp。
摘要
总体而言,可用于.NET 平台的工具可以轻松地用于管理和简化开发管道任务。除了基于微软的提供之外,还有许多采用免费订阅模式的提供商。这为个人/独立开发者创造了巨大的机会。
对于源代码控制,最合理的选择是 Git 和 TFVC。虽然 TFVC 对于基于 Windows 的开发环境设置的开发者来说是一个理想的解决方案,但 Git 为 Windows 和 Mac OS 环境中的 Xamarin Studio 提供了原生集成。
不论是选择哪个仓库,Visual Studio Team Services 或其他 CI 平台,如 TeamCity,都可以用于创建自动测试和构建工作流程。
最后,beta 测试和收集的遥测数据是 Xamarin 项目的根本要素。通过实际用例和用法模式的分析数据,开发者可以微调他们的应用程序,并在实际发布前避免问题。
在最后一章中,我们将讨论 Xamarin 应用程序提交到商店的准备步骤和分发选项。
第十二章。ALM – 应用商店和发布
本章解释了与应用程序包准备和发布相关的流程,这构成了应用程序生命周期的最后一步。关于应用程序包和包的一般信息之后,接着是关于不同发布渠道和发布管理工具的信息。本章分为以下部分:
- 
发布包 
- 
分发选项 
- 
商业应用程序 
发布包
在每个 Xamarin 目标平台上,发布包与开发阶段和测试阶段准备的开发包在几个方面有所不同。发布包经过优化,以占用更少的空间并在运行时消耗更少的资源(包括处理时间和内存资源)。它们也不包含用于即时(JIT)调试所需的符号文件或进程间通信通道(例如Java 调试线协议(JDWP))。还重要的是要提到,一旦 Xamarin.iOS 和 Xamarin.Android 项目构建用于发布,它们实际上与使用原生开发工具构建的应用程序没有太大区别。
为了准备应用程序发布,开发者在实际构建应用程序之前需要采取几个准备步骤。这些步骤在每个平台上略有不同。
Xamarin.Android 应用程序包(.apk)
准备 Xamarin.Android 应用程序发布包的开发者应遵循一定的清单来创建一个优化的发布包。
禁用调试
准备 Xamarin.Android 应用程序发布的初始步骤是禁用 Xamarin 工具或adb用于与Java 虚拟机(JVM)通信的 Java 调试线协议(JDWP)调试通道。如果不禁用,此通道可能会造成安全风险。
可以通过应用程序清单或AssemblyInfo.cs文件来禁用 JDWP。为了通过应用程序清单禁用调试,需要在应用程序节点上设置android:debuggable属性为false:
<application android:label="Fibonnaci Calculator" 
              android:debuggable="false" 
              android:icon="@drawable/Icon">
</application>
AssemblyInfo.cs中的条目看起来类似:
[assembly: Application(Debuggable = false, BackupAgent = typeof(PreferencesBackupService))]
小贴士
注意,调试构建包含某些权限,如存储访问和互联网使用,会自动启用。一旦应用程序使用发布配置构建,运行应用程序进行另一轮回归测试是一个好主意,如果需要,修改应用程序清单中的显式权限声明。
链接
在开发阶段,应用程序部署通常包含整个 Xamarin.Android 运行时程序集集(无链接)。链接是一个过程,其中只将所需的组件引入到应用程序包中,以减小应用程序包的大小。在链接过程中使用了一个静态分析算法(即编译前编译),在这个过程中,会识别依赖关系并将其包含在包中。
有三个可用的选项定义了哪些组件将经过链接过程:
- 
None: 这是调试构建的默认配置值。不执行链接操作。 
- 
仅 Sdk 组件:仅链接 Xamarin.Android 运行时组件。 
- 
Sdk 和用户组件:对 Xamarin.Android 运行时组件和应用程序库进行静态分析以确定代码可达性。 ![链接]() 图 1:链接器选项 
为了确保某些类型和命名空间包含在最终包中,即使它们不是静态可访问的,也可以通过使用所需类型作为参数的公共类声明和公共方法创建必要的代码可达性(参见LinkerPleaseInclude.cs):
public class LinkerPleaseInclude
{
    public void Include(Activity act)
    {
        act.Title = act.Title + "";
    } 
}
使用链接描述文件也可以实现链接特定类型和方法。为了创建链接描述文件,应在 Xamarin.Android 项目中创建一个设置构建操作为LinkDescription的 XML 文件。LinkDescription的文件架构使用简单的声明性结构:
<?xml version="1.0" encoding="utf-8" ?>
<linker>
  <assembly fullname="Mono.Android">
    <type fullname="Android.App.Activity" >
      <method name="get_Title" />
      <method name="set_Title" />
    </type>
  </assembly>
</linker>
一旦应用程序构建完成并且包已导出,比较None、Sdk和All组件的签名apk包的大小,会发现明显的尺寸减少:

图 2:Android 链接器结果
就像发布构建一样,在链接步骤之后,强烈建议运行另一个设置回归测试,以查看应用程序功能是否按预期工作。
打包选项
重要应用程序包相关配置值可以在项目属性页的Android 选项标签页的打包部分找到。尽管在正常发布构建中,大多数配置值默认禁用,但在某些场景下,它们可能用于优化发布包。使用共享运行时和使用快速部署在正常情况下是针对调试构建的,用于提高开发者的生产力。
- 
将组件打包成原生代码:此选项指示 Mono 编译器将应用程序组件打包成一个原生共享库,作为安全措施(仅适用于企业许可证)。 
- 
为每个选定的 ABI 生成一个包(.apk):每个选定的应用程序二进制接口(ABI)将导致编译器生成一个单独的包。例如,如果选择了 armeabi-v7a 和 x86 CPU 架构,将生成两个应用程序包。 
- 
AOT 编译(实验性):提前编译将应用程序组件转换为原生代码,以减少应用程序的初始化时间,同时增加应用程序包的大小(仅适用于商业或企业许可证)。 
- 
启用多 DEX:为了绕过 DEX 方法计数限制,Android Lollipop(API 21)版本中引入了多 DEX 功能,并为 API 级别 4 到 20 发布了回溯支持库。此选项启用使用多个 DEX 文件。 注意Android 应用程序包包含一个名为 Dalvik 可执行文件(DEX)的可执行字节码文件。此文件包含应用程序运行时使用的编译代码,并且引用的方法数量有限制,为 64*210(65536)个(包括 Android 框架方法、库方法和应用程序引入的自定义代码)。 
- 
启用 ProGuard:ProGuard 是另一个可以帮助减小应用程序和 DEX 声明的选项。对于使用原生工具集开发的应用程序,ProGuard 还可以混淆应用程序代码,但此选项目前不可用于 Xamarin.Android 应用程序。 
打包
准备步骤完成后,可以使用 Visual Studio 或 Xamarin Studio 创建 Xamarin.Android 应用程序包。Xamarin Studio 提供归档构建的选项,以便它们可以轻松签名并推送到可用渠道。
可以使用项目上下文菜单中的归档以发布选项来归档应用程序包。(同样,可以使用查看归档按钮访问以前的归档。)在归档视图中,所选的应用程序包可以签名并准备好提交到商店或临时(参见分发选项)分发。

图 3:Xamarin Studio 包归档
使用 Visual Studio 的导出 Android 包选项创建的包使用调试密钥签名。这些包不应,并且在大多数情况下不能,通过正常渠道分发。为了创建发布就绪的包,应定位构建目录中的未签名包,并使用 Java SDK 中的jarsigner实用程序对包进行签名。
Xamarin.iOS 应用程序包(.ipa)
在任何 iOS 应用程序可以发布到 App Store 之前,需要配置和修改几个配置值。更重要的是,构建过程应配置为发布构建,并且在提交到 iTunes Connect 之前,包应使用适当的身份进行签名。
构建选项
对于发布构建(临时或应用商店),一旦设置了活动构建配置,一些值会自动调整为开发者的便利性。例如,与调试相关的选项,如启用性能分析和启用增量构建,会自动禁用。这些选项,连同启用调试选项一起,会产生较大的应用程序包,这些包对于商店提交无效。
除了调试选项之外,必须仔细配置支持的 CPU 架构。虽然可以组合选择(如图下所示的 ARMv7 + ARM64),但每个架构针对特定的 iPhone 或 iPad 模型。ARMv6 是 iPhone 3G 最初使用的 CPU 架构。这个架构不再被 iOS 编译器支持。从 iPhone 3GS 开始,直到 iPhone 5,包括 iPad,使用的 CPU 架构是 ARMv7。ARMv7s 和 ARM64 分别用于 iPhone 5 和 iPhone 5s。iPhone 6 使用 ARMv8,这是一种另一款 64 位处理器(即,构建要求将是 ARM64)。

图 4:iOS 构建配置
低级虚拟机(LLVM)是一组工具/库的名称,旨在为各种编程语言编写的程序进行编译时优化。它是在开源许可下发布的。在开发阶段,Xamarin 工具仅使用 mono 编译器(mtouch)。Mono 编译器生成的二进制文件优化程度较低但“可访问性”更高,这使得它们可以调试和诊断。然而,对于使用 LLVM 的发布构建,它可以生成更多优化的结果。
虽然 LLVM 提供了包大小和运行时增强,但 Thumb-2 指令集仅仅是可执行大小的改进。ARMv7 和 ARMv7s 处理器使用这个紧凑的指令集。它可以在牺牲较慢的执行时间的情况下显著减少包大小。
链接
链接的工作方式与 Xamarin.Android 平台类似。除了使用公共方法创建任意类以避免某些类被链接出来之外,在 Xamarin.iOS 上,可以在类声明上使用 Preserve 属性来通知编译器有关某个类及其成员(如 [Preserve(AllMembers = true)])的必要性。
配置文件
配置文件用于设置 iOS 应用程序的权限和包签名信息。为了创建一个可发布的 iOS 包,用户首先需要在苹果的 iOS 配置文件门户上创建应用程序元数据。
在配置文件门户上,开发者应首先选择一个独特的应用程序名称和捆绑 ID。一旦发布,这些将用于识别应用程序。此外,还需要选择应用程序所需的应用程序服务。
除了 App ID 之外,还应为应用程序创建分发配置文件。为了创建分发配置文件,需要在应用程序门户导航树中选择 Provision->Distribution 节点。使用+按钮可以创建一个新的分发配置文件。在分发配置文件向导中,用户需要选择分发类型(即,App Store或Ad Hoc),选择之前创建的 App ID,可能的部署设备和签名证书(可以从苹果的会员中心请求签名证书)。
一旦创建了 App ID 和配置文件,应在 Xamarin.iOS 项目的 iOS应用设置部分设置应用程序元数据(Info.plist和Entitlements.plist文件也可以直接配置)。
最后,可以使用发布存档按钮来创建发布包。一旦构建完成,新的包将在存档窗口中显示。选择正确的应用程序并使用签名和分发选项将打开发布向导,在此向导中可以选择并应用之前配置的配置文件到当前构建包。
Windows Phone 应用程序包 (.appx)
使用 Visual Studio 中为 Windows 应用程序开发者提供的工具集准备 Windows Phone 和 Windows Store 应用程序包。在发布准备阶段,Windows Phone 应用程序不需要或与任何 Xamarin 组件交互。
分发选项
与测试版构建一样,Xamarin 应用程序的发布版本也有不同的分发选项。公共应用商店是分发移动应用程序最简单、最方便的方式,面向大众。另一方面,对于企业应用程序的分发场景,可能需要私有应用程序分发渠道。
应用商店
对于 Xamarin.iOS 和 Windows Phone 应用程序,唯一的官方分发商店分别是苹果和微软分别维护的应用商店。这些应用商店都有明确的提交流程,包括内容验证(即,应用程序是否符合内容指南)和技术验证(即,应用程序是否符合质量标准)。在将发布包提交给这些商店之前,强烈建议阅读适当的应用程序认证指南。为了使用 iTunes Connect 工具和 Apple App Store 分发应用程序,开发者需要申请开发者账户并支付年度订阅费。Windows App Store 需要开发者账户订阅,目前是免费的。
另一方面,Android 开发者有大量的公共应用程序分发渠道可供选择。最受欢迎的商店是 Google Play 和 Amazon 应用商店。这两个商店都允许开发者发布付费和免费的应用程序。
Google Play 商店是 Android 操作系统的官方应用程序商店。它最初被称为 Android Market,后来与另外两个 Google 产品合并,即 Google 音乐和 Google 电子书商店。Google Play 商店要求开发者支付小额订阅费才能分发应用程序。安全和质量测试是应用程序认证过程中最重要的步骤之一,这使得该商店在 Android 用户中最为可信。
另一方面,Amazon 应用商店最初是为 Amazon Kindle Fire 设备专门创建的,但后来成为 Android 应用程序的第二大商店。开发者可以免费注册开发者账户,并且收入分成模式与其他流行商店相同(即,70% 开发者/30% 商店)。
除了两个最大的商店外,还有其他 Android 应用程序商店。最引人注目的应用程序商店提供商是 F-Droid 商店,它专注于 Android 操作系统的免费和开源软件(FOSS)。由于商店政策规定分发应用程序中不包含跟踪、广告或依赖项,因此该商店吸引了众多用户。
临时分发
临时分发是指通过各种通信渠道(如共享存储、在线分享、电子邮件等)将应用程序包分发给用户进行测试或私人使用的过程的名称。
这种分发方式在前一章的测试部分中提到过,但有时仅用于内部使用构建的应用程序也可以以这种方式分发。
临时分发概念可以分为两大类:签名分发和无签名分发。在空中分发应用程序包的官方方式是使用受信任的证书对应用程序包进行数字签名(即,签名身份应使用官方渠道创建,例如签名证书提供商)。一旦应用程序包使用受信任提供商的证书进行了数字签名,该应用程序就可以侧载到移动设备上。侧载是指在不使用公共或私人商店的情况下安装应用程序的过程。
如果应用程序使用自签名证书进行签名,应用程序发布者将无法识别。在这种情况下,设备所有者应允许从未知来源安装应用程序(在 Android 和 Windows 10 移动设备上),或者设备应解锁为开发者模式(在 Windows Phone 上)或越狱(在 iOS 上)。虽然在 Windows Phone 设备上解锁设备是官方流程,但越狱违反了 iOS 的最终用户许可协议。
商业线应用程序
商业线应用程序,或 LOB 应用程序,是一个通常与企业应用程序同义的术语。这些应用程序要么是内部开发的,要么是针对公司特定需求外包的。换句话说,LOB 应用程序可以归类为商业而非消费应用程序。它们通常是特定领域的,针对具有特定需求的小组。
私有渠道分发(Android)
分发为 Android 平台构建的 LOB 应用程序的一种方式是使用 Google Play 私有渠道。通过这些渠道分发的应用程序仅限于特定域的用户。为了使用私有渠道,需要订阅 Google Play for Work、Google Apps for Business、教育或政府。
虽然应用定价和其他分发设置可能仍然适用于这些私有应用程序,但在商店提交过程中会跳过测试和验证步骤。应用提交可以由渠道所有者完成,或者权限可以委派给同一域内的其他用户。
苹果开发者企业计划
苹果开发者企业计划是苹果支持公司开发和分发内部应用程序的举措。此计划仅适用于作为法律实体存在的公司(需要 D-U-N-S 号码)。一旦组织注册了企业计划,开发和管理团队成员可以分配角色,以及数字证书和配置文件。然而,这些配置文件不能包含 App Store 分发方法(即,唯一可用的配置文件是内部和临时的)。

图 5:苹果开发者企业计划的配置文件
在企业计划下构建的应用程序可以通过原生或第三方移动设备管理(MDM)解决方案以及临时包进行分发。
Windows Phone 私有分发
Windows Phone 应用程序可以用于内部使用进行开发和分发,使用从赛门铁克(Symantec)购买的签名证书(赛门铁克目前是此类证书的唯一提供者)。使用移动签名证书,应用程序包可以签名并通过 MDM 分发,或者侧载到公司设备中。
大多数 MDM 提供商,如 Microsoft Intune,都配备了公司商店应用程序,可用于为公司设备提供应用程序。设备管理系统还使直接为域用户安装公司应用程序成为可能。
还可以在设备上安装签名证书,这将使内部应用程序受益,并通过定制的公司中心应用程序分发应用程序。
摘要
在本章中,我们简要介绍了 Xamarin.Android 和 Xamarin.iOS 应用(以及 Windows Phone)的发布包准备过程。正如您所看到的,准备发布包比在所选的开发 IDE 上按下调试按钮要复杂一些。然而,每个平台都有明确的应用认证指南和在线资源。
一旦准备完成发布包,开发者需要从不同的分发选项中进行选择,包括但不限于可以用来发布发布包的公共和私有存储库。公共存储库应用可以交付给公众,而私有分发渠道或临时部署,涉及侧载和 MDMs,可以用于 LOB 应用。

 
                    
                     
                    
                 
                    
                

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号