Xamarin-精要-全-

Xamarin 精要(全)

原文:zh.annas-archive.org/md5/6891d4b27fa24e2e6240e115f2792f91

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

移动应用程序已经彻底改变了我们沟通和相互交往的方式,它们的开发现在正开始达到一定程度的成熟。为了支持移动应用程序的开发,现在有许多工具和环境可用。

Xamarin 是一套近年来越来越成功的工具集,并且越来越受到关注,尤其是那些在 .NET 和 C# 资源上有重大投资的开发店铺。Xamarin 使用 C# 包装器包装每个平台的本地 API,允许开发者以与任何本地开发者几乎相同的方式与环境交互。由于 Xamarin 应用是用 C# 开发的,因此跨平台共享代码的新可能性就出现了,带来了所有相关的优势和挑战。

随着公司寻求采用 Xamarin,将需要新的 Xamarin 开发者;他们从哪里来?在许多情况下,将会有已经熟悉 Android 和 iOS 开发的资深移动开发者。

这就是这本书的用意所在;目的是为已经熟悉 Android 和/或 iOS 开发的开发者提供一条快速通道,以便他们能够掌握 Xamarin 开发。为此,本书不专注于开发 Android 和 iOS 应用的基础知识;相反,我们专注于教授经验丰富的 Android 和 iOS 开发者如何使用 Mono、C# 和 Xamarin 工具套件开发应用程序。具体来说,我们重点关注以下主题:

  • 架构:这解释了 Xamarin 产品如何允许使用 Mono 和 C# 开发 Android 和 iOS 应用

  • 工具:这描述了提供的工具以支持应用程序的开发

  • 代码共享:这解释了可以在 Android 和 iOS 应用之间共享的代码类型以及可能出现的问题

  • 分发:这解释了在分发 Xamarin.Android 和 Xamarin.iOS 应用时应考虑的特殊注意事项

应该注意的是,在适当的地方提供了示例应用程序和代码片段。

当我最初开始使用 C# 开发 iOS 应用时,感觉有点奇怪。我并不是 Objective-C 的粉丝,但 C# 何时成为了跨平台工具的首选?我总是非常尊重 Mono 团队所取得的成就,但我通常认为微软最终会禁止 C# 和 .NET 在他们不拥有的任何平台上取得巨大成功。作为一个《星球大战》迷,以及某种程度上的一位极客,我被提醒起《星球大战》第三集中的一段对话。如果你还记得安纳金和帕帕廷之间的一段对话,其中安纳金意识到帕帕廷知道原力的黑暗面;只需将原力的黑暗面替换为 Xamarin,帕帕廷就会转向你说:“Xamarin 是一条通往许多被认为是不自然的能力的道路。”那基本上就是我的感觉;我是不是在为了学习一套最终会完全把我绑定到 Windows 上的跨平台技术而妥协?

两年后,我觉得自己可以比较自信地回答这个问题了,答案是!显然,我们工作在一个动态的行业,事情可能瞬间发生变化,但与 10 年前相比,技术世界已经发生了变化,跨平台的 C#和.NET 似乎现在对微软有利。因此,这种奇怪的感觉随着成功而减弱,看到微软和 Xamarin 之间的关系一直从强到更强,我感到鼓舞。

如果您来自 Objective-C 或 Java 背景,您可能会时不时地有同样的感受,但如果您给这些工具一个机会,我认为您会感到惊讶。

我希望您能将这本书视为您成为使用 Xamarin 产品套件成为高效移动应用开发者的宝贵资源。

本书涵盖的内容

第一章, Xamarin 和 Mono – 通向非自然之路,提供了 Mono 项目和 Xamarin 提供的基于 Mono 的商业产品套件的概述。

第二章, 揭秘 Xamarin.iOS,描述了 Mono 和 iOS 平台如何共存,并允许开发者使用 C#构建 iOS 应用。

第三章, 揭秘 Xamarin.Android,描述了 Mono 和 Android 平台如何共存,并允许开发者使用 C#构建 Android 应用。

第四章, 使用 Xamarin.iOS 开发您的第一个 iOS 应用,将引导您了解创建、编译、运行和调试一个简单的 iOS 应用的过程。

第五章, 使用 Xamarin.Android 开发您的第一个 Android 应用,将引导您了解创建、编译、运行和调试一个简单的 Android 应用的流程。

第六章, 分享游戏,展示了在 Xamarin.iOS 和 Xamarin.Android 应用之间共享代码的各种方法。

第七章, 与 MvvmCross 共享,引导您了解使用 Xamarin.Mobile 应用,它提供了一个跨平台的 API 来访问位置服务、联系人和设备摄像头。

第八章, 与 Xamarin.Forms 共享,引导您了解使用 MvvmCross 框架的基本知识,以增加平台间的代码复用。

第九章, 准备 Xamarin.iOS 应用以分发,讨论了各种分发 iOS 应用的方法,并引导您了解准备 Xamarin.iOS 应用以分发的流程。

第十章,为分发准备 Xamarin.Android 应用,讨论了分发 Android 应用的多种方法,并指导您准备 Xamarin.Android 应用分发的整个过程。

本书所需

本书包含 Android 和 iOS 示例。创建和运行所有示例的最简单配置是拥有一台基于 Intel 的 Mac,运行 OS X 10.8(Mountain Lion)或更高版本的操作系统,并安装 Xcode、iOS SDK 7.x和 Xamarin。由于 Xamarin 默认安装 Xamarin.iOS 和 Xamarin.Android,因此可以使用 Xamarin 的 30 天试用版。

以下点提供了基于特定功能和配置的详细要求。

要创建和执行本书中的 iOS 示例,您需要以下内容:

  • 运行 OS X 10.8(Mountain Lion)或更高版本的基于 Intel 的 Mac

  • 已安装 Xcode 和 iOS SDK 7 或更高版本

  • 已安装 Xamarin.iOS;30 天试用版可供使用

  • iPad 或 iPhone 可能有所帮助,但不是必需的

要使用 Xamarin.iOS 的 Visual Studio 插件,您需要以下内容:

  • 运行 Windows 7 或更高版本的 PC

  • 已安装 Visual Studio 2010、2012 或 2013;任何非 Express 版本

  • 已安装 Xamarin.iOS;30 天试用版可供使用

  • 连接到 Mac 的网络连接,满足编译和运行应用的要求

要创建和执行本书中的 iOS 示例,您需要以下内容:

  • 运行 Windows 7 或更高版本的 PC,或运行 OS X 10.8(Mountain Lion)或更高版本的基于 Intel 的 Mac

  • 已安装 Xamarin.Android;30 天试用版可供使用。Xamarin.Android 包括 Android SDK

  • 安卓手机或平板电脑可能有所帮助,但不是必需的

要使用 Xamarin.Android 的 Visual Studio 插件,您需要以下内容:

  • 运行 Windows 7 或更高版本的 PC

  • Visual Studio 2010、2012 或 2013;任何非 Express 版本

  • 已安装 Xamarin.Android;30 天试用版可供使用。Xamarin.Android 包括 Android SDK

本书面向的对象

本书是移动开发者宝贵的资源,他们已经熟悉 Android 和/或 iOS 开发,并需要快速掌握 Xamarin 开发。假设您有 Android、iOS 和 C#的背景。本书提供了 Xamarin 架构的概述,指导您创建和运行示例应用的过程,演示了 Xamarin 提供的工具的使用,并讨论了为分发准备应用的特殊注意事项。

习惯用法

在本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词如下所示:“在SetContentView()语句上设置断点。”

代码块设置如下:

protected string GetFilename()
{
  return Path.Combine (
       Environment.GetFolderPath(
           Environment.SpecialFolder.MyDocuments),
          "NationalParks.json");
}

新术语和重要词汇将以粗体显示。你会在屏幕上、菜单或对话框中看到这些词汇,例如:“从项目菜单中选择发布 Android 项目。”

注意

警告或重要注意事项将以这样的框显示。

小贴士

小贴士和技巧看起来像这样。

读者反馈

我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或可能不喜欢什么。读者反馈对我们开发你真正能从中获得最大收益的标题非常重要。

要发送给我们一般反馈,只需发送一封电子邮件到feedback@packtpub.com,并在邮件的主题中提及书名。

如果你有一本书需要我们出版,请通过www.packtpub.com上的建议标题表单发送给我们,或者发送电子邮件到suggest@packtpub.com

如果你在一个你擅长的主题上有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在你已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助你从你的购买中获得最大收益。

下载示例代码

你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。

勘误

尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果你在我们的书中发现错误——可能是文本或代码中的错误——如果你能向我们报告这一点,我们将不胜感激。这样做可以帮助其他读者避免挫败感,并帮助我们改进本书的后续版本。如果你发现任何勘误,请通过访问www.packtpub.com/support,选择你的书籍,点击勘误提交表链接,并输入你的勘误详情来报告它们。一旦你的勘误得到验证,你的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分中。

侵权

互联网上版权材料的侵权是一个持续存在的问题,涉及所有媒体。在 Packt,我们非常重视我们版权和许可证的保护。如果你在网上遇到任何我们作品的非法副本,无论形式如何,请立即向我们提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过copyright@packtpub.com与我们联系,并提供疑似侵权材料的链接。

我们感谢您在保护我们作者以及为我们提供有价值内容方面的帮助。

问题

如果你在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。

第一章. Xamarin 和 Mono – 通向非自然之路

本章概述了 Mono 项目以及 Xamarin 提供的基于 Mono 的商业产品套件。为了开始这段通往未知领域的旅程,本章将涵盖以下主题:

  • 理解 Mono

  • 为什么你应该使用 Xamarin

  • 安装 Xamarin.Android 和 Xamarin.iOS

  • 使用 Xamarin Studio 和 Visual Studio 进行开发

  • 源代码控制选项

理解 Mono

在我们直接进入关于 Xamarin 的对话之前,让我们先讨论一个相关话题:Mono。Mono 是.NET 平台的开源跨平台实现。这包括一个与 Microsoft .NET 二进制兼容的公共语言运行时CLR),一组用于 C#等语言的编译器,以及.NET 运行时库的实现。Mono CLR 已被移植到许多平台,包括基于 Linux 和 BSD 的操作系统(不仅限于 Android、iOS 和 OS X),基于 Windows 的系统,甚至一些游戏机,如 Wii、Xbox 360 和 PS4。

Mono 项目是由 Ximian 发起的,而 Ximian 后来被 Novell 收购。现在 Xamarin 领导 Mono 项目。

Xamarin 产品套件

Xamarin 是一家软件公司,提供一系列基于 Mono 的商业产品,允许开发者使用 C#和.NET 框架为 Android、iOS 和 OS X 创建应用程序。Xamarin 的联合创始人 Miguel de Icaza 自 2001 年 Mono 项目启动以来一直领导该项目。

Xamarin 的主要产品提供包括:

  • Xamarin.Android(以前称为 Mono for Android)

  • Xamarin.iOS(以前称为 MonoTouch)

  • Xamarin.Mac

每个这些产品都通过年度订阅进行授权,以下级别可供选择:

  • 入门级:这个订阅实际上是免费的,但限制了应用程序的大小。

  • 独立开发者版:这个订阅提供了构建和部署功能齐全的移动应用程序所需的一切,但只能由员工人数不超过五人的公司购买。这个订阅也不包括在“使用 Visual Studio 环境”部分中讨论的 Visual Studio 插件的使用。

  • 商业版:这个订阅增加了 Visual Studio 插件的使用以及电子邮件支持。

  • 企业版:这个订阅增加了对一系列高级组件、热修复和增强支持的访问权限。

    注意

    可以在本文节的末尾找到定价信息的链接。为了快速参考,请访问store.xamarin.com

Xamarin 还托管了一个组件商店;一个可以用于 Xamarin 应用程序的组件买卖市场。这个商店中的组件可以免费分发或出售,Xamarin 会向组件供应商支付从销售中收集到的收入的一部分。

另一项 Xamarin 提供的服务是测试云。正如其名所示,这是一个基于云的测试服务,允许团队为他们的应用创建自动化测试能力,这些测试可以在大量物理设备上运行。这对于 Android 开发者尤为重要,因为需要考虑的设备数量要多得多。

以下表格提供了有关 Xamarin 套件的附加信息的有用链接:

信息类型 访问它的 URL
产品信息 xamarin.com/tour xamarin.com/csharp xamarin.com/products xamarin.com/faq
产品定价 store.xamarin.com
组件商店 components.xamarin.com
Xamarin 测试云 xamarin.com/test-cloud

评估 Xamarin 是否是合适的工具

现在你对 Mono 和 Xamarin 产品套件有了背景知识,你可能想问自己:“Xamarin 是我的项目的合适工具吗?”

使用 Xamarin 的好处如下:

  • 基于您现有的 C#和.NET 使用技能构建:由于 C#语言和.NET 框架都提供了大量的功能,开发者需要投入大量的时间和精力来掌握它们。尽管你可以争论 Java 和 Objective-C 有相似之处(都是面向对象的语言),但将 C#和.NET 的熟练度转移到 Java 或 Objective-C 上确实存在实际成本。这就是 Xamarin 能帮到你的地方;那些在 C#和.NET 上投入了大量资金的个人和团队,如果他们希望开发 iOS 和 Android 应用,由于这些技能的需求,可能会转向使用 Xamarin。

  • 允许跨平台开发中代码的可重用性:尽管 Xamarin 套件阻止你创建一个可以部署到 Android、iOS 和 WP8 的应用,但它通过提供跨所有这些平台回收大量代码库的能力来弥补这一点。使这一切变得如此容易的一般过程是,用户界面代码和处理设备功能的代码通常是针对每个平台编写的。有了这个,客户端逻辑(代理和缓存)、客户端验证、数据缓存和客户端数据存储等事物可能被重用,这可以为你节省大量的时间和精力。我亲眼看到 Android 和 iOS 应用共享高达 50%的代码库,有些报告甚至高达 80%。你在这个重用方法上的投资越多,你实现更高百分比的可能性就越大。

然而,在使用 Xamarin 时也有一些缺点:

  • 由于许可要求而产生的成本:Xamarin 套件或工具都是商业工具,必须获得许可,这意味着有明显的入门成本。您可以在 Xamarin 的网站上查看当前的价格。

  • 等待更新:您会发现平台(Android/iOS)的新版本与支持它的 Xamarin 产品的新版本之间存在一些滞后时间。Xamarin 在操作系统的新版本发布当天就发布了 Xamarin.iOS,做得非常出色。Xamarin.Android 通常落后于这个步伐,因为谷歌不提供 beta/preview 版本。在某种程度上,这种延迟至少对于手机应用来说不是一个大问题;电信公司通常需要一段时间才会提供最新的 Android 版本供下载。

  • 性能和内存管理:这可能是 Xamarin.Android 比 Xamarin.iOS 更关心的问题。正如您将在第二章中看到的那样,揭秘 Xamarin.iOS,Xamarin.iOS 基本上构建了一个与仅使用 Xcode 和 iOS SDK 产生的二进制可执行文件类似的二进制可执行文件。然而,正如我们将在第三章中看到的那样,揭秘 Xamarin.Android,Xamarin.Android 依赖于部署 Mono CLR 和 Mono CLR 与 Dalvik VM 之间的通信。在某些情况下,Xamarin.Android 将分配 Java 和 C#对象来实现 C#或.NET 在 Android 设备上开发的一些“魔法”和“巫术”。因此,Xamarin.Android 将影响内存占用和执行性能。

  • 分发大小:有许多运行时库必须与 Xamarin 应用一起分发或链接。关于实际大小和最小化分发大小的策略将在最后一章中讨论。

虽然缺点列表可能看起来很庞大,但在大多数情况下,每个缺点的影响都可以最小化。我选择建立 Xamarin 咨询业务,因为我高度重视已识别的益处,并觉得许多在 C#和.NET 上有重大投资的团体也会看到同样的价值。如果您是高度重视 Xamarin 益处的团体或个人,那么您当然应该考虑使用它。

学习 C#

本书假设您对 C#和.NET 有实际的知识。由于这可能不是一些读者的情况,我们包括了一些链接,以帮助您快速掌握。Xamarin 提供了一个链接,从 Objective-C 的角度介绍了 C#:docs.xamarin.com/guides/ios/advanced_topics/xamarin_for_objc/primer

微软提供了一系列学习 C#的教程,可在以下网址找到:msdn.microsoft.com/en-us/library/aa288436(v=vs.71).aspx

安装 Xamarin

在继续之前,我们需要安装 Xamarin。本节将向您展示在 Android 和 iOS 平台上安装 Xamarin 的步骤,特别是 Xamarin.Android 和 Xamarin.iOS,在 OS X 和 Windows 上。

由于 Xamarin.iOS 依赖于最新的 iOS SDK 和最新的 Xcode,因此在开始 OS X 安装之前,这两个都应该安装。

小贴士

Xcode 和 iOS SDK 都是免费的,您可以从:developer.apple.com/devcenter/ios/index.action#downloads 下载这些安装程序。

此外,请注意您可以从 OS X 应用商店安装 Xcode。

同样,Xamarin.Android 依赖于最新的 Android SDK;然而,区别在于,Xamarin 安装将自动下载 Android SDK 并将其作为整体安装过程的一部分安装。因此,不需要采取任何单独的步骤。如果您已经安装了 Android SDK,您现在有机会使用它。

在 OS X 上安装 Xamarin

要在 OS X 上安装 Xamarin,请访问 www.Xamarin.com,下载 OS X 安装程序以启动它,并按照指示操作。请确保选择安装 Xamarin.Android 和 Xamarin.iOS;Xamarin.Mac 是可选的。

Xamarin.iOS Visual Studio 插件使用名为 mtbserver 的构建服务器在您的 Mac 上编译 iOS 代码。如果您计划使用 Visual Studio 插件,请确保选择允许网络连接。

在 Windows 上安装 Xamarin

现在,我们转到 Windows 安装过程。如果您计划使用 Visual Studio 插件,那么在安装 Xamarin 之前需要安装 Visual Studio。

要在 Windows 上安装 Xamarin,您需要访问 www.Xamarin.com,下载 Windows 安装程序,启动它,然后按照指示操作。请确保安装 Xamarin.Android 和 Xamarin.iOS。

开发环境

当谈到集成开发环境(IDE)时,开发者有两个选择:Xamarin Studio 或 Visual Studio。本节将向您展示如何通过这两个工作室开发适用于 Android 和 iOS 的应用程序。

使用 Xamarin Studio 环境

Xamarin Studio 是 MonoDevelop IDE 的定制版本,这可以用来开发 Android、iOS 和 OS X 的应用程序。Xamarin Studio 可在 OS X 和 Windows 上使用,具有高度先进和实用的功能,例如:

  • 代码补全

  • 智能语法高亮

  • 代码导航

  • 代码提示

  • 在模拟器或设备上运行的应用程序的集成调试

  • Git 和 Subversion 内置的源代码控制集成

如果您查看以下截图,您将看到 Xamarin Studio 在 Android 用户界面设计器打开时的样子:

使用 Xamarin Studio 环境

使用 Xamarin Studio 开发 Android 应用程序

Xamarin Studio 和 Xamarin.Android 插件允许在不使用任何其他 IDE 的情况下进行 Android 应用的完整开发和调试。Android UI 设计器也可以在 Xamarin Studio 内部使用。

使用 Xamarin Studio 开发 iOS 应用程序

Xamarin Studio 和 Xamarin.iOS 插件允许在安装了 Xcode 和 iOS SDK 的 Mac 上开发和使用 iOS 应用程序。所有代码都可以在 Xamarin Studio 内部编写、编译和调试。通常,用户界面 XIB 和/或 storyboard 文件必须在 Xcode 内部构建;Xamarin Studio 提供了一个链接到 Xcode,这样当双击 xib 或 storyboard 文件时,Xcode 将被启动。

这里有一个需要注意的地方;Xamarin 为 Xamarin Studio 开发了一个 iOS UI 设计器,但几乎一年时间都处于 alpha 状态。我看到了许多在各种论坛上的帖子,表明它很稳定并且可以使用,但 Xamarin 在澄清为什么它仍然处于 alpha 状态以及何时将转为稳定状态方面进展缓慢。我们将在第四章使用 Xamarin.iOS 开发您的第一个 iOS 应用中更详细地讨论 iOS UI 设计器的使用。

使用 Visual Studio 环境

Xamarin for Visual Studio 是一个支持开发 Xamarin.Android 和 Xamarin.iOS 应用程序的插件,并且对商业和企业订阅者开放。此插件可以与任何非 Express 版本的 Visual Studio 2010 到版本 2013 一起使用。Android 应用程序可以完全使用 Visual Studio 进行开发。为了开发 iOS 应用程序,你仍然需要一个装有 iOS SDK 和 Xcode 的 Mac 来编译和创建用户界面 xib 和/或 storyboard 文件。

小贴士

如果你已经拥有 Visual Studio 许可证并且熟悉该环境,由于它易于使用,此插件将比 Xamarin Studio 更适合你。

以下截图显示了打开 Android 用户界面设计器的 Visual Studio 2012:

使用 Visual Studio 环境

Android 用户界面设计师

使用 Visual Studio 开发 Android 应用程序

Visual Studio 为 Xamarin.Android 提供的插件允许在不使用任何其他 IDE 的情况下进行 Android 应用的完整开发和调试。此插件提供了在 Visual Studio 内部使用 Android UI 设计器的功能。对于那些拥有适当许可证并且熟悉 Visual Studio 的人来说,这可能是在 Android 开发中最好的选择。

使用 Visual Studio 开发 iOS 应用程序

用于 Xamarin.iOS 的 Visual Studio 插件允许你开发并测试 iOS 应用程序,但仅限于与安装了 Xcode 和 iOS SDK 的 Mac 一起使用。iOS 代码必须在 Mac 上使用 Xamarin 的 mtbserver 编译和执行。Mac 上的 Xcode 还必须用于开发 iOS 应用程序的用户界面 xib 和/或 Storyboard 文件。我们将在第四章中更详细地讨论这种配置,使用 Xamarin.iOS 开发您的第一个 iOS 应用程序

备注

由 Xamarin Studio 创建和使用的解决方案和项目文件与 Visual Studio 完全兼容。这为团队提供了选择使用哪个 IDE 的灵活性,并且他们可以在项目持续期间轻松地更改。

比较 IDE

以下表格显示了采用每个 IDE 的优缺点:

IDE 优点 缺点
Xamarin Studio 可用于所有 Xamarin 订阅级别,在 Windows 和 OS X 上运行 可用的生产力插件数量有限,不支持使用 TFS 进行源代码控制
Visual Studio 大多数 C#开发者已经熟悉并习惯于 Visual Studio,可以使用流行的生产力插件,如 ReShaper 和 CodeRush,可以使用 TFS 进行源代码控制和问题跟踪 需要 Xamarin 的商业或企业订阅,需要 VS 许可证,仅在 Windows 上运行,对于 iOS 开发,需要更复杂的配置,其中 VS 必须与运行 Xcode 的 Mac 通信以执行构建,并且 UI 开发必须使用 Xcode 进行

版本控制

当你有一套多样化的开发工具时,版本控制可能是一个挑战,Xamarin 无疑为大多数商店增加了多样性。挑战在于使所有不同的 IDE 和客户端应用程序中的代码共享和管理变得容易;很多时候,他们无法访问相同的存储库。由于使用 Xamarin 的好处对现有的.NET 商店非常有吸引力,许多 Xamarin 开发者会发现自己在已经承诺使用 Microsoft 团队基础服务器 (TFS)的环境中工作。不幸的是,从非 Microsoft 工具连接到 TFS 并不总是容易。在 Xamarin Studio 的情况下,有一个开源插件,Xamarin 无法直接支持,并且配置起来可能具有挑战性。

其他可以考虑的版本控制系统包括 Git 和 Subversion。Xamarin Studio 内置了对 Git 和 Subversion 的支持,并且这两个工具的插件都适用于 Visual Studio。以下表格包含了一些有用的 URL,可以下载并了解各种插件的详细信息:

插件 访问它的 URL
TFS add-in for Xamarin Studio github.com/Indomitable/monodevelop-tfs-addin
Git for Visual Studio (VS2013 内置支持)msdn.microsoft.com/en-us/library/hh850437.aspx(VS2012 需要免费插件)visualstudiogallery.msdn.microsoft.com/abafc7d6-dcaa-40f4-8a5e-d6724bdb980c
Visual Studio 的 Subversion 插件(由 VisualSVN 提供) www.visualsvn.com/visualsvn/?gclid=CMmSnY-opL0CFa07MgodDksA5g

软件开发的许多方面一样,并没有一个“一刀切”的解决方案。以下表格概述了在选择适用于 Xamarin 项目的源代码控制解决方案时需要考虑的一些优缺点:

VCS 工具 优点 缺点
TFS 已被许多企业使用,将考虑为 Xamarin Studio 提供的 Xamarin.Free 插件。 Xamarin Studio 插件过去一直被认为使用起来有问题。
Git 在 Xamarin.Free 插件中内置支持,适用于 Visual Studio 2012 和 2013。 在一个可能使用 TFS 进行 C# 代码的大组织中,难以与其他团队共享和同步代码。
Subversion 在 Xamarin Studio 中内置支持。Visual Studio 的商业插件。 在一个可能使用 TFS 进行 C# 代码的大组织中,难以与其他团队共享和同步代码。

如果你已经在使用 TFS 上投入了大量资源,尝试让这些工作也适用于你的 Xamarin 开发。这可以通过让开发者使用 Visual Studio 或者尝试使用适用于 Xamarin Studio 的 TFS 插件来实现。

摘要

在本章中,我们介绍了 Mono 以及 Xamarin 提供的商业产品套件,并考虑了使用 Xamarin 的优缺点。我们还介绍了安装过程,并初步了解了开发者可用的 IDE 选项。

在下一章中,我们将探讨 Xamarin.iOS 产品的架构。

第二章. 消除对 Xamarin.iOS 的神秘感

现在我们对 Mono 和 Xamarin 有了基本的了解,让我们深入探讨一下 Xamarin.iOS 的工作原理。本章涵盖了以下主题:

  • Xamarin.iOS 和 AOT 编译

  • Mono 组件

  • Xamarin.iOS 绑定

  • Xamarin.iOS 应用的内存管理

  • XIB 和故事板代码生成

  • Xamarin.iOS 设计器

Xamarin.iOS 和即时编译

与大多数 Mono 或 .NET 应用不同,Xamarin.iOS 应用是静态编译的,编译是通过 Mono 的 即时编译AOT)功能完成的。AOT 用于满足苹果的要求,例如,iOS 应用用于编译,避免使用即时编译功能或在虚拟机上运行。

使用 AOT 编译在 C# 语言方面带来了一些限制。在讨论了将 iOS 绑定到 C# 和 .NET 的方法之后,这些限制将更容易讨论。这就是为什么我们将这个主题推到了本章后部分的 使用 AOT 编译的限制 部分。

注意

有关 Mono AOT 编译的更多信息,请参阅以下链接:

www.mono-project.com/AOT#Full_AOT

理解 Mono 组件

Xamarin.iOS 随带一个扩展的 Silverlight 和桌面 .NET 组件集。这些库为开发者提供了 .NET 运行时库支持,包括 System.IOSystem.Threading 等命名空间。

Xamarin.iOS 与为不同配置编译的组件不兼容,这意味着您的代码必须重新编译以生成针对 Xamarin.iOS 配置的组件。如果您针对其他配置,如 Silverlight 或 .NET 4.5,也需要做同样的事情。

提示

有关随 Xamarin.iOS 一起提供的组件集的完整列表,请参阅以下链接:

docs.xamarin.com/guides/ios/under_the_hood/assemblies

Xamarin.iOS 绑定

在本节中,您将发现支撑 Xamarin.iOS 的主要力量之一。它附带了一套绑定库,为 iOS 开发提供支持。接下来,我们将详细介绍这些绑定的一些细节。

设计原则

一系列目标或设计原则指导了绑定库的开发。这些原则对于使 C# 开发者在 iOS 开发中保持高效至关重要。以下是对设计原则的总结:

  • 允许开发者以与其他 .NET 类相同的方式子类化 Objective-C 类

  • 提供调用任意 Objective-C 库的方法

  • 将常见的 Objective-C 任务转化为更简单的事情,同时使困难的 Objective-C 任务变得可完成

  • 将 Objective-C 属性暴露为 C# 属性,同时暴露强类型 API

  • 当可能时,使用原生 C# 类型代替 Objective-C 类型

  • 支持 C# 事件和 Objective-C 委托,并将 C# 委托暴露给 Objective-C API

    提示

    本节为您提供了需要牢记的原则的一般概念。如果您想找到完整的讨论,可以参考以下链接提供的官方文档:

    docs.xamarin.com/guides/ios/under_the_hood/api_design/

C# 类型与类型安全性

Xamarin.iOS 绑定旨在使用 C# 开发者熟悉的类型,并在可能的情况下提高类型安全性。

例如,API 使用 C# 字符串而不是 NSString,这意味着 UILabel 中的文本属性在 iOS SDK 中定义如下:

@property(nonatomic, copy) NSString *text

此外,在 Xamarin.iOS 中是这样暴露的:

public string Text { get; set; }

在幕后,框架负责将 C# 类型映射到 iOS SDK 所期望的适当类型。

另一个例子是 NSArray 的处理。Xamarin.iOS 不是暴露弱类型数组,而是向 UIView 的以下 Objective-C 属性暴露强类型数组:

@property(nonatomic, readonly, copy) NSArray *subviews

这以下列方式暴露为 C# 属性:

UIView[] Subviews { get; }

继承的使用

Xamarin.iOS 允许你以扩展任何 C# 类型的方式扩展任何 Objective-C 类型,并且像从重写方法中调用 "base" 这样的功能按预期工作。

映射 Objective-C 委托

在 Objective-C 中,委托设计模式被广泛用于将责任分配给各种对象。Xamarin 在将 iOS 委托映射到 C# 和 .NET 时面临了一些固有的挑战。

在 Objective-C 中,委托作为响应一组方法的对象来实现。这组方法通常定义为协议,尽管它与 C# 接口相似,但实际上 C# 接口和 Objective-C 协议之间存在显著差异:

  • 在 C# 中,实现接口的对象必须实现接口上定义的所有方法

  • 另一方面,在 Objective-C 中,采用协议的对象不需要在给定情况下实现协议的方法

另一个挑战是,在许多方面,传统的 .NET 框架更多地依赖于事件来实现类似的功能,并且事件模型对 .NET 开发者来说更为熟悉。

考虑到这些差异,并希望尽可能使 Xamarin.iOS 对 C# 开发者来说直观,同时不妥协 iOS 架构,Xamarin.iOS 提供了四种不同的方式来实现委托功能:

  • 通过 .NET 事件

  • 通过 .NET 属性

  • 通过强类型委托

  • 通过弱类型委托

通过 .NET 事件

对于许多类型,Xamarin.iOS 自动创建适当的委托并将委托调用转发到相应的 .NET 事件。这使得开发体验对 C# 和 .NET 开发者来说非常自然。

UIWebView是这一点的良好示例。iOS 定义了UIWebViewDelegate,其中包含了一系列方法,如果分配了包含以下内容的委托,UIWebView将转发这些方法:

  • webViewDidStartLoad

  • webViewDidFinishLoad

  • webView:didFailLoadWithError

在 Xamarin.iOS 类MonoTouch.UIKit.UIWebView中,我们发现有三个事件对应以下方法:

  • LoadStarted

  • LoadFinished

  • LoadError

通过.NET 属性

尽管事件具有拥有多个订阅者的优势,但它们也带来自己的限制。具体来说,这可能就是事件不能有返回类型的地方。在需要返回值的委托方法的情况下,使用委托属性。以下示例展示了如何使用委托属性UITextField。在这个例子中,一个匿名方法被分配给UITextField上的委托属性ShouldReturn

firstNameTextField.ShouldReturn = delegate (textfield)
{
      textfield.ResignFirstResponder ();
      return true;
}

通过强类型委托

如果没有提供事件或委托属性,或者你更愿意使用委托,你将很高兴地听到 Xamarin.iOS 提供了一套.NET 类,对应于每个 iOS 委托。这些类包含对应协议上定义的每个方法的定义。需要实现的方法被定义为抽象的,而可选的方法被定义为虚拟的。要使用这些委托之一,开发者只需创建一个新的类,继承自所需的委托,并重写需要实现的方法。

为了举例说明如何使用强类型委托,我们将转向UITableViewDataSource。这是 iOS 定义的用于填充UITableView实例的协议。以下示例演示了一个可以用于用电话号码填充UITableView的数据源:

public class PhoneDataSource : UITableViewDataSource
{
    List<string>_phoneList;
    public PhoneDataSource (List<string> phoneList) {
    _phoneList = phoneList;
}
public override int RowsInSection(UITableView
tableView, int section)
{
  return _phoneList.Count;
}
public override UITableViewCell GetCell(UITableView
  tableView, NSIndexPath indexPath) {
      ... // create and populate UITableViewCell
      return cell;
}
}

现在我们已经创建了委托,我们需要将其分配给一个UITableView实例。UITableViewDataSource委托的属性名为Source,以下代码展示了如何进行分配:

phoneTableView.Source = new PhoneDataSource(phoneList);

通过弱类型委托

最后,Xamarin.iOS 为你提供了一种使用弱类型委托的方法。不幸的是,这种方法需要开发者做更多的工作。

在 Xamarin.iOS 中,可以使用继承自NSObject的任何类创建弱委托。在创建弱委托时,你将负责使用Export属性正确装饰你的类,这实际上教会 iOS 如何映射方法。以下示例展示了具有适当属性规范的弱委托:

public class WeakPhoneDataSource : NSObject
{
...
[Export ("tableView:numberOfRowsInSection:")]
public override int RowsInSection(UITableView
      tableView, int section)
  {
    ...
  }
[Export ("tableView:cellForRowAtIndexPath:")]
  public override UITableViewCell GetCell(UITableView
      tableView, NSIndexPath indexPath) {
...
  }
}

最后几个步骤将弱委托分配给一个UITableView实例。按照 Xamarin.iOS 的约定,弱委托属性名称总是以Weak开头。以下示例展示了如何分配弱数据源委托:

phoneTableView.WeakSource =
    new WeakPhoneDataSource(...);

一旦弱委托被分配,任何已分配的强委托将不再接收调用。

创建绑定库

可能会有这样的情况,你需要为 Xamarin.iOS 未提供且在 Xamarin 组件商店找不到的 Objective-C 库创建自己的绑定库。Xamarin 提供了大量的指导来创建绑定,以及一个自动化的工具来帮助处理一些繁琐的工作。以下链接提供了创建 Objective-C 库自定义绑定的指导:

信息类型 访问它的 URL
一般绑定信息 docs.xamarin.com/guides/ios/advanced_topics/binding_objective-c/
Objective Sharpie 自动化工具的使用 docs.xamarin.com/guides/ios/advanced_topics/binding_objective-c/objective_sharpie/
绑定类型参考 docs.xamarin.com/guides/ios/advanced_topics/binding_objective-c/binding_types_reference_guide/

内存管理

当涉及到释放资源时,Xamarin.iOS 通过垃圾回收器GC)为你处理这些,它代表你完成这些工作。除此之外,所有从NSObject派生的对象都利用System.IDisposable接口,这样当内存释放时,开发者可以对其有所控制。

NSObject不仅实现了IDisposable接口,还遵循.NET 销毁模式。IDisposable接口只需要实现一个方法,即Dispose()。销毁模式需要实现一个额外的Dispose(bool disposing)方法。销毁参数指示该方法是否从Dispose()方法调用,在这种情况下值为true,或者从Finalize方法调用,在这种情况下值为false

销毁参数旨在用于确定是否应该释放托管对象。如果值为true,则应释放托管对象。无论值如何,都应释放非托管对象。以下代码演示了Dispose()方法中应该包含的内容:

public void Dispose ()
{
  this.Dispose (true);
  GC.SuppressFinalize (this);
}

注意Dispose(bool disposing)的调用,其值为true。方便的是,框架为你实现了Dispose()方法,作为NSObject上的虚拟方法。以下代码演示了Dispose(bool disposing)方法的实现:

class MyViewController : UIViewController {

  UIImagemyImage;

  . . .   

  public override void Dispose (bool disposing)
  {
    if (disposing){
      if (myImage!= null) {
        myImage.Dispose ();
        myImage = null;
      }
    }

    // Free unmanaged objects regardless of value.

    base.Dispose (disposing)

  }
}

注意

再次注意对base.Dispose(disposing)的调用。这个调用非常重要,因为它处理框架内部管理的资源。

为什么会有这么大的麻烦?为什么不在 Dispose() 中清理一切?答案在于垃圾回收器。垃圾回收器销毁对象的顺序未定义,因此不可预测且可能发生变化。.NET 释放模式有助于防止终结器在已释放的对象上调用 Dispose()

小贴士

您可以在以下链接中了解更多关于 .NET 释放模式的信息:

msdn.microsoft.com/en-us/library/fs2xkftw.aspx

释放托管对象使其变得无用。即使该对象的引用可能仍然存在,您也需要以假设已释放的对象不再有效的方式来构建您的软件。在某些情况下,当访问已释放对象的成员方法时,将抛出 ObjectDisposedException

对象的释放

任何时候您有一个持有大量资源且不再需要的对象,请调用 Dispose() 方法。GC 很方便且相当复杂,但可能无法完全了解特定对象分配的资源量,尤其是如果这些资源与非托管对象相关。

保持对象

要防止对象被销毁,您只需确保至少有一个对象引用被维护。一旦对象的引用计数达到 0,GC 就会高兴地调用其上的 Dispose() 方法,并且对象将不再可用。

使用 AOT 编译的限制

正如我们在本章前面提到的,使用 AOT 编译有一些限制。以下各节概述了由于使用 AOT 编译而由 Xamarin.iOS 施加的限制:

  • 不允许 NSObject 的泛型子类。以下将不允许,因为 UIViewControllerNSObject 的子类:

    class MainViewController<T> : UIViewController {
    ...
    }
    
  • P/Invoke 在泛型类中不受支持,因此以下在 Xamarin.iOS 中不受支持:

    class MyGenericType<T> {
        [DllImport ("System")]
        public static extern int getpid ();
    }
    
  • Nullable<T> 类型 上不支持 Property.SetInfo()。使用反射的 Property.SetInfo()Nullable<T> 上设置值目前不受支持。

  • 不允许动态代码生成。iOS 内核阻止应用程序动态生成代码,因此 Xamarin.iOS 强制实施以下限制:

    • 无论是 System.Reflection.Emit 还是 System.Runtime.Remoting 都不可用

    • 不支持动态创建类型

    • 反向回调必须在编译时在运行时注册

  • 对于反向回调,还有进一步的限制。在 Mono 中,您可以将 C# 委托传递给非托管代码,而不是传递函数指针。AOT 的使用对此施加了一些限制:

    • 回调方法必须使用 Mono 属性 MonoPInvokeCallbackAttribute 标记

    • 回调方法必须是静态的;不支持实例方法

禁用的运行时功能

在 Xamarin.iOS 中禁用了以下功能:

  • 分析器

  • Reflection.Emit 功能

  • Reflection.Emit.Save功能

  • COM 绑定

  • JIT 引擎

  • 元数据验证器(由于没有 JIT)

为 XIB 和 Storyboard 文件生成代码

Apple Interface Builder 是 Xcode 中内置的设计器,允许进行用户界面的可视化设计。使用 Interface Builder 是可选的;用户界面可以完全使用 iOS API 构建。Interface Builder 创建的定义保存在 XIB 或 Storyboard 文件中,区别在于 XIB 文件通常包含单个视图。另一方面,Storyboard 包含一组视图以及视图之间的过渡或 segues。

Xamarin Studio 与 Xcode 协同工作以支持 UI 设计。当在 Xamarin Studio 中双击 Storyboard 或 XIB 文件时,将启动 Xcode 以方便设计 UI。一旦在 Xcode 中保存更改并切换回 Xamarin Studio,就会生成 C#代码以支持在 Xcode 中捕获的 UI 设计。以下各节将更详细地描述此过程。

生成的类

Xamarin Studio 为 XIB 文件或 Storyboard 文件中找到的每个自定义类生成两个文件,一个设计器文件和一个非设计器文件。例如,名为LoginViewController的视图控制器将导致以下文件生成:

  • LoginViewController.cs

  • LoginViewController.designer.cs

这些文件是在 Xcode 中保存更改后生成的,并且 Xamarin Studio 获得焦点。

设计器文件

设计器文件包含在 XIB 或 Storyboard 文件中找到的自定义类的部分类定义,为输出创建属性,并为找到的操作创建部分方法。以下示例是一个具有两个UITextField控件和一个单个UIButton控件的视图控制器:

[Register ("LoginViewController")]
partial class LoginViewController
{
  [Outlet]
  MonoTouch.UIKit.UITextField textPassword { get; set; }
  [Outlet]
  MonoTouch.UIKit.UITextField textUserId { get; set; }
  [Action ("btnLoginClicked:")]
  partial void btnLoginClicked
         (MonoTouch.Foundation.NSObject sender);
void ReleaseDesignerOutlets ()
{
if (textUserId != null) {
      textUserId.Dispose ();
textUserId = null;
}
if (textPassword != null) {
textPassword.Dispose ();
textPassword = null;
}
}

注意

一旦修改了 XIB 或 Storyboard 文件,设计器文件将自动更新。因此,不应手动修改它们,因为任何更改在 Xamarin Studio 更新它们时都将丢失。

非设计器文件

设计器文件单独使用,但与一个非设计器文件结合使用。非设计器文件包含部分类规范,它完成了其对应设计器文件中定义的类。非设计器文件标识基类,定义了实例化类所需的构造函数,并提供了一个位置来实现功能,无论是通过为部分方法提供实现,还是通过在基类上重写虚拟方法。以下示例显示了一个具有重写和部分方法实现的非设计器文件:

public partial class LoginViewController : UIViewController
{
  public LoginViewController (IntPtr handle) :
    base (handle)
{
}
  public override void ViewDidLoad ()
  {
    base.ViewDidLoad ();
    // logic to perform when view has loaded...
  }
partial void btnLoginClicked (NSObject sender)
{
// logic for login goes here...
}
}

小贴士

注意,此文件中部分方法的实现是为了响应在相应的 XIB 或 Storyboard 文件中定义的操作而生成的设计器文件中的方法。

对非设计器文件所做的更改不会丢失,因为这些文件仅在 Xamarin Studio 首次遇到新自定义类时创建,并且随后不会更新。

输出属性

设计器类包含私有属性,这些属性对应于在自定义类上定义的输出,然后可以从非设计器文件中找到的 CodeBehind 类中使用。如果你需要将这些属性设置为公共的,你只需要将访问器属性添加到非设计器文件中,就像为任何给定的私有字段做的那样。

动作属性

设计器文件具有包含与自定义类上定义的所有动作相关联的部分方法的属性。你应该注意,这些方法不包含实现,它们具有双重作用:

  • 它的第一个目的是,当你将部分代码插入非设计器文件的类体中时,Xamarin Studio 将会提供自动完成所有未实现的部分方法签名,这允许开发者实现动作的逻辑。

  • 它的另一个目的是它们的签名上应用了一个属性,使它们暴露给 Objective-C 世界。因此,一旦在 iOS 中触发相应的动作,它们就可以被调用。

Xamarin.iOS 设计器

Xamarin 提供了 Apple 的 Interface Builder 的替代方案。Xamarin.iOS 设计器是用于 Xamarin Studio 环境的一个插件,它为 iOS storyboards 添加了全功能的拖放用户界面设计,所有操作都可以在 Xamarin Studio 内完成。Xamarin.iOS 设计器提供了以下关键特性:

  • 兼容的 storyboard 格式:正如你所期望的,Xamarin.iOS 设计器创建的 storyboards 使用的是与 Xcode 和 iOS SDK 相同的格式,因此在某些时候切换回 Xcode 是允许的

  • 消除与 Xcode 的同步:使用 Xamarin.iOS 设计器消除了在开发过程中使用 Xcode 的需要,以及可能发生在 Xamarin Studio 和 Xcode 之间的同步问题

  • 简单属性:Xamarin.iOS 设计器在控件被拖放到视图中时自动创建引用控件的属性

  • 简单的事件处理程序:Xamarin.iOS 设计器提供了一个更直观的方式来创建事件处理程序,它们的工作方式与 Visual Studio 在其他 UI 框架(如 Silverlight 和 WPF)上的工作方式非常相似

  • 自定义控件:用户可以在工具箱面板中创建自己的自定义 UI 控件

Xamarin.iOS 设计器只能用于创建 storyboards。如果你更喜欢或需要处理 XIB 文件,你将需要继续使用 Xcode。

概述

在本章中,我们介绍了 Xamarin.iOS 架构的要点,并试图揭示 Xamarin.iOS 允许开发者使用 C# 和 Mono 为 iOS 创建优秀原生应用的方式。虽然我们显然没有涵盖整个 iOS SDK,但我们已经描述了构建 Xamarin.iOS 所使用的方法和原则。有了这些知识,你应该能够顺利地继续进行 Xamarin.iOS 开发,并在出现问题时解决它们。

在下一章中,我们将尝试为 Xamarin.Android 实现相同的目标。

第三章。揭秘 Xamarin.Android

现在是时候深入探讨 Xamarin.Android,看看它是如何实现与 Xamarin.iOS 相同的魔法的。在本章中,我们将看到 Xamarin.iOS 和 Xamarin.Android 拥有许多相同的设计目标。然而,Xamarin.Android 不依赖于静态编译。许多目标是通过完全不同的方法实现的。本章涵盖了以下主题:

  • Mono CLR 和 Dalvik VM—并行工作

  • 应用程序打包

  • Mono 程序集

  • Xamarin.Android 绑定

  • ApplicationManifest.xml文件的属性

  • 垃圾回收

Mono CLR 和 Dalvik VM – 并行工作

Android 应用在Dalvik 虚拟机(Dalvik VM)中运行,它与 Java VM 有些相似,但针对资源有限的设备进行了优化。正如我们在第一章中讨论的,Xamarin 和 Mono – 通向非自然之路,Xamarin 产品基于 Mono 平台,该平台有自己的虚拟机,称为公共语言运行时(CLR)。这里的关键问题是,“Xamarin.Android 应用在哪个环境中运行?”答案是两者都运行。如果您看一下下一张图,您会亲自看到这两个运行时是如何共存的:

Mono CLR 和 Dalvik VM – 并行工作

这两个环境看起来相当不同,那么一个应用如何在两者中运行呢?Xamarin.Android 的强大功能是通过一个称为同伴对象的概念以及一个名为Java Native Interface(JNI)的 Java 框架实现的。

介绍 Java Native Interface

让我们从 JNI 开始。这是一个框架,允许非 Java 代码,例如 C++或 C#,调用或被运行在 JVM 内部的 Java 代码调用。正如您可以从之前的图中看到的那样,JNI 是 Xamarin.Android 整体架构中的关键组件。

小贴士

您可以在第二章中找到一些关于 JNI 的支持信息,特别是关于同伴对象,在 Packt Publishing 出版的《Xamarin Mobile Application Development for Android》中的Xamarin.Android 架构,由马克·雷诺兹撰写。

同伴对象

同伴对象是一对共同工作以执行 Android 应用功能的对象。其中一个是位于 Mono CLR 中的托管对象,另一个是位于 Dalvik VM 中的 Java 对象。

Xamarin.Android 随附一组称为 Android 绑定库的组件。Android 绑定库中的类对应于 Android 应用程序框架中的 Java 类,绑定类上的方法作为包装器,用于调用 Java 类上的相应方法。这些绑定类通常被称为 托管可调用包装器MCW)。因为每当您创建一个从这些绑定类继承的 C# 类时,在构建时都会生成一个相应的 Java 代理类。Java 代理包含对您的 C# 类中每个重写方法的生成重写,并作为包装器来调用 C# 类上的相应方法。

Peer 对象可以通过 Android 应用程序框架在 Dalvik VM 内部创建,或者可以通过您在重写方法中编写的代码在 Mono CLR 内部创建。每个 MCW 实例都保留两个 Peer 对象之间的引用,并且可以通过 Android.Runtime.IJavaObject.Handle 属性访问。

您可以亲自看到 Peer 对象如何协作:

Peer objects

Xamarin.Android 应用程序打包

Android 应用程序以 Android 包格式交付,这是一种具有 .apk 扩展名的存档文件。Android 包包含应用程序代码和运行应用程序所需的所有支持文件,包括以下内容:

  • Dalvik 可执行文件(*.dex 文件)

  • 资源

  • 原生库

  • 应用程序清单

Xamarin.Android 应用程序遵循以下标准,并增加了以下内容:

  • C# 代码编译成组件并存储在名为 assemblies 的顶级文件夹中

  • Mono 运行时库与其他原生库一起存储在 lib 文件夹中

理解 Mono 组件

与 Xamarin.iOS 一样,Xamarin.Android 随附了 Silverlight 和桌面 .NET 组件的扩展子集。这些库共同为开发者提供 .NET 运行时库支持,包括 System.IOSystem.Threading 等命名空间。

Xamarin.Android 与为不同配置编译的组件不兼容,这意味着您的代码必须重新编译以生成组件,具体针对 Xamarin.Android 配置。如果您针对其他配置,如 Silverlight 或 .NET 4.5,本质上也需要这样做。

小贴士

要获取随 Xamarin.Android 一起提供的组件的完整列表,您可以参考 docs.xamarin.com/guides/android/under_the_hood/assemblies

Xamarin.Android 绑定

Xamarin.Android 还提供了一套绑定库,为 Android 开发提供支持。绑定库构成了 Xamarin.Android 魔法的第二大组成部分,类似于 Mono CLR 和 Dalvik VM 的功能。以下部分深入探讨了这些绑定的细节。

设计原则

一系列目标或设计原则指导了绑定库的开发。这些原则对于使 C# 开发者在 Android 开发中变得高效至关重要。以下是对设计原则的总结,您将注意到与 Xamarin.iOS 绑定的一些相似之处:

  • 允许开发者以与其他 .NET 类相同的方式子类化 Java 类。

  • 使常见的 Java 任务变得简单,困难的 Java 任务变得可行。

  • 将 JavaBean 属性公开为 C# 属性。

  • 公开一个强类型 API。

  • 在适当和适用的情况下,暴露 C# 委托(lambda 表达式、匿名方法以及 System.Delegate),而不是单方法接口。

  • 提供一种机制来调用任意的 Java 库(Android.Runtime.JNIEnv)。

    小贴士

    在这些原则的完整讨论可以在 docs.xamarin.com/guides/android/advanced_topics/api_design 找到。

属性

在尽可能大的范围内,Android 框架类中的 JavaBean 属性被转换为 C# 属性。以下规则始终在发生这种情况时遵循:

  • 首先,为获取器和设置器方法对创建读写属性。

  • 对于没有相应设置器的获取器,创建只读属性。

  • 在非常罕见的情况下,如果只存在设置器,则不会创建只写属性。

  • 最后,当类型将是数组时,不会创建任何属性。

事件与监听器

Android API 依照 Java 模式来定义和连接事件监听器。C# 开发者应该更熟悉类似的概念:委托和事件。

以下是一个 Java 事件监听器的示例:

addTicketButton.setOnClickListener (
new View.OnClickListener() {
    public void onClick (View v) {
_ticketCount++;
updateLineItemCost();
    }
});

以下是与 C# 事件等效的代码:

addTicketButton.Click += (sender, e) => {
    _ticketCount++;
     UpdateLineItemCost();
};

Android 绑定在可能的情况下提供事件。以下规则被遵循:

  • 当监听器具有如 setOnClickListener 之类的设置前缀时。

  • 当监听器回调没有返回值时。

  • 当监听器只接受单个参数时,接口只有一个方法,并且接口名称以 Listener 结尾。

当事件不是由于这里列出的规则之一创建时,将生成一个支持适当签名的特定委托。

在集合方面提供特殊帮助。

原生 Android API 广泛使用 java.util 中的列表、集合和映射集合。Android 绑定通过 System.Collections.Generic 中的接口公开这些集合。此外,Xamarin.Android 提供了一组辅助类,它们实现了每个相应的 .NET 集合,并提供更快的封送处理,因为它们实际上并没有执行复制。以下表格显示了这些类的映射:

Java 类型 .NET 接口 辅助类
java.util.Set<E> ICollection<T> Android.Runtime.JavaSet<T>
java.util.List<E> IList<T> Android.Runtime.JavaList<T>
java.util.Map<K,V> IDictionary<TKey,TValue> Android.Runtime.JavaDictionary<K,V>
java.util.Collection<E> ICollection<T> Android.Runtime.JavaCollection<T>

Xamarin.Android 允许你将任何集合(实现了正确的接口)传递给 Android API 方法。例如,List实现了IList,可以在需要IList实体时使用。然而,出于性能考虑,建议你在需要将这些集合类型传递给 Android API 方法时使用辅助类。

接口

Java 和 C#都支持接口;然而,Java 支持额外的功能。此外,两者都支持定义一组方法名称和签名的能力。此外,Java 还支持以下功能:

  • 嵌套接口定义

  • 字段(仅限public final static

通常,以下项目描述了 Android 绑定如何提供以下 Java 接口:

  • 创建一个具有相同名称但以I开头并包含方法声明的 C#接口。例如,android.view.Menu被创建为Android.Views.IMenu

  • 生成一个与 Java 接口同名但包含常量定义的抽象类,例如,android.view.Menu中的常量被放置在生成的抽象类Android.Views.Menu中。

  • 为每个嵌套接口生成一个 C#接口,并使用以I开头的前缀,后跟父 Java 接口的名称,然后是嵌套 Java 接口的名称。

  • 在 Android 绑定中实现包含常量的 Android 接口的类会生成一个嵌套的InterfaceConsts类型,它也包含定义。

嵌套类映射

Java 和 C#都支持嵌套类的定义。然而,Java 支持两种类型的嵌套类:静态和非静态。以下要点说明了它是如何做到这一点的:

  • Java 静态嵌套类与 C#嵌套类相同,可以直接翻译

  • 非静态嵌套类,也称为内部类,有一些不同;适用额外的规则:

    • 在构造函数中必须提供一个指向包含类型的实例的引用作为参数,用于内部类。

    • 在从内部类继承的情况下,派生类必须嵌套在某个类型中。此类型从包含基本内部类的类继承属性,并且派生类必须提供一个与 C#包含类型相同类型的构造函数。

Runnable 接口映射

Java 提供了一个包含单个方法run()java.lang.Runnable接口,以便实现委托。Android 平台在多个地方使用此接口,例如Activity.runOnUIThread()View.post()

C# 提供了 System.Action 委托用于无返回值和无参数的方法;因此,它与 Runnable 接口非常匹配。Android 绑定为所有接受 Runnable 接口的原生 API 成员提供了接受 Action 参数的重载。

IRunnable 重载也被保留,以便可以使用从其他 API 调用返回的类型。

枚举

在许多地方,Android API 使用 int 常量作为参数来指定处理选项。为了提高类型安全性,Android 绑定在可能的情况下创建枚举来替换 int 常量。以下示例显示了使用 ActivityFlags.NewTaskenum 值而不是原生的 FLAG_ACTIVITY_NEW_TASK 常量的用法:

myIntent.SetFlags (ActivityFlags.NewTask);

使用 enum 类的另一个巨大优势是,在诸如 Xamarin Studio 和 Visual Studio 这样的 IDE 中,你将获得增强的代码补全支持。

资源

Xamarin.Android 在你的项目 Resources 文件夹中生成一个名为 Resource.Designer.cs 的文件。此文件包含你的应用中引用的所有资源的常量,并服务于与为传统 Android 应用生成的 R.java 文件相同的目的。

ApplicationManifest.xml 文件的属性

Android 应用程序有一个清单文件 (AndroidManifest.xml),它告诉 Android 平台运行应用程序所需知道的一切,包括以下功能:

  • 应用程序所需的最低 API 级别

  • 应用程序使用或所需的硬件/软件功能

  • 应用程序所需的权限

  • 应用程序启动时初始要执行的活动

  • 应用程序所需的库

Xamarin.Android 提供了一组强大的 .NET 属性,可以用来装饰你的 C# 类,这样在编译时就会自动生成 ApplicationManifest.xml 中所需的大部分信息。使用这些属性简化了保持清单与代码同步的任务。例如,如果你重命名了一个 Activity 类,那么下次编译时,清单中相应的 <Activity/> 元素将自动更新。

以下示例演示了使用 Activity 属性来指定应用的启动活动:

[Activity (Label = "My Accounts", MainLauncher = true)]
public class MyAccountsActivity : Activity
{
    ...
}

这将在 ApplicationManifest.xml 文件中产生以下条目:

<activity android:label="My Accounts"
android:name="myaoo.MyActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category
android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

虽然使用 .NET 属性是保持你的代码和清单文件同步的一种方便方式,但使用这些属性不是必需的。

ApplicationManifest.xml 文件的编辑器

Xamarin Studio 也提供了一个用于 ApplicationManifest.xml 的编辑器。这可以用来代替属性或编辑无法通过属性设置的内容,例如所需的硬件/软件功能和权限。以下截图展示了编辑器:

ApplicationManifest.xml 文件的编辑器

垃圾回收

由于 Xamarin.Android 应用程序在两个不同的虚拟机(VM)中运行,垃圾回收过程相对复杂,并带来了一些有趣的挑战。因此,我们投入了大量的时间来讨论这个过程。Xamarin.Android 使用 Mono 的简单代际垃圾回收器,它支持两种类型的收集,称为小集合和主集合:

  • 小集合:这些集合成本低廉,因此调用频率较高。小集合收集最近分配和死亡的对象,并在分配了几 MB 之后调用。您可以使用以下代码手动调用小集合:

    GC.Collect(0)
    
  • 主集合:这些集合成本高昂,因此调用频率较低。主集合回收所有已死亡的对象,并且仅在当前堆大小耗尽时调用。您可以使用以下代码手动调用主集合:

    GC.Collect() or GC.Collect(GC.MaxGeneration).
    

    注意

    您可以在www.mono-project.com/Compacting_GC上查看关于 Mono 简单代际垃圾回收器的更详细讨论。

在我们继续讨论之前,如果我们把应用程序中的对象分组到以下类别中,这将对我们有所帮助:

  • 托管对象:这些是从标准库(如 Mono 运行时库)创建的任何 C#对象。它们像其他任何 C#对象一样进行垃圾回收,并且与 Android 绑定中的任何类没有特殊联系。

  • Java 对象:这些是位于 Dalvik VM 中的 Java 对象,作为某些过程的一部分创建,但未通过 JNI 暴露给托管对象。这些对象像任何其他 Java 对象一样进行收集,关于它们我们不需要讨论太多。

  • 同等对象:如我们之前提到的,同等对象是管理对象和 Java 对象对,通过 JNI 进行通信。它们协同工作以执行 Android 应用程序的功能。

JNI 全局和弱引用

JNI 引用有几种不同类型,并且对对象何时可以被收集有很大影响。具体来说,我们将讨论两种类型的 JNI 引用,即全局和弱引用:

  • 全局引用:JNI 全局引用是从“本地”或在我们的情况下是托管代码到由 Dalvik VM 管理的 Java 对象的引用。当对象最初创建时,JNI 全局引用在同等对象之间建立。JNI 全局引用将阻止 Dalvik 垃圾回收器执行所需操作,因为它表示对象仍在使用中。

  • 弱引用:JNI 弱引用也允许托管对象引用 Java 对象,但不同之处在于弱引用将不会阻止 Dalvik VM GC 收集它。

我们将在本章后面看到它们之间的差异。

Mono 集合

Mono 集合是发生有趣事情的地方。如前所述,简单的托管对象通常进行正常收集,但同等对象通过执行以下步骤进行收集:

  1. 所有托管对等对象都符合 Mono 收集的条件,这意味着它们不被任何其他托管对象引用。它们的 JNI 全局引用被替换为 JNI 弱引用。这允许 Dalvik VM 在没有其他 Java 对象在 VM 中引用它们的情况下回收 Java 对等对象。

  2. 会调用 Dalvik VM 的 GC,允许具有弱全局引用的 Java 对等对象被回收。

  3. 在步骤 1 中创建的具有 JNI 弱引用的托管对等对象将被评估。如果 Java 对等对象已被回收,则托管对等对象也会被回收。如果 Java 对等对象尚未被回收,则它将被替换为 JNI 全局引用,托管对等对象将不会在未来的 GC 中被回收。

最终结果是,托管对等对象的实例将与其被托管代码引用或其对应的 Java 对等对象被 Java 代码引用的时间一样长。为了缩短对等对象的生存期,当它们不再需要时,请销毁对等对象。

注意

最佳实践

手动调用 Dispose() 方法通过释放 JNI 全局引用来切断对等对象之间的连接,从而允许每个 VM 尽快地回收对象。

自动收集

从 Xamarin.Android 4.1.0 版本开始,当 gref 阈值超过平台已知最大 gref 值的 90% 时,会自动执行完整垃圾回收。

当你执行自动收集时,调试日志中会显示类似以下的消息:

I/monodroid-gc(PID): 46800 outstanding GREFs. Performing a full GC!

自动 GC 的调用是非确定性的,可能不会在最佳时机发生。如果你在处理过程中遇到暂停,请查看日志中可能表明自动 GC 发生的消息。当这种情况发生时,你可以考虑何时使用 Dispose() 来减少对等对象的生存期。

帮助 GC

有多种方式可以帮助 GC 进行回收过程。以下部分提供了一些额外的思考:

  • 销毁对等对象:当托管对等对象不再需要时,请将其销毁并考虑调用小 GC。如我们之前提到的,Mono GC 对内存状况没有完整的了解。对于 Mono 来说,对等对象似乎只占用 20 字节,因为 MCWs 没有添加实例变量。因此,所有内存都与相应的 Java 对象相关联,并分配给 Dalvik VM。如果你有一个加载了 2 MB 图像的 Android.Graphics.Bitmap 实例,Mono GC 只能看到 20 字节,因此销毁对象对 GC 来说将是低优先级的。

  • 减少对等对象中的直接引用:在 GC 期间扫描托管对等对象时,会扫描整个对象图,这意味着它直接引用的每个对象也会被扫描。具有大量直接引用的对象可能导致 GC 运行时发生暂停。

  • 小型收集: 小型收集相对便宜。您可以考虑在活动结束时或完成一组重要的服务调用或后台处理之后调用小型收集。

  • 主要收集: 主要收集成本较高,应很少手动执行。只有在经过一段重要的处理周期后,大量资源已释放且您可以忍受应用响应性暂停时,才考虑手动调用主要收集。

Xamarin.Android 设计器

Xamarin 为 Xamarin Studio 提供了一个插件,可用于设计 Xamarin.Android 应用的布局文件。该设计器支持内容模式进行视觉拖放和模式进行基于 XML 的编辑。以下截图显示了在内容模式下打开的设计器:

Xamarin.Android 设计器

摘要

在本章中,我们回顾了 Xamarin.Android 的架构,讨论了设计目标,并查看了一些实现细节。我们还探讨了 Xamarin.Android 应用中的内存管理是如何工作的。在下一章中,我们将开始开发一个 Xamarin.iOS 应用。

第四章。使用 Xamarin.iOS 开发您的第一个 iOS 应用

在本章中,我们终于可以开始编写一些代码了。我们将开发一个样例应用,展示开发 Xamarin.iOS 应用的基础,并将涵盖以下主题:

  • 样例应用的概述

  • 创建 Xamarin.iOS 应用

  • 使用 Xamarin Studio 运行和调试应用

  • 使用 Xamarin iOS 设计器

  • 扩展样例应用

  • MonoTouch.Dialog

样例国家公园应用

在本章中,我们将创建一个样例应用,我们将在第八章与 Xamarin.Forms 共享中继续使用它。该应用将允许您查看、创建、编辑和删除有关国家公园的信息,并将具有与 iOS 7 联系人应用类似的用户界面和流程。以下屏幕截图展示了用户界面将如何组织:

样例国家公园应用

以下是国家公园应用的不同视图:

  • 列表视图:此视图显示国家公园列表,允许查看公园并创建新的公园

  • 详情视图:此视图以只读模式显示国家公园的所有属性,并允许导航查看公园的照片或查看前往公园的路线

  • 编辑视图:此视图允许您编辑新的或现有的公园,以及删除公园

创建样例应用

将使用 Xamarin.iOS 模板来创建样例应用,为我们提供大部分已经就位的功能。

在本章中,我们将展示从下载的解决方案中提取的样例代码。请随意以任何方式偏离,以将应用引导到您认为合适的方向。

小贴士

下载示例代码

您可以从www.packtpub.com下载示例代码文件,以获取您购买的所有 Packt Publishing 书籍。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册以将文件直接通过电子邮件发送给您。

要创建国家公园样例应用,请执行以下步骤:

  1. 启动 Xamarin Studio。

  2. 文件菜单中,导航到新建 | 解决方案。将显示新建解决方案对话框,如下截图所示:创建样例应用

  3. 在对话框的左侧导航到C# | iOS | iPhone Storyboard,在中间部分选择主-详情应用

  4. 名称字段中输入NationalParks.iOS,通过点击浏览按钮选择您想要放置代码的位置,将解决方案名称更改为NationalParks,保留为解决方案创建目录复选框,然后点击确定

  5. 将项目命名为NationalParks.iOS的一个原因是在未来的章节中,我们将向同一个解决方案中添加一个名为NationalParks.Droid的新项目。这个项目将清楚地标识每个项目支持的平台。

  6. Xamarin Studio 将创建解决方案和项目文件夹,为解决方案和项目生成一些文件,然后打开新的解决方案和项目。以下截图显示了打开新项目的 Xamarin Studio:创建示例应用

通过选择 Master-Detail 模板,Xamarin Studio 已生成一个具有主视图(列表)和详细视图的功能性应用,以及在这两个视图之间导航所需的一切。

让我们简要看看自动创建的内容:

  • MainStoryboard.storyboard:创建了一个包含用户界面定义的故事板文件,并命名为MainStoryboard.storyboard。双击此文件以在 Xcode 中打开它。您会注意到故事板包含两个视图控制器:MasterViewControllerDetailViewController,它们之间有一个单一的转场。

  • MasterViewController:由于在故事板中定义了MasterViewController,创建了MasterViewController.cs及其对应的MasterViewController.designer.cs文件。MasterViewController.cs是我们将添加代码的文件,而在这里我们重写方法和添加逻辑。

  • DataSourceMasterViewController包含一个名为DataSource的内部类,这是一个UITableViewSource的特化。DataSource类负责向MasterViewController上的表格视图提供填充的UICellViews

  • DetailViewController:由于在故事板中定义了DetailViewController,创建了DetailViewController.cs及其对应的DetailViewController.designer.cs文件。这用于在MasterViewController上的表格视图中显示特定项的属性。

项目选项视图

有许多选项可以设置,这些选项会影响 iOS 应用的构建和执行方式。这些选项可以在项目选项视图中查看和调整。以下部分对 iOS 应用最为重要:

  • iOS 应用:这包括描述应用的设置,包括支持的设备、iOS 目标版本、支持的朝向、图标等

  • iOS IPA 选项:这包括与创建用于 ad hoc 分发的 IPA 包相关的设置

  • iOS 捆绑签名:这包括在构建过程中控制如何对捆绑进行签名的设置

  • iOS 构建:这包括编译和链接过程使用的设置,用于优化生成的可执行文件

在运行应用之前,我们需要为我们的 iOS 目标版本选择一个设置。要调整此设置,请按照以下步骤操作:

  1. 解决方案面板下选择示例应用解决方案中的示例应用项目。

  2. 右键单击并选择选项

  3. 选择iOS 应用程序部分,并将部署目标选项设置为7.0,然后点击确定项目选项视图

在 Xamarin Studio 中运行和调试

现在我们已经对我们所创建的内容有了很好的理解,让我们花几分钟时间看看 Xamarin Studio 提供的运行和调试应用程序的功能。工具支持运行和调试应用程序的方式对开发者的生产力有重大影响。Xamarin Studio 提供了一套强大的调试功能,与最现代的开发环境相当,可以使用 iOS 模拟器或物理设备。与 iOS 开发一样,使用物理设备可以提供最准确的结果。

Xamarin Studio 左上角的两个下拉菜单控制要生成的构建类型(发布调试),以及当选择调试时,应使用哪个 iOS 模拟器。构建类型包括Ad-HocAppStore调试发布。除了调试之外的所有构建类型将在第九章准备 Xamarin.iOS 应用程序以分发中讨论。以下截图显示了调试构建类型:

在 Xamarin Studio 中运行和调试

注意,对于构建类型和 iOS 模拟器提供的各种选项,都选择了调试。选择iOS 设备允许你在连接的设备上调试应用程序。

要调试一个应用程序,请按照以下步骤操作:

  1. 将构建类型选择为调试,并从iPhone Retina (4 英寸 64 位)中选择iOS 模拟器选项为iOS 7.1

  2. 通过点击左侧任务栏上的开始按钮启动调试会话。您也可以通过主菜单栏导航到运行 | 开始调试来启动调试会话。

  3. Xamarin Studio 将编译应用程序,启动 iOS 模拟器,在模拟器上安装应用程序,并最终启动应用程序。Xamarin Studio 通过任务栏中间的状态窗口通知您正在发生的事情。以下截图显示了构建过程中的状态窗口:在 Xamarin Studio 中运行和调试

  4. 初始时显示一个空列表。点击+(添加)按钮几次,你将看到日期/时间字段被添加到列表中。选择一个条目,详细视图将显示,如下截图所示:在 Xamarin Studio 中运行和调试

  5. 通过双击左侧的解决方案垫打开MasterViewController.cs。在AddNewItem()方法的第一行设置断点。

  6. 在应用程序中点击+(添加)按钮。你会注意到应用程序在断点处停止,如下所示:在 Xamarin Studio 中运行和调试

  7. 您将在任务栏中找到基本流程控制。这些允许您继续执行、跳过当前行、进入当前函数和退出当前函数。任务栏将出现:在 Xamarin Studio 中运行和调试

    提示

    完整的流程控制和调试选项可以在 运行 主菜单下找到。

  8. AddNewItem() 的第一行开始,选择 DateTime.Now,右键单击,并选择 表达式评估器。此对话框允许您快速查看应用程序执行期间对象的状态,如下所示:在 Xamarin Studio 中运行和调试

  9. 您还会注意到 Xamarin Studio 底部的一组面板,其中包含 观察局部变量断点线程应用程序输出调用堆栈 选项卡。在 Xamarin Studio 中运行和调试

  10. 点击继续图标以允许应用继续运行。

如您从之前的练习中看到的,Xamarin Studio 和 iOS 模拟器提供了一套强大的功能来运行和调试应用程序。

扩展示例应用

现在,是时候扩展应用了。我们面前有两个主要任务:

  • 创建一种方法,以便从文件中加载和保存国家公园

  • 增强用户界面以显示所有适当的属性,并允许查看和编辑数据

存储和加载国家公园

我们将使用一个简单的 JSON 格式的文本文件来存储信息。.NET 提供了库来完成这项任务,但我最有成功经验的库是 Json.NET。Json.NET 是由 James Newton-King 创建的开源库,这绝对值得考虑。Json.NET 也可在 Xamarin 组件商店中找到,因此我们可以直接从那里将其添加到我们的项目中。

添加 Json.NET

要将 Json.NET 添加到示例应用中,请执行以下步骤:

  1. 展开 NationalParks.iOS 项目,选择 Components 文件夹,并选择 编辑组件

  2. 在右上角,单击 获取更多组件 并在搜索字段中输入 Json.NET

  3. 从列表中选择 Json.NET 并选择 添加到应用

创建一个实体类

我们现在需要一个表示我们主题的实体类:国家公园。这将是一个具有少量属性的简单 .NET 类。

要创建实体类,请执行以下步骤:

  1. 右键单击 NationalParks.iOS 项目并选择 新建文件 选项。

  2. 新建文件 对话框中,选择 常规 部分,选择 空类,在 名称 字段中输入 NationalPark,然后单击 新建

以下代码演示了实体类所需的代码:

public class NationalPark
{
  public NationalPark ()
  {
    Id = Guid.NewGuid ().ToString();
    Name = "New Park";
  }

  public string Id { get; set; }
  public string Name { get; set; }
  public string Description { get; set; }
  public string State { get; set; }
  public string Country { get; set; }
  public double? Latitude { get; set; }
  public double? Longitude { get; set; }

  public override string ToString ()
  {
    return Name;
  }
}

添加 JSON 格式化文件

现在,我们需要一个包含 JSON 格式化国家公园的文件。您可以在下载的解决方案的 assets 文件夹中找到此类文件。该文件的名称是 NationalParks.json

要将数据文件添加到项目中,请执行以下步骤:

  1. NationalParks.json 文件复制到 NationalParks.iOS 项目文件夹。

  2. 右键单击 NationalParks.iOS 项目并选择 添加文件,选择 NationalParks.json 并单击 打开

  3. 双击 NationalParks.json 文件以打开它并查看内容。

在编译和部署过程中处理文件时,必须设置几个文件属性,以确定如何处理文件。我们希望文件被视为内容,并在部署时放置在与应用相同的文件夹中。以下截图显示了完成此操作所需的设置。调整这些设置的选项卡位于 Xamarin Studio 右侧的 属性 选项卡上。

添加 JSON 格式的文件

这不是应用程序在生产分发中存储数据的理想位置,但在这个章节中可以达到我们的目的。在第五章,使用 Xamarin.Android 开发您的第一个 Android 应用中,当我们讨论在 iOS 和 Android 应用之间共享代码时,我们将构建一个更健壮的存储机制。

从 JSON 格式的文件中加载对象

现在,我们需要添加将数据从文件加载到列表中的逻辑。

要从文件加载对象,执行以下步骤:

  1. 如您所忆,当我们的应用生成时,放置在 MasterViewController.cs 中的 UITableViewSource 文件使用 List<object> 对象来填充列表。我们需要将其转换为 List<NationalPark>Parks,如下所示:

    readonly List<NationalPark> parks;
    

    注意,我们不需要实例化 Parks 列表;Json.NET 将在我们反序列化 JSON 字符串时为我们完成这项工作。

  2. 我们还需要将 DataSource 上定义的 Objects 属性转换为以下内容:

    public IList<NationalPark> Parks {get {return parks;}}
    
  3. 在添加加载和反序列化步骤之前,添加 System.IONewtonsoft.Jsonusing 语句。

    using System.IO;
    . . .
    using Newtonsoft.Json;
    
  4. JSON 文件将放置在 app 文件夹中;Environment.CurrentDirectory 属性为我们提供了到这个位置的路径。从该文件加载对象需要两个基本步骤。第一步是使用 File.ReadAllText() 将文本读取到字符串中。第二步是使用 JsonConvert.DeserializeObject<>() 将对象反序列化到列表中。以下代码示例演示了需要在 DataSource 类的构造函数中放置的内容:

    string dataFolder = Environment.CurrentDirectory;
    string serializedParks =
        File.ReadAllText (Path.Combine(dataFolder,
            "NationalParks.json"));
    parks =
        JsonConvert.DeserializeObject<List<NationalPark>>
            (serializedParks);
    

将对象保存到 JSON 格式的文件

将对象保存到 JSON 格式的文件与加载它们一样简单。只需调用 JsonConvert.SerializeObject() 创建对象(s)的 JSON 表示形式并将其写入到文本文件中,使用 File.WriteAllText()。以下代码演示了需要的内容:

string dataFolder = Environment.CurrentDirectory;
string serializedParks =
     JsonConvert.SerializeObject (dataSource.Parks);
File.WriteAllText(Path.Combine(dataFolder,
     "NationalParks.json"), serializedParks);

我们将在接下来的章节中,标题为 实现 Done Clicked 事件处理程序 中使用这个逻辑。

运行应用

我们现在可以查看一些我们的工作了。运行应用并注意以下内容:

  • MasterViewController 使用 NationalParks.json 中的信息填充

  • 选择一个公园将显示填充有公园名称的DetailViewController

  • 点击MasterViewController中的添加按钮,可以添加一个名为新公园的新公园。

增强 UI

我们现在将注意力转向创建一个更健壮的 UI,它将支持列出项目、查看项目详情以及编辑新的和现有项目。这是移动应用中常见的模式,我们已经有大约 75%的需求。我们需要进行以下添加:

  1. 添加一个名为EditViewController的新视图控制器,它可以用来编辑新的或现有的国家公园。

  2. MasterViewController中的添加按钮更改为在EditViewController中打开一个新的国家公园条目。

  3. DetailViewController添加字段,用于显示国家公园的所有属性和一个编辑按钮,该按钮将导航到EditViewController以编辑当前项。

正如我们在第二章中讨论的,揭秘 Xamarin.iOS,我们有两种编辑故事板的方法:Xcode 界面构建器和 Xamarin.iOS 设计器。可以根据个人喜好选择使用任何一种工具。在 Xamarin Studio 中,你可以通过选择一个故事板文件,右键单击它并选择打开方式来选择要启动的工具。以下截图显示了故事板上下文菜单:

增强 UI

本章的其余部分将基于使用 Xamarin.iOS 设计器。如果你选择使用 Xcode 界面构建器,你需要知道,当 Xamarin Studio 再次激活时,会进行一个同步过程。这个过程将 Xcode 中的更改与 C#设计器类文件同步,并创建适当的输出和动作。以下截图显示了同步期间 Xamarin Studio 的状态栏:

增强 UI

注意

如果你需要更多关于 Xamarin.iOS 设计器的指导,或者需要 Xcode 界面构建器的入门或复习课程,以下链接提供了教程:

探索 Xamarin.iOS 设计器

Xamarin.iOS Designer 提供了一套完整的工具来创建和编辑故事板文件。由于这可能是您第一次使用此工具,我们将花几分钟时间熟悉它。为此,请按照以下步骤操作:

  1. 双击 MainStoryboard.storyboard 以在 Xamarin.iOS Designer 中打开故事板。您将看到 NavigationControllerMasterViewControllerDetailViewController 以及连接一切的任务,如下面的截图所示:浏览 Xamarin.iOS Designer

  2. 注意位于 Xamarin Studio 右下角的 工具箱 选项卡。它包含可以在故事板中使用的所有项目。您可以使用搜索字段搜索项目。工具箱 选项卡如下面的截图所示:浏览 Xamarin.iOS Designer

  3. 注意位于 Xamarin Studio 右上角的 文档大纲 选项卡。此视图以分层形式显示故事板的内容,您可以使用它来查看更详细的级别。文档大纲 选项卡对于查看和选择故事板中的特定元素非常有帮助,如下面的截图所示:浏览 Xamarin.iOS Designer

  4. 注意位于 Xamarin Studio 右上角的 属性 选项卡;您可以通过点击标签为 属性 的选项卡来访问它。属性 选项卡允许您编辑当前选中项的属性。在 小部件 部分输入控件的名称将自动创建一个出口,在 事件 部分输入名称将自动创建一个动作。属性 选项卡如下面的截图所示:浏览 Xamarin.iOS Designer

  5. 注意设计师顶部的部分,其中包含调整选项的多个控件,例如 iOS 版本、设备大小、设备方向和缩放级别。还有用于建立约束的控件,如下面的截图所示:浏览 Xamarin.iOS Designer

  6. 在设计师中选择项目可能有点棘手,尤其是在选择视图控制器时。如果您在视图控制器的中间点击,将选择视图而不是视图控制器。有三种不同的方法来选择视图控制器:

    • 在视图控制器的中间右键单击并导航到 视图控制器 | 选择,如下面的截图所示:浏览 Xamarin.iOS Designer

    • 点击视图控制器底部的栏,如下面的截图所示:浏览 Xamarin.iOS Designer

    • 文档大纲 选项卡中选择视图控制器

添加 EditViewController 和 segue

在对 Xamarin.iOS Designer 有基本了解之后,我们现在可以添加一个新的视图控制器和 segue。

要添加 EditViewController 和 segue,请执行以下步骤:

  1. 双击MainStoryboard.storyboard以在 Xamarin.iOS 设计器中打开故事板。您将在文件中看到MasterViewControllerDetailViewController,它们之间有一个转换。

  2. 通过从工具箱垫中选择视图控制器项并拖放到设计视图中创建一个新的UIViewController

  3. 通过单击底部栏以选择它,切换到属性垫,并在字段中输入EditViewController来命名新的视图控制器。以下截图显示了属性垫:添加 EditViewController 和转换

  4. DetailViewController的导航项右侧添加一个栏按钮项,并将标识符按钮在属性垫的小部件部分设置为编辑

  5. DetailViewController编辑按钮到新控制器EditViewController添加一个推送转换。按住Ctrl键,点击并按住编辑按钮,将其拖到EditViewController底部的栏上,释放鼠标,在属性垫的小部件部分的标识符选项中选择推送,并输入editFromDetail

  6. MasterViewController的导航项右侧添加一个栏按钮项,并将标识符按钮设置为添加

  7. MasterViewController添加按钮到新控制器EditViewController添加一个推送转换。按住Ctrl键,点击并按住添加按钮,将其拖到EditViewController底部的栏上,释放鼠标,在属性垫的小部件部分的标识符选项中选择推送,并输入editFromMaster

  8. EditViewController的导航项右侧添加一个栏按钮项,并将标识符选项设置为完成。按钮命名为DoneButton。命名按钮将创建一个出口,稍后可以用作分配传统.NET 事件处理器的引用。

  9. EditViewController的中心添加一个标签UILabel。这将被临时用来显示项目的名称,在我们测试和调试应用的导航时使用。将此UILabel实例命名为editDescriptionLabel

  10. EditViewController中添加一个UIButton实例,并将标题选项设置为删除。在属性垫的事件部分的触摸事件中添加一个名为DeleteClicked的操作。创建操作将生成一个部分方法,我们稍后可以用逻辑来完成以实现DeleteClicked事件处理器。

  11. 保存所有做出的更改。

  12. 现在,我们需要编写一些代码来将所有这些内容串联起来。让我们先看看由于我们在 Xamarin.iOS Designer 中的工作而生成的一些代码。你会找到为 EditViewController 添加的两个文件,一个名为 EditViewController.designer.cs 的设计器文件,嵌套在一个非设计器文件 EditViewController.cs 之下。双击设计器类以查看内容,如下面的代码片段所示:

[Outlet]
MonoTouch.UIKit.UIBarButtonItem DoneButton { get; set; }
[Outlet]

MonoTouch.UIKit.UILabel editContent { get; set; }
[Action ("DeleteClicked:")]
Partial void DeleteClicked (
    MonoTouch.Foundation.NSObject sender);

注意

注意,EditViewController 是一个部分类;这两个输出和动作是基于我们制定的规格生成的。

实现 DoneClicked 事件处理器

对于 完成 按钮,我们创建了一个输出,这样我们就可以在运行时将 .NET 事件处理器分配给该对象。当点击 完成 时,我们需要做一些事情。首先,检查我们是否处理的是新对象并将其添加到 _parks 集合中。如果是这样,那么我们需要将 _parks 集合保存到 NationalParks.json

要实现 完成 按钮,请执行以下步骤:

  1. 创建一个保存更改到 NationalParks.json 的方法,如下所示:

    private void SaveParks()
    {
       string dataFolder = Environment.CurrentDirectory;
       string serializedParks = JsonConvert.SerializeObject (_parks);
          File.WriteAllText(Path.Combine(dataFolder,
            "NationalParks.json"), serializedParks);
    }
    
  2. 创建一个名为 DoneClicked 的 .NET 事件处理器,并添加逻辑将 _park 添加到 _parks 集合中。如果是新公园,调用 SaveParks() 方法将更新保存到 NationalParks.json,并使用以下代码片段返回到上一个视图控制器:

    private void DoneClicked (object sender, EventArgs e)
    {
    
      if (!_parks.Contains (_park))
          _parks.Add (_park);
    
      SaveParks ();
      NavigationController.PopViewControllerAnimated (true);
    }
    
    Assign the DoneClicked event handler to the Clicked event on the DoneButton outlet in ViewDidLoad().
    
    public override void ViewDidLoad (){
      . . .   DoneButton.Clicked += DoneClicked;}
    

实现 DeleteClicked 动作

我们为 删除 按钮创建了一个动作,这导致在设计师类中创建了一个部分方法。我们现在需要为部分方法创建一个实现。

要实现 删除 动作,你只需要为 DeleteClicked 添加一个 partial 方法实现,从 parks 集合中移除 _park 并将更改保存到 NationalParks.json 文件中,然后返回到 MasterViewController。这可以通过以下方式完成:

partial void DeleteClicked (UIButton sender)
{
    if (_parks.Contains(_park))
       _parks.Remove(_park);

   SaveParks();

    NavigationController.PopToRootViewController(true);
    }

展示的两种实现事件处理器的方案基本上完成了相同的事情,没有明显的优势。由于我们没有在 ViewDidLoad() 中为动作进行事件处理器分配,所以代码稍微少一些。这实际上取决于你更喜欢哪种方法,并感到最舒适。

传递数据

所有 iOS 应用都需要在视图之间导航并传递数据。由于我们使用故事板和转场来处理 UI,与导航相关的大部分工作都由我们完成。然而,我们需要在视图之间传递数据。这需要两个部分来完成:定义一个视图控制器将接受数据的方式,并从发起视图控制器使用此机制。就接受数据而言,这可以通过在视图控制器上使用简单的属性或定义一个接受数据并将其保存到私有变量的方法来完成。我们将采用定义一个接受导航数据的方法,这也是为我们生成的代码所采用的方法。

要完成接受导航数据的逻辑,执行以下步骤:

  1. 打开 DetailViewController 并定位 SetDetailItem 方法。

  2. 让我们先从更改名称开始,使其更具意义。在编辑器中选择 SetDetailItem 文本,右键单击并导航到 重构 | 重命名。输入 SetNavData 并点击 确定

  3. 让我们也使用相同的方法将 ConfigureView () 重命名为 ToUI()

  4. 修改 SetNavData() 方法,使其接受一个 NationalPark 项目列表以及应该显示的单个公园,并将这些参数保存到一组私有变量中。同时,移除对 ToUI() 的调用;我们将在下一步将此移动到更合适的位置,如下面的代码所示:

    IList<NationalPark> _parks;
    NationalPark _park;
    . . .
    public void SetNavData(
        IList<NationalPark> parks, NationalPark park)
    {
        _parks = parks;
        _park = park;
    }
    
  5. 重写 ViewWillAppear() 方法以调用 ToUI(),如下所示:

    public override void ViewWillAppear (bool animated)
    {
        ToUI ();
    }
    
  6. 更新 ToUI() 方法,使其使用私有 _park 变量填充 UILabel,如下所示:

    void ToUI()
    {
        // Update the user interface for the detail item
        if (_park != null)
           detailDescriptionLabel.Text = _park.ToString ();
    }
    
  7. 现在,将 SetNavData()ToUI() 方法添加到与 DetailViewController 具有相同功能的 EditViewController 中。

现在我们已经处理了接收导航数据,我们将注意力转向传递数据。当使用 segue 时,iOS 视图控制器有一个可以重写的 PrepareForSegue() 方法,用于准备目标视图控制器以进行显示。我们需要在 MasterViewControllerDetailViewController 中重写 PrepareForSegue()

要完成传递导航数据的逻辑,执行以下步骤:

  1. 打开 MasterViewController 并定位现有的 PrepareForSegue() 方法。

  2. MasterViewController 实际上有两个 segue:原始的 segue 用于导航到 DetailViewController,以及我们添加的用于导航到 EditViewController 的新 segue。PrepareForSegue() 方法提供了一个具有 Identifier 属性的 segue 参数,可以用来确定正在采取哪个导航路径。修改 PrepareForSegue() 中的代码,使其根据 segue 标识符在适当的视图控制器上调用 SetNavData(),如下所示:

    public override void PrepareForSegue (
        UIStoryboardSegue segue, NSObject sender)
    {
      if (segue.Identifier == "showDetail") {
        var indexPath = TableView.IndexPathForSelectedRow;
        var item = dataSource.Parks [indexPath.Row];
        ((DetailViewController)segue.
            DestinationViewController).SetNavData
               (dataSource.Parks, item);
      }
      else if (segue.Identifier == "editFromMaster") {
        ((EditViewController)segue.
            DestinationViewController).SetNavData
              (dataSource.Parks, new NationalPark());
      }
    }
    
  3. 现在,打开 DetailViewController 并为 PrepareForSegue() 创建一个覆盖,以便将导航数据传递给 EditViewController,如下所示:

    public override void PrepareForSegue (
        UIStoryboardSegue segue, NSObject sender)
    {
      if (segue.Identifier == "editFromDetail") {
       ((EditViewController)segue.
         DestinationViewController).SetNavData
           (_parks, _park);
      }
    }
    

运行应用

我们已经做了很多修改,现在可以运行应用来测试基本导航。启动应用并测试导航到各个视图;观察以下内容:

  1. 当你在 MasterViewController 上点击 +(添加)按钮时,在 EditViewController 中将显示一个新的国家公园。

  2. 当你在 DetailViewController 上点击 编辑 按钮,应用将导航到显示当前公园的 EditViewController

  3. 当你在 EditViewController 上点击 完成 按钮,它将带你回到之前的视图控制器,无论是 MasterViewController 还是 DetailViewController

  4. 当你在 EditViewController 上点击 删除 按钮,它将带你到 MasterViewController

以下截图展示了你应该看到的内容:

运行应用

完成示例应用

视图控制器和导航现在已就位。我们现在需要添加一些额外的控件来查看和编辑信息,以及一点逻辑。

完成 DetailViewController

要完成DetailViewController,我们需要一组UILabel控件,用于显示公园的属性,并添加可以启动查看照片或接收方向的按钮。

要完成DetailViewController,执行以下步骤:

  1. DetailViewController的视图中添加一个UIScrollView

  2. NationalPark上定义的每个属性(除了Id属性)添加UILabel控件。同时添加可以用于属性标签的UILabel控件。使用示例国家公园应用部分中的屏幕截图作为布局控件的指南。

  3. 为每个将用于显示公园属性的UILabel控件输入一个名称,以便创建输出。

  4. 更新ToUI()方法,以便UILabel控件填充来自公园的数据,如下所示:

    void ToUI()
    {
      // Update the user interface for the detail item
      if (IsViewLoaded && _park != null) {
          nameLabel.Text = _park.Name;
          descriptionLabel.Text = _park.Description;
          stateLabel.Text = _park.State;
          countryLabel.Text = _park.Country;
          latitudeLabel.Text = _park.Latitude.ToString ();
          longitudeLabel.Text = _park.Longitude.ToString ();
      }
    }
    
  5. Touch Down事件中添加一个标题为“照片”的UIButton实例,并命名为PhotoClicked

  6. PhotoClicked操作添加实现,该操作打开一个 URL 以查看在www.bing.com上使用公园名称作为搜索参数的照片:

    partial void PhotosClicked (UIButton sender)
    {
        string encodedUriString =
           Uri.EscapeUriString(String.Format(
              "http://www.bing.com/images/search?q={0}", _park.Name));
       NSUrl url = new NSUrl(encodedUriString);
       UIApplication.SharedApplication.OpenUrl (url);
    }
    
  7. Touch Down事件中添加一个标题为“方向”的UIButton实例,并命名为DirectionsClicked

  8. DirectionsClicked操作添加实现,该操作打开一个 URL 以接收公园的经纬度坐标的路线:

    partial void DirectionsClicked (UIButton sender)
    {
        if ((_park.Latitude.HasValue) && (_park.Longitude.HasValue))
        {
            NSUrl url = new NSUrl (
                   String.Format(
                       "http://maps.apple.com/maps?daddr={0},{1}",
                        _park.Latitude, _park.Longitude));
    
           UIApplication.SharedApplication.OpenUrl (url);
       }
    }
    
  9. UIScrollViewUILabel添加适当的约束,以便在横屏和竖屏模式下滚动和布局按预期工作。查看示例以获得更多清晰度。

完成 EditViewController

要完成EditViewController,我们需要添加标签和编辑控件以编辑公园数据。我们还需要进行一些数据转换并保存更新。

要完成EditViewController,执行以下步骤:

  1. EditViewController的视图中添加一个UIScrollView实例。

  2. EditViewController类中添加控件以及相应的输出,以便编辑NationalPark实体上的每个属性。UITextField控件可用于除描述属性之外的所有内容,该属性更适合使用UITextView控件。同时添加UITextLabel控件来标记公园的属性。您还可以再次使用示例国家公园应用部分中的屏幕截图作为指南。

  3. 更新ToUI()方法以考虑新字段:

    private void ToUI ()
    {
       // Update the user interface for the detail item
       if (IsViewLoaded && _park != null) {
         nameTextField.Text = _park.Name;
         descriptionTextView.Text = _park.Description;
         stateTextField.Text = _park.State;
         countryTextField.Text = _park.State;
         latitudeTextField.Text = _park.Latitude.ToString();
         longitudeTextField.Text =
                 _park.Longitude.ToString(); 
       }
    }
    
  4. 创建一个新的方法,在保存之前将数据从 UI 控件移动到实体类,如下所示:

    void ToPark()
    {
      _park.Name = nameTextField.Text;
      _park.Description = descriptionTextView.Text;
      _park.State = stateTextField.Text;
      _park.Country = countryTextField.Text;
    
      if (String.IsNullOrEmpty (latitudeTextField.Text))
        _park.Latitude =
            Double.Parse (latitudeTextField.Text);
      else
            _park.Latitude = null;
    
      if (String.IsNullOrEmpty (longitudeTextField.Text))
        _park.Longitude =
            Double.Parse (longitudeTextField.Text);
      else
        _park.Longitude = null;
    }
    
  5. 更新DoneClicked()操作,以便在将更改保存到NationalParks.json之前调用ToPark(),将值移动到公园对象:

    partial void DoneClicked (NSObject sender)
       {
        ToPark ();
    
        . . .
    }
    
  6. UIScrollViewUITextFields 添加适当的约束,以便在横屏和竖屏模式下滚动和布局按预期工作。查看参考解决方案以获得更多清晰度。

  7. 当键盘显示时,添加逻辑以将活动 UITextField 滚动到视图中。有几种实现此功能的方法。请参考示例以获取解决方案。

运行应用

好的,我们现在有一个功能相当完善的应用。在模拟器中运行应用并测试每个屏幕和导航路径。以下截图显示了三个视图控制器最终的结果:

运行应用

MonoTouch.Dialog

MonoTouch.DialogMT.D)是用于 Xamarin.iOS 的一个框架,它提供了一种声明式的方法来开发用户界面,并消除了编写大量繁琐代码的需要。MT.D 基于使用 UITableView 控件来提供导航并允许用户与数据交互。

更多关于 MT.D 的信息可以在 docs.xamarin.com/guides/ios/user_interface/monotouch.dialog/ 找到。

摘要

在本章中,我们创建了一个示例 Xamarin.iOS 应用并展示了在使用 Xamarin.iOS 平台时需要理解的概念。虽然我们没有展示我们可以在 iOS 应用中使用到的所有功能,但现在你应该对如何访问这些功能感到舒适。

在下一章中,我们将为 Android 构建相同的示例应用。

第五章. 使用 Xamarin.Android 开发您的第一个 Android 应用

在本章中,我们将使用 Xamarin.Android 开发一个类似于上一章中的NationalParks.iOS的示例应用。本章涵盖了以下主题:

  • 示例应用概述

  • 创建 Xamarin.Android 应用

  • 使用 Xamarin Studio 编辑 Android 布局文件

  • 使用 Xamarin Studio 运行和调试应用

  • 使用 Visual Studio 运行和调试应用

  • 检查 Xamarin.Android 应用的编译时生成元素

示例应用

本章我们将创建的示例应用将遵循上一章中NationalParks.iOS应用的基本设计。要回顾屏幕原型和一般描述,您可以参考第四章中的示例国家公园应用部分,使用 Xamarin.iOS 开发您的第一个 iOS 应用。以下截图显示了提供的解决方案中的 Android 屏幕:

示例应用

我们将在应用的 Android 版本中引入一个设计变更;我们将创建一个单例类来帮助管理将公园加载和保存到 JSON 格式文件。我们将在创建单例类的下一节中进一步讨论这个问题,创建 NationalParksData 单例

创建 NationalParks.Droid

我们将首先创建一个新的 Android 项目,并将其添加到上一章创建的现有NationalParks解决方案中。Xamarin Studio 允许 Android 和 iOS 项目成为同一解决方案的一部分。这在我们向后续章节过渡,特别是关注代码重用的章节时,证明是非常有用的。您会发现下一章,第六章,分享游戏,将向您展示如何做到这一点。

要创建国家公园 Android 应用,请执行以下步骤:

  1. 您首先需要启动 Xamarin Studio 并打开上一章创建的NationalParks解决方案。

  2. 然后,在 Xamarin Studio 左侧的解决方案面板中选择NationalParks解决方案,右键单击它,然后导航到添加 | 添加新项目…,如图所示:创建 NationalParks.Droid

  3. 在对话框的左侧导航到C# | Android,在中间部分选择Android 应用程序,如下所示:创建 NationalParks.Droid

  4. 现在,您需要在名称字段中输入NationalParks.Droid并点击确定。Xamarin Studio 将创建一个新的项目,将其添加到NationalParks解决方案中,并打开它。

检查应用

已创建一个简单的工作应用,其中包含多个文件;让我们花几分钟时间回顾一下创建了什么。

资源

Resources 文件夹对应于传统 Java Android 应用中的 res 文件夹。它包含可用于各种类型的子文件夹,包括布局、菜单、可绘制元素、字符串和样式。Resources 中的子文件夹遵循与传统 Java Android 应用相同的命名约定,如可绘制元素、布局、values 等。

Resource.designer.cs 文件

Resource.designer.cs 是位于 Resources 文件夹中的一个 C# 源文件,由 Xamarin.Android 生成,包含应用中所有资源的常量 ID 定义;它对应于为 Java Android 应用生成的 R.java 文件。

MainActivity.cs 文件

MainActivity.cs 是位于 NationalParks.Droid 项目根目录中的一个 C# 源文件,并且是项目中添加的唯一活动。打开文件查看内容。注意类顶部的属性:

[Activity (Label = "NationalParks.Droid",
    MainLauncher = true)]

LabelMainLauncher 的指定将影响 ApplicationManifest.xml 的内容。注意以下代码片段中的重写 OnCreate() 方法:

protected override void OnCreate (Bundle bundle)
{
    base.OnCreate (bundle);

    // Set our view from the "main" layout resource
    SetContentView (Resource.Layout.Main);

    // Get our button from the layout resource,
    // and attach an event to it
    Button button =
        FindViewById<Button> (Resource.Id.myButton);

    button.Click += delegate {
        button.Text = string.Format (
            "{0} clicks!", count++);
    };
}

除了 OnCreate() 使用 C# 语法外,其中的代码看起来与您可能在 Java Android 应用中找到的代码非常相似。接近顶部,内容被设置为 Main 布局文件;Resource.Layout.Main 是在 Resource.designer.cs 中定义的一个常量。通过调用 FindViewById() 获取对 Button 实例的引用,然后分配一个事件处理器来处理点击事件。

主.axml 文件

Main.axml 是位于 Resources/layout 文件夹中的一个 XML 布局文件。Xamarin.Android 使用 .axml 扩展名而不是简单的 .xml 来表示布局文件。除了使用不同的扩展名外,Xamarin.Android 以与 Java Android 应用基本相同的方式处理布局文件。打开 Main.axml 查看内容;屏幕底部有标签页可以切换到视觉、内容视图和源或 XML 视图。注意定义了一个使用 LinearLayout 的单个 Button 实例。

Xamarin.Android 遵循 Android 命名约定来命名布局文件夹,如下所示:

  • Resources/layout:这种命名约定用于正常屏幕尺寸(默认)

  • Resources/layout-small:这种命名约定用于小屏幕

  • Resources/layout-large:这种命名约定用于大屏幕

  • Resources/layout-land:这种命名约定用于横屏模式下的正常屏幕尺寸

项目选项

有许多选项可以设置,这些选项会影响您的应用编译、链接和执行的方式。这些选项可以从 项目选项 对话框中查看和修改。对 Android 应用最有兴趣的部分如下:

  • 构建 | 常规:此设置用于目标框架版本

  • 构建 | Android 构建:此设置用于编译和链接过程以优化生成的可执行文件

  • 构建 | Android 应用程序:此设置提供默认包名、应用程序版本号和应用程序权限

要为NationalParks.Droid设置目标框架版本,请执行以下步骤:

  1. 解决方案选项卡中选择NationalParks.Droid项目。

  2. 右键单击并选择选项

  3. 导航到构建 | 常规,将目标框架选项设置为4.0.3 (冰激凌三明治),然后点击确定

Xamarin Studio 首选项

Xamarin Studio 提供了一个首选项对话框,允许您调整控制环境操作的各种首选项。以下是一些:

  • 项目 | SDK 位置 | Android:使用此选项,您可以控制用于编译和运行应用程序的 Android SDK、Java SDK 和 Android NDK 的位置

  • 项目 | Android:这些设置影响 Android 模拟器的启动方式,包括命令行参数

使用 Xamarin Studio 运行和调试

虽然我们目前拥有的应用程序非常简单,但它可以运行,现在是时候看看如何执行和调试 Xamarin.Android 应用程序了。应用程序可以通过多种方式执行;最常见的方式是 Android 模拟器和物理设备。

使用 Android 模拟器运行应用程序

Xamarin.Android 与 Android 模拟器一起工作,以支持执行和调试您的应用程序。当 Xamarin.Android 安装后,会自动为您设置一系列Android 虚拟设备AVD)。您可以通过从工具菜单中选择打开 Android 模拟器管理器来启动 AVD 管理器。

要运行NationalParks.Droid,请执行以下步骤:

  1. 点击任务栏左侧的启动/停止按钮。您也可以通过按F5键或导航到运行 | 开始调试来运行应用程序。

  2. 然后,在选择设备对话框中选择一个 AVD,然后点击启动模拟器

  3. 当模拟器完成启动后,在设备列表中选择正在运行的模拟器实例名称,然后点击确定

  4. 现在,您需要点击应用程序上的Hello World按钮,并注意标题的变化。

  5. 切换回 Xamarin Studio,通过点击任务栏左侧的启动/停止按钮来停止应用程序。

  6. 打开MainActivity.cs,通过在编辑器窗口的左侧边缘点击来在OnCreate()中的SetContentView()语句上设置断点,您可以在下面的屏幕截图中看到。此时,重新启动NationalParks.Droid;应用程序将在断点处停止:使用 Android 模拟器运行应用程序

  7. 您将在任务栏中找到基本流程控制,用于逐步执行。这些允许您(从左到右的图标)继续执行、跳过当前行、进入当前函数和退出当前函数:使用 Android 模拟器运行应用程序

  8. 使用步骤控件跳转到第 27 行,突出显示文本中的按钮,右键单击并选择表达式评估器表达式评估器对话框可以用来查看程序执行期间对象的状态,如下所示:使用 Android 模拟器运行应用

  9. 您还会注意到 Xamarin Studio 底部有一组面板,包含观察局部变量断点线程应用程序输出调用堆栈标签页,如下所示:使用 Android 模拟器运行应用

  10. 点击继续按钮以允许应用继续运行。

如您所见,Xamarin Studio 与 Android 模拟器结合使用,提供了一个强大的环境来执行和调试应用,其中包含大多数现代 IDE 中可以找到的功能。

您可以从 AVD 管理器(工具 | 打开 Android 模拟器管理器)修改 AVD 列表,也可以从 Android SDK 管理器(工具 | 打开 Android SDK 管理器)调整 Android SDK。

在物理设备上运行应用

Xamarin Studio 还支持在物理设备上调试应用。这通常是开发调试应用最有效的方法,因为许多设备功能在模拟器中配置和使用可能具有挑战性。要让 Xamarin Studio 与设备协同工作,实际上并没有什么特别之处;只需按照设备上启用 USB 调试的正常步骤操作,将设备连接到您的计算机,并从 Xamarin Studio 启动应用;设备将显示在 Xamarin Studio 的选择设备对话框中。如您所知,在 Windows 上,需要一个与所使用的设备相对应的特殊 USB 驱动程序;通常,OS X 用户无需担心。

使用模拟器或物理设备进行调试的问题并不独特,甚至与 Xamarin 的使用无关;这是所有 Android 开发者都会面临的问题。

使用 Genymotion 运行应用

不久前,我了解到另一种运行 Android 应用的选择。Genymotion 是一个基于 VirtualBox 虚拟化平台的产品。Genymotion 为市场上许多 Android 设备提供了一套虚拟设备模板。一旦创建了一个虚拟设备,您只需启动它,它就会像正在运行的 AVD 一样,在 Xamarin Studio 的选择设备对话框中可供选择。

由于 Genymotion 附带了许多不同的设备模板,它是一个出色的测试工具。Genymotion 的启动时间和执行时间也比标准 Android 模拟器快得多。根据您需要的功能,有免费和付费版本,无论您是使用 Xamarin.Android 还是原生 Java Android 开发。您可以在他们的主页www.genymotion.com上找到更多关于 Genymotion 的信息。

扩展 NationalParks.Droid

由于我们对起点有很好的理解,现在我们可以将注意力转向增强现有功能以支持我们需要的特性。我们需要完成以下增强:

  1. MainActivity 中添加一个 ListView 实例以列出国家公园,并在 ActionBar 类中添加一个添加动作以添加新的国家公园。

  2. 添加一个可以用来查看和更新国家公园的详细视图,包括保存和删除国家公园的操作,以及查看 www.Bing.com 上的照片和从地图提供商获取方向。

  3. 添加逻辑以将国家公园加载和保存到 JSON 格式的文本文件中。

存储和加载国家公园

NationalParks.iOS 项目类似,我们将把我们的公园数据存储在 JSON 格式的文本文件中。在这个项目中,我们将创建一个单例类来帮助管理加载和保存公园。我们选择单例类有几个原因:

  • 在下一章中,我们将开始探讨代码的共享和重用;这将使我们开始构建我们希望重用的解决方案。

  • 在 Android 中,在 Activities 之间传递对象比在 iOS 中在 ViewControllers 之间传递对象要困难一些,而单例类将提供一种方便的方式来共享公园数据

添加 Json.NET

如果你完成了 第四章,使用 Xamarin.iOS 开发您的第一个 iOS 应用,Json.NET 将已经安装在你的机器上,你只需将其添加到你的项目中。

要将 Json.NET 添加到 NationalParks.Droid 项目中,请执行以下步骤:

  1. 解决方案 面板中,选择 NationalParks.Droid 项目的 Components 文件夹,右键单击并选择 编辑组件

  2. 如果你看到 安装在这台机器上 部分列出了 Json.NET,点击 添加到项目 即可完成;否则继续下一步。

  3. 在右上角,点击 获取更多组件 并在搜索字段中输入 Json.NET

  4. 从列表中选择 Json.NET 并选择 添加到应用

借用实体类和 JSON 文件

我们需要一个表示我们主题领域的实体类:国家公园。如果你在 第四章,使用 Xamarin.iOS 开发您的第一个 iOS 应用 中工作过,这听起来很熟悉,在那里我们创建了一个。由于已经存在一个,我们无需从头创建,所以让我们从 NationalParks.iOS 中复制它。在 第六章,共享游戏 中,我们将查看如何在项目之间实际共享代码。

要复制 NationalPark.cs 文件,请执行以下步骤:

  1. NationalParks.iOS 项目中,选择 NationalPark.cs 文件,右键单击它,并选择 复制

  2. 选择 NationalPark.Droid 项目,右键单击它并选择 粘贴

创建 NationalParksData 单例

如前所述,我们将创建一个单例类来简化共享和访问国家公园。单例是一种设计模式,它将类在应用程序中可以存在的实例数量限制为单个实例。单例有助于维护全局状态并在多个视图中共享单个对象。对于我们的目的,单例模式简化了管理单个国家公园集合以及存储加载和保存公园到 JSON 格式文件的逻辑。

要创建 NationalParksData,请执行以下步骤:

  1. 选择 NationalParks.Droid,右键单击它,然后导航到 添加 | 新建文件

  2. 选择 通用 | 空类,在 名称 字段中输入 NationalParksData,然后单击 新建

  3. 添加一个 static instance 属性来访问 NationalParksData 的单个实例,并在属性的获取器中初始化单例实例,如下所示:

    private static NationalParksData _instance;
    public static NationalParksData Instance
    {
        get { return _instance ??
            (_instance = new NationalParksData()); }
    }
    
  4. 添加一个 Parks 集合属性以将公园加载到:

       public List<NationalPark> Parks { get; protected set; }
    

    注意

    注意使用 protected set,这可以保护 Parks 属性不被单例类外部修改。

  5. 我们需要确定几个地方来加载和保存到 JSON 格式文件的公园的文件名。创建一个返回完全限定文件名的方法,如下所示:

    protected string GetFilename()
    {
      return Path.Combine (
           Environment.GetFolderPath(
               Environment.SpecialFolder.MyDocuments),
              "NationalParks.json");
    }
    
  6. 添加一个私有构造函数,如果存在文件,则加载 Parks 集合。提供私有构造函数是实现单例模式的一部分,因为它有助于确保只有一个实例存在。可以使用以下代码片段添加私有构造函数:

    private NationalParksData ()
    {
      if (File.Exists (GetFilename())) {
        string var serializedParks = File.ReadAllText
            (GetFilename());
        Parks =
          JsonConvert.DeserializeObject<List<NationalPark>>
            (serializedParks);
      }
      else
        Parks = new List<NationalPark> ();
    }
    
  7. 添加一个 Save() 方法。此方法接受一个公园,如果它是新公园,则将其添加到 Parks 集合中,然后将集合保存到文件中,如下所示:

    public void Save(NationalPark park)
    {
      if (Parks != null) {
        if (!Parks.Contains (park))
          _parks.Add (park);
        string var serializedParks =
          JsonConvert.SerializeObject (Parks);
        File.WriteAllText (GetFilename (), serializedParks);
      }
    }
    
  8. 添加一个 Delete() 方法。此方法从 Parks 集合中删除公园,并将更新后的集合保存到文件中,如下所示:

    public void Delete(NationalPark park)
    {
      if (Parks != null) {
        Parks.Remove (park);
        string serializedParks =
          JsonConvert.SerializeObject (Parks);
        File.WriteAllText (GetFilename (), serializedParks);
      }
    }
    

增强 MainActivity

NationalParksData 单例就绪后,我们可以继续进行一些 UI 工作。Xamarin.Android 项目模板在上一章中并没有给我们太多帮助。我们需要向 MainActivity 添加一个列表视图,创建一个 DetailActivity 来查看公园,并创建一个 EditActivity 来更新和删除公园。MainActivity 是一个很好的起点。

添加 ListView 实例

当我们创建项目时生成的默认视图 (Main.xml) 包含一个位于 LinearLayout 中的 Button 实例。我们需要删除此按钮并添加一个 ListView 实例来显示我们的公园。

探索 Xamarin.Android 设计器

Xamarin Studio 提供了一个图形设计工具来创建和编辑布局文件。由于这是我们第一次使用这个工具,我们将花几分钟时间熟悉它。请执行以下步骤:

  1. 打开 Main.xml;注意视图底部的两个标签页,内容。选择内容标签页时,会显示布局的可视表示。选择标签页时,会在 XML 编辑器中显示原始 XML。

  2. 现在,切换到内容标签页。注意在 Xamarin Studio 的右侧有两个面板,文档大纲属性。当在内容模式下打开布局时,文档大纲面板显示布局文件内容的分层视图。文档大纲面板显示了 LinearLayout 中的按钮控件。

  3. 属性面板显示当前所选小部件的属性。选择按钮实例,切换到属性面板。注意属性面板顶部的标签:小部件样式布局滚动行为。这些标签将特定小部件可用的各种类型属性分组在一起。

编辑 Main.xml 文件

要在 Main.xml 中添加 ListView 实例,请执行以下步骤:

  1. 内容模式下打开 Main.axml,选择按钮实例,右键单击它,然后选择删除(或按删除键)。

  2. 工具箱标签页顶部的搜索框中输入 List。选择显示的 ListView 小部件,并将其拖放到 Main.axml 中。

  3. 文档大纲面板中,选择 ListView 小部件。

  4. 小部件标签页下的属性面板中,将ID值输入为 @+id/parksListView

  5. 文档大纲面板中,选择 LinearLayout 小部件。

  6. 布局标签页下的属性面板中,将填充值输入为 8dp

创建适配器

我们需要一个 ListAdapter 实例来填充我们的 ListView 以显示国家公园。我们将创建一个继承自 BaseAdapter 的适配器。

要创建 NationalParksAdapter.cs,请执行以下步骤:

  1. 选择 NationalParks.Droid 项目,右键单击它,然后选择新建文件。在新建文件对话框中,导航到Android | Android 类

  2. 名称字段中输入 NationalParks.cs 并单击新建

  3. NationalParksAdapter 修改为公共类,并使用 NationalPark 作为类型规范扩展 BaseAdapter<>,如下所示:

    public class NationalParksAdapter :
        BaseAdapter<NationalPark>
    {
    }
    
  4. 将光标放在 BaseAdapater<> 上,右键单击它,然后导航到重构 | 实现抽象成员,然后按 Enter。Xamarin Studio 将为每个抽象方法创建一个默认的方法存根,其中包含抛出异常 NotImplementedException 的代码。

  5. 在这个阶段,你可以实现一个接受活动并保存其引用以在 GetView() 中使用的构造函数,如下面的代码片段所示:

    private Activity _context;
    public NationalParksAdapter(Activity context)
    {
        _context = context;
    }
    
  6. 实现GetItemId()方法,并返回作为 ID 传入的位置。GetItemId()方法旨在为在AdapterView中显示的数据行提供一个 ID。不幸的是,该方法必须返回一个long实例,而我们的 ID 是一个 GUID。我们能做的最好的事情就是返回传递给我们的位置,如下所示:

    public override long GetItemId(int position)
    {
        return  position;
    }
    
  7. 实现Count属性,以返回Parks集合中的项目数量,如下所示:

    public override int Count
    {
        get { return NationalParksData.Instance.Parks.Count; }
    }
    
  8. 实现索引属性,并返回位于Parks集合中传入位置处的NationalPark实例,如下所示:

    public override NationalPark this[int position]
    {
        get { return NationalParksData.Instance.Parks[position]; }
    }
    
  9. 实现GetView()方法,并返回一个使用默认 Android 布局SimpleListItem1填充的View实例,如下所示:

    public override View GetView(int position,
        View convertView, ViewGroup parent)
    {
        View view = convertView;
        if (view == null) {
            view = 
                _context.LayoutInflater.Inflate(
                    Android.Resource.Layout.SimpleListItem1,
                    null);
        }
    
        view.FindViewById<TextView>
            (Android.Resource.Id.Text1).Text =
                NationalParksData.Instance.Parks [position].Name;
    
        return view;
    }
    
  10. 为了完成这些步骤,将适配器连接到MainActivity上的ListView。这通常在OnCreate()方法中完成,如下所示:

    NationalParksAdapter _adapter;
    . . .
    protected override void OnCreate (Bundle bundle)
    {
        . . . 
        _adapter = new NationalParksAdapter (this);
        FindViewById<ListView>
           (Resource.Id.parksListView).Adapter = _adapter;
        . . .
    }
    

向 ActionBar 添加新操作

现在,我们需要向 ActionBar 添加一个Add操作,它可以用来创建一个新的国家公园。

要创建Add操作,执行以下步骤:

  1. 首先,在NationalParks.Droid项目中选择Resources文件夹,右键单击它,然后导航到添加 | 新建文件夹

  2. 在这一点上,将文件夹命名为menu

  3. 选择新创建的menu文件夹,右键单击它,然后导航到添加 | 新建文件,然后选择XML | 空 XML 文件,在名称字段中输入MainMenu.xml,然后点击新建

  4. 在新创建的 XML 文件中填写一个用于Add操作的菜单定义。以下示例演示了所需的内容:

    <menu
      >
        <item android:id="@+id/actionNew"
            android:icon="@drawable/ic_new"
            android:title="New"
            android:showAsAction="ifRoom" />
    </menu>
    
  5. Assets文件夹中的所有图像文件(*.png)复制到NationalParks.Droid项目中的Resources/drawable文件夹。

  6. 选择Resources/drawable文件夹,右键单击并选择添加文件,选择所有图像文件,包括ic_new.png,然后点击打开

现在我们已经有了菜单定义和图形,我们需要添加一些代码来放置菜单。Android 提供了几个虚拟方法来创建和处理 ActionBar 项的点击事件。

覆盖OnCreateOptionsMenu()方法

当活动启动时,会调用OnCreateOptionsMenu()方法,并提供了一个创建 ActionBar 项的地方。以下代码演示了如何使用MainMenu.xml中的定义来创建Add操作:

public override bool OnCreateOptionsMenu(IMenu menu)
{
    MenuInflater.Inflate(Resource.Menu.MainMenu, menu);
    return base.OnCreateOptionsMenu(menu);
}

覆盖OnOptionsItemSelected()方法

当在 ActionBar 中点击操作时,会调用OnOptionsItemsSelected()方法,并提供了一个处理请求的地方。在我们的例子中,我们希望导航到尚未创建的详细视图。目前,只需简单地实现OnOptionsItemSelected()方法,并使用占位符来代替导航逻辑。以下代码演示了所需的内容:

public override bool OnOptionsItemSelected (
    IMenuItem item)
{
    switch (item.ItemId)
    {
    case Resource.Id.actionNew:
        // Navigate to Detail View
        return true;

    default :
        return base.OnOptionsItemSelected(item);
    }
}

运行应用

我们已经完成了对MainActivity的增强。运行应用并查看更改。当您首次启动应用时,您会注意到ListView是空的。您可以使用Android Device MonitorADM)将NationalParks.json文件放置在模拟器虚拟设备中。Xamarin Studio 没有配置用于 ADM 的菜单项,但您可以使用Preferences | External Tools添加一个。

使用 ADM 应用程序将NationalParks.json上传到模拟器。重新启动NationalParks.Droid;您现在应该看到公园列表。

创建 DetailActivity 视图

现在,让我们添加一个显示国家公园详细信息的视图。为此,我们需要创建一个简单的视图,其中ScrollView作为父ViewGroup,并为NationalPark实体类上的每个属性添加EditText小部件。

要创建DetailActivity视图,请执行以下步骤:

  1. Solution面板中选择NationalParks.Droid项目,右键单击并导航到Add | New File

  2. 在此之后,导航到Android | Android Activity,将Name字段的值设置为DetailActivity,然后点击New

  3. 然后,在NationalParks.Droid中的Resources/layout文件夹上右键单击,并导航到Add | New File

  4. 导航到Android | Android Layout,将Name字段的值设置为Detail,然后点击New

  5. Outline面板中,选择LinearLayout,右键单击它,并选择Delete

  6. Toolbox面板中选择ScrollView小部件,并将其拖放到Detail.axml布局中。

  7. Toolbox面板中选择LinearLayout,并将其拖放到Detail.axml布局中。

  8. Layout选项卡下的Properties面板中,将LinearLayoutPadding设置为8dp

  9. NationalPark实体类上的每个属性添加TextView小部件(除了ID属性)。还要添加作为标签的TextView小部件。对于将用于显示属性的每个TextView小部件,将ID属性填写为与实体属性名称相对应的名称,例如nameTextView。根据您的偏好排列小部件;您可以使用第四章中The sample national parks app部分的屏幕模拟图,或作为指南的示例解决方案。

  10. Content模式下查看Detail.axml,并根据需要调整。

  11. DetailActivity.OnCreate()中,添加对SetContentView()的调用,并传递Detail.axml的布局 ID,如下所示:

    SetContentView (Resource.Layout.Detail);
    

添加 ActionBar 项目

现在,我们需要向操作栏添加三个项目:编辑公园的操作、在www.bing.com上查看公园的照片,以及获取前往公园的路线。按照之前创建新菜单定义文件DetailMenu.xml的相同步骤进行操作。以下 XML 显示了需要使用的代码:

<menu >
    <item android:id="@+id/actionEdit"
        android:icon="@drawable/ic_edit"
       android:title="Edit"
       android:showAsAction="ifRoom" />
   <item android:id="@+id/actionPhotos"
       android:title="Photos"
       android:showAsAction="never" />
    <item android:id="@+id/actionDirections"
       android:title="Directions"
       android:showAsAction="never" />
</menu>

在添加菜单定义后,实现OnCreateOptionsMenu()OnOptionsItemSelected()方法,就像我们为MainActivity所做的那样。只需添加空占位符来处理实际的动作,我们将在接下来的部分中填充逻辑。

填充DetailActivity

OnCreate()中添加逻辑以填充DetailActivity,按照以下步骤操作:

  1. 第一步是确定是否将公园Id作为意图附加信息传递。如果传递了,则在NationalParksDataParks列表中定位它。如果没有,则使用以下代码片段创建一个新实例:

    if (Intent.HasExtra ("parkid")) {
        string parkId = Intent.GetStringExtra ("parkid");
        _park = NationalParksData.Instance.
            Parks.FirstOrDefault (x => x.Id == parkId);
    }
    else
        _park = new NationalPark ();
    
  2. 现在根据公园的数据填充EditText字段。示例解决方案有一个ParkToUI()方法用于此逻辑,如下所示:

    protected void ParkToUI()
    {
        _nameEditText.Text = _park.Name;
        . . .
        _latEditText.Text = _park.Latitude.ToString();
        . . .
    }
    

处理显示照片动作

我们希望将用户引导到www.bing.com以查看公园的照片。这可以通过一个简单的ActionView意图和一个格式正确的搜索 URI 来实现。

要处理显示照片动作,需要在OnOptionsItemSelected()方法中创建一个ActionView意图,并传入一个格式化的 URI 来搜索www.bing.com上的照片。以下代码演示了所需的操作:

public override bool OnOptionsItemSelected (
    IMenuItem item)
{
    switch (item.ItemId) {
    . . .
    case Resource.Id.actionPhotos:
        Intent urlIntent =
            new Intent (Intent.ActionView);
          urlIntent.SetData (
            Android.Net.Uri.Parse (
            String.Format(
                "http://www.bing.com/images/search?q={0}",
                _park.Name)));
        StartActivity (urlIntent);
        return true;
    . . .
    }
}

处理显示路线动作

我们希望将用户引导到外部地图应用以获取前往公园的路线。同样,这可以通过一个简单的ActionView意图以及一个格式正确的 URI 请求地图信息来实现。

要处理显示路线动作,在OnOptionsItemSelected()方法中创建逻辑以创建一个ActionView意图,并传入一个格式化的 URI 来显示地图信息。以下代码演示了所需的操作:

case Resource.Id.actionDirections:

    if ((_park.Latitude.HasValue) &&
       (_park.Longitude.HasValue)) {
        Intent mapIntent = new Intent
           (Intent.ActionView,
            Android.Net.Uri.Parse (
            String.Format ("geo:0,0?q={0},{1}&z=16 ({2})",
            _park.Latitude,
            _park.Longitude,
            _park.Name)));
        StartActivity (mapIntent);
    }

    return true;

添加导航

现在我们已经有了DetailActivity,我们需要回到MainActivity并添加一些导航逻辑,以便当在列表中选择公园时,将显示DetailActivity

当用户点击ListView中的项目时,可以通过为ListView.OnItemClicked提供事件处理程序来处理。

要从MainActivity添加导航,请执行以下步骤:

  1. 打开MainActivity.cs

  2. 创建一个事件处理程序来处理OnItemClicked事件。以下事件处理程序代表了所需的内容:

    public void ParkClicked(object sender,
        ListView.ItemClickEventArgs e)
    {
        Intent intent = new Intent (this,
            typeof(DetailActivity));
        intent.PutExtra("parkid", adapter[e.Position].Id);
        StartActivity (intent);
    }
    
  3. OnCreate()方法中连接事件处理程序,如下所示:

    FindViewById<ListView>
        (Resource.Id.parksListView).ItemClick   += ParkClicked;
    

运行应用程序

我们现在已经完成了DetailActivity。运行应用程序并选择一个公园以显示新的活动。选择显示照片和显示路线动作。如果您在模拟器中运行应用程序,您将无法查看路线,因为模拟器无法访问 Google Play 服务。

创建EditActivity

我们现在准备添加我们的最后一个活动,EditActivity。这个练习将与我们刚刚完成的练习类似,但我们将使用EditText小部件,以便用户可以修改数据。此外,EditActivity可以用来显示现有的公园或新的公园。

要创建EditActivity,请执行以下步骤:

  1. 按照上一节中使用的相同步骤创建一个新的活动和一个名为EditActivityEdit.axml的布局文件。

  2. 此外,以与Detail.axml相同的方式添加ScrollViewLinearLayoutPadding

  3. NationalPark实体类上的每个属性(除了Id属性)添加TextView小部件和EditText小部件。TextView小部件应用作标签,而EditText小部件用于编辑属性。对于将用于显示属性的每个EditView小部件,将Id属性填充为与实体上的属性名称相对应的名称,例如nameTextView。根据您的偏好排列小部件;您可以使用第四章中示例国家公园应用程序部分的屏幕原型,或作为指南的示例解决方案。

  4. 内容模式下审查Edit.axml并根据需要调整。

  5. EditActivity.OnCreate()中,添加对SetContentView()的调用并传入Edit.axml的布局Id,如下所示:

    SetContentView (Resource.Layout.Edit);
    

添加 ActionBar 项目

我们现在需要向工具栏添加三个项目:编辑公园的操作、在www.bing.com上查看公园的照片,以及获取公园的路线。按照上一节中添加新的 ActionBar 操作部分中使用的相同步骤创建一个新的菜单定义文件,命名为DetailMenu.xml。以下 XML 显示了所需的代码:

<menu >
    <item android:id="@+id/actionSave"
    android:icon="@drawable/ic_save"
      android:title="Save"
      android:showAsAction="always" />
    <item android:id="@+id/actionDelete"
    android:icon="@drawable/ic_delete"
      android:title="Delete"
      android:showAsAction="always" />
</menu>

在添加菜单定义后,实现OnCreateOptionsMenu()OnOptionsItemSelected()方法,就像我们为MainActivity所做的那样。为每个操作添加空占位符,我们将在接下来的部分中填充逻辑,如下所示:

public override bool OnOptionsItemSelected (IMenuItem item)
{
    switch (item.ItemId)
    {
        case Resource.Id.actionSave:
            // will add save logic here…
            return true;

        case Resource.Id.actionDelete: 
            // will add delete logic here…
            return true;

        default :
            return base.OnOptionsItemSelected(item);
    }
}

创建小部件的引用变量

由于我们将数据放入EditText小部件,然后再将其拉出来,因此为小部件创建引用变量并在OnCreate()方法中设置引用是有意义的。

要为小部件创建引用变量,请执行以下步骤:

  1. EditActivity类中创建一组EditText对象的引用,如下所示:

    EditText _nameEditText;
    EditText _descrEditText;
    . . .
    
  2. EditActivityOnCreate()方法中,使用FindViewById()设置对适当小部件的引用,如下所示:

    _nameEditText= FindViewById<EditText>
        (Resource.Id.nameEditText);
    _descrEditText = FindViewById<EditText>
        (Resource.Id.descrEditText);
    . . .
    

填充EditActivity

要填充EditActivity,请执行以下步骤:

  1. 创建一个名为ParkToUI()的方法,将数据从_park对象移动到EditText小部件,如下所示:

    protected void ParkToUI()
    {
          _nameEditText.Text = _park.Name;
          _descrEditText.Text = _park.Description;
          . . .
    }
    
  2. 重写OnResume()并添加对ToUI()方法的调用以填充EditText小部件,如下所示:

    protected override void OnResume ()
    {
        base.OnResume ();
        ParkToUI ();
    }
    

处理保存操作

当点击保存操作时,会调用OnOptionsItemSelected()方法。在DetailActivity上创建一个Save()方法,并在OnOptionsItemSelected()中调用它。解决方案项目有一个UIToPark()方法,用于从EditText小部件获取内容,并在保存之前填充Park实体。

处理保存操作,请执行以下步骤:

  1. 创建一个名为ToPark()的方法,将数据从EditText小部件移动到_park对象。此方法将在处理保存操作时使用,如下所示:

    protected void UIToPark()
    {
        _park.Name = _nameEditText.Text;
        _park.Description = _descrEditText.Text;
        . . .
        if (!String.IsNullOrEmpty (_latEditText.Text))
            _park.Latitude = Double.Parse (_latEditText.Text);
        else
            _park.Latitude = null;
        . . .
    }
    
  2. 创建一个处理保存公园的方法,该方法调用UIToPark()以将更改填充到_park对象中,然后它调用NationalParksData上的Save()方法以将更改保存到文件,设置结果代码,并结束活动。所需的代码如下:

    protected void SavePark()
      {
          UIToPark ();
          NationalParksData.Instance.Save (_park);
    
        Intent returnIntent = new Intent ();
        returnIntent.PutExtra ("parkdeleted", false);
        SetResult (Result.Ok, returnIntent);
    
          Finish ();
    }
    

    注意

    注意,名为parkdeleted的布尔Extra被设置为false。这用于通知调用活动公园没有被删除。

  3. 更新OnOptionsItemSelected()以调用SavePark(),如下所示:

    case Resource.Id.actionSave:
          SavePark ();
          return true;
    

处理删除操作

处理删除操作与保存操作类似,但稍微简单一些,因为我们不需要从 UI 小部件保存更改。

要处理删除操作,请执行以下步骤:

  1. 创建一个方法来处理删除公园的操作,通过在NationalParksData上调用Delete()方法,设置结果代码,并结束活动,如下所示:

    protected void DeletePark()
    {
        NationalParksData.Instance.Delete (_park);
    
        Intent returnIntent = new Intent ();
        returnIntent.PutExtra ("parkdeleted", true);
        SetResult (Result.Ok, returnIntent);
    
        Finish ();
    }
    

    注意

    注意,名为parkdeleted的布尔Extra被设置为true,以通知调用活动公园已被删除。这对于DetailActivity很重要,因为当一个公园之前显示为已删除时,它应该结束并返回到MainActivity

  2. 更新OnOptionsItemSelected()以调用DeletePark(),如下所示:

    case Resource.Id.actionDelete:
          DeletePark ();
          return true;
    

添加导航

现在EditActivity已经就绪,我们需要在用户选择新建操作时在MainActivity中添加导航逻辑,以及在用户选择编辑操作时在DetailActivity中添加导航逻辑。

在新操作上进行导航

如您所回忆的,在MainActivityOnMenuItemSelected()中,我们在需要导航到EditActivity的地方添加了一个注释。我们现在可以用以下StartActivity()的使用来替换这个注释:

public override bool OnOptionsItemSelected (
    IMenuItem item)
{
    switch (item.ItemId)
    {
        case Resource.Id.actionNew:
            StartActivity (typeof(DetailActivity));
            return true;
        default :
            return base.OnOptionsItemSelected(item);
    }
}

在编辑操作上进行导航

同样,我们还需要在DetailActivityOnMenuItemSelected()中添加导航代码。然而,有一些区别。我们需要传递我们想要编辑的公园的Id属性,并且我们希望接收一个结果,指示用户是否删除了此公园。所需的代码如下:

case Resource.Id.actionEdit:
    Intent editIntent = new Intent(this, 
        typeof(EditActivity));
    editIntent.PutExtra("parkid", _park.Id);
    StartActivityForResult(editIntent, 1);
    return true;

DetailActivity还需要检测公园被删除的情况,以便它可以完成并返回到MainActivity查看列表。为此,重写OnActivityResult()并检查名为parkdeleted的布尔Extra,以确定公园是否被删除,如下所示:

protected override void OnActivityResult (
    int requestCode, Result resultCode, Intent data)
  {
      if ((requestCode == 1) && (resultCode == Result.Ok))
      {
          if (data.GetBooleanExtra ("parkdeleted", false))
              Finish ();
          }
        else
            base.OnActivityResult (
                requestCode, resultCode, data);
      }
}

MainActivity中刷新 ListView

我们需要实现的最后一件事是逻辑,该逻辑将刷新MainActivity中的ListView,以反映在EditActivity上可能进行的任何更改。为此,在MainActivityOnResume()方法的重写中调用适配器对象的NotifyDataSetChanged(),如下所示:

protected override void OnResume ()
{
    base.OnResume ();
    adapter.NotifyDataSetChanged ();
}

运行应用程序

我们现在已经完成了 NationalParks.Droid 应用程序。你现在应该能够运行你的应用程序并练习每个功能。

在 Visual Studio 中使用 Xamarin.Android 项目

如果你在一台安装了 Visual Studio 2010 或 Visual Studio 2013(当前版本)的 Windows 机器上安装了 Xamarin.Android,Xamarin.Android Visual Studio 附加组件已经安装。在 Visual Studio 中处理项目与在 Xamarin Studio 中处理项目类似,但某些功能除外。要访问项目选项,请执行以下步骤:

  1. 选择 NationalParks.Droid 项目,右键单击并选择 属性。将打开一个多标签窗口,允许指定各种项目相关选项。

  2. 要访问 Visual Studio 中与 Xamarin.Android 相关的选项,请转到 工具 | 选项 | Xamarin | Android 设置

  3. 要访问 AVD 管理器,请转到 工具 | 打开 Android 模拟器管理器

  4. 要管理你的 Xamarin 账户并激活许可证,请转到 工具 | Xamarin 账户

如果你在一台安装了 Visual Studio 的 Windows 机器上工作,并且还没有尝试过附加组件,请打开 Visual Studio 中的 NationalParks.Droid 并运行应用程序。

查看生成的元素

在结束本章之前,让我们看看幕后发生的一些事情。

同伴对象

在 第三章 的 揭秘 Xamarin.Android 中,我们讨论了在 Xamarin.Android 应用程序中同伴对象的作用。现在,让我们看看我们项目中的一个生成的 Java 同伴对象。这些类的源代码可以在 NationalParks.Droid/obj/Debug/android/src 中找到。打开 nationalparks.droid.MainActivity.java。现在,注意以下提示:

  • MainActivity 继承自 android.app.Activity

  • 我们为每个创建覆盖的方法都创建了一个相应的方法,该方法调用我们的覆盖方法。例如,我们为 OnCreate() 创建了一个覆盖方法。生成的类有一个名为 onCreate() 的方法,该方法调用一个私有本地方法 n_onCreate(),该方法通过 JNI 引用指向我们的覆盖方法。

  • MainActivity 的静态类初始化器使用 mono.android.Runtime.register() 方法注册所有用于 JNI 的本地方法。

  • 类构造函数使用 mono.android.TypeManager.Activate() 方法激活我们的托管 C# 类的实例。

AndroidManifest.xml 文件

Xamarin.Android 在构建时使用两个源生成 AndroidManifest.xml 文件:第一个是 NationalParks.Droid/PropertiesAndroidManifest.xml 文件的内容,第二个是类上指定的属性,主要是项目中的活动。你可以在 NationalParks.Droid/obj/Debug/android 中找到生成的 AndroidManifest.xml。使用文本编辑器打开文件,并注意以下提示:

  • 文件中有两个 <activity/> 元素,MainActivity 被指定为启动活动。这些条目是从每个活动类中指定的属性生成的。

  • 指定了一个 INTERNET 权限。这来自 NationalParks.Droid/Properties 文件夹中的 AndroidManifest.xml 文件。

APK 文件

另一个值得关注的点是生成的 Xamarin.Android 应用程序的 APK。我们将在第十章 准备分发 Xamarin.Android 应用 中详细说明如何创建 APK,准备分发 Xamarin.Android 应用。这是一个相当简单的过程;如果您迫不及待,可以使用以下步骤:

  1. 在工具栏的左上角,将构建类型设置为 发布

  2. 项目 菜单中选择 发布 Android 项目

  3. 发布 Android 应用 对话框中,选择 创建新密钥库,填写所有必需的信息,然后点击 创建

  4. Xamarin.Android 将将 APK 发布到您选择的目录。由于 APK 是 ZIP 文件,只需解压 APK 即可查看内容。

    以下截图显示了生成的 APK 的内容:

    APK 文件

以下表格提供了 APK 内容的描述:

内容 描述
assemblies/System.* 这些程序集包含核心 .NET 命名空间,例如 System.IOSystem.Collection
assemblies/Mono.Android.dll 此程序集包含 Xamarin.Android 绑定类
assemblies/NationalParks.Droid.dll 此程序集包含我们创建的类:MainActivityDetailActivityNationalParksAdapter
assemblies/Newtonsoft.Json.dll 此程序集包含 Json.NET 类
classes.dex 此文件包含所有以 Dalvik 编译格式生成的 Java 同伴对象
lib/armeabi-v7a/libmonodroid.so 这是 Android 的 Mono CLR
res/* 此文件夹包含所有资源;可绘制对象、布局、菜单等

摘要

在本章中,我们创建了一个示例 Xamarin.Android 应用,并演示了与 Xamarin.Android 平台一起工作时需要理解的概念。虽然我们没有演示 Android 应用中可以使用的所有功能,但您现在应该对如何访问这些功能感到舒适。

在下一章中,我们将关注跨应用共享代码等重要主题,这是使用 Xamarin 的关键优势之一。

第六章. 分享游戏

在本章中,我们将讨论使用 Xamarin 开发中最有趣和最重要的方面之一:跨平台代码共享。我们将涵盖以下主题:

  • 文件链接技术

  • 可移植类库

  • 每种方法的优缺点

分享和重用

使用 Xamarin 和 C#的一个优点是能够在移动应用程序以及其他.NET 解决方案之间共享代码。代码的重用可以提供显著的生产力和可靠性优势,同时减少许多与长期运行应用程序相关的长期维护难题。这很好,但任何长期参与软件开发的人都知道,重用不是免费的,也不是简单就能实现的。

代码重用的实际方面;问题在于,“在物理上,我该如何打包我的代码以便重用?”为此,我们可以使用以下三种方法之一:

  • 将源代码共享,可以编译成多个项目

  • 共享动态链接库DLL),可以被多个项目引用

  • 将代码作为服务共享,以便多个客户端远程访问

此外,还有一些更战略性的方面;同样的问题再次出现,“我该如何组织我的代码,以便我可以重用更多?”为了解决这个问题,我们有以下选择:

  • 创建分层方法,以便将数据访问逻辑和业务验证从用户界面逻辑中分离出来

  • 利用接口和框架将特定平台的服务从可重用层抽象出来

在本章中,我们将涉及这两个方面的重用,但主要关注重用的实际方面。具体来说,我们将介绍两种不同的方法来打包代码以供重用。

那么,我们应该尝试重用代码的哪些部分?在我们对NationalParks应用程序所做的工作中,一组明显的代码非常适合重用:持久化代码,这是从 JSON 文件加载公园并将其保存回同一文件的逻辑。在第五章《使用 Xamarin.Android 开发您的第一个 Android 应用程序》中,我们通过创建NationalParkData单例来朝着可重用解决方案迈进。在本章中,我们将展示两种不同的方法,用于在两个项目以及可能需要它的其他.NET 项目中共享NationalParkData单例。

传统的源文件链接

文件链接是指一种技术,其中源代码文件通过 Xamarin 项目进行链接或引用,并在对项目运行构建时与项目中的其他源代码一起编译。当使用文件链接时,不会为共享的文件创建单独的 DLL,而是代码被编译成与链接的文件相同的项目生成的 DLL;在我们的例子中,是NationalParks.iOS.dllNationalParks.Droid.dll

创建共享库项目

我们将首先创建一个新的Library项目来存放可重用代码。要创建Library项目,请执行以下步骤:

  1. 将名为NationalParks.Data的新库项目添加到NationalParks解决方案中。您可以在新建项目对话框下的C# | 中找到Library项目模板,如下截图所示:创建共享库项目

  2. 从新项目中移除MyClass.cs。在移除文件时,选择删除将删除文件的项目引用,并从文件系统中删除底层文件。

  3. 构建 | 常规下的项目选项对话框中,将目标框架选项设置为Mono/.NET 4.5

  4. NationalPark.csNationalParkData.cs文件从NationalParks.Droid移动到NationalPark.Data

  5. 打开NationalPark.csNationalParkData.cs,并将命名空间更改为NationalParks.Data

  6. NationalParkData中添加一个公共字符串DataDir属性,并在GetFilename()方法中使用它,如下所示:

    public string DataDir { get; set; }
    . . .
    protected string GetFilename()
    {
        return Path.Combine (DataDir, "NationalParks.json");
    }
    
  7. 将加载parks数据的逻辑从构造函数移动到名为Load()的新方法中,如下代码片段所示:

    public void Load()
    {
      if (File.Exists (GetFilename())) {
        string serializedParks =
            File.ReadAllText (GetFilename());
            _parks = JsonConvert.DeserializeObject
                <List<NationalPark>> (serializedParks);
      }
      else
      _parks = new List<NationalPark> ();
    }
    
  8. 编译NationalParks.Data。由于对 Json.NET 的未解析引用,您将收到编译错误。不幸的是,我们无法简单地添加之前从 Xamarin 组件存储中下载的 Json.NET 组件版本,因为这个版本是为与 Xamarin.iOS 和 Xamarin.Android 配置一起使用而构建的,并且与 Mono/.NET 4.5 库项目不兼容。

  9. 使用 NuGet 将 Json.NET 库添加到项目中。选择NationalParks.Data,右键单击它,然后导航到添加 | 添加包。在搜索字段中输入Json.NET,在列表中检查Json.NET条目,然后选择添加包。以下截图显示了添加包对话框:创建共享库项目

  10. 编译NationalParks.Data;这次您应该不会收到编译错误。

更新 NationalParks.Droid 以使用共享文件

现在我们已经将NationalParksData单例放在一个单独的项目中,我们现在可以重用它了。

为了更新NationalParks.Droid以使用共享解决方案,执行以下步骤:

  1. 解决方案面板中选择NationalPark.csNationalParksData.cs,右键单击它,选择移除,然后选择删除。这将从项目中删除所选文件,并从项目文件夹中物理删除它们。

  2. NationalParks.Droid中添加一个名为NationalParks.Data的文件夹。这个文件夹将不包含任何文件,但将仅用于项目结构中,以组织对共享文件的链接。

  3. 选择NationalParks.Data文件夹,右键单击它,然后导航到添加 | 添加文件以将现有文件添加到项目中。

  4. 添加文件 对话框中,导航到 NationalParks.Data 项目文件夹,选择 NationalPark.csNationalParkData.cs,然后点击 打开

  5. 添加文件到文件夹 对话框中,选择 添加文件链接,勾选 为所有选定的文件使用相同的操作 选项,然后点击 确定。展开 NationalParks.Data 文件夹,可以看到添加了两个文件链接。以下截图显示了 添加文件到文件夹 对话框:更新 NationalParks.Droid 以使用共享文件

  6. NationalParks.Data 命名空间中添加 using 子句,并在 MainActivityDetailActivityEditActivityNationalParksAdapter 中移除任何 Newtonsoft.Jsonusing 指令。

  7. MainActivity.OnCreate() 中,在创建 ListView 适配器之前设置 NationalParksData.DataDir 属性并调用 Load() 方法:

     NationalParksData.Instance.DataDir =
         System.Environment.GetFolderPath (
            System.Environment.SpecialFolder.MyDocuments);
    NationalParksData.Instance.Load ();
    
  8. 编译并运行应用程序。你应该看不到明显的行为变化,但我们现在以可共享的方式使用序列化和存储逻辑。

更新 NationalParks.iOS 以使用共享文件

现在,让我们继续更新 NationalParks.iOS。在这里我们还有更多工作要做,因为如果你还记得,我们之前将文件处理逻辑分散在几个区域。

为了更新 NationalParks.iOS 以使用共享解决方案,请执行以下步骤:

  1. 从项目中移除 NationalPark.cs

  2. NationalParks.Droid 项目中添加一个名为 NationalParks.Data 的文件夹。

  3. 将文件链接添加到 NationalPark.csNationalParkData.cs

  4. 打开 MasterViewController.cs,添加 NationalParks.Datausing 实例,并移除 Newtonsoft.Jsonusing 实例。

  5. MasterViewController.ViewDidLoad() 中,在创建 UITableView 的数据源之前设置 DataDir 属性:

    NationalParksData.Instance.DataDir =
        Environment.CurrentDirectory;
    NationalParksData.Instance.Load ();
    
  6. DataSource 类中,移除 Parks 集合,并在构造函数中移除 Parks 集合的加载操作。

  7. 更新 DataSource 中的方法,以便引用 NationalParksData 中的 Parks 集合属性。

  8. DataSource 中移除 Parks 属性,并更新 MasterViewController.PrepareForSegue() 以使用 NationalParksData 中的 Parks 属性。

  9. 打开 DetailViewController 并添加 NationalParks.Datausing 实例。

  10. SetNavData() 中,移除 Parks 集合参数、相应的私有变量,然后更新 MasterViewController 中的导航逻辑。

  11. 打开 EditViewController 并添加 NationalParks.Datausing 指令。

  12. SetNavData() 中,移除 Parks 集合参数、相应的私有变量,然后更新 MasterViewControllerDetailViewController 中的导航逻辑,以便不传递任何 Parks 集合。

  13. 移除 SaveParks() 方法。

  14. DoneClicked() 中,将添加公园到集合并保存集合的逻辑替换为对 NationalParksData.Instance.Save() 的调用,如下所示:

    private void DoneClicked (object sender, EventArgs e)
    {
        ToPark ();
        NationalParksData.Instance.Save (_park);
        NavigationController.PopViewControllerAnimated (true);
    }
    
  15. DeleteClicked()中,将移除公园集合并保存集合的逻辑替换为对NationalParks.Instance.Delete()的调用,如下所示:

    partial void DeleteClicked (UIButton sender)
    {
        NationalParksData.Instance.Delete(_park);
       NavigationController.PopToRootViewController(true);
    }
    
  16. 编译并运行应用程序。与NationalParks.Droid一样,你应该看不到明显的行为变化。

可移植类库

可移植类库PCL)是符合微软标准的库,可以以二进制格式跨许多不同的平台共享,例如 Windows 7 桌面、Windows 8 桌面、Windows 8 手机、Xbox 360 和 Mono。PCL 的一个大优点是你可以为所有这些平台共享单个二进制文件,避免分发源代码。然而,也有一些重大的挑战。

我们面临的一个直接问题是我们的代码使用了跨所有平台不支持的 API;具体来说,是File.Exists()File.ReadAllText()File.WriteAllText()。这看起来有些令人惊讶,但大多数System.IO在所有.NET 配置文件中并不通用;因此,在共享代码中处理文件 I/O 逻辑可能会有点困难。在我们的案例中,只有三个方法,我们可以通过创建一个 IO 接口轻松地将这种逻辑从共享代码中抽象出来。使用我们的共享解决方案的每个平台都将负责提供 IO 接口的实现。

创建NationalParks.PortableData

第一步是创建一个可移植类库来存放我们的共享解决方案。要创建NationalParks.PortableData,请执行以下步骤:

  1. 将一个新的可移植类库项目添加到NationalParks解决方案中。项目模板可以在C# | 可移植库下找到。

  2. 从新创建的项目中删除MyClass.cs

  3. NationalPark.csNationalParksData.csNationalParks.Data项目复制到NationalParks.PortableData

  4. 添加对 Json.NET 可移植类库的引用。

  5. 创建IFileHandler接口并添加三个方法,这些方法抽象了我们需要的三个 IO 方法。最好使读取和写入方法异步返回Task<>,因为许多平台只支持异步 IO。这将简化在这些平台上实现接口的过程。以下代码演示了所需的操作:

    public interface IFileHandler
    {
        bool FileExists (string filename);
        Task<string> ReadAllText (string filename);
        Task WriteAllText (string filename, string content);
    }
    
  6. NationalParksData中添加一个公共的IFileHandler属性,并将所有逻辑更改为使用此属性而不是使用System.IO.File,如下所示:

    public IFileHandler FileHandler { get; set; }
    . . .
    public async Task Load()
    {
      if (FileHandler.FileExists (GetFilename())) {
        string serializedParks =
          await FileHandler.ReadAllText (GetFilename());
      Parks = JsonConvert.DeserializeObject
              <List<NationalPark>> (serializedParks);
      }
      . . .
    }
    . . .
    public Task Save(NationalPark park)
    {
      . . .
      return FileHandler.WriteAllText (
        GetFilename (), serializedParks);
    }
    public Task Delete(NationalPark park)
    {
      . . .
      return FileHandler.WriteAllText (
        GetFilename (), serializedParks);
    }
    

实现IFileHandler

我们现在需要创建一个IFileHandler的实现,该实现可以由我们的两个项目使用。我们将通过前几节中介绍的文件链接方法共享文件处理实现。

要实现IFileHandler,请执行以下步骤:

  1. NationalParks解决方案中,创建一个名为NationalParks.IO的新Library项目,并将目标框架选项设置为Mono/.NET 4.5。这将作为我们的文件处理实现共享项目。

  2. 删除默认创建的 MyClass.cs 文件,并将对 NationalParks.PortableData 的引用添加进来。这将使我们能够访问我们打算实现的 IFileHandler 接口。

  3. NationalParks.IO 中创建一个名为 FileHandler 的类。添加对 NationalParks.PortableData 命名空间的 using 指令,并指定该类实现 IFileHandler 接口。

  4. 使用 重构 下的 实现 接口菜单项为接口上的每个方法创建存根实现。

  5. 实现每个存根方法。以下代码演示了所需的操作:

    #region IFileHandler implementation
    public bool FileExists (string filename)
    {
        return File.Exists (filename);
    }
    public async Task<string> ReadAllText (string filename)
    {
        using (StreamReader reader =
            File.OpenText(filename)) {
            return await reader.ReadToEndAsync();
        }
    }
    public async Task WriteAllText (string filename,
        string content)
    {
        using (StreamWriter writer =
            File.CreateText (filename)) {
            await writer.WriteAsync (content);
        }
    }
    #endregion
    

更新 NationalParks.Droid 以使用 PCL

现在,是时候更新 NationalParks.Droid 以使用我们新的 PCL。

为了更新 NationalParks.Droid 以使用 NationalParks.PortableData,执行以下步骤:

  1. NationalParks.Droid 项目中,删除 NationalParks.Data 文件夹,创建一个名为 NationalParks.IO 的新文件夹,并将对 NationalParks.PortableData 的引用添加到其中。

  2. NationalParks.IO 文件夹中,将 链接 添加到 FileHandler 类。

  3. MainActivity.cs 中,为 NationalParks.IONationalParks.PortableData 添加一个 using 子句。

  4. MainActivity.OnCreate() 中,使用 FileHandler 的一个实例初始化 FileHandler 属性,在调用 Load() 的调用上放置一个 await 实例,并将 NationalParksAdapter 的赋值移动到调用 Load() 之前,如下面的代码片段所示:

    _adapter = new NationalParksAdapter (this);
    NationalParksData.Instance.FileHandler =
        new FileHandler ();
    NationalParksData.Instance.DataDir =
      System.Environment.GetFolderPath (
          System.Environment.SpecialFolder.MyDocuments);
    await NationalParksData.Instance.Load ();
    
  5. 由于我们现在正在异步加载数据,OnPause() 方法可能会在 OnCreate() 的异步返回之前被调用。因此,我们需要在 OnPause() 中的逻辑上添加一个空检查,该逻辑调用 NotifyDataSetChanged(),如下所示:

    protected override void OnResume ()
    {
       basse.OnResume ();
       if (_adapter != null)
           _adapter.NotifyDataSetChanged ();
    }
    
  6. NationalParksAdapter.csDetailActivity.csEditActivity.cs 中,为 NationalParks.PortableData 添加一个 using 子句,并删除对 NationalParks.Datausing 指令。

  7. 编译并运行应用程序。

更新 NationalParks.iOS 以使用 PCL

现在,是时候更新 NationalParks.IOS。大部分步骤与之前基本相同。

为了更新 NationalParks.iOS 以使用 NationalParks.PortableData,执行以下步骤:

  1. NationalParks.Droid 项目中,删除 NationalParks.Data 文件夹,创建一个名为 NationalParks.IO 的新文件夹,并将对 NationalParks.PortableData 的引用添加到其中。

  2. NationalParks.IO 文件夹中,将 链接 添加到 FileHandler 类。

  3. MasterViewController.cs 中,为 NationalParks.IONationalParks.PortableData 添加一个 using 子句,并删除对 NationalParks.Datausing 指令。

  4. MasterViewController.ViewDidLoad() 中,使用 FileHandler 的一个实例初始化 FileHandler 属性,在调用 Load() 的调用上放置一个 await 实例,并在数据源赋值后调用 TableView.ReloadData(),如下面的代码片段所示:

    NationalParksData.Instance.FileHandler =
        new FileHandler ();
    NationalParksData.Instance.DataDir = 
        Environment.CurrentDirectory;
    await NationalParksData.Instance.Load ();
    TableView.Source = dataSource = new DataSource (this);
    TableView.ReloadData ();
    
  5. DetailViewController.csEditViewController.cs中,将NationalParks.Datausing指令替换为NationalParks.PortableData

  6. 编译并运行应用程序。

代码共享技术的优缺点

现在我们已经对在 Xamarin.iOS 和 Xamarin.Android 应用之间共享代码的两种实用方法有了些经验,让我们来看看它们的优缺点。以下表格总结了每种方法的优缺点:

优点 缺点
文件链接
  • 这允许更广泛地使用.NET API,假设这些 API 被所有将使用共享代码的平台支持。如果你只针对 Xamarin.iOS 和 Xamarin.Android,这效果相当不错。

|

  • 这需要共享源代码。

  • 这些 API 依赖性问题可能直到为每个目标平台编译共享代码时才会被发现。

|

可移植类库
  • 这确保了平台 API 的兼容性。

  • 这允许分发二进制代码。

|

  • 这限制了可用于代码中的命名空间和 API。

|

概述

在本章中,我们回顾了在 Xamarin 项目以及其他.NET 解决方案之间共享代码的两种实用方法。在下一章中,我们将探讨 MvvmCross,这是一个简化实现 Model-View-ViewModel 设计模式的框架,增加了跨平台共享代码的数量。

第七章. 与 MvvmCross 共享

在上一章中,我们介绍了跨项目和平台重用代码的基本方法。在本章中,我们将进一步探讨如何使用设计模式和框架来增加可重用代码的数量。我们将涵盖以下主题:

  • MvvmCross 简介

  • MVVM 设计模式

  • 核心概念

  • 视图、ViewModel 和命令

  • 数据绑定

  • 导航(ViewModel 到 ViewModel)

  • 项目组织

  • 启动过程

  • 创建 NationalParks.MvvmCross

在一个章节中尝试涵盖 MvvmCross 以及一个工作示例,这无疑有些雄心勃勃。我们的方法将是首先从高层次介绍核心概念,然后深入其中,使用 MvvmCross 创建国家公园示例应用。这将帮助你基本了解如何使用该框架及其使用价值。考虑到这一点,让我们开始吧。

介绍 MvvmCross

MvvmCross 是由 Stuart Lodge 创建的开源框架。它基于 模型-视图-ViewModelMVVM)设计模式,旨在增强跨多个平台(包括 Xamarin.Android、Xamarin.iOS、Windows Phone、Windows Store、WPF 和 Mac OS X)的代码重用。MvvmCross 项目托管在 GitHub 上,可以通过 github.com/MvvmCross/MvvmCross 访问。

MVVM 模式

MVVM 是模型-视图-控制器模式的变体。它将传统上放置在 视图 对象中的逻辑分离成两个不同的对象,一个称为 视图,另一个称为 ViewModel。视图负责提供用户界面,而 ViewModel 负责表示逻辑。表示逻辑包括将数据从模型转换为用户界面可以处理的形式,以及将用户与视图的交互映射回发送回模型的请求。以下图表描述了 MVVM 中各种对象之间的通信方式:

MVVM 模式

虽然 MVVM 提供了一个更复杂的实现模型,但它具有显著的优点,如下所述:

  • ViewModels 及其与 Models 的交互通常可以使用框架(如 NUnit)进行测试,这些框架比结合用户界面和表示层的应用程序要容易得多。

  • ViewModels 通常可以在不同的用户界面技术和平台上重用。

这些因素使得 MVVM 方法既灵活又强大。

视图

在 MvvmCross 应用程序中,视图使用平台特定的结构实现。对于 iOS 应用程序,视图通常实现为 ViewControllers 和 XIB 文件。MvvmCross 提供了一组基类,例如MvxViewController,iOS ViewControllers 从中继承。也可以与自定义演示者结合使用 Storyboard 来创建视图;我们将在本章后面的标题为实现 iOS 用户界面的部分简要讨论此选项。

对于 Android 应用程序,视图通常实现为MvxActivityMvxFragment,以及它们相关的布局文件。

ViewModel

ViewModel 是提供数据表示逻辑的类,用于应用程序中的视图。数据作为 ViewModel 上的属性暴露给 View,可以从 View 中调用的逻辑作为命令暴露。ViewModel 继承自MvxViewModel基类。

命令

命令在 ViewModel 中使用,以暴露可以从 View 中调用的逻辑,以响应用户交互。命令架构基于在许多 Microsoft 框架中使用的ICommand接口,例如Windows 演示基础WPF)和 Silverlight。MvvmCross 提供了IMvxCommand,它是ICommand的扩展,以及一个名为MvxCommand的实现。

命令通常定义为 ViewModel 上的属性。例如:

public  IMvxCommand ParkSelected { get; protected set; }

每个命令都有一个定义了动作方法的实现,该方法实现了要调用的逻辑:

protected void ParkSelectedExec(NationalPark park)
{
   . . .// logic goes here
}

必须初始化命令,并将相应的动作方法分配:

ParkSelected =
    new MvxCommand<NationalPark> (ParkSelectedExec);

数据绑定

数据绑定通过建立允许数据交换的双向链接,促进了 View 和 ViewModel 之间的通信。MvvmCross 提供的数据绑定功能基于在许多 Microsoft 基于 XAML 的 UI 框架中找到的功能,如 WPF 和 Silverlight。基本思想是您希望将 UI 控件中的属性绑定,例如 Android 应用程序中EditText控制的Text属性绑定到数据对象的属性,如NationalParkDescription属性。以下图表描述了此场景:

数据绑定

绑定模式

可以使用四种不同的绑定模式进行数据绑定:

  • 单向绑定:此模式指示数据绑定框架将值从 ViewModel 传输到 View,并将 ViewModel 上属性的任何更新传输到其绑定的 View 属性。

  • 单向源绑定:此模式指示数据绑定框架将值从 View 传输到 ViewModel,并将 View 属性的更新传输到其绑定的 ViewModel 属性。

  • 双向绑定:此模式指示数据绑定框架在 ViewModel 和 View 之间双向传输值,并且任一对象的更新都将导致另一个对象更新。当值正在编辑时,此绑定模式非常有用。

  • 一次性绑定:此模式告诉数据绑定框架在绑定建立时从 ViewModel 转移值到 View;在此模式下,ViewModel 属性的更新不会被 View 监控。

INotifyPropertyChanged 接口

INotifyPropertyChanged 接口是使数据绑定有效运行的重要组成部分;它充当源对象和目标对象之间的契约。正如其名称所暗示的,它定义了一个契约,允许源对象在数据发生变化时通知目标对象,从而允许目标对象采取任何必要的行动,例如刷新其显示。

该接口由一个单一的事件——PropertyChanged 事件组成,目标对象可以订阅该事件,并且当源对象中的属性发生变化时,该事件会被触发。以下示例演示了如何实现 INotifyPropertyChanged

public class NationalPark : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler
     PropertyChanged;
  // rather than use "… code" it is safer to use
  // the comment form 
  string _name;
  public string Name
  {
    get { return _name; }
    set
    {
        if (value.Equals (_name,
            StringComparison.Ordinal))
        {
      // Nothing to do - the value hasn't changed;
      return;
        }
        _name = value;
        OnPropertyChanged();
    }
  }
  . . . 
  void OnPropertyChanged(
    [CallerMemberName] string propertyName = null)
  {
      var handler = PropertyChanged;
  if (handler != null)
  {
      handler(this,
            new PropertyChangedEventArgs(propertyName));
  }
  }
}

绑定规范

绑定可以通过几种方式指定。对于 Android 应用,绑定可以在布局文件中指定。以下示例演示了如何将 TextView 实例的 Text 属性绑定到 NationalPark 实例的 Description 属性:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/descrTextView"
    local:MvxBind="Text Park.Description" />

对于 iOS,绑定必须使用绑定 API 完成。CreateBinding() 是可以在 MvxViewController 上找到的方法。以下示例演示了如何将 Description 属性绑定到一个 UILabel 实例:

this.CreateBinding (this.descriptionLabel).
    To ((DetailViewModel vm) => vm.Park.Description).
    Apply ();

导航到 ViewModel 之间

在应用内导航到各种屏幕是一个重要的功能。在 MvvmCross 应用中,这一功能是在 ViewModel 层实现的,以便导航逻辑可以被重用。MvvmCross 通过使用从 MvxNavigatingObject 继承的 ShowViewModel<T>() 方法来支持 ViewModel 之间的导航,MvxNavigatingObjectMvxViewModel 的基类。以下示例演示了如何导航到 DetailViewModel

ShowViewModel<DetailViewModel>();

传递参数

在许多情况下,需要将信息传递到目标 ViewModel。MvvmCross 提供了多种实现这一目标的方法。主要方法是通过创建一个包含简单公共属性的类,并将该类的实例传递给 ShowViewModel<T>()。以下示例演示了如何在导航过程中定义和使用 parameters 类:

public class DetailParams
{
    public int ParkId { get; set; }
}

// using the parameters class
ShowViewModel<DetailViewModel>(
new DetailViewParam() { ParkId = 0 });

为了接收和使用参数,目标 ViewModel 实现了一个 Init() 方法,该方法接受 parameters 类的实例:

public class DetailViewModel : MvxViewModel
{
    . . .
    public void Init(DetailViewParams parameters)
    {
        // use the parameters here . . .
    }
}

解决方案/项目组织

MvvmCross 解决方案的组织方式与我们如何在 第六章 中组织 PCL 解决方案的方式相似,即 共享游戏。每个 MvvmCross 解决方案将有一个单独的核心 PCL 项目,其中包含可重用的代码,以及一系列包含各种应用的特定平台项目。以下图展示了总体结构:

解决方案/项目组织

启动过程

MvvmCross 应用通常遵循一个标准的启动序列,该序列由每个应用中特定平台的代码启动。有几个类协同完成启动;其中一些类位于核心项目中,而另一些则位于特定平台的项目中。以下各节描述了每个参与类的职责。

App.cs

核心项目包含一个继承自 MvxApplicationApp 类。App 类包含对 Initialize() 方法的重写,以便至少可以注册在应用启动时应展示的第一个 ViewModel:

RegisterAppStart<ViewModels.MasterViewModel>();

Setup.cs

Android 和 iOS 项目都有一个 Setup 类,负责在启动期间从核心项目中创建 App 对象。这是通过重写 CreateApp() 方法来实现的:

protected override IMvxApplication CreateApp()
{
    return new Core.App();
}

对于 Android 应用,Setup 继承自 MvxAndroidSetup。对于 iOS 应用,Setup 继承自 MvxTouchSetup

Android 启动

Android 应用使用一个特殊的 Activity 启动屏幕启动,该屏幕调用 Setup 类并启动 MvvmCross 启动过程。所有这些都会自动为你完成;你所需要做的只是包含启动屏幕定义,并确保它被标记为启动活动。定义如下:

[Activity(
  Label="NationalParks.Droid", MainLauncher = true,
  Icon="@drawable/icon", Theme="@style/Theme.Splash",
  NoHistory=true,
  ScreenOrientation = ScreenOrientation.Portrait)]
public class SplashScreen : MvxSplashScreenActivity
{
    public SplashScreen():base(Resource.Layout.SplashScreen)
    {
    }
}

iOS 启动

iOS 应用启动稍微自动化程度较低,并从 AppDelegateFinishedLaunching() 方法中启动:

public override bool FinishedLaunching (
    UIApplication app, NSDictionary options)
{
    _window = new UIWindow (UIScreen.MainScreen.Bounds);

    var setup = new Setup(this, _window);
    setup.Initialize();
    var startup = Mvx.Resolve<IMvxAppStart>();
    startup.Start();

    _window.MakeKeyAndVisible ();

    return true;
}

创建 NationalParks.MvvmCross

现在我们已经对 MvvmCross 框架有了基本了解,让我们将所学知识付诸实践,将 NationalParks 应用转换为利用我们刚刚学到的功能。

创建 MvvmCross 核心项目

我们将首先创建核心项目。该项目将包含在 iOS 和 Android 应用之间共享的所有代码,主要是以 ViewModel 的形式。核心项目将作为可移植类库构建。

要创建 NationalParks.Core,请执行以下步骤:

  1. 从主菜单导航到 文件 | 新建解决方案

  2. 新建解决方案 对话框中,导航到 C# | 可移植库,在项目 名称 字段中输入 NationalParks.Core,在 解决方案 字段中输入 NationalParks.MvvmCross,然后点击 确定

  3. 从 NuGet 将 MvvmCross 启动包添加到项目中。选择 NationalParks.Core 项目,从主菜单导航到 项目 | 添加包。在搜索字段中输入 MvvmCross starter

  4. 选择 MvvmCross – Hot Tuna Starter Pack 条目,然后点击 添加包创建 MvvmCross 核心项目

  5. 由于添加了包,NationalParks.Core 中添加了一些内容,具体如下:

    • 包含与 MvvmCross 启动包相关联的库(dlls)列表的 packages.config 文件。这些条目是到整体解决方案 Packages 文件夹中实际库的链接。

    • 一个包含名为FirstViewModel的示例 ViewModel 的ViewModels文件夹。

    • App.cs中的App类,其中包含一个Initialize()方法,通过调用RegisterAppStart()来启动FirstViewModel。我们最终将将其更改为启动与列出国家公园的视图关联的MasterViewModel类。

创建 MvvmCross Android 应用

下一步是在同一解决方案中创建 Android 应用项目。

要创建NationalParks.Droid,请完成以下步骤:

  1. 选择NationalParks.MvvmCross解决方案,右键单击它,然后导航到添加 | 新建项目

  2. 新项目对话框中,导航到C# | Android | Android Application,在名称字段中输入NationalParks.Droid,然后点击确定

  3. 通过选择NationalParks.Droid并从主菜单导航到项目 | 添加包,将 MvvmCross 启动套件包添加到新项目中。

  4. 由于添加了包,NationalParks.Droid中添加了一些内容,如下所示:

    • packages.config:此文件包含与 MvvmCross 启动套件包关联的库列表(dlls)。这些条目是到整体解决方案Packages文件夹中实际库的链接,其中包含实际下载的库。

    • FirstView:此类位于Views文件夹中,对应于在NationalParks.Core中创建的FirstViewModel

    • FirstView:此布局位于Resources\layout中,由FirstView活动使用。这是一个传统的 Android 布局文件,除了它包含在EditViewTextView元素中的绑定声明。

    • Setup:此文件继承自MvxAndroidSetup。此类负责从核心项目创建App类的实例,然后通过调用RegisterAppStart()显示第一个 ViewModel。

    • SplashScreen:此类继承自MvxSplashScreenActivitySplashScreen类被标记为主要启动活动,因此通过调用Setup.Initialize()初始化MvvmCross应用。

  5. 通过选择References文件夹,右键单击它,选择编辑引用,选择项目选项卡,勾选NationalParks.Core,然后点击确定,为NationalParks.Core添加引用。

  6. 删除MainActivity.cs,因为它不再需要,并且将创建一个构建错误。这是因为它被标记为主要启动类,新的SplashScreen类也是如此。同时,删除相应的Resources\layout\main.axml布局文件。

  7. 运行应用。应用将展示 FirstViewModel,它与相应的 FirstView 实例通过 EditView 类链接,而 TextView 展示相同的 Hello MvvmCross 文本。当你编辑 EditView 类中的文本时,TextView 类将通过数据绑定自动更新。以下截图展示了你应该看到的内容:创建 MvvmCross Android 应用

重用 NationalParks.PortableData 和 NationalParks.IO

在我们开始创建应用中的视图和视图模型之前,我们首先需要引入之前工作中的一些代码,这些代码可以用来维护公园。为此,我们将简单地重用之前创建的 NationalParksData 单例和 FileHandler 类。

要重用 NationalParksData 单例和 FileHandler 类,请完成以下步骤:

  1. NationalParks.PortableDataNationalParks.IO 从 第六章 中创建的解决方案复制到 NationalParks.MvvmCross 解决方案文件夹中。

  2. NationalParks.Droid 项目中添加对 NationalParks.PortableData 的引用。

  3. NationalParks.Droid 项目中创建一个名为 NationalParks.IO 的文件夹,并将 FileHandler.cs 文件从 NationalParks.IO 项目中添加链接。回想一下,FileHandler 类不能包含在可移植类库中,因为它使用了无法从可移植类库引用的文件 IO API。

  4. 编译项目。现在项目应该可以干净地编译。

实现 INotifyPropertyChanged 接口

我们将使用数据绑定将 UI 控件绑定到 NationalPark 对象,因此我们需要实现 INotifyPropertyChanged 接口。这确保了对公园属性所做的更改被报告给适当的 UI 控件。

要实现 INotifyPropertyChanged,请完成以下步骤:

  1. NationalParks.PortableData 项目中打开 NationalPark.cs 文件。

  2. 指定 NationalPark 类实现 INotifyPropertyChanged 接口。

  3. 选择 INotifyPropertyChanged 接口,右键单击它,导航到 重构 | 实现接口,然后按 Enter。输入以下代码片段:

    public class NationalPark : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler
            PropertyChanged;
        . . .
    }
    
  4. 在每个属性设置器方法中添加一个可调用的 OnPropertyChanged() 方法:

    void OnPropertyChanged(
        [CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this,
                new PropertyChangedEventArgs(propertyName));
        }
    }
    
  5. 更新每个属性定义,以调用设置器的方式与 Name 属性所示相同:

    string _name;
    public string Name
    {
      get { return _name; }
      set
      {
        if (value.Equals (_name, StringComparison.Ordinal))
        {
          // Nothing to do - the value hasn't changed;
      return;
        }
        _name = value;
        OnPropertyChanged();
      }
    }
    
  6. 编译项目。现在项目应该可以干净地编译。我们现在可以使用 NationalParksData 单例在我们的新项目中,并且它支持数据绑定。

实现安卓用户界面

现在,我们已准备好创建应用所需的视图和视图模型。我们正在创建的应用将遵循之前章节中使用的相同流程:

  • 一个用于查看国家公园的主列表视图

  • 一个用于查看特定公园详细信息的详细视图

  • 一个用于编辑新或现有公园的编辑视图

在 Android 应用程序中创建视图和 ViewModel 的过程通常包括三个不同的步骤:

  1. 在核心项目中创建一个 ViewModel,其中包含支持视图所需的数据和事件处理程序(命令)。

  2. 创建一个包含视觉元素和数据绑定规范的 Android 布局。

  3. 创建一个 Android 活动,它对应于 ViewModel 并显示布局。

在我们的案例中,这个过程将略有不同,因为我们将重用我们的一些先前工作,特别是布局文件和菜单定义。

要重用布局文件和菜单定义,请执行以下步骤:

  1. Master.axmlDetail.axmlEdit.axml从第五章中创建的解决方案的Resources\layout文件夹复制到NationalParks.Droid项目的Resources\layout文件夹中,并通过选择布局文件夹并导航到添加 | 添加文件将其添加到项目中。

  2. MasterMenu.xmlDetailMenu.xmlEditMenu.xml从第五章中创建的解决方案的Resources\menu文件夹复制到NationalParks.Droid项目的Resources\menu文件夹中,并通过选择menu文件夹并导航到添加 | 添加文件将其添加到项目中。

实现主列表视图

我们现在可以开始实现我们的第一个视图/ViewModel 组合,即主列表视图。

创建 MasterViewModel

第一步是创建一个 ViewModel 并添加一个将提供数据给显示国家公园的列表视图的属性,以及一些初始化代码。

要创建MasterViewModel,请完成以下步骤:

  1. NationalParks.Core中的ViewModels文件夹上右键单击,并导航到添加 | 新建文件

  2. 新建文件对话框中,导航到常规 | 空类,在名称字段中输入MasterViewModel,然后点击新建

  3. 修改类定义,使MasterViewModel继承自MvxViewModel;您还需要添加几个using指令:

    . . .
    using Cirrious.CrossCore.Platform;
    using Cirrious.MvvmCross.ViewModels;
    . . .
    namespace NationalParks.Core.ViewModels
    {
      public class MasterViewModel : MvxViewModel
      {
             . . .
       }
    }
    
  4. MasterViewModel中添加一个属性,该属性是一个NationalPark元素的列表。这个属性将稍后绑定到列表视图:

    private List<NationalPark> _parks;
    public List<NationalPark> Parks
    {
        get { return _parks; }
        set { _parks = value;
              RaisePropertyChanged(() => Parks);
        }
     }
    
  5. MasterViewModel上重写Start()方法,以从NationalParksData单例加载数据到_parks集合。您需要再次添加NationalParks.PortableData命名空间的using指令:

    . . .
    using NationalParks.PortableData;
    . . .
    public async override void Start ()
    {
        base.Start ();
        await NationalParksData.Instance.Load ();
        Parks = new List<NationalPark> (
            NationalParksData.Instance.Parks);
    }
    
  6. 我们现在需要修改应用程序启动顺序,以便MasterViewModel是第一个启动的 ViewModel。在NationalParks.Core中打开App.cs,将RegisterAppStart()的调用更改为引用MasterViewModel

    RegisterAppStart<ViewModels.MasterViewModel>();
    

更新 Master.axml 布局

更新Master.axml,以便利用 MvvmCross 提供的数据绑定功能。

要更新 Master.axml,请完成以下步骤:

  1. 打开 Master.axml 并在 XML 的顶部添加一个命名空间定义,以包含 NationalParks.Droid 命名空间:

    此命名空间定义是必需的,以便允许 Android 解析将要指定的 MvvmCross 特定元素。

  2. ListView 元素更改为 Mvx.MvxListView 元素:

    <Mvx.MvxListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/parksListView" />
    
  3. MvxListView 元素添加数据绑定规范,将列表视图的 ItemsSource 属性绑定到 MasterViewModelParks 属性,如下所示:

        . . .
        android:id="@+id/parksListView"
        local:MvxBind="ItemsSource Parks" />
    
  4. 向元素定义添加一个列表项模板属性。此布局控制列表视图中将显示的每个项的内容:

    local:MvxItemTemplate="@layout/nationalparkitem"
    
  5. 创建 NationalParkItem 布局,并提供 TextView 元素以显示公园的名称和描述,如下所示:

    <LinearLayout 
    
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="40sp"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="20sp"/>
    </LinearLayout>
    
  6. 将数据绑定规范添加到每个 TextView 元素中:

    . . .
            local:MvxBind="Text Name" />
    . . .
            local:MvxBind="Text Description" />
    . . .
    

    备注

    注意,在这种情况下,数据绑定的上下文是绑定到 MvxListView 的集合中的一项的实例,在这个例子中,是一个 NationalPark 的实例。

创建 MasterView 活动

接下来,创建 MasterView,它是一个与 MasterViewModel 对应的 MvxActivity 实例。

要创建 MasterView,请完成以下步骤:

  1. NationalParks.Core 中的 ViewModels 文件夹中,右键单击它,导航到 添加 | 新建文件

  2. 新建文件 对话框中,导航到 Android | Activity,在 名称 字段中输入 MasterView,并选择 新建

  3. 修改类规范,使其继承自 MvxActivity;您还需要添加一些 using 指令,如下所示:

    using Cirrious.MvvmCross.Droid.Views;
    using NationalParks.Core.ViewModels;
    . . .
    namespace NationalParks.Droid.Views
    {
        [Activity(Label = "Parks")]
        public class MasterView : MvxActivity
        {
            . . .
        }
    }
    
  4. 打开 Setup.cs 并向 CreateApp() 方法添加代码以初始化 NationalParksData 单例的文件处理程序和路径,如下所示:

    protected override IMvxApplication CreateApp()
    {
     NationalParksData.Instance.FileHandler =
     new FileHandler ();
     NationalParksData.Instance.DataDir =
     System.Environment.GetFolderPath(
     System.Environment.SpecialFolder.MyDocuments);
     return new Core.App();
    }
    
    
  5. 编译并运行应用;您需要使用 Android 设备监控器将 NationalParks.json 文件复制到设备或模拟器。NationalParks.json 中的所有公园都应显示。

实现详细视图

现在我们已经有了显示国家公园的主列表视图,我们可以专注于创建详细视图。我们将对详细视图执行与刚刚完成的主视图相同的步骤。

创建 DetailViewModel

我们通过以下步骤开始创建 DetailViewModel

  1. 按照创建 MasterViewModel 所使用的相同程序,在 NationalParks.CoreViewModel 文件夹中创建一个新的名为 DetailViewModel 的 ViewModel。

  2. 添加一个 NationalPark 属性以支持视图控件的数据绑定,如下所示:

    protected NationalPark _park;
    public NationalPark Park
    { 
        get { return _park; }
        set { _park = value;
              RaisePropertyChanged(() => Park);
         }
    }
    
  3. 创建一个 Parameters 类,可以用来传递应显示的公园 ID。在参数的类定义中创建此类很方便:

    public class DetailViewModel : MvxViewModel
    {
        public class Parameters
        {
            public string ParkId { get; set; }
        }
        . . .
    
  4. 实现一个 Init() 方法,该方法将接受 Parameters 类的实例并从 NationalParkData 获取相应的国家公园:

    public void Init(Parameters parameters)
    {
        Park = NationalParksData.Instance.Parks.
            FirstOrDefault(x => x.Id == parameters.ParkId);
    }
    

更新 Detail.axml 布局

接下来,我们将更新布局文件。需要做的主要更改是向布局文件中添加数据绑定规范。

要更新Detail.axml布局,执行以下步骤:

  1. 打开Detail.axml并将项目命名空间添加到 XML 文件中:

  2. 将数据绑定规范添加到每个对应于国家公园属性的TextView元素中,如下所示公园名称的示例:

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/nameTextView"
        local:MvxBind="Text Park.Name" />
    

创建DetailView活动

现在,创建一个将与DetailViewModel一起工作的MvxActivity实例。

要创建DetailView,执行以下步骤:

  1. 按照之前创建MasterView时使用的相同程序,在NationalParks.DroidViews文件夹中创建一个名为DetailView的新视图。

  2. 实现OnCreateOptionsMenu()OnOptionsItemSelected()方法,以便我们的菜单可访问。现在暂时注释掉OnOptionsItemSelected()中与Edit操作相关的部分;一旦编辑视图完成,我们将填充这部分。

添加导航

最后一步是添加导航,以便当在MasterViewMvxListView中点击项目时,公园将在详情视图中显示。我们将使用command属性和数据绑定来完成此操作:

要添加导航,执行以下步骤:

  1. 打开MasterViewModel并添加一个IMvxCommand属性;这将用于处理被选中的公园:

    protected IMvxCommand ParkSelected { get; protected set; }
    
  2. 创建一个Action委托,当执行ParkSelected命令时将被调用,如下所示:

    protected void ParkSelectedExec(NationalPark park)
    {
        ShowViewModel<DetailViewModel> (
            new DetailViewModel.Parameters ()
                { ParkId = park.Id });
    }
    
  3. MasterViewModel的构造函数中初始化command属性:

    ParkClicked =
        new MvxCommand<NationalPark> (ParkSelectedExec);
    
  4. 现在,最后一步,在Master.axml中的MvvListView上添加数据绑定规范,将ItemClick事件绑定到我们刚刚创建的MasterViewModel上的ParkClicked命令:

    local:MvxBind="ItemsSource Parks; ItemClick ParkClicked"
    
  5. 编译并运行应用。现在点击列表视图中的公园应该会导航到详情视图,显示选中的公园。

实现编辑视图

我们现在几乎已经成为了实现新视图和视图模型的专家。接下来要做的最后一个视图是编辑视图。

创建EditViewModel

如我们之前所做的那样,我们从 ViewModel 开始。

要创建EditViewModel,完成以下步骤:

  1. 按照本章之前用于创建EditViewModel的相同过程,添加数据绑定属性并创建一个Parameters类用于导航。

  2. 实现一个Init()方法,该方法将接受Parameters类的实例,并在编辑现有公园的情况下从NationalParkData获取相应的国家公园,或者如果用户选择了New操作,则创建一个新的实例。检查传入的参数以确定意图:

    public void Init(Parameters parameters)
    {
        if (string.IsNullOrEmpty (parameters.ParkId))
            Park = new NationalPark ();
        else
            Park =
                NationalParksData.Instance.
                Parks.FirstOrDefault(
                x => x.Id == parameters.ParkId);
    }
    

更新Edit.axml布局

更新Edit.axml以提供数据绑定规范。

要更新 Edit.axml 布局,你首先需要打开 Edit.axml 并将项目命名空间添加到 XML 文件中。然后,将数据绑定规范添加到每个对应于国家公园属性的 EditView 元素。

创建 EditView 活动

创建一个名为 EditView 的新 MvxActivity 实例,它将与 EditViewModel 一起工作。

要创建 EditView,请执行以下步骤:

  1. 按照创建 DetailView 所使用的相同程序,在 NationalParks.DroidViews 文件夹中创建一个名为 EditView 的新视图。

  2. 实现 OnCreateOptionsMenu()OnOptionsItemSelected() 方法,以便从 ActionBar 中访问 Done 操作。你可以从 第六章 中创建的解决方案中复制这些方法的实现,共享游戏。将 Done 的实现更改为调用 EditViewModel 上的 Done 命令。

添加导航

MasterView 中点击 New (+) 和在 DetailView 中点击 Edit 时添加导航。让我们从 MasterView 开始。

要从 MasterViewModel 添加导航,完成以下步骤:

  1. 打开 MasterViewModel.cs 并添加一个 NewParkClicked 命令属性以及该命令的处理程序。确保在构造函数中初始化命令,如下所示:

    protected IMvxCommand NewParkClicked { get; set; }
    protected void NewParkClickedExec()
    {
      ShowViewModel<EditViewModel> ();
    }
    

    注意,我们不会将参数类传递给 ShowViewModel()。这将导致创建一个默认实例并将其传递,这意味着 ParkId 将为 null。我们将使用这种方式来确定是否应该创建一个新的公园。

  2. 现在,是时候将 NewParkClicked 命令连接到 actionNew 菜单项了。我们没有使用数据绑定来完成此操作的方法,因此我们将求助于更传统的做法——我们将使用 OnOptionsItemSelected() 方法。添加逻辑以调用 NewParkClickedExecute() 方法,如下所示:

    case Resource.Id.actionNew:
        ((MasterViewModel)ViewModel).
            NewParkClicked.Execute ();
        return true; 
    

要从 DetailViewModel 添加导航,完成以下步骤:

  1. 打开 DetailViewModel.cs 并添加一个 EditParkClicked 命令属性以及该命令的处理程序。确保在构造函数中初始化命令,如下面的代码片段所示:

    protected IMvxCommand EditPark { get; protected set;}
    protected void EditParkHandler()
    {
        ShowViewModel<EditViewModel> (
            new EditViewModel.Parameters ()
                { ParkId = _park.Id });
    }
    

    注意

    注意,创建了一个 Parameters 类的实例,初始化并传递给 ShowViewModel() 方法。这个实例将反过来传递给 EditViewModel 上的 Init() 方法。

  2. MasterViewModel 的构造函数中初始化 command 属性,如下所示:

    EditPark =
        new MvxCommand<NationalPark> (EditParkHandler);
    
  3. 现在,更新 DetailView 中的 OnOptionsItemSelect() 方法,以便在选择 Edit 操作时调用 DetailView.EditPark 命令:

    case Resource.Id.actionEdit:
        ((DetailViewModel)ViewModel).EditPark.Execute ();
        return true;
    
  4. 编译并运行 NationalParks.Droid。你现在应该有一个功能齐全的应用程序,它能够创建新的公园并编辑现有的公园。对 EditView 所做的更改应自动反映在 MasterViewDetailView 中。

创建 MvvmCross iOS 应用

使用 MvvmCross 创建 Android 应用的过程提供了对整体架构如何工作的深入了解。由于以下两个原因,创建 iOS 解决方案应该会容易得多:首先,我们了解了如何与 MvvmCross 交互;其次,我们放置在NationalParks.Core中的所有逻辑都是可重用的,因此我们只需要创建应用视图部分和启动代码。

要创建NationalParks.iOS,请完成以下步骤:

  1. 选择NationalParks.MvvmCross解决方案,右键单击它,然后导航到添加 | 新建项目

  2. 新建项目对话框中,导航到C# | iOS | iPhone | 单视图应用,在名称字段中输入NationalParks.iOS,然后点击确定

  3. 通过选择NationalParks.iOS并从主菜单导航到项目 | 添加包,将 MvvmCross 启动套件包添加到新项目中。

  4. 由于添加了包,NationalParks.iOS添加了一些内容。具体如下:

    • packages.config:这个文件包含与 MvvmCross 启动套件包关联的库列表。这些条目是到整体解决方案Packages文件夹中实际库的链接,其中包含实际下载的库。

    • FirstView:这个类放在Views文件夹中,对应于在NationalParks.Core中创建的FirstViewModel实例。

    • Setup:这个类继承自MvxTouchSetup。这个类负责从核心项目创建App类的实例,然后通过调用RegisterAppStart()显示第一个 ViewModel。

    • AppDelegate.cs.txt:这个类包含示例启动代码,应该放置在实际的AppDelegate.cs文件中。

实现 iOS 用户界面

我们现在准备为 iOS 应用创建用户界面。好消息是我们已经实现了所有的 ViewModel,因此我们可以简单地重用它们。坏消息是我们不能轻易地重用之前工作的 storyboards;MvvmCross 应用通常使用 XIB 文件。其中一个原因是 storyboards 旨在提供导航能力,而 MvvmCross 应用将这一责任委托给了 ViewModel 和 presenter。虽然可以使用 storyboards 与自定义 presenter 结合使用,但本章的剩余部分将专注于使用 XIB 文件,因为这更为常见。屏幕布局,如第四章中所述的使用 Xamarin.iOS 开发您的第一个 iOS 应用,可以按照以下截图所示使用:

实现 iOS 用户界面

我们现在准备开始。

实现主视图

我们将首先工作的视图是主视图。

要实现主视图,请完成以下步骤:

  1. 通过在NationalParks.iOSViews文件夹上右键单击并导航到添加 | 新建文件 | iOS | iPhone 视图控制器,创建一个名为MasterView的新ViewController类。

  2. 打开MasterView.xib,按照屏幕布局排列控件。为每个编辑控件添加出口。

  3. 打开MasterView.cs,添加以下模板逻辑以处理 iOS 7 上的约束,如下所示:

    // ios7 layout
    if (RespondsToSelector(new
        Selector("edgesForExtendedLayout")))
        EdgesForExtendedLayout = UIRectEdge.None;
    
  4. ViewDidLoad()方法内,添加创建parksTableViewMvxStandardTableViewSource的逻辑:

    MvxStandardTableViewSource _source;
    . . .
    _source = new MvxStandardTableViewSource(
        parksTableView,
        UITableViewCellStyle.Subtitle,
        new NSString("cell"),
        "TitleText Name; DetailText Description",
         0);
    parksTableView.Source = _source;
    

    注意,示例使用的是Subtitle单元格样式,并将国家公园名称和描述绑定到标题和副标题。

  5. ViewDidShow()方法中添加绑定逻辑。在上一步中,我们为UITableViewCell的属性提供了绑定上下文中的属性规范。在本步骤中,我们需要为MasterModelView上的Parks属性设置绑定上下文:

    var set = this.CreateBindingSet<MasterView,
        MasterViewModel>();
    set.Bind (_source).To (vm => vm.Parks);
    set.Apply();
    
  6. 编译并运行应用程序。NationalParks.json中的所有公园都应显示出来。

实现详细视图

现在,按照以下步骤实现详细视图:

  1. 创建一个名为DetailView的新ViewController实例。

  2. 打开DetailView.xib,按照以下代码排列控件。为每个编辑控件添加出口。

  3. 打开DetailView.cs,并在ViewDidShow()方法中添加绑定逻辑:

    this.CreateBinding (this.nameLabel).
        To ((DetailViewModel vm) => vm.Park.Name).Apply ();
    this.CreateBinding (this.descriptionLabel).
        To ((DetailViewModel vm) => vm.Park.Description).
            Apply ();
    this.CreateBinding (this.stateLabel).
        To ((DetailViewModel vm) => vm.Park.State).Apply ();
    this.CreateBinding (this.countryLabel).
        To ((DetailViewModel vm) => vm.Park.Country).
            Apply ();
    this.CreateBinding (this.latLabel).
        To ((DetailViewModel vm) => vm.Park.Latitude).
            Apply ();
    this.CreateBinding (this.lonLabel).
        To ((DetailViewModel vm) => vm.Park.Longitude).
            Apply ();
    

添加导航

从主视图添加导航,以便当选择一个公园时,显示详细视图,显示该公园。

要添加导航,请完成以下步骤:

  1. 打开MasterView.cs,创建一个名为ParkSelected的事件处理程序,并将其分配给在ViewDidLoad()方法中创建的MvxStandardTableViewSource上的SelectedItemChanged事件:

    . . .
        _source.SelectedItemChanged += ParkSelected;
    . . .
    protected void ParkSelected(object sender, EventArgs e)
    {
        . . .
    }
    
  2. 在事件处理程序内,调用MasterViewModel上的ParkSelected命令,并传入所选公园:

    ((MasterViewModel)ViewModel).ParkSelected.Execute (
            (NationalPark)_source.SelectedItem);
    
  3. 编译并运行NationalParks.iOS。现在,从列表视图中选择一个公园应导航到详细视图,显示所选公园。

实现编辑视图

我们现在需要实现 iOS 应用程序的最后几个视图之一,即编辑视图。

要实现编辑视图,请完成以下步骤:

  1. 创建一个名为EditView的新ViewController实例。

  2. 打开EditView.xib,按照布局截图排列控件。为每个编辑控件添加出口。

  3. 打开EditView.cs,并在ViewDidShow()方法中添加数据绑定逻辑。您应使用与详细视图相同的数据绑定方法。

  4. 添加一个名为DoneClicked的事件处理程序,并在事件处理程序内调用EditViewModel上的Done命令:

    protected void DoneClicked (object sender, EventArgs e)
    {
        ((EditViewModel)ViewModel).Done.Execute();
    }
    
  5. ViewDidLoad()中,为EditViewNavigationItem添加UIBarButtonItem,并将其DoneClicked事件处理程序分配给它,如下所示:

    NavigationItem.SetRightBarButtonItem(
        new UIBarButtonItem(UIBarButtonSystemItem.Done,
            DoneClicked), true);  
    

添加导航

在两个地方添加导航:当从主视图点击新建(+时,以及在详细视图中的编辑按钮上点击。让我们从主视图开始。

要为主视图添加导航,执行以下步骤:

  1. 打开MasterView.cs文件,添加一个名为NewParkClicked的事件处理程序。在事件处理程序中,在MasterViewModel上调用NewParkClicked命令:

    protected void NewParkClicked(object sender,
            EventArgs e)
    {
        ((MasterViewModel)ViewModel).
                NewParkClicked.Execute ();
    }
    
  2. ViewDidLoad()中,为MasterViewNavigationItem添加UIBarButtonItem,并将NewParkClicked事件处理程序分配给它:

    NavigationItem.SetRightBarButtonItem(
        new UIBarButtonItem(UIBarButtonSystemItem.Add,
            NewParkClicked), true);
    

要为详情视图添加导航,执行以下步骤:

  1. 打开DetailView.cs文件,添加一个名为EditParkClicked的事件处理程序。在事件处理程序中,在DetailViewModel上调用EditParkClicked命令:

    protected void EditParkClicked (object sender,
        EventArgs e)
    {
        ((DetailViewModel)ViewModel).EditPark.Execute ();
    }
    
  2. ViewDidLoad()中,为MasterViewNavigationItem添加UIBarButtonItem,并将EditParkClicked事件处理程序分配给它:

    NavigationItem.SetRightBarButtonItem(
        new UIBarButtonItem(UIBarButtonSystemItem.Edit,
            EditParkClicked), true);
    

刷新主视图列表

需要最后注意的一个细节是在EditView上更改项目时刷新MasterView上的UITableView控件。

要刷新主视图列表,执行以下步骤:

  1. 打开MasterView.cs文件,并在MasterViewViewDidAppear()方法中调用parksTableViewReloadData()

    public override void ViewDidAppear (bool animated)
    {
        base.ViewDidAppear (animated);
        parksTableView.ReloadData();
    }
    
  2. 编译并运行NationalParks.iOS。你现在应该有一个功能齐全的应用程序,它能够创建新的公园并编辑现有的公园。对EditView所做的更改应自动反映在MasterViewDetailVIew中。

考虑到优缺点

完成我们的工作后,我们现在有了做一些基本观察的基础。让我们从优点开始:

  • MvvmCross 无疑增加了可以在各个平台之间复用的代码量。ViewModel 包含视图所需的数据,获取和转换数据以供查看所需的逻辑,以及由用户交互触发的命令逻辑。在我们的示例应用程序中,ViewModel 相对简单;然而,应用程序越复杂,复用性可能就越高。

  • 由于 MvvmCross 依赖于使用每个平台的本地 UI 框架,每个应用程序都有一个本地的外观和感觉,并且当需要时,我们有一个自然层实现特定平台的逻辑。

  • MvvmCross 的数据绑定功能还消除了大量必须编写的繁琐代码。

所有这些优点并不一定是免费的;让我们看看一些缺点:

  • 第一个缺点是复杂性;你必须在 Xamarin、Android 和 iOS 之上学习另一个框架。

  • 在某些方面,MvvmCross 迫使你将应用程序在不同平台上的工作方式对齐,以实现最大的复用。由于表示逻辑包含在 ViewModel 中,视图被迫与之对齐。你的 UI 在各个平台上的偏差越大,你能够实际复用 ViewModel 的可能性就越小。

考虑到这些因素,我肯定会考虑在跨平台移动项目中使用 MvvmCross。是的,你需要学习一个额外的框架,而且你很可能需要调整一些应用布局的方式,但我认为 MvvmCross 提供了足够的价值和灵活性,使得这些问题变得可行。我是一个重用的大粉丝,MvvmCross 确实将重用提升到了新的水平。

摘要

在本章中,我们回顾了 MvvmCross 的高级概念,并通过一个实际练习将国家公园应用转换为使用 MvvmCross 框架并增加代码重用。在下一章中,我们将采用类似的方法来探索 Xamarin.Forms 框架,以评估其使用如何影响代码重用。

第八章. 使用 Xamarin.Forms 进行共享

在本章中,我们将讨论 Xamarin.Forms,这是一个跨平台开发框架。考虑到这一点,我们将涵盖以下领域:

  • 页面、视图(控件)和布局

  • 在 Xamarin.Forms 中的导航

  • XAML 和代码后置类

  • 数据绑定

  • 渲染器

  • DependencyService API

  • 应用程序启动

  • 项目组织

  • NationalParks 应用程序转换为使用 Xamarin.Forms

对 Xamarin.Forms 框架的深入了解

Xamarin.Forms 框架可用于为 Android、iOS 和 Windows Phone 开发移动应用程序。它为每个平台使用几乎相同的源代码库,同时仍然提供特定于平台的外观和感觉。Xamarin.Forms 可从 Xamarin 的任何付费许可证或 30 天评估版中使用。

小贴士

虽然我们提到 Xamarin.Forms 应用程序可以在 Windows Phone 上运行,但 Windows Phone 的许可、配置和开发细节超出了本书的范围。

与本书之前描述的方法不同,Xamarin.Forms 为您提供了一套抽象,涵盖了整个用户界面,从而允许 UI 代码和规范在多个平台之间重用。在运行时,Xamarin.Forms 使用每个平台本地的控件来渲染用户界面,这使得应用程序能够保持本地的外观和感觉。

本章分为两个主要部分:在第一部分,我们介绍在使用 Xamarin.Forms 之前需要理解的核心概念;在第二部分,我们将把我们的 NationalParks 应用程序转换为使用 Xamarin.Forms 框架。

页面

页面是一个视觉元素,用于组织用户在屏幕上一次性看到的全部内容。Xamarin.Forms 页面本质上类似于 Android 活动或 iOS 视图控制器。Xamarin.Forms 为您的应用程序提供了以下基本页面,其中每个类型都有相应的描述:

类型 描述
ContentPage 这允许您将一组控件或视图组织成布局以供显示和与用户交互
MasterDetailPage 这管理两个页面——主页面和详细页面——以及它们之间的导航
NavigationPage 这管理一组其他页面的导航
TabbedPage 这管理一组子页面,并允许您通过标签进行导航
CarouselPage 这管理一组子页面,并允许您通过滑动进行导航

视图

视图是一个视觉控件(或小部件),用于展示信息并允许用户与您的应用程序交互(如按钮、标签和编辑框等)。这些控件通常继承自 View 类。以下表格表示了在撰写本书时 Xamarin.Forms 提供的视图列表:

ActivityIndicator BoxView Button DatePicker
Editor Entry Image Label
ListView OpenGLView Picker ProgressBar
SearchBar Slider Stepper Switch
TableView TimePicker WebView

布局

控件托管在一种特殊类型的 View 中,称为布局。有两种不同类型的布局:托管和非托管。托管布局负责安排其托管的控件,而非托管布局需要开发者指定控件应该如何排列。Xamarin.Forms 提供了以下布局:

布局 描述
ContentView 这是一个可以包含子视图的布局。通常,ContentView不直接使用,而是用作其他布局的基础。
Frame 这是一个可以包含单个子视图并提供填充等框架选项的布局。
ScrollView 这个布局能够滚动其子视图。
AbsoluteLayout 这个布局允许其子视图根据应用程序的要求通过绝对位置进行定位。
Grid 这个布局允许内容在行和列中显示。
RelativeLayout 这个布局通过使用约束将视图相对于它拥有的其他视图进行定位。
StackLayout 这个布局将视图水平或垂直地放置在单行中。

单元格

单元格是一种特殊的控件,用于在列表中排列信息;具体来说,是ListViewTableView。单元格从Element类派生,而不是从VisualElement类派生,并作为模板来创建VisualElements

Xamarin.Forms 提供了以下类型的 Cells:

单元格类型 描述
EntryCell 这是一个带有标签和单个文本输入字段的 Cell。
SwitchCell 这是一个带有标签和开关视图(开/关)的 Cell。
TextCell 这是一个带有主文本和次文本的 Cell。通常,主文本用作标题,次文本用作副标题。
ImageCell 这是一个包含图像的TextCell

导航

在 Xamarin.Forms 应用程序中,导航是通过使用VisualElement的导航属性来实现的。这通常通过页面访问,导航属性的类型为INavigation接口,它提供了以下方法:

类型 描述
PushAsync() 此方法将页面推送到导航堆栈
PushModalAsync() 此方法将页面推送到导航堆栈作为模态对话框
PopAsync() 此方法从导航堆栈中移除当前页面
PopModalAsync() 此方法从导航堆栈中移除当前模态页面
PopToRootAsync() 此方法从导航堆栈中移除所有页面,除了根页面

在 Xamarin.Forms 中,导航的美丽之处在于其简单性。要导航到新页面并将数据传递到新页面,你只需要创建一个新页面的实例,在构造函数中传递数据,然后将此页面推送到导航堆栈,如下面的代码示例所示:

public partial class ParkDetailPage : ContentPage
{
    . . .
    public void EditClicked(object sender, EventArgs e)
    {
        Navigation.PushModalAsync (
            new ParkEditPage (_park));
    }
}

定义 Xamarin.Forms 用户界面

如同许多 UI 框架一样,Xamarin.Forms 允许两种不同的方法来创建用户界面:声明性和程序化:

  • 程序化方法:当使用这种方法时,开发者将 API 调用嵌入到构建 UI 中,并控制大小和位置

  • 声明性方法:当使用这种方法时,开发者创建 XAML 文件来定义用户界面的内容和布局

可扩展应用程序标记语言 (XAML)

可扩展应用程序标记语言XAML)是由微软开发的一种基于 XML 的语言。XAML 允许开发者使用 XML 来指定要实例化的对象层次结构。它可以以多种方式使用,但最成功的是作为指定Windows Presentation FoundationWPF)、Silverlight、Windows Runtime 和现在 Xamarin.Forms 用户界面的手段。

XAML 文件在构建时解析以验证指定的对象,并在运行时实例化对象层次结构。

除了指定对象层次结构外,XAML 还允许开发者指定属性值和分配事件处理器。但是,它不允许你嵌入代码或逻辑。

以下 XAML 文件定义了ContentPage视图的内容:

<?xml version="1.0" encoding="UTF-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/...
    xmlns:x="http://schemas.microsoft.com/winfx...
    x:Class="NationalParks.ParkEditPage">
  <StackLayout Orientation="Vertical"
    HorizontalOptions="StartAndExpand">
    <Grid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Label
          Text="Name:"
          Grid.Row="0" Grid.Column="0" />
  <Entry
          x:Name="descriptionEntry"
          Text="{Binding Name}"
          Grid.Row="0" Grid.Column="1"
          HorizontalOptions="FillAndExpand" />
  . . .
   </Grid>
   <Button
       x:Name="doneButton" 
       Text="Done"
       Clicked="DoneClicked" />
  </StackLayout>
</ContentPage>

看一下之前的 XAML 规范:

  • 类名为ParkEditPage,并在ContentPage元素中指定

  • 使用Grid布局来组织页面中的内容

  • 两个组件被分配了属性名称,nameEntrydoneButton

  • doneButton组件被分配了一个名为DoneClickedClicked事件处理器

代码后置类

当你在 Xamarin.Forms 应用程序中创建一个页面时,实际上会创建两个文件:一个 XAML 文件和一个类文件。Xamarin Studio 将类文件嵌套在 XAML 文件下的解决方案面板中,如下面的截图所示:

代码后置类

.xaml.cs文件有时被称为代码后置类。它们被创建来包含所有与页面定义一起使用的应用程序逻辑的事件处理器。以下示例显示了ParkEditPage的代码后置类:

public partial class ParkEditPage : ContentPage
{
  NationalPark _park;
  public ParkEditPage()
  {
    InitializeComponent ();
  }
  protected void DoneClicked(object sender, EventArgs e)
  {
    // perform event handler logic
  }
}

你应该注意以下类定义的方面:

  • ParkEditPage是一个部分类定义。

  • DoneClicked()事件处理器定义在这个类文件中。这是分配给 XAML 中Done按钮的事件处理器。

  • 在早期文件中没有定义属性定义。

那么,属性定义在哪里呢?每次构建应用程序时,Xamarin Studio 都会生成第二个代码文件。对于我们的示例,文件将被命名为ParkEditPage.xaml.g.cs,并将包含以下代码片段:

public partial class ParkEditPage : ContentPage {
    private Entry nameEntry;    
    . . .    
    private Button doneButton;  
    private void InitializeComponent() {
        this.LoadFromXaml(typeof(ParkEditPage));
        nameEntry =
          this.FindByName <Entry>("nameEntry");
        . . .
        doneButton =
          this.FindByName <Button>("doneButton");
    }
}

你应该注意以下几点:

  • ParkEditPage文件上定义了两个属性:nameEntrydoneButton。这些属性直接从 XAML 文件中找到的名称生成。

  • 生成一个名为InitializeComponent()的方法。该方法必须从ParkEditPage.xaml.cs中定义的任何构造函数中调用。

  • InitializeComponent()方法调用LoadFromXaml()来实例化由ParkEditPage.xaml定义的所有对象。

  • InitializeComponent()方法调用FindByName()将每个属性绑定到其对应的实例。

数据绑定

数据绑定的概念在第七章“与 MvvmCross 共享”部分中详细讨论,该部分标题为“数据绑定”。Xamarin.Forms 提供了一个遵循与 MvvmCross、Windows Presentation Foundation (WPF)、Silverlight 和 Windows Runtime 相同架构的数据绑定功能。

在 Xamarin.Forms 应用程序中,绑定规范通常在 XAML 中指定。以下 XAML 规范演示了将Entry控制的Text属性绑定到NationalPark对象的Name属性:

    <Entry x:Name="nameEntry"
        Text="{Binding Name}"
        Grid.Row="0" Grid.Column="1"
        HorizontalOptions="FillAndExpand" />

通常,绑定上下文通过代码设置。以下示例演示了如何在页面级别通过编程方式设置绑定上下文为NationalPark对象:

public ParkEditPage (NationalPark park)
{
    InitializeComponent ();
    _park = park;
    BindingContext = _park;
}

在前面的示例中,为整个页面设置了绑定上下文。有时,控件提供了需要设置的绑定上下文以完成数据绑定。以下示例演示了设置ListView控制的绑定上下文:

parksListView.ItemsSource =
  NationalParksData.Instance.Parks;

注意

注意,ListView控制的绑定上下文是一个名为ItemsSource的属性。

使用 Renderer

Xamarin.Forms 使用平台原生控件来渲染用户界面,这使得应用程序能够保持用户对每个平台预期的外观和感觉。这是通过使用 Renderer 实现的。页面、布局和控制代表用于描述用户界面的抽象集合。这些元素中的每一个都是使用Renderer类渲染的,该类根据应用程序运行的平台创建一个原生控件。

开发者可以创建自己的 Renderer 以自定义特定控件在平台上的渲染方式。

原生特性和 DependencyService API

到目前为止,我们主要关注的是使用可以在所有平台上重用的抽象。那么,如果你需要访问特定平台的特定功能呢?这就是DependencyService API 的用武之地。DependencyService API 是一个允许每个平台注册一个特定于平台的服务的 API,该服务可以通过一个公共接口被共享代码调用。

使用DependencyService API 涉及以下三个步骤:

  1. 首先,你需要创建一个接口,该接口公开了必须为每个平台实现的平台特定方法。

  2. 在此步骤之后,为每个平台创建接口的实现,并使用assembly属性注册实现。

  3. 总结来说,从共享代码中调用DependencyService.Get<MyInterface>以查找适当的实现并调用返回实例上的服务。

我们将在本章的添加对 DependencyService 的调用部分稍后演示DependencyService API 的使用。

应用启动

Xamarin.Forms 应用以原生应用的方式启动,这意味着遵循传统的启动序列。在启动序列中,应用执行以下两个任务:

  1. 调用初始化 Xamarin.Forms 运行时的方法。

  2. 启动第一个页面。

共享应用类

默认情况下,Xamarin.Forms 应用会创建一个共享的App类,其中包含一个返回当应用启动时应展示的第一个页面的单个静态方法。以下代码演示了这一点:

public class App
{
    public static Page GetMainPage()
    {
        return new HelloWorldPage();
    }
}

这种简单的方法允许每个应用的特定平台启动代码调用GetMainPage()方法以确定要启动哪个页面。因此,它只在一个地方指定。

iOS 应用

在一个 Xamarin.Forms iOS 应用中,初始化是在AppDelegate类的FinishedLaunching()方法中执行的,如下所示:

[Register("AppDelegate")]
public partial class AppDelegate : UIApplicationDelegate
{
  UIWindow window;
  public override bool FinishedLaunching(UIApplication app, NSDictionary options)
  {
    Forms.Init();
    window = new UIWindow(UIScreen.MainScreen.Bounds);
    window.RootViewController =
        App.GetMainPage().CreateViewController();
    window.MakeKeyAndVisible();
    return true;
  }
}

安卓应用

在一个 Xamarin.Forms 安卓应用中,初始化是在带有MainLauncher=true属性的Activity实例中完成的。这可以在以下代码片段中看到:

namespace HelloWorld.Android
{
    [Activity(Label="HelloWorld", MainLauncher=true)]
    public class MainActivity : AndroidActivity
    {
        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            Xamarin.Forms.Forms.Init(this, bundle);
            SetPage(App.GetMainPage());
        }
    }
}

项目组织

Xamarin.Forms 项目通常使用以下两种项目模板之一创建,可以通过导航到“新建解决方案”对话框中的C# | 移动应用来找到:

  • 空白应用 (Xamarin.Forms 可移植)

  • 空白应用 (Xamarin.Forms 共享)

这两种模板之间的区别是用于存放共享代码的项目类型。使用第一个模板,共享代码存放在可移植类库中,而使用第二个模板,共享代码存放在共享项目中。共享项目允许所有引用项目重用其包含的代码,但代码是针对每个引用项目特定编译的。

小贴士

如果你计划在某个时候将 Windows Phone 项目添加到解决方案中,你将熟悉使用 PCL 解决方案。这将要求你在 PCL 的限制内工作,但将确保你的代码与更多平台兼容。

创建一个 Xamarin.Forms 解决方案后,你会看到创建了三个实际的项目。第一个项目包含共享代码,第二个项目包含 iOS 代码,第三个项目包含安卓代码。如果我们成功使用 Xamarin.Forms,大部分代码将最终位于共享项目中。以下截图显示了使用 PCL 模板创建的项目示例:

项目组织

创建 NationalParks 的 Xamarin.Forms 应用

现在我们已经对 Xamarin.Forms 有了坚实的理解,让我们将NationalParks应用程序转换为使用新框架。在这个练习中,我们将遵循迄今为止在 iOS 应用程序中使用的相同应用程序流程,这意味着我们将有一个列表页面、一个用于查看的详细页面以及一个用于添加和更新的编辑页面。

创建解决方案

我们将首先通过以下步骤创建一个全新的项目:

  1. 首先,访问文件菜单并导航到新建 | 新建解决方案

  2. 新建解决方案对话框中,导航到C# | 移动应用,选择空白应用(Xamarin.Forms Portable)模板,在名称字段中输入NationalParks,选择合适的位置值,然后点击确定

  3. 检查项目结构。您将看到以下提示:

    • NationalParks.iOS项目中打开AppDelegate.cs。注意对Forms.Init()App.GetMainPage()的调用。

    • NationalParks.Android项目中打开MainActivity.cs。注意对Forms.Init()App.GetMainPage()的调用。

    • NationalParks项目中打开App.cs。注意静态方法GetMainPage()

  4. 最后,运行NationalParks.AndroidNationalParks.iOS项目。

添加 NationalParks.PortableData

我们下一步是将第七章中的存储解决方案分享与 MvvmCross 引入。按照以下步骤将存储解决方案添加到我们的新 Xamarin.Forms 解决方案中:

  1. 首先,您需要将第七章中的NationalParks.PortableDataNationalParks.IO项目从解决方案文件夹复制到新的解决方案文件夹中。

  2. 将每个项目添加到新的解决方案文件夹中,方法是选择解决方案,右键单击它,导航到添加 | 添加现有项目,然后选择项目文件,例如,NationalParks.IO.csproj

  3. 通过选择每个项目的引用文件夹,右键单击它们,选择编辑引用,并将NationalParks.PortableData项目作为引用添加到新的NationalParksNationalParks.AndroidNationalParks.iOS项目中。

  4. 现在,我们需要将FileHandler.cs文件链接添加到NationalParks.AndroidNationalParks.iOS项目中。对于每个项目,创建一个名为NationalParks.IO的新文件夹,并选择新文件夹,右键单击它,导航到添加 | 添加文件,选择FileHandler,选择打开,选择添加文件链接,然后点击确定

  5. 为了验证所有之前的步骤,您应该编译新的解决方案。

实现 ParksListPage

现在,我们可以开始工作于用户界面,从显示公园的列表视图开始,按照以下步骤操作:

  1. 选择NationalParks项目,右键单击它,然后导航到添加 | 新建文件。从新建文件对话框中,导航到表单 | 表单内容页 XAML,在名称字段中输入ParksListPage,然后选择新建

  2. 现在,你应该打开ParkListPage.xaml。你会看到一个空的ContentPage元素。添加一个垂直方向的StackLayout,其中包含一个ListView实例和一个Button实例,如下所示:

    <StackLayout Orientation="Vertical"
        HorizontalOptions="StartAndExpand">
        <ListView x:Name="parksListView"
            IsVisible="true"
            ItemSelected="ParkSelected">
        </ListView>
        <Button Text="New"
      Clicked="NewClicked" />
    </StackLayout>
    

    提示

    注意parkListViewParkSelected事件处理程序和New按钮的NewClicked事件处理程序。

  3. 现在,让我们为ListView添加行定义。ListView元素有一个DataTemplate属性,它定义了每一行的布局。以下布局应定义一个标签,用于显示公园的名称和描述。这应该放置在 XAML 中的ListView元素内:

    <ListView.ItemTemplate>
      <DataTemplate>
        <ViewCell>
           <ViewCell.View>
         <StackLayout Orientation="Vertical"
               HorizontalOptions="StartAndExpand">
               <Label Text="{Binding Name}"
                 HorizontalOptions="FillAndExpand" />
             </StackLayout>
          </ViewCell.View>
        </ViewCell>
      </DataTemplate>
    </ListView.ItemTemplate>
    

    注意两个Label视图的绑定规范。

  4. NationalParks项目中打开App.cs,将主页面更改为ParksListPage。我们还需要创建NavigationPage作为ParksListPage的所有者以支持推送和弹出导航。GetMainPage()方法应包含以下代码:

    public static Page GetMainPage ()
    {
        NavigationPage mainPage =
            new NavigationPage(new ParksListPage());
        return mainPage;
    }
    
  5. NationalParks.iOS项目中打开AppDelegate.cs。然后,在调用Forms.Init()之前,将以下初始化代码添加到FinishedLaunching()方法中:

    // Initialize data service
    NationalParksData.Instance.DataDir =
        Environment.CurrentDirectory;
    NationalParksData.Instance.FileHandler =
        new FileHandler ();
    
  6. NationalParks.Android项目中打开MainActivity.cs。一旦进入,请在调用Forms.Init()之前在OnCreate()方法中添加以下初始化代码:

    // Initialize data service
    NationalParksData.Instance.DataDir =
        System.Environment.GetFolderPath(
            System.Environment.SpecialFolder.MyDocuments);
    NationalParksData.Instance.FileHandler =
        new FileHandler ();
    
  7. 打开ParksListPage.xaml.cs,添加一个用于加载公园数据的方法,并设置绑定上下文:

    public async Task LoadData()
    {
      await NationalParksData.Instance.Load();
      parksListView.ItemsSource =
          NationalParksData.Instance.Parks;
    }
    
  8. 在构造函数中添加对LoadData()的调用:

      InitializeComponent ();
      LoadData ();
    

    注意

    你将无法在LoadData()方法中使用await,因为它是在构造函数中被调用的。在这种情况下,实际上没有必要等待调用。

  9. 最后一步是创建两个用于NewClickedParkSelected的占位事件处理程序,我们将在完成应用程序时填充它们。

  10. 我们现在可以测试我们的工作了。编译并运行NationalParks.iOSNationalParks.Android应用程序。

实现ParkDetailPage

现在,我们需要一个页面来显示公园的详细信息。要创建ParkDetailPage,请执行以下步骤:

  1. 添加一个名为ParkDetailPage的新ContentPage实例。

  2. 对于ParkDetailPage,我们将在Grid中显示一系列Label视图,并在Grid下方显示一组Button以启动操作。所有这些内容都将托管在垂直方向的StackLayout中。首先添加与上一节相同的StackLayout

  3. 添加一个包含一系列Label视图的Grid布局,以显示NationalPark的属性,如下所示:

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Label Text="Name:"
            Grid.Row="0" Grid.Column="0" />
        <Label Text="{Binding Name}"
            Grid.Row="0" Grid.Column="1"
            HorizontalOptions="FillAndExpand" />
        . . .
    </Grid>
    

    注意

    注意Grid.RowGrid.Column的指定,它们控制LabelEntry视图的定位。

  4. 现在,添加三个用于页面可执行操作的Button定义,如下所示:

    <Button Text="Edit"
                  WidthRequest="175"
                  HorizontalOptions="Center"
                  Clicked="EditClicked" />
    <Button Text="Directions"
                  WidthRequest="175"
                  HorizontalOptions="Center"
                  Clicked="DirectionsClicked" />
    <Button Text="Photos"
                  WidthRequest="175"
                  HorizontalOptions="Center"
                  Clicked="PhotosClicked" />
    
  5. 添加一个构造函数,接受要显示的NationalPark实例。以下代码示例演示了所需的内容:

    NationalPark _park;
    public ParkDetailPage (NationalPark park)
    {
    InitializeComponent ();
      _park = park;
      BindingContext = _park;
    }
    

    注意

    注意到构造函数中的最后一行设置了BindingContext。这告诉页面如何解析在 XAML 中声明的绑定规范。

  6. EditClickedDirectionsClickedPhotosClicked添加存根事件处理器。

  7. 现在,我们需要回到ParksListPage类并添加导航逻辑。打开ParksListPage.xaml.cs并更新ParkSelected()事件处理器,以便对ParkDetailPagePushAsync()进行调用,如下所示:

    protected void ParkSelected(object sender,
    SelectedItemChangedEventArgs e)
    {
        Navigation.PushAsync (new
            ParkDetailPage ((NationalPark) e.SelectedItem));
    }
    
  8. 编译并运行NationalParks.iOSNationalParks.Android应用程序。

使用 DependencyService 显示方向和照片

如我们之前讨论的,DependencyService API 允许应用程序利用平台特定的功能。我们将演示如何使用DependencyService来实现显示公园方向和照片的功能。

创建接口

第一步是在共享项目中创建一个接口,描述需要支持的方法。要创建IParkInfoServices接口,请执行以下步骤:

  1. 首先,选择NationalParks项目,右键单击它,然后导航到添加 | 新建文件

  2. 导航到通用 | 空接口,在名称字段中输入IParkInfoServices,并选择新建

  3. 现在,您需要在接口上创建两个方法,一个用于显示方向,一个用于显示照片;每个都应该接受NationalPark作为参数:

    public interface IParkInfoServices
    {
        void ShowDirections(NationalPark park);
        void ShowPhotos(NationalPark park);
    }
    

创建 iOS 实现

现在,让我们通过以下步骤创建一个 iOS 实现:

  1. 选择NationalParks.iOS项目,右键单击它,然后导航到添加 | 新建文件。在这个对话框中,导航到通用 | 空类,在名称字段中输入iOSParkInfoServices,并选择新建

  2. 添加命名空间using子句,Xamarin.Forms、NationalParksNationalParks.PortableDataNationalParks.iOS

  3. iOSParkInfoServices类规范更改,使其实现IParkInfoServices

  4. 选择IParkInfoService,右键单击它,导航到重构 | 实现接口,然后按Enter

  5. 然后,您应该提供两个方法调用的实现,如下所示:

    public void ShowDirections(NationalPark park)
    {
      if ((park.Latitude.HasValue) &&
          (park.Longitude.HasValue))
      {
        NSUrl url = new NSUrl (
            String.Format (
     "http://maps.apple.com/maps?daddr={0},{1}&saddr=CurrentLocation", park.Latitude, park.Longitude));
    
        UIApplication.SharedApplication.OpenUrl (url);
      }
    }
    public void ShowPhotos(NationalPark park)
    {
        UIApplication.SharedApplication.OpenUrl (
            new NSUrl(String.Format (
              "http://www.bing.com/images/search?q={0}",
              park.Name)));
    }
    
  6. 最后,将以下Dependency属性添加到class文件中的namespace定义外部,如下所示:

    [assembly: Dependency (typeof (iOSParkInfoServices))]
    namespace NationalParks.iOS
    {
        . . .
    

    Dependency属性将类注册到DependencyService,以便当调用Get()时,可以找到特定平台的实现。

创建 Android 实现

现在,让我们通过以下步骤创建一个 Android 实现:

  1. 选择NationalParks.Android项目,右键单击它,然后导航到添加 | 新建文件。然后,在新建文件对话框中,导航到通用 | 空类,在名称字段中输入AndroidParkInfoServices,并选择新建

  2. 为命名空间添加using子句,Xamarin.Forms、NationalParksNationalParks.PortableDataNationalParks.Droid

  3. AndroidParkInfoServices类的规范更改,使其实现IParkInfoServices

  4. 然后,选择IParkInfoService,右键单击它,导航到重构 | 实现接口,并按Enter

  5. 为以下两个方法调用提供实现,如下所示:

    public void ShowDirections(NationalPark park)
    {
      if ((park.Latitude.HasValue) &&
        (park.Longitude.HasValue))
     {
         Intent mapIntent = new Intent (Intent.ActionView,
          Android.Net.Uri.Parse (
         String.Format ("geo:0,0?q={0},{1}&z=16 ({2})",
                park.Latitude, park.Longitude,
                park.Name)));
          Forms.Context.StartActivity (mapIntent);
      }
    }
    public void ShowPhotos(NationalPark park)
    {
      Intent urlIntent = new Intent (Intent.ActionView);
      urlIntent.SetData (Android.Net.Uri.Parse (
            String.Format (
            "http://www.bing.com/images/search?q={0}",
            park.Name)));
      Forms.Context.StartActivity (urlIntent);
    }
    

    注意

    注意Forms.Context的使用。在 Android 的情况下,它包含当前正在执行的Activity;在我们的情况下,是MainActivity

  6. 将以下Dependency属性添加到class文件中namespace定义之外,如下所示:

    [assembly: Dependency (typeof (AndroidParkInfoServices))]
    namespace NationalParks.iOS
    {
        . . .
    

添加对 DependencyService 的调用

现在,我们需要向共享项目添加代码,以便实际调用ShowDirections()ShowPhotos()。您需要做的就是打开ParkDetailPage.xaml.cs,并填写DirectionsClicked()PhotosClicked()的占位符实现,如下所示:

public void DirectionsClicked(
    object sender, EventArgs e)
{
    DependencyService.Get<IParkInfoServices> ().
        ShowDirections (_park);
}
public void PhotosClicked(
    object sender, EventArgs e)
{
    DependencyService.Get<IParkInfoServices> ().
        ShowPhotos (_park);
}

运行应用程序

我们终于准备好运行应用程序了。虽然有几个步骤,但DependencyService API 提供了一个非常干净的方法来分离共享和平台特定的代码。运行NationalParks.iOSNationalParks.Android应用程序。

实现ParkEditPage

现在,我们需要一个页面来更新公园信息。要实现ParkEditPage,执行以下步骤:

  1. 首先,添加一个名为ParkDetailsPage的新ContentPage

  2. 我们将使用与ParkDetailPage相似的布局来实现ParkEditPage,除了我们将使用Entry视图来允许编辑NationalPark的属性。向ParkEditPage添加一个StackLayoutGrid实例,并为NationalPark的每个属性添加一系列LabelEntry视图,如下面的代码片段所示:

    <Label Text="Name:"
        Grid.Row="0" Grid.Column="0" />
    <Entry x:Name="nameEntry"
        Text="{Binding Name}"
        Grid.Row="0" Grid.Column="1"
        HorizontalOptions="FillAndExpand" />
    
  3. 然后,您可以在编辑过程中添加一个Done按钮,如下所示:

    <Button x:Name="doneButton"
                 Text="Done"
                  WidthRequest="175"
                  HorizontalOptions="Center"
                  Clicked="DoneClicked" />
    
  4. 创建两个构造函数,一个接受NationalPark实例并将用于编辑现有公园,另一个不接受NationalPark实例并将用于创建新公园,如下面的代码片段所示:

    public ParkEditPage()
    {
    InitializeComponent ();
      _park = new NationalPark ();
      BindingContext = _park;
    }
    public ParkEditPage (NationalPark park)
    {
      InitializeComponent ();
      _park = park;
      BindingContext = _park;
    }
    
  5. 创建一个名为DoneClicked()的事件处理程序,其中包含保存更新后的公园的调用,以及导航调用PopAsync()以返回显示ParkEditPage的页面,如下所示:

    protected void DoneClicked(object sender, EventArgs e)
    {
        NationalParksData.Instance.Save(_park);
        Navigation.PopAsync ();
    }
    
  6. 现在,我们需要向ParkListPageParkDetailPage添加导航逻辑。打开ParkDetailPage.xaml.cs,并填写EditClicked()事件处理程序,其中包含调用PushAsync()以显示ParkEditPage。将正在查看的公园传递给ParkEditPage构造函数,如下所示:

    public void EditClicked(object sender, EventArgs e)
    {
       Navigation.PushAsync(new ParkEditPage(_park));
    }
    
  7. 打开ParkListPage.xaml.cs,并填写NewClicked()事件处理程序,其中包含调用PushAsync()以显示ParkEditPage。调用空的ParkEditPage构造函数,以便创建一个新的公园,如下所示:

    protected void NewClicked(object sender, EventArgs e)
    {
        Navigation.PushAsync(new ParkEditPage());
    }
    
  8. 现在,我们的应用程序已经准备好了;编译并运行NationalParks.iOSNationalParks.Android应用程序。

考虑利弊

如我们所见,Xamarin.Forms 提供了一种稳健的方法来显著增加在您的移动应用程序中重用的代码量;它具有许多优秀功能:

  • XAML 是定义用户界面的好方法,并允许您以方便、简洁的方式创建属性和分配事件处理器

  • 数据绑定功能非常出色,消除了编写大量繁琐、令人厌烦的代码

  • DependencyService API 提供了一种访问特定平台功能的好方法

  • Renderer 架构提供了极致的可定制性

然而,在撰写本书时,Xamarin.Forms 仍然有些不成熟,存在一些弱点:

  • 没有可视设计器用于 XAML 代码,因此您必须构建您的 UI 并运行应用程序以查看其视觉渲染效果

  • 由于框架的新颖性,可用的示例数量有限,并且许多示例使用代码构建用户界面,而不是 XAML

  • 验证功能似乎相当薄弱

这些批评不应过于严重;跨平台 UI 框架很难构建,我坚信 Xamarin 正在正确的轨道上,并将迅速发展该框架。

摘要

在本章中,我们回顾了 Xamarin.Forms 的功能,并将我们的现有 NationalParks 应用程序转换为使用该框架。在下一章中,我们将探讨为 iOS 应用程序准备分发的过程。

第九章:准备分发 Xamarin.iOS 应用

在本章中,我们将讨论与准备分发 Xamarin.iOS 应用相关的活动,并查看分发应用的多种选项。虽然我们将讨论的活动是任何 iOS 应用部署的组成部分,但我们将尝试缩小覆盖范围,仅涵盖开发 Xamarin.iOS 应用时独特的方面。我们将涵盖以下主题:

  • 应用分析

  • iOS 应用分发构建设置

  • 应用分发选项

准备分发

到目前为止,我们的应用已经构建并按预期工作;大部分工作已经完成。现在,我们将注意力转向准备应用分发。本节讨论以下三个准备应用分发的方面:

  • 应用分析:在这里,我们将查看内存分配问题和性能瓶颈

  • iOS 应用设置:在这里,我们将更新版本和构建号等信息设置

  • iOS 构建设置:在这里,我们将调整基于目标设备、期望的性能特性和可部署大小而影响的代码生成设置

分析 Xamarin.iOS 应用

分析允许开发者在应用执行期间监控其应用,并识别与内存分配和性能瓶颈相关的问题。分析活动可以在应用开发的整个生命周期中执行,但将其纳入过程的后期阶段作为分发前的最终验证特别有益。

Xamarin.iOS 开发者可以选择两种工具来分析应用:MonoTouch 分析器和 Apple 的 Instruments 应用。我们不会复制这些应用的现有文档,但会提供以下链接供参考:

工具 URL
MonoTouch 分析器 docs.xamarin.com/guides/ios/deployment,_testing,_and_metrics/monotouch_profiler/
Apple 的 Instruments 应用 docs.xamarin.com/guides/ios/deployment,_testing,_and_metrics/walkthrough_Apples_instrument/

iOS 应用(Info.plist)设置

很可能,当你准备开始分发过程时,你需要在 Info.plist 中进行的设置大部分已经完成。但是,你可能需要更新一些设置,特别是版本和构建设置。以下截图显示了 iOS 应用 设置屏幕:

iOS 应用(Info.plist)设置

iOS 构建设置

Xamarin.iOS 提供了许多选项来优化基于目标设备、可部署应用的大小和执行速度的构建过程。以下几节讨论了与生成最终分发构建最相关的设置。

SDK 选项

SDK 版本应设置为应用可以部署到的最低 iOS 版本。很可能这个设置已经在开发过程中已经确定。

链接器选项

用于构建 Xamarin.iOS 应用的 mTouch 工具包括一个链接器,链接器的目的是减小最终应用的大小。链接器通过在您的应用中对代码执行静态分析来实现这一点,评估引用组件中哪些类和方法实际上被使用,并删除未使用的类、方法和属性。

可以在项目选项 | iOS 构建下的常规选项卡中设置链接器的选项,如下面的截图所示:

链接器选项

可以设置以下选项来控制链接过程:

  • 不链接:此选项禁用链接器并确保所有引用的组件都包含在内,而不进行修改。请注意,这是针对 iOS 模拟器的构建的默认设置,因为排除耗时的静态分析过程可以节省时间。因此,生成的大型 DLL 仍然可以相对快速地部署到模拟器。

  • 仅链接 SDK 组件:此选项告诉链接器仅对 SDK 组件(即与 Xamarin.iOS 一起发货的组件)进行操作。这是针对设备的目标构建的默认设置。

  • 链接所有组件:此选项告诉链接器对整个应用以及所有引用的组件进行操作。这允许链接器使用更大的优化集,并产生可能的最小应用。然而,当链接器以这种方式运行时,由于静态分析过程中做出的错误假设,它更有可能破坏您的代码的一部分。特别是,静态分析可能会因为反射、序列化或任何类型或成员实例未静态引用的代码而陷入困境。

以下表格总结了在第六章 共享游戏中生产的NationalParks应用两个版本链接的结果:

文件链接版本 PCL 版本
不链接 47.5 MB 48.4 MB
仅链接 SDK 组件 6.7 MB 7.3 MB
链接所有组件 5.8 MB 6.4 MB

如表所示,从不链接仅链接 SDK 组件的转变实现了应用大小最大的差异。

覆盖链接器

链接器在上一节中展示了其提供的巨大好处。然而,有时你可能需要覆盖链接器的默认行为,因为链接器可能会删除应用实际使用的类型和成员实例。这将导致与这些类型和/或成员未找到相关的运行时异常。以下表格描述了三种改变链接器行为的方法,以避免丢失重要的类型和成员:

技术 描述

| 保留代码 | 如果你在测试中确定链接器正在删除你的应用所需的类或方法,你可以通过在类和/或方法上使用Preserve属性来明确告诉链接器始终包含它们。要保留整个类型,请使用:

[Preserve (AllMembers = true)]

要保留单个成员,请使用:

[Preserve (Conditional=true)]

|

| 跳过程序集 | 在某些情况下,你可能需要告诉链接器跳过整个程序集,因为你没有修改源代码的能力(第三方库)。这可以通过使用命令行选项linkskip来完成。例如:

--linkskip=someassembly

|

禁用链接移除 链接器采用的一种优化是移除在实际设备上非常不可能使用的代码;那些标记为不受支持或禁止的功能。在罕见的情况下,这些功能可能对应用的功能是必需的。此优化可以通过使用命令行选项--nolinkaway来禁用。

调试选项

调试选项应始终在发布构建中禁用。启用调试可能导致二进制文件显著增大。

代码生成选项

代码生成选项控制根据目标处理器和期望的性能特性在构建过程中创建的代码。此设置下的选项将在以下章节中解释。

支持的架构

支持的架构标识了结果构建应支持的处理器架构。从原始 iPhone 到 iPhone 3G,使用了 ARMv6 CPU。较新的 iPhone 和 iPad 型号使用 ARMv7 或 ARMv7s 架构,而 iPhone 5s 引入了基于 ARMv8a 架构的 A7 处理器的使用。

ARMv6 不支持 Xcode 4.5 之前的版本。如果你需要为旧设备构建,你需要使用安装的较早版本的 Xcode。

LLVM 优化编译器

Xamarin.iOS 附带两个不同的代码生成引擎:普通的 Mono 代码生成引擎和基于 LLVM 优化编译器的引擎。LLVM 引擎在编译时间成本下,比 Mono 引擎生成更快、更小的代码。因此,当你在开发应用时,Mono 代码生成引擎更方便使用,而 LLVM 引擎则更适合用于将分发构建。

ARM thumb 指令集

ARM Thumb 指令集是 ARM 处理器使用的更紧凑的指令集。通过启用 Thumb 支持,你可以以牺牲较慢的执行时间为代价来减小可执行文件的大小。Thumb 由 ARMv6、ARMv7 和 ARMv7s 支持。

分发 Xamarin.iOS 应用

Xamarin.iOS 支持所有 iOS 开发者可访问的传统分发方法。Xamarin 网站和 Apple 开发者网站上关于 iOS 应用分发的信息非常丰富。我们并不试图复制那些详尽的资源库。以下章节旨在从 Xamarin.iOS 的角度提供一个概述。

Ad Hoc 和企业分发

Ad Hoc 分发和企业分发允许应用在不经过 App Store 的情况下进行分发。Ad Hoc 通常用于支持即将发布的测试工作。企业分发用于分发不面向公众的应用,而是面向单个企业内部用户使用。在两种情况下,都必须创建一个 iOS App Store Package (IPA)。

注意

生成企业分发需要 Apple 的企业账户和 Enterprise Xamarin.iOS 许可证。

创建 IPA 的步骤如下:

  1. developer.apple.com 上为您的应用创建并安装分发配置文件。有关此过程的详细说明,请参阅 docs.xamarin.com/guides/ios/deployment,_testing,_and_metrics/app_distribution_overview/publishing_to_the_app_store/ 中的 创建和安装分发配置文件 部分。

  2. 通过导航到 项目选项 | 构建 | iOS 包签名,设置用于构建新安装配置文件的 配置文件 选项,如图所示:Ad Hoc 和企业分发

  3. 将应用的 Bundle Identifier 选项设置为在创建分发配置文件时使用的相同值,方法是通过导航到 项目选项 | 构建 | iOS 应用,如图所示:Ad Hoc 和企业分发

  4. IPA 创建完成后,只需在 finder 中导航到 IPA,双击它,它将在 iTunes 中打开。现在可以使用 iTunes 在设备上同步应用。

TestFlight 分发

TestFlight是一个基于云的应用程序分发服务,用于分发应用程序的预发布版本。Xamarin Studio 提供了与TestFlight服务的集成,以便可以直接从 IDE 中上传Ad Hoc构建。在上传构建之前,您必须在TestFlight服务中创建一个账户并定义测试团队和应用程序。这可以通过www.testflightapp.com完成。

要将构建上传到 TestFlight,请执行以下步骤:

  1. 选择Ad Hoc作为构建类型,并从主菜单中的项目 | 发布导航到TestFlight

  2. 输入API 令牌团队令牌,这些是在您设置应用程序和团队时由TestFlight分配的。您可以点击这些字段旁边的链接,在浏览器中显示相应的值。TestFlight 分发

  3. 输入发布说明,以便让测试者知道新版本中修复了什么以及/或者添加了什么。

  4. 输入分发列表并开启通知团队成员,以便在发布说明中发送电子邮件通知。

  5. 选择选项,替换具有相同名称的现有二进制文件上传 dSYMs,然后点击发布。Xamarin Studio 将构建应用程序并将其上传到 TestFlight。

App Store 提交

通过 App Store 分发应用程序对 Xamarin.iOS 应用程序来说与其他 iOS 应用程序几乎相同。除了生成发布构建之外,大部分工作都是在 Xamarin Studio 之外完成的。您需要在项目选项对话框的iOS 捆绑签名部分输入配置文件值。

以下链接提供了将 Xamarin.iOS 应用程序发布到 App Store 的详细步骤:docs.xamarin.com/guides/ios/deployment,_testing,_and_metrics/app_distribution_overview/publishing_to_the_app_store/

摘要

在本章中,我们讨论了准备应用程序分发相关的活动、可用的分发渠道以及分发应用程序涉及的过程。在下一章中,我们将探讨分发 Xamarin.Android 应用程序的相同方面。

第十章:准备分发 Xamarin.Android 应用程序

在本章中,我们将讨论与准备分发 Xamarin.Android 应用程序相关的活动,并查看分发应用程序的各种选项。我们将讨论的许多活动是任何 Android 应用程序部署的组成部分。然而,在本章中,我们将尝试缩小覆盖范围,仅关注使用 Xamarin.Android 开发时独特的方面。我们将涵盖以下主题:

  • 应用程序性能分析

  • 分发应用程序的 Android 构建设置

  • 应用程序分发选项

准备发布 APK

在发布签名的 APK 文件之前,需要完成一系列活动。以下章节讨论了在生成发布 APK 之前应考虑的主题。

性能分析 Xamarin.Android 应用程序

Xamarin.Android 商业许可证为性能分析 Android 应用程序提供有限支持。性能分析可以是非常有效的识别内存泄漏和进程瓶颈的方法。

小贴士

本书将不会涵盖性能分析,但以下链接提供了使用 Xamarin.Android 性能分析功能的概述:docs.xamarin.com/guides/android/deployment,_testing,_and_metrics/profiling

除了使用 Xamarin.Android 提供的工具外,还可以使用传统的 Android 性能分析工具,如 Traceview 和 dmtracedump。更多信息请参阅 developer.android.com/tools/debugging/debugging-tracing.html

禁用调试

在开发 Xamarin.Android 应用程序时,Xamarin Studio 支持使用 Java 调试 Wire ProtocolJDWP)进行调试。这个功能需要在发布构建中禁用,因为它存在安全风险。你有两种不同的方法来完成这个任务:

  • 修改 AndroidManifest.xml 中的设置

  • 修改 AssemblyInfo.cs 中的设置

修改 AndroidManifest.xml 中的设置

第一种方法可以通过以下列表来完成,该列表展示了如何从 AndroidManifest.xml 文件中关闭 JDWP 调试:

    <application .. .
        android:debuggable="false" .. .
    </application>

修改 AssemblyInfo.cs 中的设置

替代方法是通过在 AssemblyInfo.cs 中禁用 JDWP 来实现的。这种方法的优势在于它基于当前选定的配置。以下列表展示了如何使用条件指令来关闭 JDWP 调试:

    #if RELEASE
    [assembly: Application(Debuggable=false)]
    #else
    [assembly: Application(Debuggable=true)]
    #endif

Android 应用程序(AndroidManifest.xml)设置

当你开始考虑部署你的应用程序时,你已经在 AndroidManifest.xml 中建立了大多数需要的设置。然而,你需要更新版本信息。请注意,版本号在安装过程中由 Android 平台使用,以确定 APK 是否是现有应用程序的更新。版本名称是自由形式的文本,可以用来以任何希望的方式跟踪应用程序版本。这可以通过打开 项目选项 对话框并导航到 构建 | Android 应用程序,或者通过双击 NationalParks/Properties/AndroidManifest.xml 来完成。以下截图显示了 Android 应用程序 设置对话框:

Android 应用程序 (AndroidManifest.xml) 设置

链接器选项

在构建过程中,Xamarin.Android 对构成应用程序的组件进行静态分析,并尝试消除不需要的类型和成员实例。控制此过程的设置可以在 项目选项 对话框中查看和设置,该对话框位于 Android 构建 部分下,如下截图所示:

链接器选项

Xamarin.Android 支持与 Xamarin.iOS 相同的链接器选项。在查看和调整 链接器选项 设置时,请确保首先从 配置 下拉菜单中选择 发布。以下链接选项可用:

  • 不链接:此选项禁用链接器,并确保所有引用的组件都包含在内,而不进行修改。这是针对 iOS 模拟器构建的默认设置。这消除了链接的耗时过程,并将大文件部署到模拟器相对较快。

  • 仅链接 SDK 组件:此选项告诉链接器仅对 SDK 组件进行操作;那些与 Xamarin.iOS 一起发货的组件。这是针对设备构建的默认设置。

  • 链接所有组件:此选项告诉链接器对整个应用程序以及所有引用的组件进行操作。这允许链接器使用更大的优化集,并产生可能的最小应用程序。然而,当链接器以这种方式运行时,由于静态分析过程中做出的错误假设,它可能会破坏你的代码的一部分。特别是,反射和序列化的使用可能会使静态分析失败。

以下表格显示了 APK 文件大小根据链接器设置的变化:

文件链接版本 PCL 版本
不链接 26.4 MB 27.5 MB
仅链接 SDK 组件 4.3 MB 4.3 MB
链接所有组件 4.1 MB 4.2 MB

覆盖链接器

在某些情况下,链接选项可能会有负面影响,例如意外删除重要的类型和/或成员。对于已经编译和链接为发布模式的程序,在分发之前进行彻底测试以识别此类问题非常重要。在某些情况下,你应该进行超出初始开发者测试的测试,并且这应该使用发布模式生成的 APK 文件进行。

如果遇到与缺失类型或定位特定方法相关的任何运行时异常,你可以使用以下方法之一来向链接器提供明确的指令。

使用属性保留代码

如果你通过测试确定链接过程移除了你的应用所需的类或方法,你可以通过在类和/或方法上使用Preserve属性来明确告诉链接器始终包含它们。

要保留整个类型,请使用以下命令:

[Preserve (AllMembers = true)]

要保留单个成员,请使用以下命令:

[Preserve (Conditional=true)]

使用自定义链接器文件保留代码

有时候,你可能无法访问源代码,但仍然需要保留特定的类型和/或成员。这可以通过自定义链接器文件实现。以下示例指示链接器始终包含特定类型的特定成员:

<?xml version="1.0" encoding="UTF-8" ?>
<linker>
  <assembly fullname="Mono.Android">
    <type fullname="Android.Widget.AdapterView">
      <method name="GetGetAdapterHandler"/>
      <method name="GetSetAdapter_Landroid_widget_Adapter_Handler"/>
    </type>
  </assembly>
</linker>

你可以通过添加一个简单的 XML 文件并将其填充与上一个示例类似的内容来向项目中添加自定义链接文件;实际上,它在项目结构中的位置并不重要。在将文件添加到项目后,选择该文件,打开属性面板,然后为构建操作选择LinkDescription,如图所示:

使用自定义链接器文件保留代码

跳过程序集

你也可以指示链接器跳过整个程序集,这样所有的类型和成员都将被保留。这可以通过以下两种方式实现:

  • 使用命令行选项linkskip,例如,--linkskip=someassembly

  • 使用AndroidLinkSkip MSBuild 属性,如下所示:

    <PropertyGroup>
        <AndroidLinkSkip>Assembly1;Assembly2</AndroidLinkSkip>
    </PropertyGroup>
    

支持的 ABIs

Android 支持多种不同的 CPU 架构。Android 平台定义了一组应用程序二进制接口ABI),对应不同的 CPU 架构。默认情况下,Xamarin.Android 假定 armeabi-v7a 适用于大多数情况。要支持额外的架构,请在Android 构建部分下的项目选项对话框中检查每个适用的选项。

支持的 ABIs

发布发布版 APK

现在你已经调整了生成发布构建所需的所有设置,你就可以发布实际的 APK 了。当我们说发布时,我们只是意味着生成一个可以上传到 Google Play Store 的 APK。以下章节将讨论在 Xamarin Studio 内部生成签名 APK 的步骤。

密钥库

密钥库是由 Java SDK 中的 keytool 程序创建和管理的安全证书数据库。您可以使用 keytool 命令在 Xamarin Studio 外部创建密钥库,或者从 Xamarin Studio 内部创建,它提供了一个与 keytool 命令交互的 UI。下一节将指导您在 Xamarin Studio 内部发布 APK 和创建新密钥库的步骤。

从 Xamarin.Android 发布

以下步骤指导您在创建签名 APK 的过程中创建新密钥库:

  1. 配置下拉框中,选择发布

  2. 从主菜单导航到项目 | 发布 Android 应用程序;注意发布 Android 应用程序向导的密钥库选择页面,如下截图所示:从 Xamarin.Android 发布

  3. 选择创建新密钥库,选择一个包含密钥库名称的位置,并输入密码并确认。示例密钥库位于项目文件夹中,名为NationalParks.keystore,密码为nationalparks

  4. 选择前进;您将看到密钥创建页面,如下截图所示:从 Xamarin.Android 发布

  5. 输入所有相关信息。本例使用nationalparks作为别名字段和密码

  6. 选择前进;您将看到发布 Android 应用程序向导的选择目标页面,如下截图所示:从 Xamarin.Android 发布

  7. 选择所需的目标目录选项,然后点击创建。Xamarin Studio 将为发布编译应用程序并生成签名 APK 文件。您应该在发布包面板中看到以下内容:从 Xamarin.Android 发布

生成的 APK 已准备好进行最终测试和潜在的分发。请确保安全并备份您的密钥库,因为它对于分发未来版本至关重要。

从 Xamarin.Android 重新发布

应用程序后续发布应始终使用原始密钥库。为此,只需在发布 Android 应用程序向导的密钥库选择页面选择使用现有密钥库

从 Visual Studio 发布

在 Visual Studio 内部发布签名 APK 基本上遵循相同的流程。要这样做,只需从主菜单导航到工具 | 发布 Android 应用程序

应用程序分发选项

分发 Xamarin.Android 应用程序与任何其他 Android 应用程序没有区别。它们可以通过所有正常渠道分发,包括应用商店、电子邮件附件、网站链接、U 盘等。

摘要

在本章中,我们回顾了与准备应用程序分发相关的内容以及实际生成签名发布 APK 文件的流程。

本章完成了我们关于 Xamarin Essentials 的书籍。我们试图为经验丰富的移动开发者提供一个高效的方法,以便他们能够快速掌握使用 Xamarin 平台开发应用程序。我们回顾了 Xamarin 架构,为 iOS 和 Android 开发了功能性的应用程序,并探讨了如何通过使用多种不同的方法和框架,在移动平台间共享代码来最大化 Xamarin 的价值。现在,你已经准备好将 Xamarin 投入使用了。

我希望你们觉得这本书是一本有用的资源,并且它激发了你使用 Xamarin 开发优秀移动应用程序的热情。

posted @ 2025-10-25 10:40  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报