-NET-移动开发-全-
.NET 移动开发(全)
零、前言
在这本书中,您将学习如何使用微软提供的 Xamarin、。net 5 和 Azure 云服务的工具集,为包括 iOS、Android 和 UWP 在内的多种平台设计和开发高度吸引人的、可维护的和健壮的移动应用。 这本书不仅提供了实践示例和演练,而且还提供了关于云计算和移动架构模式的有用见解。 您还将了解使用最新的工具和平台来管理您的移动项目的生命周期的最有效的方法,现代 DevOps 生态系统提供。
这本书是写给谁的
本书是为希望开发跨平台移动应用的移动开发人员编写的。 需要有 c#编程经验。 需要对核心元素和使用。net 开发跨平台应用有一定的了解和理解。
这本书的内容
第一章,入门。net 5.0,简要介绍。net Core,同时解释。net 基础设施的不同层。 可以与。net 一起使用的语言、运行时和扩展将被讨论和分析。
第二章,Xamarin, Mono 和。net Standard 的定义,阐述了。net Core 和 Xamarin 之间的关系。 你将了解 Xamarin 源代码是如何执行 MonoTouch 在 iOS 和 Mono 运行时在 Android。
第三章,使用通用 Windows 平台开发,讨论了允许 UWP 应用在 Windows 10 生态系统中可移植的组件,以及它们如何与。net Core 相关联。
第四章、使用 Xamarin 开发移动应用,讲解 Xamarin 和 Xamarin。 形成开发策略,我们将创建一个 Xamarin。 表单应用,我们将在本书的其余部分进行开发。 我们还将讨论可能对我们有所帮助的架构模型。
第五章,UI 开发 Xamarin 的,需要看特定的 UI 模式,允许开发人员和用户体验设计师来用户期望之间的妥协和产品要求,以创建一个平台和跨平台产品一致的用户体验。
第六章,Xamarin 定制 表格,详细介绍了 Xamarin 的定制步骤和流程。 表单不影响性能或用户体验。 将分析的一些特性包括效果、行为、扩展和自定义渲染器。
第七章,Azure 服务对于移动应用,探讨了事实有许多服务作为服务(SaaS)、平台(PaaS)或基础设施(IaaS),如通知中心,认知服务,和蔚蓝的功能, 这可以改变用户对您的应用的印象,而不需要额外的开发时间。 本章将快速概述在开发。net Core 应用时使用其中一些服务。
第八章,创建一个数据存储与宇宙 DB*,解释了宇宙范式的数据库提供了一种多模型和多 API,允许应用时使用多个数据模型存储应用数据和最合适的 API 的应用,如 SQL, JavaScript,小鬼,MongoDB。 在本章中,我们将为应用创建数据存储并实现数据访问模块。
第 9 章,创建微服务 Azure App Services,介绍了 Azure App Service 的基础知识,我们将使用 ASP 为我们的应用创建一个简单的、面向数据的后端。 通过 Azure Active Directory 提供的身份验证。 其他实现将包括离线同步和推送通知。
第十章,使用。NET Core Azure Serverless,显示了如何将 Azure 功能合并到我们的基础设施来处理数据在不同的触发,以及如何整合 Azure 功能与逻辑应用将被用作一个处理单元在我们的设置。
第 11 章,流体应用与异步模式,解释了在开发 Xamarin 应用与 ASP.NET Core 应用,任务的框架和响应式模块都可以帮助分发执行线程,并创建一个平滑和不间断的执行流。 本章将介绍与这些模块相关的一些模式,并将它们应用于应用的各个部分。
第十二章,管理应用数据,解释说,为了避免冲突和数据同步问题,开发人员必须要勤奋有关的程序实现根据手头的类型的数据。 本章将讨论使用 SQLite 和 Entity Framework Core 等产品可能的数据同步和离线存储场景,以及 Azure App Service 提供的开箱即开的离线支持。
第十三章,参与用户通知和图形 API*,简要解释了通知和图形 API 可用于提高用户参与度利用推送通知和图形 API。 我们将使用 Azure notification hub 为跨平台应用创建一个通知实现。 我们还将为我们的应用会话创建所谓的活动条目,以便我们可以创建一个可在多个平台上访问的时间轴。
第 14 章,Azure DevOps 和 Visual Studio 应用中心,展示了如何使用 Visual Studio 团队服务和应用中心建立一个完整的、自动化的管道 Xamarin 的应用将连接源存储库提交最后的商店。
第 15 章,应用遥测与应用见解,观点解释了应用是一个伟大的收集候选人 Xamarin 的遥测数据使用 Azure 托管 web 服务基础架构的应用,因为它内在与 Azure 模块的集成, 以及 App Center 遥测的连续导出功能。
第 16 章,自动化测试*,讨论了如何创建单元和编码 UI 测试,以及围绕它们的架构模式。 数据驱动的单元测试、模拟和 Xamarin UI 测试是将要讨论的一些概念。
第 17 章,部署 Azure 模块,演示了如何配置用于 Azure web 服务实现的 ARM 模板, 以及我们之前使用的其他服务(比如 Cosmos DB 和 Notification hub),这样我们就可以使用 Visual Studio Team services 构建和发布管道来创建部署。 在本章中,我们主要关注的是将配置值引入模板,并为创建登台环境做好准备。
第 18 章,CI/CD with Azure DevOps,解释了开发者如何使用 Visual Studio Team Services 提供的工具集为构建、测试和部署创建完全自动化的模板。 在本章中,我们将为 Xamarin 建立与 Azure 部署管道一致的构建和发布管道。
为了最大限度地了解这本书
这本书主要针对的是。net 开发人员,他们对 Xamarin 和。net 的使用经验很少。 与云基础设施相关的部分大量使用 Azure 云基础设施中的各种服务。 然而,熟悉 Azure 门户的基本管理概念就足够学习更高级的主题了。
对于代码示例,整本书都结合使用了 Windows 和 macOS 开发环境。 使用示例的理想设置是将 macOS 与 Windows 10 虚拟机结合使用。 这样,就可以使用来自这两个环境的示例。

实现代码遍历的 IDE 选择是 Windows 上的 Visual Studio 2019 和 macOS 上的 Visual Studio for Mac。 支持这两个平台的 Visual Studio Code 可以用来创建脚本和 Python 示例。
如果你正在使用这本书的数字版本,我们建议你自己输入代码或通过 GitHub 存储库访问代码(链接在下一节中)。 这样做将帮助您避免任何与复制和粘贴代码相关的潜在错误。
*# 下载示例代码文件
你可以从 GitHub 上的https://github.com/PacktPublishing/Mobile-Development-with-.NET-Second-Edition下载这本书的示例代码文件。 如果代码有更新,它将在现有的 GitHub 存储库中更新。
我们还可以在https://github.com/PacktPublishing/中找到丰富的图书和视频目录中的其他代码包。 检查出来!
使用的约定
本书中使用了许多文本约定。
Code in text:表示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 url、用户输入和 Twitter 句柄。 下面是一个例子:“IObserver和IObservable接口,它们构成了可观察的和所谓的反应模式的基础。”
一段代码设置如下:
namespace FirstXamarinFormsApplication
{
public partial class MainPage : ContentPage
{
InitializeComponent();
BindingContext = new MainPageViewModel();
}
}
当我们希望提请您注意代码块的特定部分时,相关的行或项以粗体显示:
public App()
{
InitializeComponent();
MainPage = NavigationPage(new ListItemView())
}
任何命令行输入或输出都写如下:
$ docker run -p 8000:80 netcore-usersapi
粗体:表示新词条、重要词汇或在屏幕上看到的词汇。 例如,菜单或对话框中的单词会像这样出现在文本中。 下面是一个例子:“ALM 过程和版本控制选项都可以在项目设置的Advanced部分中使用。”
小贴士或重要提示
出现这样的。
联系
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并发送电子邮件至customercare@packtpub.com。
Errata:尽管我们已尽一切努力确保内容的准确性,但错误还是会发生。 如果您在这本书中发现了错误,请向我们报告,我们将不胜感激。 请访问www.packtpub.com/support/errata,选择您的图书,点击勘误表提交链接,并输入详细信息。
盗版:如果您在互联网上发现我们的作品以任何形式的非法拷贝,请提供我们的位置地址或网址。 请通过copyright@packt.com与我们联系,并附上资料链接。
如果你有兴趣成为一名作家:如果你有一个你擅长的话题,并且你有兴趣写作或写一本书,请访问authors.packtpub.com。
评论
请留下评论。 一旦你阅读和使用这本书,为什么不在你购买它的网站上留下评论? 潜在的读者可以看到并使用您的公正意见来做出购买决定,我们在 Packt 可以理解您对我们的产品的看法,我们的作者可以看到您对他们的书的反馈。 谢谢你!
更多关于 packt.com 的信息,请访问packt.com。****
一、.NET 5.0 入门
。NET Core(以前称为. net vNext)一般的涵盖性术语用于微软的跨平台工具集,旨在解决集中式/改变框架的缺点(经典。net Framework)通过创建一个可移植的、平台无关的、模块化的运行时和框架。 这种分散的开发平台从 v5.0 开始取代了经典的。net 框架,它允许开发人员根据目标平台使用通用的。net 基类库(. net 标准的实现)以及各种运行时和应用模型为多个平台创建应用。
本章将简要介绍新的。net 框架,同时解释。net Core 基础设施的不同层。 . net Core, . net Standard 和 Xamarin 的结合是跨平台项目的关键,并且打开了许多以前只对 Windows 开发人员开放的大门。 创建能够在 Linux 机器和容器上运行的 web 应用的能力,以及针对 iOS、Android、通用 Windows 平台(UWP)和 Tizen 的移动应用的实现,只是旨在强调这种跨平台方法的能力的几个例子。
在本章中,我们将分析移动应用的跨平台开发工具和框架,并初步了解. net Core 开发。
以下部分将指导你开始使用。net 5.0:
- 探索跨平台开发
- 了解。NET Core
- 使用。net 5.0 进行开发
技术要求
你可以通过以下 GitHub 链接找到本章使用的代码:https://github.com/PacktPublishing/Mobile-Development-with-.NET-Second-Edition/tree/master/chapter01/src。
探索跨平台开发
术语跨平台应用开发指的是创建可以在多个操作系统上运行的软件应用的过程。 在这本书中,我们不会试图回答的问题的原因,但如何开发跨平台应用——更确切地说,我们将努力创建一个跨平台的应用使用微软提供的工具集和。NET Core。**
**在我们开始讨论。net Core 之前,让我们先看看开发多平台应用的过程。 面对跨平台需求,产品团队可以选择多条路径,引导开发人员走过不同的应用生命周期。
在本书中,我们将为各种场景定义假想的用户故事。 我们将从一个整体的用户故事开始,强调。net Core 的重要性:
“作为产品所有者,我希望我的消费者应用能够运行在 iOS 和 Android 移动平台上,以及 Windows、Linux 和 macOS 桌面运行时上,这样我就可以扩大我的覆盖范围和用户基础。”
为了满足这些需求,我们可以选择几种不同的方式来实现应用:
- 完全本机应用
- 混合应用
- 跨平台的
让我们看一看这些方法。
开发完全本机应用
遵循这条路径可能会创建最多性能的应用,并增加了开发人员对平台 api 的可访问性。 然而,这种类型的开发团队需要更广泛的技能,以便在多个平台上创建相同的应用。 在多个平台上进行开发还会增加开发人员在应用上投入的时间。
考虑在前一节中给出的场景中,我们可能需要开发客户端应用在可可和 CocoaTouch (macOS 和 iOS), Java (Android), . net (Windows),和 c++ (Linux),最终构建一个 web 服务基础设施使用我们的选择的另一个开发平台。 换句话说,这种方法实际上是实现一个多平台的应用,而不是跨平台的应用。
混合应用
本地托管 web 应用(也称为混合应用)是另一种流行的选择,对于(尤其是移动)开发人员来说。 在这个体系结构中,响应式 web 应用将被托管在目标平台上的一个瘦本机控制上。 本机 web 容器也将负责提供对本机平台 api 的 web 运行时的访问。 这些混合应用甚至不需要打包成应用包,而是打包成渐进 Web 应用(PWAs),这样用户就可以从他们的 Web 浏览器直接访问它们。 虽然开发资源的使用比本地跨平台框架方法更有效,但这种类型的应用通常容易出现性能问题。
对于手头的业务需求,我们可能会开发一个 web 服务层和一个小的单页应用(SPA),其中一部分被打包为混合应用。
原生跨平台框架
开发平台,如 React Native、Xamarin 和. net Core,为目标平台提供了急需的抽象,因此开发可以使用一个框架和一个开发工具包进行多个运行时。 换句话说,开发者仍然可以使用本地平台(例如 Android 或 iOS SDK)提供的 api,但开发是使用单一语言和框架执行的。 这种方法不仅减少了开发资源,而且还免去了为多个平台管理多个源存储库的负担。 通过这种方式,可以使用同一个源创建多个应用头。
例如,使用。net Core,开发团队可以使用相同的开发套件实现所有目标平台,从而为每个目标平台创建多个客户端应用,以及 web 服务基础设施。
在跨平台实现中,从架构上讲,应用由三个不同的层组成:
- 应用模型(消费者应用的实现层)
- 框架(可用于开发人员的工具集)
- 平台抽象(管理或运行时来托管应用)
在这种情况下,我们实际上是在追求创建一个与平台无关的应用层,该应用层将在平台抽象层上提供服务。 平台抽象层,无论我们处理的是本机 web 主机还是本机跨平台框架,都负责提供单一应用实现和多态运行时组件之间的桥梁。
. net Core 和 Mono 提供了运行时,而. net Standard 提供了框架抽象,这意味着跨平台应用可以在多个平台上实现和分发。 在移动应用上使用 Xamarin 和。net Standard 框架,在 web 基础设施上使用。net Core,可以创建复杂的支持云的本地移动应用。
正如您可以很容易地观察到的,本机跨平台框架在开发成本、性能和目标应用的本地性之间提供了最佳折衷,为开发人员提供了为多个平台创建应用的理想选择。 从这个角度来看,. net (Core)和 Xamarin 共同发展成为一个跨平台框架和运行时,已经成为移动应用最突出的开发平台之一。
我们已经讨论了实现跨平台应用的不同方法,并确定了这些方法的优缺点。 现在我们可以开始探索。net 生态系统和跨平台工具集了。
了解。net Core
为了让理解。net Core 的起源和动机,让我们从以下引文开始:
“最大限度地发挥其产品与其他软件有效结合的潜力,同时最大限度地减少对其进一步重组的任何限制的软件生产商,将成为软件行业的幸存者,该行业正围绕着商品数据的网络交换进行重组。”
——David Stutz——微软共享源公共语言基础设施的总项目经理,2004 年。
. net Core 最早可以追溯到 2001 年,当时共享源公共语言基础结构(SSCLI)是在转子的代码下被共享的(非商业用途)。 这就是 ECMA 335,即Common Language Infrastructure (CLI)标准实现。 转子可以在 FreeBSD(版本 4.7 或更新)和 macOS X 10.2 上构建。 它是这样设计的,一个薄的平台抽象层(PAL)是唯一需要将 CLI 移植到不同平台的东西。 这个版本构成了将. net 迁移到跨平台基础设施的初始步骤。
2001 年也是 Mono 项目作为一个开源项目诞生的一年,它将。net 的一部分作为开发平台基础设施移植到 Linux 平台上。 2004 年,Mono 在 Linux 上发布了最初的版本,这将导致它在其他平台上的移植,比如 iOS (MonoTouch)和 Android (MonoDroid),并最终以 Xamarin 的名义并入。net 生态系统。
这种方法背后的驱动力之一是。net 框架被设计成一个系统范围的整体框架并被分发。 仅依赖于框架的一小部分的应用需要在目标操作系统上安装完整的框架。 它不支持仅适用于应用的场景,即不同的应用可以在不同的版本上运行,而无需安装系统范围的升级。 然而,更重要的是,由于。net 框架和 Windows API 组件之间的紧密耦合,使用。net 开发的应用被隐式地绑定到 Windows 上。net Core 就是在这些激励下诞生的,为。net 开发人员打开了各种平台的大门。
最终,在 2020 年,. net Core 取代了经典的。net。 统一的。net 平台现在为开发人员提供了单一的运行时和框架,可以使用单一的代码库来创建跨平台应用。
从语义上讲,.NET 现在描述了一整套交叉开发工具的完整基础设施,这些工具依赖于公共语言基础设施和多个运行时,包括.NET Core Runtime, .NET,也被称为大 CLR, Mono 运行时和 Xamarin:

图 1.1 - .NET 生态系统
在这种设置中,. net CLI(公共语言基础设施)由基类库实现组成,基类库实现定义了受支持的运行时需要提供的标准。 基类库负责提供 PAL,该 PAL 由宿主运行时以自适应层的名义提供。 这个基础设施由编译器支持服务,如罗斯林和Mono 编译器(MCS)以及【5】即时(JIT)和【显示】****提前(AOT)编译器如 RyuJIT(.NET Core)、mTouch 和 LLVM(适用于 Xamarin.iOS),以便为目标平台生成和执行应用二进制文件。
**总的来说,. net Core 是一个快速发展的生态系统,拥有大量受支持的平台、运行时和工具。 这些组件中的大多数都可以在 GitHub 上作为。net Foundation 监督下的开源项目找到。 这种开源的增长是。net Core 今天达到顶峰的关键因素之一,它实现的 api 几乎与。net Framework 完全匹配。 这就是为什么。net 5.0 版本标志着。net 时代的结束(正如我们所知),因为在这个版本中,两个。net 框架被合并为一个,而。net Core 实际上取代了。net 本身。 现在,让我们开始在。net 5.0 中进行开发。
使用。net 5.0 开发
. net 应用可以用 Windows 平台上的 Visual Studio 和 macOS 上的 Visual Studio for Mac(继承自 Xamarin Studio)来开发。 Visual Studio Code(一个开源项目)和 Rider (JetBrain 的开发 IDE)为这两个平台以及基于 unix 的系统提供了支持。 虽然这些环境提供了所需的用户友好的开发 UI,但从技术上讲,. net 应用可以用一个简单的文本编辑器编写,并使用. net Core 命令行工具集进行编译。
. net Core CLI 中唯一的内在运行时是. net Core 运行时,它主要用于创建控制台应用,并访问完整的基类库。
闲话少说,让我们用 CLI 工具创建我们的第一个跨平台应用,并看看它在多个目标平台上的行为。 在本例中,我们将开发一个具有基本算术操作支持的简单计算器作为控制台应用。
创建与运行时无关的应用
在开发计算器时,我们的主要目标是创建一个可以在多个平台(换句话说,Windows 和 macOS)上运行的应用。 首先,我们将在 macOS 上创建安装了。net Core 的控制台应用:
$ mkdir calculator && cd $_
$ dotnet --version
5.0.100-preview.7.20366.6
$ dotnet new solution
The template "Solution File" was created successfully.
$ dotnet new console --name "calculator.console" --output "calculator.console"
The template "Console Application" was created successfully.
$ cd calculator.console
重要提示
在本例中,我们使用了控制台模板,但是还有许多其他现成的模板,比如类库、单元测试项目、ASP.NET Core,以及更具体的模板,如 Razor Page, MVC ViewStart, asp.net.NET Core Web 应用和 Blazor 服务器应用。
控制台calculator.console应用应该已经在您指定的文件夹中创建。
为了恢复与任何项目关联的 NuGet 包,您可以在命令行或终端窗口中使用dotnet restore命令,这取决于您的操作系统。
重要提示
通常,您不需要使用restore命令,因为编译已经为您完成了这些工作。 在创建模板的情况下,最后一步实际上是恢复 NuGet 包。
接下来,将以下实现复制到创建的program.cs文件中,替换Main方法:
static char[] _numberChars = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
static char[] _opChars = new[] { '+', '-', '*', '/', '=' };
static void Main(string[] args)
{
var calculator = new Calculator();
calculator.ResultChanged = (result) =>
{
Console.Clear();
Console.WriteLine($"{Environment.NewLine}{result}");
};
// TODO: Get input.
}
这里,我们将计算器逻辑委托给一个简单的计算器状态机实现:
public class Calculator
{
private int _state = 0;
string _firstNumber;
string _currentNumber = string.Empty;
char _operation;
public Action<string> ResultChanged = null;
public void PushNumber(string value)
{
// Removed for brevity
}
public void PushOperation(char operation)
{
// Removed for brevity
}
private int Calculate(int number1, int number2, char
operation)
{
// Removed for brevity
}
}
Calculate方法是实际的计算器实现,这也需要添加到Calculator类中:
private int Calculate(int number1, int number2, char operation)
{
switch(operation)
{
case '+':
return number1 + number2;
case '-':
return number1 - number2;
case 'x':
return number1 * number2;
case '/':
return number1 / number2;
default:
throw new NotSupportedException();
}
}
最后,我们只需要扩展我们的Main方法来检索用户输入。 现在,您可以将用户输入的Main方法中的占位符替换为以下内容:
while (true)
{
var key = Console.ReadKey().KeyChar;
if (key == 'e') { break; }
if (_numberChars.Contains(key))
{ calculator.PushNumber(key.ToString()); }
if(_opChars.Contains(key))
{ calculator.PushOperation(key); }
}
现在我们的应用项目已经准备好了(在编辑program.cs文件之后),我们可以构建并运行控制台应用,并开始输入如下内容:
$ dotnet run
123+1=124
这里,我们使用run命令在当前平台(macOS)中编译和运行应用。 如果导航到build文件夹,您会注意到,CLI 实际上创建了一个动态链接库(DLL)文件,而不是可执行文件。 原因是,由于没有定义其他编译选项,应用被创建为一个依赖于框架的应用。 我们可以尝试使用dotnet命令运行应用,它被称为驱动程序:
$ cd bin/Debug/net5.0/
$ ls
calculator.console.deps.json
calculator.console.pdb
calculator.console.runtimeconfig.json
calculator.console.dll
calculator.console.runtimeconfig.dev.json
$ dotnet calculator.console.dll
这里,需要注意的是,我们使用的描述是依赖于框架(在本例中,是. net Core App 5.0 运行时)。 如果我们讨论的是在。net Core 之前的。net Framework,那么这个将严格指的是 Windows 平台。 然而,在这个上下文中,它指的是一个仅依赖于框架本身,且与平台无关的应用。 为了在 Windows 上测试我们的应用,我们可以将bin文件夹复制到安装了目标框架的 Windows 机器上,并尝试运行我们的应用:

图 1.2 -运行。NET Core 应用
如果两个系统都安装了。net 5.0 运行时,Windows Console 上的应用二进制文件给出的结果与 macOS 终端完全相同。
重要提示
为了验证所需的框架已安装在目标机器上,您可以使用dotnet --info或dotnet --list-runtimes命令,这将列出目标机器上已安装的运行时。
为了测试创建的demo.dll文件的运行时独立性,我们可以尝试用 Mono 运行时运行它。 在 macOS 上,您可以尝试下面的命令来执行我们的应用:
$ cd bin/Debug/net5.0/
$ mono calculator.console.dll
如果我们要分析清楚的架构洋葱图的背景下发生了什么,你会注意到。net 运行时将代表应用的基础设施,而提供了 NET Core 应用抽象的控制台将组成应用的 UI。 虽然基础设施是由目标平台提供的,但 UI 和应用核心是跨平台的:

图 1.3 -一个平台-不可知的。net 应用
在这个图中,两个操作系统都安装了. net Runtime,它提供了. net BCL 的实现,允许在两个平台上执行相同的二进制包。
再进一步,现在让我们尝试将基础设施输入打包到应用中,并准备一个依赖于平台的包,而不是依赖于框架的包。
定义运行时和自包含的部署
在前面的示例中,我们创建了一个与操作系统无关的控制台应用。 但是,它依赖于NETCore.App运行时。 如果我们想将这个应用部署到一个没有安装。net Core 运行时和/或 SDK 的目标系统,该怎么办?
当需要发布。net Core 应用时,您可以包含来自。net Core 框架的依赖项,并创建一个所谓的自包含包。 然而,沿着这条路下去,你将需要定义目标平台(操作系统和 CPU 架构)使用运行时的标识符(掉),. net CLI 可以下载所需的依赖项,并将它们包括在你的包。
运行时可以定义为项目文件的一部分,也可以定义为publish执行期间的参数。 让我们修改我们的项目文件以包含运行时标识符,而不是命令参数:

图 1.4 -设置运行时标识符
这里,我们编辑了项目文件,以 x64 架构的 Windows10 为目标,这意味着打包的应用将只针对这个平台。
现在,如果我们要发布应用(注意,发布过程将在 macOS 上进行),它将为所定义的目标平台创建一个可执行文件:
$ nano calculator.console.csproj
$ dotnet publish
Microsoft (R) Build Engine version 16.7.0-preview-20360-03+188921e2f for .NET
Copyright (C) Microsoft Corporation. All rights reserved.
Determining projects to restore...
Restored /Users/can/Work/Book/calculator/calculator.console/calculator.console.csproj (in 6.87 sec).
calculator.console -> /Users/can/Work/Book/calculator/calculator.console/bin/Debug/net5.0/win10-x64/calculator.console.dll
calculator.console -> /Users/can/Work/Book/calculator/calculator.console/bin/Debug/net5.0/win10-x64/publish/
这里,我们使用终端编辑器根据运行时需求修改项目文件。 一旦dotnet publish命令执行完毕,publish文件夹将包含。net Core runtime 和针对 Windows 10 runtime 的框架中所有必需的包:

图 1.5 - .NET 自包含发布包
注意,一旦定义了部署目标平台,就会创建一个可执行文件,不再需要驱动程序。 事实上,在这里可执行文件的唯一目的是充当。net Core 创建的动态类库的访问点(主机)。
与之前的应用包相比,该应用包包含基础架构内容 t 以及应用核心:

图 1.6 -特定于平台的部署
一些最著名的运行时包括在三种不同架构(x86、x64 和 ARM)上的 Windows 7 到 Windows 10,多个 macOS 版本,以及各种发行版和 Linux 版本,包括 OpenSuse、Fedora、Debian、Ubuntu、RedHat 和 Tizen。 然而,考虑到我们创建跨平台应用的目标,. net 项目的目标框架定义帮助我们打包平台无关的应用二进制文件,只依赖于目标运行时,而不是平台。 现在让我们仔细看看可能的目标框架选项。
定义框架
在前面的示例中,我们一直使用。net 5.0 作为目标框架。 ,至于这个控制台应用的独立部署,这已经被证明是足够的,如果我们准备 Xamarin 或 UWP 应用中,我们可能需要使用标准的。net 的核心应用和特定于平台的目标的基础设施,比如 Xamarin.iOS。
可以使用<TargetFrameworks>项目属性更改目标平台框架。 我们必须使用分配给所需框架的名称:

图 1.7 -平台名称
在本节中,使用。net 5.0,我们开发了一个跨平台控制台应用,它可以在 Windows 和 macOS 平台上运行。 我们已经研究了同一个应用的运行时和框架依赖版本。 同一个控制台应用在 Windows 的命令提示符和 macOS 终端以及 Linux Bash 上的执行是一个令人印象深刻的景象:

图 1.8 -多平台上的。net 控制台计算器
正如您在这个示例中看到的,相同的源代码甚至相同的二进制代码在多个平台上执行,得到相同的结果。 . net 应用的这种可移植性应该展示了. net 生态系统的灵活性,它从桌面和控制台应用扩展到使用 Xamarin 进行移动开发。
总结
随着微软采用新的面向开放源代码的方法,。net 生态系统正在以指数级的速度增长。 各种运行时和框架现在都是社区驱动的项目的一部分,这些项目覆盖了原先. net 框架的大部分,而具有讽刺意味的是,原先的。net 框架注定要成为 Windows 本身的一部分。
到目前为止,我们只是简单地看了一下。net 应用模型和基础设施。 为了演示。net 的可移植性,我们创建了一个简单的计算器应用,然后针对不同的运行时和平台进行编译。
在下一章中,我们将继续讨论 Xamarin,它是。net 移动平台标准的运行时和实现的提供者。 我们将实现我们的第一个经典 Xamarin 以及 Xamarin。 表单应用。****
二、Xamarin, Mono 和 .NET 标准的定义
Xamarin 是现代. net 基础设施的应用模型实现。 作为跨平台基础设施的一部分,Xamarin 使用 Mono 运行时,而 Mono 运行时又充当. net 标准基类库/库的适配层。 通过 Mono 运行时(MonoTouch 和 MonoDroid)提供的抽象,Xamarin 可以瞄准 iOS 和 Android 等移动平台。
本章将尝试定义。net 和 Xamarin 之间的关系。 首先准备 Xamarin 开发环境并创建第一个 Xamarin 应用。 然后你会发现。net 源代码是如何在 iOS 上的 MonoTouch 和 Android 上的 Mono 运行时中执行的。 这将帮助你更好地理解。net 的跨平台特性以及它是如何在移动平台上运行的。
下面的小节将带领你实现你的第一个 Xamarin 应用:
- Xamarin 的理解
- 设置开发环境
- 创建您的第一个 Xamarin 应用
- Xamarin 的发展。 形式
- 将触角延伸到
让我们开始吧!
理解 Xamarin
Xamarin,作为一个平台,可以被认为是 Mono 项目的遗产,Mono 项目是一个开源项目,由后来建立 Xamarin 小组的一些关键人员领导。 Mono 最初是。net 面向 Linux 的公共语言基础结构(CLI)实现,它允许开发人员使用。net(2.0)框架模块创建 Linux 应用。 后来,Mono 的运行时和编译器实现被移植到其他平台上,直到 Xamarin 在微软的。net Core 生态系统中占据了一席之地。 Xamarin 套件是. net Core 的旗舰产品之一,也是跨平台开发的关键技术。
设置开发环境
作为计划使用 Xamarin 创建本地移动应用的开发人员,您有几个设置开发环境的选项。 在开发方面,macOS 和 Windows 都可以使用,使用 Visual Studio 或 Rider ide。
作为。net 开发人员,如果你正在寻找一个熟悉的环境和 IDE,最好的选择是在 Windows 上使用 Visual Studio。
为了使用与 xamarin 相关的模板和可用的 sdk,第一步是使用 Visual Studio 安装程序安装所需的组件:

图 2.1 - Visual Studio 安装程序
当你安装带有。net 组件的Mobile Development 时,所需要的 sdk(针对 Android 和 iOS)会自动安装,所以你不需要做任何额外的先决条件安装。
设置完成后,在跨平台应用部分,以及平台专用部分,即Android和iOS下,可以使用各种项目模板。 跨平台 Xamarin 应用的多项目模板将帮助您使用 Xamarin 完成项目创建过程。 表单,而可用的Android 应用和iOS 应用模板使用经典的 Xamarin 基础设施创建应用项目:

图 2.2 -跨平台应用
使用这个模板,Visual Studio 将为每个选定的平台(iOS、Android 和 UWP 之外的选定平台)创建一个公共项目(共享或。net Standard)和一个项目。 在本例中,我们将使用Shared Project代码共享策略,选择 iOS 和 Android 作为目标平台。
重要提示
需要注意的是,如果您是在 Windows 机器上开发,则需要使用 macOS 构建服务(带有 Xamarin 的 macOS 设备)。 iOS 和 Xcode 安装)需要编译和使用模拟器与 iOS 项目。
如果你在第一次编译 iOS 项目时,收到一个指向缺失 Xcode 组件或框架的错误,你需要确保 Xcode IDE 至少手动运行一次,这样你才能同意条款和条件。 这允许 Xcode 通过安装额外的组件来完成设置。
在这个解决方案中,您将拥有特定于平台的项目,以及基本的样板代码和一个包含Main.xaml文件的共享项目,该文件是一个简单的 XAML 视图。 当特定平台的项目被用来托管使用声明性 XAML 页面创建的视图时,Android 项目中的MainActivity.cs文件和 iOS 项目中的Main.cs文件被用来初始化 Xamarin。 表单 UI 框架并呈现视图。
这个 XAML 视图树使用指定的呈现器在目标平台上呈现。 它使用了页面、布局和视图层次结构:

图 2.3 - Xamarin。 表单样板应用
在本节中,我们设置开发环境,并使用为我们生成的样板应用对其进行测试。 假设您的开发环境已经准备就绪,我们可以继续并开始实现前一章中的跨平台计算器应用——首先作为一个经典的 Xamarin 应用,然后作为 Xamarin 应用。 表单应用。
创建第一个 Xamarin 应用
我们在上一节中创建的项目使用了 Xamarin。 UI 呈现形式。 虽然这可能是实现跨平台应用的最有效方式,但在某些情况下,您可能需要实现一个非常特定于平台的应用(这包括许多平台 api 和专门的 UI 组件)。 在这些类型的情况下,您可以求助于创建经典的 Xamarin。 iOS 和/或 Xamarin 的。 Android 应用。 让我们实现这两个计算器应用。
Xamarin on Android - Mono Droid
我们将从 Xamain.Android 开始实现。 应用将使用带有标准计算器布局的单个视图,并且我们将尝试重用我们在前一章中创建的控制台计算器的计算器逻辑。 废话少说,让我们开始创建我们的应用:
-
For the Android application, we will add a new project to our calculator solution, namely
calculator.android. For this example, we will use the Blank Android App template:![Figure 2.4 – Blank Android App]()
图 2.4 -空白 Android 应用
这将为 Xamarin 创建一个标准样板应用项目。 Android 的单一视图和相关的布局文件。 如果你打开已创建的
Main.axml文件,设计器视图将被加载,它可以用来创建我们的计算器:![Figure 2.5 – Xamarin.Android Designer View]()
图 2.5 - Xamarin。 Android 设计视图
-
Now, let's start designing our calculator view by creating the result. When handling the Android XML layout files, you are given the option to either use the designer or the source view. When using the designer view to create the welcome view, you have to drag and drop the text view control and adjust the alignment, layout, and gravity properties for the label.
使用源代码视图,你也可以粘贴以下布局声明,看看应用在 Android 平台上运行时的样子:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/txtResult" android:layout_width="match_parent" android:layout_height="100dp" android:gravity="center|right" android:textSize="30sp" android:layout_margin="5dp" android:text="0" /> </LinearLayout>这将是上面的视图,我们将使用它来显示计算结果。
-
接下来,让我们创建数字垫。 为了创建按钮行,我们将使用具有水平方向的
LinearLayout。 您可以将以下行插入结果TextView的右边,以便它包含第一行按钮:<LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal"> <!-- The buttons will sit here > </LinearLayout> -
Next, you can use simple
Buttoncontrols within the horizontalLinearLayoutto create the number pad:<Button android:id="@+id/number7" android:text="7" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:textSize="20sp" /> <Button android:id="@+id/number8" android:text="8" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:textSize="20sp" /> <Button android:id="@+id/number9" android:text="9" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:textSize="20sp" /> <Button android:id="@+id/opDivide" android:text="/" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:textSize="20sp" />对其他行重复此操作,根据按钮文本标签更改
android:id属性,直到您到达最后一行(即 4 5 6 *和 1 2 3 -)。 每一行将使用一个单独的具有水平方向的LinearLayout。 最后一行将只有三个按钮(即 0 = +)。 为了使两个列的按钮跨度为 0,可以使用2的layout_weight。最后的布局应该类似于如下所示:
![Figure 2.6 – Android Calculator Layout]()
图 2.6 - Android 计算器布局
-
Next, we will modify the generated Android activity code to introduce the calculator logic, while keeping the IDs of the controls we have added to the layout in mind.
为了保持对我们添加的控件的引用,我们应该创建私有字段:
Button _btnNumber0, _btnNumber1, _btnNumber2, _btnNumber3, _btnNumber4, _btnNumber5, _btnNumber6, _btnNumber7, _btnNumber8, _btnNumber9; Button _btnOpAdd, _btnOpSubstract, _btnOpMultiply, _btnOpDivide, _btnOpEqual; TextView _txtResult; -
在此代码的执行过程中,一旦内容视图被设置为布局,我们就可以检索该视图并使用
FindViewById函数将其分配给私有字段。 为了截获该活动生命周期事件并分配相应的引用,您可以使用OnCreate方法:protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); Xamarin.Essentials.Platform.Init(this, savedInstanceState); SetContentView(Resource.Layout.activity_main); _btnNumber0 = FindViewById<Button>(Resource.Id.number0); // … // Removed for brevity } -
现在,从控制台应用复制
Calculator类,并在OnCreate方法中实例化它。 -
在此之后,我们可以将事件处理程序分配给按钮来执行计算逻辑:
-
最后,对于计算器逻辑的结果委托,我们可以简单地将
result值分配给_txtResult字段:_calculator.ResultChanged = (result) => { _txtResult.Text = result; };
现在可以运行应用并测试计算器的实现。
Xamarin 的。 Android 平台的功能有点像。net Core。 不像 Xamarin 的。 iOS 对代码生成没有限制,所以 Mono Droid 运行时执行是使用 JIT 编译器完成的,JIT 编译器负责提供作为应用包一部分的 IL 包。 Mono Droid 运行时以的原生代码形式存在于应用包中,它取代了。net Core 运行时:

图 2.7 - Xamarin Android 运行时
在探索了 Android 平台之后,让我们用 Mono Touch 在 iOS 平台上重新创建计算器应用。
Xamarin 在 iOS - Mono Touch 上
正如您在 Android 示例中看到的,在一个经典的 Xamarin 应用中,视图是使用本地 SDK 组件和工具集创建的。 这样,自定义本地控件就可以引入到视图中,而无需为通用 UI 基础设施创建指令(即自定义呈现器)。 以 Xamarin 为例。 在 iOS 中,开发者可以使用 iOS 名称空间和 UI 元素创建应用视图,也可以使用 xib 或带后台控制器的故事板。
现在,让我们重新创建我们在 Xamarin 上做的计算器应用的实现。 Android Xamarin.iOS:
-
We will start by creating our project. Use the iOS Single View App application template listed under the iOS section:
![Figure 2.8 – Creating a Xamarin.iOS Project]()
图 2.8 -创建 Xamarin iOS 项目
这个模板将创建一个简单的 Xamarin。 iOS 应用,带有单个视图、关联的故事板和主视图的控制器。
-
创建项目之后,打开
Main.storyboard文件开始创建用户界面。 对于 Windows 或 macOS 上的 Visual Studio,使用 Visual Studio 设计器和 Xcode 设计器,这取决于你的开发环境设置。 如果你使用 Windows,你需要有一个 Xamarin 构建代理,它已经安装了 XCode 和 Xamarin,并且已经与你的 Windows 环境配对。 -
若要创建按钮控件,请从工具箱中拖放按钮控件。 从布局开始,您可以使用大小为90作为按钮的宽度和高度。 在本例中,我们使用深色背景色和黑色作为底色。
-
To complete the UI, we will need to introduce a label control that should appear on top of the keypad layout.
现在,如果我们编译并运行应用,您将得到与 Xamarin 类似的视图。 上一节提到的 Android:
![Figure 2.9 – Calculator Layout for iOS]()
图 2.9 - iOS 计算器布局
-
现在我们的 UI 已经准备好了,我们可以开始介绍计算器逻辑了。 我们将从创建 UI 控件的所谓出口开始,这样它们就可以从视图控制器中引用。 您可以通过简单地为 UI 控件分配一个名称来创建一个出口。 在命名控件时,我们将使用与 Android 应用中相同的字段名(即
. _btnNumber0和_btnOpAdd)。 在命名了 UI 上的所有控件之后,您可以打开ViewController.designer.cs文件来验证是否已经为所有控件创建了出口。 -
Next, we will introduce the calculator logic. In order to import the application logic, copy and paste the calculator class into the
ViewController.csfile. Now, in theViewDidLoadmethod, we can copy and paste the same event handling logic that we had for the Android sample:base.ViewDidLoad(); _calculator = new Calculator(); _btnNumber1.TouchDown += (_, __) => _calculator.PushNumber("0"); // … Removed for brevity _calculator.ResultChanged = (result) => { _txtResult.Text = result; };正如您可以看到的,这里唯一的变化是使用
TouchDown事件而不是Click事件来将操作传播到应用逻辑。
现在可以构建并运行应用。 行为将与应用的 Android 版本相同。
使用 Xamarin 的。 iOS,在编译过程中,我们创建的项目,与 c#和。net(标准)模块首先是编译成一个微软中间语言(MSIL),就像任何其他。net 项目,然后用 AOT 编译编译为本机代码。 此时,最关键的组件之一是 monotouch 运行时,它作为位于 iOS 内核之上的适配层,允许. net 标准库访问系统级函数。 在编译期间,就像应用代码一样,monotouch 运行时库以及。net 标准包被链接并转化(com)成本地代码。
重要提示
由于 iOS 上的代码生成限制,AOT 编译只在编译后的包被部署到实际设备时才需要。 对于其他平台或在 iOS 模拟器上运行应用时,使用 JIT 编译器将 MSIL 编译为本地代码——不是在编译时,而是在运行时。
下面的图表概括了 Xamarin 的 APT 和 LLVM 的编译过程。 iOS 应用:

图 2.10 - Xamarin iOS 编译
此时,您可能已经注意到 Xamarin 之间的相似之处。 Android 和 Xamarin 的。 iOS 平台,其中之一是应用域实现。 在这两个示例中,我们都使用了先前为控制台应用示例实现的相同应用逻辑。 为了重用这个逻辑,我们复制了计算器类的实现。 对于我们的解决方案的可维护性,如果我们能够重用这个实现,将是很有帮助的。 在下一节中,我们将讨论使用. net 作为这些平台之间的公共基础来创建可重用组件的可能性。
使用。net 和 Xamarin
即使 Xamarin 和/或。net Core 目标平台(平台 api)如果作为平台无关的框架具有相同的设置、功能和功能,它们也会被视为,但这些目标平台彼此都是不同的。 适应层(. net Standard 的实现)允许我们,作为开发人员,以同样的方式对待这些平台。
在。net 模块统一和标准化之前,连同共享项目,跨平台兼容性是通过目标平台上实现的功能的公分母来维护的。 换句话说,每个选定平台上的可用 api 组成了一个概要文件,该概要文件确定了这些平台可以使用的功能子集。 这些平台无关的项目用于实现应用逻辑,然后被打包到所谓的可移植类库(pcl)中。 pcl 是跨平台项目的重要组成部分,因为它们可以创建和共享在多个平台上执行的应用代码:

图 2.11 - Portable Class Library
在某种程度上,由于各种平台上的。net API 实现都(几乎)聚合到同一个子集中,一组标准的。net API 被定义为跨平台实现的通用实现基础——。net standard。 作为一个简单的类比,. net Standard 可以被认为是用于访问由目标平台运行时实现的平台 api 的接口。 在。net 5 中,这个子集现在被定义为。net 平台,这使得它成为一个真正的跨平台框架。
使用. net(标准),我们可以创建一个共享的应用核心项目,它可以被 Xamarin 经典和。net 控制台应用引用。 这允许我们创建一个可测试的与平台无关的逻辑应用,然后可以将其作为一个独立的库进行测试。
您可以使用. net Library项目模板创建。net 标准库。 一旦您选择了这个模板,您还需要选择net5.0作为目标框架。 现在您可以将Calculator类实现复制并粘贴到这个项目中,并从。net 计算器和 Xamarin 中引用它。 iOS 和 Xamarin 的。 Android 应用。
尽管这有助于提高业务逻辑的可维护性,但在 Xamarin 经典实现中,我们仍然需要处理两个独立的 ui。 为了为多个平台创建和管理一个通用的声明性 UI,我们可以使用 Xamarin.Forms。
Xamarin。 形式
在 Android 和 iOS 的例子中,我们遵循了几乎相同的实现方法,它由三个步骤组成,如下:
- 声明 UI 元素。
- 创建应用域逻辑。
- 将应用逻辑集成到 UI 中。
这两个平台之间的主要区别在于如何执行第一步。 这就是 Xamarin。 表单可以帮助开发人员。
Xamarin 的。 表单极大地简化了在两个完全不同的平台上使用相同的声明性视图树创建 UI 移动应用的过程,尽管这些平台上的本地方法实际上几乎完全不同。
从 UI 渲染器的角度来看,Xamarin。 表单提供本机呈现,在编译时(已编译的 XAMLs)和运行时(运行时呈现)使用相同的工具集有两种不同的方式。 在这两个场景中,在 XAML 布局中声明的页面布局-视图层次结构都是使用渲染器呈现的。 渲染器可以被描述为视图抽象在目标平台上的实现。 例如,iOS 上的 label 元素的渲染器负责将 label 控件(以及它的布局属性)转换为 UILabel 控件。
然而,Xamarin 的。 表单不能仅仅归类为一个 UI 框架,因为它提供了各种开箱即开的模块,这些模块对于大多数移动应用项目都是必不可少的,比如依赖服务和信使服务。 作为创建 SOLID 应用的主要模式之一,这些组件提供了在特定于平台的实现上创建抽象的工具,从而统一跨平台体系结构以创建跨多个平台的应用逻辑。
此外,数据绑定的概念,是的心脏和灵魂 Model-View-ViewModel(MVVM)实现,可以直接引入在 XAML 层面,节省开发人员不必【显示】创建自己的数据同步逻辑:****
**1. In order to demonstrate the capabilities of Xamarin.Forms, we will implement our calculator application using Xamarin.FormFirst step will be to create the Xamarin.Forms application using the multi-project template for a Blank Forms App:

图 2.12 -创建 Xamarin 表单应用
-
这将创建三个名为
calculator.forms的项目(Xamarin)。 组成通用申请书)、calculator.forms.Android、calculator.forms.iOS。 -
现在已经创建了项目,我们可以从
calculator.forms项目中添加对calculator.core项目的引用(来自前面的部分)。 这将允许我们重用计算器逻辑。 -
接下来,我们需要在公共应用项目下创建
MainPageViewModel类。 我们将在这个类中集成计算器逻辑:public class MainPageViewModel : INotifyPropertyChanged { private Calculator _calculator = new Calculator(); private string _result; public event PropertyChangedEventHandler PropertyChanged; public MainPageViewModel() { } } -
视图模型需要有两个命令来处理数字推送和操作推送操作。 将这两个声明添加到类:
public Command<string> PushNumberCommand { get; set; } public Command<char> PushOperationCommand { get; set; } -
最后,视图模型还应该声明一个
Result字段。 这应该使用PropertyChanged事件处理程序将更改传播到视图:public string Result { get => _result; set { _result = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Result))); } } -
现在我们可以初始化命令,并将结果委托分配给构造函数中的计算器实例:
PushNumberCommand = new Command<string>(_ => _calculator.PushNumber(_)); PushOperationCommand = new Command<char>(_ => _calculator.PushOperation(_)); _calculator.ResultChanged = _ => { Result = _; }; -
现在,让我们将这个视图模型作为绑定上下文分配给主视图:
public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); BindingContext = new MainPageViewModel(); } } -
We can now create our UI elements and bind the appropriate commands to our buttons. We will start by creating our
Gridlayout (copy and paste the following XAML intoMainPage.xaml):<?xml version="1.0" encoding="utf-8"?> <ContentPage xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="calculator.forms.MainPage" <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> </Grid> </ContentPage>这将创建一个五行四列的网格。
-
Insert the result label into the first row and make sure it spans for four columns:
```
<Label FontSize="50" HorizontalTextAlignment="End" Grid.Row="0" Grid.ColumnSpan="4"
Text="{Binding Result}" />
```
注意绑定上下文的`Result`属性的绑定设置。
- Next, we will create the buttons for the calculator's keypad. Use the following example to create the buttons on the grid with the correct column and row values:
```
<Button Text="7" BackgroundColor="DarkGray" TextColor="Black" Grid.Row="1" Grid.Column="0"
Command="{Binding PushNumberCommand}" CommandParameter="7" />
```
确保每个数字按钮的`Command`属性绑定到绑定上下文的`PushNumberCommand`属性是很重要的。
- 对操作按钮重复此过程:
```
<Button Text="7" BackgroundColor="DarkGray" TextColor="Black" Grid.Row="1" Grid.Column="0"
Command="{Binding PushNumberCommand}" CommandParameter="7" />
```
这就是计算器的实现。 我们的应用现在已经准备好进行第一次测试运行。
通过重新审视我们的洋葱结构,我们可以很容易地看到如何使用 Xamarin。 Forms 在平台之间扩展了应用的可移植部分:

图 2.13 - Xamarin 与 Xamarin。 表单
在本节中,我们使用 Xamarin.Forms 创建了一个针对 iOS 和 Android 的跨平台应用,该应用使用单个声明性 UI。 除了 UI 实现是在两个平台之间完全共享的这一事实之外,使用数据绑定还大大降低了应用的复杂性,从而创建了健壮和可维护的移动应用。 这里,我们只针对 iOS 和 Android,但也可以使用标准或扩展 sdk 将平台覆盖范围扩展到其他平台,同时对 UI 布局进行最小的修改。
扩展范围
最后,既然我们在讨论 Xamarin,那么提到 Xamarin 和/或 Xamarin 是很重要的。 表单不会将开发人员绑定到 Android 和 iOS 手机或平板设备上。 通过使用 Xamarin 和 Xamarin。 形式,开发人员可以瞄准各种设备,从简单的可穿戴设备,如智能手表,到物联网设备和家用电器。
当为 iOS 或基于 android 的设备开发应用时,可以使用完全相同的工具集,而更专门的平台(如 Tizen)可以构成一个目标平台,考虑到.NET 标准实现本身就存在:

图 2.14 -其他目标平台
来源:(https://developer.tizen.org/development/training/overview#type/ [CC BY-SA 3.0(https://creativecommons.org/licenses/by-sa/3.0)]))
Tizen 实现也是 Xamarin 使用。net 的一个很好的例子。 窗体和 Linux 内核。
如您所见,Xamarin 和. net 提供了必要的基础设施和抽象,以便跨平台应用可以分布到各种平台上。
总结
在这一章中,我们学习了 Xamarin,它是。net 支持的主要运行时之一,以及如何使用它为多个平台创建移动应用。 我们使用 Xamarin 探索了两种不同的跨平台开发方法。 而经典的 Xamarin 方法允许开发人员直接与本地组件 Xamarin 交互。 表单提供了一种更通用和可维护的方法。 我们还看到了 Xamarin 基础设施如何扩展到其他平台,如可穿戴设备和 Tizen,以及通用 Windows 平台(UWP)。
在下一章中,我们将深入了解 UWP,以及它如何为执行跨平台开发项目的。net 开发人员做出贡献。 UWP 作为跨平台。net 项目中最成熟的成员,可以为开发人员提供一个完全独立的开发市场。**
三、通用 Windows 平台开发
通用 Windows 平台(UWP)是一个通用 API 层,它允许开发人员为各种平台创建应用,从台式电脑到 HoloLens 等小众设备。 与 Xamarin 设置相比,UWP 应用与。net 框架和运行时组件的耦合程度更高。 UWP 使用了两套完全不同的。net 框架:。net Native 和。net Core。 在这里,. net Core 充当 BCL 库,而. net Native 是应用模型的一部分。
本章将讨论允许 UWP 应用在 Windows 10 生态系统中可移植的组件,以及它们如何与。net 相关联。 我们将使用 Xamarin 为通用 Windows 平台重新创建前一章中的计算器示例。 形式,然后尝试识别 Xamarin 之间的区别。 从 XAML 的角度看表单和 UWP。 我们还将看看如何在 UWP 中使用. net Native。
以下部分将帮助您创建您的第一个 Xamarin 应用:
- 通用 Windows 平台简介
- 创建 UWP 应用
- 了解 XAML 分歧
- 使用。net 标准和。net 本机
- 使用平台扩展
让我们开始吧!
介绍通用 Windows 平台
Windows,在 Windows 8(和 Windows 运行时)发布之前,就公开了一组扁平的 Windows api 和 COM 扩展, . net 模块依赖于这些函数,其中包含了该 API 层的平台调用(P-Invoke)语句,以利用其操作系统级功能。
Windows 运行时(WinRT)提供了一个更易于访问和管理的开发界面,可用于多种开发语言。 WinRT 可以用于常见的。net 语言(包括 c#和 VB),以及 c++和 JavaScript。
使用 WinRT 创建的通用基础,UWP 提供了微软生态系统中急需的多个平台的融合。 开发人员能够使用相同的 SDK 为各种设备创建应用,这些设备都是深奥的目标。 使用 UWP 开发工具,具有共享模块和用户界面的应用可以针对桌面设备、游戏机和增强现实设备,以及移动和物联网实现:

图 3.1 -通用 Windows 平台
每个设备家族都允许 UWP 中可用的 api 的子集,并且由开发人员决定他们想要实现的平台。 此外,每个平台都引入了仅适用于该平台的扩展 api。 这些设备族之间的差异可以通过平台扩展来处理,这些扩展可以在编译时包含在你的项目中,也可以在运行时执行设备族检查。
UWP 不仅应该作为一组开发工具来评估,而且应该作为一个真正的应用平台来评估。 作为一个平台,它强加了一些关于运行时环境应该如何处理应用的安全策略。 更具体地说,应用沙箱模型,这是其他移动平台上的一个常见概念,也是 UWP 强加的。 即使是为 UWP 编写的桌面应用也应该遵守安装和执行策略,以便标准化用户的安装过程,并通过划分应用来保护运行时。 最后,由于该平台的标准化,公共应用商店可以用于多个平台。
既然我们理解了 UWP 是什么,我们可以看看它与 Xamarin 的联系。 在下一节中,我们将扩展 Xamarin。 带有 UWP 目标的表单示例。
创建 UWP 应用
在一个跨平台的。net Core 上下文中,UWP 依赖于。net 框架本身。 然而,. net 框架确实实现了。net 标准,因此跨平台应用的可移植模块可以被 UWP 应用使用。 换句话说,与 Xamarin 实现类似,可以从 UWP 应用中提取共享(可能与平台无关)的应用代码,从而只保留本机 UI 实现作为 UWP 特定的模块。 反过来,UWP 项目可以作为任何涉及。net Standard 和/或 Xamarin 的移动开发工作的一部分。
当实现本地 UI 时,开发人员有两个本质上相似的选项,这取决于 Xamarin 项目中现有的项目架构:
- 您可以使用本机 XAML 方法创建 UWP UI(也就是说,在特定于平台的项目中创建用户界面,并且只共享业务逻辑)。
- 您可以使用 Xamarin 创建一个 UWP 目标。 为平台依赖关系创建并保留特定于平台的项目。
使用我们之前的 Xamarin 和 Xamarin。 窗体应用,我们可以添加 UWP 项目,以便我们可以部署我们的应用到 Windows 10 设备:

图 3.2 -创建 UWP 项目
现在已经创建了项目,我们可以引用与平台无关的共享(或。net 标准)项目(即calculator.core)并重用业务逻辑。 Xamarin 的。 窗体,我们还可以包括窗体项目(即calculator.forms)和引导 Xamarin。 表单应用。 Xamarin 的引导。 表单应用与安装 Xamarin 一样简单。 form NuGet package for UWP 并加载 Xamarin。 以前创建的表单应用。
用于 UWP 应用渲染 Xamarin。 我们创建的表单视图,我们需要安装 Xamarin。 窗体包,并确保所有目标平台项目都安装了相同的版本:

图 3.3 -添加 Xamarin Nuget 形式包
现在已经安装了表单包,我们可以修改MainPage.xaml文件和MainPage.xaml.cs来引导表单布局。 首先,我们将MainPage视图转换为 Forms 页面:
<uwp:WindowsPage
x:Class="calculator.forms.uwp.MainPage"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:calculator.forms.uwp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uwp="using:Xamarin.Forms.Platform.UWP"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid />
</uwp:WindowsPage>
这里,我们使用了来自 Xamarin 的uwp名称空间。 将MainPage转换为 Xamarin 的窗体包。 特定于平台的页面形式。
现在,我们将装载 Xamarin。 表单应用:
public sealed partial class MainPage : WindowsPage
{
public MainPage()
{
this.InitializeComponent();
LoadApplication(new calculator.forms.App());
}
}
现在运行应用可能会导致一个异常,说明 Xamarin。 表单应该已经初始化。 初始化可以包含在OnLaunched事件重写方法中,该方法可以在App.xaml.cs文件中找到:
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
Xamarin.Forms.Forms.Init(e);
//… Removed for brevity
}
现在,运行应用将显示与之前平台相同的 UI,但作为一个 UWP 应用:

图 3.4 - UWP 平台上的计算器
正如您所看到的,使用最少数量的平台代码,我们就能够将 Windows 平台作为应用的目标之一。 使用 Xamarin 经典方法且只使用视图模型,以及使用 UWP 工具集创建本地 UI,都可以实现同样的效果。
然而,如果我们想要使用 XAML 为 UWP 创建一个“完全”的本地应用用户界面呢? 为了在 XAML 中创建 UWP 应用 UI,我们需要使用稍微不同的控件和布局结构。
理解 XAML 差异
Xamarin 的。 表单和 UWP 可以使用可扩展应用标记语言(XAML)来使用声明性 UI 页面。 它最初是作为 WindowsPresentation Foundation 的一部分引入的,从。net 3.0 开始,已经在。net 应用中广泛使用。
虽然两个开发平台都提供了类似的 UI 元素,但它们使用的控件和布局略有不同,这可能会导致在创建面向 iOS/Android(使用 Xamarin.Forms)和 UWP 的跨平台应用时,UI 不一致。
让我们来看看布局结构:

图 3.5 - UWP 和 Xamarin 表格布局
与布局相似的,控件也有一些细微的变化。 通过观察这些平台上使用的控制方式,你可以很容易地发现细微的差别:

图 3.6 - UWP 和 Xamarin 表单控件
这两种 XAML 实现之间的差异源于 UWP 继承了 Windows Presentation Foundation 的遗产,而 Xamarin。 表单是独立开发的,以统一 Android 和 iOS 的视图层次结构。
为了演示这两种 XAML 语法之间的差异,我们可以考虑下面的 Xamarin。 表单布局:
<StackLayout>
<Label Text="{Binding Platform, StringFormat='Welcome to {0}!'}"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
</StackLayout>
这个视图将在绑定到Platform参数的屏幕中央显示一些欢迎文本。 这种布局结构将是相同的 iOS, Android,和 UWP 平台,考虑到 Xamarin。 使用表单呈现引擎。
然而,如果我们想要使用 UWP 的本地控件来实现我们在 UWP 平台上显示的相同视图,它必须被翻译为如下:
<StackPanel VerticalAlignment="Center">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Platform,
Converter={StaticResource FormatConverter},
ConverterParameter='Welcome to {0}!'}" />
</StackPanel>
注意,StackLayout被翻译成StackPanel,而TextBlock被翻译成TextBlock。 另外,将HorizontalOptions和VerticalOptions属性分别改为HorizontalAlignment和VerticalAlignment属性。 另一个很大的区别是,UWP 不支持Binding标记扩展的StringFormat属性。 为了支持字符串格式,我们需要创建自己的值转换器。
重要提示
Window Community Toolkit 是一个帮助函数和自定义控件的集合,可以简化开发者在创建 UWP 应用时的任务。 该工具包已经包含了常用的转换器,其中一个叫做StringFormatConverter。
从架构的角度来看,使用 UWP XAML 视图类似于在 Android 应用中使用原生 XML 视图,或者在 Xamarin 上为 iOS 应用使用故事板。 虽然这两种 XAML 语法是相似的,但是用于呈现这些视图的基础结构是完全不同的。 每种方法都可以根据所选择的开发策略来使用(本地 vs .表单),同时考虑到 UWP 本质上是一个. net 应用模型,在 Xamarin 中使用本地 UWP 视图。 表格申请仍然可以容纳没有太多麻烦。 类似地,UWP 也有两个. net 编译器变体,可以在编译期间用于应用运行时。
使用。net 标准和。net 原生 UWP
正如我们之前看到的,UWP 使用。net Framework 符合。net 标准。 . net 的实现标准的。net 框架内被用作共同基类库(BCL),核心公共语言运行时(【显示】核心 CLR)负责执行模块与。net 实现标准的定义。 除了。net Core 和。net Standard,另一个。net 概念对通用 Windows 应用来说是非常宝贵的,那就是。net Native。
. net 原生提供了一组工具,这些工具负责从。net 应用为 UWP 生成原生代码,绕过中间语言(IL)**。 使用。net 原生工具链,。net 标准类库,以及公共语言运行时基础设施模块,如垃圾收集,被链接到更小的动态链接库(类似于 iOS 和 Android 的 Xamarin 构建过程)。
为了启用本机编译,您需要为当前配置(例如,Release x64)启用. net 本机工具链,该工具链将用于准备应用中的appx包和appx包。 这需要为支持的架构(ARM,x86和x64)创建:

图 3.7 - .NET 原生工具链
在链接过程中,执行以下几个操作:
- 使用反射和元数据的代码路径被替换为静态本机代码。
- 消除所有元数据(在适用的地方)。
- 链接出未使用的第三方库,以及。net Framework 类库。
- 用主要包含垃圾回收器的重构版本替换完整的公共语言运行库。
因此,类似于. net (Core)运行时应用(具有指定的平台目标),UWP 应用可以通过将这些依赖关系转换为应用本身的本地引用来从对. net 框架的直接依赖关系中清除出来。 这反过来又反映了应用的性能和可移植性增强。
另一个允许 UWP 应用模型的核心部分可移植的 UWP 特性是它的平台扩展。 接下来,我们将看看 UWP 扩展,它提供了对 UWP 生态系统中特定于平台的 api 的访问。
使用平台扩展
如前所述,UWP 支持多种设备。 每个设备都执行它自己的。net 标准和 UWP 应用模型的实现。
然而,这个完整 API 层的表面区域可能并不总是适用于目标平台。 UWP 应用模型包含特定于这些设备的一个子集的 api。 事实上,这些类型的 API 模块在核心 UWP SDK 中被留作占位符方法,而实际的实现包含在扩展模块中,可以在你的 UWP 应用中引用:

图 3.8 - UWP 平台扩展
如果不添加特定的 SDK,开发人员就只能使用通用的 api。 如果不添加扩展模块,某些特定于平台的方法很可能抛出NotImplementedException或类似的问题,因为这些方法的实际实现只存在于平台扩展库中。
在包含目标平台扩展后,开发人员还负责执行方法和事件的运行时检查,以查看当前设备运行时是否支持这些 api。 开发人员可以使用各种ApiInformation方法,如IsTypePresent、IsEventPresent、IsMethodPresent和IsPropertyPresent。
例如,要检查当前设备是否支持CameraPressed事件(它可能出现在移动设备上,但不太可能在桌面 PC 上支持),我们需要求助于IsEventPresent:
bool isHardwareButtons_CameraPressedAPIPresent = Windows.Foundation.Metadata.ApiInformation.IsEventPresent ("Windows.Phone.UI.Input.HardwareButtons", "CameraPressed");
这个运行时检查也可以在契约级别执行,以查看是否支持一组事件或用于执行某个动作的其他类成员:
bool isWindows_Devices_Scanners_ScannerDeviceContract_1_0Present = Windows.Foundation.Metadata.ApiInformation.IsApiContractPresent ("Windows.Devices.Scanners.ScannerDeviceContract", 1, 0);
这样,开发人员就可以避免应用的行为和对需求的遵从性既不能预先确定也不能预测的情况。
正如您所看到的,UWP 不仅仅是集成到 Xamarin 中的应用模型。 将基础架构作为跨平台应用模型的一部分; 它还通过可扩展性 sdk 为各种特定于设备的功能提供可扩展性选项。
总结
总的来说,UWP 是。net 家族中最复杂的成员之一。 与 Xamarin 非常相似。 在本质上形成架构,并利用类似的编译/执行过程,但在某些基本方面(如 XAML 语法和应用生命周期)有所不同。 然而,正如我们在实现计算器应用时看到的,它可以很容易地包含在 Xamarin 和 Xamarin 中并与之一起执行。 在不增加开发时间或维护成本的情况下形成项目。 我们看到的可扩展性 sdk 还向我们展示了能够将应用交付给各种 uwp 的额外好处。
在下一章中,我们将集中讨论 Xamarin 和 Xamarin。 我们可以利用的形式和不同的建筑模型。**
四、使用 Xamarin 开发移动应用
在使用 Xamarin 进行跨平台开发时,重要的是要理解应用源代码不能完全跨平台。 Xamarin 应用的平台无关模块各不相同,这取决于应用的内容以及所使用的开发方法。 Xamarin classic 和 Xamarin。 表单是(主要)用于创建 iOS 和 Android 平台的本地应用的两种不同方法。 虽然 Xamarin 经典使用了更原生的方法,但实际上是将原生平台实现策略迁移到。net 生态系统 Xamarin。 Forms 为本机 UI 实现提供了一个额外的抽象层。
在本章中,我们将学习 Xamarin 和 Xamarin。 制定开发策略并创建 Xamarin。 形式应用,我们将在本书的剩余部分开发。 我们还将讨论与表示层以及其他层相关的架构模型和设计模式。
下面的章节将指导你使用 Xamarin 框架和工具集实现一个跨平台的本地移动应用:
- 在 Xamarin 和 Xamarin 之间做选择。 形式
- 组织 Xamarin 的。 表单应用项目
- 选择表示架构
- 有用的架构模式
在本章结束时,您将能够设置 Xamarin 的初始结构。 表单应用与表示模型以及主要体系结构。
技术要求
你可以在本书的 GitHub 库中找到本章将要使用的代码:https://github.com/PacktPublishing/Mobile-Development-with-.NET-Second-Edition/tree/master/chapter04。
选择 Xamarin 和 Xamarin。 形式
Xamarin 作为运行时和框架,为开发人员提供了创建跨平台应用所需的所有工具。 在这个任务中,一个关键目标是用最少的资源和时间创建一个代码库; 另一个是降低项目的维护成本。 这就是 Xamarin。 形式进入画面。
正如我们之前在第 2 章定义 Xamarin、Mono 和。net Standard中所解释的,通过使用 Xamarin 的经典方法,开发人员可以创建本地应用。 使用这种方法,我们并不真正担心创建跨平台应用的问题,因为我们正在创建一个应用,因为所有目标平台都使用相同的开发工具和语言。 在这种情况下,目标平台之间的共享组件将仅限于业务逻辑(即视图模型)和数据访问层(即模型)。 但是,如果我们处理的是消费者应用,业务逻辑和数据访问层的实现对于移动应用来说就不那么重要了。
在云连接的移动应用中,作为主流面向消费者的套件的一部分,我们通常希望实际的域实现被转移到基于云的服务中。 然后,这些服务将通过针对特定平台的服务 façade 提供。 这样,移动应用只负责通过网关 API façade 执行简单的服务调用,该网关被标记为下游微服务 bundle 上的客户端网关,如下图所示:

图 4.1 -云连接客户端服务后端
在基础设施的设置类似,其中每个应用(也就是说,多个移动应用,以及 web 应用)-标记为客户在前面的图中可以受益于一个定制的移动 API 网关(即端网关),该平台实现相互偏离, 主要是因为单独的 UI 层。 换句话说,扩展共享业务逻辑和数据访问层并不能真正增加共享代码的数量。
一般的经验法则是,Xamarin 经典应用建议与具有关键特性的应用一起使用,这些特性依赖于它们所运行的平台(外围 api、内在 UI 组件、性能需求等等)。 但是,对于具有云服务后台的主流移动应用,使用 Xamarin.Forms 可能是更好的选择。
Xamarin 的。 表单框架的目标是标准化 UI 实现过程,同时保持应用在多个平台上的原生性。 本质上,就是使用 Xamarin 创建的应用。 表单使用来自目标平台的本地 UI 元素呈现。 事实上,编译和链接之后,Xamarin 应用包与 Xamarin 没有什么不同。 窗体应用的任何给定的目标平台。
简而言之,就是 Xamarin。 表单应用可能更适合面向消费者的、云连接的应用,因为实际的业务关注点和相关领域是在服务层实现的,而移动应用只负责提供对服务层的访问,提供最直观和最吸引人的用户界面。
组织 Xamarin。 表单应用项目
即使是最简单的云连接的移动应用也可能发展到无法管理的规模。 为了控制项目的可维护性,您需要以适当的结构组织应用的不同层。 让我们看看 Xamarin 的不同层。 表单应用以及如何组织这些层以获得更好的可维护性。
开发 Xamarin 时。 表单应用,应用的要领包括目标平台项目。 这些项目也认识 NAS 负责人,它们充当初始化 Xamarin 的工具。 表单框架和应用,它们还包含本地呈现或 API 实现。 此外,我们将拥有一个包含 Xamarin 的平台无关的项目。 形式的观点。 与平台无关的项目还将包含平台抽象,以便自定义组件可以在特定于平台的项目上实现。
随着项目的增长,开发人员将需要创建一个单独的项目,该项目将只包含视图模型和平台无关的服务的实现。 在这种情况下,项目将成为单元测试过程的主要目标,因为该层不直接依赖于 UI 元素或平台服务。 此外,可以使用一个单独的项目在服务层和客户端应用之间共享数据传输对象(DTO)模型。 在这样的设置中,整体架构布局看起来类似如下:

图 4.2 - Xamarin。 项目结构形式
前面的图显示了如何将这些组件分组到单独的项目中,以及它们的依赖结构是什么样子的。 如左侧所示,我们有平台管理项目,其中包含对本机平台的引用,而在右侧,我们有. net 标准实现,它构成了应用中与平台无关的部分。 依赖层次结构中的最高元素是原生项目。 在层次结构的底部,我们有公共包,它包含各种实用程序、数据契约和对应用服务的可能抽象。
在某些需要测试特定于平台的 api 的实现中,使用特定于平台的单元测试,这些测试在目标平台上而不是在开发平台本身上执行。 这些测试也可以是平台集成测试。 此外,您可能会看到自动 UI 测试被添加到移动解决方案中,这些测试将作为功能测试的一部分执行。
在 Xamarin 中考虑这个结构。 表单应用,让我们看看下面的用户故事:
“作为产品所有者,我想为我们的跨店购物平台创造一个移动解决方案,这样目标消费群可以扩展到 iOS 和 Android 的移动用户。”
在这个故事中,正如我们前面提到的,我们有一个基于云的服务后端实现,它已经被一个已建立的 web 应用使用。 产品所有者正在考虑将后端公开给一组移动应用。
让我们通过创建初始解决方案来开始实现这个故事。 遵循以下步骤:
-
Create an empty solution called
ShopAcross.Mobilethat will contain the mobile application projects using the Blank Solution project template under the Miscellaneous section:![Figure 4.3 – Visual Studio – New Solution]()
图 4.3 - Visual Studio -新解决方案
-
创建一个名为
Client的解决方案文件夹。 -
创建 Xamarin 的。 使用
Blank Forms App创建项目。 应用名称使用ShopAcross,项目名称使用ShopAcross.Mobile.Client。 -
Now, create a .NET Standard Library project that will contain the DTO contacts for service communication called
ShopAcross.Service.Data.在更大的项目中,这个 DTO 包可能以 NuGet 包的形式交付,但是在这里,让我们假设我们手动创建数据契约。
-
Next, create the application core project using the .NET Standard Library project template called
ShopAcross.Mobile.Core.我们将使用这个项目来存储视图模型。 由于在这个项目中,我们可能会使用一些在数据绑定中使用的原语,所以我们还应该添加一个对
Xamarin.FormsNuGet 包的引用。 -
现在,从
ShopAcross.Mobile.Core项目的中,向ShopAcross.Service.Data项目添加一个项目引用。 -
将来自
ShopAcross.Mobile.Client、ShopAcross.Mobile.Client.iOS和ShopAcross.Mobile.Client.Android项目的其他引用添加到ShopAcross.Mobile.Core中。
最终的结构应该类似如下:

图 4.4 - Xamarin 表单项目
在本节中,我们了解了组成 Xamarin 的元素。 表单应用。 我们仔细研究了这些元素的功能以及它们是如何相互依赖的。 最后,使用这些关于不同层的信息,我们为示例移动应用创建了初始解决方案结构。
选择表示架构
既然我们已经准备好了项目结构并将应用划分为上下文相关的层,那么我们就应该为应用如何将从数据源检索的数据呈现给用户建立一个结构模型。 让我们仔细看看在使用表示层时可以利用的不同结构模型。
在开发跨平台移动应用时,选择表示架构可能是最关键的决策之一。 视图和业务逻辑实现应该考虑所选模式所需的体系结构概念。
模型-视图-控制器(MVC)实现
iOS 和 Android 平台本质上都被设计为使用模型-视图-控制器(MVC)模式的派生。 如果我们处理本机应用,这将是最符合逻辑的路径使用 MVC调节控制器为 iOS 和【显示】Model-View-Presenter(【病人】MVP)或略派生版本,然后Model-View-Adapter【t16.1】(MVA)模式为 Android。 严格的 MVC 模式以单向数据流为目标,并简化了实现:********

图 4.5 -经典 MVC 流程
MVC 模式的诞生是对单一责任原则的回应。 在此模式中,View(UI 实现)组件负责显示从模型(服务层)接收到的数据。 在前一个图的中,这个流程用Updates标签进行了注释。 然后,视图负责将用户输入委托给控制器。 从某种意义上说,用户本质上与控制器进行交互(也就是说,他们使用控制器)。 然后控制器通过操作模型来传达用户所要求的更改。
虽然它被广泛用于 web 应用,但通常,它的派生版本用于移动和桌面应用。 这种模式的衍生品包括 MVA 和 MVP。
MVA 架构(或调节控制器),适配器之间充当中介和模型,并负责定义策略,一个或多个视图组件,以及充当观察者这些 UI 组件:****
**
图 4.6 -中介控制器
在 MVC 实现中,当同时使用经典和中介模式时,控制器成为应用的核心和灵魂。 需要意识到模型,以及查看(紧密耦合),因为它实现了事件战略视图(用户输入)作为战略实施者和观察者。
让我们在为上一节中创建的应用实现登录视图的同时演示这个模式:
-
首先,我们需要创建视图。 在
ShopAcross.Mobile项目中使用名为LoginView的 XAML 设计组件创建内容页面:<ContentPage xmlns=http://xamarin.com/schemas/2014/forms xmlns:x=http://schemas.microsoft.com/winfx/2009/xaml x:Class="ShopAcross.Mobile.Client.LoginView"> <ContentPage.Content> <StackLayout VerticalOptions="CenterAndExpand" Padding="20"> <Label Text="Username" /> <Entry x:Name="usernameEntry" Placeholder="username"/> <Label Text="Password" /> <Entry x:Name="passwordEntry" IsPassword="true" Placeholder="password" /> <Button x:Name="loginButton" Text="Login" /> <Label x:Name="messageLabel" /> </StackLayout> </ContentPage.Content> </ContentPage> -
接下来,创建两个条目(即用户名和密码)、一个用于登录的按钮和一个标签字段,我们将使用它来显示登录函数的结果。
-
为了支持该视图的控制器实现,该控制器将处理字段验证,以及登录和注册操作,我们必须将入口和按钮组件暴露给关联的控制器:
public partial class LoginView : ContentPage { // private LoginViewController _controller; public LoginView() { InitializeComponent(); // _controller = new LoginViewController(this); } internal Entry UserName { get { return this.usernameEntry; }} internal Entry Password { get { return this.passwordEntry; }} internal Label Result { get { return this.messageLabel; }} internal Button Login { get { return this.loginButton; }} } -
现在,使用引用 t 创建控制器到视图类:
public class LoginViewController { private LoginView _loginView; public LoginViewController(LoginView view) { _loginView = view; } } -
控制器应该为由于用户交互而在视图上引发的各种事件定义事件处理方法。 向控制器类添加以下事件处理程序方法:
public class LoginViewController { // … cont'd void LoginClicked(object sender, EventArgs e) { // TODO: Login _loginView.Result.Text = "Successfully Logged In!"; } void UserNameTextChanged(object sender, TextChangedEventArgs e) { // TODO: Validate } } -
既然已经创建了事件处理程序方法,我们就可以订阅视图引用上的事件了。 您可以将这些订阅调用添加到构造函数:
public LoginViewController(LoginView view) { _loginView = view; _loginView.Login.Clicked += LoginClicked; _loginView.UserName.TextChanged += UserNameTextChanged; } -
最后,您可以注释掉
LoginView类中的控制器引用。
如您所见,尽管我们可以进一步重构代码,以便在控制器和视图之间插入抽象层,但两者之间存在强耦合。 为了解决这个问题,我们可以使用Model-View-ViewModel(MVVM)设置。
Model-View-ViewModel (MVVM)实现
在对紧密耦合问题的回应中,另一个 MVC 的衍生版本随着Windows Presentation Foundation(WPF)的发布而诞生。 由 Microsoft 提出的想法是由控制器(在本例中为ViewModel)公开出口,出口与视图元素耦合的概念。 这些插座及其耦合的概念被称为 abinding。 在 MVVM 模式中,绑定替换了View和ViewModel之间的双向通信路径,而ViewModel仍然与Model紧密耦合:

图 4.7 - MVVM 模式
使用绑定,我们可以减少视图模型对视图元素内部工作的了解,然后让应用运行时处理如何同步View和ViewModel的出口。
除了绑定之外,“命令”的概念也变得非常重要,因此我们可以将用户操作委托给ViewModel。 命令是一个有状态的单个执行单元,它表示用于执行该函数的函数和数据。
使用前面的例子,我们可以创建一个视图模型来演示使用 MVVM 的好处:
-
Let's start by creating a class that will represent the user interaction points on our view. Create a new class under the
ShopAcross.Mobile.Coreproject calledLoginViewModel:public class LoginViewModel { private string _userName; private string _password; private string _result; public LoginViewModel() { } }请注意,类定义了三个字段,它们代表视图上的两个输入字段,分别是用户名和密码,以及一个显示登录结果的结果字段。
-
接下来,让我们粗略地暴露这些字段的一些属性:
public class LoginViewModel { // … cont'd public string UserName { get { return _userName; } set { if (_userName != value) { _userName = value; } } } public string Password { get { return _password; } set { if (_password != value) { _password = value; } } } public string Result { get { return _result; } set { if (_result != value) { _result = value; } } } } -
最后添加
Login功能:public class LoginViewModel { // … cont'd public void Login() { //TODO: Login Result = "Successfully Logged In!"; } } -
At this stage of the implementation, we can bind the
Entryfields from our view to the view-model. To assign the view-model to the view, we need to useBindingContextfrom ourLoginView, making sure theShopAcross.Mobile.Corenamespace is added with ausingstatement:public partial class LoginView : ContentPage { public LoginView() { InitializeComponent(); //_controller = new LoginViewController(this); BindingContext = new LoginViewModel(); } }现在我们不再使用控制器,您还可以删除暴露控件的出口(内部属性)。
-
Now, let's set up the bindings for the
Entryfields:<Label Text="Username" /> <Entry x:Name="usernameEntry" Placeholder="username" Text="{Binding UserName}" /> <Label Text="Password" /> <Entry x:Name="passwordEntry" IsPassword="true" Placeholder="password" Text="{Binding Password}" /> <Button x:Name="loginButton" Text="Login" /> <Label x:Name="messageLabel" Text="{Binding Result}" />在执行此示例时,您将注意到包含单向数据流(即,
UserName和Password字段仅从View传播到视图模型)的条目的值的行为符合预期; 在关联字段中输入的值将按预期推到属性中。
视图到视图模型绑定上下文的设置也可以在 XAML 中完成。 <ContentPage.BindingContext>可用于将绑定上下文设置到视图模型,该视图模型是使用正确的clr名称空间(例如<core:LoginViewModel />)初始化的。 为了使其像预期的那样工作,视图模型类需要有一个无参数的构造函数。
为了提高绑定的性能并减少用于某个绑定的资源,重要的是要定义绑定的方向。 有多种BindingMode可供选择,如下:
- 单向:在
ViewModel更新值时使用。 它应该反映在观点上。 - OneWayToSource:当视图改变一个值时使用。 值更改应该被推到视图模型中。
- two - way:数据流是双向的。
- OneTime:绑定上下文被绑定后,数据同步只发生一次,并且数据已经从视图模型传播到视图。
有了这个信息,用户名和密码字段应该使用OneWayToSource绑定,而消息标签应该使用OneWay绑定模式,因为结果只由视图模型更新。
下一步是设置要执行的函数的命令(即登录和注册)。 从语义上讲,命令由一个方法(包含它的数据和/或参数)和一个状态(它是否可以被执行)组成。 该结构由ICommand接口描述:
public interface ICommand
{
void Execute(object arg);
bool CanExecute(object arg);
event EventHandler CanExecuteChanged;
}
Xamarin 的。 窗体,该接口有两种实现:Command和Command<T>。 使用这些类中的任何一个,都可以完成命令绑定。 例如,为了将Login方法作为命令公开,请遵循以下步骤:
-
首先,在
LoginViewModel类private Command _loginCommand; public ICommand LoginCommand { get { return _loginCommand; } }中声明我们的
Commandpr 属性 -
In order to initialize
_loginCommand, use the following constructor:public LoginViewModel() { _loginCommand = new Command(Login, Validate); }注意,我们使用了两个操作来初始化该命令。 的第一个参数,这是
Action类型,是实际的方法执行,而第二个参数,是Func<bool>的类型,是一个方法引用,返回一个指示Boolean该方法能否被执行。 -
Validate方法的实现应该像这样:k -
最后,为了完成实现,当
UserName或Password字段发生变化时,发送CanExecuteChanged事件:public string UserName { get { return _userName; } set { if (_userName != value) { _userName = value; _loginCommand.ChangeCanExecute(); } } } public string Password { get { return _password; } set { if (_password != value) { _password = value; _loginCommand.ChangeCanExecute(); } } } -
Now, add a command binding to the Login button so that the created
LoginCommandis utilized by the view:<Button x:Name="loginButton" Text="Login" Command="{Binding LoginCommand}"/>现在,如果您要运行应用,您将看到命令的禁用和启用状态如何反映在 UI 上:
![Figure 4.8 – MVVM command binding]()
图 4.8 - MVVM 命令绑定
现在命令已经设置好了,我们只有结果消息绑定,它是,仍然没有按预期工作。 此时,点击Login按钮将更新视图模型数据,但用户界面不会反映此数据更改。 这样做的原因是该字段应该用
OneWay绑定绑定(源中的更改应该反映在目标上)。 这样做的主要要求是源(视图模型)应该实现在System.ComponentModel名称空间中定义的INotifyPropertyChanged接口。INotifyPropertyChanged是将绑定上下文上的更改传播到视图元素的基本机制:/// <summary> Notifies clients that a property value has changed. </summary> public interface INotifyPropertyChanged { /// <summary> Occurs when a property value changes. </summary> event PropertyChangedEventHandler PropertyChanged; }一个简单的实现需要调用带有当前正在更改的属性的
PropertyChanged事件。重要提示
如果您对一个属性所做的更改影响了多个数据点(例如,分配一个列表数据源更改了项计数属性),那么视图模型将负责为 UI 需要使其失效的所有属性触发相同的事件。
-
通过引入接口实现并在
Result属性的 setter 上使用事件触发器,我们应该能够看到Login命令的结果:public class LoginViewModel : INotifyPropertyChanged { // … cont'd public event PropertyChangedEventHandler PropertyChanged; public string Result { get { return _result; } set { if (_result != value) { _result = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof (Result))); } } } }
这就完成了登录视图的视图模型实现。 现在我们可以为LoginViewModel类创建单元测试,并使用额外的异常处理和其他必要的扩展来扩展实现。
单向数据流的架构模式
在我们已经实现的 mediationController 和 MVVM 模式中,随着视图复杂性的增加,数据流路径变得难以想象的复杂。 这种复杂性主要是由于视图模型的双工特性,这降低了视图模型的可预见性。 在一个复杂的视图中,即使视图模型仍然保留固有的有限自动机属性,因为随着引入的每个双向绑定,状态的数量呈指数增长,代码的可维护性直线下降。
作为对这个复杂性问题的响应,在数据驱动的应用项目中,可以使用其他几个 MVC 衍生工具来强制执行单向数据流。 简单地说,这些模式旨在实现单向数据流,即将应用数据视为不可变项,并通过用户输入和/或数据触发器将应用状态更新为新状态。
通量结构是对双向数据问题的响应之一。 在 Flux 模型中,应用状态是在不可变数据存储中管理的。 然后,应用域可以查询这些数据存储,以及修改这些存储(也就是说,通过创建新的不可变版本),使用 reducer:

图 4.9 -通量结构
Xamarin 的。 形式,如前面所示图中,通量的构件(如数据存储、操作(也称为异径接头),和调度员可以引入现有的 MVVM 模式。 通过创建数据存储来管理应用数据,并将数据从数据存储定向到视图模型,可以实现单向数据流。 为了处理用户交互,我们可以创建小的操作块,这些操作块由命令触发,以分派 reducer,然后更改数据存储。**
*Xamarin 中使用的其他单向架构模型。 表单是模型-视图-更新(由 f#语言普及)和模型-视图-意图(特别是在 Android 应用中使用)。
在本节中,我们仔细研究了用于演示结构的不同架构模式。 我们还使用 MVC 和 MVVM 实现了应用的第一个视图。 我们创建了一个设置,视图负责创建视图模型; 然而,通过使用反转(IoC)的实现,例如依赖注入或服务定位器模式,视图可以取消此职责。 在下一节中,我们将仔细研究 Xamarin 中其他突出的架构模式。 形式的生态系统。
有用的架构模式
Xamarin 的。 表单作为一个框架,包含帮助开发人员实现知名架构模式的模块,这样他们就可以创建可维护且健壮的应用。 在这一节中,我们将仔细研究一些最重要的架构/设计模式; 也就是控制反转、事件聚合器和装饰器。
控制反转
IoC 是一种设计原则,其中为类的依赖关系选择具体实现的职责被委托给外部组件或源。 通过这种方式,类与它们的依赖关系是分离的,这样它们就可以被替换/更新而不需要太多麻烦。
这一原则最常见的实现是使用服务定位器模式,其中创建一个容器来存储具体的实现。 这通常是通过适当的抽象来注册的,如下图所示:

图 4.10 - Xamarin 的控制反转 形式
Xamarin 的。 表单提供了DependencyService,当您为平台无关的需求创建平台特定的实现时,尤其有用。 我们还必须记住,它只能在 Xamarin 中使用。 形式平台项目; 否则,我们将创建一个不必要的 Xamarin 依赖。 表单库。
正如您所看到的,控制反转不仅有助于减少模块间的耦合,而且还允许我们抽象特定于平台的组件。
事件聚合器
事件聚合器(也称为发布者/订阅者或 Pub-Sub)是一种消息传递模式,其中消息/事件的发送方(称为发布者)不编程将消息直接发送到单个/特定的接收者(称为订阅者),而是: 将发布的消息分类到类中,而不知道可能存在哪些订阅者(如果有的话)。 然后通过所谓的聚合器将消息汇集起来,并传递给订阅者。 这种方法在保持有效的消息传递通道的同时,提供了发布者和订阅者之间的完全解耦。
事件聚合器在 Xamarin 中的实现。 表单框架通过MessagingCenter完成。 MessagingCenter公开了一个简单的 API,它由两个用于订阅者的方法(即订阅和取消订阅)和一个用于发布者的方法(发送)组成。
我们可以演示如何应用事件聚合器模式,方法是为应用中的服务通信或身份验证问题创建一个简单的事件接收器。 在这个事件接收器(它将是我们的订阅者)中,我们可以订阅从各种视图模型(假定它们都实现相同的基类型)接收到的错误消息,并使用友好的对话框提醒用户:
MessagingCenter.Subscribe<BaseViewModel> (this, "ServiceError", (sender, arg) =>
{
// TODO: Handler the error
});
在视图模型中,未处理异常的事件中会出现以下情况:
public void Login()
{
try
{
//TODO: Login
Result = "Successfully Logged In!";
}
catch(Exception ex)
{
MessagingCenter.Send(this, "ServiceError", ex.Message);
}
}
MessagingC``enter允许我们连接到不同的层,几乎在应用中创建一个消息总线。 如图所示,在 Xamarin 中已经为您提供了这个体系结构模式的基础结构。 形式框架。
装饰师
装饰器模式是一种设计模式,它允许行为动态地添加到单个对象中,而不影响来自同一类的其他对象的行为。 Xamarin 的。 表单利用此模式使用平台无关的可视元素(XAML 中使用的视图),并将呈现程序附加到这些元素上,这些元素定义了在目标平台上呈现它们的方式(创建本机特定于平台的控件)。 Xamarin 的成分。 表单元素不会改变渲染器的行为,反之亦然,它允许开发人员创建自定义的渲染器,并将它们附加到视图中,而不影响其他可视元素。 下图显示了渲染器类与装饰器模式交互的抽象:

图 4.11 -装饰模式在 Xamarin。 形式
如你所见,每个 Xamarin。 表单控件由共享/标准域中的视图定义表示。 该控件引用视图呈现器的特定抽象,然后在目标平台上使用给定视图呈现的特定于平台的实现。 在这种设置中,派生的自定义控件实现立即继承了基本控件的视图呈现器关联。 自定义渲染器,然而,需要导出一个特定的共享控件定义,以便布局引擎使用。
类似的方法也用于创建所谓的效果,即附加于现有视觉元素及其对应元素的简单行为修饰符。
总结
在本章中,我们深入探讨了实现 Xamarin 应用的架构方面,并为 MVVM 应用建立了基础。 为了演示不同表示架构的实现,我们使用 MVC 和 MVVM 模式实现了登录视图。 正如您所看到的,虽然这两种模式都可以用于 Xamarin。 表单应用,每种表单应用对视图和控制器之间的交互都有不同的理解,每种表单应用都有优点和缺点。 此外,其他模式,如 MVU 和 Flux,可以进一步改善项目的可维护性。 我们还简要地浏览了实现 Xamarin 应用可能需要的其他几个模式。 您现在应该能够创建一个 Xamarin。 从头创建应用,并使用适当的应用基础结构和体系结构为您的下一个项目设置样板解决方案。
在下一章中,我们将使用标准 Xamarin 实现应用的初始视图。 表单组件。 在本书的其余部分中,我们将尝试实现本章中讨论的应用组件。***
五、使用 Xamarin 开发 UI
Material Design 是 Android 应用最突出的 UI 模式,苹果的人机界面指南,以及 UWP 的流体 UI 语言,这使得用户体验设计师和开发人员难以决定统一的应用设计。 要考虑的因素包括但不限于,用户对目标平台的期望,以及与品牌相关的产品所有者的要求,无论平台是什么。
在本章中,我们将演示如何设置应用布局,同时利用一些重要的决策因素来实现一致的用户体验设计。 然后,我们将创建简单的应用页面,并使用标准导航服务将它们连接起来,以创建导航层次结构。 我们还将后退一步,看看用于创建应用页面层次结构的 Xamarin Shell 实现。 我们还将浏览 Xamarin。 表单查看元素,并了解如何使用 MVVM 将元素连接到应用数据,而不需要将它们与业务逻辑强耦合。
下面的主题将带领你创建我们的示例应用的框架:
- 应用的布局
- 实现导航结构
- 实现壳导航
- 使用 Xamarin 的。 表单和本地控件
- 创建数据驱动的观点
- 集合视图
在本章结束时,您将能够创建有吸引力的数据驱动的 Xamarin。 表单页面使用开箱即用的布局和视图,并在它们之间设置各种导航层次结构。
技术要求
你可以在本书的 GitHub 库中找到本章将要使用的代码:https://github.com/PacktPublishing/Mobile-Development-with-.NET-Second-Edition/tree/master/chapter05。
应用布局
在本节中,我们将看看某些 UI 模式,这些模式允许开发人员和 UX 设计人员在用户期望和产品需求之间创建一个折中方案。 通过这样做,可以在所有平台上实现一致的 UX。
对于设计人员和开发人员来说,应用生命周期中最激动人心的阶段可能是设计阶段。 在这个阶段,有许多因素需要仔细考虑,从而避免任何草率的决定。 简单地说,应用的设计应该满足以下要求:
- 消费者的预期
- 平台规则
- 开发成本
让我们开始吧!
消费者期望
应用的特性集应该真正与客户的期望相关联。 布局选项和导航层次结构应该服务于应用的目的,同时牢记用户交互需求。 根据需求,应用可以设计为单页面应用,也可以设计为具有复杂导航页面层次结构的应用; 内容可以是纯文本,也可以使用富媒体元素; 上下文操作可以提供对用户操作的访问,或者可以将交互放置在多个应用页面上。
一般来说,在视图级别,应用视图包含三种不同类型的元素:内容、导航和操作。 创造这些元素的最佳混合是开发者和设计师的责任。 在大多数现代应用中,内容元素具有多种功能,内容元素成为用户交互点和导航元素。
例如,让我们假设,作为我们的设计需求,我们有一个项目列表,其中包含一个图像、一个简单的标题和一个描述(即一个简单的两列两行模板)。 此外,我们需要实现与这些内容项相关的操作。 在这种情况下,元素的设计、流和行为实际上取决于这些行为是什么。 为了演示内容元素上的不同交互,让我们使用以下用户存储 y:
“作为一个用户,我希望看到一个项目列表,并与它们交互,这样我就可以对这些项目执行某些操作。”
列表视图和恭维页面的交互模型可能有很大不同。 让我们仔细看看几个将内容项用作交互元素的模型:
- 列表/详细信息视图:这个列表可以是用于详细信息导航的公共项目列表视图,其中项目详细信息页面展示了项目可用的操作。 在本例中,我们假设用户在对一个项目执行必要的操作之前需要查看项目的详细信息(例如,如果仅使用清单很难区分这些项目):

图 5.1 -内容元素作为导航项
这个列表视图中的内容项表现为导航项,在详细信息屏幕上,用户可以执行与这些特定项相关的操作。 但是,对于一个简单的操作,用户将需要更改视图并丢失列表的上下文。
- Item Context Actions:如果一个动作可以在列表视图上执行,我们不需要将用户带到二级视图,可以让他们直接与列表交互来执行这些动作:

图 5.2 -内容元素作为上下文操作项
在此实现中,列表视图充当单个交互上下文,并直接在项上执行操作。 换句话说,内容元素被用作操作元素,而不是用于导航。
-
List Context Actions: Finally, if there are actions available for execution on multiple content elements, the content items themselves could be used with additional styling (for example, an overlay of a checkmark on the image element). This implementation would replace the possible use of checkboxes or radio buttons for the economical use of design space. This would further improve the user experience since we would, again, be allowing the user to interact with the content itself rather than the user input elements.
重要提示
此外,为了减少不必要的控制元素和修饰,iOS 和 Windows 平台在创建内容元素时强调书法的使用。 当使用字体变化时,可以调整某些内容元素的视觉优先级以提供正确的信息。 例如,在前面的示例中,元素的标题是使用较小的字体创建的,因此强调了描述。
平台要求
当处理跨平台移动应用时,开发人员需要创建满足多种设计界面的应用,以及针对多种操作系统和习惯用法的指导方针。 平台命令指的是开发人员和设计人员需要找到一个折衷的平台指南,以创建跨平台的统一 UI 体验。
重要提示
Xamarin 中的习语是如何定义形态因子的。 表单应用。 它可以用于创建各种手机形态因素的特定视图,以及平板电脑,桌面和电视,甚至为可穿戴设备的设计表面,如 Tizen 手表。
在处理不同的习惯用法时,应该考虑目标设备功能、设计界面和输入方法。
为了充分利用桌面和移动设备上的 web 应用空间,开发人员经常使用响应式设计技术,也可以应用到 Xamarin 应用:
- 流体布局:在流体布局中,项目被堆叠在水平列表中,根据可用的水平空间,项目可以根据需要占用尽可能多的行来列出它们:

图 5.3 -习语的流动布局
- 方向改变:在屏幕较宽的设备上,在水平列表中列出的项目可以垂直堆叠在屏幕较小的设备上,屏幕的高度相对于宽度更大。
- 重组:一般元素布局可根据可用空间完全重组。 例如,一个视图可以在横向模式下使用三个片段,而在纵向模式下,两个片段可以合并为 into:

图 5.4 -为习语重组 UI 元素
- 调整:富媒体内容元素,以及文本内容,可以调整大小,以充分利用可用的设计空间:

图 5.5 -为习语调整 UI 元素的大小
此外,正如我们前面提到的,设备功能在应用如何响应用户输入方面扮演着重要的角色。 例如,硬件返回按钮就是这种设计考虑的一个很好的例子。 如果我们正在设计针对 Android、iOS 和 UWP 平台的移动应用,我们需要记住,只有 Android 提供硬件或软件返回按钮。 这种能力,或者在其他平台上缺乏这种能力,使得在第二层应用视图中包含后退导航元素至关重要。 同样,如果我们设计一个应用在移动设备上使用(比方说,在 iOS 和 Android),但与此同时,应用应该运行在电视与 Tizen 或 Android 操作系统,输入方法和用户通过导航屏幕如何成为一个至关重要的设计因素。
开发成本
最后,技术可行性是客观分析应用设计需求的另一个重要方面。 在某些情况下,创建一个自定义控件来模拟 web 应用的开发成本超过了添加到相同应用的本地副本的业务或平台价值。
每个移动平台的 Xamarin 和 Xamarin。 表单目标提供不同的用户体验和一组不同的控件。 Xamarin 的。 表单在这组本机控件上创建一个抽象,以便在特定平台上使用本机视图呈现相同的抽象。 在这种情况下,尝试引入新的设计元素或自定义控件,这些元素在外观上有本质上的不同,但它们的行为却彼此相似,这可能会带来代价高昂的后果。
例如,如果应用的 web 对等体为某个首选项使用复选框,那么在本例中使用的移动视图将是一个切换开关。 坚持使用复选框将意味着额外的开发时间,以及目标平台上不理想的用户体验。 类似地,使用复选框进行(多重)选择而不是突出显示所选内容可能会导致特定移动平台和平台用户的 UX 退化。
如您所见,在设计应用布局时需要考虑几个因素。 当你在设计游戏时,你应该牢记这些用户体验因素。 换句话说,移动应用 UX 设计并不是简单地缩减应用在 web 或桌面平台上的功能。 到目前为止,我们已经了解了用户对应用的期望、平台要求和开发成本。 找到这些因素之间的最佳折衷可以为所有涉众提供理想的应用。 然而,应用设计决策并没有到此为止。 在下一节中,我们将演示移动应用中的不同导航策略。
实现导航结构
在开始开发之前,要做的一个主要决定是决定应用的导航层次结构。 一般来说,这个决定应该在用户体验设计阶段就考虑到。
根据应用的需求和目标用户,可以采用不同的方式设计导航层次结构。 其中一些导航策略可以总结如下:
- 单页视图
- 简单的导航
- 多页视图
- 主/详细视图
让我们开始吧!
单页视图
在单页视图中,顾名思义,单页视图用于内容和可能的用户交互,而操作要么在此视图上执行,要么在操作表单上执行。 根据设计需求,可以使用单页实现来实现该视图,例如ContentPage或TemplatedPage:

图 5.6 - Xamarin 表单页面类型
ContentPage是最常用的页面定义之一。 使用这种页面结构,开发人员可以自由地在内容页面的内容定义中包含任何布局和视图元素。
现在,让我们用下面的 g 用户故事来扩展我们的样例应用的需求:
“作为一个用户,我希望有一个最近添加到商店的项目列表,这样我可以很容易地获知新产品。”
为了实现这个需求,我们将在应用中修改MainPage,并使用以下步骤将列表视图添加到该页面:
-
打开
ShopAcross解决方案,创建一个名为HomeView的新内容页面(使用Forms ContentPage XAML模板): -
打开
App.xaml.cs文件,通过将MainPage设置为HomeView:public App() { InitializeComponent(); MainPage = new HomeView(); }的新实例来修改构造器
-
Before adding the list view and the related content items, let's designate the content area and the list context-related action items toolbar:
<?xml version="1.0" encoding="UTF-8"?> <ContentPage Title="Home" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="ShopAcross.Mobile.Client.HomeView"> <ContentPage.ToolbarItems> <!-- Removed for brevity --> </ContentPage.ToolbarItems> <ContentPage.Content> <!-- Removed for brevity --> </ContentPage.Content> </ContentPage>这里,正在使用的内容容器是
Content和Toolbar项。 它们将分别用于创建项目的列表视图和工具栏操作按钮。 -
现在,让我们用一个空的模板添加列表视图。 使用
ContentPage.Content插入我们的ListView:<ListView ItemsSource="{Binding RecentProducts}" SeparatorVisibility="None"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <!-- TODO --> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> -
现在,为列表视图项插入
ViewCell定义: -
在这个阶段,应用将无法工作,因为缺少绑定和绑定上下文。 为了解决这个问题,让我们在
ShopAcross.Mobile.Core项目中创建一个基本绑定数据对象(即BaseBindableObject):public class BaseBindableObject : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void SendPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } -
现在,将下面的代码复制粘贴到核心项目中的视图模型类(即
ProductViewModel)中: -
现在可以将
HomeView的视图模型添加到HomeViewModel类:public class HomeViewModel: BaseBindableObject { public HomeViewModel() { RecentProducts = new ObservableCollection< ProductViewModel>(GetRecentProducts()); } public ObservableCollection<ProductViewModel> RecentProducts { get; } = new ObservableCollection<ProductViewModel>(); public IEnumerable<ProductViewModel> GetRecentProducts() { yield return new ProductViewModel { Title = "First Item", Description = "First Item short description", Image = "https://picsum.photos/800?image=0" }; //... Removed for brevity } } -
最后,创建一个新的实例
HomeViewModel,并将其分配给HomeView:public HomeView() { InitializeComponent(); BindingContext = new HomeViewModel(); }的类
BindingContext
现在,运行应用将产生一个使用已定义项模板显示列表视图的内容页面。
ContentPage是TemplatedPage的衍生,而TemplatedPage是另一种可用于 Xamarin 的页面类型。 表单应用。 TemplatedPage允许开发人员为TemplatePage(即ContentPage)创建基本样式,以便将某些全局级别的自定义应用于这些页面。
例如,在中,为了使用页脚扩展之前的实现,我们可以遵循以下步骤:
-
First, define a style for this page (in
App.xaml):<Application.Resources> <ResourceDictionary> <ControlTemplate x:Key="PageTemplate"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="25" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> </Grid.ColumnDefinitions> <ContentPresenter Grid.Row="0" /> <BoxView Grid.Row="1" Color="Navy" /> <Label Grid.Row="1" Margin="10,0,0,0" Text="(c) Hands-On Cross Platform 2020" TextColor="White" VerticalOptions="Center" /> </Grid> </ControlTemplate> </ResourceDictionary> </Application.Resources>在这个模板中,注意
ContentPresenter被用作为ContentPage的占位符。 -
现在,我们可以在
HomeView页面中使用这个模板,代码如下:<ContentPage xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" ControlTemplate="{StaticResource PageTemplate}" x:Class="ShopAcross.Mobile.Client.HomeView">
这将导致页脚 app 运行在我们的HomeView上:

Figure 5.7 – HomeView
到目前为止,我们只将应用的主页创建为内容页面。 然而,我们已经实现了基本的结构来继续引入额外的视图和视图模型。 现在,让我们学习如何导航到视图和从视图导航。
导航简单
在 Xamarin 生态系统中,每个平台都有其固有的导航堆栈,应用围绕这些堆栈构建。 开发人员负责维护这些堆栈,以便为用户创建所需的 UX 流。
在页面之间导航,Xamarin。 Forms 公开了一个Navigation服务,它可以与NavigationPage抽象页面实现一起使用。 换句话说,NavigationPage不能被归类为为用户提供内容的页面类型; 然而,它是 Xamarin 中用于维护导航堆栈和导航栏的关键组件。 表单应用。
在上一节中,我们创建了我们的HomeView,它显示了最近添加的产品的列表。 为了演示一些简单的导航方法,让我们考虑下面的用户故事:
“作为一个用户,我希望能够从主页上打开最近添加的产品的详细信息,这样我就可以获得有关所选产品的额外信息。”
为了显示产品的详细信息,我们需要介绍我们的产品详细信息视图。 遵循以下步骤:
-
让我们首先在名为
ProductDetailsView的ShopAccross.Mobile.Client项目中创建另一个基于 xaml 的ContentPage。 -
现在,在content部分添加以下内容:
<StackLayout Padding="10" Orientation="Vertical" Spacing="10"> <Label Text="{Binding Title, Mode=OneTime}" FontSize="Large" /> <Image Source="{Binding Image}" HorizontalOptions="FillAndExpand" /> <Label Text="{Binding Description}" FontSize="Large" /> </StackLayout> -
现在我们已经准备好了基本的
ProductDetailsView,我们可以从HomeView导航到ProductDetailsView。 要创建导航元素,让我们引入一个导航页面作为主页,根元素设置为HomeView:public App() { InitializeComponent(); MainPage = new NavigationPage(new HomeView()); } -
现在,将以下方法添加到
HomeView.xaml.cs: -
最后,将事件处理程序添加到
HomeView.xaml到,当点击相应的项目时,导航到产品的详细信息:<ListView ItemTapped="Handle_ItemTapped" ItemsSource="{Binding RecentProducts}" >
在本例中,我们从HomeView导航到ProductDetailsView。 在这个导航之后,在 iOS 上,你会注意到第一页的标题作为后退按钮文本插入到导航栏中。 此外,由于Title属性用于HomeView,文本也会显示在导航栏中。
Xamarin 的之前。 窗体 3.2 中,自定义导航栏显示内容和方式的唯一方法是使用某种形式的本地自定义(例如,NavigationPage的自定义渲染器)。 尽管如此,您现在可以使用导航页的TitleView依赖属性向导航栏添加自定义元素。
以HomeView页面为例,我们可以在ContentPage中添加以下 XAML 部分:
<NavigationPage.TitleView>
<StackLayout Orientation="Horizontal" VerticalOptions="Center" Spacing="10">
<Image Source="xamarin.png"/>
<Label
Text="Custom Title View"
FontSize="16"
TextColor="Black"
VerticalTextAlignment="Center" />
</StackLayout>
</NavigationPage.TitleView>
为了使xamarin.png文件对应用可用,你需要将其添加到 iOS 上的Resources文件夹和 Android 上的resources/drawable文件夹。
结果视图将具有已定义的StackLayout,而不是先前显示的Home标题:

图 5.8 -自定义导航页标题
尽管这种定制会稍微影响导航的原生行为,但在某些场景中,这种妥协是合理的; 例如,在标题视图上有一个搜索框是 Android 应用的一个突出模式。
在本节中,我们向导航层次结构中又添加了一层,并实现了这些层之间的导航功能。 到目前为止,我们只使用了ContentPage作为视图的基础。 但是,有些模板可以承载多个页面。
多页浏览
CarouselPage和TabbedPage是两种 Xamarin。 从MultiPage抽象派生出的页面实现。 这些页面都可以承载多个在它们之间具有唯一导航的页面。
为了说明MultiPage实现的用法,we 可以使用以下用户故事:
“作为一名用户,我希望拥有一系列最近产品的详细信息,这样我就可以轻松地通过滑动手势浏览它们,这样我就可以轻松地访问各种产品和详细信息,而不是从列表视图中选择一个。”
在这个实现中,我们将使用之前创建的HomeView和HomeViewModel。 废话少说,让我们开始实现:
-
我们将通过创建一个
CarouselPage来开始实现。 我们可以使用基于 xaml 的Content Page模板作为起点。 让我们将这一页命名为RecentProductsView。 -
现在已经创建了页面,您可以使用下面的 XAML 内容来定义来自最近产品列表的不同产品,作为该页面的多个子页面的绑定上下文:
-
我们还需要将类声明改为派生自
CarouselPage而不是ContentPage:public partial class RecentProductsView : CarouselPage { } -
然后,我们需要将一个新的实例
HomeViewModel分配给类构造器中的BindingContext:public RecentProductsView() { InitializeComponent(); BindingContext = new HomeViewModel(); } -
现在我们已经创建了
RecentProductsView,可以将导航方法添加到HomeView:private void RecentItems_Clicked(object sender, EventArgs e) { var recentProducts = new RecentProductsView(); Navigation.PushAsync(recentProducts); } -
最后,我们可以将工具栏菜单按钮添加到
HomeView:<ContentPage.ToolbarItems> <ToolbarItem Text="Recent" Clicked="RecentItems_Clicked" /> </ContentPage.ToolbarItems>
现在我们有了最近的条目页面集,让我们考虑以下的用户故事:
“作为一个注册用户,我希望有设置收藏类别的选项,这样当我浏览最近添加的产品时,只显示我选择的类别中的产品。”
为了让用户选择某些类别作为他们的收藏,我们需要为用户准备一个设置屏幕。 在这个设置屏幕上,我们可以显示任何个性化选项。 如果我们要实现一个单页应用,我们可以使用一个展板来显示这个页面,但是为了演示对等导航和多页模板,让我们使用一个选项卡页面来向我们的应用引入其他页面。
按照以下步骤创建SettingsView,并将其作为 peer 添加到HomeView:
-
在创建
TabbedPage之前,我们应该介绍SettingsView。 使用基于 xaml 的ContentPage模板创建一个名为SettingsView的页面。 -
创建页面后,添加以下内容:
<ContentPage.Content> <StackLayout Orientation="Vertical" Padding="10"> <Label Text="Selected Categories" FontSize="Title" /> <ListView> </ListView> </StackLayout> </ContentPage.Content> -
对于模板,我们将使用简单的
Grid和两列:<ListView.ItemTemplate> <DataTemplate> <ViewCell> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="75" /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Switch Grid.Column="0" /> <Label Text="{Binding .}" Grid.Column="1" VerticalTextAlignment="Center" FontSize="Subtitle"/> </Grid> </ViewCell> </DataTemplate> </ListView.ItemTemplate> -
对于内容,我们可以使用一组任意类别:
<ListView.ItemsSource> <x:Array Type="{x:Type x:String}"> <x:String>computers</x:String> <x:String>white furniture</x:String> <x:String>gadgets</x:String> <x:String>car electronics</x:String> <x:String>iot</x:String> </x:Array> </ListView.ItemsSource> -
现在,使用 Forms
TabbedPageXAML 模板添加RootTabbedView。 -
现在,将以下内容添加到我们的根页面:
<?xml version="1.0" encoding="utf-8"?> <TabbedPage xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:ShopAcross.Mobile.Client" xmlns:core="clr-namespace:ShopAcross.Mobile.Core;assembly=ShopAcross.Mobile.Core" x:Class="ShopAcross.Mobile.Client.RootTabbedView"> <TabbedPage.BindingContext> <core:HomeViewModel /> </TabbedPage.BindingContext> <!--Pages can be added as references or inline--> <TabbedPage.Children> <NavigationPage Title="Home" Icon="xamarin.png"> <x:Arguments> <local:HomeView BindingContext="{Binding .}" /> </x:Arguments> </NavigationPage> <NavigationPage Title="Settings" Icon="xamarin.png"> <x:Arguments> <local:SettingsView/> </x:Arguments> </NavigationPage> </TabbedPage.Children> </TabbedPage> -
最后,将
MainPage任务更改为App.xaml.cs中新创建的任务。
实际上,结果页面将在各自的布局和导航堆栈中托管两个子页面。 如果您现在单击最近的工具栏操作按钮,您将看到CarouselPage和TabbedPage是如何在同一个应用中使用的:

图 5.9 -多页面视图
重要的信息
需要注意的是,在 iOS 上,子元素的标题和图标属性用于创建选项卡导航项。 为使图标正确显示,正常分辨率为30x30,高分辨率为60x60,iPhone 6 分辨率为90x90。 在 Android 上,标题用于创建选项卡项。
在中,TabbedPage是 iOS 应用中位于导航层次结构顶部的基本控件之一。 可以通过为每个选项卡分别创建一个导航堆栈来扩展TabbedPage的实现。 这样,在选项卡之间的导航将为每个选项卡独立保留导航堆栈,并支持前后导航。
在本节中,我们介绍了多页面视图,您可以使用它来创建所谓的同行导航结构。 特别地,TabbedPage是 iOS 应用中最突出的导航层设置。 另一个多页面设置被用作导航堆栈的根,在这里您需要多个页面可以同时被用户访问,这是主/详细设置。 在下一节中,我们将为应用设置导航弹出菜单。
Master/detail 视图
在 Android 和 UWP 上,突出的导航模式和相关的页面类型是 master/detail,并使用一个所谓的导航抽屉。 在这个模式中,通过ContentPage(称为母版页)来维护跨导航结构的跳跃(跨越层次结构的不同层)或跨导航(在同一层内)。 用户与母版页的交互(显示在导航抽屉中)被传播到Detail视图。 在此设置中,详细信息视图的导航堆栈是存在的,而主视图是静态的。
要复制前面示例中的选项卡结构,我们可以创建MasterDetailPage,它将托管我们的菜单项列表。 MasterDetailPage将由Master内容页面和Detail页面组成,其中托管NavigationPage以创建导航堆栈。 按照以下步骤完成设置:
-
我们将从使用 Forms MasterDetailPage XAML 模板创建我们的
MasterDetailPage开始。 以RootView作为名称。 这个模板应该创建三个单独的页面和我作为多页面设置的一部分。 在本练习中,我们将不使用RootViewMaster和RootViewDetail页,也不使用RootViewMenuItem类,因此您可以安全地删除它们。 -
现在,打开
RootView.xaml文件,添加以下内容设置 up 的主部分:<MasterDetailPage.Master> <ContentPage Title="Main" Padding="0,30,0,0" Icon="slideout.png"> <StackLayout> <ListView x:Name="listView" ItemsSource="{Binding .}" SeparatorVisibility="None"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <Grid Padding="5,10"> <Grid.ColumnDefinitions> <ColumnDefinition Width="30"/> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Image Source="{Binding Icon}" /> <Label Grid.Column="1" Text="{Binding Title}" /> </Grid> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> </StackLayout> </ContentPage> </MasterDetailPage.Master> -
Notice that the
Masterpage simply creates aListViewcontaining the menu item entries.重要提示
还需要注意的是,所谓的汉堡包菜单图标需要添加为
Master页面的Icon属性(见slideout.png); 否则,将使用母版页的标题来代替菜单图标。 -
在 iOS 中添加
slideout.png到Resources文件夹,在 Android 中添加资源/绘制文件夹。 -
接下来,我们将创建一个简单的数据结构来存储菜单元数据。 您可以将以下类添加到一个新文件或
RootView.xaml.cs文件:public class NavigationItem { public int Id { get; set; } public string Title { get; set; } public string Icon { get; set; } } -
现在,我们应该创建用于导航的列表。 将下面的初始化代码添加到
RootView.xaml.cs的构造函数中:public RootView() { InitializeComponent(); var list = new List<NavigationItem>(); list.Add(new NavigationItem { Id = 0, Title = "Home", Icon = "xamarin.png" }); list.Add(new NavigationItem { Id = 1, Title = "Settings", Icon = "xamarin.png" }); BindingContext = list; } -
The
Detailpage assignment will now look as follows:<MasterDetailPage.Detail> <NavigationPage Title="List"> <x:Arguments> <local:HomeView /> </x:Arguments> </NavigationPage> </MasterDetailPage.Detail>在这个点,运行应用将创建导航抽屉和包含的
Master页:![Figure 5.10 – Master/Detail View]()
图 5.10 -主/详细视图
-
为了完成实现,我们还需要处理主列表中的
ItemTapped事件:
现在,实现已经完成。 每次使用菜单项时,导航类别将被更改,并创建一个新的导航堆栈; 但是,在导航类别中,导航堆栈是完整的。 另外,请注意,MasterDetailPage的IsPresented属性被设置为false,以便在创建新的详细信息视图时立即关闭母版页。
在本节中,我们从一个单视图应用开始实现,该应用是通过添加多页面视图以及导航抽屉展开的。 在这些设置中,我们广泛地使用了 Xamarin.Forms 的NavigationPage和NavigationService实现。 除了这个经典的实现之外,您还可以在应用中使用 Xamarin Shell 来降低设置导航基础设施的复杂性。 在下一节中,我们将快速了解如何使用 Xamarin Shell 创建类似的应用层次结构。
实现 Shell 导航
在本节中,我们将使用 Xamarin Shell 来演示它如何使开发人员的工作更轻松。 我们将使用 Xamarin Shell 实现一个简单的 Master/Detail 视图。
在导航层次结构比三层垂直和对等导航更复杂的应用中,广泛使用导航服务以及重新创建的视图和视图模型可能会导致可维护性和性能问题。 不同层和同级页面之间的导航链接尤其会给开发团队带来严重的头痛。
Xamarin Shell 可以通过在导航基础设施和 Xamarin 之间引入一个层来帮助减轻这种复杂性。 表单页面。 Shell的前提是提供类似于 web 应用的路由处理和模板基础设施,以便能够轻松创建复杂的导航链接和多页面视图。
说明 Xamarin Shell 如何工作的最简单方法是重新创建我们在前一节中创建的应用层次结构。 按照以下步骤转换应用,使其可以使用Shell:
-
我们将从添加所谓的
AppShell开始。AppShell将用于注册我们申请的主要路线。 为此,创建一个新的基于 xaml 的内容页面AppShell。 -
创建
AppShell后,将AppShell.xaml的内容更改为以下内容:<Shell xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:ShopAcross.Mobile.Client" x:Class="ShopAcross.Mobile.Client.AppShell"> <FlyoutItem Title="Home" Icon="xamarin.png"> <ShellContent ContentTemplate="{DataTemplate local:HomeView}"/> </FlyoutItem> <FlyoutItem Title="Settings" Icon="xamarin.png"> <ShellContent ContentTemplate="{DataTemplate local:SettingsView}"/> </FlyoutItem> </Shell> -
Now, let's change the base class definition in
AppShell.xaml.cs:public partial class AppShell : Shell { public AppShell() { InitializeComponent(); } }现在,我们已经成功地创建了带有两个飞行导航项的
AppShell。 -
接下来,让我们通过将
AppShell分配给MainPage:public App() { InitializeComponent(); //MainPage = new RootView(); MainPage = new AppShell(); }来介绍
AppShell到基础设施中。
现在,如果您运行应用,您将看到主/详细设置通过Shell设置; 您不需要设置项目列表和处理不同的事件来实现有状态导航菜单。
另一个重要的注意事项是,在更改SettingsView上的设置(即切换其中一个类别)并导航返回SettingsView后,您将注意到SettingsView保留了的状态。 这可以为您提供有关 Xamarin Shell 上如何管理有状态导航的线索。
让我们的实现更进一步,让我们为导航菜单项引入路由,并在视图中使用这些路由:
-
First, let's add the following routes to the defined
ShellContentitems inAppShell.xaml:<ShellContent Route="home" ContentTemplate="{DataTemplate local:HomeView}"/> <ShellContent Route="settings" ContentTemplate="{DataTemplate local:SettingsView}"/>一旦这些路由配置好了,我们就可以使用 Shell 导航进入某个导航状态。
-
现在,添加一个按钮到
SettingsView.xaml:<Button Text="Back to Home" Clicked="Button_Clicked" /> -
然后在
SettingsView.xaml.cs中添加以下方法:private async void Button_Clicked(object sender, EventArgs e) { await Shell.Current.GoToAsync("//home"); }
现在,如果您运行应用,导航到设置视图,并单击按钮,Shell 应该会将您带回主页。
基于 uri 的导航基础结构还允许在层之间导航,以及相对路径。
在本节中,我们试图演示 Xamarin Shell 的强大功能,至少在实现导航层次结构方面是如此。 Xamarin Shell 提供了其他有用的布局,以及搜索等功能。 我们将在下一节中讨论这些内容。
使用 Xamarin。 表单和本地控件
现在我们对不同的页面类型和导航模式更加熟悉了,我们可以继续为我们的页面创建实际的 UI。 在本节中,我们将演示各种 Xamarin。 表单元素及其用法,以及如何在 Xamarin 中使用本地控件。 视觉形式树。
为 Xamarin 目标平台创建足够灵活的 UX 可能非常复杂,特别是当涉及的涉众不熟悉上述 UX 设计因素时。 然而,Xamarin 的。 表单提供了各种布局和视图,帮助开发人员找到项目需求的最佳解决方案。
重要提示
Xamarin 的。 表单,可视化树由三层组成:页面、布局和视图。 布局用作视图的容器,视图是用于创建页面的用户控件。 这些是用户的主要交互界面。
让我们仔细看看 UI 组件。
布局
布局是容器元素,用于在设计图面上分配用户控件。 为了满足平台要求,布局可以用来对齐、堆叠和定位视图元素。 不同类型的布局如下:
-
StackLayout: This is one of the most overused layout structures in Xamarin.Forms. It is used to stack various view and other layout elements with prescribed requirements. These requirements are defined through various dependency or instance properties, such as alignment options and dimension requests.例如,在
ProductDetailsView页面上,我们使用StackLayout将条目的Image与其各自的标题和描述结合起来:<StackLayout Padding="10" Orientation="Vertical"> <Label Text="{Binding Title}" FontSize="Large" /> <Image Source="{Binding Image}" HorizontalOptions="FillAndExpand"/> <Label Text="{Binding Description}" /> </StackLayout>在这个设置中,重要的声明是
Orientation,它定义了堆叠应该垂直发生;HorizontalOptions,定义为Image元素,它允许Image根据可用空间水平和垂直展开; 以及StackLayout,可以用来创建“方向-改变-响应”的行为。 -
FlexLayout: This can be used to create fluid and flexible arrangements of view elements that can adapt to the available surface.FlexLayouthas many available directives that developers can use to define alignment directions. In order to demonstrate just a few of these, let's assumeProductDetailsViewrequires an implementation of a horizontal layout, where certain features are listed in a floating stack that can be wrapped into as many rows as required:<StackLayout Padding="10" Orientation="Vertical" Spacing="10"> <Label Text="{Binding Title}" FontSize="Large" /> <Image Source="{Binding Image}" HorizontalOptions="FillAndExpand" /> <FlexLayout Direction="Row" Wrap="Wrap"> <Label Text="Feature 1" Margin="4" VerticalTextAlignment="Center" BackgroundColor="Gray" /> <Label Text="Feat. 2" Margin="4" VerticalTextAlignment="Center" BackgroundColor="Lime"/> <!-- Additional Labels --> </FlexLayout> <Label Text="{Binding Description}" /> </StackLayout>这将创建一个类似于流体布局响应 UI 模式中描述的设计结构:

图 5.11 - Flex 布局
-
Grid: If it is not desired for the views in a layout to expand and trigger layout cycles – in other words, if a certain page requires a more top-down layout structure (that is, with the parent element determining the layout) – thenGridwould be the most suitable control. Using theGridlayout, controls can be laid out in accordance with column and row definitions, which can be adjusted to respond to control size changes or the overall size ofGrid.在为我们的页面创建控制模板时,我们使用了
Grid来创建一个刚性结构,以便我们可以将页脚的绝对高度值,同时允许内容演示者覆盖屏幕的其余部分:<ControlTemplate x:Key="PageTemplate"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="25" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> </Grid.ColumnDefinitions> <ContentPresenter Grid.Row="0" /> <BoxView Grid.Row="1" Color="Navy" /> <Label Grid.Row="1" Margin="10,0,0,0" Text="(c) Hands-On Cross Platform 2018" TextColor="White" VerticalOptions="Center" /> </Grid> </ControlTemplate>注意,我们为标签使用了边距值。 为了避免使用页边距,我们可以创建一个具有固定值的列定义,并根据期望的结果设置该页边距列,以便它也适用于内容演示者:
<Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="25" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="10 /> <ColumnDefinition /> <ColumnDefinition Width="10" /> </Grid.ColumnDefinitions> <ContentPresenter Grid.Column="1" Grid.Row="0" /> <BoxView Grid.Row="1" Grid.ColumnSpan="3" Color="Navy" /> <Label Grid.Row="1" Grid.Column="1" Text="(c) Hands-On Cross Platform 2018" TextColor="White" VerticalOptions="Center" /> </Grid>这样设置后,
BoxView将扩展到三个列上,而脚注文本和实际内容将被隔离到第二列 column -1 上,column -0 和 column -2 充当页边距。Grid也可以用来构造视图的某一段。 例如,如果我们将规格节添加到我们的ProductDetailsView页中,它将类似于:<StackLayout Padding="10" Orientation="Vertical" Spacing="10"> <!-- Removed for Brevity --> <Label Text="{Binding Description}" /> <Label Text="Specifications" Font="Bold" /> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="3*" /> <ColumnDefinition Width="5*" /> </Grid.ColumnDefinitions> <Label Text="Specification 1" Grid.Column="0" Grid.Row="0"/> <Label Text="Value for Specification" Grid.Column="1" Grid.Row="0" TextColor="Gray"/> <Label Text="Another Spec." Grid.Column="0" Grid.Row="1" /> <Label Text="Value for Specification that is a little longer" Grid.Column="1" Grid.Row="1" TextColor="Gray"/> <!-- Additional Specs go here --> </Grid> </StackLayout>请注意,列被设置为使用屏幕的 3/8 和 5/8,以便最佳地使用可用的空间。 这将创建一个类似如下的视图:

图 5.12 -网格布局
将最后一个元素添加到屏幕后,您可能会注意到屏幕空间垂直耗尽,因此最终的网格元素可能溢出视图端口,这取决于屏幕大小。
-
ScrollView: To get the screen to scroll so that all the content is visible to the user, we can introduceScrollView.ScrollViewis another prominent layout element and acts as a scrollable container for the contained view elements.为了能够滚动屏幕,使所有的规格都可见,我们可以简单地将
ProductDetailsView.xaml中的主布局封装到ScrollView中:<ContentPage.Content> <ScrollView> <StackLayout Padding="10" Orientation="Vertical" Spacing="10"> <!-- Removed for brevity --> </StackLayout> </ScrollView> </ContentPage.Content>当涉及
Entry字段时,会用到ScrollView的额外用法。 当用户点击Entry字段时,移动设备上的行为是键盘从屏幕底部向上滑动,产生垂直偏移,减少了设计空间。 在包含Entry的视图中,键盘可能与当前聚焦的Entry字段重叠。 这将产生不理想的用户体验。 为了纠正这种行为,表单内容应该放在ScrollView中,这样键盘的外观就不会将有问题的Entry字段推到屏幕之外。 -
AbsoluteLayout和RelativeLayout:这些是我们在中尚未涵盖的其他布局选项。 这两个布局,一般来说,治疗的观点几乎像一个画布,让物品被放置在彼此之上,使用当前的屏幕(在AbsoluteLayout的情况下)或其他控件(在RelativeLayout的情况下)作为定位参考。
举例来说,如果我们放置一个浮动操作按钮(工厂)对我们的HomeView从材料设计,我们可以很容易地实现,使用绝对布局,按钮在屏幕的右下角(即位置比例)和利润率的增加我们的工厂:
<AbsoluteLayout>
<ListView
ItemsSource="{Binding Items}"
ItemTapped="Handle_ItemTapped"
SeparatorVisibility="None" >
<ListView.ItemTemplate>
<DataTemplate>
<!-- Removed for brevity -->
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Image
Source="AddIcon.png"
HeightRequest="60"
WidthRequest="60"
AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="1.0,1.0"
Margin="10"/>
</AbsoluteLayout>
这将创建一个 FAB(即,使用的图像代替 FAB)显示在列表视图项上:

图 5.13 -相对布局
此外,RelativeLayout以类似的方式允许开发人员在元素之间以及视图本身创建比例计算。
Xamarin。 表格视图元素
到目前为止,我们在应用中使用的主要视图元素是Label和Image(同时创建列表和详细信息视图)。 此外,在登录屏幕上,我们使用了Entry和Button视图。 这两组控件之间的主要区别在于,虽然Label和Image用于显示(通常)只读内容,但Entry和Button是用于用户输入的元素。
如果我们仔细观察Label,我们会发现在我们的设计中,各种属性用于创建文本内容的定制显示,以强调书法/排版(参考 iOS 和 UWP 的平台要求)。 开发人员不仅可以定制文本内容的外观和感觉,还可以使用Span元素创建富文本内容。 span 类似于 WPF 中的Run元素和具有相同名称的 web 元素(即Span)。 在后来的 Xamarin 版本中。 窗体Span可以识别手势,使开发人员能够在单个文本内容块中创建交互区域。 要使用 span,我们可以使用标签的FormattedText属性。
为了进一步定制(或者将品牌应用于)应用,还可以引入自定义字体。 当涉及到包含自定义字体时,每个平台都需要执行不同的步骤。
作为第一步,开发人员需要访问字体的 TFF 文件,并且需要将该文件复制到特定于平台的项目中。 在 iOS 上,文件需要设置为BundleResource,在 Android 上设置为AndroidAsset。 仅在 iOS 上,自定义字体应该声明为字体的一部分,字体由Info.plist文件中的应用条目提供:

图 5.14 - Bundle 资源
此时,我们已经使用的自定义字体可以通过FontFamily属性添加到目标标签; 然而,在 Android 和 iOS 中,字体家族的声明是不同的:
<Label Text="{Binding Description}">
<Label.FontFamily>
<OnPlatform x:TypeArguments="x:String">
<On Platform="iOS" Value="Ubuntu-Light" />
<On Platform="Android" Value="Ubuntu-Light.ttf#Ubuntu-
Light" />
<On Platform="UWP" Value="Assets/Fonts/Ubuntu-
Light.ttf#Ubuntu-Light" />
</OnPlatform>
</Label.FontFamily>
</Label>
为了更方便地使用字体或将其应用到应用中的所有标签,可以使用App.xaml文件。 这个将把它添加到应用的资源中:
<Application.Resources>
<ResourceDictionary>
<!-- Removed for brevity -->
<OnPlatform x:Key="UbuntuBold" x:TypeArguments="x:String">
<On Platform="iOS">Ubuntu-Bold</On>
<On Platform="Android">Ubuntu-Bold.ttf#Ubuntu-Bold</On>
</OnPlatform>
<OnPlatform x:Key="UbuntuItalic" x:TypeArguments="x:String">
<On Platform="iOS">Ubuntu-Italic</On>
<On Platform="Android">Ubuntu-Italic.ttf#Ubuntu-
Italic</On>
</OnPlatform>
<!-- Additional Fonts and Styles -->
</ResourceDictionary>
</Application.Resources>
现在,我们可以为特定的目标定义内隐或外显风格:
<Style x:Key="BoldLabelStyle" TargetType="Label">
<Setter Property="FontFamily" Value="{StaticResource UbuntuBold}" />
</Style>
<!-- Or an implicit style for all labels -->
<!--
<Style TargetType="Label">
<Setter Property="FontFamily" Value="{StaticResource UbuntuRegular}" />
</Style>
-->
重要的信息
这可以进一步包含包含字形(例如FontAwesome)的字体,这样我们就可以将标签用作菜单图标。 一个简单的实现是创建一个派生于Label的自定义控件,并设置一个以该自定义控件为目标的全局隐式样式。
与Label对应的是Entry和Editor,两者都源于InputView抽象。 这些控件可以放在用户表单中,分别处理单行或多行文本输入。 改善用户体验,这两个控件公开Keyboard属性,可用于设置适当的类型的软件键盘用户条目(例如,Chat,Default,Email、【显示】,Telephone,等等)。
其余的用户输入控件是更特定于场景的,如BoxView、Slider、Map和WebView。
值得一提的是,还有三个额外的用户输入控件,即Picker、DatePicker和TimePicker。 这些选择器表示显示在表单上的数据字段和聚焦数据字段后使用的选择器对话框的组合。
如果定制这些控件不满足 UX 要求,Xamarin。 表单允许开发人员引用和使用本地控件。
本机组件
在某些情况下,开发人员需要求助于使用本地用户控件——特别是当某个控件仅在某个平台上存在时(即没有 Xamarin)。 表单抽象用于特定的 UI 元素)。 在这些类型的情况下,Xamarin 允许用户在 Xamarin 中声明本地视图。 表单 XAML 并设置/绑定这些控件的属性。
要包含本机视图,首先必须声明本机视图的命名空间:
xmlns:ios="clr-namespace:UIKit;assembly=Xamarin.iOS;targetPlatform=iOS"
xmlns:androidWidget="clr-namespace:Android.Widget;assembly=Mono.Android;targetPlatform=Android"
xmlns:formsandroid="clr-namespace:Xamarin.Forms;assembly=Xamarin.Forms.Platform.Android;targetPlatform=Android"
例如,一旦命名空间被声明,我们就可以将Label替换为ItemView.xaml,并直接使用其本地对应版本:
<!-- <Label Text="{Binding Description}" /> -->
<ios:UILabel Text="{Binding Description}" View.HorizontalOptions="Start"/>
<androidWidget:TextView Text="{Binding Description}" x:Arguments="{x:Static formsandroid:Forms.Context}" />
现在,视图将为每个平台包含一个不同的本地控件。 此外,UILabel.Text和TextView.Text属性现在携带到Description字段的绑定。
重要提示
需要注意的是,要让本机视图引用工作,所讨论的视图不应该包含在XamlCompilation中。 换句话说,视图应该带有[XamlCompilation(XamlCompilationOptions.Skip)]属性。
还可以使用本机类型和属性进一步定制本机字段。 例如,为添加一个投影到UILabel项,我们可以使用ShadowColor和ShadowOffset值:
<ios:UILabel
Text="{Binding Description}"
View.HorizontalOptions="Start"
ShadowColor="{x:Static ios:UIColor.Gray}">
<ios:UILabel.ShadowOffset>
<iosGraphics:CGSize>
<x:Arguments>
<x:Single>1</x:Single>
<x:Single>2</x:Single>
</x:Arguments>
</iosGraphics:CGSize>
</ios:UILabel.ShadowOffset>
</ios:UILabel>
该声明的结果如下(将其与 Xamarin 进行比较。 formLabel字段我们之前定义):

图 5.15 -原生属性
至此,我们已经完成了这个实现。 在本节中,我们使用额外的视图元素和基本布局扩展了示例应用。 如您所见,这些开箱即用的布局和视图为开发人员提供了简单的定制选项。 现在我们已经有了一个基本的 UI,让我们仔细看看如何将引入这些 UI 元素的域数据。 在下一节中,我们将讨论数据驱动视图。
创建数据驱动视图
MVVM 架构,正如你在第四章,用 Xamarin 开发移动应用中看到的,主要集中在数据以及如何从视图中解耦数据。 然而,这种解耦并不意味着创建的视图和控件不应该响应数据内容的更改,这些更改可能是用户输入的结果,也可能是更新状态数据的结果。 为了方便数据模型的传播,从视图模型到视图,以及视图、数据绑定和其他数据相关的 Xamarin 之间的传播。 表单机制是至关重要的工具。
在本节中,我们将演示允许我们作为开发人员检索、转换和更新域数据而不直接引用这些数据点的各种特性。 首先,我们将修改关于数据绑定基础知识。 然后,我们将继续讨论值转换器,以及如何将它们与数据绑定结合使用。 我们还将研究数据触发器和可视化状态,以及数据如何驱动 UI 上的更改,并将某些行为强加于视图元素。
数据绑定要领
Xamarin 中最简单的数据绑定。 窗体包含我们想要链接到当前视图属性的属性的路径。 在这种类型的声明中,我们假设整个和/或父视图的BindingContext类被设置为使用目标源视图模型。
如果我们看看从HomeView到ProductDetailsView的导航实现,你会注意到列表中选择的项目被设置为ProductDetailsView的绑定上下文:
private void Handle_ItemTapped(object sender, Xamarin.Forms.ItemTappedEventArgs e)
{
var itemView = new ProductDetailsView();
itemView.BindingContext = e.Item;
Navigation.PushAsync(itemView);
}
设置BindingContext后,我们可以继续使用ProductViewModel的属性模型,假设ProductViewModel被设置为触发Title属性的PropertyChangedEvent(从INotifyPropertyChanged开始):
<Label Text="{Binding Title}" FontSize="Large" />
数据绑定并不总是需要与值属性相关(例如,Text、SelectedItem等等); 它还可以用来识别视图的视觉属性。
例如,我们之前添加到ProductDetailsView的芯片定义当前选择的项目是否支持某些特性。 让我们假设在视图模型侧有布尔属性来显示或隐藏这些值。 绑定看起来类似如下:
<Label x:Name="Feat1" Text="Feature 1" IsVisible="{Binding HasFeature1}" BackgroundColor="Gray" />
<Label x:Name="Feat2" Text="Feat. 2" IsVisible="{Binding HasFeature2}" BackgroundColor="Lime"/>
在这两个绑定场景中,我们都将一个值从视图模型绑定到一个特定的视图元素。 另一个有效的场景是视图的更改影响另一个视图(即视图到视图的绑定)。 让我们假设,在ProductDetailsView上,规格的可见性取决于标签的可见性,将x:Name设置为Feat1:
<Grid IsVisible="{Binding Path=IsVisible,Source={x:Reference Feat1}}">
需要注意的是,在真实的项目中,视图到视图的绑定通常用于将用户输入反映在另一个视图上。 在本例中,绑定使用相同的视图模型属性(即HasFeature1)会更合适。
在创建可视化树之后,我们到目前为止所概述的绑定实际上并不依赖于 UI 中反映的任何更改。 在这样的设置中,监听视图模型属性上的任何更改事件是可以避免的性能损失。 为了弥补这个开销,我们可以将绑定模式设置为OneTime:
<Label Text="{Binding Title, Mode=OneTime}" FontSize="Large" />
这样,绑定只在BindingContext发生变化时执行。 如果我们希望ViewModel(通常被称为源)中的变化反映在View(被称为目标)中,我们可以使用OneWay绑定。 如果由OneWay绑定提供的单向数据流的方向是相反的,我们也可以使用OneWayToSource。 绑定提供了支持双向数据流的基础架构。
尽管运行时试图在建立绑定时将源类型转换为目标类型,但结果可能并不总是理想的(例如,不同类型的ToString方法可能不能提供正确的显示值)。 在这些情况下,开发人员可以求助于使用值转换器。
值转换器
值转换器可以被描述为实现IValueConverter接口的简单转换工具。 这个接口提供了两个方法,它们允许我们将源转换为目标,以及将目标转换为源,以支持各种绑定场景。
例如,如果我们要显示库存中一个项目的发布日期,我们将需要绑定到ProductViewModel上的相应属性。 然而,一旦页面被呈现,结果就不那么令人满意了:

图 5.16 -显示的完整日期格式
要格式化日期,we 可以创建一个值转换器,该转换器负责将DateTime值转换为字符串:
public class DateFormatConverter : IValueConverter
{
public object Convert(object value, Type targetType, object
parameter, CultureInfo culture)
{
if(value is DateTime date)
{
return date.ToShortDateString();
}
return null;
}
public object ConvertBack(object value, Type targetType, object
parameter, CultureInfo culture)
{
// No Need to implement ConvertBack for OneTime and OneWay bindings.
throw new NotImplementedException();
}
}
它还负责在我们的ProductDetailsViewXAML 中声明这个转换器:
<ContentPage
...
xmlns:converters="using:FirstXamarinFormsApplication.Client.Converters"
x:Class="FirstXamarinFormsApplication.Client.ItemView">
<ContentPage.Resources>
<ResourceDictionary>
<converters:DateFormatConverter x:Key="DateFormatConverter" />
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Content>
<!-- Removed for brevity -->
<Label Text="{Binding ReleaseDate, Converter={StaticResource DateFormatConverter}}" />
<!-- Removed for brevity -->
</ContentPage.Content>
</ContentPage>
现在,显示将使用短日期格式,这是与文化相关的(例如,EN-US 地区的M/d/yyyy):

图 5.17 -格式化日期显示
通过使用绑定传递日期格式字符串(例如M/d/yyyy)来使用固定的日期格式,我们可以进一步实现该实现。
Xamarin 的。 Forms 还提供了格式化字符串的使用来处理简单的字符串转换,因此可以避免等简单的转换器(如DateFormatConverter)。 具有固定日期格式的相同实现可以设置如下:
<Label Text="{Binding ReleaseDate, StringFormat='Release {0:M/d/yyyy}'}}" />
结果会是这样的:

图 5.18 -字符串格式示例
此外,我们可能喜欢处理将发布日期设置为 null 的场景(也就是说,当ReleaseDate属性设置为Nullable<DateTime>或简单地DateTime)。 对于这种情况,我们可以使用TargetNullValue:
<Label Text="{Binding ReleaseDate, StringFormat='Release {0:M/d/yyyy}', TargetNullValue='Release Unknown'}" />
TargetNullValue,顾名思义,是一个替换值,当绑定目标已被解析,但找到的值为空时。 类似地,当运行时不能解析绑定上下文上的目标属性时,可以使用FallbackValue。
扩展这个实现,如果发布日期未知,我们可能想用不同的颜色显示Label。 为了实现这一点,我们可以创建一个根据发布值返回特定颜色的转换器,但是我们也可以使用一个属性触发器来根据标签的Text属性值设置字体颜色。 在这种情况下,使用触发器是更好的选择,因为使用转换器将意味着硬编码颜色值,而触发器可以使用动态或静态资源,并可以使用目标视图的样式应用。
触发器
可以将触发器定义为需要执行的声明性动作。 不同类型的触发器如下:
- 属性触发器:视图的属性更改
- 数据触发器:绑定更改数据值
- 事件触发:目标视图中特定事件的发生
- 多触发器:用于实现多触发器组合
为了说明触发器的使用,我们可以使用前面的示例,其中某项的ReleaseDate不存在。 在这个场景中,由于定义了TargetNullValue属性,标签的文本将被设置为Release Unknown。 这里,我们可以使用属性触发器来设置字体颜色:
<Label x:Name="ReleaseDate" Text="{Binding ReleaseDate, StringFormat='Release {0:M/d/yyyy}', TargetNullValue='Release Unknown'}">
<Label.Triggers>
<Trigger TargetType="Label" Property="Text" Value="Release
Unknown">
<Setter Property="TextColor" Value="Red" />
</Trigger>
</Label.Triggers>
</Label>
这里,目标类型定义包含元素(即触发器操作的目标),而属性和值定义触发器的原因。 然后可以将多个设置器应用于正在修改视图值的目标。
以类似的方式,我们可以创建一个数据触发器来设置标题的颜色,这取决于发布日期标签的值:
<Label Text="{Binding Title, Mode=OneTime}" FontSize="Large">
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Source={x:Reference ReleaseDate},
Path=Text}"
Value="Release Unknown">
<Setter Property="TextColor" Value="Red" />
</DataTrigger>
</Label.Triggers>
</Label>
这里,我们将DataTrigger的绑定上下文设置为另一个视图(即视图到视图)绑定。 如果我们使用视图模型作为绑定上下文,我们也可以使用ReleaseDate。
最后,如果我们有没有发布日期的,但我们有数据来支持一个项目实际上已经向公众发布了,我们可以使用MultiTrigger:
<MultiTrigger TargetType="Label">
<MultiTrigger.Conditions>
<PropertyCondition Property="Text" Value="Release Unknown" />
<BindingCondition Binding="{Binding IsReleased}" Value="false"/>
</MultiTrigger.Conditions>
<Setter Property="TextColor" Value="Red" />
</MultiTrigger>
事件触发器是触发器家族的奇怪成员,因为它们依赖于在目标视图而不是Setters上触发的事件; 他们使用Action。
例如,为了增加一点 UX 增强,我们可以在项目视图中为图像添加淡出动画。 要使用这个动画,我们需要实现它作为Action的一部分:
public class AppearingAction : TriggerAction<VisualElement>
{
public AppearingAction() { }
public int StartsFrom { set; get; }
protected override void Invoke(VisualElement visual)
{
visual.Animate("FadeIn",
new Animation((opacity) => visual.Opacity = opacity, 0, 1),
length: 1000, // milliseconds
easing: Easing.Linear);
}
}
现在已经创建了TriggerAction,我们可以在映像上定义一个事件触发器(即使用BindingContextChanged事件):
<Image Source="{Binding Image}" HorizontalOptions="FillAndExpand">
<Image.Triggers>
<EventTrigger Event="BindingContextChanged">
<actions:AppearingAction />
</EventTrigger>
</Image.Triggers>
</Image>
这将创建一个微妙的淡入效果,这应该与正在加载的图像一致,从而提供一个更愉快的用户体验。
动作还可以通过使用EnterAction和ExitAction与属性和数据触发器一起使用,这两个触发器根据触发条件定义了这两种状态。 然而,在属性和数据触发器的上下文中,要创建更广义的状态,以及修改控件的公共状态,可以使用Visual State Manager(VSM)。 通过这种方式,多个设置器可以统一在一个状态中,从而减少 XAML 树中的混乱,并创建更易于维护的结构。
视觉状态
可视化状态和 VSM 将是 WPF 和 UWP 开发人员熟悉的概念; 然而,他们在 Xamarin 中缺失。 窗体运行时直到最近。 可视状态定义控件呈现时必须满足的各种条件。 例如,一个Entry元素可以处于Normal、Focused或Disabled状态,每种状态都为元素定义了不同的视觉 setter。 此外,还可以为可视元素定义自定义状态,并且根据触发器或对VisualStateManager的显式调用,可以管理元素的可视状态。
为了演示这一点,我们可以为我们的标签创建三个不同的状态(例如,Released、UnReleased和Unknown),并使用我们的触发器来处理这些状态。
首先,我们需要定义标签控件的状态(然后它可以作为样式的一部分移动到资源字典中):
<Label x:Name="ReleaseDate" ...>
<Label.Triggers>
<!-- Removed for Brevity -->
</Label.Triggers>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Released">
<VisualState.Setters>
<Setter
Property="BackgroundColor"
Value="Lime" />
<Setter
Property="TextColor"
Value="Black" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="UnReleased">
<VisualState.Setters>
<Setter Property="TextColor" Value="Black" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Unknown">
<VisualState.Setters>
<Setter Property="TextColor" Value="Red" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Label>
如您所见,已定义状态中的一个是Unknown,它应该将文本颜色设置为红色。 要使用触发器来改变标签的状态,我们可以实现一个触发器动作:
public class ChangeStateAction : TriggerAction<VisualElement>
{
public ChangeStateAction() { }
public string State { set; get; }
protected override void Invoke(VisualElement visual)
{
if(visual.HasVisualStateGroups())
{
VisualStateManager.GoToState(visual, State);
}
}
}
我们可以使用这个动作作为我们之前定义的多触发器的EnterAction:
<MultiTrigger TargetType="Label">
<MultiTrigger.Conditions>
<!-- Removed for brevity -->
</MultiTrigger.Conditions>
<MultiTrigger.EnterActions>
<actions:ChangeStateAction State="Unknown" />
</MultiTrigger.EnterActions>
</MultiTrigger>
我们可以通过使用 setter 来达到相同的结果。 但是,需要指出的是,一旦标签被设置为给定状态,如果不定义ExitAction,它将不会恢复到以前的状态。
在本节中,您学习了如何成功地保持数据与视图元素的解耦,同时创建一个声明性可视树,该树将响应不同的数据类型和用户交互。 本节中使用的大多数可视化元素都针对单个数据项及其属性。 在下一节中,我们将分析专门处理数据项集合的不同视图。
集合视图
在 Xamarin 的早期阶段。 窗体,StackLayout和ListView是显示元素集合的两个最流行的选项。 的区分因素这两个观点的是StackLayout只是用于静态内容(也就是说,没有收集数据绑定)而ListView是用于创建一个集合视图的数据项的集合(即ItemsSource)显示在模板定义的形式。
事实上,到目前为止,我们在创建HomeView和RootView菜单时使用了ListView元素。 如果你观察 HomeView 如何使用ListView,你会立即注意到集合绑定的主要元素允许数据绑定在ListView:
<ListView ItemsSource="{Binding RecentProducts}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid>
<!-- Removed for brevity -->
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
ItemsSource可以描述为集合绑定上下文,它定义在视图中使用什么数据,而ItemTemplate包含DataTemplate,它用于定义数据应该如何显示。
ItemsSource定义为IEnumerable,这意味着可以将任何类型的集合作为数据源分配给ListView。 然而,在处理动态数据集时,使用实现INotifyCollectionChanged接口的集合(如ObservableCollection)是非常有用的。 在这种设置中,对数据源集合的任何更改(例如添加或删除项)都将立即反映在呈现的元素集上。
DataTemplate可以使用ViewCell定义自定义模板。 例如,在我们的示例中,我们使用了Grid来设置产品列表的布局。 在这些类型的场景中,一般的经验法则是避免相对大小和位置,这样我们就不会招致性能损失。 此外,可以使用专门的细胞模板(如EntryCell、SwitchCell、TextCell或ImageCell来表示简单的数据项。
dattemplate 可以与DataTemplateSelector组合使用,以根据针对绑定数据上下文的谓词显示不同的模板。
如前所述,StackLayout和Grid最初仅用于显示静态内容。 但是,这些静态视图也可以使用附加的属性绑定到数据集合,例如BindableLayout、ItemsSource和BindableLayout.ItemTemplate。 就像在我们的ListView示例中一样,这些属性将在呈现时用于在父布局中创建子元素。 然而,如果集合大小很大且数据是动态的,那么使用BindableLayout设置和StackLayout之类的控件会带来性能损失。
显示数据项集合的另一个选项是使用CollectionView。 CollectionView比ListView引入得晚一些,它提供了更灵活的模板选项集,并且性能更好。 与ListView一样,CollectionView也支持开箱即用的“下拉刷新”功能。
总结
在本章中,我们使用 Xamarin 的固有控件实现了一些简单的视图。 窗体框架,并设置一个基本的导航层次结构。 我们还研究了 Xamarin Shell 导航基础设施,它为导航基础设施提供了另一种选择。 在我们的视图中,我们使用了各种控件,并讨论了如何基于简单的数据项和集合创建响应性和数据驱动的 UI 元素。
通过大量的布局、视图和定制选项,开发人员可以创建有吸引力和直观的用户界面。 此外,数据驱动的 UI 选项可以帮助开发人员从这些视图中分离(解耦)任何业务域实现,从而提高任何移动开发项目的可维护性。
然而,有时,标准控制可能不足以满足项目需求。 在下一章中,我们将进一步研究如何定制现有的 UI 视图并实现自定义的本地元素。
六、定制 Xamarin.Forms
Xamarin 的。 表单允许开发人员以各种方式修改 ui 呈现基础设施。 开发人员引入的自定义可以针对某个控件元素上的某个平台特性,也可以创建一个全新的视图控件。 这些定制可以在 Xamarin 上进行。 窗体层或目标本机平台上。
在本章中,我们将介绍定制 Xamarin 所涉及的步骤和过程。 在性能或用户体验(UX)方面不打折扣。 我们将从定义定制的开发领域开始。 从与平台无关的自定义开始,比如行为、样式和 XAML 扩展,我们将通过实现特定于平台的功能和自定义进入本机域。 最后,我们将看看自定义呈现器和自定义控件。
下面几节将介绍 Xamarin 的不同开发领域。 形式的定制:
- Xamarin 的。 形式发展领域
- Xamarin 的。 形式共享域
- 自定义本机域
- 创建自定义控件
到本章结束时,您将能够在 Xamarin 中自定义视图元素。 形式和本土边界。 此外,您还可以向这些元素添加行为修改。
技术要求
你可以通过 GitHub 上的https://github.com/PacktPublishing/Mobile-Development-with-.NET-Second-Edition/tree/master/chapter06找到本章的代码。
Xamarin。 形式发展领域
在本节中,我们将从定义 Xamarin 的开发领域开始。 表单应用。 我们将创建一个象限平面,将共享和本地作为一个轴,业务逻辑和 UI 作为另一个轴。 然后,我们将根据它们的实现和利用在这些象限中放置各种定制选项。
到目前为止,在本书中,您将注意到使用 Xamarin 进行应用开发。 表单框架在多个域上执行。 而 Xamarin 的。 表单层创建了一个共享开发域,该域将用于目标本机平台,目标平台仍然可以用于特定于平台的实现。
如果我们要分离一颗 Xamarin。 根据发展战略和应用领域类别将应用划分为四个象限,它看起来是这样的:

图 6.1 Xamarin 定制域
在这个设置中,象限 I(即共享业务逻辑)表示应用的核心逻辑实现。 这个域将包含视图模型、域数据描述和服务客户端实现。 最重要的是,特定于平台的 api 的抽象(即将在本机平台上实现的接口)应该在这个域中创建,以便其他域以及这个域中的视图模型可以使用它们。
象限 II 和 III 表示我们需要实现的 UI 定制,以便为应用创建所需的 UX。 到目前为止,我们一直只使用象限 II 中的来创建可视化树。 简单的数据驱动应用和业务线(Line-of-Business)(LOB)应用可以单独使用这个域。 然而,如果我们要创建面向消费者的应用,那么遵循品牌 UX 需求并创建直观的 UI 应该是我们的主要目标。 在这个场景中,我们可以为 Xamarin 创建自定义。 使用象限 III 形成视图。
在这个范例中,象限 I 仅使用数据绑定和转换器实现与象限 II 连接。 象限 II 负责将交付的数据传播到象限 III。
在象限 II 中,针对开发人员的定制选项主要限于 Xamarin 提供的开箱即用视图提供的可扩展性选项。 形式框架。 这些视图的组合和行为修改可以提供高度可维护性的跨平台源代码。 通过使用样式选项、可视化状态和数据驱动模板,UX 可以满足这些需求。
当从共享平台移动到本地平台时(即从象限 II 移动到象限 III),开发人员可以使用平台细节和 Xamarin。 形成效应。 使用这些扩展点,作为开发人员,我们可以修改本地控件的行为并修改呈现的本地 UI,从而在 Xamarin 之间创建一座桥梁。 表单视图抽象和目标本机控件。 这些可扩展性特性与 Xamarin 的组合。 表单行为可以提高应用的可维护性。
特定象限 iii 的开发包括自定义渲染器和本地控件。 可以在 Xamarin 下创建和组合本地控件。 形成组合,从而降低 Xamarin 的复杂性。 形成 XAML 树(即复合控件)。
最后,象限 IV 表示特定于平台的 api,比如地理位置,外围设备的使用,比如蓝牙或 NFC,或者需要本地实现的 SaaS 集成/ sdk。
我们现在已经将我们的定制选项划分为特定的象限。 查看所有这些选项是如何位于这些象限上的,可以帮助我们识别特定场景的特定定制选项。 一般的经验法则是从象限 I 开始权衡选项,如果没有其他选项可用,就转向下一个象限。 当您移动到象限 III 和 IV 时,因为跨平台代码的数量减少了,项目的可维护性也减少了。 因此,不浪费时间,让我们从共享域定制选项开始,即象限 I 和 II。
Xamarin。 形式共享域
在第五章、使用 Xamarin 开发 UI中,我们使用了固有的 Xamarin。 表单控件及其样式属性来创建我们的 UI。 通过使用数据绑定和数据触发器,我们创建了数据驱动视图。 当然,可扩展性选项并不局限于此层上可用的控制属性。 呈现控件的行为和外观都可以使用标准定制和可扩展性选项进行修改。 让我们看看共享 Xamarin 中的不同定制选项。 表单域。
使用风格
在前面的一章中,在我们的 ShopAcross 应用中,当处理产品详细信息视图时,我们创建了一个简单的芯片容器来显示当前通过应用提供的项目的各种特性。
在前面的设置中,我们只对标签使用了Margin属性和VerticalTextAlignment:
<FlexLayout Direction="Row" Wrap="Wrap">
<Label Text="Feature 1" Margin="4" VerticalTextAlignment="Center" BackgroundColor="Gray" />
<Label Text="Feat. 2" Margin="4" VerticalTextAlignment="Center" BackgroundColor="Lime"/>
<!-- Additional Labels -->
</FlexLayout>
此流体布局设置创建包含特性名称的小矩形。 然而,外观和感觉与芯片的材料设计(例如,填充和圆角)略有不同。
现在让我们修改这些项目,使标签看起来更像芯片,以改善用户体验:
-
We will start by wrapping the label in a frame and then styling the frame. Open
ProductDetailsView.xamland add a frame outside each feature label. Then, move theBackgroundColorassignment to theFrameelement:<Frame BackgroundColor="Gray" CornerRadius="7" Padding="3" Margin="4" HasShadow="false"> <Label x:Name="Feat1" Text="Feature 1" VerticalTextAlignment="Center" HorizontalTextAlignment="Center" /> </Frame>这当然会为我们的 chip 创建一个更理想的查找:
![Figure 6.2 – Customized Chips]()
图 6.2 -定制芯片
然而,请注意,将这些属性添加到每个特性将创建一个完全冗余的 XAML 结构。
我们可以将公共属性定义提取为两个单独的样式(即,一个用于特征标签,一个用于框架本身),它们将应用于每个元素,从而减少冗余的机会。
-
为此,打开
App.xaml并在现有的ResourceDictionary中添加以下样式定义:<Style TargetType="Frame"> <Setter Property="HasShadow" Value="false" /> </Style> <Style TargetType="Frame" x:Key="ChipContainer"> <Setter Property="CornerRadius" Value="7" /> <Setter Property="Padding" Value="3" /> <Setter Property="Margin" Value="3" /> </Style> <Style TargetType="Label" x:Key="ChipLabel"> <Setter Property="VerticalTextAlignment" Value="Center" /> <Setter Property="HorizontalTextAlignment" Value="Center" /> <Setter Property="TextColor" Value="White" /> </Style> -
接下来,应用【显示】这些隐式(即
HasShadow="false"setter 将适用于所有应用级别上的帧)【病人】和**显式风格(注意x:Key【T2 宣言】和ChipLabel风格)Frame和Label控制:<FlexLayout Direction="Row" Wrap="Wrap" FlowDirection="LeftToRight" AlignItems="Start"> <Frame BackgroundColor="Gray" Style="{StaticResource ChipContainer}"> <Label x:Name="Feat1" Text="Feature 1" Style=" {StaticResource ChipLabel}" /> </Frame> <Frame BackgroundColor="Lime" Style="{StaticResource ChipContainer}"> <Label x:Name="Feat2" Text="Feat. 2" Style="{StaticResource ChipLabel}" /> </Frame> <!-- Additional Labels --> </FlexLayout> ```**
通过这样做,我们将减少 XAML 树中的杂乱和冗余。 样式可以在应用级别(如本场景所示)声明为全局样式使用App.xaml。 此外,还可以使用本地资源字典在页面和视图级别声明。
另一种样式化控件的方法是使用 CSS 样式表。 虽然这些样式表目前不支持 XAML 控件样式的全部范围,但它们可以被证明是强大的,特别是在使用 CSS 选择器时。 让我们开始学习 Xamarin 中的 CSS。 通过为我们的芯片视图重新创建样式:
-
首先,在
ShopAcross.Mobile.Client项目中创建一个名为Resources的新文件夹。 然后,添加一个名为Styles.css的新文件。 确保此文件的构建操作设置为EmbeddedResource。 -
Next, add the following style declarations to
Styles.css:.ChipContainerClass { border-radius: 7; padding: 3; margin: 3; } .ChipLabelClass { text-align: center; vertical-align: central; color: white; }对于那些不熟悉 CSS 的人,我们在这里创建了两个样式类,命名为
ChipContainerClass和ChipLabelClass。 -
现在,使用
StyleClass属性将这些样式类添加到Frame和Label控件中: -
In order to decrease the clutter, apply the style directly to the child label within the frame with the
ChipContainerClassstyle class (note that we will not need to use an explicit style declaration for theLabelelement):.ChipContainerClass { border-radius: 7; padding: 3; margin: 3; } .ChipContainerClass>^label { text-align: center; vertical-align: central; color: white; }.ChipContainerClass>label和.ChipContainerClass>^label的区别是,通过使用^(基类)符号,我们可以确保即使我们修改视图使用自定义控制从label推导,风格以同样的方式被应用。 -
现在,从
Label控件中删除ChipLabelClass声明。
如您所见,样式确实有助于减少 XAML 声明中冗余的机会。 通过这样做,您创建了一个更易于维护的项目。 样式还可以使用来创建绑定到系统主题或用户首选项的主题。 样式也可以与 Xamarin 一起使用。 表单行为不仅可以修改可视化,还可以修改元素的行为。
创造行为
行为是装饰器模式的的有力使用,允许开发人员修改他们的 Xamarin。 窗体控件,而无需创建派生控件。 在本节中,我们将创建一个简单的行为来演示行为如何帮助创建数据驱动的应用 UI。
为了演示行为的使用,让我们看一下下面的用户故事:
作为一名软件开发人员,我希望将 LoginView 中的验证行为委托给 UI 元素,以便在不同的控件中重用相同的行为,而不将其复制到不同的视图模型中。
您可能还记得,在LoginView中,我们实际上使用了Command.CanExecute委托来验证字段。 在本例中,我们将分离电子邮件字段和密码字段的验证器。 通过这种方式,我们可以允许 UI 将错误输入的结果反馈给用户。 这比只禁用登录窗口更加友好。 要设置它,请遵循以下步骤:
-
首先,创建一个验证规则基础设施,从验证接口开始。 在
ShopAcross.Mobile.Core项目中创建一个名为Common的文件夹,并添加一个名为IValidationRule:public interface IValidationRule<T> { string ValidationMessage { get; set; } bool Validate (T value); }的新接口
-
In order to implement the required validation, create a new class, called
RequiredValidationRule, deriving fromIValidationRule. We can also add a short validation message stating that this field is a required field:public class RequiredValidationRule : IValidationRule<string> { public string ValidationMessage { get; set; } = "This field is a required field"; public bool Validate (string value) { return !string.IsNullOrEmpty(value); } }现在,我们可以为
Entry字段创建验证行为,它将使用任何给定的验证规则(从RequiredValidationRule开始,这是我们刚刚实现的)。 -
For this, create a new folder, called
Behaviors, in theShopAcross.Mobile.Clientproject, and add a new class, calledValidationBehavior:public class ValidationBehavior : Behavior<Entry> { protected override void OnAttachedTo(Entry bindable) { base.OnAttachedTo(bindable); bindable.TextChanged += ValidateField; } protected override void OnDetachingFrom(Entry bindable) { base.OnDetachingFrom(bindable); bindable.TextChanged -= ValidateField; } private void ValidateField(object sender, TextChangedEventArgs args) { if (sender is Entry entry) { // TODO: } } }在这个实现中,
OnAttachedTo和OnDetachingFrom方法是关键的入口点和拆卸逻辑实现。 在此场景中,当行为附加到目标控件时,我们订阅TextChanged事件,当行为被删除时,我们从事件取消订阅,以便避免任何不希望的内存泄漏问题。 这个实现也很重要,因为考虑象限平面,我们通过向 Xamarin 添加这个验证行为从象限 I 跨越到象限 II。 形成Entry视图元素。 -
The next order of business is to implement a bindable property for the validation rule so that the validation rules are dictated by the view model (or another business logic module), decoupling it from the view.
为此,打开
ValidationBehavior类并添加以下 ing 属性:public static readonly BindableProperty ValidationRuleProperty = BindableProperty.CreateAttached("ValidationRule", typeof(IValidationRule<string>), typeof(ValidationBehavior), null); public static readonly BindableProperty HasErrorProperty = BindableProperty.CreateAttached("HasError", typeof(bool), typeof(ValidationBehavior), false, BindingMode.TwoWay); public IValidationRule<string> ValidationRule { get { return this.GetValue(ValidationRuleProperty) as IValidationRule<string>; } set { this.SetValue(ValidationRuleProperty, value); } } public bool HasError { get { return (bool) GetValue(HasErrorProperty); } set { SetValue(HasErrorProperty, value); } } -
现在我们有了验证规则的出口和输出字段(这样我们就可以给它附加额外的 UX 逻辑),为
ValidateField方法添加以下实现:private void ValidateField(object sender, TextChangedEventArgs args) { if (sender is Entry entry && ValidationRule != null) { if (!ValidationRule.Validate(args.NewTextValue)) { entry.BackgroundColor = Color.Crimson; HasError = true; } else { entry.BackgroundColor = Color.White; HasError = false; } } } -
接下来,使用适当的规则属性(在本例中为
UserNameValidation)扩展类: -
Now, bind the behavior to the validation rule that's exposed from the view model. Then, observe the
Entryfield behavior according to the text input:<Entry x:Name="usernameEntry" Placeholder="username" Text="{Binding UserName, Mode=OneWayToSource}" > <Entry.Behaviors> <behaviors:ValidationBehavior x:Name="UserNameValidation" ValidationRule="{Binding BindingContext.UserNameValidation, Source={x:Reference Root}}" /> </Entry.Behaviors> </Entry>附加行为时,您需要添加
ContentPage的x:Name属性声明的值在LoginView.xaml``Root(请查看前面的引用),引入 CLR 命名空间参考B【5】。这里,主要的好处是我们不需要修改
Entry字段,实现的行为可以作为一个单独的模块进行维护。重要提示
行为的绑定上下文与页面布局或视图不同,这就是为什么验证规则的绑定值源必须引用页面本身,并使用
BindingContext作为绑定路径的一部分。 -
要扩展这个实现,添加一个验证错误消息标签,该标签将与
HasError可绑定属性一起显示(可以在页面布局的任何地方,只要UserNameValidation元素是可访问的): -
结果将类似如下:

图 6.3 - Username 字段中的自定义行为
在这里,我们实现了一个验证行为,该行为可以绑定到应用核心层中的验证实现。 换句话说,我们已经创建了另一个象限二世之间的桥梁,即这种方式,实现可以使用每当我们需要实现一个类似的验证在项目中的任何条目,以及视图模型只是负责定义验证需求。 然而,使用这种实现,控件上的行为声明可能会再次导致较大窗体的混乱。 您可以使用附加属性来减少启用和禁用行为时重复代码的数量。 在下一节中,我们将带您完成附加属性的创建。
附加属性
另一种改变默认控件行为的方法是通过为现有控件声明一个可绑定的扩展来使用附加属性。 这种方法通常用于小的行为调整,例如启用/禁用其他行为和添加/删除效果。
我们可以通过使用附加属性(而不是行为)重新创建前面的场景来演示此实现。 让我们开始:
-
In order to implement such a behavior, first, we need to create a bindable property that will be used with the Xamarin.Forms view elements. For this, create a new folder, called
Extensions, in theShopAcross.Mobile.Clientproject, and add a new class, calledValidations:public static class Validations { public static readonly BindableProperty ValidateRequiredProperty = BindableProperty.CreateAttached( "ValidateRequired", typeof(bool), typeof(RequiredValidationRule), false, propertyChanged: OnValidateRequiredChanged); public static bool GetValidateRequired(BindableObject view) { return (bool)view.GetValue(ValidateRequiredProperty); } public static void SetValidateRequired(BindableObject view, bool value) { view.SetValue(ValidateRequiredProperty, value); } private static void OnValidateRequiredChanged( BindableObject bindable, object oldValue, object newValue) { // TODO: } }通过附加行为,可以直接访问静态类,以便它将附加属性设置到当前控件(而不是创建和添加行为)。
-
现在,删除行为声明并将附加属性设置到
usernameEntry视图:<Entry x:Name="usernameEntry" Placeholder="username" Text="{Binding UserName, Mode=OneWayToSource}" extensions:Validations.ValidateRequired="true" > -
接下来,实现属性更改处理程序
ValidateRequired。 这样,我们就可以将附加的属性插入和删除所需的验证到各种Entry视图:private static void OnValidateRequiredChanged( BindableObject bindable, object oldValue, object newValue) { if(bindable is Entry entry) { if ((bool)newValue) { entry.Behaviors.Add(new ValidationBehavior() { ValidationRule = new RequiredValidationRule() }); } else { var behaviorToRemove = entry.Behaviors .OfType<ValidationBehavior>() .FirstOrDefault( item => item.ValidationRule is RequiredValidationRule); if (behaviorToRemove != null) { entry.Behaviors.Remove(behaviorToRemove); } } } }
这里,我们创建了一个附加属性来修改 Xamarin 添加的行为。 表单元素。 记住,附加属性也可以用来修改视图元素的其他属性。
XAML 标记扩展
在本节中,您将学习如何使用并创建另一个非常实用的自定义选项:标记扩展。
到目前为止,当我们创建 XAML 视图时,我们使用了 Xamarin 所支持的几个标记扩展。 表单框架或 XAML 名称空间本身。 其中一些扩展如下:
x:Reference:用于指同一页面上的另一个视图。Binding:这在整个视图模型实现中都被使用。StaticResource:用于指样式。
这些都是由 Xamarin 中相关的服务实现解析的标记扩展。 形式框架。
为了满足应用中的特定需求,可以实现自定义标记扩展,以创建更具维护性的 XAML 结构。 要创建标记扩展,需要实现IMarkupExtension<T>类。 这取决于需要提供的类型。
例如,在前面的示例中,错误标签和字段描述符被硬编码到 XAML 视图中。 如果应用需要支持多个本地化,这可能会产生问题。
让我们使用下面的用户故事来实现一个自定义标记扩展:
作为用户,我希望将应用的 UI 翻译成我手机的语言设置,这样我就可以轻松地浏览包含本地语言内容的 UI。
这里,我们将实现LoginView的本地化。 以下步骤将指导你完成这个过程:
-
First, create a markup extension that will translate the associated text values. Then, create a class in the
Extensionsfolder, calledTranslateExtension:[ContentProperty("Text")] public class TranslateExtension : IMarkupExtension<string> { public string Text { get; set; } public string ProvideValue(IServiceProvider serviceProvider) { // TODO: return Text; } object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) { return (this as IMarkupExtension<string>).ProvideValue(serviceProvider); } }注意,
Text属性被设置为ContentProperty,这允许开发人员通过手动为扩展添加一个值来为该扩展提供一个值。 -
将扩展并入 e XAML 结构
LoginView.xaml:<Label Text="{extensions:Translate LblUsername}" /> <Entry x:Name="usernameEntry" Placeholder="username" Text="{Binding UserName, Mode=OneWayToSource}" > <Entry.Behaviors> <behaviors:ValidationBehavior x:Name="UserNameValidation" ValidationRule="{Binding BindingContext.UserNameValidation, Source={x:Reference Root}}" /> </Entry.Behaviors> </Entry> <Label Text="{extensions:Translate LblRequiredError}" FontSize="12" TextColor="Gray" IsVisible="{Binding HasError, Source={x:Reference UserNameValidation}}"/> -
The
ProvideValuemethod will, therefore, need to translate theLblUsernameandLblRequiredErrorkeys. Use the following implementation to achieve this:public string ProvideValue(IServiceProvider serviceProvider) { switch (Text) { case "LblRequiredError": return "This a required field"; case "LblUsername": return "Username"; default: return Text; } }这里,我们使用硬编码的值进行翻译。 但是,在实际实现中,您可以根据系统的当前区域性设置从 web 服务或资源文件加载值。
在本节中,我们为象限 II(即共享域)创建了各种定制。 我们演示了对风格、行为和附加属性的使用。 我们甚至创建了一个标记扩展来方便地翻译字符串资源。 您可能已经注意到,到目前为止,我们还没有接触本地项目。 我们的完整实现是在共享 UI 和核心项目上完成的。 在下一节中,我们将继续讨论象限 III 和自定义本地控件。
自定义本机域
UI 控件的本地自定义可以从简单的特定于平台的调整到创建完全自定义的本地控件来替换现有的平台渲染器。 在本节中,我们将在象限 III 上实现自定义,这与平台无关。 我们将进一步研究平台细节和 Xamarin 效果。
平台细节
而 UI 由 Xamarin 提供。 表单对于大多数 UX 需求来说是足够可定制的,可能需要额外的本机行为。 对于某些本机控件行为,可以使用目标控件的IElementConfiguration接口实现来访问特定于平台的配置。 例如,为了更改UpdateMode选择器(即Immediately或WhenFinished),您可以使用On<iOS>方法访问特定于平台的行为:
var picker = new Xamarin.Forms.Picker();
picker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
同样的可以在 XAML 中使用Xamarin.Forms.PlatformConfiguration.iOSSpecific命名空间实现:
<ContentPage
...
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core">
<!-- ... -->
<Picker ios:Picker.UpdateMode="WhenFinished">
<!-- Removed for brevity -->
</Picker>
<!-- ... -->
</ContentPage>
类似的平台配置可用于相同名称空间(即Xamarin.Forms.PlatformConfiguration)中的其他控件和平台。 这些特定于平台的附加属性通过公开可以在特定平台上操作本地控件的方法,在象限 III 和象限 II 之间创建了一座桥梁。 但是,如果我们想要修改的本地属性没有由特定的平台公开该怎么办? 我们可能需要使用效应。
Xamarin。 形式的影响
Xamarin 的。 形式效果是在跨平台域(象限 II)和本地域(象限 III)之间的完美桥梁。效果通常用于通过共享域暴露特定的平台行为或给定本地控件的实现。 这是为了确保实现不需要一个全新的自定义本地控件。
类似于 Xamarin。 表单视图/控件,影响分别存在于共享域和本地域的抽象和实现上。 虽然共享域用于创建路由效果,但本机项目负责使用它。
例如,让我们假设我们收到的产品项的详细信息实际上包含一些 HTML 数据,我们希望在应用中显示这些数据:
作为产品所有者,我希望在本地移动平台上重用 web 应用中基于 html 的产品描述。 这就使得内容在推送到移动平台之前不需要经过清理。
在这个场景中,我们知道 Xamarin 上的Label元素。 表单在 iOS 中使用UILabel呈现,在 Android 中使用TextView呈现。 虽然UILabel提供了AttributedString属性(可以从 HTML 创建),但 Android 平台提供了一个用于解析 HTML 的固有模块。 我们可以使用效果公开这些特定于平台的特性,因此启用 Xamarin。 表单抽象来接受 HTML 输入。 让我们开始:
-
创建路由效果,为平台效果提供数据。 然后,在
ShopAcross.Mobile.Client项目中创建一个新文件夹,ca 命名Effects,以及一个类HtmlTextEffect:public class HtmlTextEffect: RoutingEffect { public HtmlTextEffect(): base("ShopAcross.HtmlTextEffect") { } public string HtmlText { get; set; } } -
Now, we can use this effect in our XAML. Open
ProductDetailsView.xamland add the following effect to theDescriptionlabel:<Label Text="{Binding Description}"> <Label.Effects> <effects:HtmlTextEffect HtmlText="<b>Here</b> is some <u>HTML</u>" /> </Label.Effects> </Label>要注意,
HtmlText属性中的内容应该进行编码,以避免 XAML 编译问题,这点很重要。 另外,如果平台没有实现这种路由效果,标签仍然会显示绑定数据。现在,我们需要实现 iOS 效果,它将解析我们的效果的
HtmlText属性。 -
Create a new folder, called
Effects, in theShopAcross.Mobile.Client.iOSproject, and create a new class, calledHtmlTextEffect:[assembly: ResolutionGroupName("ShopAcross")] [assembly: ExportEffect(typeof(HtmlTextEffect), "HtmlTextEffect")] namespace ShopAcross.Mobile.Client.iOS.Effects { public class HtmlTextEffect: PlatformEffect { protected override void OnAttached() { } protected override void OnDetached() { } } }平台效应主要由两个主要组成部分组成:注册和实现。 在运行时环境中使用
ExportEffect属性的ResolutionGroupName注册的效果来解决在第一步中实现的路由效果。 另外,请注意,ExportEffect属性使用对特定于 ios 的属性的引用,因此您需要为当前命名空间(即ShopAcross.Mobile.Client.iOS.Effects)添加using语句。为了修改本机控件,您现在可以使用
PlatformEffect的Control属性。Element属性指 Xamarin。 需要这种效果的窗体控件。 -
现在,实现
OnAttached方法(该方法将在PlatformEffect解决时执行),如果附加的控件上存在PlatformEffect,则添加AttributedText:protected override void OnAttached() { var htmlTextEffect = Element.Effects .OfType<Client.Effects.HtmlTextEffect> ().FirstOrDefault(); if(htmlTextEffect != null && Control is UILabel label) { var documentAttributes = new NSAttributedStringDocumentAttributes(); documentAttributes.DocumentType = NSDocumentType.HTML; var error = new NSError(); label.AttributedText = new NSAttributedString(htmlTextEffect.HtmlText, documentAttributes, ref error); } } -
Android 平台的一个类似的实现将创建控件的 HTML 渲染。 使用
OnAttached属性添加 HTML 内容:protected override void OnAttached() { var htmlTextEffect = Element.Effects .OfType<Client.Effects.HtmlTextEffect> ().FirstOrDefault(); if (htmlTextEffect != null && Control is TextView label) { label.SetText( Html.FromHtml(htmlTextEffect.HtmlText, FromHtmlOptions.ModeLegacy), TextView.BufferType.Spannable); } }
结果屏幕应该显示硬编码文本,而不是提供的视图模型数据:

图 6.4 -使用效果修复 HTML 内容
虽然我们已经成功地在 Xamarin 上显示 HTML 内容。 在表单视图中,我们使用的值仍然是不可绑定的。 通过少量的重组和使用附加属性(即附加行为),我们可以同时使用数据绑定和效果。
复合定制
当行为和效果一起使用时,可以为常见的本地元素需求创建有说服力的解决方案,而无需求助于自定义控件和渲染器。
现在让我们扩展我们的效果,使用视图模型提供的数据:
-
Before we begin, let's first extend our
ProductViewModelclass with anIsHtmlproperty and add some sample data toHomeViewModel:yield return new ProductViewModel { Title = "First Item", IsHtml = true, Description = "<b/>Here</b> is some <u>HTML</u>;", Image = "https://picsum.photos/800?image=0" };并且,从我们结束的
HtmlText效果的地方继续,让我们创建一个附加的行为,它将允许我们打开或关闭 HTML 呈现。 -
在
ShopAcross.Mobile.Client项目的Extensions文件夹中创建一个类HtmlText -
The behavior of this attached property will result in the addition or removal of the HTML effect, depending on the
IsHtmlproperty declaration. Add the following code toOnHtmlPropertyChanged:if (newValue is bool isHtml && isHtml) { view.Effects.Add(new HtmlTextEffect()); } else { var htmlEffect = view.Effects .FirstOrDefault(e => e is HtmlTextEffect); if (htmlEffect != null) { view.Effects.Remove(htmlEffect); } }现在,我们可以修改 HTML 效果,以便它使用表单视图上现有的文本赋值,分别为 iOS 和 Android 平台创建
NSAttributedText和ISpannable。 -
Copy and replace the existing effects for both iOS and Android with the following code:
public class HtmlTextEffect: PlatformEffect { protected override void OnAttached() { SetHtmlText(); } protected override void OnDetached() { // TODO: Remove formatted text } protected override void OnElementPropertyChanged(PropertyChangedEventArgs args) { base.OnElementPropertyChanged(args); if (args.PropertyName == Label.TextProperty.PropertyName) { SetHtmlText(); } } private void SetHtmlText() { // Removed for brevity } }请注意,我们还使用了
OnElementPropertyChanged方法来侦听Text属性值的任何更改。 这将是绑定数据的主要访问点。 -
对于
SetHtmlText方法,在 iOS 中使用下面的方法,类似地,修改 Android 效果,使其使用Label元素的Text属性,而不是解决附加的效果:private void SetHtmlText() { if (Control is UILabel label && Element is Label formLabel) { var documentAttributes = new NSAttributedStringDocumentAttributes(); documentAttributes.DocumentType = NSDocumentType.HTML; var error = new NSError(); label.AttributedText = new NSAttributedString(formLabel.Text, documentAttributes, ref error); } } -
现在,我们将把行为添加到我们的 XAML:
<Label Text="{Binding Description}" effects:HtmlText.IsHtml="{Binding IsHtml}" />
现在,我们可以使用视图模型上的IsHtml附加属性来控制两个平台上显示的文本属性。
在实现了这个复合定制之后,我们将结束本节。 到目前为止,我们关注的是第三象限中的自定义,并尝试创建到象限 II 的桥梁。 我们使用了平台的细节,使用效果实现了本地控件修改,最后,我们创建了利用效果和附加属性的复合定制。 如果这些定制选项都不能提供所需 UI 的实际需求,那么可以考虑使用一个完整的定制控件实现作为替代选项。 在下一节中,我们将了解创建自定义控件时的各种不同选项。
创建自定义控件
就像任何其他开发平台一样,它也可以创建自定义视图/控件,这些视图/控件在外观、行为和渲染上都与开箱即用的 Xamarin 不同。 表单控件。 然而,创建自定义控件并不意味着完整的 Xamarin。 表单呈现的基础设施需要为目标平台和共享域实现。 根据用户体验和平台的需求,可能会出现以下情况:
- 自定义控件可以单独作为其他 Xamarin 的组合来创建。 表单控件。
- 现有 Xamarin 的。 窗体控件可以用不同平台上的自定义呈现器进行修改。
- Xamarin 的习俗。 可以使用自定义呈现器创建表单控件。
创建 Xamarin 表格控制
Xamarin 的。 窗体控件可以创建,原因有很多,其中之一是减少 XAML 树中的杂乱,并创建可重用的视图块。 让我们开始吧。
首先,我们将后退一步,看看我们之前为登录屏幕创建的可验证条目:
<Label x:Name="lblUserName" Text="..." />
<Entry x:Name="txtUserName" Placeholder=".." Text="..." >
<Entry.Behaviors>
<behaviors:ValidationBehavior x:Name="UserNameValidation"
ValidationRule="..." />
</Entry.Behaviors>
</Entry>
<Label x:Name="errUserName" Text="..." IsVisible="..."/>
控制块由与条目相关的标签和错误标签组成,错误标签只有在标签中存在验证错误时才可见。 密码字段也使用了类似的结构。 通过简单地公开两个绑定数据点,可以轻松地将该块转换为自定义控件。 以下步骤将指导您通过过程从该入口块中提取自定义控件:
-
In order to create the base control, we will use
ContentView. Add a new folder in theShopAcross.Mobile.Clientproject namedControls, and add aContentView, calledValidatableEntry, using the Forms ContentView XAML template:<ContentView xmlns:x=http://schemas.microsoft.com/winfx/2009/xaml xmlns:behaviors="clr-namespace:ShopAcross.Mobile.Client.Behaviors" x:Class=" ShopAcross.Mobile.Client.Controls.ValidatableEntry" x:Name="RootView"> <ContentView.Content> <StackLayout> <!-- TODO: // Insert Controls --> </StackLayout> </ContentView.Content> </ContentView>注意,这里使用了名称声明来创建对控件本身的引用。 这是因为我们将在控件上创建可绑定属性,并将它们绑定到我们之前标识的子值。
-
没有 w,在
ValidatableEntry.xaml.cs文件中创建我们的可绑定属性: -
We should also create accessors for these properties using the bindable properties as a backing field:
public string Label { get { return (string)GetValue(LabelProperty); } set { SetValue(LabelProperty, value); } }对
Placeholder、Value和ValidationRule属性重复相同的步骤。 -
接下来,将这些属性连接到视图元素的子属性,我们将把这些子属性添加到
ValidatableEntry.xaml:<StackLayout> <Label Text="{Binding Label, Source={x:Reference RootView}}" /> <Entry Placeholder="{Binding Placeholder, Source={x:Reference RootView}}" Text="{Binding Value, Mode=OneWayToSource, Source={x:Reference RootView}}" > <Entry.Behaviors> <behaviors:ValidationBehavior x:Name="ValidationBehavior" ValidationRule="{Binding ValidationRule, Source={x:Reference RootView}}" /> </Entry.Behaviors> </Entry> <Label Text="{Binding ValidationRule.ValidationMessage, Source={x:Reference RootView}}" FontSize="12" TextColor="Gray" IsVisible="{Binding HasError, Source={x:Reference ValidationBehavior}}"/> </StackLayout>中的
ContentView.Content节点中。 -
最后,用我们的自定义控件
<controls:ValidatableEntry Label="{extensions:Translate LblUsername}" Placeholder="{behaviors:Translate LblUsername}" ValidationRule="{Binding UserNameValidation}" Value="{Binding UserName, Mode=OneWayToSource}"/>替换
LoginView.xaml文件中的原始入口块:
在这里,我们创建了自定义的ContentView,它将可视化树的一个节点捆绑到单个控件中。 此控件还可用于其他需要验证的输入字段。
在本节中,我们专门讨论了在象限 II 中完成的一个实现。 我们使用现有的 Xamarin。 窗体基础结构,以创建可在具有多个本机元素的本机平台上呈现的自定义控件。 接下来,我们将看看如何为 Android 创建一个自定义渲染器,这样我们就可以利用内置的验证显示和浮动标签设计概念。
创建自定义渲染器
有时候,目标平台可以通过使用 Xamarin.Forms 的自定义控件提供超出我们预期需求的开箱即开的功能。 在这些类型的情况下,替换 Xamarin 可能是一个好主意。 使用自定义实现在特定平台上形成实现。
例如,在上一节中,我们试图通过自定义实现实现的表单输入字段,如果用遵循材料设计指南的TextInputLayout实现,将看起来更适合平台:

图 6.5 -材料设计浮动标签条目
在这个布局中,我们可以将标签绑定到浮动标签,将错误文本绑定到浮动标签编辑文本的帮助文本区域。 然而,默认情况下,Xamarin。 表单使用FormsEditText(是EditText的衍生品)而不是TextInputLayout用于 Android。 为了解决这个问题,我们可以实现自己的自定义渲染器,它将使用所需的控件。 让我们看看如何做到这一点:
-
The first step in creating a renderer is to decide whether to create a renderer deriving from
ViewRenderer<TView,TNativeView>or the actual render implementation. ForEntryRenderer, the Xamarin.Forms base class isViewRenderer<Entry, FormsEditText>. Unfortunately, this means that we won't be able to make use of the base class implementation since our renderer will need to returnTextInputLayoutto the native platform. Therefore, we will need to create a renderer from scratch. To do this, create a new folder, calledRenderers, in theShopAcross.Mobile.Client.Androidproject, and add a new class, calledFloatingLabelEntryRenderer. The renderer declaration should look like this:public class FloatingLabelEntryRenderer : ViewRenderer<Entry, TextInputLayout> { public FloatingLabelEntryRenderer(Context context) : base(context) { } private EditText EditText => Control.EditText; protected override TextInputLayout CreateNativeControl() { // TODO: return null; } protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { // TODO: } protected override void OnElementChanged(ElementChangedEventArgs<Entry> e) { base.OnElementChanged(e); // TODO: } }在这个声明中,我们首先应该处理几个覆盖方法,如下所示:
CreateNativeControl:它负责使用Element属性创建本机控件。OnElementChanged:行为和效果方面与OnAttached相似。OnElementPropertyChanged:这用于同步来自 Xamarin 的更改。 元素到本地元素。
-
对于
CreateNativeControl,我们需要创建一个EditText控件,就像标准的渲染器一样,但是我们还希望将其封装在TextInputLayout:protected override TextInputLayout CreateNativeControl() { var textInputLayout = new TextInputLayout(Context); var editText = new EditText(Context); editText.SetTextSize(ComplexUnitType.Sp, (float)Element.FontSize); textInputLayout.AddView(editText); return textInputLayout; }中。
-
对于
OnElementPropertyChanged,我们感兴趣的是Placeholder属性和相关的OneWay绑定(即从Element到Native)。 因此,我们将使用Placeholder值作为EditText字段的提示文本: -
除了属性之外,当
Element附加到渲染器(即初始同步)时,我们还想更新占位符: -
Another value we would like to keep in sync is the actual
Textvalue. However, here, the synchronization should be able to supportTwoWaybinding.为了监听输入文本的变化,我们将在渲染器中实现
ITextWatcher接口:public FloatingLabelEntryRenderer : ViewRenderer<Entry, TextInputLayout>, ITextWatcher { // … removed for brevity void ITextWatcher.AfterTextChanged(IEditable @string) { } void ITextWatcher.BeforeTextChanged(ICharSequence s, int start, int count, int after) { } void ITextWatcher.OnTextChanged(ICharSequence s, int start, int before, int count) { if (string.IsNullOrEmpty(Element.Text) && s.Length() == 0) { return; } ((IElementController)Element) .SetValueFromRenderer(Entry.TextProperty, s.ToString()); } } -
现在我们可以在元素更改时引入文本观察器:
if (e.OldElement == null) { var textView = CreateNativeControl(); textView.EditText.AddTextChangedListener(this); SetNativeControl(textView); } -
一旦渲染器完成,我们还需要注册渲染器,以便 Xamarin。 窗体 runtime 知道
Entry控件和这个新的渲染器之间的关联:[assembly: ExportRenderer(typeof(Entry), typeof(FloatingLabelEntryRenderer))] namespace ShopAcross.Mobile.Client.Droid.Renderers -
Now that the renderer is going to be handling both the label and the placeholder, we won't need the additional label within
ValidatableEntry, so we will only be using them for iOS:<ContentView> <OnPlatform x:TypeArguments="View"> <On Platform="iOS"> <Label Text="{Binding Label, Source={x:Reference RootView}}" /> </On> </OnPlatform> </ContentView> <Entry Placeholder="{Binding Placeholder, Source={x:Reference RootView}}" Text="{Binding Value, Mode=OneWayToSource, Source={x:Reference RootView}}" > <Entry.Behaviors> <behaviors:ValidationBehavior x:Name="ValidationBehavior" ValidationRule="{Binding ValidationRule, Source={x:Reference RootView}}" /> </Entry.Behaviors> </Entry>重要提示
我们用包装
OnPlatform声明的原因是,即使它在语法上是正确的,但由于反射的实现方式,向具有多个子视图的父视图添加一个视图是无法呈现的。 为了解决这个问题,需要将平台特定的声明包装成带有单个子视图的良性视图。最终的结果是这样的:

图 6.6 -浮动标签的自定义渲染器
注意,浮动的标签在 Android 平台的用户名字段中显示,而在 iOS 上,Label视图在用户名的Entry字段上呈现。
在本节中,我们实现了一个自定义的渲染器,它代替了 Android 平台的开箱即用的实现,作为对被渲染的本地控件的回报。 同样重要的是,我们用来处理文本更改的ITextWatcher接口是一个 Java 接口。 这确实证明了我们现在已经完全移动到象限 III,并正在接近象限 IV。我们可以进一步扩展这个实现,在自定义控制中包含误差指示器。 但是,这意味着我们需要创建一个自定义控件并将一个自定义渲染器附加到它。 在下一节中,我们将使用自定义呈现器实现一个完整的自定义控件。
创建自定义 Xamarin 表格控制
要创建一个完整的自定义控件,实现需要从 Xamarin 开始。 视图的抽象形式。 这种抽象提供了与 XAML 以及与该特定视图相关联的视图模型(即业务逻辑)的集成。
因此,对于浮动标签条目,我们需要创建一个具有必需的可绑定属性的控件。 对于我们的用例,除了Entry控件属性之外,我们还需要验证错误描述和标识是否存在此类错误的标志。 让我们开始实现我们的自定义控件:
-
我们将从
Entry本身派生自定义控件并添加附加属性开始。 为此实现,在ShopAcross.Mobile.Client项目的Controlsfolder 中创建一个新类,并使用以下类定义: -
现在,修改您的
FloatingLabelRenderer以使用新的控件作为TElement类型参数:[assembly: ExportRenderer(typeof(FloatingLabelEntry), typeof(FloatingLabelEntryRenderer))] namespace ShopAcross.Mobile.Client.Droid.Renderers { public class FloatingLabelEntryRenderer : ViewRenderer<FloatingLabelEntry, TextInputLayout>, ITextWatcher -
在渲染器中,我们需要监听任何
HasErrorProperty更改,并相应地设置错误描述和错误指示器。 那么,让我们将OnElementPropertyChanged展开如下:protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { .... else if (e.PropertyName == FloatingLabelEntry.HasErrorProperty.PropertyName) { if (!Element.HasError || string.IsNullOrEmpty(Element.ErrorMessage)) { EditText.Error = null; Control.ErrorEnabled = false; } else { Control.ErrorEnabled = true; EditText.Error = Element.ErrorMessage; } } .... } -
在
ValidatableEntry中使用这个控件来代替Entry控件,将创建一个愉快的材料设计布局:<controls:FloatingLabelEntry Placeholder="{Binding Placeholder, Source={x:Reference RootView}}" Text="{Binding Value, Mode=OneWayToSource, Source={x:Reference RootView}}" ErrorMessage="{Binding ValidationRule.ValidationMessage, Source={x:Reference RootView}}" HasError="{Binding HasError, Source={x:Reference ValidationBehavior}}"> <Entry.Behaviors> <behaviors:ValidationBehavior x:Name="ValidationBehavior" ValidationRule="{Binding ValidationRule, Source={x:Reference RootView}}" /> </Entry.Behaviors> </controls:FloatingLabelEntry>
结果页面现在应该将验证错误消息显示为中的TextInputLayout中的所谓提示:

图 6.7 -自定义浮动标签控件
重要提示
即使我们已经为 Android 和 iOS 创建并使用了这个自定义控件,因为 iOS 渲染器没有实现,iOS 仍然会显示继承树中下一个最好的东西(即EntryRenderer)。
在本节中,我们通过在 Xamarin 中实现一个自定义控件开始象限 II 中的实现。 形式共享域。 然后,我们继续创建一个自定义渲染器和一个自定义控件。
总结
总的来说,Xamarin 的。 表单对于不同的场景有许多扩展点。 然而,作为开发人员,我们应该谨慎地使用这些扩展点,以便创建健壮、简单且复杂的 ui。 在本章中,为了理解可用的定制选项,我们确定了 Xamarin 的实现域/象限。 表单应用,并介绍了每个象限的不同定制选项。 最后,我们创建了一个自定义控件来演示共享域和本地域中用户控件的完整实现。
本章结束了我们项目的 Xamarin 方面的开发工作。 在接下来的几章中,我们将继续使用。net Core 为我们的移动应用开发一个云基础设施。
七、面向移动应用的 Azure 服务
无论您是处理一个小型的启动应用,还是处理一个企业应用的大量数据,Microsoft Azure 总是一个方便的选择,因为它提供的低成本订阅模型和可伸缩性。 有许多可用的服务在不同的管理服务模式,如软件即服务(SaaS),平台即服务(PaaS)和【显示】基础设施作为服务(IaaS)。 这些功能包括通知中心、认知服务和 Azure 功能,它们可以改变用户对您的应用的印象,而不需要额外的开发时间。 本章将为您提供关于如何在开发。net Core 应用时使用这些服务的快速概述。
**在本章中,我们将使用 Azure 平台上提供的服务来设计我们的服务后端。 我们将首先浏览 Azure 平台上可用的服务,然后继续深入研究数据存储、Azure 无服务器 PaaS 产品,最后是开发服务。 以下主题将指导你完成本章:
- Azure 服务的概述
- 数据存储
- Azure Serverless
- 开发服务
在本章结束时,您将熟悉各种利用 Azure 服务的架构模型,并更好地理解如何将这些模型整合到您的移动应用项目中。 我们将仔细研究持久性服务以及 Azure 无服务器服务。 最后,我们将讨论 Azure DevOps 和 Visual Studio App Center。
Azure 服务概述
我们正生活在云计算的时代。 我们 10 年前学习并应用到应用中的许多软件范例现在已经完全过时了。 为了可维护性和性能,分布式模块已经取代了传统的 n 层应用和开发团队的简单性。
废话少说,让我们开始通过设置架构和探索 Azure 平台的概念来准备应用的范围。
分布式系统简介
在本节中,我们将讨论分布式后端系统的不同托管模型,以及这些设置的优缺点。
在本书前几章中,我们开始了客户端应用的开发; 这将需要一些额外的视图和修改。 为了继续开发,我们首先需要设置我们的后端。 对于我们的应用,我们需要一个服务后端,它将做以下工作:
- 提供关于产品的静态元数据
- 管理用户配置文件并维护用户特定的信息
- 允许用户上传和公开共享数据
- 索引和搜索用户上传和共享
- 使用实时更新通知一组用户
现在,把这些需求和我们创建云基础设施的目标放在一边,让我们试着想象一下,我们如何通过一个本地 n 层应用设置来实现一个分布式系统:

【t】【t】
图 7.1 - n 层本地后端
在上图所示的设置中,我们有一个 web 层,它向客户端公开封闭的逻辑 n 层结构。 请注意,系统被划分为逻辑层,并且不涉及无线通信。 我们将使用一个本地服务器来维护这个结构。 如果需要扩展,具有负载平衡实现的多个服务器仍然可以工作。 在大多数情况下,同步和规范化将在数据层进行。 从部署和管理的角度来看,每个部署都将导致一个完整的更新(在多个服务器上)。 此外,每个逻辑模块的需求必须单独维护,即使它是一个整体实现,而且不应该因为这些需求而轻视对本地服务器的应用更新。 部署到不同的服务器也必须小心处理。
重要提示
Knight Capital Group 是一家美国全球金融服务公司,专门从事销售和交易的电子执行。 2012 年 8 月,该公司的资产从 4 亿美元一夜之间破产,原因是该公司运营的 8 台服务器中只有 7 台发布了一项新部署。 在这个 45 分钟的噩梦中,正确的部署与旧代码在单个服务器上竞争,导致了 4.6 亿美元的损失。
我们可以轻松地将整个 web 应用移动到云 IaaS虚拟机(虚拟机)。 然而,这种迁移只会有助于维护,并且扩展仍然必须是在系统级别而不是组件级别。 在这种情况下,瓶颈很可能是数据层,因为伸缩的应用组件只会对数据存储库施加更大的压力。
为了理解这个 n 层设置,让我们仔细看看可能涉及到的组件。 我们将使用一个 SQL 数据库来存储数据,一个消息队列(如 Rabbit MQ 或 MSMQ)和一个 ASP.NET web API 实现。 身份管理可能是一个集成的解决方案,例如 ASP。 净的身份。 通知可以是来自客户端的轮询实现,或者可以考虑在 ASP 中使用 SignalR 实现。 净的 web 应用。 搜索功能可能必须在 SQL Server 级别上才能提高性能。 所有这些都是基于这样一个假设:我们使用的是 Microsoft . net 栈,目标托管平台是 Windows 主机上的 Microsoft IIS 服务器。
接下来,让我们将逻辑模块分解为更小的服务,这些服务可以在面向服务的体系结构(SOA)生态系统中相互通信

图 7.2 -微服务设置
与图 7.1 中的相比,前面截图中的设置要轻一些,图 7.1中的每个组件都可以独立开发和部署(也就是说,与系统中的其他元素解耦)。 从维护的角度来看,每个服务可以部署到单独的服务器或虚拟机上。 反过来,它们可以相互独立地进行缩放。 而且,这些服务中的每一个现在都可以被容器化,这样我们就可以完全将服务从操作系统中分离出来。 毕竟,我们只需要有一个 web 服务器,我们的服务集可以在其中托管和服务给客户端。 在这一点上,. net Core 将把我们的应用变成一个跨平台的 web 模块,允许我们同时使用 Windows 和 Unix 容器。 这整个过程可以被标记为从 IaaS 策略迁移到 PaaS 方法。 此外,应用现在可以实现一个基础结构作为代码(IaC)结构,在这个结构中,我们不需要担心应用正在运行的服务器的当前状态。
嗯,这听起来很棒,但是它与云架构和 Azure 有什么关系呢? 创建云就绪应用的主要目的是创建具有功能独立的模块的应用,这些模块可以由适当的、可维护的和可伸缩的云资源托管。 在这一点上,我们不再谈论单个应用,而是一组为各种应用需求携手工作的资源 ts:

图 7.3 - Azure PaaS 设置
在前面的分布式模型图中,每个组件都由简单的 PaaS 服务组成,它们之间没有直接依赖关系。 这些组件是完全可扩展的,只要满足系统要求,就可以更换。 例如,如果我们从一个小的 web API 应用服务开始,它可能驻留在一个应用服务计划中。 然而,如果满足了这些需求,那么我们就可以用 Azure 的功能实现来替换这个微服务,它将改变部署模型和执行运行时,但仍然保持系统的完整性。 总的来说,在云模型中,单个组件的可替换性(只要整个系统处于检查状态)将风险和维护工作降到最低。
回到我们的需求,我们可以在关系数据库(如 SQL Server PaaS)或使用 Cosmos DB 的 NoSQL 设置之间自由选择。 此外,我们可以通过在数据存储和 web 网关之间使用 Redis 缓存来提高性能。 搜索功能可以使用 Azure 搜索索引执行,应用服务和 Azure 函数可以用于 API 层。 此外,一个简单的 ESB 实现或 Azure 持久函数可以帮助实现长时间运行的异步操作。 最后,通知可以通过使用 Azure SignalR 或通知中心来执行。
当然,资源的选择很大程度上取决于所选择的体系结构方法。
云架构
在本节中,我们将分析云托管分布式应用和相关 Azure 服务产品的几种架构模式。
在云平台中,系统的设计由单个组件组成。 虽然应该分别设计和开发每个组件,但这些组件的组合方式应该遵循特定的体系结构模式,这些模式将允许系统提供弹性、可维护性、可伸缩性和安全性。
特别是对于移动应用,下面的一些组合模型有助于应用的成功。
为了关联这些模式,我们将讨论我们的 ShopAcross 应用,所以让我们定义一个新的用户故事,我们将在接下来的部分工作:
作为产品所有者,我想引入一个拍卖功能,用户可以为他们的商品创建帖子,比如要拍卖的车辆,这样我就可以增加我的应用的目标群体。
该特性增加了对应用持久性存储的数据输入/输出,并增加了后端服务的复杂性。 现在让我们看看几个可以帮助我们实现这一目标的模型。
门户聚合
在微服务设置中,应用由多个域组成,每个域实现自己的微服务对等体。 由于域是隔离的,因此可以通过执行对后端服务的多个调用来构造客户机应用视图所需的数据。 在一个不断发展的应用生态系统中,这将及时将业务层的所有复杂性推到客户端应用中。 虽然这对于 web 应用来说仍然是可以接受的,但移动应用的性能将随着系统复杂性的增长而降低。 为了避免这个问题,可以在客户端应用和微服务之间放置一个网关服务 façade:

图 7.4 -网关聚合设置
让我们考虑在应用中应用相同的逻辑。 让我们假设拍卖车辆的数据由一个 API(业务项)处理,车辆的元数据由另一个 API(静态数据)处理,用户信息通过另一个 API 提供,最后,我们有一个投标 API。 虽然这种设置为微服务设置提供了必要的隔离,但它要求客户端应用执行多个服务调用来查看和/或创建单个发布。 在这样的场景中,可以使用网关来编排微服务,以便客户机应用可以免除此责任。
事实上,如果我们计划将 web 应用作为客户端来支持,那么数据模型和服务编排可能与移动应用不同。 在这种情况下,我们需要考虑为每个客户端应用创建单独的网关,从而降低单个超级网关的可维护性成本。
后端,前端
在多客户端系统中,每个客户端可能需要以某种方式聚合数据。 这将取决于目标平台资源、技术可行性和用例。 在这种场景中,需要网关 API 为不同的微服务和数据组合公开多个服务端点。 相反,每个客户端应用可以通过单独的网关提供数据,从而减少通过单个 façade 支持多个客户端应用的复杂性:

图 7.5 -前端的后端
在前面的示例中,我们设置了三个单独的网关(换句话说,是 Gate-A、Gate-B 和 Gate-C),以支持三个不同的客户机应用。 每个网关实现自己的聚合模型,而不是创建一个复杂的 façade,其中充满了特定于客户端的调整。
例如,让我们假设我们的应用开发团队在一个针对 iOS 和 Android 平台的原始移动应用之上实现了一个 UWP 应用。 在这种情况下,UWP 视图将在更大的设计范围内查看,并且数据需求将不同于移动应用。 对于一个简单的解决方案,网关 API 端点现在可以使用参数来扩展,以限制或扩展响应中返回的对象树(即,info、normal 或 extended),或者可以引入额外的Get{Entity}Extended端点。 然而,通过这种方式,在最小化客户端应用复杂性的同时,我们正在导致网关的增长,并降低了这一层的可维护性。 如果我们引入单独的网关,我们将为已经拥有单独应用生命周期的客户端分离这些 api 的生命周期。 这可以帮助创建一个更易于维护的系统。
但是,如果在整个客户机应用的执行过程中有某些重复的组合或聚合,该怎么办? 这些重复模式可以解释为数据设计问题,其中数据隔离导致性能下降。 事实上,如果微服务设置需要这些领域分离,那么我们将需要在数据存储级别上提出一个数据组合。
物化视图
某些数据维度的聚合可以在数据存储级别上完成。 事实上,作为具有 SQL 背景的开发人员,我们熟悉可以由多个关系表组成的 SQL 视图,并且可以在数据存储级别上进行索引。 虽然这提供了对可用数据的不同视角,但它甚至可以创建多个特定于领域的数据模型的聚合。 尽管如此,这些视图仍然只是一个运行时抽象。 此外,如果持久性存储分散在多个服务器上,我们将需要一个独立的进程来同步域存储和另一个聚合存储之间的数据,并持久化非规范化的数据。 换句话说,具体化视图。 类似的策略可以应用于 Cosmos 等 NoSQL 数据库。
例如,可以使用 Azure Cosmos DB 更改提要在 Cosmos DB 上执行这个数据反规范化过程。 一个文档集合上的更改可以跨多个集合进行同步,这些集合为执行各种搜索或聚合数据定量进行了优化:

图 7.6 - Azure 函数的反规范化数据
例如,回到我们的拍卖功能,当我们处理搜索功能时,我们将对多个文档集合执行搜索; 也就是说,用户将需要根据车辆、拍卖数据、出价和个人资料数据进行搜索。 换句话说,不同维度上的数据点都应该通过内部连接用于搜索执行。 这可以通过使用车辆岗位的汇总表来实现,允许可搜索字段在集合之间同步。
cache-aside 模式
缓存是另一个可以帮助提高应用性能的因素,也就是说,我们缓存的数据类型和我们缓存这些信息的应用层。 cache-aside 模式是一个多路复用器的实现,它将处理缓存存储和数据存储之间的数据一致性,这取决于传入的请求和数据寿命:

图 7.7 - Cache-aside 实现
在这种设置中,首先在缓存存储中搜索带有特定惟一标识符(例如{EntityName}_{EntityId})的传入请求,如果没有,则从数据存储中检索并插入到缓存中。 通过这种方式,下一个请求将能够从缓存中检索数据。
在缓存还是不缓存的困境中,数据熵可能是一个基本的决定因素。 例如,缓存静态引用项的数据可能是有益的; 然而,缓存拍卖信息,其中的数据是不纯的,并且对相同数据点的重复请求比静态引用更不可能,不会为系统提供额外的价值。
缓存边策略也可以在客户端使用本地存储(如 SQLite)来实现。 有时,在服务器端缓存某个没有意义的文档集合可以在客户端缓存。 例如,当前用户的特定车型和模型的车辆元数据可能是重复请求模式; 然而,考虑到该数据的熵以及其他用户对同一项的访问频率,它不会是一个服务器缓存维度。
基于队列的负载均衡
消息队列不是一个新概念,也不是云架构所独有的。 然而,在具有微服务的分布式系统中,它们可以帮助解耦服务,并允许您控制资源的利用。 为可伸缩性和性能而设计的无服务器组件,如 Azure Functions,可以为云基础设施中的工作队列提供优秀的消费者。
例如,让我们考虑一个应用用例,其中注册用户正在创建一个拍卖项目。 他们已经选择了车型和型号,添加了额外的信息,甚至还添加了几张照片。 此时,如果我们允许将拍卖项目的发布作为一个同步请求,我们将把管道中的某些模块锁定为单个请求。 首先,请求需要在数据存储中创建一个文档; 但是,系统中还会触发其他功能来处理映像、通知订阅的用户,甚至启动内容管理员的审批流程。 现在,假设这个请求由应用的多个用户执行(例如,多个注册用户创建多个帖子)。 这将导致资源利用率达到峰值,进而将应用的恢复力和可用性置于风险之中。
作为这个问题的解决方案,我们可以创建一个消息队列,由 Azure 函数使用,它将协调拍卖数据的创建。 消息队列可以是企业服务总线,也可以是 Azure sto 狂暴队列:

图 7.8 -基于队列的请求处理
这听起来很棒,但是这个实现如何影响客户端实现? 客户机应用需要实现一种轮询策略来检索异步作业的状态,或者可以使用推拉机制来通知它,在推拉机制中,服务器将在进程排队之前首先发送拍卖 ID。 然后,当它完成时,服务器可以用相同的 ID 通知客户机,允许它提取完成的服务器数据。 此时,可以存储本地版本的数据并将其提供给用户,直到实际的服务器数据可用为止。 对于这种类型的通知,可以使用 Azure SignalR 或 Notifications Hub 等通知机制。
竞争消费者
在前面的示例中,我们使用 Azure 函数作为消息队列的消费者。 这种方法已经可以作为竞争消费者的实现来接受,其中提供的消息队列由多个工作模块处理。
虽然这将提供可伸缩需求并允许性能执行,但作为产品所有者,我们将无法控制创建来使用消息队列中的事件的函数实例。 为了能够控制和管理队列,可以引入消息代理机制,该机制将控制进入队列的消息流。 消息被推入队列后,多个使用者可以检索、处理和完成消息。
发布者/订阅者的模式
让我们假设我们已经完成了代理队列的实现,并分派了一个使用者来完成一个长时间运行的操作。 此时,正如前面提到的,我们的应用正在期待一个完成信号,以便摆脱任何瞬时数据。
在一个开放的系统,就像我们在实施的过程中,每个服务可以相互通信(而不是一个封闭的系统,执行顺序处理下游),我们面对的不再是一个确定性的同步模型,然而系统的消费者仍然期望结果。 为了允许源系统(即发布者)将操作的输出传播给相关方(即订阅者),可以建立输出通道。 这个实现模式可以归因于发布者/订阅方模式(也称为发布/订阅模式)。
回到我们的异步 web 请求,输出通道然后将结果传递给通知模块,并将结果传递给客户端应用。
同样的模式实现可以通过使用服务总线的另一个消息队列或者使用 Azure 基础设施 EventGrid 上的 pub/sub 模式的实际实现来建立。 这些服务中的任何一个都可以允许长时间运行的流程的输出向相关方散开,比如一个 Azure 函数,它将在 Azure SignalR 上推送通知消息或触发消息。
断路器和重试模式
在一个云系统中,涉及多个移动的块,很难避免失败。 在这种情况下,系统的弹性取决于它从故障中恢复的速度和频率。 断路器和重试模式是微服务生态系统中通常引入的互补模式。 如果即将发生故障,可以使用断路模式来减少系统的时间和资源。 在这些类型的情况下,最好允许系统尽早失败,而不是推迟失败,以便由辅助进程处理故障或启动故障转移机制。
例如,如果我们有一个容易超时的服务(例如,负载过重或由于外部服务故障),可以实现一个断路器,以在闭路状态下连续监视传入的请求。 对于客户端应用,可以无缝地重试失败。 当后续的故障发生时,电路可以暂时处于半开或开的状态,这样下面的请求在不尝试执行的情况下立即被丢弃(知道它可能会失败,直到问题得到解决)。 在这种状态下,客户端应用可以禁用该特性,或者,如果实现了故障转移/解决方案,则可以使用该实现。 一旦电路开放状态过期,系统可以重新引入这个端点,首先是半开放状态,最后是关闭状态,系统就被说成痊愈了。
内置的 Azure 监控功能以及应用遥测功能可以提供警报和通知,这有助于维护 Azure 应用。
Azure 服务提供商和资源类型
在前面的小节中,我们分析了各种模型并提到了几种 Azure 产品。 在本部分中,您将了解 Azure 生态系统中这些服务产品是如何组织的。
Azure 生态系统及其提供的不断增长的服务集允许开发人员轻松地创建各种分布式云应用。 正如我们在云架构一节中看到的,许多 PaaS 和 SaaS 产品通过设计可伸缩和有弹性的应用,为日常问题创建了一个解决方案目录。
通过快速查看服务目录(不完整),您会注意到每个服务都是作为提供者类别的一部分提供的:

图 7.9 - Azure 服务提供
每个类别中的服务由一个或多个服务提供商提供。 这些目录中的每个服务都进行了版本管理,以便 Azure Resource Manager 能够处理这些服务的供应。
为了可视化同一屋檐下可用的提供商数量,你可以使用 Azure PowerShell 模块:
Get-AzResourceProvider -ListAvailable | Select-Object ProviderNamespace, RegistrationState
这将返回一组可供您订阅的提供商。 这些提供商可以是微软提供的模块或第三方提供的:

图 7.10 - Microsoft Azure 提供商
重要提示
Microsoft Azure 文档提供了有用的 Azure PowerShell 命令,这些命令可以直接在 Cloud Shell 中执行,而无需使用 PowerShell(在 Windows 上)或 Bash(在 Linux 或 macOS 上)。 此外,跨平台版本 PowerShell (Core)在非 windows 操作系统上也可用,它利用了。net Core。
如果你潜水到一个特定的名称空间,例如,Microsoft.Compute提供者名称空间,你可以得到一个更好的提供的服务的概述,和这些资源的地理区域中可以看到 f 问题从截图:

图 7.11 -微软。 计算提供者服务
在 Azure 资源组中,Resource类型定义了我们真正需要的资源以及该资源的版本。 这些资源定义,如果作为资源组Azure 资源管理器(ARM)模板的一部分准备,就构成了我们的声明式 IaC。
ARM 是一个平台服务,它允许在订阅中提供资源。 它公开了一个 web API,可以使用 PowerShell、Azure CLI 和 CloudShell,以及 Azure 门户本身。 资源管理器模板中使用的声明式语法提供了一致的、幂等的部署体验,这允许开发人员和自动化工程师自信地管理基础设施生命周期。
在本节中,讨论了几种分布式应用模型以及体系结构模式。 我们还简要地看了一下 Azure 提供目录中的资源组和提供者。 在下一节中,我们将重点讨论 Azure 上可用的持久性存储服务。
数据存储
定义域并创建我们的分布式系统将要构建的体系结构,从决定持久性存储开始。 反过来,可以定义数据域,并指定访问模型。 在大多数情况下,这个决策不需要局限于单个数据存储,但是系统可以使用多种数据类型和不同的数据存储。 Azure 平台提供了具有不同数据管理概念和特性集的各种资源。 选择最适合应用需求并考虑成本和管理的数据存储模型是很重要的。 现在让我们看看这些不同的模型以及何时使用它们。
关系数据库资源
关系数据库可能是数据存储中最重要的应用。 事务一致性实现原子,的,,持久【显示】(酸)原则为开发人员提供了一个强大的保证一致性。 然而,从可伸缩性和性能的角度来看,在大多数情况下,常见的 SQL 实现(如 MSSQL 或 MySQL)比 NoSQL 数据库(如 Mongo 和 Cosmos DB)的性能要好。 Azure SQL Database、Azure Database for MySQL 和 PostgreSQL 在 Azure 平台上都可以作为 IaaS 和 PaaS 产品。
在 PaaS 资源模型中,数据库的操作成本和可伸缩性是通过一个称为数据库事务单元(DTU**)的单元来处理的。 这个单元是一个抽象的基准测试,它使用 CPU、内存和数据 I/O 度量来计算。 换句话说,DTU 不是一个精确的度量,而是根据上述度量的标准化值。 微软提供了一个 DTU 计算器,它可以根据在实时数据库上收集的性能计数器来估计 DTU 的使用情况。
从安全性的角度来看,Azure SQL 数据库有几个高级特性。 这些安全特性可用于不同级别的数据可访问性:
- 网络安全由防火墙维护,访问权限由 IP 和虚拟网络防火墙规则显式授予。
- 访问管理实现包括 SQL 认证和 Azure Active Directory 认证。 安全权限可以像数据表和行一样进行粒度化。
- 威胁保护可以通过日志分析和数据审计以及威胁检测服务提供。
- 通过不同级别的数据屏蔽和加密进行信息保护,保护了数据本身。
正如您所看到的,作为最保守的数据模型之一,它在 Azure 平台上仍然非常流行,可以作为 IaaS 和 PaaS 产品使用。 现在我们已经讨论了关系数据库,让我们转向更“自由”的 NoSQL 数据存储模型。
Azure 存储
Azure 存储模型是云生态系统中最古老的服务之一。 它是一个 NoSQL 存储,为开发人员提供了一个持久的、可伸缩的持久层。 Azure 存储由四个不同的数据服务组成,每个服务都通过 HTTP/HTTPS 和一个完善的 REST API 进行访问。
让我们仔细看看 Azure 存储中可用的这些数据服务。
Azure 斑点
Azure Blob 存储是为非结构化数据提供的云存储。 blob 可用于存储任何类型的数据块,如文本或二进制数据。 Azure Blob 存储可以通过为创建的存储帐户提供的 URL 访问:
http://{storageaccountname}.blob.core.windows.net
每个存储帐户至少包含一个容器,用于组织创建的 blob。 三种类型的 blob 用于不同类型的数据块上传:
- 块斑点:这些是为大二进制数据设计的。 一个块块的大小可以达到 4.7 TB。 每个块块由更小的数据块组成,这些数据块可以单独管理。 每个块可以容纳高达 100 MB 的数据。 每个块应该定义一个块 ID,它应该符合 blob 中特定的长度。 块块可以看作是离散的存储对象,比如本地操作系统中的文件。 它们通常用于存储单个文件,如媒体内容。
- 页团:当需要随机读写操作时,使用。 这些斑点由 512 字节的页面组成。 一个页面 blob 可以存储高达 8tb 的数据。 为了创建一个页面 blob,应该指定一个最大的大小。 然后,通过指定与 512 字节页面边界对齐的偏移量和范围,可以在页面中添加内容。 存储在云中的 vhd 非常适合页面团的使用场景。 事实上,为 Azure VM 提供的持久磁盘是基于页面 blob 的(也就是说,它们是 Azure IaaS 磁盘)。
- 追加斑点:正如名称所示,这些是仅追加斑点。 它们不能被更新或删除,并且不支持对单个块的管理。 它们经常用于记录信息。 一个附加 blob 可以增长到 195 GB。
如您所见,blob 存储,特别是块 blob,是存储应用图像内容的理想选择。 Azure Storage Client 库方法为 blob 提供了对 CRUD 操作的访问,可以直接在客户端应用中使用。 然而,通常使用后端服务来执行实际的上传到 blob 存储是一种安全感知的方法,这样 Azure 安全密钥就可以保存在服务器中而不是客户机中。
Azure 文件
Azure 文件可以视为一个云托管的文件共享系统。 可以通过服务器消息块(SMB)访问它,也称为 Samba,并允许在混合(即本地和云)场景中使用存储资源。 使用网络共享文件夹(甚至本地文件)的遗留应用可以很容易地指向 Azure 文件网络存储。 Azure 文件,就像任何其他 Azure 存储数据服务一样,可以通过 REST API 和 Azure 存储客户端库访问。
Azure 队列
为了实现异步处理模式,如果您不追求高级功能和队列一致性,那么 Azure 队列可以成为服务总线之外的划算的替代方案。 Azure 队列可以更大,更容易实现和管理,以实现更简单的用例。 与服务总线类似,Azure 队列消息也可以与 Azure 函数一起使用,其中每个消息触发一个 Azure 函数来处理处理。 如果不使用触发器,那么只有轮询机制可以处理消息队列。 这是因为,与服务总线不同,它们不提供阻塞访问或事件触发机制,如服务总线上的OnMessage。
Azure 表
Azure 表是一个 nosql 结构的云数据存储解决方案。 Azure 表存储的实现遵循一种键值对(KVP)方法,其中没有公共模式的结构化数据可以存储在表存储中。 Azure Table 存储数据可以很容易地在 Azure 门户上可视化,并且通过其他 Azure 存储服务等 Azure storage 客户端库支持数据操作。 然而,Azure 表存储现在是 Azure Cosmos DB 的一部分,可以使用 Cosmos DB 表 API 和 SDK 访问。
Cosmos DB
Cosmos DB 是微软在 Azure 云上提供的 multi-façade,全球分布式数据库服务。 Azure Cosmos DB 的主要优点是可伸缩性和可用性,因此它是任何基于云计算的项目的强有力的候选人。 作为一个写优化的数据库引擎,它保证在全球范围内 99%的读/写查询上的延迟小于 10 毫秒。
Cosmos DB 为开发人员提供了五种不同的一致性模型,以根据需求在性能和可用性之间实现最佳折衷。 所谓的一致性频谱定义了强一致性和最终一致性之间的不同级别,或者换句话说,更高的可用性和更高的吞吐量。
尽管它被设计为 NoSQL 存储,但它确实支持各种存储模型协议,包括 SQL。 这些存储协议支持使用现有的客户端驱动程序和 sdk,并且可以无缝地替换现有的 NoSQL 数据存储。 每个 API 模型也可以通过 gh 使用可用的 REST API:

图 7.12 - CosmosDB 访问模型
由于它提供了各种访问模型,CosmosDB 正在成为 Azure 平台上数据持久性的首选服务。 但是,如果我们要处理的数据比我们在长期持久性存储中存储的数据类型更不稳定,该怎么办呢? Azure 缓存的 Redis 可以是一个伟大的解决方案,临时数据是使用在这些类型的场景。
Azure Cache for Redis
Azure Cache for Redis 是一个提供类似 Redis 的内存数据结构存储的提供商。 通过减少实际持久性存储的负载,它有助于提高分布式系统的性能和可伸缩性。 使用 Redis,数据存储为 kvp,由于其复制特性,它也可以用作分布式队列。 Redis 支持以原子方式执行事务。
在 Azure Cache 的帮助下,我们将在应用后端使用缓存模式来实现 Redis。
在本节中,我们浏览了 Azure 上用于数据存储的 PaaS 产品。 在下一节中,我们将了解另一个对分布式系统的可维护性和成本有很大帮助的 PaaS 服务:Azure 无服务器。
Azure 无服务器
您可能已经注意到,在现代云应用中,PaaS 组件比 IaaS 资源更丰富。 在这里,应用虚拟机被更小的应用容器取代,数据库作为平台取代了集群数据库服务器。 Azure 无服务器将基础设施和平台管理向前推进了一步。 在无服务器资源模型(如 Azure Functions)中,事件驱动的应用逻辑在由平台本身提供、扩展和管理的平台上按需执行。 在 Azure 无服务器平台中,事件触发器可以从消息队列到网络钩子等多种多样,可以内在地集成到生态系统中的各种资源中。
Azure 功能
Azure 功能是管理的、事件驱动的逻辑实现,可以为云架构提供轻量级的特别解决方案。 在 Azure 函数中,工程团队不仅忽略了执行基础设施,也忽略了平台,因为 Azure 函数是用。net Core、Java 和 Python 跨平台实现的。
Azure 函数的执行从触发器开始。 Azure 功能支持多种执行模型,包括:
- 数据触发器:
CosmosDBTrigger、BlobTrigger - 周期触发器:
TimerTrigger - 队列触发:
QueueTrigger,ServicesBusQueueTrigger,ServiceBusTopicTrigger - 事件触发:
EventGridTrigger和EventHubTrigger
Azure 函数的触发器在函数清单/配置中定义:function.json。
触发器一旦实现,函数运行时就执行 Azure 函数的 run 块。 传递给运行块的请求参数(即输入绑定)由所使用的触发器决定。
例如,下面的函数实现是由 Azure 存储队列中的一个消息条目触发的:
public static class MyQueueSample
{
[FunctionName("LogQueueMessage")]
public static void Run(
[QueueTrigger("%queueappsetting%")] string
queueItem, ILogger log)
{
log.LogInformation($"Function was called with:
{queueItem}");
}
}
输出参数(即输出绑定)也可以定义为函数声明中的out参数:
public static class MyQueueSample
{
[FunctionName("LogQueueMessage")]
public static void Run(
[QueueTrigger("%queueappsetting%")] string
queueItem,
[Queue("%queueappsetting%-out")] string outputItem
ILogger log)
{
log.LogInformation($"Function was called with:
{queueItem}");
}
}
注意,[Queue]属性在 Azure 函数的队列存储绑定中用作输出绑定,它将在另一个队列中创建一个新的消息条目。 对于 Azure 函数,还有更多类似的开箱即用绑定类型。
重要提示
在这些例子中,我们使用了 c#和。net Standard 来创建已编译的 Azure 函数。 基于脚本的 c#、Node.js 和 Python 也是使用类似方法创建函数的选项。
从概念上讲,Azure 函数可以被视为逐调用的 web 服务。 然而,作为 Azure 函数的扩展,持久性函数允许开发人员创建持久性(即有状态的)函数。 这些函数允许您使用检查点编写有状态函数,其中编排器函数可以分派无状态函数并执行工作流。
Azure 函数既可以作为在特定触发器上执行业务逻辑的单个模块使用,也可以作为一组强制工作流(使用持久函数)使用; 或者,它们可以作为Azure Logic Apps的处理单元。
Azure Logic 应用
Azure Logic 应用是声明式工作流定义,用于编排任务、流程和工作流。 与函数类似,它们可以与许多其他 Azure 资源以及外部资源集成。 逻辑应用是使用 JSON 应用定义模式创建、版本控制和配给的。 Azure 门户的和 Visual Studio 都提供了设计器:

图 7.13 -逻辑应用设计器
逻辑应用的任务不仅限于 Azure 功能,还包括所谓的商业和/或第三方连接器(例如,使用 Twilio 发送短信,使用 SendGrid 发送电子邮件,或发布 Tweet)。 此外,企业集成包(EIP)提供行业标准消息传递协议。
与 Azure 函数一样,逻辑应用的执行从触发器开始,每个输出步骤都存储在执行上下文中。 处理块,如条件、开关和foreach循环在应用流中是可用的。 此外,逻辑应用工作流可以通过Azure 事件网格事件进行调度。
Azure 事件网格
Azure Event Grid 是一个基于云的事件聚合实现,它支持发布/订阅事件路由策略。 事件网格由事件源(即发布者)和事件订阅(即使用者)组成:

图 7.14 -事件网格参与者
与 Azure 函数触发器类似,开发人员可以使用各种事件源,Azure 事件网格可用于将某些事件从一个 Azure 资源路由到另一个 Azure 资源,或将某些事件多播。 事件不需要由系统资源触发,但也可以使用 HTTP 请求创建事件,从而允许自定义模块向消费者发送事件。
使用事件网格,我们完成了本节。 在本节中,我们讨论了 Azure 函数和逻辑应用,最后讨论了事件网格,它可以用作应用中使用的其他 Azure 组件之间的中介。 现在我们已经浏览了可以在应用中使用的服务,接下来让我们看看可以帮助我们管理项目生命周期的服务。
开发服务
Azure 资源不仅仅局限于应用生命周期中提供和维护的应用需求。 它们还包括某些用于实现应用生命周期和开发管道的平台服务,例如 Azure DevOps 和 Visual Studio App Center。 在本节中,我们将了解这些免费增值服务,在本书的其余部分中,我们将使用它们来管理应用的开发和部署。
Azure DevOps
AzureDevOps(以前称为 TFS 在线或 Visual Studio 团队服务),开始在微软应用生命周期管理(ALM)本地产品套件 TFS,现在是利用最广泛的免费商业模式管理门户。 Azure DevOps 实例可以从 Azure Portal 创建,也可以通过 Azure DevOps 门户创建。 这个采购过程从创建 DevOps 组织开始。
一旦创建了组织,就可以创建一个包含源控制存储库和待办事项列表的新项目。 在项目设置的Advanced部分中,可以使用中的 ALM 过程和版本控制选项:

图 7.15 - Azure DevOps 项目设置
需要指出的是,TFVC 和 Git 存储库是同时可用的。 一个单独的 Azure DevOps 项目可能包含多个存储库。 由于跨平台支持和与 ide(如 Visual Studio Code 和 Visual Studio for Mac)的集成,以及本地 ide(如 Android Studio)的集成,Git 通常是 Xamarin 和本地移动开发者的首选存储库类型。
Azure DevOps 上的 DevOps 实现提供了广泛的特性集。 Azure 门户(除了概述)分为五个主要部分,如下:
- 董事会(项目管理)
- 回购(版本控制)
- 管道(CI / CD)
- 测试计划(测试管理)
- 工件(包管理)
根据免费订阅模式,一个项目最多可以免费包含 5 个贡献者。 额外的团队成员将需要具有有效的 Visual Studio 或Microsoft Developer Network(MSDN)许可证,否则他们将被分配给只读的涉事者角色。
Visual Studio App Center
Visual Studio App Center 是一套工具,它将移动开发人员使用的各种开发服务(如 Xamarin、Native 和 Hybrid)捆绑到一个管理门户中。 App Center 与 Azure DevOps 紧密集成,它们可以相互配合使用。 App Center 支持多种应用平台,这些平台具有多种特性:

图 7.16 - App Center 平台
从 CI/CD 的角度来看,App Center 允许使用来自各种存储库系统(如 Azure DevOps 和 GitHub)的源工件来构建移动应用。 应用可以使用开箱即用的构建模板进行编译,应用包可以分发给指定的组,而不需要使用任何其他存储。
准备好的应用包还可以通过 UI 测试进行自动验收测试。 自动化 UI 测试支持多个测试运行时,例如 Xamarin。 外的和 Appium。
最后,可以从移动应用的 beta 版和生产版收集应用遥测和诊断数据,并将有价值的应用反馈重新引入 backlog 中。 推送通知是另一个有价值的功能,可以用来吸引应用用户。
App Center 也采用免费订阅模式,其中的构建和测试时间受到订阅的限制; 然而,CI/CD 特性的有限使用和无限的分发特性是免费的。
总结
总的来说,使用紧密集成的 Azure 模块,现在开发分布式应用要容易得多。 云应用和混合应用都可以使用可用的资源和通过。net Core 堆栈实现的模块来创建。 同样重要的是要记住,资源不应该定义应用需求; 相反,应该设计一个最佳的解决方案,同时考虑到可用的模块、需求和成本。
在本章中,我们讨论了云环境中的各种应用模型和架构模型。 我们还浏览了可用的 Azure 资源,这些资源将在接下来的章节中用于创建我们的应用后端。 在下一章中,我们将使用 Cosmos DB 创建我们的数据存储。****
八、使用 Cosmos DB 创建数据存储
创建数据存储是移动和 web 应用项目的重要组成部分。 可伸缩性、成本效益和性能是决定哪个数据库适合应用的三个关键因素。 Cosmos DB 具有广泛的可伸缩性选项和订阅模型,可以为移动应用提供理想的解决方案。 Cosmos DB 提供了多模型和多 API 范例,允许应用使用多个数据模型,同时使用最适合应用的 API 和模型(如 SQL、Cassandra、Gremlin 或 MongoDB)存储应用数据。
在本章中,我们将讨论 Cosmos DB 的基本概念,分析和实验数据访问模型,最后,我们将开始为我们的应用创建数据模型和数据存储,并实现数据访问模块。
在本章中,我们将涵盖以下主题:
- 宇宙 DB 的基础知识
- 数据访问模型
- 建模数据
- 深入了解 Cosmos DB
在本章结束时,你将会熟练地使用 Cosmos DB 提供的 SQL API 或 Mongo 访问模型来实现和访问数据模型。
技术要求
你可以在以下 GitHub 链接中找到本章使用的代码:https://github.com/PacktPublishing/Mobile-Development-with-.NET-Second-Edition/tree/master/chapter08。
宇宙 DB 的基本原理
从基于云的应用的角度来看,Cosmos DB是另一个可用的持久性存储,您可以将其包含在资源组中。 正如我们前面所讨论的,Cosmos DB 最大的优势还构成了 Cosmos DB 的独特特性集,即全局分布、多模型和高可用性。
在我们的示例应用中,我们将使用 Cosmos DB 并围绕可用的持久性模型创建我们的数据模型。 让我们开始添加一个 Cosmos DB 实例到我们的资源组:

F 图 8.1 -创建 Cosmos 数据库帐户
在这个屏幕上,我们将设置资源组、帐户名称(它还定义了访问 URL)和与数据访问模型以及全局分布相关的其他参数。 在决定任何 Cosmos 资源属性之前,让我们先看看这些基本概念。 在接下来的小节中,我们将了解 Cosmos DB 如何使用全球分布,一致性频谱看起来像什么,以及不同的一致性要求,最后,如何根据您的需求调整定价模型。
全球分销
全局分发是处理应用的全球范围的可用选项。 如果您计划让您的应用在全球范围内可用,并且您希望在每个市场中具有相同的延迟,那么可以选择 Geo-Redundancy 以允许在多个地区分布。 创建 Cosmos DB 资源后,可以使用全局复制数据刀片:

Figure 8.2 - Cosmos DB 全局复制
太棒了! 现在,我们在四个大洲的五个不同的数据中心展示了这个应用。 换句话说,我们为我们的持久化存储启用了多宿主。
除了两地三中心冗余功能外,还可以启用多个写区域。 多个写区域允许您为分布式数据存储设置多个主服务器。 这些数据不仅可以在不同的区域进行复制,还可以提供相同的写吞吐量。
在配置全局分布区域时,您可以设置其中一个读区域,即它的故障转移区域。
一致性谱
一旦数据持久性成为全球分布式、多宿主操作,一致性概念就成为一个基本主题。 在 Cosmos DB 中,有五个定义良好的一致性级别,简单地说,就是允许开发者优化一致性和性能之间的权衡:

图 8.3 -一致性谱
默认的一致性级别可以在数据库级别上设置,也可以在使用数据的客户机会话中设置。 但是,客户端集的一致性不能设置为高于默认一致性级别的一致性。
为了理解这种一致性,我们将使用微软研究论文中使用的棒球类比。 在本例中,我们将考虑棒球比赛中的各个利益相关者以及他们如何读取和写入比分。
毫无疑问,最需要一致性的人是官方的记分员。 他们将读取当前的分数,并在任何一支队伍得分时增加分数。 然后,他们希望得到他们正在读取的数据是最新版本的保证。 然而,由于记分员是唯一将执行写操作的人,我们也许能够侥幸不一致的阅读水平,如会话一致性,它提供了单调读一致性,单调写道,“读己之所写”一致性,write-follows-reads 保证考虑到有一个作家会话。
继官方记分员之后,另一个有强一致性要求的利益相关者是裁判,在本垒板后面主持棒球比赛的人。 裁判在第九局下半局结束比赛的决定取决于主队是否领先(也就是说,如果客队没有办法扳平比分,主队就没有必要击球)。 在这个决定中,他们需要线性化保证(也就是说,他们将读取任何给定数据点的最新版本)。 从他们的角度来看,每个操作都应该以原子的方式发生,并且存在一个单一的全局状态(即真相的来源)。 在这种设置中,性能(即低延迟)会因分布式集群中的仲裁状态而受到影响。
不像裁判,一个定期的记者(例如,30 分钟)就像保证一个一致的前缀; 换句话说,它们只依赖于数据的一致状态,直到返回写操作为止。 对他们来说,数据状态一致性比结果的延迟更重要,因为操作会定期执行,以提供全面的更新。
另一个不太关心延迟但关心一致状态的利益相关者是体育记者。 作者可以收到游戏的最终结果,并在第二天提供他们的评论,只要他们收到的结果是正确的。 在与此类似的场景中,最终一致性可能会返回正确的最终结果,但是当您想用延迟时间限制最终一致性承诺时,有界过时*可能是一个解决方案。 事实上,裁判本可以对他们的读操作使用类似的策略,以更短的延迟。
通过将这些概念应用到我们的应用模型中,我们可以自己决定哪些模块需要哪种类型的一致性。
让我们假设一旦交易完成,我们的用户就会收到来自参与者的评论或评级。 在这个场景中,评级系统实际上并不需要一组有序的写操作,一致性也不是很重要。 如果我们以最终一致性的承诺来编写和阅读,那么我们就能够为同一用户处理每一个评论。
接下来是通知系统,它在特定的时间间隔内只向感兴趣的各方发送拍卖物品的最高出价。 在这里,只需要在承诺的情况下执行读取操作,以便保持将出价写入数据存储的顺序,换句话说,使用一致的前缀。 如果我们发送的统计数据与类似,即物品的价值在过去一小时内增加了 30%,这就变得尤为重要。 类似地,这种一致性的周期可以由读系统定义,使其成为有界过时一致性。
现在,让我们假设,用户希望在观察列表中保留一组拍卖。 这个观察列表将只由用户自己编写,重要的读取保证是读取您所写的内容。 这可以通过会话一致性来处理。 此外,创建一个新的拍卖物品或更新将再次只是会话一致。
最后,可能在设置过程中最一致的过程是实际投标(即强一致性)。 在这里,为了竞拍一个拍卖项目,以及宣布拍卖的结果,我们依赖于强烈的一致性,因为竞拍项目是一个多参与者操作,我们希望确保传入的出价以一致的方式执行。
当然,这只是一种自以为是的设置,成本和实现完全没有考虑在内。 在真实的实现中,会话一致性在降低成本的同时提供了一致性和性能之间的最佳权衡。
定价
Cosmos DB 的定价模式相当复杂。 此计算涉及许多因素,例如全局可用性、一致性和另一个称为请求单元(RU)的抽象单元。 与数据事务单元(DTU)类似,它是用于读取 1 KB 项的系统资源(例如,CPU、内存和 IO)的度量。 有很多因素会影响 RU 的使用,比如条目大小、复杂性、索引、一致性级别和执行的查询。 通过使用 DB 返回的请求收费头来跟踪 RU 的消费是可能的。
让我们看看下面的文档 DB 客户端执行:
var query = client.CreateDocumentQuery<Item>(
UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId),
new FeedOptions { MaxItemCount = -1 })
.Where(item => !item.IsCompleted)
.AsDocumentQuery();
这将转换成一个 SQL 查询如下:
select * from Items where Items.isCompleted = false
我们可以通过使用Data Explorer刀片上的查询统计信息标签来获取请求费用:

图 8.4 - Cosmos DB 查询统计
根据数据集和应用的执行情况,请求收费变得越来越重要。 Cosmos DB 成本可以通过分析提供的遥测技术来优化应用的需求。
在本节中,我们介绍了 Cosmos DB 的基本概念,包括全局分布和一致性。 最后,讨论了基于云的数据存储的关键决策因素之一——定价模型,并对请求单元进行了分析。
数据访问模型
在创建 Cosmos DB 实例之前,可能要选择的最重要的选项是访问模型(即 API)。 在我们的应用中,我们将使用 SQL API,因为它本质上是唯一的本机访问模型,并允许使用其他特性,如触发器。 这就是为什么 SQL API 将是本节首先讨论的访问模型。 尽管如此,我们还将讨论 Mongo API,它可以提供一个可行的替代方案,其强大的社区支持以及减少了厂商锁定的风险。 本节将讨论的其他选项包括 Gremlin、Cassandra 和 Azure Table Storage。
SQL API
SQL API 允许开发人员使用 SQL 方言查询基于 json 的 NoSQL 数据结构。 与实际的 SQL 实现类似,SQL API 支持使用存储过程、触发器(即更改提要)和用户定义函数。 对 SQL 查询的支持允许(部分)使用 LINQ 和现有的客户端 sdk,比如实体框架。
The MongoDB API
由 Cosmos DB 提供的 MongoDB API 为 MongoDB 查询语言提供了广泛的支持(在本文撰写时,MongoDB 3.4 有线协议还在预览中)。 使用 MongoDB API 类型创建的 Cosmos DB 实例可以使用现有的数据管理器访问,例如 Compass、Studio 3T、RoboMongo 和 Mongoose。 这种对 MongoDB 的全面支持为开发人员提供了从现有 MongoDB 存储进行无缝迁移的选择。 Azure 门户数据提供了对 MongoDB 资源的 shell 和查询访问,以便可视化和分析数据。 为了演示这一点,让我们从 MongoDB 文档库中执行几个 MongoDB 查询。
假设我们有一个集合survey,我们将从插入调查结果的集合开始:
db.survey.insert([
{ "_id": 1, "results": [{ "product": "abc", "score": 10 }, { "product": "xyz", "score": 5 }]},
{ "_id": 2, "results": [{ "product": "abc", "score": 8 }, { "product": "xyz", "score": 7 }]},
{ "_id": 3, "results": [{ "product": "abc", "score": 7 }, { "product": "xyz", "score": 8 }]}
])
这将导致的错误消息类似如下:
ERROR: Cannot deserialize a 'BsonDocument' from BsonType 'Array'.
这是,因为insert命令在 web shell 上不完全支持。 为了能够正确地执行命令,我们需要转移到本地终端(假设 Mongo 工具集已经安装):
$ mongo handsoncrossplatformmongo.documents.azure.com:10255 -u handsoncrossplatformmongo -p {PrimaryKey} --ssl --sslAllowInvalidCertificates
MongoDB shell version v4.0.3
connecting to: mongodb://handsoncrossplatformmongo.documents.azure.com:10255/test
WARNING: No implicit session: Logical Sessions are only supported on server versions 3.6 and greater.
Implicit session: dummy session
MongoDB server version: 3.2.0
WARNING: shell and server versions do not match
globaldb:PRIMARY>show databases
sample 0.000GB
globaldb:PRIMARY>use sample
switched to db sample
globaldb:PRIMARY>db.survey.find()
globaldb:PRIMARY>db.survey.insert([{"_id":1, "results":[{"product":"abc", "score":10}, { "product":"xyz", "score":5}]}, { "_id":2, "results":[{"product":"abc", "score":8}, { "product":"xyz", "score":7}]}, { "_id":3, "results":[{"product":"abc", "score":7}, { "product":"xyz", "score":8}]} ])
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 3,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
})
globaldb:PRIMARY>db.survey.find()
{"_id":1, "results":[{"product":"abc", "score":10}, {"product":"xyz", "score":5}]}
{"_id":2, "results":[{"product":"abc", "score" : 8}, {"product":"xyz", "score":7}]}
{"_id":3, "results":[{ "product":"abc", "score" : 7}, {"product":"xyz", "score":8}]}
重要提示
MongoDB 的服务器和客户端Mongo.exe可以从 MongoDB 网站上下载。 在 macOS 上,brew install mongo命令将安装 Mongo。 个性化的连接字符串或完整的 shell 连接命令可以从 Cosmos DB 资源的 Quick Start 部分复制。
接下来,我们可以继续在云 shell 或本地mongoshell 上执行。 现在我们将执行一个find查询,其中产品应该是"xyz",并且评分应该大于或等于8:
db.survey.find(
{ results: { $elemMatch: { product: "xyz", score: { $gte: 8 } } } }
)
接下来我们将找到所有包含产品"xyz"的调查结果:
db.survey.find(
{ "results.product": "xyz" }
)
最后,我们将在产品为"abc"的地方增加第一个分数:
db.survey.update({
"results.product" : "abc"
},
{
$inc : {'results.0.score' : 1}
});
你可以在数据资源管理器的 shell 窗口中可视化结果:

图 8.5 - Data explorer shell
正如我们在这里说明的,Mongo 数据库和文档完全由 Cosmos DB 支持。 使用 Mongo 数据存储实现的应用可以很容易地在简单的 Mongo 守护进程上开发和测试,并且可以使用 Cosmos DB 部署到云环境中。 这使得 Mongo 成为 Cosmos DB 上最具吸引力的通用 NoSQL 访问模型之一。
其他
Gremlin 是一个图数据模型,Cassandra 是一个列族模型,它们都支持有线协议。 这些 api 允许与集群计算和大数据分析平台(如 Spark (GraphX))集成。 可以在 Azure hinsight 中创建 Apache Spark 集群来分析流和历史数据。
正如我们前面提到的,Cosmos DB 的最后一个成员是 Azure Table 存储,它提供对键/值对存储的访问,该存储支持数据的自动分片以及索引。
Cosmos DB 中支持的每个访问模型都可以用于特定的情况,SQL API 和 Mongo 是两个最常见的访问模型。 记住一个单一的 Cosmos DB 订阅应该使用一个特定的模型,这一点也很重要。 一旦确定了访问模型,就可以开始对数据建模。
建模数据
熟悉 Cosmos DB 提供的各种数据模型的最好方法是使用提供的 NoSQL 数据访问 api 实现固有的关系域模型。 这样,就更容易掌握不同数据模型的好处。 在本节中,我们将使用 SQL API 访问模型在 Cosmos DB 上创建域的主聚合根,并实现存储库类,这些类将在 web 应用中用于访问这些文档集合。 最后,我们还将讨论非正规化和引用数据。
对于这个练习,让我们为我们的拍卖应用创建一个关系数据模型。
在这个设置中,我们有三个大的数据集群:
- 车辆,包括制造商,型号,年份,发动机规格,以及描述车辆的一些附加属性
- 用户,由拍卖车辆的卖家和买家组成
- Auctions,由销售用户提供的一些关于销售的元数据,以及用户提供的车辆和出价组成
我们将使用 SQL API 描述此数据。
创建和访问文档
最微不足道的方式考虑到数据模型的设计,当处理一个 NoSQL 数据库,将想象数据转换对象(DTO)模型所需的应用。 在非 rbms 数据平台的情况下,重要的是要记住我们不受引用、唯一键或多对多关系的约束。
例如,让我们看一下最简单的模型,即User。 用户将拥有基本的概要信息,这些信息可以在应用的其余部分中使用。 现在,让我们想象一下用户对象的 DTO 是什么样的:
{
"id": "efd68a2f-7309-41c0-af52-696ebe820199",
"firstName": "John",
"lastName": "Smith",
"address": {
"addressTypeId": 4000,
"city": "Seattle",
"countryCode": "USA",
"stateOrProvince": "Washington",
"street1": "159 S. Jackson St.",
"street2": "Suite 400",
"zipCode": "98101"
},
"email": {
"emailTypeId": 1000,
"emailAddress": "john.smith@test.com"
},
"isActive": true,
"phone": {
"phoneTypeId": 1000,
"number": "+1 121-212-3333"
},
"otherPhones": [{
"phoneTypeId": 3000,
"number": "+1 111-222-3333"
}],
"signUpDate": "2001-11-02T00:00:00Z"
}
让我们创建名称为UsersCollection和的集合,分区键设置为/address/countryCode。
接下来,将这个 data 导入我们的数据库:

图 8.6 - Cosmos DB Documents 视图
好吧; 现在,我们创建了第一个文档。 但是这些由系统添加的额外字段是什么? 这些是对存放集合文档数据的容器和项的引用:

图 8.7 -系统生成的属性
在这些字段中,最重要的字段是_etag和_ts,它们都定义了实体在给定时间点的状态。 请注意,描述并不指向文档,而是指向项目和实体。 主要原因是在 Cosmos DB 中,存储桶被称为容器,而存储在这些容器中的实体被称为项目。 集合、表或图形是这些容器的实现,这取决于所使用的 API 类型。
现在,我们可以开始创建我们的数据访问层,这将是 User API 的一部分,以向我们的移动应用提供所需的数据。 让我们开始:
-
首先,让我们使用以下命令在您选择的新文件夹中创建一个新的解决方案文件:
-
现在我们可以创建我们的核心存储库项目,我们将使用它来存储我们的存储库接口:
dotnet new classlib -o ShopAcross.Web.Repository -
在这个新创建的项目中创建一个通用接口,允许我们检索用户提要以及单个用户
public interface IRepository<T> where T : class { Task<T> GetItemAsync(string id); Task<IEnumerable<T>> GetItemsAsync(); //Task<Document> AddItemAsync(T item); //Task DeleteItemAsync(string id); //Task<Document> UpdateItemAsync(string id, T item); } -
创建一个新项目,该项目将使用 Cosmos DB 上的 SQL API 实现存储库访问模型,如下所示:
-
Createour iimplementation for Cosmos DB:
public class CosmosCollection<T> : IRepository<T> where T : class { private Container _cosmosContainer; public CosmosCollection(string collectionName) { CollectionId = collectionName; var client = new CosmosClient(Endpoint, Key); var client = new CosmosClient(Endpoint, Key); var database = client.GetDatabase(DatabaseId); _cosmosContainer = database.GetContainer(CollectionId); } // ... Removed for brevity } -
实现获取所有条目的存储库方法:
-
让我们对检索单个条目执行相同的操作:
public async Task<T>GetItemAsync(string id) { // Query for items by a property other than Id var queryDefinition = new QueryDefinition ($"select * from {CollectionId} c where c.Id = @EntityId") .WithParameter("@EntityId", id); using FeedIterator<T> resultSet = _cosmosContainer.GetItemQueryIterator<T>(queryDefinition); var response = await resultSet.ReadNextAsync(); return response.FirstOrDefault(); } -
现在,我们已经准备好加载我们已经导入到文档集合中的文档:
var cosmosCollection = new CosmosCollection<User>("UsersCollection"); var collection = await cosmosCollection.GetItemsAsync() -
您还可以传递分区键(即
countryCode)以降低查询成本,否则将是一个跨分区调用:using FeedIterator<T> resultSet = _cosmosContainer.GetItemQueryIterator<T>( queryDefinition: null, requestOptions: new QueryRequestOptions { MaxItemCount = -1, PartitionKey = new PartitionKey("USA") }); -
现在,为给定集合加载完整的条目集。 然而,在大多数情况下,我们将使用谓词来加载所需的集合。 因此,在查询中添加一个
Where子句:
```
public async Task<IEnumerable<T>> GetItemsAsync(
Expression<Func<T, bool>> predicate)
{
using FeedIterator<T> resultSet =
_cosmosContainer.GetItemLinqQueryable<T>(
requestOptions: new QueryRequestOptions
{
MaxItemCount = -1,
PartitionKey = new PartitionKey("USA")
})
.Where(predicate)
.ToFeedIterator();
// ...
}
```
- 现在,创建相应的添加、更新和删除方法,这将为集合提供完整的 CRUD 操作:
```
public async Task<T>AddItemAsync(T item)
{
var resp = await _cosmosContainer.CreateItemAsync(item);
return resp.Resource;
}
public async Task<T> UpdateItemAsync(string id, T item)
{
var resp = await _cosmosContainer.ReplaceItemAsync(item, id);
return resp.Resource;
}
public async Task DeleteItemAsync(string id)
{
_ = await _cosmosContainer.DeleteItemAsync<T>(id, PartitionKey.None);
}
```
- 最后,优先方法后,为了避免文档集每次都手动创建,可以使用初始化函数来创建数据库和收集如果他们不 exi 圣当客户是第一次创建。
现在,我们已经创建了一个完整的文档集合和基本的 CRUD 函数。 现在,我们将通过数据反规格化继续并进一步扩展我们的领域模型。
非正态化数据
数据标准化(Data normalization)是通过分解现有结构和创建替换引用来构建数据库模型的过程,以减少冗余,提高数据完整性。 然而,数据规范化本质上适用于关系数据库。 在文档集合的情况下,嵌入式数据优先于引用完整性。 此外,跨单个集合边界的数据视图还应该根据设计需求在不同的枢轴上进行复制。
让我们继续进行关于车辆和拍卖的数据模型设计。 这两个数据域将使用单独的 api 进行处理,并拥有单独的集合。 但是,在一般的提要(例如,最新的拍卖)中,我们需要检索关于拍卖的数据,以及拍卖中的汽车和用户为该特定拍卖提供的出价。 让我们看看如何做到这一点:
-
For the vehicle's declaration, we will need the main product information:
{ "id" : "f5574e12-01dc-4639-abeb-722e8e53e64f", "make" : "Volvo", "model": "S60", "year": 2018, "engine": { "displacement" : "2.0", "power" : 150, "torque" : 320, "fuelType": { "id": "11", "name": "Diesel" } }, "doors": 4, "driveType": { "id" : "20", "name" : "FWD" }, "primaryPicture" : "", "pictures" : [], "color": "black", "features": [ "Heated Seats", "Automatic Mirrors", "Windscreen Defrost", "Dimmed Mirrors", "Blind Spot Detection" ]注意,特性数组包含从引用值列表中选择的特性列表,但是我们没有创建多对多关系表,而是选择在这里嵌入数据,这是对常规形式的一种折衷。 类似的方法也可以用于
fuelType和driveType引用,但从概念上讲,我们在这些数据点上具有多对一关系,因此它们本身作为引用数据对象嵌入。 -
移动,创建拍卖数据:
{ "id" : "7ad0d2d4-e19c-4715-921b-950387abbe50", "title" : "Volvo S60 for Sale", "description" : "..." "vehicle": { "id" : "f5574e12-01dc-4639-abeb-722e8e53e64f", "make" : "Volvo", "model": "S60", "year": 2018, "engine": { "displacement" : "2.0", "power" : 150, "torque" : 320, "fuel": { "id": "11", "name": "Diesel" } }, "primaryPicture" : "", "color": "black" }, "startingPrice": { "value" : 25000, "currency" : { "id" : "32", "name" : "USD", "symbol" : "$" } }, "created": "2019-03-01T10:00Z", "countryCode": "USA" "user": { "id" : "efd68a2f-7309-41c0-af52-696ebe820199", "firstName": "John", "lastName": "Smith" } } -
If this was a relational model, this data would have been enough for identifying an auction. Nevertheless, it would decrease the number of round trips to load additional data if we embedded the highest bid (or even the most recent or highest bids) within the same structure:
"highestBids":[ { "id" : "5d669390-2ba4-467a-b7f9-26fea2d6a129", "offer" : { "value" : 26000, }, "user": { "id" : "f50e4bd2-6beb-4345-9c30-18b3436e7766", "firstName": "Jack", "lastName": "Lemon", }, "created" : "2019-03-12T11:00Z" } ],重要提示
在这个场景中,我们还可以将完整的投标结构嵌入到拍卖模型中。 虽然这将减少数据在集合之间的冗余和分散,但出价并不像我们在汽车对象中看到的特性集那样是一个有限的集合,而且每个新的出价都需要对拍卖集合进行完整的文档替换。
我们可以说Auction表充当了清单的物化视图,而车辆和出价提供了对应用视图所需数据点的方便访问。 在这里,数据完整性的责任落在客户机应用而不是数据库本身。
参考资料
在前面的示例中,我们广泛地使用嵌入来创建优化的数据结构。 当然,这并不意味着我们没有使用任何参考。 所使用的大多数嵌入对象实际上都是引用描述。
为了可视化参考数据点,让我们标准化我们的拍卖数据:
{
"id" : "7ad0d2d4-e19c-4715-921b-950387abbe50",
"description" : "Volvo S60 for Sale",
"vehicleId": "f5574e12-01dc-4639-abeb-722e8e53e64f",
"startingPrice": {
"value" : 25000,
"currencyId" : "32"
},
"highestBids":[
"5d669390-2ba4-467a-b7f9-26fea2d6a129"
],
"created": "2019-03-01T10:00Z",
"countryCode": "USA",
"userId": "efd68a2f-7309-41c0-af52-696ebe820199"
}
关系在这里,我们有一个1-*``Vehicle和Auction,【T7 之间的关系1-*】货币和起始价值,1-*关系拍卖投标,和一个1-*关系用户和拍卖。 所有这些引用都嵌入到auction对象中,但是相互引用呢? 例如,如果我们正在实现一个用户配置视图,我们可能想要显示他们参与了多少投标,并可能从获胜的买家或卖家获得反馈值:
{
"id": "efd68a2f-7309-41c0-af52-696ebe820199",
"firstName": "John",
"lastName": "Smith",
"numberOfAuctions" : 1,
"auctions" : [
{
"auctionId": "7ad0d2d4-e19c-4715-921b-950387abbe50",
"role" : { "roledId" : "20", "roleName": "seller" },
"auctionReview" : 1,
"auctionState" : { "stateId" : "10", "stateName" : "Closed" }
}
]
...
}
这些情形的类型完全取决于应用用例。 正如前面提到的,在 NoSQL 设置中,我们没有绑定到外键和约束,设计也不应该规定嵌入或引用。 Cosmos DB 提供了存储过程和触发器等特性,将数据完整性的责任分配给数据库。 此外,索引和分区策略可以提高应用的整体性能。
Cosmos DB in depth
作为一个平台,Cosmos DB 不仅仅是一个简单的数据库。 数据模型的设计以及数据访问层的实现在很大程度上取决于所使用的特性。 分区和索引的设置有助于提高性能,同时也为查询策略提供了路线图。 数据触发,存储过程【显示】、和改变饲料是扩展点,允许开发者实现综合语言事务 JavaScript 块,这可以极大地降低系统的整体复杂性也弥补写事务支持非正规数据妥协。
分区
Cosmos 使用了两种类型的分区——即物理分区和逻辑分区——来伸缩数据库中的单个容器(即集合)。 在创建容器时定义的分区键定义了逻辑分区。 然后将这些逻辑分区分成若干组,每个组包含一组副本,以便能够水平扩展数据库。
在这个方案中,分区键的选择成为决定查询性能的重要决策。 通过正确选择分区键,数据将被统一分片(即数据分片),这样分布式系统就不会显示所谓的热分区(即某些分区上的请求峰值,而其他分区是空闲的)。 热分区最终会导致性能下降。
在UsersCollection中,我们使用/address/countryCode作为分区键。 这意味着我们期望一组用户在各个国家中呈正态分布。 然而,在现实生活中,来自某个市场的用户数量实际上取决于该市场的规模。 通俗地说,如果我们考虑到人口数量和需求,土耳其或德国的用户数量不可能与波斯尼亚-黑塞哥维那相同。
重要提示
在 Cosmos DB 中创建容器之后,就不支持更改集合的其他属性,比如 ID 或分区键。 只能更新索引策略。
分区键不一定需要是对数据的语义分析。 例如,在UsersCollection场景中,根据定义的分区键很容易的名字的第一个字母,他们签约,以及合成分区键,如生成的值从一个范围(例如 1 - 100)指定在创建的时候。 尽管如此,由于容器中某项的 ID 在该容器中是唯一的,所以容器和 ID 的组合定义了该项的索引。 为了实现更高的吞吐量,查询应该在特定的容器中执行。 换句话说,如果分区键可以在客户端查询之前计算,应用将比执行跨分区查询执行得更好:
FeedIterator<T>resultSet = _cosmosContainer
.GetItemLinqQueryable<T>(
requestOptions: new QueryRequestOptions
{
MaxItemCount = -1,
PartitionKey = new PartitionKey("USA")
})
.Where(predicate)
.ToFeedIterator();
例如,让我们以为例,看看这个集合的以下执行:
var cosmosCollection = new CosmosCollection<User>("UsersCollection");
await cosmosCollection.GetItemsAsync(
(item) =>item.FirstName.StartsWith("J"));
// Calling with the partition key
await cosmosCollection.GetItemsAsync(
(item) =>item.FirstName.StartsWith("J"), "USA");
当使用前面的表达式时,这个查询的结果(对于每个分区只有两个条目的集合)如下:
Executing Query without PartitionKey
Query: {"query":"SELECT VALUE root FROM root WHERE STARTSWITH(root[\"firstName\"], \"J\") "}
Request Charge : 2.96 RUs
Partition Execution Duration: 218.08ms
Scheduling Response Time: 26.67ms
Scheduling Run Time: 217.45ms
Scheduling Turnaround Time: 244.65ms
Executing Query with PartitionKey
Query: {"query":"SELECT VALUE root FROM root WHERE STARTSWITH(root[\"firstName\"], \"J\") "}
Request Charge : 3.13 RUs
Partition Execution Duration: 136.37ms
Scheduling Response Time: 0.03ms
Scheduling Run Time: 136.37ms
Scheduling Turnaround Time: 136.41ms
即使使用最小的数据集,执行结果也显示出在执行所需的总时间上有相当大的改进。
重要提示
为了检索 Cosmos 查询的指标,可以使用FeedResponse<T>.Diagnostics属性。 与以前的 SDK 不同,诊断数据收集在默认情况下是启用的。
以类似的方式,我们可以扩展汽车和拍卖的数据模型,我们可以用汽车的型号或颜色创建集合,这样我们就有了均匀分布的分区。
索引
Azure Cosmos DB,根据默认,假设项目中的每个属性都应该被索引。 当一个复杂对象被推入集合时,该对象被视为具有组成节点和值以及叶节点的属性的树。 这样,树的每个分支上的每个属性都是可查询的。 每个后续对象要么使用相同的索引树,要么使用附加属性展开它。
这种索引行为可以在任何时候对任何集合进行更改。 这可以帮助提高数据集的成本和性能。 索引定义使用通配符值来定义应该包含哪些路径和/或排除哪些路径。
例如,我们来看看我们的AuctionsCollection的索引策略:

图 8.8 -索引策略
除了被排除的_etag字段外,/*声明包含了完整的对象树。 这些索引可以使用更专门化的索引类型和路径进行优化。
例如,我们排除所有路径,并引入我们自己的索引:
"includedPaths": [
{
"path": "/description/?",
"indexes": [
{
"kind": "Hash",
"dataType": "String",
"precision": -1
}
]
},
{
"path": "/vehicle/*",
"indexes": [
{
"kind": "Hash",
"dataType": "String",
"precision": -1
},
{
"kind": "Range",
"dataType": "Number",
"precision": -1
}
]
}
],
"excludedPaths": [
{
"path": "/*"
}
]
这里,我们添加了两个索引:一个哈希索引用于描述字段的标量值(即/?),一个范围和/或哈希索引用于车辆路径及其下的所有节点(即/*)。 哈希索引类型是用于相等查询的索引,而范围索引类型用于比较或排序。
通过使用正确的索引路径和类型,可以降低查询成本,并避免扫描查询。 如果索引模式被设置为None而不是Consistent,那么数据库将在给定的集合上返回一个错误。 查询仍然可以使用EnableScanInQuery标志执行。
可编程性
Cosmos 最有用的特性之一是它的服务器端可编程性,它允许开发人员创建存储过程、函数和数据库触发器。 对于在 SQL 数据库上创建应用的开发人员来说,这些概念并不陌生,但是在 NoSQL 数据库上创建存储过程的能力,以及在 JavaScript 等客户端脚本语言上创建存储过程的能力,是前所未有的。
作为一个简单的例子,让我们实现一个触发器来计算用户配置文件的聚合值:
-
As you may remember, we added the following reference values to
UserProfilefor the cross-collection partition:public class User { [JsonProperty("id")] public string Id { get; set; } [JsonProperty("firstName")] public string FirstName { get; set; } //... [JsonProperty("numberOfAuctions")] public int NumberOfAuctions { get; set; } [JsonProperty("auctions")] public List<BasicAuction> Auctions { get; set; } //... }现在,让我们创建一个聚合更新函数,它将在用户配置文件上有更新时更新拍卖数量。 我们将使用这个函数拦截对集合的更新请求(即一个预执行触发器)并修改对象的内容。
-
函数应该首先从执行上下文中检索当前集合和文档:
function updateAggregates(){ // HTTP error codes sent to our callback function by server. var ErrorCode = { RETRY_WITH: 449, } var collection = getContext().getCollection(); var collectionLink = collection.getSelfLink(); // Get the document from request (the script runs as trigger, // thus the input comes in request). var document = getContext().getRequest().getBody(); -
现在,让我们的函数计算更新正在推动的拍卖:
if(document.auctions != null) { document.numberOfAuctions = document.auctions.length; } getContext().getRequest().setBody(document); -
We can now add this trigger to the
UsersCollectionas a Pre trigger on Replace calls:![Figure 8.9 – Cosmos DB trigger]()
图 8.9 - Cosmos DB 触发器
-
However, the
triggerfunction will still not execute until we explicitly add the trigger to the client request:var requestOption = new ItemRequestOptions(); requestOption.PreTriggers= new []{ "updateAggregates"}; await _client.ReplaceDocumentAsync(item, id, requestOption);太棒了! 每次更新用户的个人资料时,都会计算用户参与拍卖的次数。 然而,为了插入一个新的拍卖(例如,当用户实际创建一个拍卖或投标时),我们需要更新整个用户配置文件(也就是说,部分更新目前在 SQL API 上不支持)。
-
让我们创建一个存储过程,该存储过程将在特定的用户配置文件中插入拍卖项目,以推送部分更新:
function insertAuction(id, auction) { var collection = getContext().getCollection(); var collectionLink = collection.getSelfLink(); var response = getContext().getResponse(); -
接下来,检索需要将拍卖插入的用户配置文件对象:
var documentFilter = 'Select * FROM r where r.id = \'' + id + '\''; var isAccepted = collection.queryDocuments( collectionLink, documentFilter, function (err, docs, options) { if (err) throw err; var userProfile = docs[0]; // TODO: Insert Auction }); -
现在,我们可以用新的拍卖
userProfile.auctions[userProfile.auctions.length] = auction; collection.replaceDocument(userProfile._self, userProfile, function (err) { if (err) throw err; });来更新文档:
-
最后,我们将为
UserProfileRepository:public async Task InsertAuction(string userId, Auction auction) { try { _ = await _cosmosContainer.Scripts .ExecuteStoredProcedureAsync<T>( "insertAuction", new PartitionKey("USA"), (dynamic)userId, (dynamic)auction); } catch (DocumentClientException e) { throw; } }创建一个附加函数。
现在,将拍卖插入到用户配置文件中,并且在调用存储过程时更新聚合列。
触发器、函数和存储的过程都被限制在创建它们的集合中。 换句话说,一个集合触发器不能对另一个集合执行任何更改。 为了执行这样的更新,我们需要使用一个外部进程,比如调用者应用本身,或者一个由 Cosmos DB 上的更改提要触发的 Azure 函数。
change feed
Azure Cosmos DB 持续地监视集合中的更改,并且这些更改可以通过更改提要推送到各种服务。 通过更改 feed 推送的事件可以被 Azure Functions 和 App Services,以及流处理和数据管理流程使用。
插入、更新和软删除操作可以通过更改提要进行监视,每个更改在更改提要中只出现一次。 如果对某项进行多个更新,则更改提要中只包含最新的更改,从而使其成为健壮且易于管理的事件处理管道。
总结
Cosmos DB 为 NoSQL 数据库概念提供了新的视角,为各种场景提供了广泛的服务。 此外,使用 Cosmos DB 访问模型,与关系数据模型相比,使用者应用对参考数据完整性负有更多责任。 数据容器之间的薄弱链接可以作为微服务体系结构的优势。
在本章中,您使用 SQL API 作为访问模型创建了一个全新的 Cosmos DB 资源。 然而,我们也讨论了其他访问模型,我们也在 Mongo 上执行了示例查询。 创建 Cosmos DB 资源之后,您就创建了示例文档集合,对数据建模,并实现了简单的存储库类来访问这些数据。 现在,您了解了 Cosmos DB 的基本概念,并准备将其用作持久性存储。
在下一章中,我们将为应用套件创建服务层。*
九、创建 Azure 应用服务
Azure 应用服务是一种平台即服务(PaaS),面向移动和应用开发人员,可以托管多种不同的应用模型和服务。 虽然开发人员可以在几分钟内创建一个简单的移动应用服务来充当数据存储访问层,而无需编写一行代码,但复杂而健壮的。net Core 应用也可以通过与其他 Azure 服务的内在集成来实现。
在本章中,我们将学习 Azure App Service 的基础知识,并使用 ASP 为我们的应用创建一个简单的面向数据的后端.NET 5,认证由Azure Active Directory(Azure AD)提供。 我们也将提高我们的 web API 端点 Redis 缓存的性能。 以下部分将指导您创建我们的服务后端:
- 选择正确的应用模式
- 创建我们的第一个微服务
- 集成 Redis 缓存
- 托管服务
- 确保应用
到本章结束时,您将完全有能力从头开始设计和创建 web 服务,并将它们集成到云基础设施中,同时还具有额外的性能和身份验证方面的非功能性需求。
技术要求
你可以通过以下 GitHub 链接找到本章使用的代码:https://github.com/PacktPublishing/Mobile-Development-with-.NET-Second-Edition/tree/master/chapter09。
选择正确的应用模式
Azure 堆栈提供了多个主机 web 应用的方法,不同的从简单的基础设施即服务(IaaS)产品,如虚拟机(【显示】vm)完全托管 PaaS 托管服务等应用服务。 因为。net Core 和 ASP 的平台无关性.NET Core、甚至 Linux 容器和容器编排服务(如 Kubernetes)都是可用的选项。****
Azure 计算服务可以根据职责的划分分为三个主要类别,即 IaaS、PaaS 和容器即服务(CaaS**),如下图所示:
【t】【t】

图 9.1 -托管主机模型
除了 IaaS 和 PaaS 之外,还有各种托管选项,每种都有自己的优点和用例。 现在我们将仔细研究这些产品。
下面几节将带领您了解 Azure 生态系统中的不同应用托管模型。
Azure 虚拟机
虚拟机(虚拟机)是微软 Azure Cloud 上最古老的 IaaS 产品之一。 简单地说,这个产品提供了一个在云上具有完全控制的托管服务器。 Azure 自动化服务为您提供很需要的工具来管理这些服务器与基础设施代码(【显示】IaC)原则使用 PowerShell期望状态配置【病人】(****DSC)和计划运行手册。 您可以根据您的应用需求轻松地伸缩和监视它们。
有了 Windows 和 Linux 变体,Azure vm 可以成为现有应用的简单、浅层云迁移路径。
VM 的扩展是通过在 VM 级别上调整系统配置(例如,CPU、虚拟磁盘、RAM 等)或在基础架构中引入额外的 VM 来实现的。
虚拟机托管发生在托管模型频谱的最左边; 换句话说,它提供了一个几乎完全自管理的主机模型。 在管理区域,我们有集装箱服务。
Azure 中的容器
如果虚拟机是硬件的虚拟化,则容器将操作系统虚拟化。 容器为共享相同操作系统内核的应用创建隔离的沙箱。 这样,由应用及其依赖项组成的应用容器可以轻松地托管在任何满足容器需求的环境中。 容器映像可以作为多个应用容器执行,这些容器实例可以使用 Docker Swarm、Kubernetes 和 Service Fabric 等编制工具进行编排。
Azure 目前有两个托管容器编排产品:Azure Kubernetes Service(AKS)和 Service Fabric Mesh。
Azure 集装箱服务与 Kubernetes
Azure 容器服务(ACS)是一个托管容器调度环境,在这里开发人员可以轻松地部署容器,并享受自动恢复和缩放体验。
Kubernetes 是一个开放源码的容器编排系统,最初是由谷歌设计和开发的。 Azure Kubernetes 是该服务的托管实现,其中大部分配置和管理职责都委托给了平台本身。 在这种设置中,作为这个 PaaS 产品的消费者,您只负责管理和维护代理节点。
AKS 支持 Linux 容器和 Windows 操作系统虚拟容器。 Windows 容器支持目前处于私有预览状态。
服务结构网格
Azure Service FabricMesh 是一个完全受管理的容器编排平台,在这个平台中,使用者无需与底层集群的配置或维护进行任何直接交互。 所谓的多语言服务(即任何语言和任何操作系统)是在容器中运行的。 在这种设置中,开发人员只负责指定应用所需的资源,例如容器的数量及其大小、网络需求和自动伸缩规则。
一旦部署了应用容器,Service Fabric Mesh 将容器托管在一个由数千台机器组成的集群中,而集群操作对开发人员是完全隐藏的。 通过软件定义网络(SDN)的智能消息路由实现了微服务之间的业务发现和路由。
尽管服务结构网格共享相同的底层平台,但它与 Azure 服务结构有很大的不同。 虽然 Service Fabric Mesh 是一个托管解决方案,但 Azure Service Fabric 是一个完整的微服务平台,允许开发人员创建容器化或本地云应用。
Azure 服务结构的微服务
Azure Service Fabric 是一个托管和开发平台,允许开发人员创建由微服务组成的企业级应用。 Service Fabric 提供了全面的运行时和生命周期管理功能,以及使用可变状态容器(如可靠的字典和队列)的持久编程模型。
服务 Fabric 应用可以由三种不同的托管服务模型组成:容器、服务/参与者和来宾可执行文件。
与它的托管版本(即 Service Fabric Mesh)和 Azure Kubernetes 类似,Service Fabric 能够运行针对 Linux 和 Windows 容器的容器化应用。 可以很容易地将容器包含在一个 Service Fabric 应用中,该应用与其他组件捆绑在一起,并通过高密度共享计算池(称为集群)和预定义的节点进行伸缩。
可靠的服务和可靠的参与者是真正的云本地服务,它们利用了服务结构编程模型。 在本地开发集群和。net Core 以及 Java 可用的 sdk 上进行开发的方便性,允许开发人员以平台无关的方式创建有状态和无状态的微服务。
重要提示
开发环境可用于 Windows、Linux 和 Mac OS X, Linux 和 OS X 开发设置依赖于在容器中运行 Service Fabric 本身,. net Core 是创建针对这些平台的应用的首选语言。 可用的 Visual Studio Code 扩展使得在每个操作系统上开发。NET Core 服务结构应用变得很容易。
最后,客户可执行文件可以是在各种语言/框架(如 Node.js、Java 或 c++)上开发的应用。 客户可执行文件作为无状态服务进行管理和处理,可以与其他服务 Fabric 服务并排放置在集群节点上。
Azure 基于容器的资源可以为开发高度复杂的微服务生态系统提供灵活性; 但是,您为容器和容器托管选择的任何模型都需要配置和基础设施管理,并且通常需要一个陡峭的学习曲线。 如果你正在寻找一个在配置和基础设施管理方面负担更轻的替代方案,我们可以继续我们的托管模型,选择 Azure App Service 作为我们的托管模型。
Azure 应用服务
Azure 应用服务是一个完全管理的 web 应用托管服务。 App Service 可以用于托管应用,而不考虑开发平台或操作系统。 App Service 为 ASP 提供了一流的支持.NET (Core)、Java、Ruby、Node.js、PHP 和 Python。 它可以与 Azure DevOps、GitHub 和 BitBucket 等 DevOps 平台进行即时集成和部署。 通常托管环境的大多数管理功能都集成到 App Service 刀片上的 Azure 门户中,例如伸缩、CORS、应用设置、SSL 等等。 此外,WebJobs 可以用来创建后台进程,这些进程可以作为应用包的一部分定期执行。
容器 Web 应用是 Azure 应用服务的另一个特性,它允许将容器化应用部署为应用服务,并与 Kubernetes 进行协调。
移动应用服务(Mobile App Service)是一种为简单的移动应用集成所有必要功能的简单方法,如身份验证、推送通知和离线同步。 然而,移动应用服务目前正在过渡到应用中心,不支持 ASP。 净的核心。
最后,Azure Functions 提供了一个平台来创建代码片段或无状态按需函数,并托管它们,而无需显式地提供或管理基础设施。
在本节中,我们了解了不同的托管模型及其优缺点。我们从 Azure vm 开始研究,对容器做了简短的介绍,最后讨论了 Azure 应用服务和功能的 PaaS 模型。 现在我们已经讨论了关于选择模型的基础知识,接下来我们将看看如何创建我们的第一个微服务。
打造首个微服务
对于我们的移动应用,在第八章,用 Cosmos DB 创建一个数据存储中,我们创建了一个简单的数据访问代理,从 Cosmos DB 中检索数据。 在这个练习中,我们将创建小型 web API 组件,这些组件将在集合上公开用于 CRUD 操作的各种方法。
初始设置
让我们开始我们的实现:
-
First, create an ASP.NET Core project:
![Figure 9.2 – Creating an ASP.NET Project]()
图 9.2 -创建 ASP。 网项目
-
创建项目后,进行快速测试,检查
dotnet核心组件是否正确设置。 -
打开一个控制台窗口并导航到项目文件夹。 以下命令将恢复引用的包并编译应用:
-
Once the application is built, we can use the
runcommand and execute aGETcall to theapi/valuesendpoint:![Figure 9.3 – First Run for the ASP.NET Core App]()
图 9.3 -第一次运行 ASP.NET Core 应用
这将导致
WeatherForecastController控制器的GET方法的值输出。重要提示
在前面的示例中,我们使用
curl来执行一个快速 HTTP 请求。 客户端 URL(curl)是一个实用程序,可在基于 unix 的系统、macOS 和 Windows 10 上使用。 -
Next, we will set up the Swagger endpoint so that we have a metadata endpoint, as well as a UI to execute test requests. For this purpose, we will be using the Swashbuckle NuGet packages to generate the API endpoint metadata. A basic setup of Swashbuckle requires three packages, and we can reference them together by adding the
Swashbuckle.AspNetCoremeta-package:![Figure 9.4 – Swashbuckle NuGet]()
图 9.4 - Swashbuckle NuGet
-
在添加了元包和依赖项之后,修改
Startup类来声明服务: -
现在,运行应用并导航到
{dev host}/swagger端点。 我们将看到生成的 Swagger UI 和方法声明:

图 9.5 - Swagger UI
现在我们已经准备好了样板 API 项目,这意味着我们可以继续实现我们的服务了。
实现检索操作
考虑到拍卖数据集,由于我们已经创建了 MVC 应用,我们应该在控制器中包含两个GET方法:一个用于检索拍卖的完整集合,另一个用于检索特定的拍卖。
让我们看看如何做到这一点:
-
在开始实现之前,让我们将
WeatherForecastController重命名为AuctionsController。 -
We can simply initialize our repository and return the results for the first
GETmethod:[HttpGet] public async Task<IEnumerable<Auction>> Get() { var result = Enumerable.Empty<Auction>(); try { result = await _cosmosCollection.GetItemsAsync(item => true); } catch (Exception ex) { // Log the error or throw depending on the requirements } return result; }注意,这里使用的谓词针对的是完整的拍卖集。 我们可以使用查询参数来扩展这个实现,这些查询参数使用附加的谓词来过滤集合。
-
The
GETmethod for retrieving a specific item would not be much different:[HttpGet("{id}")] public async Task<User> Get(string id) { User result = null; try { result = await _cosmosCollection.GetItemAsync(id); } catch (Exception ex) { // Log or throw error depending on the requirements } return result; }这样就可以满足需求,但是为了改进移动应用和 Cosmos 集合之间的交互,我们还可以启用OData查询,并创建一个到数据存储的透明查询管道。 为此,我们可以使用可用的。net Core 包实现 OData 控制器,或者在当前 MVC 控制器上启用查询。
重要提示
需要注意的是,我们用来生成 Swagger UI 的 Swashbuckle 包目前不支持 OData 控制器,因此这些 api 在 Swagger 接口上不可用。
-
对于 OData 的实现,我们首先在启动类中设置基础设施:
public void ConfigureServices(IServiceCollection services) { services.AddOData(); services.AddODataQueryFilter(); // ... Removed } -
现在,为 MVC 控制器设置路由:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseMvc(builder => { builder.Count().Filter().OrderBy().Expand().Select().MaxTop(null); builder.EnableDependencyInjection(); }); // ... Removed } -
在 Cosmos 存储库客户端上创建一个新的方法,该方法将返回一个可查询集而不是一个结果集:
public IQueryable<T> GetItemsAsync() { var feedOptions = new FeedOptions { MaxItemCount = -1, PopulateQueryMetrics = true, EnableCrossPartitionQuery = true }; IOrderedQueryable<T> query = _client.CreateDocumentQuery<T>( UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), feedOptions); return query; } -
Finally, we need to implement our query action:
[HttpGet] [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)] public ActionResult<IQueryable<Auction>> Get(ODataQueryOptions<Auction> queryOptions) { var items = _cosmosCollection.GetItemsAsync(); return Ok(queryOptions.ApplyTo(items.AsQueryable())); }现在,可以在集合上执行简单的 OData 查询(例如,第一层 OData 过滤器查询)。 例如,如果我们在
Users端点上执行一个查询,一个简单的过滤器查询以检索一个或多个拍卖的用户将如下所示:http://localhost:20337/api/users?$filter=NumberOfAuctions ge 1
为了能够扩大查询选项,执行查询相关的实体,我们需要创建一个实体数据模型(EDM)和注册各自 OData 控制器。****
**重要提示
支持高级搜索的另一个更合适的选项是创建一个 Azure 搜索索引,并在该索引之上公开 Azure 搜索功能。
对于内存中的 EDM 和数据上下文,我们将使用Microsoft.EntityFrameworkCore的特性和功能。 让我们从实现开始:
-
创建一个
DbContext,它将定义我们的主数据模型和实体之间的关系:public class AuctionsStoreContext : DbContext { public AuctionsStoreContext(DbContextOptions <AuctionsStoreContext> options) : base(options) { } public DbSet<Auction> Auctions { get; set; } public DbSet<User> Users { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<User>().OwnsOne(c => c.Address); modelBuilder.Entity<User>().HasMany<Auction>(c => c.Auctions); } } -
现在,让我们用在
ConfigureServices方法中注册这个上下文:public void ConfigureServices(IServiceCollection services) { servics.AddDbContext<AuctionsStoreContext>(option => option.UseInMemoryData("AuctionsContext")); services.AddOData(); services.AddODataQueryFilter(); // ... Removed } -
现在,创建一个返回 EDM 的方法:
private static IEdmModel GetEdmModel() { ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); var auctionsSet = builder.EntitySet<Auction>("Auctions"); var usersSet = builder.EntitySet<User>("Users"); builder.ComplexType<Vehicle>(); builder.ComplexType<Engine>(); return builder.GetEdmModel(); } -
最后注册一条 OData 路由,实体控制器可以通过这条路由服务:
app.UseMvc(builder => { builder.Count().Filter().OrderBy().Expand().Select().MaxTop(null); builder.EnableDependencyInjection(); builder.MapODataServiceRoute("odata", "odata", GetEdmModel()); }); -
Now that the infrastructure is ready, you can navigate to the
$metadataendpoint to take a look at the EDM that was generated:![Figure 9.6 – OData EDM]()
Figure 9.6 – OData EDM
-
现在,通过实现快速
ODataController,我们可以向 OData 查询公开整个集合:public class AuctionsController : ODataController { private readonly CosmosCollection<Auction> _cosmosCollection; public AuctionsController() { _cosmosCollection = new CosmosCollection<Auction>("AuctionsCollection"); } // GET: api/Users [EnableQuery] public IActionResult Get() { var items = _cosmosCollection.GetItemsAsync(); return Ok(items.AsQueryable()); } [EnableQuery] public async Task<IActionResult> Get(string key) { var auction = await _cosmosCollection.GetItemsAsync(item => item.Id == key); return Ok(auction.FirstOrDefault()); } }
有了给定的ODataController和用户集合的附加实现,就可以执行各种实体过滤器表达式,如以下所示:
http://localhost:20337/odata/auctions?$filter=Vehicle/Engine/Power%20gt%20120http://localhost:20337/odata/users?$filter=Address/City%20eq%20'London'http://localhost:20337/odata/users?$filter=startswith(FirstName,%20'J')http://localhost:20337/odata/auctions('7ad0d2d4-e19c-4715-921b-950387abbe50')
至此,我们已经成功地使用 ASP 为 Cosmos Document DB 公开了一个 OData 端点.NET Core 和实体框架核心。 现在我们已经有了检索方法,我们需要实现处理数据创建和操作的方法。
实现更新方法
当我们处理不支持部分更新的 NoSQL 数据存储时,至少可以说,实现更新端点实际上是不同的。 根据应用需求,我们可以选择两个不同的模式。
在经典的并发模型中,我们将接收一个带有完整对象主体的PUT请求,并检查更新是否正在对象的最新版本上执行。 可以在_ts属性集合项上执行此并发检查。 时间戳属性也被 Cosmos DB 容器本身用于处理并发性问题。 让我们开始吧。
在这个模型中,传入的对象体将被验证,以检查它是否携带最新的时间戳,如果不携带,表示冲突的409响应将作为响应发送回。 如果时间戳与存储库中的时间戳相匹配,那么我们可以自由地修改实体:
[EnableQuery]
[HttpPut]
public async Task<IActionResult> Put([FromODataUri]string key, [FromBody] Auction auctionUpdate)
{
var cosmosCollection = new CosmosCollection<Auction>("AuctionsCollection");
var auction = (await cosmosCollection.GetItemsAsync(item => item.Id == key)).FirstOrDefault();
if (auction == null)
{
return NotFound();
}
if (auction.TimeStamp != auctionUpdate.TimeStamp)
{
return Conflict();
}
await cosmosCollection.UpdateItemAsync(key, auctionUpdate);
return Accepted(auction);
}
然而,使用这种方法,随着对象树的大小增长,以及容器项的复杂性增加,更新的请求将变得更大,更难以执行(例如,更有可能发生冲突、无意识地删除属性等等)。 下面的截图显示了一个只更新拍卖文档的描述字段的请求:

图 9.7 - ASP。 网络更新请求/响应
为了减少更新调用的影响,至少对于客户端,我们可以使用PATCH方法。 在PATCH方法中,只有对象树的一部分作为增量传递,或者只有部分更新操作作为补丁操作传递。
让我们为相同的拍卖服务实现一个PATCH操作,并检查请求:
[EnableQuery]
[HttpPatch]
public async Task<IActionResult> Patch(
string key,
[FromBody] JsonPatchDocument<Auction> auctionPatch)
{
var cosmosCollection = new CosmosCollection<Auction>("AuctionsCollection");
var auction = (await cosmosCollection.GetItemsAsync(item => item.Id == key)).FirstOrDefault();
if (auction == null)
{
return NotFound();
}
auctionPatch.ApplyTo(auction);
await cosmosCollection.UpdateItemAsync(key, auction);
return Accepted(auction);
}
对给定端点的请求如下所示:
PATCH /odata/auctions('3634031a-1f45-4aa0-9385-5e2c86795c49')
[
{"op" : "replace", "path" : "description", "value" : "Updated Description"}
]
我们用时间戳值实现的乐观并发控制机制也可以用Test操作实现:
PATCH /odata/auctions('3634031a-1f45-4aa0-9385-5e2c86795c49')
[
{ "op": "test", "path": "_ts", "value": 1552741629 },
{ "op" : "replace", "path" : "vehicle/year", "value" : 2017},
{ "op" : "replace", "path" : "vehicle/engine/displacement", "value" : "2.4"}
]
在本例中,如果时间戳值与请求中的值不匹配,后续的更新操作将不会执行。
我们已经完成了UPDATE和PATCH方法,现在让我们继续DELETE动作。
软删除
如果您计划将存储级操作与触发器和/或更改提要集成在一起,那么可以使用软删除实现而不是实现对象的完全删除。 在软删除方法中,我们可以使用一个特定的属性(例如isDeleted)来扩展实体模型,该属性将定义文档被消费应用删除。
在此设置中,消费应用可以使用已实现的PATCH方法或显式的DELETE方法,可以为我们的实体服务实现该方法。
让我们来看看下面的PATCH请求:
PATCH /odata/auctions('3634031a-1f45-4aa0-9385-5e2c86795c49')
[
{ "op": "test", "path": "_ts", "value": 1552741629 },
{ "op" : "replace", "path" : "isDeleted", "value" : true},
{ "op" : "replace", "path" : "ttl", "value" : "30"}
]
通过此请求,我们表示应该将具有给定 ID 的拍卖实体标记为删除。 此外,通过设置Time To Live(TTL)属性,我们将触发给定实体的过期时间。 通过这种方式,触发器和更改提要都将收到关于此更新的通知,并且在给定的 TTL 内,实体将从数据存储中删除。
重要提示
TTL 是 Cosmos DB 的固有特性。 TTL 可以在容器级别设置,也可以在项目级别设置。 如果在容器级别没有设置值,那么平台将忽略项目的值集。 但是,容器的默认过期时间可以为-1,并且我们希望在某段时间后过期的项目可以声明一个大于 0 的值。 TTL 不消耗资源,不作为消耗的 RUs 的一部分计算。
通过删除实现,我们拥有一组完整的函数,可以根据 Cosmos 集合为微服务创建基本的 CRUD 结构。 我们用一个简单的。net core 风格的请求管道开始实现,我们创建了UPDATE和PATCH方法,最后,我们用软删除实现完成了实现。 现在我们可以开始处理 API 的润饰了。 我们将从使用 Redis 的性能改进开始。
集成 Redis 缓存
在具有细粒度微服务架构的分布式云应用中,分布式缓存可以提供非常需要的数据一致性以及性能改进。 一般来说,基础设施的分布、数据模型和成本是决定是否使用分布式缓存实现的因素。
ASP.NET Core 提供了各种缓存选项,其中之一是分布式缓存。 可选择的分布式缓存选项如下:
- 分布式内存缓存
- 分布式 SQL 服务器缓存
- 分布式缓存复述,
虽然内存缓存并不是一个生产准备策略,但 SQL 和 Redis 对于使用。net Core 开发的云应用来说是可行的选择。 然而,对于 NoSQL 数据库和半结构化数据,Redis 将是一个理想的选择。 让我们来看看如何引入分布式缓存并使其准备好使用:
-
In order to introduce a distributed cache that can be used across controllers, we would need to use the available extensions so that we can inject an appropriate implementation of the
IDistributedCacheinterface.IDistributedCachewould be our main tool for implementing the cache, aside from the pattern we mentioned previously:public interface IDistributedCache { byte[] Get(string key); Task<byte[]> GetAsync(string key, CancellationToken token = default(CancellationToken)); void Set(string key, byte[] value, DistributedCacheEntryOptions options); Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken)); void Refresh(string key); Task RefreshAsync(string key, CancellationToken token = default(CancellationToken)); void Remove(string key); Task RemoveAsync(string key, CancellationToken token = default(CancellationToken)); }如您所见,使用注入的实例,我们将能够以字节数组的形式设置和获取数据结构。 应用将首先访问分布式缓存以检索数据。 如果我们需要的数据不存在,我们将从实际的数据存储(或服务)中检索它,并将结果存储在我们的缓存中。
-
在我们可以在我们的应用中实现 Redis 缓存之前,我们可以前往 Azure 门户,在我们一直使用的资源组中创建一个 Microsoft Azure Redis 缓存资源。
-
一旦 Redis 缓存实例被创建,记录一个可用的连接字符串。 连接字符串可以在管理键刀片下找到,可以使用概述屏幕上的显示访问键选项访问该刀片。 现在我们可以继续我们的实现。
-
Start the implementation by installing the required Redis extension:
![Figure 9.8 – Redis Extension]()
图 9.8 - Redis 扩展
-
在安装了扩展及其依赖项之后,使用扩展方法
services.AddDistributedRedisCache( option => { option.Configuration = Configuration.GetConnectionString("AzureRedisConnection"); option.InstanceName = "master"; });配置我们的分布式缓存服务
-
现在,在
appsettings.json文件中插入我们从 Azure 门户检索到的连接字符串,然后转到我们的控制器,为IDistributedCache实例设置构造函数注入:private readonly IDistributedCache _distributedCache; public UsersController(IDistributedCache distributedCacheInstance) { _distributedCache = distributedCacheInstance; }
就这些了——分布式缓存已经可以使用了。 现在,我们可以将序列化的数据项作为键/值(其中值是序列化的字节数组)插入到缓存中,而无需与实际数据源通信。
与前端模式的后端结合,Redis 缓存可以将缓存的数据交付到应用网关服务,而无需联系多个微服务,提供了一个简单的成本削减解决方案,以及承诺的性能增强。
托管服务
由于我们的 web 实现使用了 ASP.NET Core,我们有多种部署和托管选项,可以跨越 Windows 和 Linux 平台。 我们将考虑的第一个选项是 Azure Web App 设置,它可以设置在 Windows 或 Linux 服务计划中。 然后我们将移动我们的 ASP。 它可以托管在我们之前提到的任何容器编排平台上,也可以简单地托管在一个运行在 Linux 服务器群上的 Azure web 应用中。
Azure Web App for App Service
此时,不需要任何额外的实现或配置,我们的微服务就可以部署了,它们已经可以作为一个完全托管的应用服务托管在 Azure 云上了。 为了将服务部署为应用服务,您可以使用 Visual Studio Azure 扩展,它允许您创建发布配置文件以及目标托管环境。
让我们看看如何做到这一点:
-
Right-click on the project to be deployed. You will see the Pick a publish target selection window:
![Figure 9.9 – Publish Target]()
图 9.9 -发布目标
对于完整的托管选项,我们可以选择App Service或App Service Linux选项,并继续创建新的应用服务。
-
If we were to choose the App Service option, the application would be hosted on the Windows platform with a full .NET Framework profile, whereas the Linux option would use Linux operating systems with the .NET Core runtime. Selecting the Create New option allows us to select/create the resource group we want the App Service instance to be added to:
![Figure 9.10 – Azure App Service]()
图 9.10 - Azure App Service
-
一旦发布,从站点 URL 字段复制 URL 并使用
curl:Microsoft Windows [Version 10.0.17134.590] (c) 2018 Microsoft Corporation. All rights reserved. C:\Users\can.bilgin>curl https://netcoreuserapi-dev.azurewebsites.net/odata/users?$filter=FirstName%20eq%20%27Jane%27 {"@odata.context":"https://netcoreuserapi-dev.azurewebsites .net/odata/$metadata#Users","value":[{"Id":"7aa0c870- cb90-4f02-bf7e-867914383190","FirstName":"Jane","LastName": "Doe","NumberOfAuctions":1,"Auctions":[],"Address": {"AddressTypeId":4000,"City":"Seattle","Street1":"23 Pike St.","CountryCode":"USA"}}]} C:\Users\can.bilgin>执行查询
集装箱服务
托管的另一种选择是将我们的应用容器化,这将带来作为代码原则配置的额外好处。 在容器设置中,每个服务都将被隔离在自己的沙箱中,并以高度的灵活性和性能轻松地从一个环境迁移到下一个环境。 如果将容器部署到前面提到的容器注册中心和平台(如 ACS、AKS、Service Fabric 和 Service Fabric Mesh),那么与 web 应用相比,容器还可以节省成本。
重要提示
容器是隔离的、受管理的、可移植的操作环境。 它们提供了一个位置,在这个位置上,应用可以在不影响系统其他部分的情况下运行,系统也不会影响应用。 与 vm 相比,它们提供了更高的服务器密度,因为它们不共享硬件,而是共享操作系统内核。
Docker 是一种容器技术,近年来已经几乎成为容器本身的同义词。 Docker 容器托管环境可以使用提供的免费软件在 Windows 和 macOS 上创建。 让我们开始:
-
为了准备一台用于 Docker 容器化的 Windows 开发机器,我们需要确保已经安装了以下设备:
-
Docker for Windows(用于承载 Windows 和 Linux 容器)
-
用于 Visual Studio 的 Docker 工具(可选择安装 Visual Studio 2017 v15.8+)
-
Now that we have the prerequisites, we can add a Docker container image definition to each microservice project using the Add | Docker Support menu item:
![Figure 9.11 – Docker Support]()
图 9.11 - Docker 支持
-
这将创建一个多级 Docker 文件,简单来说,它将做以下工作:
-
将应用的源代码复制到容器映像中。
-
恢复所需的。net Core 运行时组件,这取决于容器的类型(Windows 或 Linux)。
-
编译应用。
-
使用编译后的应用组件创建最终的容器映像。
-
这里,我们为
UsersApi创建的 Docker 文件如下所示:
如你所见,定义 base 的第一个阶段是引用公有 Docker 注册表中的 microsoft 管理的容器映像。 构建映像是我们拥有 ASP 源代码的地方.NET Core 应用。 最后,构建和最终映像是将应用编译(即dotnet publish)并将其设置为容器的入口点的最后阶段。
换句话说,创建的容器映像拥有最终的应用代码以及所需的组件,而不管主机操作系统和该主机上运行的其他容器是什么。
现在,如果你在控制台或终端(取决于安装 Docker 的操作系统)导航到UsersApi项目的父目录,并执行以下命令,Docker 将构建容器镜像:
docker build -f ./NetCore.Web.UsersApi/Dockerfile -t netcore-usersapi .
一旦 Docker 守护进程构建了容器镜像,你可以使用下面的命令来检查镜像是否可以作为容器启动:
docker image ls
如果映像在可用容器映像列表中,现在可以使用暴露的端口80或443运行该容器(下面的命令将容器端口80映射到主机端口8000):
docker run -p 8000:80 netcore-usersapi
当然,容器开发与 Windows 平台上的 Visual Studio 的集成程度更高。 事实上,ASP.NET Core 应用,一旦容器化,包含一个运行/调试 Docker 配置文件,可以直接从 Visual Studio UI 启动:

图 9.12 - Docker 启动设置
在这个阶段,我们的容器配置已经可以使用了,并且它已经可以部署到 Azure Web App for Containers 中了。 可以使用 Visual Studio 中的可用工具添加容器编排支持。
在本节中,我们测试了 PaaS 和 CaaS 模型,首先使用应用服务来托管应用,然后使用 Docker 将. net 应用容器化。 ASP.NET 应用包和容器可以托管在 App Service 上。 现在我们的应用已经准备好在云上托管了,让我们看看保护它的可用选项。
保护应用
在具有客户端特定后端的微服务设置中,可以使用多种身份验证策略来保护 web 应用。 ASP.NET Core 提供了所需的 OWIN 中间件组件来支持大多数这些场景。
根据网关和下游业务架构,可以在网关上实现身份验证/授权,用户身份可以转移到后端服务:

图 9.13 -网关标识
另一种方法是,每个服务可以在联合设置中使用相同的身份提供者。 在此设置中,客户端应用将使用专用的安全令牌服务(STS),并且需要在 STS 和应用服务之间建立信任关系:

图 9.14 -微服务身份
在选择身份验证和授权策略时,一定要记住,此设置中的身份使用者将是本地移动客户端。 当涉及移动应用时,选择的认证流程一般为 OAuth 2 授权码流:

图 9.15 - OAuth 阶段
再一次,这取决于您正在构建的应用,多个OpenID 身份连接(OIDC)提供者,如微软的 Live, Facebook 和谷歌,可以引入允许用户选择他们喜欢的身份。
**## NET Core 身份
ASP.NET Core Identity 是默认的成员系统,它可以提供相对简单但广泛的 STS 实现,以及登录、注册和管理 ui。 与它的前身相比,ASP.NET Core Identity 为开发人员提供了更广泛的身份验证场景,如 OAuth、双因素身份验证、基于时间的一次性密码二维码(TOTP)等。
ASP.NET Core Identity 默认情况下使用 SQL 数据库作为持久性存储,并且可以用其他存储库实现替换。 实体框架核心用于实现标准存储库功能。
由 ASP 支持的外部 OIDC 提供商.NET Core 身份是 Facebook, Twitter,谷歌和微软。 可以在第三方或社区提供的包中找到其他提供者实现。
使用 ASP.NET Core Identity,创建的 STS 可以由 Xamarin 应用通过一组简单的 HTTP 请求使用,以注册、验证和授权用户。 此外,Xamarin 应用可以利用可用的身份提供者 sdk,以及跨提供者包。
虽然这种身份管理应该足够了,因为需求只针对一个 ASP.NET 基于核心的解决方案,一旦将额外的 Azure 资源包括在分布式应用中,例如 Azure 无服务器组件,基于云的身份管理可能是一个更好的选择。
Azure AD
Azure 广告基于云的身份作为服务(IDaaS)提供,迄今为止,唯一的认证和身份管理过程与资源管理器集成的分布式应用开发的 Azure 的基础设施。 Azure AD 用于管理对资源组中任何 SaaS/PaaS 资源的访问。 它支持 OpenID Connect、OAuth 和 SAML 等协议,以提供对目录内资源的 SSO 和访问控制。
**可以使用目录中定义的标识原则来设置访问资源和资源之间的授权。 该原则可以表示具有单个组织(可能具有关联的本地活动目录)、应用(在目录或外部应用中设置的资源,如本地移动应用)的用户,或者来自不同 Azure 目录或外部身份提供者的外部身份。
一般来说,这种在组织单元中定义用户身份,并将来自其他目录的用户作为来宾引入的设置称为 Azure ADBusiness to Business(B2B)。
在中,为了使用 Azure AD B2B 建立一个应用范围的认证方案,遵循以下步骤:
-
Create a directory that will define the organization that will be using the application suite (that is, the mobile application and associated services).
重要提示
任何 Azure 订阅至少都附带免费的 Azure AD(取决于订阅类型),而且在大多数情况下,不需要创建新目录。
您可以使用 Azure 门户上的create a Resource接口来创建一个新目录。 一旦您选择了 Azure AD,您将需要声明一个组织名称和初始域名(例如
netcorecrossplatform.onmicrosoft.com)。 -
Additional custom domains can be added to this declaration at a later time:
![Figure 9.16 – Creating Azure Active Directory]()
图 9.16 -创建 Azure Active Directory
-
Once the directory is created, you'll see that the organization should be available as a domain option so that you can set up the authentication for an ASP.NET Core web application using Visual Studio:
![Figure 9.17 – Adding Authentication]()
图 9.17 -添加认证
一旦应用项目被创建或使用所选的认证选项更新,它将自动向 Azure AD 添加一个应用注册表,并为应用认证添加/配置所需的中间件。 Azure AD 在应用启动时的配置看起来类似如下:
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme) .AddAzureADBearer(options => Configuration.Bind("AzureAd", options));创建的配置(匹配 Azure AD 应用注册)是,如下:
"AzureAd": { "Instance": "https://login.microsoftonline.com/", "Domain": "netcorecrossplatform.onmicrosoft.com", "TenantId": "f381eb86-1781-4732-9543-9729eef9f843", "ClientId": "ababb076-abb9-4426-b7df-6b9d3922f797" },对于 Azure AD,在客户端应用上进行身份验证(考虑到我们正在使用 Xamarin 和 Xamarin。 窗体作为开发平台)可以使用Microsoft 身份验证库(MSAL)实现。 遵循以下步骤:
-
In order for the client application to be able to use the identity federation within this organization, register (yet) another application on Azure AD. However, this registration should be declared for a native application:
![Figure 9.18 – Registering an Application to Azure AD]()
图 9.18 -注册一个应用到 Azure AD
-
创建应用注册后,使用当前目录(即租户)和客户端应用(即应用注册)设置身份验证库。 在这个设置中,身份流可以简单地定义如下:
-
本机应用使用授权代码流检索访问令牌
-
本机应用执行对网关服务(即我们的 ASP. net 服务)的 HTTP 请求.NET Core 服务暴露特定于移动应用的端点)
-
The gateway service verifying the token and retrieving an on-behalf-of token to call the downstream stream services
通过这种方式,可以将用户身份传播到每个层,并且可以使用声明原则实现所需的授权过程。
-
-
In order to allow identity propagation, the gateway service application registration (that is, the service principal) should be given the required identity delegation permissions to the downstream service registrations:
![Figure 9.19 – Adding API Permissions]()
图 9.19 -添加 API 权限
-
现在,用户标识可以访问资源,前提是它们的标识存在于目标组织中,并且具有所需的权限。
对于一个面向业务应用(即业务(LOB)应用),B2B Azure 广告可以提供一个安全的身份管理解决方案轻松,并没有额外的自定义实现。 然而,如果应用需要面向客户端,我们将需要一个更灵活的解决方案,并提供额外的注册支持。 Azure B2C 可以为个人用户帐户提供所需的支持。
Azure AD B2C
Azure AD B2C 是面向消费者场景的身份管理服务,可以选择自定义和控制客户如何注册、登录以及在使用您的应用时如何管理他们的配置文件。 这针对各种平台。
B2C 是一种现代的联合身份管理服务,其中消费者应用(即依赖方)可以使用多个身份提供者和验证方法。
在 B2C 领域中,用于注册和登录的用户流被称为用户旅程。 如果需要,可以使用策略定制用户旅程。 身份体验框架使用这些策略来实现所需的用户流。 身份体验框架是建立多方信任并完成用户旅程步骤的底层平台。
与 Azure AD 本身类似,租户描述一个用户域,其中可以定义用户和应用之间的某些关系。 然而,在 B2C 中,域是特定于客户的,而不是特定于组织的。 换句话说,租户定义一个由策略描述和链接的标识提供者控制的访问组。
在这种设置中,多个应用可以访问多个租户,这使得 B2C 非常适合开发公司,因为它们拥有一套要发布给消费者的应用。 消费者一旦注册使用一个链接的 OIDC 身份提供商,就可以访问多个面向消费者的应用。
在本节中,我们开始使用。net Core Identity 进行安全模型研究。 虽然它为较小的使用者应用提供了最主流的需求,但如果身份管理需求的数量增加,它很快就会变得过于复杂。 正如您所看到的,Azure AD 也可以很容易地集成到 ASP 中.NET 应用,并且可以为复杂的 LOB 场景提供基础设施。 最后,我们简要地介绍了 Azure AD B2C,它提供了一个完整的联邦身份管理平台,具有灵活的扩展选项。
总结
在本章中,我们浏览了 PaaS 平台,以及可用于托管和实现 ASP 的架构方法.NET Core web 服务。 使用 ASP 提供的灵活的基础设施。 对于开发人员来说,实现使用来自 Cosmos DB 集合的数据的微服务是一种解脱。 在域对象上包含 CRUD 操作的服务可以通过 Redis 和容器化进行优化和改进,并托管在多个平台和操作系统上。 安全性是我们在分布式云架构中主要关注的问题之一,可以通过在 Azure 云堆栈上使用可用的身份基础设施和 iaas 产品(如 Azure AD 和 Azure AD B2C)来确保安全性。
在下一章中,我们将转向 Azure 无服务器,这是另一个服务平台,在它上。net Core 可以被证明是至关重要的。************
十、在 Azure 无服务器环境下使用 .NET Core
Azure 函数是利用各种触发器(包括 HTTP 请求)的无服务器计算模块。 通过使用 Azure 功能,开发人员可以创建业务逻辑容器,完全摆脱单一 web 应用范例和基础设施带来的问题。 它们可以用作简单的 HTTP 请求处理单元和所谓的微服务,也可以用于编排复杂的工作流。 Azure 函数有两种风格(编译型和基于脚本的),可以用不同的语言编写,包括带有。NET Core 模块的 c#。
在本章中,您将有机会使用 Azure 函数的不同运行时,并了解 Azure 无服务器的可用配置、触发器和托管选项。 然后我们将把 Azure 功能合并到我们的基础设施中,这样我们就可以处理不同触发器上的数据。 然后,我们将把 Azure 的功能与一个逻辑应用集成在一起,它将在我们的设置中用作一个处理单元。
本章将涵盖以下主题:
- 理解 Azure Serverless
- 实现 Azure 功能
- 创建逻辑应用
- 将 Azure 服务与功能集成
在本章结束时,您将更多地了解 Azure 函数和逻辑应用的基本概念。 您将能够将 Azure 无服务器组件集成到您的云基础设施中,并根据您的需求实现功能。 换句话说,您将能够在您的下一个基于云的项目中,自信地为您的特殊需求设置 Azure 功能和逻辑应用。
了解 Azure 无服务器
在云平台上开发分布式系统有它的优点,也有它的缺点,例如,当您管理的数据分散在多个进程上时,复杂性会越来越大。 发现自己处于死锁的情况是非常常见的,在这种情况下,为了降低引入新特性的成本,您不得不在架构需求上做出妥协。 Azure 无服务器组件可以通过其简单的事件驱动计算经验为特殊需求提供灵活的解决方案。
在第八章,【5】创建一个数据存储与宇宙 DB,我们创建了一个文档结构库,后来,在【显示】第 9 章,【病人】创建 Microservices Azure 应用服务,我们实现了 ASP.NET Core 服务作为容器化的微服务,这样我们就可以涵盖我们的主要应用用例。 可以将这些用例视为通过应用的主要数据流,我们对性能的主要关注集中在这些数据路径上。 然而,第二个用例,如跟踪状态的拍卖的用户,他们曾参与,或创建一个提要通知用户有关新车拍卖,的功能,可以增加用户的回报率和维护用户群。 因此,我们需要一个坚定的、事件驱动的策略,它不会干扰主要功能,并且应该能够在不干扰基础设施的情况下伸缩。
简而言之,Azure 功能和其他 Azure 无服务器组件是为这些类型的事件驱动场景量身定制的 Azure 产品,在这些场景中需要协调一个或多个 Azure 基础设施服务。
开发 Azure 功能
Azure 功能,作为 Azure 无服务器生态系统中最早的成员之一,为开发语言和 sdk 提供了多种选择,欢迎来自不同平台的开发人员。 在本节中,我们将了解开发选项和功能集成选项。 我们最终将实现一个示例 Azure 函数,使用来自 Cosmos DB 数据库中不同文档存储的数据创建一个物化视图。
开发环境中用于开发 Azure 功能的可用选项包括但不限于以下选项:
- 使用 Azure 门户
- 使用 Azure CLI 和 Azure 功能的核心工具
- 使用 Visual Studio 或 Visual Studio 代码
- 使用其他 ide,如 Eclipse 或 IntelliJ IDEA
至于语言和运行时,我们可以用以下方法创建函数:
- Java/Maven
- Python
- c#(.NET Core 和脚本)
- JavaScript/Node
- f#(。 净核心)
如您所见,可以使用多个平台和多个开发环境组合来创建 Azure 功能。 这意味着任何操作系统,包括 Windows、macOS 和 Linux,都可以用作开发站。 在接下来的部分中,我们将尝试使用这些平台和工具中的一些来开发 Azure 函数,以展示 Azure 函数的通用性。
使用 Azure 函数运行时
我们将从可用的 Azure 功能选项开始我们的旅程。 在本演示中,我们将首先使用 Python 创建一个 Azure 函数,只使用一个简单的文本处理程序,然后继续在。net 和 c#上执行同样的操作。
闲话少说,让我们从使用 macOS 的跨平台开发工具集开始,通过使用 Azure functions Core Tools 创建我们的示例函数:
-
为了安装平台运行时,我们将首先注册
azure/functions存储库:brew tap azure/functions -
Once the
azure/functionsrepository has been registered, continue with the installation of Azure Functions Core Tools:brew install azure-functions-core-tools安装应该不会花费很长时间,您应该会看到类似这样的输出:
![Figure 10.1 – Installing Azure Functions Core Tools]()
图 10.1 -安装 Azure 功能核心工具
安装完成后,我们可以继续开发示例函数。 为了演示函数,我们将创建一个简单的计算器函数(即x + y = z)。
-
接下来,初始化一个虚拟工作环境以从 Python 开发开始:
$ python3 -V Python 3.6.4 $ python3 -m venv .env $ ls .env $ source .env/bin/activate (.env) $ -
创建并激活环境之后,使用以下命令初始化函数项目。 出现提示时,选择
python作为运行时: -
创建项目之后,创建一个名为
add的新函数。 提示时,选择HTTP trigger作为模板:(.env) $ cd myazurefunctions/ (.env) $ func new Select a template: 1\. Azure Blob Storage trigger ... 9\. Timer trigger Choose option: 5 HTTP trigger Function name: [HttpTrigger] add -
Now that the function has been created, you can use any editor to edit the
__init__.pyfile in order to implement the function, as follows:![Figure 10.2 – Azure Function in Python]()
图 10.2 - Python 中的 Azure 函数
-
为了测试我们的函数,在项目目录中,执行以下命令,它将启动本地函数服务器:
func host start -
一旦函数服务器运行,就在终端窗口上显示的给定端口上执行
get查询。 这会触发 HTTP 请求并返回结果:curl 'http://localhost:7071/api/add?x=5&y=8' Addition result is 13
我们现在已经成功地使用 Python 和 HTTP 触发器模板创建了一个 Azure 函数。
如果在创建步骤中,我们让选择了第一个选项,也就是dotnet,那么这个项目将使用编译过的 c#函数模板创建:
$ func init myazurefunctions
Select a worker runtime:
1\. dotnet
2\. node
3\. python
Choose option: 1
dotnet
$ cd myazurefunctions
$ func new
Select a template:
1\. QueueTrigger
2\. HttpTrigger
...
12\. IotHubTrigger
Choose option: 2
Function name: add
$ nano add.cs
$ func host start
我们的add函数的源代码看起来类似如下:

图 10.3 - c#中的 Azure 函数
当然,虽然这只适用于演示的目的,但使用 Visual Studio(针对 macOS 和 Windows)以及 Visual Studio Code 进行开发将提供真实的. net 开发环境。
在本部分中,我们已经成功地在 Python 和。net 上创建了一个 http 触发函数。 在此过程中,作为一个开发环境,我们使用了一个简单的文本编辑器和 Azure CLI 来演示 Azure 功能开发的简单性。 在下一节中,我们将深入了解可用于函数的其他触发器,并了解绑定是如何工作的。
函数触发器和绑定
函数项目和我们在前一节中创建的函数,尽管它们是在完全独立的运行时上实现和执行的,但是携带函数的表现遵循相同的模式(即function.json)。 虽然 Python 实现的清单可以在与函数同名的文件夹中找到,但 dotnet 版本只能在编译时从实现中使用的属性中生成(可以在bin/output/<function>文件夹中找到)。 比较这两个清单,我们可以立即识别出为这些函数定义输入、输出和触发机制的各个部分:

图 10.4 - Azure 函数绑定
正如前面提到的,Azure 函数是事件驱动的 Azure 资源。 触发器机制不仅定义函数何时执行,还定义输入和输出数据类型和/或连接的服务。 例如,一个 blob 存储条目可以用作触发器,也可以用作输入和输出。
类似地,HttpTrigger可以定义执行方法以及输入和输出机制(如前面的示例所示)。 这样,额外的服务集成就可以作为声明性属性而不是功能性实现包含在函数中。
其中一些绑定类型如下:

图 10.5 -可用的 Azure 绑定选项
除了之外,还有其他扩展可以通过 Azure Functions Core Tools 或 NuGet 包获得,默认情况下,只有定时器和 HTTP 扩展会在函数运行时中注册。
如您所见,Azure Function 绑定提供了一种非常简单的清单方法来将函数与来自不同资源的各种事件集成在一起。 然而,清单可能不足以配置我们的事件驱动函数; 我们需要一个合适的配置设施。
功能配置
Azure 函数使用与 ASP 相同的配置基础设施.NET Core 应用,因此使用了Microsoft.Extensions.Configuration模块。
在开发期间,当应用在本地运行时上运行时,为了从local.settings.json文件读取配置值,需要创建一个配置构建器并使用AddJsonFile扩展方法。 创建配置实例后,可以通过配置实例的 indexer 属性访问配置值和连接字符串。
在部署到 Azure 基础设施的过程中,设置文件被用作模板,用于创建将通过 Azure 门户和资源管理器进行管理的应用设置。 也可以使用相同的原则访问这些值,但它们是作为环境变量添加的。
为了支持这两种场景,我们可以使用在创建配置实例时可用的扩展方法:
var config = new ConfigurationBuilder()
.SetBasePath(context.FunctionAppDirectory)
.AddJsonFile("local.settings.json", optional: true,
reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
现在,我们已经熟悉了如何设置函数清单并配置它,让我们看看用于托管它的选项。
托管功能
Azure 功能一旦部署,就托管在 App Service 基础设施上。 在 App Service 中,正如您在前面的示例中看到的,只有计算资源(可能还有其他集成资源)会累积到您的账单中。 此外,在消费计划中,Azure 函数仅在被已配置的事件之一触发时才激活; 因此,在任务关键型集成场景中,Azure Functions 具有极高的成本效益。 函数资源还可以根据它们所处理的负载进行伸缩和缩小。
第二个可供功能使用的计划是保费计划。 在付费计划中,您可以选择设置始终运行的函数以避免冷启动,还可以配置无限的执行时间。 无限的持续时间对于长时间运行的进程来说很有用,因为默认情况下,Azure 函数有 5 分钟的硬限制,通过额外配置可以将其延长到 10 分钟。
本节总结了 Azure 函数可用的托管选项,并完成了对 Azure 函数基本概念的介绍。 现在,让我们使用这些概念为我们的ShopAcross应用实现一个 Azure 函数。
创建第一个 Azure 函数
前面在 Azure 的上下文中提到的模式之一是实体化视图。 例如,在我们的初始结构中,我们有关于嵌入在用户文档中的拍卖的基本信息。 通过这种方式,拍卖可以作为用户配置文件的一部分,并且可以根据用户参与成功拍卖的情况对他们进行评级。 在这种设置中,由于用户配置文件上的拍卖数据只是来自主拍卖表的非规范化数据块,因此服务不需要直接与拍卖表交互。
让我们看看下面的用户故事,看看我们如何实现这个解决方案:
“作为一个解决方案架构师,我想实现一个 Azure 功能,用修改过的拍卖数据来更新 Cosmos DB Users 集合,这样拍卖 API 就可以从用户 API 中分离出来。”
我们在这里的任务是实现一个 Azure 函数,该函数将在修改拍卖文档时触发。 本文档的更改应传播到用户集合:

图 10.6 -在 Cosmos DB 上同步文档数据
在这个设置中,我们将从以下步骤开始:
-
First, we will create our Azure Functions project, which will be hosted as a function app in our resource group:
![Figure 10.7 – Cosmos DB Triggered Function Template]()
图 10.7 - Cosmos DB 触发函数模板
这将创建我们的第一个函数,其声明如下:
[FunctionName("Function1")] public static void Run( [CosmosDBTrigger( databaseName: "ProductsDb", collectionName: "AuctionsCollection", ConnectionStringSetting = "DbConnection", LeaseCollectionName = "leases")] IReadOnlyList<Document> input, ILogger log)通过使用
CosmosDBTrigger,我们是指示 Azure 函数运行时创建一个租赁,这样我们可以连接到宇宙 DB 改变以给定的数据库(即ProductsDb)和集合(即AuctionsCollection)使用连接字符串集设置(即DbConnection)。 -
现在,让我们展开配置,以包含给定的连接字符串设置:
{ "ConnectionStrings": { "DbConnection": "AccountEndpoint=https://handsoncrossplatform.documents.azure.com:443/;AccountKey=...;" } } -
Let's now add the additional lease settings. After this step, our trigger declaration will look like this:
[CosmosDBTrigger( databaseName: "ProductsDb", collectionName: "AuctionsCollection", ConnectionStringSetting = "DbConnection", LeaseCollectionPrefix = "AuctionsTrigger", LeaseCollectionName = "LeasesCollection")]通过定义租约集合,您可以记录 Azure 函数使用的触发器。 为了使用单个租赁集合,在
LeaseCollectionName选项之上,我们还可以将LeasePrefix属性添加到声明中。 这样,每个租约条目将收到一个前缀值,这取决于函数声明。 -
After this, we can run our function in debug mode and see whether our trigger is working as expected. After updating a document on the
AuctionsCollectioncollection, you will receive the updated data almost immediately:![Figure 10.8 – Executing the Azure Function]()
图 10.8 -执行 Azure 功能
-
我们现在收到修改后的文件 如果修改仅基于传入的数据,我们可以添加一个带有单个文档的输出绑定或一个
async收集器来修改或将文档插入到特定的集合中。 但是,我们想要更新用户参与的拍卖列表。 因此,我们将使用属性声明获得 Cosmos 客户端实例:[CosmosDBTrigger( databaseName: "ProductsDb", collectionName: "AuctionsCollection", ConnectionStringSetting = "DbConnection", LeaseCollectionPrefix = "AuctionsTrigger", LeaseCollectionName = "LeasesCollection")]IReadOnlyList<Document> input, [CosmosDB( databaseName: "ProductsDb", collectionName: "UsersCollection", ConnectionStringSetting = "DbConnection")] DocumentClient client,
现在,使用客户机,我们可以对 Users 文档集合执行必要的更新。
在本节中,我们已经成功地使用 Python 和 c#,使用 HTTP 和 Cosmos DB 触发器实现了函数。 现在我们对 Azure 功能的可用开发和集成选项有了更广泛的了解。 接下来,我们将研究 Azure 的另一个无服务器成员,逻辑应用。
开发逻辑应用
逻辑应用是简单的、事件驱动的工作流声明,可以利用许多内在的动作以及其他 Azure 无服务器资源。 他们还开发了与 Azure 功能类似的基于触发的执行策略。 在本节中,我们将学习如何使用逻辑应用创建简单的工作流,以及如何将它们与其他 Azure 资源集成。
从理论上讲,当一个开发人员需要实现一个逻辑应用时,除了文本编辑器,他不需要其他任何东西,因为逻辑应用是 ARM 资源模板的扩展。 逻辑应用的清单包含四个主要成分:
- 参数
- 触发器
- 行动
- 输出
参数、触发器和输出,类似于 Azure 函数中的绑定概念,定义应用何时以及如何执行。 操作定义应用应该做什么。
逻辑应用可以使用带有额外模式和/或可视化支持的 IDE(如 visual Studio)创建,也可以使用 web 门户在 Azure 门户上单独开发。
下面的章节将带你通过使用 logic App Designer、连接器、Azure 功能和内置流控制机制来创建和设计逻辑应用。
实现逻辑应用
为了用 Visual Studio 创建一个逻辑应用,我们需要做以下工作:
-
We need to use the Azure Resource Group project template and select the Logic App template from the screen that follows:
![Figure 10.9 – Azure Logic App]()
图 10.9 - Azure Logic App
这将创建一个包含逻辑应用定义的资源组清单。 逻辑应用现在可以使用 Visual Studio 中的逻辑应用设计器进行修改,如果安装了 Azure logic Apps Tools 扩展(右键单击资源组 JSON 文件,并选择Open with logic app designer)。
-
实现 Logic App 的第一步是选择触发器,这将是我们工作流中的初始步骤。 对于本例,让我们选择当接收到 HTTP 请求时。
-
Now that the logic app flow has been created, let's expand the HTTP request and paste a sample JSON payload as the body of a request we are expecting for this application trigger:
![Figure 10.10 – HTTP Trigger Schema]()
图 10.10 - HTTP 触发器模式
这将生成
Request Body JSON Schema。 现在,我们可以发送请求,就像在示例 JSON 有效负载中一样。 -
Next, we will add an action to send an email (there are many email solutions; for this example, we will be using the Send an email action using Outlook):
![Figure 10.11 – Send an Email Action]()
图 10.11 -发送电子邮件操作
如您所见,我们实际上正在使用触发器中定义的电子邮件、主题和消息参数来填充电子邮件操作。
-
最后,我们可以添加一个Response操作,并定义响应头和响应体。 现在,我们的应用已经准备好执行:

图 10.12 - HTTP 响应动作
部署逻辑应用之后,您可以从 Azure 门户设计器检索请求 URL 以及集成的安全令牌。 使用所需的参数执行简单的POST调用将触发逻辑应用并触发操作:
curl -H "Content-Type:application/json" -X POST -d '{"email":"can.bilgin@authoritypartners.com", "title":"Test", "subject":"Test", "message" : "Test Message"}' "https://prod-00.northcentralus.logic.azure.com:443/workflows/5bb----------/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=eOB----------------"
Successfully sent the email to can.bilgin@authoritypartners.com
如您所见,使用逻辑应用,这些类型的简单或更复杂的业务工作流可以声明性地转换为 web 服务,并在诸如队列、提要和 webhook 等触发器上执行。 连接器是这个设置中的关键组件,它服务于这些操作和逻辑应用可用的触发器。
使用连接器
在前面的示例中,我们使用了 HTTP 触发器和响应操作,以及 Outlook 电子邮件操作。 这些操作和触发器被打包在逻辑应用基础设施的所谓连接器中。 连接器本质上是更大的 SaaS 生态系统的一部分,该生态系统还包括 Microsoft Flow 和 Power Apps,以及逻辑应用。 连接器可以描述为各种 SaaS 产品(例如,电子邮件、社交媒体、发布管理、HTTP 请求、文件传输等)的封装连接组件。
在标准的免费连接器集(包括第三方连接器)之上,企业集成包(EIP)是一个高级产品,它为 B2B 企业集成服务提供构建块。 这些集成场景通常支持围绕着行业标准,也就是说,电子数据交换【显示】(****EDI)【病人】和企业应用集成(EAI)。
还可以创建自定义逻辑应用连接器,以便实现可用连接器集无法实现的其他自定义用例。
如果/当通过提供的操作不能满足需求,Azure 功能可以作为任务集成到逻辑应用中。 这样,任何自定义逻辑都可以通过。net Core 和逻辑应用和功能之间的简单 HTTP 通信嵌入到工作流中。
创建我们的第一个逻辑应用
到目前为止,主服务应用被构建为,以适应主要应用用例,并为用户提供数据,以便他们可以创建拍卖和用户配置文件,以及对拍卖进行投标。 然而,我们需要找到更多方法来利用用户的兴趣来吸引他们。 对于这种类型的业务模型,我们可以利用各种通知渠道。 这些渠道中最突出的是定期通知电子邮件设置。
我们在这个例子中使用的用户故事如下所示:
“作为产品所有者,如果有新的拍卖可用,我会定期向注册用户发送电子邮件,根据他们之前的兴趣,这样我就可以吸引用户,提高回报率。”
在开始实现逻辑应用之前,尽管可以使用 Cosmos DB 连接器来加载数据,但让我们再创建两个 Azure 函数来分别加载用户和电子邮件目标和内容的最新拍卖。 这两个函数都应该使用HttpTrigger,并且应该返回 JSON 数据作为响应。 对于本练习,您可以使用在前一节中创建的相同的 Azure 函数项目。 让我们开始:
-
返回我们将发送通知的用户列表的函数如下:
[FunctionName("RetrieveUsersList")] public static async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log) { // TODO: Retrieve users from UsersCollection var users = new List<User>(); users.Add(new User{ Email = "can.bilgin@authoritypartners.com", FirstName = "Can"}); return (ActionResult)new OkObjectResult(users); } -
Next, we will need the data for the latest auctions:
[FunctionName("RetrieveLatestAuctionsList")] public static async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log) { // TODO: Retrieve latest auctions from AuctionsCollection var auctions = new List<Auction>(); auctions.Add(new Auction { Brand = "Volvo", Model = "S60", Year = 2017, CurrentHighestBid = 26000, StartingPrice = 25000 }); return (ActionResult)new OkObjectResult(auctions); }现在我们已经准备好了数据提要,我们可以开始实现逻辑应用了。
重要提示
在本例中,我们使用了 Azure 函数来检索一组 dto,以达到成本效益。 还可以创建一个更改提要功能,以便在数据存储用新用户或拍卖/投标更新时准备每日通知提要。 通过这种方式,逻辑应用可以直接从每日提要文档集合或表存储中加载数据。
-
In our previous example, we created a logic app using an HTTP trigger. For this example, let's start with a recurrence trigger so that we can process our data and prepare a periodic feed:
![Figure 10.13 – Recurrence Trigger Setup]()
图 10.13 -重复触发设置
-
Next, let's retrieve the set of users using our Azure function. In order to select a function from the available actions, you should locate the Azure Functions action in the Choose an action dialog, then select the target function app that contains your functions, and finally, select the desired function:
![Figure 10.14 – Azure Functions with the Logic App]()
图 10.14 - Azure 功能与逻辑应用
-
Once we have retrieved the results from the Azure function, it will be in JSON format. In order to ease the design and access properties of the contained data items, it would be good to include a data parse action with a predefined schema. At this point, you can have a simple run and copy the results:
![Figure 10.15 – Calling the Azure Function]()
图 10.15 -调用 Azure 功能
-
Now, repeat the same actions for the auctions list data, so that we can start building out email content. Our current workflow should look similar to the following:
![Figure 10.16 – Sequential Azure Function Calls]()
图 10.16 -顺序 Azure 函数调用
-
在我们继续准备电子邮件内容并将其发送给列表中的每个用户之前,让我们构造一下流,这样针对用户的数据检索操作和拍卖不会按顺序执行:

图 10.17 -并行 Azure 函数调用
到目前为止,我们已经创建了一个逻辑应用,它使用两个单独的 Azure 函数从 Cosmos DB 检索用户列表和可用拍卖。 然后逻辑应用解析 JSON 数据,以便下面的操作可以利用对象数据。 现在我们可以继续使用其他控制语句并准备电子邮件内容。
工作流执行控制
根据定义,作为一种编制工具,Logic Apps 使用诸如foreach、switch和conditionals等控制语句,以便使用可用的操作组成一个受控工作流。 这些控制语句可以使用逻辑应用上下文中其他操作的输入和输出值作为工作流中的操作。 可用的控制语句集如下:
- 条件:用于评估一个条件,并根据结果定义两个不同的路径
- Foreach:用于执行序列中每个项目的相关操作的路径
- 作用域:用于封装动作块
- 开关:根据开关输入,用于执行多个独立的动作块
- Terminate:终止当前逻辑应用的执行
- Until:用作
while循环,其中执行动作块,直到定义的条件计算为true
这些语句可以通过Logic App Designer中的Control操作来访问:

图 10.18 -控制动作
在我们的示例中,我们应该向每个用户发送一封带有最新拍卖列表的电子邮件。 对于用户列表中的每个操作,我们可以使用来实现这一点:

图 10.19 -从 For each 循环中使用变量
正如你所看到的,我们使用的是UsersList行动的主体(即body(UsersList),使用逻辑应用符号),列表中的每一项,我们检索电子邮件(即items('For_each')['email'])和firstName。 以类似的方式,我们可以准备拍卖的电子邮件主体,并将结果分配为主题的主体。 除了这个简单的设置外,还可以根据用户的兴趣使用可用的数据操作对内容进行过滤:

图 10.20 -完整的 Azure Logic 应用视图
现在,我们将定期向用户发送拍卖更新,而不必妥协或增加我们当前服务基础设施的额外复杂性。
在本节中,我们已经了解了基本的逻辑应用概念,并使用几个控制语句、连接器和自定义 Azure 函数实现了一个功能齐全的 Azure logic 应用。 如您所见,Azure 逻辑应用设计的声明性和事件驱动特性使它们成为编排次要事件驱动流程的好选择。
与 Azure 服务集成
到目前为止,我们只在逻辑应用和 Azure 功能的上下文中使用了 Cosmos DB,在众多 Azure 服务中,我们可以将其与 Azure 无服务器组件集成。 在本节中,我们将分析 Azure 无服务器组件与其他 Azure 资源的其他可用集成选项。
如您所见,这些集成可以通过 Azure 功能的绑定和逻辑应用的连接器来实现。 使用这个集成的业务模型,可以组合多个体系结构模式,并且可以完成事件驱动的场景。
下面几节将带您了解可以与 Azure 无服务器组件集成的基于存储库、队列和事件的 Azure 资源。 让我们更深入地了解其中一些集成服务。
存储库
在 Azure 无服务器环境中,可以说几乎所有的 Azure 存储库模型都与基础设施紧密集成。 让我们来看看以下的 g 模型:
- Cosmos DB:这为 Azure 函数提供了一个可用绑定。 这是一个具有各种操作的连接器,用于执行主流 CRUD 操作,以及用于创建、检索和执行存储过程的高级场景。 Cosmos DB 也可以用来触发 Azure 函数。
- SQL ServerSQL Server:这是另一个存储库服务,可以通过可用的连接器集成到逻辑应用中,从而允许创建或修改条目等触发器。 逻辑应用也可以使用 SQL 和Azure SQL 数据仓库连接器在 SQL 实例上执行原始和结构化查询。 此外,SQL 连接可以在 Azure 函数中初始化,只需要使用作为。net Core 一部分可用的本机 SQL 客户端即可。
- Azure Table Storage:这是另一个存储库模型,可以与 Azure 无服务器组件集成。 表存储表可以用作输入参数,也可以作为 Azure 功能基础设施中输出配置的一部分,接收新实体。 逻辑应用的连接器可以用来执行表存储上的各种操作:

图 10.21 - Azure 表存储操作
- Azure Blob Storage:这可以作为函数和逻辑应用的触发器。 可用的函数绑定和应用连接器提供了各种可以在无服务器应用模型中使用的任务和绑定元素。
队列处理
为了实现上述基于队列的负载均衡模式,Azure 分布式系统可以利用 Azure 队列和Azure 服务总线。 通过这种方式,可以实现各种异步处理模式。
Azure 队列可以配置来触发功能和逻辑应用。 绑定和连接器都有可用的操作,以便它们可以侦听特定的队列。 连接器包含用于执行基本操作的操作,包括但不限于创建消息队列、插入消息和检索消息。 Azure 消息队列还可以用作 Azure 函数的输出目标,以在配置的队列中创建消息。
Azure 服务总线的连接器和绑定具有一组扩展的可用操作和各种触发器。 作为触发器设置的一部分,队列和主题都可用于侦听新消息。 逻辑应用还可以执行几乎所有可能的操作,托管客户机可以通过与管理基本消息、死信队列、锁、主题和订阅相关的操作实现这些操作。
事件聚合
另一个 Azure 无服务器生态系统的成员,事件网格是在分布式服务组件之间实现经典的发布者/订户模型的最合适的候选人,特别是在涉及 Azure 功能和逻辑应用时。 在事件网格之外的中,对于涉及事件流而不是离散事件分发的大数据管道,事件集线器是最佳选择。
Event Grid 聚合从各种所谓的事件源收集的事件,例如容器注册中心、资源组、服务总线和存储。 事件网格还可以使用和交付由功能强大的组件发布的自定义主题。 然后将聚合的事件分散到注册的使用者或所谓的事件处理程序。 事件网格的事件处理程序包括但不限于以下内容:
- Azure 自动化
- Azure 的功能
- 活动中心
- 混合连接
- 逻辑应用
- 微软流
- 队列存储
- Webhooks
这种基础设施意味着开发人员不受功能和逻辑应用的可用触发器的限制,因为它们是某个关键任务场景的初始点。 他们还可以创建一个完整的事件驱动订阅模型。
事件 hub可以集成为事件网格事件的消费者,并用作 Azure 功能的触发器和输出。 一个连接器可用于具有触发器和动作的逻辑应用。 事件中心与 Azure 函数一起使用时,可以为处理大数据流创建一个非常敏捷的伸缩模型。
我们现在有了一个关于 Azure 无服务器组件的集成选项的完整的图片,以及在哪些场景中可以利用它们。
总结
总之,作为 Azure 无服务器平台的一部分,Azure 功能和逻辑应用提供了专门的事件驱动解决方案,以填补任何分布式云应用的空白。 在本章中,我们分析了 Azure 函数可用的开发选项。 我们已经实现了简单的 Azure 函数来在 Cosmos DB 设置上反规范化数据。 最后,我们通过使用开箱即用的连接器任务,以及使用 HTTP 和周期性触发器的 Azure 函数来创建逻辑应用。
本章结束了我们对 Azure 云服务相关主题的讨论。 在接下来的章节中,我们将研究更高级的主题,以改进 Xamarin 应用和基于云的服务后端之间的集成。
十一、流体应用与异步模式
一个有吸引力的移动应用的关键属性之一是它的响应能力。 用户更希望应用不干扰用户的交互,而是能够以流畅的方式呈现和执行用户手势。 为了实现快速和流畅的应用规范,以及性能,异步执行模式可以发挥作用。 在开发 Xamarin 应用时,以及 ASP.NET Core 中,任务的框架和响应式模块都可以帮助分配执行线程,并创建一个平滑且不间断的执行流。
在本章中,我们将学习任务框架的真正组成部分以及与之相关的基本概念。 我们还将介绍与异步执行模型(包括可等待和可观察模型)相关的一些最重要的模式,然后将它们应用到应用的各个部分。 最后,我们还将了解本机异步执行模型。
下面几节将带你了解一些异步执行的关键实现场景:
- 利用任务和待办事项
- 异步执行模式
- 本机异步执行
在本章结束时,您将能够将 TPL 和本地异步特性引入到您的移动和 web 应用中。 它们将用 Xamarin 和。net 5 编写,以帮助您创建响应更快、更敏捷的应用。
利用任务和待办事项
在本节中,我们将研究任务和异步执行模式的基础知识,并确定使它们成为任何现代应用的基本部分的主要因素。
用户体验(UX)是一个术语,用于描述 UI 组件的组成以及用户如何与它们交互。 换句话说,UX 不仅仅是应用是如何设计的,还包括用户对应用的印象。 在这种情况下,应用的响应性是定义应用质量的关键因素之一。
一般来说,一个简单的交互用例从用户交互开始。 这种交互可以是点击屏幕上的某个区域,在画布上的某个手势,或者在屏幕上可编辑字段中的实际用户输入。 一旦用户交互触发执行流,应用业务逻辑就负责更新 UI,以便通知用户输入的结果。
如您所见,在简单交互模型的异步版本中,应用开始执行指定的业务流,而不等待它完成。 同时,用户可以自由地与 UI 的其他部分进行交互。 一旦结果可用,就会通知应用的 UI 它完成了。
此交互模型定义并满足简单的执行场景,例如使用正则表达式验证电子邮件字段或显示一个展板来显示项目上所需的详细信息。 然而,随着交互模型和业务逻辑变得越来越复杂,并且出现了额外的依赖关系(如 web 服务),我们应该对用户评估应用正在进行的工作(例如,下载远程资源时的进度条)。 为此,我们可以扩展我们的交互模型,让它为用户提供持续的更新:

图 11.1 -线程池概念
现在,UI 不断地从后台进程接收更新。 这些更新可以像加载器环的忙音信号一样简单,也可以像复杂的完成率组件的数据更新一样简单。 然而,这种模式提出了另一个问题,即应用 UI 如何处理来自后台处理的多个更新。 在回答这个问题之前,让我们仔细看看应用的 UI 基础结构和基于任务的执行。
任务执行
一个应用 UI,不管它是在哪个平台上实现的,总是遵循单线程模型。 即使底层平台或硬件支持多线程,运行时也要负责提供一个分派器来呈现 UI。 这有助于我们避免多个线程试图在同一时间更新屏幕的同一部分。
在这个单线程模型中,应用负责将后台处理留给子线程,并同步回 UI 线程。
。net 框架引入了基于任务的线程模型,也称为任务异步编程(点击)模型,在。net 4.0,自那以后,已成为异步执行规范 Xamarin 等,特别是在移动平台上。
简单地说,TAP 提供了对经典线程模型的抽象。 在这种方法中,开发人员和应用(隐式地)不直接负责处理线程的创建、执行和同步,而只是简单地创建异步工作块(即任务),从而允许底层运行时处理所有繁重的工作。 特别是考虑到。net Standard 是各种运行时(如。net Core 和 Mono)的完整抽象,这个抽象允许每个平台实现最适合平台的方式来处理多线程。 这就是为什么在跨平台模块中不能使用Thread类的主要原因之一。 取而代之的是特定于平台的框架模块(例如,Xamarin)。 iOS 和 Xamarin.Android)提供了对经典线程模型的访问。
可以使用Task类中提供的静态帮助器方法创建一个简单的异步块:
Task.Run(() =>
{
// Run code here
})
在这个例子中,我们在任务中包装了一个同步代码块。 现在,声明方法可以返回创建的任务块。 或者,如果有其他异步块,它应该使用await关键字来执行该块,从而创建一个async方法:
public Task SimpleyAsyncChain()
{
return Task.Run(...);
}
public async Task MyAsyncMethod()
{
var result = await Task.Run(...);
await OtherAsyncMethod(result);
// example async method
await Task.Delay(300);
}
本例中的两个实现都创建了一个可以在顶层等待的异步方法链。 异常处理也可以使用简单的try/catch块引入,这与使用同步代码没有什么不同:
public async Task<MyEntity> MyAsyncMethodWithExceptionHandling()
{
MyEntity result = null;
try
{
result = await Task.Run(...);
}
catch(Exception ex)
{
// TODO: Log the exception
}
return result;
}
虽然任务可以顺序执行,这是在MyAsyncMethod方法中完成的,但如果异步块之间没有依赖关系,它们也可以并行执行,允许运行时尽可能多地利用多线程:
public async Task MyParallelAsyncMethod()
{
var result = await Task.Run(...);
await Task.WhenAll(OtherAsyncMethod(result), Task.Delay(300));
}
以 TAP 模型提供的为基础,让我们来看看以下用户故事:
“作为一个注册用户,我希望有一个专属于我的个人资料的视图,这样我就可以在应用中查看和验证我的公共信息。”
也许基于任务的方法最突出的应用是在应用需要与远程后端(例如,基于 rest 的 web 服务)交互时。 然而,任务是深度集成的,并且.NET Framework 实际上是处理多线程的方法。 例如,服务代理客户机的起点将是创建一个简单的 REST 客户机,该客户机将针对目标 API 端点执行各种 HTTP 方法。
在实现我们的 rest 客户端之前,我们需要定义客户端接口:
public interface IRestClient
{
Task<TResult> GetAsync<TResult>(string resourceEndpoint, string id)
where TResult : class;
Task<TEntity> PostAsync<TEntity>(string resourceEndpoint, TEntity
entity) where TEntity : class;
Task<TEntity> PutAsync<TEntity>(string resourceEndpoint, string id,
TEntity entity) where TEntity : class;
Task<TResult> DeleteAsync<TResult>(string resourceEndpoint, string
id) where TResult : class; }
我们可以使用更专门的方法来扩展这个接口,例如GetListAsync方法,它可以帮助序列化一个项目列表:
Task<IEnumerable<TResult>> GetListAsync<TResult>(string resourceEndpoint) where TResult : class;
现在,这些方法的实现可以使用简单的HttpClient方法来执行远程调用和某种类型的序列化/反序列化:
public async Task<TResult> GetAsync<TResult>(string resourceEndpoint, string id)
where TResult : class
{
var request = new HttpRequestMessage(HttpMethod.Get, $"
{resourceEndpoint}/{id}");
var response = await _client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<TResult>(content);
}
// TODO: Throw Exception?
return null;
}
在这里,客户端成员字段是在RestClient的构造函数中初始化的,可能有一个基 URL 声明,以及额外的 HTTP 处理程序:
public RestClient(string baseUrl)
{
// TODO: Insert the authorization handler?
_client = new HttpClient();
_client.BaseAddress = new Uri(baseUrl);
}
使用RestClient,我们可以创建另一个抽象级别来实现特定于 api 的方法调用,将数据转换对象转换为域实体:
public async Task<User> GetUser(string userId)
{
User result = null;
// Should we initialize the client here? Is UserApi client going to
//be singleton?
var client = new RestClient(_configuration["serviceUrl"]);
try
{
var dtoUser = await client.GetAsync<User>
(_configuration["usersApi"], userId);
result = User.FromDto(dtoUser);
}
catch (Exception ex)
{
// TODO:
}
return result;
}
这里,我们有一个异步方法链,它最终执行一个远程调用。 在此之上,我们现在必须将用户 API 检索调用连接到我们的视图模型,该模型应该立即加载相关的用户数据,以便它可以显示在目标视图上。 在这个用例中,触发业务流的用户交互可能是用户点击用户概要链接。 应用通过导航到目标视图进行响应,目标视图初始化视图模型。 视图模型依次为用户配置文件请求远程数据:
public async Task RetrieveUserProfile()
{
if (string.IsNullOrEmpty(NavigationParameter))
{
// TODO: Error/Exception
}
var userId = NavigationParameter;
var userResult = await _usersApi.GetUser(userId);
CurrentUser = userResult;
}
设置了CurrentUser属性后,视图将被通知并更新,以显示检索到的信息。
这个实现将在一个简单的异步链中工作,因为语言提供的async/await构造在编译过程中被转换为状态机。 这确保异步线程返回 UI 线程,以便视图模型的更新可以传播回 UI。
如果我们想确保用户数据分配是在 UI 线程上执行的,我们可以使用InvokeOnMainThread方法,指示运行时在主 UI 线程上执行异步代码块:
Device.BeginInvokeOnMainThread (() => {
CurrentUser = userResult;
});
当我们处理多个同步上下文时,主线程的调用就成为异步链的重要组成部分。 但是什么是同步上下文,我们如何在多线程移动应用中管理它? 下一节将给出答案!
同步上下文
当使用 TAP 处理异步方法调用时,重要的是要理解async和await是 c#提供的语言结构,而实际的多线程执行是注入的编译器生成的代码,用于替换异步/await 块。 如果编译器生成的异步状态机是密切观察和分析async方法构建器,您会注意到,在任何异步等待电话,当前同步上下文捕获,后来,当异步操作完成后,再次使用执行继续行动。
在 Xamarin 的应用中,的同步上下文——类似于执行上下文——是指当前的线程异步阻止被称为,以及目标线程当前异步块应该屈服。 如果一个 ASP.NET Core 应用在放大镜下,同步上下文将引用当前的HttpRequest对象。 在某些情况下,线程池可能在同步上下文中发挥作用。
正如我们前面提到的,由于在执行开始时捕获的上下文可能用于主 UI 线程,因此,实际上,将在 UI 线程中使用UserProfile的前一个异步示例。 一旦检索操作完成,将在主线程上执行延续操作(即,将结果分配给视图模型)。 然而,将异步方法交还给 UI 可能会导致性能损失,如果没有正确处理等待链,甚至会导致死锁。 在灾难性的场景中,UI 线程可能最终等待异步块,而异步块又等待 UI 线程返回。 为了避免这种情况发生,强烈建议使用显式控制捕获的上下文并使用ConfigureAwait方法生成目标上下文。 此外,尤其是在本机移动应用中,您应该使用ConfigureAwait(false)将 UI 线程从任何长时间运行的任务同步中释放出来(也就是说,不要屈服于捕获的上下文)。 这确保异步方法不会合并回 UI 线程,并且异步组合在单独的线程池中处理。 例如,让我们在前面的例子中添加一个额外的异步方法到async链:
var userResult = await _usersApi.GetUser(userId).ConfigureAwait(false);
var additionalUserData = _usersApi.GetUserDetails(userId).ConfigureAwait(false);
CurrentUser = userResult;
与前面的示例不同,此方法中的最后一条语句(continuation 操作)将在与 UI 线程不同的线程上执行。 第一个异步调用不会返回给 UI 线程,因为ConfigureAwait创建了一个辅助同步上下文。 然后,这个辅助上下文将被用作第二个异步调用的捕获上下文,它将生成第二个异步调用。 最后,分配结果的语句将在这个次要上下文中执行。 这意味着如果没有BeginInvokeOnMainThreadhelper 的执行,UI 很可能不会被传入的数据更新。 然而,当然,我们必须谨慎使用多个同步上下文和主线程。 在创建响应式应用时,我们不希望这些事件驱动的异步任务对视图和视图模型造成破坏。 控制它们的最简单方法是使用其他可用的控制机制,比如锁、信号量和互斥锁。
单次执行保证
异步任务实现的另一个流行的领域是通过整个移动应用的视图模型公开的命令。 如果要执行的业务流程作为响应用户输入(例如,提交按钮执行更新调用用户配置文件)取决于异步代码块,然后命令应该以这样一种方式实现,您可以调用异步功能正常。
让我们用现有的视图模型来演示一下。 首先,我们需要实现我们的内部执行方法:
public async Task ExecuteUpdateUserProfile()
{
try
{
await _usersApi.UpdateUser(CurrentUser);
}
catch (Exception ex)
{
// TODO:
}
}
现在,让我们声明我们的命令:
public ICommand UpdateUserCommand
{
get
{
if (_updateUserCommand == null)
{
_updateUserCommand = new Command(async () => await
ExecuteUpdateUserProfile());
}
return _updateUserCommand;
}
}
此时,如果命令绑定到用户控件(如按钮),那么多次点击该按钮将导致同一命令的多次执行。 虽然这可能不会在业务流上造成任何问题(也就是说,用户将被多次更新当前数据),但它可能会导致性能下降和服务端不必要的资源消耗。
锁和监视器,以及我们在经典线程中熟悉的互斥锁实现,也可以使用SemaphoreSlim在基于任务的异步代码块中实现。 SemaphoreSlim的主要用途可以概括为对一个或多个异步块进行节流。
对于这个场景,我们可以初始化一个只有一个可用槽位的信号量:
private static readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1);
在命令方法的执行块中,我们可以检查当前信号量是否有租约。 如果没有,我们在一个插槽上放置一个租约,并在命令执行完成后释放它:
public async Task ExecuteUpdateUserProfile()
{
if (Semaphore.CurrentCount == 0)
{
return;
}
await Semaphore.WaitAsync().ConfigureAwait(false);
try { ... } catch { ... }
Semaphore.Release();
}
这样,该命令不能同时执行多次,从而避免数据冲突。 这里需要注意的是,由于信号量计数是在命令执行后释放的,因此必须使用try/catch块来防止在错误发生后锁住信号量。
逻辑任务
在检索的例子中,我们在BeginInvokeOnMainThread块中执行视图模型数据分配块。 虽然这实际上保证了视图模型更改将被传播到 UI 线程,但使用这种类型的执行,我们不能真正地说一旦等待执行的异步方法完成,以及何时更新了视图模型。 此外,UI 执行块可以使用另一个异步代码块(例如,在检索数据时显示一个弹出窗口)。 在这种情况下,我们可以利用一个任务完成源,这样我们就可以更严格地控制异步代码块何时真正完成:
public async Task RetrieveUserProfile()
{
// Removed for brevity
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
var userResult = await _usersApi.GetUser(userId);
Device.BeginInvokeOnMainThread(async () => {
CurrentUser = userResult;
await ShowPopupAsync(); // async method
tcs.SetResult(0);
});
await tcs.Task.ConfigureAwait(false);
}
在本例中,我们使用了TaskCompletionSource,它表示异步状态机并接受结果或异常。 这个状态机只有在 UI 块的执行完成和RetrieveUserProfile方法最终完成时才会得到结果。
TaskCompletionSource在描述异步块方面的本地 UI 流时也很有用。 例如,用户从可用内容提供者中选择媒体文件的完整 UI 流程可以描述为异步块。 在这种情况下,完成源将在用户打开文件选择器对话框时初始化,并且在用户从所选内容源中选择某个文件时设置结果。 如果用户点击某个对话框上的取消按钮,该实现可以扩展为抛出异常。 这样,由多个屏幕和交互组成的用户流可以抽象为异步方法,这意味着它们可以被应用的视图或视图模型轻松使用。
命令模式
命令模式是无功移动应用的流量模式的衍生。 在 Android 世界,这种模式下以类似的方式实现这个名字模型视图的目的(本研究),其唯一目的是创建一个单向的数据流和减少【显示】的复杂性源于的双重性质Model-View-ViewMode(MVVM【病人】)。
在此模式中,每个视图都配备了多个命令,这些命令是自包含的执行块,引用了底层应用基础设施(类似于工作单元)。 在这种情况下,用户交互被路由到相应的命令,命令的结果通过广播传播到相关的控件(例如,使用BroadcastReceiver实现)。
. net 标准中的任务基础设施及其实现的运行时,例如. net Core,允许开发人员实现可等待上下文元素,这些元素可以很容易地表示命令,并可以使用Task语法等待。
要使类实例具有可等待性,它应该实现GetAwaiter方法,而该方法又由. net 任务基础结构使用。 在命令模式实现中,我们可以先创建一个用于依赖注入的基抽象类,并实现awaitable方法:
public abstract class BaseCommand
{
protected BaseCommand(IConfiguration configurationInstance, IUserApi userApi)
{
ConfigurationService = configurationInstance;
UserApi = userApi;
}
protected IConfiguration ConfigurationService { get; private set; }
protected IUserApi UserApi { get; private set;}
public virtual TaskAwaiter GetAwaiter()
{
return InternalExecute().GetAwaiter();
}
protected virtual async Task InternalExecute()
{
}
}
我们还可以扩展命令实现来实际返回一个结果:
public abstract class BaseCommand<TResult> : BaseCommand
{
protected TResult Result { get; set; }
public new TaskAwaiter<TResult> GetAwaiter()
{
return ProcessCommand().GetAwaiter();
}
protected override async Task InternalExecute()
{
Result = await ProcessCommand().ConfigureAwait(true);
await base.InternalExecute().ConfigureAwait(true);
}
protected virtual async Task<TResult> ProcessCommand()
{
// To be implemented by the deriving classes
return default(TResult);
}
}
现在,一个实际命令的实现—例如,更新用户配置文件—看起来类似如下:
public class UpdateUserCommand : BaseServiceCommand<User>
{
User _userDetails;
public UpdateProfileCommand(IConfiguration configuration, IUsersApi
usersApi, User user):
base(configuration, usersApi)
{
_userDetails = user;
}
protected async override Task<string> ProcessCommand()
{
try
{
Result = await _usersApi.UpdateUser(CurrentUser);
return Result;
}
catch (Exception ex)
{
// TODO:
}
}
}
最后,实现的命令可以初始化并像这样执行:
var result = await new UpdateUserCommand(configuration, usersApi, user);
这里,base 命令还可以利用服务定位器或某种类型的属性注入,这样就不需要将服务集与命令参数一起注入。 此外,可以利用消息传递服务广播它成功地为多个用户控件执行了命令。
创造生产者/消费者
线程安全集合是。net Core 中异步工具集的宝贵成员,就像它们在完整的。net 框架中一样。 通过使用阻塞集合,可以实现并发模型,为多线程上的异步任务提供公共基础。 毫无疑问,这些模型中最突出的是生产者/消费者模式实现。 在此范例中,在并行线程/任务上执行的方法将生成数据项,这些数据项将被另一个称为消费者的并行操作使用,直到达到一个边界限制或生产完成。 这两个方法将共享相同的阻塞集合,其中阻塞集合将充当两个异步块之间的代理。
让我们用一个小的实现来说明这个模式:
-
我们将从创建阻塞集合开始,该集合将用于存储
Auction项:BlockingCollection<Auction> auctions = new BlockingCollection(100); -
我们现在可以使用后台任务将
Auction项添加到阻塞集合中。 这里,GetNewAuction方法将检索/创建拍卖实例,并将它们向下推到管道中:Task.Run(() => { while(hasMoreAuctions) { auctions.Add(GetNewAuction); } auctions.CompleteAdding(); }); -
与生产者类似,我们可以启动一个单独的消费者线程,该线程将处理交付的项目:
Task.Run(() => { while (!auctions.IsCompleted) { Process(auctions.Take()); } } -
将这个实现更进一步,我们可以使用
GetConsumingEnumerable方法来创建一个阻塞枚举对象:Task.Run(() => { foreach(var auction in auctions.GetConsumingEnumerable()) { Process(auction); } } -
最后,通过使用
Parallel.ForEach,我们可以添加更多的消费者,而不需要通过非琐碎的同步实现:Parallel.Foreach(auctions.GetConsumingEnumerable(), Process);
现在,生产者生成的数据将被多个消费者消费,直到拍卖集合发送IsCompleted信号,这将导致消费的枚举对象中断并继续执行代码的其余部分。 在这种设置中,每个消费者将接收不同的数据项。 但是,如果有多个消费者期望同一组数据执行不同的操作,那会怎样呢? 这种类型的设置可以通过一个可观察/观察者实现来实现。
使用可见数据流
在。net 4 中引入了IObserver和IObservable接口,它们构成了可观察的和所谓的反应模式的基础。 IObservable最突出的实现是在 Rx Extensions NuGet 库中。 目前这是一个开源项目,由。net 基金会管理。
在我们进入反应性数据之前,让我们后退一步,尝试演示不同类型的数据流。 我们将从一些简单的同步代码开始这个演示,然后我们可以使用这些代码作为其他实现的基本需求。 在我们开始之前,您应该创建一个新的.NET 5 控制台项目,并修改Main方法,使其为async:
class Program
{
static async Task Main(string[] args)
{
}
}
现在已经创建了控制台应用项目,让我们添加一个名为GetNumbers的新方法:
public static IEnumerable<int> GetNumbers()
{
var count = 0;
while (count < 10)
{
yield return count++;
}
}
这个方法将创建一个同步数据管道,通过一个简单的for/each循环将其打印在屏幕上:
Console.WriteLine("Synchronous Data");
foreach (var item in GetNumbers())
{
Console.WriteLine($"Sync: {item}");
}
这里发生的情况是,每次for/each循环从可枚举对象请求一个新项时,GetNumbers方法生成一个数字,直到这些数字达到 10。
现在,假设我们要异步地检索并将这些数据推送到管道中。 我们不能使用Task<IEnumerable<int>>,因为这意味着我们需要等待完整的数据集完成加载,并且我们可以枚举它。 然而,我们可以使用异步任务的“流”:
public static IEnumerable<Task<int>> GetAsyncNumbers()
{
var count = 0;
while (count < 10)
{
yield return Task.Run(async () =>
{
await Task.Delay(200);
return count++;
});
}
}
这里,我们使用一个Task.Delay调用来模拟一个异步操作。 现在,让我们使用这个新的生成器来打印数据:
Console.WriteLine("Asynchronous Data");
foreach (var itemTask in GetAsyncNumbers())
{
await itemTask
.ContinueWith(_ => Console.WriteLine($"Async: {_.Result}"));
}
在这个阶段,这个实现的发展可以向多个不同的方向发展。 第一个方向是创建一个阻塞集合,并遍历可消费的 enumerable。 现在,让我们重写我们的数据源来生成一个阻塞集合:
private static BlockingCollection<int> StartGetNumbers()
{
var blockingCollection = new BlockingCollection<int>();
Task.Run(
async () =>
{
var count = 0;
while (count < 10)
{
await Task.Delay(200);
blockingCollection.Add(count++);
}
blockingCollection.CompleteAdding();
});
return blockingCollection;
}
按照前面的示例,消费者看起来类似于下面的:
Console.WriteLine("ConsumerProducer");
foreach (var item in StartGetNumbers().GetConsumingEnumerable())
{
Console.WriteLine($"Consumer: {item}");
}
我们可以采取的另一个方向是创建一个可枚举的async:
private static async IAsyncEnumerable<int> GetNumbersAsync()
{
var count = 0;
while (count < 10)
{
await Task.Delay(1000);
yield return count++;
}
}
现在,我们可以异步地消耗枚举对象:
Console.WriteLine("Async Stream");
await foreach (int number in GetNumbersAsync())
{
Console.WriteLine($"Async Stream: {number}");
}
到目前为止,我们已经异步地使用了带有阻塞集合和异步枚举的数据源。 现在,让我们看看使用可观察对象会是什么样子。
可观察对象可以使用 Rx 扩展创建,它可以在 NuGet 包中同名的System.Reactive命名空间下找到。 如果我们用一个可观察对象重写之前的实现,它看起来会是这样的:
private static IObservable<int> GetNumbersObservable()
{
return Observable.Create<int>(
async _ =>
{
var count = 0;
while (count < 20)
{
await Task.Delay(200);
_.OnNext(count++);
}
_.OnCompleted();
});
}
这个实现中的重要部分是OnNext和OnCompleted方法,它们控制数据流。 这个实现将产生一个冷的观察对象,当第一个观察对象连接到它时,它将开始产生数据。 如果它是一个热门的可观察对象,数据源将推送新项,而不管当前的观察者订阅计数。 关于热可观察对象的另一个特性是,它们将表现为多播生产者,而对于冷可观察对象,我们要么重申事件,要么有一组相互竞争的消费者,这取决于可观察函数的设置。
现在我们已经创建了我们的可观察对象,让我们为它创建一个订阅:
Console.Write("Observables");
var observable = GetNumbersObservable();
var subscriber = observable
.Subscribe(_ => Console.WriteLine($"Observer: {_}"));
在通知集合和使用System.Reactive.Linq实现时,可观察对象非常灵活。 这样,就可以引入过滤和数据转换来帮助您修改到达观察者的数据。 例如,使用前面的例子,我们可以引入一个只推送偶数的过滤器:
var evenSubscriber = observable
.Where(_ => _ % 2 == 0)
.Subscribe(_ => Console.WriteLine($"Even Observer: {_}"));
Rx 项目中还有来自其他异步模式的各种附加控制方法和转换策略。 在这里,我们只是演示了一个简单的数据管道示例并对其进行了过滤。 此外,还有另一个 Xamarin 的开源项目,它建立在 Rx Extensions 提供的基础上。
在本节中,我们简要概述了. net 上可用的异步功能,以及如何在应用 UI 和域实现中利用它们。 在下一节中,我们将研究移动应用中针对视图及其关联的视图模型或控制器的更专门的执行模式。
异步执行模式
任务通常用于为异步块创建一个简单的顺序执行。 然而,在某些情况下,等待任务完成可能是不必要或不可能的。 我们可以列举几个不可能或不需要等待任务的场景:
- 如果我们正在执行异步块(类似于 update user 命令),我们只需将该命令绑定到控件上,并以“扔了就忘了”的方式执行它。
- 如果我们的异步块需要在构造函数中执行,我们将没有简单的方法来等待任务。
- 如果异步代码需要作为事件处理程序的一部分执行。
关于常见的问题,这里可以列出多个例子,例如以下:
- 方法声明不应该显示
async和void返回类型。 - 方法不应该被强制与
Wait方法或Result属性同步执行。 - 依赖于异步块结果的方法; 应该避免竞争条件。
可以使用各种模式来规避这些不可期待的场景。 在下面几节中,我们将仔细研究如何初始化一个依赖于将要完成的异步流程的视图模型。 然后,我们将把我们的 TPL 知识应用于异步事件处理。 最后,我们将学习如何在命令中处理异步方法。
服务初始化模式
在前面描述的构造函数场景中,让我们假设视图模型的构造函数应该检索一定数量的数据。 这将被相同视图模型的方法或命令使用。 如果我们执行方法而不等待结果,就不能保证在执行命令时,async构造函数的执行已经完成。
让我们用一个抽象的例子来说明这一点:
public class MyViewModel
{
public MyViewModel()
{
// Can't await the method;
MyAsyncMethod();
}
private async Task MyAsyncMethod()
{
// Load data from service to the ViewModel
}
public async Task ExecuteMyCommand()
{
// Data from the MyAsyncMethod is required
}
}
当视图模型初始化后立即调用ExecuteMyCommand方法时,很有可能会出现竞争条件和可能的 bug,从而在一段时间内无法进行开发。
在所谓的服务初始化模式中,为了验证MyAsyncMethod是否成功执行,我们可以将生成的任务分配给一个字段,并使用该字段等待之前启动的任务:
public class MyViewModel
{
private Task _myAsyncMethodExecution = null;
public MyViewModel()
{
_myAsyncMethodExecution = MyAsyncMethod();
}
// ...
public async Task ExecuteMyCommand()
{
await _myAsyncMethodExecution;
// Data from the MyAsyncMethod is required
}
}
这样,就避免了异步竞争条件,并且命令的执行将需要确保任务引用已经完成。
异步事件处理
正如前面提到的,如果调用链要求异步执行,那么async链应该一直传播到调用层次结构的顶层。 偏离此设置可能会导致线程问题、竞态条件和可能的死锁。 然而,同样重要的是,方法不偏离asyncTask 声明,确保async堆栈和任何生成的结果和错误都被保留。
带有异步代码的事件处理程序就是一个很好的例子,在这种情况下,我们对方法的签名没有太多要说的。 例如,让我们看一下按钮点击处理程序,它应该执行一个awaitable方法:
public async void OnSubmitButtonTapped(object sender, EventArgs e)
{
var result = await ExecuteMyCommand();
// do additional work
}
一旦这个事件处理程序订阅了按钮所单击的事件,异步代码将被正确地执行,并且我们不会注意到它的任何问题。 然而,使用 void 作为返回类型的方法声明将绕过运行时的错误处理基础结构,在出现错误的情况下,无论异常源是什么,应用都将崩溃,而不会留下任何错误的痕迹。 我们还应该提到,与这种类型的声明相关的编译器警告将被添加到项目的技术债务中。
在这里,我们可以创建 TAP 到异步编程模型(APM)的转换,它可以将异步链转换为回调方法,并引入一个错误处理程序。 这样,就不需要用异步签名声明OnSubmitButtonTapped方法。 我们可以很容易地引入一个扩展方法,它将使用回调函数执行异步任务:
public static class TaskExtensions
{
public static async void WithCallback<TResult>(
this Task<TResult> asyncMethod,
Action<TResult> onResult = null,
Action<Exception> onError = null)
{
try
{
var result = await asyncMethod;
onResult?.Invoke(result);
}
catch (Exception ex)
{
onError?.Invoke(ex);
}
}
}
可以引入另一个扩展方法来转换任务而不返回任何数据:
public static async void WithCallback(
this Task asyncMethod,
Action onResult = null,
Action<Exception> onError = null)
{
try
{
await asyncMethod;
onResult?.Invoke();
}
catch (Exception ex)
{
onError?.Invoke(ex);
}
}
现在,我们的异步事件处理程序可以重写以利用扩展方法:
public void OnSubmitButtonTapped(object sender, EventArgs e)
{
ExecuteMyCommand()
.WithCallback((result) => {
//do additional work
});
}
这样,我们将优雅地中断异步链,而不会危及任务的基础结构。
异步命令
在异步 UI 实现中,几乎不可能避免处理异步任务的命令声明和绑定。 这里的一般方法是创建一个async委托,并将其作为操作传递给命令。 然而,这种基于承诺的执行削弱了我们查看异步块的完整生命周期的能力。 这使得为这些块创建单元测试变得更加困难,并且避免了终端事件(例如导航到不同的视图或关闭应用)中断执行的情况。
让我们看看我们之前实现的UpdateUserCommand:
_updateUserCommand = new Command(async () => await ExecuteUpdateUserProfile());
在这里,该命令只负责初始化用户配置文件更新。 但是,一旦执行了命令,就绝对不能保证完成了ExecuteUpdateUserProfile方法的全部执行。
为了弥补异步执行监视或缺乏异步执行监视,我们可以实现一个异步命令,该命令在命令本身中跟随任务的执行。 让我们从声明异步命令接口开始:
public interface IAsyncCommand : ICommand
{
Task ExecuteAsync(object parameter);
}
在这里,我们声明了主执行方法的异步版本,它将被实际的命令方法使用。 让我们实现AsyncCommand类:
public class AsyncCommand : IAsyncCommand
{
// ...
public AsyncCommand(
Func<object, Task> execute,
Func<object, bool> canExecute = null,
Action<Exception> onError = null)
{
// ...
}
// ...
}
该命令将是接收异步任务和错误回调函数。 然后async将使用异步委托,如下所示:
public async Task ExecuteAsync(object parameter)
{
if (CanExecute(parameter))
{
try
{
await _semaphore.WaitAsync();
RaiseCanExecuteChanged();
await _execute(parameter);
}
finally
{
_semaphore.Release();
}
}
RaiseCanExecuteChanged();
}
注意,我们现在已经成功地集成了以前在异步命令块中实现的一次性执行修复。 每次租用信号量时,我们将引发一个事件,从而将CanExecute更改事件传播到绑定的用户控件。
最后,实际的ICommand接口将通过使用回调转换的扩展方法来使用ExecuteAsync方法:
void ICommand.Execute(object parameter)
{
ExecuteAsync(parameter).WithCallback(null, _onError);
}
现在,应用单元测试可以直接使用ExecuteAsync方法,而绑定仍然使用Execute方法。 我们甚至可以通过公开 task 类型的属性进一步扩展这个实现,就像我们在服务初始化模式中所做的那样,从而允许连续的方法检查方法完成情况。
本节主要讨论异步执行模式,这些模式不仅可以帮助我们实现 Xamarin 应用,还可以帮助我们实现 ASP。 净的 web 服务。 当然,这些模式利用。net Core 或 mono 运行时,当移动应用实际正在使用时也适用。 如果我们有一个场景需要后端进程在应用的后台存在,我们可能需要求助于使用目标平台的本地特性。
本机异步执行
除了。net Core 提供的异步基础设施外,Xamarin 目标平台还提供了一些后台执行过程,这些过程可以帮助那些正在实现模块的开发人员,这些模块可以在应用没有实际工作时工作。 反过来,各种业务流程与主应用 UI 分开执行,创建轻量级和响应性强的 UX。
Android 服务
在 Android 平台上,后台进程可以作为服务实现。 服务是执行模块,可以按需启动或按计划启动。 例如,一个已启动的服务可以带有一个意图来启动。 这将一直运行,直到请求终止(或自行终止)。 这里,重要的是要注意,一旦意图实现,启动服务的流程和服务本身之间没有直接通信。
为了实现一个简单的启动服务,你需要实现Service类,并装饰启动服务的ServiceAttribute属性,以便它可以包含在应用清单中:
[Service]
public class MyStartedService : Service
{
public override IBinder OnBind(Intent intent)
{
return null;
}
public override StartCommandResult OnStartCommand(
Intent intent, StartCommandFlags flags, int startId)
{
// DO Work, can reference common core modules
return StartCommandResult.NotSticky;
}
}
一旦创建了服务,您可以使用Intent启动服务,如下所示:
var intent = new Intent (this, typeof(MyStartedService));
StartService(intent);
您也可以使用AlarmManager定期发起服务。
服务实现的另一个选项是使用绑定服务。 与已启动的服务不同,它们通过使用活页夹保持开放的通信渠道。 绑定服务方法可以由初始化流程(如活动)调用。
iOS 背景
iOS 平台也提供了从远程服务器获取额外数据的后台机制,即使应用甚至设备处于非活动状态。 虽然它不像 Android 上的警报管理器那样可靠,但这些后台任务都经过了高度优化,以保存电池。 这些后台任务可以作为对某些系统事件的响应执行,比如地理位置更新或以特定的名义间隔执行。 我们在这里使用了名义上的这个词,因为后台任务执行的时间段是不确定的,并且可能根据后台任务的执行性能以及可用的系统资源随时间变化。
例如,为了执行后台获取,你需要在后台模式中启用后台获取:

图 11.2 - iOS 后台模式
一旦Background``fetch被启用,我们就可以引入我们的取回机制,该机制将被定期执行。 这种获取机制通常会进行远程服务调用来更新要显示的数据,因此一旦应用进入前台,它就不需要重复这些刷新数据调用。 执行获取可以在AppDelegate中建立:
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
global::Xamarin.Forms.Forms.Init();
LoadApplication(new App());
UIApplication.SharedApplication
.SetMinimumBackgroundFetchInterval(UIApplication.BackgroundFetchIntervalMinimum);
return base.FinishedLaunching(app, options);
}
现在,iOS 运行时将定期调用PerformFetch方法,所以我们可以在这里注入我们的检索代码:
public override void PerformFetch(
UIApplication application, Action<UIBackgroundFetchResult>
completionHandler)
{
// TODO: Perform fetch
// Return the correct status according to the fetch execution
completionHandler(UIBackgroundFetchResult.NewData);
}
返回的结果状态很重要,因为运行时利用该结果来优化获取间隔。 结果状态可以是以下三种状态之一:
UIBackgroundFetchResult.NewData:当新内容被获取,应用被更新时调用。UIBackgroundFetchResult.NoData:当获取新内容完成,但没有可用内容时调用。UIBackgroundFetchResult.Failed:用于错误处理; 当获取无法完成时调用。
除了后台获取,NSUrlSession与后台传输基础设施结合,可以提供后台检索机制,这些机制可以合并到后台获取操作中。 通过这种方式,应用内容可以保持最新,即使它处于活动状态。
正如我们在这里所演示的,Android 和 iOS 平台都提供了自己的异步流机制,并且这些特性都可以在 Xamarin 平台上使用。 根据用例的不同,开发人员可以自由选择是使用特定的 TPL 模式实现还是本地子过程来处理异步执行。
总结
简而言之,移动应用不应该被设计为在用户交互层上执行长时间运行的任务,而应该使用异步机制来执行这些工作流。 在本例中,UI 只负责通知用户后台执行状态。 在过去,后台任务是通过经典的. net 线程模型来处理的,而现在,TAP 模型提供了一组丰富的功能,它将开发人员从创建、管理和同步线程和线程池的负担中解放出来。 在本章中,我们已经看到了可以帮助我们创建后台任务的各种模式。 然后这些结果将返回给 UI 线程,以便异步流程结果可以传播到 UI。 我们还讨论了同步机制和任务的不同策略,从而避免了死锁和竞争条件。 此外,我们还研究了 iOS 和 Android 的原生后台程序。
总的来说,异步任务和后台技术主要用于一个共同目标:在应用域中保持数据的最新。 在下一章中,我们将仔细研究有效管理应用数据的不同技术。
十二、应用数据管理
大多数移动应用都与某些数据集耦合,这些数据集要么通过服务后端下载,要么绑定到应用中。 这些数据集可以是不同的,从简单的静态文本内容到与特定上下文相关的事务集的实时更新。 在这种情况下,开发人员的任务是在远程交互和数据缓存之间创建最佳平衡。 此外,离线支持正在成为移动应用开发领域的规范。 为了避免数据冲突和同步问题,开发人员必须认真考虑根据手头数据类型实现的过程。 在本章中,我们将讨论可能使用 SQLite 和 Akavache 的数据同步和离线存储场景,以及新的。NET Core 模块,如实体框架核心。
下面几节将让你深入了解如何管理应用数据:
- 使用瞬时缓存提高 HTTP 性能
- 使用 SQLite 的持久数据缓存
- 数据访问模式
在本章结束时,您将了解如何在本地缓存和存储机制上投入少量精力来改善用户体验。 您将能够使用不同的模式和平台在多个平台上实现本地存储。 您还将熟悉各种数据访问模式及其在 Xamarin 平台上的应用。
使用瞬态缓存提高 HTTP 性能
在本节中,我们将看一看客户端缓存的基本原理,并了解如何改进客户端应用和服务器基础设施上的 web api 之间的通信线。 我们将扩展使用客户端缓存和 ETags 的瞬时缓存的主题,以及使用键/值存储的特定于请求的缓存。
在第 11 章、带异步模式的流体应用中,客户端应用与服务基础设施保持直接异步服务通信线路。 这样,移动应用将加载在每个视图模型创建中显示特定视图所需的新数据。 虽然这为应用提供了最新的上下文,但对于用户来说,这可能不是最理想的体验,因为当我们处理移动应用时,我们需要考虑带宽和网络速度问题。
在开发移动应用时,一个常见的错误是假定在模拟器上运行的应用一旦部署到物理设备上,其行为将是相同的。 换句话说,假设在开发机器上使用的高速互联网连接与在移动设备上可能使用的 3G 网络连接是一样的,这是相当天真的。
幸运的是,开发者可以在 iOS 和 Android 设备模拟器/模拟器上模拟各种网络场景。 在 Android 上,仿真器为网络类型提供了一个有价值的仿真选项。 网络类型选择允许您选择不同的网络类型,从全球移动通讯系统(GSM)【显示】长期演进(LTE),作为【病人】以及信号强度(可怜,温和,善良,伟大)。 在 iOS 上,模拟各种网络连接的最简单的方法是安装网络链接调节工具,它可以在附加工具 for Xcode开发者下载包中找到。 安装包后,可以调整主机的网络连接,也可以隐式调整 iOS 模拟设备的连接。****
**现在,我们可以模拟网络连接问题了,让我们看看如何提高应用的响应能力,即使是在低于标准的网络条件下。
客户端缓存
在,创建微服务 Azure App Services中,我们利用服务器端缓存,通过使用 Redis 缓存实现一个简单的缓存旁模式。 但是,这个缓存只能帮助我们提高服务基础设施的性能。 为了能够使用缓存的数据创建和显示视图模型数据,我们需要在移动应用端实现缓存存储。
*实现此模式的最简单方法是为从服务器检索的不同实体创建简单的缓存存储。 例如,如果我们要检索某个拍卖的详细信息,我们可以首先检查缓存存储中是否存在数据。 如果数据项不存在,我们可以从远程服务器检索它,并用实体更新我们的本地存储。
为了演示这个场景,让我们从开始,创建一个简单的缓存存储接口,只展示设置和检索某个实体类型的方法:
public interface ICacheStore<TEntity>
{
Task<TEntity> GetAsync(string id);
Task SetAsync(TEntity entity);
}
这个实现假设将要处理的实体都具有字符串类型的标识符。
接下来,我们将实现 Auctions API 的构造函数:
public class AuctionsApi : IAuctionsApi
{
private readonly IConfiguration _configuration;
private readonly ICacheStore<Auction> _cacheStore;
public AuctionsApi(IConfiguration configurationInstance, ICacheStore<Auction> cacheStore)
{
_configuration = configurationInstance;
_cacheStore = cacheStore;
}
}
需要注意的是,我们有意省略了缓存存储的实现。 在这种情况下,一个简单的内存缓存或一个复杂的本地存储缓存都可以满足接口需求。
现在我们已经创建了的IAuctionsApi实现,我们可以继续使用缓存存储作为第一个地址来实现get方法来检查目标拍卖:
public async Task<Auction> GetAuction(string auctionId)
{
// Try retrieve the auction from cache store
Auction result = await _cacheStore.GetAsync(auctionId);
// If the auction exists in our cache store we short-circuit
if (result != null) { return result; }
// ...
}
我们有从缓存存储返回的数据实体。 现在,我们将实现远程检索过程,以防数据项在本地存储中不存在:
var client = new RestClient(_configuration["serviceUrl"]);
try
{
result = await client.GetAsync<Auction>(_configuration["auctionsApi"], auctionId);
await _cacheStore.SetAsync(result);
}
catch (Exception ex)
{
// TODO:
}
return result;
这样就完成了一个简单的缓存模式实现。 然而,我们的工作并没有完成缓存存储,因为这个实现假设,一旦实体缓存在本地存储中,我们就不需要从远程服务器检索相同的实体。
实体标签(ETag)验证
当然,在大多数情况下,我们之前关于静态数据的假设会失败,特别是在处理像Auction这样的实体时,其中的数据熵相对较高。 创建视图模型时,我们需要确保应用呈现实体的最新版本。 这不仅可以避免在我们的视图中显示不正确的数据,还可以避免冲突错误(即引用 Cosmos DB 上的时间戳完整性检查)。
为了实现这一点,我们需要一个验证过程来验证手头的实体是最新的,而不需要检索更新的版本。 实体标签(ETag)是 HTTP 协议定义的一部分。 它被用作可用的 web 缓存验证机制的一部分。 使用 ETag,客户端应用可以发出条件请求,如果要检索的实体有一个最新版本,则返回完整的数据集。 否则,web 服务器应该响应 304(未修改)状态码。 客户端执行条件请求的一种方式是使用If-None-Match头,并附带现有的 ETag 值。
现在,让我们后退一步,看看我们在 RESTful facade 中实现的拍卖控制器:
public async Task<IActionResult> Get(string key)
{
var cosmosCollection = new CosmosCollection<Auction>("AuctionsCollection");
var resultantSet = await cosmosCollection.GetItemsAsync(item => item.Id == key);
var auction = resultantSet.FirstOrDefault();
if(auction == null)
{
return NotFound();
}
return Ok(auction);
}
因为我们使用了Timestamp字段从宇宙检索数据库来验证是否我们正在努力推动文档的实体集合是最新版本,公平地使用相同的字段来识别一个特定的实体的当前版本。 换句话说,除了特定实体的 ID 外,时间戳字段还将定义实体的特定版本。 让我们利用If-None-Match头来检查给定实体在被客户端应用加载后是否发生了任何变化。 首先,我们将检查客户端是否发送了条件头:
// Get the version stamp of the entity
var entityTag = string.Empty;
if (Request.Headers.ContainsKey("If-None-Match"))
{
entityTag = Request.Headers["If-None-Match"].First();
}
不管实体标记值是多少,我们将从文档集合中检索实体的最新版本。 然而,一旦实体被检索,我们将比较 ETag 头的值和从 Cosmos DB 集合中检索到的时间戳:
if (int.TryParse(entityTag, out int timeStamp) && auction.TimeStamp == timeStamp)
{
// There were no changes with the entity
return StatusCode((int)HttpStatusCode.NotModified);
}
这样就完成了服务器端设置。 现在,我们将修改我们的客户端应用,让它发送条件检索头:
// Try retrieve the auction from cache store
Auction result = await _cacheStore.GetAsync(auctionId);
Dictionary<string, string> headers = null;
// If the auction exists we will add the If-None-Match header
if (result != null)
{
headers = new Dictionary<string, string>();
headers.Add(HttpRequestHeader.IfNoneMatch.ToString(), result.Timestamp.ToString());
}
var client = new RestClient(_configuration["serviceUrl"], headers);
此时,每当我们检索拍卖实体时,我们将尝试从本地缓存加载它。 但是,我们没有将缩短方法调用,而是将条件检索头添加到RestClient中。 当然,我们可以进一步重构,以创建一个HttpHandler,我们可以将其作为行为传递给HttpClient,或者甚至在RestClient上引入一个行为,以一种通用的方式处理缓存。
此外,我们还需要修改RestClient,以便它能够处理将由服务器返回的NotModified响应。 使用标准 web 缓存控制机制的开源缓存策略实现可能是这些修改的另一个解决方案。
键值存储
在瞬时缓存上下文中,即使是一个简单的键值存储也可以提高 HTTP 性能。 Akavache 是一个异步的持久的键/值存储,是开源场景中另一个可用的缓存解决方案。 从技术上讲,Akavache 是用于各种缓存存储的。net 标准实现。 它是在。net 标准框架上实现的,可以在 Xamarin 和。net Core 应用上使用。
在 Akavache 中,缓存存储作为 blob 存储实现在各种介质上,比如内存中、本地机器或用户帐户。 虽然这些商店不涉及跨平台的特定系统位置,但每个商店都有各自的翻译,取决于目标平台。 例如,用户帐户和安全存储是指共同的偏好在 iOS,他们将 iTunes 云备份,而在 UWP 平台上,这两个都是指用户设置和/或漫游用户数据和他们将存储在云与微软用户的帐户相关联。 因此,每个平台都对这个本地 blob 存储施加自己的限制。
使用为 blob 缓存存储抽象提供的扩展方法也可以很容易地使用这些存储。 扩展方法适用于使用缓存一旁模式检索数据,概述如下:
// Immediately return a cached version of an object if available, but *always*
// also execute fetchFunc to retrieve the latest version of an object.
IObservable<T> GetAndFetchLatest<T>(this IBlobCache This,
string key,
Func<IObservable<T>> fetchFunc,
Func<DateTimeOffset, bool> fetchPredicate = null,
DateTimeOffset? absoluteExpiration = null,
bool shouldInvalidateOnError = false,
Func<T, bool> cacheValidationPredicate = null)
如您所见,使用fetchFunc检索数据并将其放入当前的IBlobCache对象中。 通常,用于本地缓存的键是资源 URL 本身。 此外,可以包含一个缓存验证谓词来验证检索到的缓存数据是否仍然有效(例如,没有过期)。
还需要注意的是,Akavache 大量使用了响应式扩展,并且返回类型通常是可观察到的,而不是简单的任务。 因此,返回的数据应该主要通过事件订阅来处理。 根据缓存数据的状态,完成可能会触发多次(即一次用于数据的缓存版本,一次用于远程检索)。
瞬态缓存在低带宽连接场景中可以起到挽救作用。 到目前为止,我们一直专注于数据缓存的这一方面。 我们研究了使用本地缓存存储数据元素、控制缓存过期的实体标记以及用于创建通用实体存储的键值存储的示例。 然而,这些解决方案都不能提供全面的离线功能。 对于一个功能齐全的离线应用,你可能需要将数据存储在一个关系模型中,特别是当你检索的数据没有太多熵的时候。 在这些类型的情况下,您需要的不仅仅是键/值存储。
持久关系数据缓存
在前一节的示例中,我们没有为本地数据使用关系数据存储。 在大多数情况下,尤其是,如果我们处理的是 NoSQL 数据库,关系数据范式就失去了它的吸引力,为了提高性能,数据反规范化取代了对数据一致性的关注。 然而,在某些场景中,为了找到两者之间的最佳折衷,我们可能需要求助于关系数据映射。 在本节中,我们将了解 SQLite,以及如何使用它在移动应用中创建本地可用的关系数据存储。 我们将使用 SQLite 实现数据缓存.NET 以及实体框架核心。
在关系数据管理领域,移动应用最流行的数据管理系统无疑是 SQLite。 SQLite 的核心是一个包含在 C 编程库中的关系数据库管理系统。 SQLite 与其他关系数据管理系统的区别在于,SQLite 引擎不使用或不需要使用应用与之通信的独立进程。 在任何应用场景中,SQLite 数据存储和引擎都是应用本身不可分割的部分。 简单地说,SQLite 不是客户机-服务器引擎,而是嵌入式数据存储。
SQLite 有各种不同的实现,它们跨越了广泛的平台,比如本地移动平台、web 和桌面应用,以及嵌入式系统。 尽管某些浏览器仍然不支持,也可能永远不会支持 SQLite,但它仍然是 Xamarin 应用和目标移动平台的本地商店。
SQLite。 净
SQLite。 网是最早实现的 SQLite便携类库(PCL)平台,而且它仍然仍然是最受欢迎的跨平台实现(现在针对。NET 标准)。
**SQLite 的实现模式.NET 是基于定义实体索引和其他数据列的域模型实体属性:
public class Vehicle
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
[Indexed]
public string RemoteId { get; set; }
public string Color { get; set; }
public string Model { get; set; }
public int Year { get; set; }
public string AuctionId { get; set; }
}
可以使用SQLite.NET Extensions模块中包含的属性(例如ForeignKey、OneToMany和ManyToOne)来介绍实体之间的关系,该属性允许开发人员创建 ORM 模型,并帮助进行数据检索和更新过程。
准备好实体模型后,可以使用各种文件路径组合创建db连接:
// Path to the db file
var dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Auctions.db");
var db = new SQLiteAsyncConnection(dbPath);
通过db连接,可以使用 LINQ 语法和熟悉的表交互上下文执行各种操作:
await db.CreateTableAsync<Vehicle>();
var query = db.Table<Vehicle>().Where(_ => _.Year == 2018);
var auctionsFor2018 = await query.ToListAsync();
Sqlite.NET还支持基于文本的查询,而无需求助于 LINQ-2-Entity 语法。
SQLCipher是另一个可用于为敏感数据场景创建加密数据库存储的扩展。
实体框架核心
实体框架核心结合了多年积累的 ORM 结构和 SQLite 支持,使其成为本地存储实现的有力候选。 类似于实体框架的经典。net 版本,可以使用UseSqlite扩展名和DbContextOptionsBuilder的文件路径创建和查询数据上下文:
public class AuctionsDatabaseContext : DbContext
{
public DbSet<Auction> Auctions { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var dbPath = string.Empty;
switch (Device.RuntimePlatform)
{
case Device.Android:
dbPath = Path.Combine(
Environment.GetFolderPath(
Environment.SpecialFolder.MyDocuments),
"Auctions.db");
// removed for brevity
}
optionsBuilder.UserSqlite($"Filename={dbPath});
}
}
现在,DbSet<TEntity>声明可以用于构造 LINQ 查询,用于检索数据,而上下文本身可以用于推送更新。
除了广泛的关系功能,实体框架核心还提供了InMemory数据库支持。 通过使用InMemory数据库代替应用存储,开发人员可以轻松地模拟本地缓存实现来创建单元和集成测试。
如您所见到目前为止在这一章,在移动应用开发中,我们可以实现瞬态数据缓存机制,集中精力提高 HTTP 通信性能,或者我们可以去更复杂的结构化数据存储帮助我们介绍在我们消费者应用离线特性。 两者之间的选择很大程度上取决于数据的类型和我们正在处理的应用所使用的用例。 根据所讨论的数据类型,我们可以使用各种不同的数据访问模式。
数据访问模式
到目前为止,我们已经定义了各种数据存储,这些数据存储将在客户机边界内用作辅助数据源,以帮助处理脱机场景以及网络连接有问题时的情况。 这样,用户界面不会出现空白——或者更糟,进入无限加载循环。 相反,在进行远程检索时,将立即向用户显示数据集的以前版本。 在本节中,我们将仔细研究我们可以利用前面提到的方法和技术实现的模式。
在深入研究协调本地和远程数据的不同架构模式之前,让我们通过识别应用中的数据类型来尝试描述协调的含义。 根据具体数据元素的生命周期和熵值,我们可以将数据类型分为以下几类:
- 暂态数据:这些数据元素将不断变化,本地存储应该持续失效。 在处理这种数据类型时,应用应该首先加载本地缓存以尽可能快地响应用户输入,但每次我们都应该执行远程调用来更新本地缓存和视图模型数据。 这方面的例子将是拍卖信息和投标条目。
- 参考数据:这些数据元素会不时地变化,但是可以使用缓存存储作为真实的来源(如果它存在的话),并提供合理的生存时间。 用户配置文件和车辆数据可能是这种数据类型的好例子。 在处理引用数据时,视图模型应该尝试从缓存加载实体。 如果它不存在或数据已经过期,应用应该到达远程服务器并使用远程数据更新本地缓存。
- 静态数据:应用中有某些数据元素可能永远不会更改。 这些静态数据元素,如国家列表或内部枚举器描述,在正常情况下只加载一次,并从本地存储中提供。
除了这些数据类型之外,我们还应该提到易变实体类型,除非有明确的指示,否则应用根本不应该缓存这些类型。 例如,想象一个搜索查询,其中包含一组关于车辆或拍卖集的任意查询参数,返回一个分页的条目列表。 试图通过视图模型缓存这些数据和服务器是非常不合理的。 另一方面,我们可能希望显示在主仪表板上的最新条目列表实际上可以被归类为瞬时数据。
现在,让我们看看一些简单的实现模式,它们可能有助于实现流体数据流和用户体验。
实现存储库模式
当然,在讨论数据存储时,开发工作中最重要的丰富模式是存储库模式。 在本地和远程数据的上下文中,我们可以创建实现相同存储库接口的小型存储库,并创建一个管理器类来协调这些存储库。
让我们从临时数据存储库开始,例如Auctions。 对于 Auctions API,让我们假设我们有三个方法来检索:
- 拍卖清单
- 具体拍卖的细节
- 更新特定拍卖的方法
让我们为这些方法定义接口:
public interface IAuctionsApi
{
Task<IEnumerable<Auction>> GetAuctions();
Task<Auction> GetAuction(string auctionId);
Task UpdateAuction(Auction auction);
}
我们现在有两个竞争的存储库,它们实现了相同的接口:
public class RemoteAuctionsApi : IAuctionsApi
{
// ...
}
public class LocalAuctionsApi : IAuctionsApi
{
// ...
}
这里,最简单的方法处理数据是实现一个包装器,它将保持更新本地缓存,通过包装器实例,以及本地存储库实例视图模型视图模型,可以处理初始缓存负载场景,因此利用本地存储库,然后调用包装器实例。
让我们继续并实现包装器类,我们将其称为管理器:
public class ManagerAuctionsApi : IAuctionsApi
{
private readonly IAuctionsApi _localApi;
private readonly IAuctionsApi _remoteApi;
// ...
public async Task<Auction> GetAuction(string auctionId)
{
var auction = await _remoteApi.GetAuction(auctionId);
await _localApi.UpdateAuction(auction);
return auction;
}
}
视图-模型加载策略将调用本地 API,更新视图模型数据,并通过ContinueWith动作调用远程 API 来更新模型:
Action<Auction> updateViewModel = (auction) => {
Device.BeginInvokeOnMainTread(() => this.Auction = auction)
};
var localResult = await _localApi.GetAuction(auctionId);
updateViewModel(localResult);
// Not awaited
_managerApi.GetAuction(auctionId).ContinueWith(task => updateViewModel(task.Result));
参考存储库的实现将使用不同的策略:
public async Task<User> GetUser(string userId)
{
var user = await _localApi.GetUser(userId);
if (user != null) { return user; }
user = await _remoteApi.GetUser(userId);
await _localApi.UpdateUser(user);
return user;
}
在这个实现中,视图模型负责通过合并来自本地和远程存储库的结果并将结果传播到视图来完成数据加载。 如果存储库对来自本地存储和远程存储的结果之间的差异有足够的意识,那会怎么样呢?
可观察存储库
通过使用响应式的扩展,我们可以实现一个通知观察对象,并订阅视图模型上的各种事件:
OnNext:我们将更新视图模型中的数据,该视图模型将被触发两次。OnCompleted:我们将隐藏到目前为止显示的任何进度指示器。
在实现视图模型订阅之前,让我们以一种响应式的方式实现GetAuction方法:
public IObservable<Auction> GetAuction(string auctionId)
{
var localAuction = _localApi.GetAuction(auctionId).ToObservable();
var remoteAuction = _remoteApi.GetAuction(auctionId).ToObservable();
// Don't forget to update the local cache
remoteAuction.Subscribe(auction => _localApi.UpdateAuction(auction));
return localAuction.Merge(remoteAuction);
}
现在我们有了我们的可观察结果,我们可以在我们的视图模型中实现订阅模型:
var auctionsObservable = _managerAuctionsApi.GetAuction(auctionId);
auctionsObservable.Subscribe(auction => updateViewModel(localResult));
await auctionsObservable();
IsBusy = false;
在从可观察结果到任务或从任务到可观察结果的转换中,小心处理跨线程问题是很重要的。 一般的经验法则是,所有更新 UI 线程上元素的代码都应该使用Device.BeginInvokeOnMainTread执行。
静态数据加载会坚持与引用存储库相同的策略,但也可以初始化静态缓存应用第一次运行时使用一个后台任务或嵌入到静态数据与 JSON 文件或播种 SQLite 数据库文件到应用包中。 在某些时候,这些数据仍然可以与远程服务器同步。 数据解析器
前面提到的静态数据元素——特别是通过 ID 引用附加到主集合的数据点——可能永远不会改变,但它们将与主数据元素一起传输。 为了减少有效负载大小,我们实际上可以忽略 DTO 模型上的这些数据对象,只通过 ID 表示它们。 然而,在这样的实现中,我们需要找到一种方法来将这些 ID 引用解析为真实的数据元素。
例如,让我们看看作为车辆描述一部分的引擎实体:
public class Engine
{
[JsonProperty("displacement")]
public string Displacement { get; set; }
[JsonProperty("fuel")]
public FuelType Fuel { get; set; }
// ...
}
客户端使用相同的 DTO 对象。 这里,Fuel类型作为一个复杂对象而不是单个标识符返回(可用选项是 diesel 或 gas)。 假设在客户机应用上初始化了燃料类型的静态数据,我们实际上不需要从服务器检索它。 让我们用标识符(即字符串类型的标识符)替换属性。
在客户端,让我们扩展我们的模型,让它有一个引用 ID 和一个实体:
public class Engine
{
[JsonProperty("displacement")]
public string Displacement { get; set; }
[JsonProperty("fuelId")]
public string FuelId { get; set; }
[JsonIgnore]
public KeyIdentifier Fuel { get; set; }
}
使用 ignore 属性,引用 ID 将不会在序列化阶段被省略。 但是,每次从服务器检索Engine实例时,我们仍然需要解析此条目。
此外,我们还用一个通用实体表示类似的键/值对,例如FuelType复杂对象:
public class KeyIdentifier
{
public string KeyGroup { get; set; } // for example, "FuelType"
public string Key { get; set; } // for example, "1"
public string Value { get; set; } // for example, "Diesel"
}
运行时识别需要解析的属性和Target要分配数据的属性的最简单方法是创建一个自定义数据注释属性:
[AttributeUsage(AttributeTargets.Property)]
public class ReferenceAttribute : Attribute
{
public ReferenceAttribute(string keyType, string field)
{
KeyType = keyType;
Field = field;
}
public string KeyType { get; set; }
public string Field { get; set; }
}
现在,使用我们刚刚创建的注释,引擎 DTO 类应该是这样的:
public class Engine
{
[JsonProperty("displacement")]
public string Displacement { get; set; }
[Reference("FuelType", nameof(Fuel))]
[JsonProperty("fuelId")]
public string FuelId { get; set; }
[JsonIgnore]
public KeyIdentifier Fuel { get; set; }
}
我们现在声明FuelId字段是对FuelType类型的KeyIdentifier对象的引用,并且解析后的数据应该分配给一个名为Fuel的属性。
假设关于FuelType的静态数据存储在本地存储中,并且可以作为KeyIdentifier对象检索,我们现在可以为静态数据实现一个通用转换器:
public async Task TranslateStaticKeys<TEntity>(TEntity entity)
{
var entityType = typeof(Entity);
var properties = entityType.GetRuntimeProperties();
foreach(var property in properties)
{
var refAttribute =
property.CustomAttributes.FirstOrDefault(item =>
item.AttributeType == typeof(ReferenceAttribute));
if(customAttribute == null) { continue; }
var keyParameters = refAttribute.ConstructorArguments;
// e.g FuelType => Fuel
var keyType = keyParameters.Value.ToString();
var propertyName = keyParameters.Value.ToString();
targetProperty = properties.FirstOrDefault(item => item.Name ==
propertyName);
try
{
await TranslateKeyProperty(property, targetProperty,
entity, keyType);
}
catch(Exception ex)
{
// TODO:
}
}
}
TranslateStaticKeys方法遍历实体的属性,如果它用ReferenceAttribute标识一个属性,则调用TranslateKeyProperty方法。
实际的转换方法将遵循类似的实现路径,因此检索给定类型的键/值对集合,将键 ID 解析为KeyIdentifier,并最终将数据分配给目标属性:
public async Task TranslateKeyProperty<TEntity>(
PropertyInfo sourceProperty,
PropertyInfo targetProperty,
TEntity entity,
string keyType)
{
IEnumerable<KeyIdentifier> keyIdentifiers = await _api.GetStaticValues(keyType);
if(targetProperty != null)
{
var sourceValue = sourceProperty.GetValue(entity)?.ToString();
if(!string.IsNullOrEmpty(sourceValue))
{
var keyIdentifier = keyIdentifiers.FirstOrDefault(item =>
item.Id == sourceValue);
targetProperty.SetValue(entity, keyIdentifier);
}
}
}
现在,在获取一个实体之后,我们可以简单地调用translate方法来解析所有未完成的数据点:
var auction = await _managerAuctionsApi.Get(auctionId);
await Translator.TranslateReferences(auction.Vehicle.Engine);
实现可以扩展到集合和遍历整个对象树。
本节从概念上介绍了数据类型和各种相关的架构模式。 简而言之,根据情况和用例,您可以选择将这些模式与瞬态或持久数据缓存存储结合使用。
总结
在本章中,我们实现并使用了不同的模式和技术来创建能够离线响应的应用。 在本章中,我们的主要目标是创建一个基础设施,它将同步和协调本地和远程存储之间的数据流,从而实现愉快的用户体验。 最初,我们使用缓存旁路模式,并使用标准 HTTP 协议定义来缓存和验证缓存的数据。 我们还分析了 SQLite、实体框架核心和 Akavache 等技术。 最后,我们简要地看了一下 Realm 组件,以及如何使用它们来管理跨设备和平台的数据。 总的来说,在社区中有许多可供开发人员使用的技术,为您的特定用例选择正确的模式和技术堆栈,以获得最佳的用户体验和满意度,这一点非常重要。
在下一章中,我们将看看 Graph API、推送通知和其他吸引客户的方法。*****
十三、使用通知和图形 API 吸引用户
推送通知是应用基础结构向用户传递消息的主要工具。 它们被用于向用户广播更新,根据某些操作发送通知,并根据用户满意度或指标吸引他们。 另一方面,Microsoft Graph API 是一种服务,它为通过 Office 365、Windows 10 和其他 Microsoft 服务积累的数据提供统一网关。
简而言之,本章将解释如何利用推送通知和 Graph API 数据来提高用户参与度。 我们将从学习移动平台上通知服务的基础知识开始。 然后,我们将开始使用 Azure notification hub 为我们的跨平台应用创建一个通知服务实现,并使用高级场景迭代这个主题。 我们还将了解用于 Xamarin 应用的 Graph API 的特性集。
下面的章节将推动关于用户参与的讨论:
- 了解本地通知服务
- 配置 Azure 通知中心
- 创建通知服务
- 图形 API 和罗马项目
在本章结束时,你将更好地理解用户粘性对手机平台的意义。 您将能够在 Azure 上设置自己的通知中心,并从 Graph API 上提供的广泛 API 中为您的应用确定合适的工具。
了解本机通知服务
从名称可以推断出推送通知,它是从后端服务器应用推送到目标应用用户界面的消息的通用名称。 推送通知可以针对特定设备,也可以针对一组用户。 它们可以从简单的通知消息到对目标平台上的应用后端进行用户不可见的调用。
在进入高级通知场景之前,我们将首先关注通知提供者的基本概念以及如何在移动平台上使用它们。
通知提供者
一般来说,推送通知是从应用后端服务发送到目标设备。 通知交付由特定于平台的通知提供者处理,这些提供者称为,即平台通知系统(PNSes)。 为了能够向 iOS、Android 和 UWP 应用发送推送通知,作为开发者,你需要创建以下通知管理套件/基础设施:
- Apple 推送通知服务(APNS)
- Firebase 云消息(FCM)
- Windows 通知服务(WNS】T3)
每个通知提供者实现不同的协议和数据模式来向用户发送通知。 这个模型对于跨平台应用的主要问题如下:
- 跨平台推送通知:每种推送服务(iOS 的 APNS, Android 的 FCM, Windows 的 WNS)都有不同的协议。
- 不同内容模板:通知模板以及数据结构在各大平台上都是完全不同的。
- 分词:这是基于兴趣和位置,只显示每个部分最相关的内容。
- 维护精确的设备注册:动态的用户基础,通过在启动时添加注册、更新标签和剪枝来完成。
- 高容量低延迟:跨平台实现难以满足高容量低延迟的应用需求。
总的来说,这些缺点源于每个 PNS 的设计和操作都有一个特定的平台,因此忽略了跨平台目标群体的需求。 然而,无论实现如何不同,在每个 PNS 中发送通知的基本流程或多或少是相同的。
使用 PNS 发送通知
使用本机通知提供程序,如果目标设备已经打开通知通道,应用后端可以向目标设备发送通知。 虽然此流程的实现高度依赖于手头的平台,但从较高的层次上讲,所有 pnse 上的注册和通知流程非常相似。
设备应该通过注册自己到 PNS 并检索所谓的 PNS 句柄来启动这个流程。 这个句柄可以是一个令牌(例如,APNS)或一个简单的 URI(例如,WNS)。 一旦获取了 PNS,就应该将其作为名片交付给后端服务,如下图中的步骤 2所示:

图 13.1 -推送通知服务流程
当后端服务想要发送一个通知到这个特定的设备时,它使用保存的设备的 PNS 句柄联系 PNS,以便 PNS 可以发送通知。
一般约束
苹果推送通知服务(apn)和重火力点云消息(FCM)基础设施都利用【显示】某些清单声明非常特定于应用的安装在目标设备上。****
**APNS 要求用于签名应用包的应用 ID(和证书)具有非通配符绑定声明(也就是说,com.mycompany.*类型的证书不能用于此实现)。
设备句柄(字母数字令牌或 URL,取决于平台)是通知过程中需要和使用的唯一设备信息。 相反,通知服务应该支持多个应用和平台。
Azure 通知中心
在存在多个平台和多个服务提供者的环境中,Azure Notification hub 充当作为创建通知请求的后端服务和将这些通知请求传递到目标设备的提供者服务之间的中介。
通知中心基础设施
考虑到应用的发布环境(即 alpha、beta 和 prod)和通知中心,每个环境都应该在 Azure 基础设施上设置为单独的中心。 尽管如此,通知集线器可以与所谓的名称空间相结合,以便在一个地方管理每个平台的应用环境。
通知中心
从语义上讲,通知中心指的是 Azure 通知中心基础设施中最小的资源。 它直接映射到运行在特定环境上的应用,并为每个平台通知系统(PNS)持有一个证书。 应用可以是混合的、本地的或跨平台的。 每个通知中心都有针对不同通知平台的配置参数,以支持跨平台消息传递基础设施:

图 13.2 - Apple PNS 设置
通知名称空间
另一方面,名称空间是集线器的集合。 可以使用通知名称空间管理不同的环境,并为更大的目标受众创建通知 hub 集群。 使用命名空间,开发人员或配置管理团队可以跟踪通知集线器的状态,并将其作为调度通知的主集线器,如下图所示:

图 13.3 -通知集线器配置
在理想的设置中,特定于应用的通知服务将与特定的集线器通信,而命名空间将用于配置和管理这些集线器:

图 13.4 -推送通知集线器拓扑
在此配置中,特定于应用的通知服务从不直接与特定于平台的通知服务通信; 相反,通知将消息传递到目标平台。 在类似的方法中,共享一个公共目标用户组的多个应用也可以统一在一个名称空间下,并使用相同的通知名称空间。
为了理解应用注册流程,我们应该进一步了解通知中心注册和通知流程。
使用 Azure 通知中心的通知
Azure 通知中心采用非常类似的策略,为注册用户设备提供推送通知。 毕竟,它为本机 pnse 提供了基本的配置管理。 与 PNS 流程类似,Azure 通知中心流可以分为两个部分定义:注册和通知。 这些部分还通过推送通知定义用户参与会话的生命周期。 让我们仔细看看注册和通知流程。
登记
Azure Notification hub 支持两种类型的设备注册——客户端注册和后端注册:
- 在客户端注册场景中,应用使用本地平台上接收到的设备句柄调用 Azure 通知中心。 在这种情况下,只能通过设备句柄来识别用户。 为了让后端服务向目标设备发送通知消息,设备应该将通知信息传递给后端服务。
- 在后端注册场景中,后端服务用 Azure 通知中心处理注册。 此场景允许后端服务向注册数据插入附加信息,例如用户信息(例如,用户 ID、电子邮件、角色和组)、应用平台信息(例如,应用运行时、发布版本和发布阶段),以及应用和/或用户的其他标识符。 这种类型的注册流程更适合于多个应用可能从多个源接收通知的实现,以便通知服务可以充当事件聚合器。
使用通知集线器将目标设备注册到通知提供程序,要么使用称为注册的数据包,要么使用称为安装的扩展设备信息集。 注册包包含设备的通知句柄或通知 URI,安装包含附加的特定于设备的信息。
在注册过程中,客户端应用(或后端通知服务)还可以注册与标记关联的通知模板,以个性化通知并创建通知目标。 这些标记稍后可用于将通知广播到属于单个用户的设备组,或具有多个设备的用户组,从而支持广播就绪的基础设施。
通知
一旦设备与后端服务和通知中心注册,后端服务就可以使用针对特定平台的设备的 PNS 句柄发送通知。 对于这种类型的通知,后端服务器应该确定目标平台是什么,使用目标平台的模式准备一个通知消息,最后将通知消息发送到给定的句柄。
向用户发送通知的另一种方法是使用通知标记。 特定的用户标记可以与关联的模板一起使用,以针对多个平台。 在这种情况下,在注册期间,设备将使用特定的模板(取决于平台)和标识用户的用户标签(例如username: can.bilgin)注册。 这样,发送到带有给定标记的通知集线器的任何通知消息都将被路由到具有此注册的目标设备。
最后,广播消息使用定义用户特定兴趣的各种标记,可以与通知一起使用。 在此通知场景中,应用接口将公开某些通知设置,然后将这些通知设置转换为注册标记。 每次用户更新这些设置时,设备注册到后端服务将需要使用更新后的标记集进行更新。 例如,在我们的应用中,如果我们要包括通知设置允许用户创建一个新的拍卖时接收通知特定的制造或汽车模型,我们可以使用这个标签尽快发送通知类似的用户在应用中创建一个新的条目数据存储。
正如我们在本节中看到的,Azure Notification hub 实际上是一个 façade,它为多个服务提供商提供了统一的管理和开发工具集,从而简化了跨平台应用的笨重的本地通知集成。 现在我们已经学习了本地推送通知和 Azure 通知中心,让我们继续实现一个简单的通知服务。
创建通知服务
根据通知目标组的粒度,应用设计可以包括通知服务; 否则,另一种选择是设置通知基础设施可以集成到任何一个现有服务中。 例如,如果应用不需要特定于用户的消息传递场景,那么就不需要跟踪设备注册。 在这种情况下,可以实现客户端注册机制来使用通知类别,并且后端服务可以将通知发送到目标标记。
作为我们的拍卖应用的一部分,让我们使用下面的用户故事来启动我们的通知服务的实现:
“作为一个产品所有者,我希望创建一个通知服务,这样我就可以有一个开放的渠道与我的用户组,通过这个渠道我可以让他们单独或作为一个组来提高回报率。”
根据这个请求,我们可以开始分析不同的用例和实现模式。
定义需求
由于通知服务将支持来自各个模块的传入通知,因此架构设计和实现将需要遵循某些指导方针,同时满足注册和通知要求:
- 通知服务应该设计为支持不同应用版本(例如,alpha、beta 和 prod)的多个通知集线器。
- 通知服务应该设计为接收针对特定设备、特定用户或一组用户(例如,兴趣组、特定角色和参与特定活动的人员)的通知请求。
- 应该使用不同的通知模板分配不同的通知事件。
- 用户应该根据他们的语言偏好使用不同的模板注册。
- 用户应该能够选择加入或退出某些通知。
总的来说,通知服务实现应该实现一个事件聚合器模式,其中发布者是通知的源,并负责确定通知规范和目标的定义, 而订阅者是本地移动应用,它们提交定义其地址参数的注册请求。 一旦满足目标标准的通知从管道中下来,该通知就被路由到关联的目标。
设备注册
设备注册是第一个由开发团队实现和测试的用例。 在这个场景中,用户将打开(或安装)拍卖应用,并通过一个可用的身份提供者进行身份验证。 在这个阶段,我们有设备信息和用户身份,我们可以使用它进行注册。 让我们看看这是如何做到的:
-
在用户打开应用时(对于返回的用户,即一个现有的标识),或者在注册/身份验证完成后,需要打开通知通道。 在 iOS 上,我们可以使用
UserNotificationCenter模块对通知进行授权:UNUserNotificationCenter.Current.RequestAuthorization( UNAuthorizationOptions.Alert | UNAuthorizationOptions.Sound | UNAuthorizationOptions.Sound, (granted, error) => { if (granted) { InvokeOnMainThread(UIApplication.SharedApplication .RegisterForRemoteNotifications); } }); -
一旦注册被调用,
AppDelegate.cs文件中的override方法就可以用来检索设备令牌:public override void RegisteredForRemoteNotifications( UIApplication application, NSData deviceToken) -
现在,我们可以使用授权令牌将此令牌发送到通知服务设备注册控制器,该令牌用于识别用户,该令牌使用注册数据对象
public class DeviceRegistration { public string RegistrationId { get; set; } // Registration Id public string Platform { get; set; } // wns, apns, fcm public string Handle { get; set; } // token or uri public string[] Tags { get; set; } // any additional tags } -
在我们开始与通知中心交互之前,我们需要安装
Microsoft.Azure.NotificationHubsNuGet 包,它将提供集成方法和数据对象。 实际上,可以在客户端安装相同的包,以便轻松创建通知通道并检索发送到后端服务所需的信息。 -
一旦在我们的服务上收到设备注册,根据它是一个新的注册还是一个以前注册的更新,我们可以制定流程来清理以前的注册,并为设备创建一个通知中心注册 ID:
public async Task<IActionResult> Post(DeviceRegistration device) { // New registration, execute cleanup if (device.RegistrationId == null && device.Handle != null) { var registrations = await _hub.GetRegistrationsByChannelAsync(device.Handle, 100); foreach (var registration in registrations) { await _hub.DeleteRegistrationAsync(registration); } device.RegistrationId = await _hub.CreateRegistrationIdAsync(); } // ready for registration // ... } -
现在,我们可以使用适当的(即特定于平台的)通道信息创建注册描述,以及我们刚刚创建的注册 ID:
RegistrationDescription deviceRegistration = null; switch(device.Platform) { // ... case "apns": deviceRegistration = new AppleRegistrationDescription(device.Handle); break; //... } deviceRegistration.RegistrationId = device.RegistrationId; -
我们还将在注册期间需要当前用户身份作为标记。 让我们添加这个标签,以及用户发送的其他标签:
deviceRegistration.Tags = new HashSet<string>(device.Tags); // Get the user email depending on the current identity provider deviceRegistration.Tags.Add($"username:{GetCurrentUser()}"); -
最后,我们可以通过将注册数据传递到我们的 hub:
await _hub.CreateOrUpdateRegistrationAsync(deviceRegistration);来完成注册。
在这里,如果我们使用基于安装的注册而不是设备注册,我们将对注册过程有更多的控制。 最大的优势之一是设备安装注册提供定制的模板关联:
var deviceInstallation = new Installation();
// ... populate fields
deviceInstallation.Templates = new Dictionary<string, InstallationTemplate>();
deviceInstallation.Templates.Add(
"type:Welcome",
new InstallationTemplate
{
Body = "{\"aps\": {\"alert\" : \"Hi ${FullName} welcome to Auctions!\" }}"
});
再进一步,在注册过程中,可以根据用户的首选语言加载模板:
var template = new InstallationTemplate()
template.Body = "{\"aps\": {\"alert\": \"
+ GetMessage("Welcome", user.PreferredLanguage) + "\" }}";
这是用户注册设备的方式。 现在,我们将继续传送通知。
发送通知
既然用户已经注册了设备,我们就可以在服务器上实现发送通知方法。 我们希望支持免费的文本通知,以及带有数据的模板消息。 让我们开始:
-
让我们从创建基本通知方法开始,该方法将定义目标和消息:
public class NotificationRequest { public BaseNotificationMessage Message { get; set; } public string Destination { get; set; } } -
对于简单的消息场景,调用服务模块并不真正知道目标平台——它只是定义一个用户和一个消息项。 因此,我们需要为所有三个平台生成一条消息,从而覆盖用户可能拥有的所有可能设备:
public class SimpleNotificationMessage : BaseNotificationMessage { public string Message { get; set; } public IEnumerable<(string, string)> GetPlatformMessages() { yield return ("wns", @"<toast><visual><binding template=""ToastText01""><text id=""1"">" + Message + "</text></binding></visual></toast>"); yield return ("apns", "{\"aps\":{\"alert\":\"" + Message + "\"}}"); yield return ("fcm", "{\"data\":{\"message\":\"" + Message + "\"}}"); } } -
For the templated version, the message looks a little simpler since we assume that the device already registered a template for the given tag and so we don't need to worry about the target platform. Now, we just need to provide the parameters that are required for the template:
public class TemplateNotificationMessage : BaseNotificationMessage { public string TemplateTag { get; set; } public Dictionary<string, string> Parameters { get; set; } }重要提示
这通常是一个好主意来创建一个通用的消息模板,将被用来发送简单文本消息并注册这个模板作为一个简单的模板信息,所以我们不需要为每个平台创建一个单独的消息,推动通过所有可用的设备通道。
-
现在,我们可以处理通知服务上的通知请求,并将其交付给目标用户:
if (request.Message is SimpleNotificationMessage simpleMessage) { foreach (var message in simpleMessage.GetPlatformMessages()) { switch (message.Item1) { case "wns": await _hub.SendWindowsNativeNotificationAsync( message.Item2, $"username:{request.Destination}"); break; case "aps": await _hub.SendAppleNativeNotificationAsync( message.Item2, $"username:{request.Destination}"); break; case "fcm": await _hub.SendFcmNativeNotificationAsync( message.Item2, $"username:{request.Destination}"); break; } } } else if(request.Message is TemplateNotificationMessage templateMessage) { await _hub.SendTemplateNotificationAsync( templateMessage.Parameters, $"username:{request.Destination}"); }
我们已经成功地使用username:<email>标记将消息发送给目标用户。 这样,用户将在其设备上接收到推送通知。 由特定于平台的实现来处理消息。
广播到多个设备
在前面的示例中,我们使用username标记将特定的用户定义为通知消息的目标。 除了单个标记外,还可以使用标记表达式定义订阅特定通知类别的更大组用户。 例如,为了将通知发送给所有接受拍卖更新的新拍卖用户以及对特定汽车制造商感兴趣的用户,标签可能如下所示:
notification:NewAuction && interested:Manufacturer:Volvo
另一种情况是发送一个关于拍卖最高出价的通知:
notification:HighestBid && auction:afabc239-a5ee-45da-9236-37abc3ca1375 && !username:john.smith@test.com
这里,我们有三个标签,去年的是标签,确保用户出价最高的得不到的消息发送到其他用户参与拍卖的,这样他们就可以获得更个性化的恭喜类型的消息。
高级场景
这里演示的通知消息只是通知的简单模型,不需要在客户机应用或服务器端广泛实现。
现在,我们将看看一些更高级的使用场景。
推,拉
银行、医疗保健和政府部门的通知中心客户不能通过公共云服务(如 APNS、FCM 或 WNS)传递敏感数据。 为了支持这些类型的场景,我们可以使用一种方案,其中通知服务器只负责发送消息 ID,而客户端应用只使用给定的消息 ID 检索目标消息。 这种模式通常称为推-2-拉模式,它是将通信通道从通知集线器和 pnse 迁移到公共 web 服务通道的好方法。
用于推送消息的富媒体
本机通知提供者(PNS)通过发送的通知消息在呈现给用户之前也可以被拦截和处理。 在 iOS 平台上,Notification Extension 框架允许我们创建可以修改可变通知消息的扩展。
为了创建一个通知扩展,你可以添加一个新项目,并选择最适合你的通知扩展:

图 13.5 - Xamarin 通知内容扩展
通过内容扩展,可以创建可以显示富媒体内容的交互式通知视图,而服务扩展可用于拦截和处理通知负载(例如,解密安全负载)。
Android 有类似的机制,允许您在通知内容到达用户之前修改通知内容。 toast 消息还可以显示应答控件,该控件接受就地用户交互。 虽然这种参与模型增加了应用的可用性,但它对回报率没有贡献。
到目前为止,在本章中,我们已经阐述了使用推送通知的各种方式以及使用这些方法的场景。 总之,推送通知是一种强大的开发工具,能够吸引用户回到应用中,并保持他们的粘性,即使应用没有运行。 然而,如果这种参与不是在个人层面上,那就没有比垃圾邮件更好的了。 Graph API 是一项很好的资产,可以改善个性化和跨设备的连续性。
Graph API 和 Project Rome
既然我们在讨论跨平台参与,现在是讨论 Graph API 和 Project Rome 的好时机。 Microsoft Cloud 基础设施中提供的这些交织的基础设施服务允许开发人员创建跨平台和设备边界的应用体验。
图形 API
Graph API 是微软云服务的一个集合,用于与通过各种平台(包括 Microsoft Office 365 和 Microsoft Live)收集的数据进行交互。 这个数据网络中的数据元素是围绕当前登录的用户构建的。 与某些应用(例如,已创建的会议、已发送的电子邮件或添加到公司董事的新联系人)或设备(例如,在新设备上的登录)的交互被创建为关系图中的新节点。
然后可以使用这些节点为用户创建更浸入式的体验,用户正在与来自不同来源的应用数据进行交互。
例如,只需在应用中使用 Microsoft Identity,在用户授权访问资源之后,应用下载文档、创建缩略图、检索目标用户,并在将其发送给一组用户之前创建完整的电子邮件。 除了基于微软的应用,第三方应用也可以创建图形数据来增加用户粘性。
罗马计划
Project Rome 在 Graph API 的前提下构建,它可以被定义为一个设备运行时,用于连接和集成基于 windows 的跨平台设备到 Project Rome 基础设施服务。 该运行时是微软云中的基础设施服务和 api 之间的桥梁,api 作为 Windows、Android、iOS 和 Microsoft Graph 的编程模型交付,因此使客户端和云应用能够使用 Project Rome 的功能构建体验。
Project Rome 公开了几个关键 api,其中大多数都可以在跨平台应用实现中使用。 当前的 API 集合由以下几个部分组成:
- 设备继电器
- 用户活动
- 通知
- 远程会话
- 附近的分享
其中一些特性仅适用于 Windows 平台,但所有这些特性都需要在应用域中使用 Microsoft 标识。 需要注意的是,所有这些特性都可以通过使用 REST 接口的 Graph API 使用。
让我们仔细看看这些特性,并找出不同的用例。
设备继电器
设备中继是一组模块,模块允许设备到设备的通信和切换功能。 从语义上讲,特性集类似于以前在 UWP 平台上可用的应用服务和 URI 启动功能。 然而,通过设备中继,应用可以与另一个设备甚至另一个平台上的另一个应用进行通信(例如,使用 SDK,可以从 Android 手机启动 Windows 设备上的应用)。
用户活动
用户活动是与 Graph API 最紧密的集成之一。 使用用户活动和活动提要,应用可以在应用最后一次使用时创建带有相关操作的历史提要。 这些 feed 被输入到 Graph API 中,并在不同设备之间同步。 例如,使用活动提要,我们可以创建用户在某个会话中查看的拍卖/车辆的历史记录。 然后,一旦这些项目通过同一个活动 ID 同步到 Windows 设备上,用户就可以轻松地单击提要中的项目并返回到之前的会话。 如果 Windows 设备没有安装该应用,操作系统会建议用户在 Windows 平台上安装该应用。 这样,应用在多个平台上的渗透率就会增加。 简单地说,这个功能是专门用于跨设备和平台的用户体验的连续性。
用户活动可能包含关于作为活动历史记录条目创建的活动的深层链接、可视化和元数据。
通知
Project Rome 通知,也被称为 Microsoft Graph 通知,是通知用户的另一种方式。 Graph Notifications 与其他特定于平台的通知提供者的区别在于,Graph Notifications 的目标用户不知道他们所使用的平台。 换句话说,Graph Notifications 的目标不是设备,而是用户。 除了基于用户的通知模型外,Graph 通知基础结构中的通知状态在设备之间是同步的,因此应用本身不需要做更多的事情来将某个消息设置为驳回或类似于反映多个设备上的用户交互。
远程会话
Remote Sessions API 是一个仅针对 windows 的 API,它允许设备创建共享会话、加入会话以及在不同用户之间创建交互式消息传递平台。 创建的会话可以用于跨设备发送消息,以及在联合会话中保持共享会话数据。
附近的分享
附近共享允许应用通过蓝牙或 Wi-Fi 向附近的设备发送文件或网站。 这个 API 可以在 Windows 上作为以及本地 Android 运行时使用。 在共享操作期间,附近的共享功能也足够智能,可以通过选择蓝牙或网络连接来获取两个设备之间最快的路径。 作为共享模块的一部分的发现功能允许应用通过蓝牙发现可能的共享操作目标。
在本节中,我们浏览了 Graph API 及其跨平台用户 SDK 的一些功能。 现在,您已经了解了 Graph API 提供了对数据的访问和对其他生产力平台(如 Office 365)的集成。 通过使用这些数据和可用的扩展机制,您的消费者应用可以个性化用户体验,扩展用户交互,不仅跨用户会话,还跨设备。 此外,使用 Project Rome sdk,您可以增强移动平台上的用户交互性和体验。
总结
在本章中,我们已经了解了使用推送通知和 Microsoft Graph API 实现来提高用户参与度的方法。 保持用户粘性是维持应用回报率的关键因素。 推送通知是一个很好的工具,你可以使用它来吸引你的用户,即使你的应用不活跃。 通过在 pns 和目标设备运行时之间创建一个抽象层,Azure 通知名称空间和中心使这个约定更容易实现。 在推送通知的基础上,我们分析了通过 Project Rome 和 rest API 在 Graph API 中可用的各种 API。
在本书的这一部分,包括前三章,我们用异步实现、本地数据管理和通知增强了我们的应用。 在本书的其余部分中,我们将关注如何有效地管理应用生命周期,并创建一个完全自动化的开发和交付管道,以缩短我们的上市时间,同时增加发布节奏。 在下一章中,我们将学习应用生命周期管理的基本概念。**
十四、Azure DevOps 和 Visual Studio App Center
Visual Studio App Center 是微软提供的一种一体化服务,主要供移动应用开发人员使用。 Xamarin 平台和 UWP 应用都是受支持的平台。 App Center 的主要目标是为移动项目创建一个自动构建-测试-分发管道。 App Center 对 iOS 和 Android 开发者来说也很有价值,因为它是唯一一个为两个目标运行时提供统一测试版发行版的平台,支持遥测收集和崩溃分析。 通过使用 Azure DevOps(以前称为 Visual Studio Team Service)和 App Center,开发人员可以为 Xamarin 应用建立一个完全自动化的管道,将源存储库连接到最终的商店提交。
本章将展示 Azure DevOps 和 App Center 的基本特性,以及如何创建适合个人开发人员和开发团队的高效应用开发管道。
本章将涵盖以下主题:
- 使用 Azure DevOps 和 Git
- 创建 Xamarin 应用包
- Xamarin 的应用中心
- 借助 App Center 进行分销
- 应用中心遥测和诊断
在本章结束时,您将学习如何有效地使用 Git 为您将存储在 Git 上的项目创建可管理的历史记录。 使用这些 Git 存储库,你将能够创建持续集成和交付管道,向 App Center 提供 Android 和 iOS 包。 这些数据将用于分发和遥测收集。
使用 Azure DevOps 和 Git
我们将以介绍 Git 以及如何在 Azure DevOps 上使用 Git 开始我们的 Azure DevOps 之旅。 我们还将讨论用于 Git 分支和管理这些分支的不同技术。
开发人员使用的 Azure DevOps 的第一个也是最重要的模块是其可用的源代码控制选项。 开发人员可以选择使用Team Foundation Version Control(TFVC)或 Git 来管理源代码(甚至同时使用两者)。 然而,随着分散的源代码控制管理的日益流行,由于开发工具集提供的灵活性和集成,Git 对许多人来说是更有利的选择。 Git 本地集成了 Visual Studio 和 Visual Studio for Mac。
使用 Azure DevOps 创建 Git 存储库
多个 Git 存储库可以托管在 Azure DevOps 中的同一个项目集合下,这取决于所需要的项目结构。 每个存储库都可以使用不同的安全性和分支策略进行管理。
要创建 Git 存储库,我们将使用 Azure DevOps 的 Repos 部分。 一旦创建了 DevOps 项目,就会为您创建一个需要初始化的空 Git 存储库。 这里的其他选择是导入一个现有的 Git 仓库(不一定是从其他 Azure DevOps 项目或组织),或者从你的工作站推送一个现有的本地仓库:

图 14.1 -初始化 Azure DevOps 存储库
重要提示
在创建无法更改的 Visual Studio Team Services 项目时,选择存储库类型是一个最初的决定。 然而,使用 Azure DevOps,即使在初始项目创建之后,也可以创建 Git 存储库,并与 TFVC 存储库并排。
重要的是要注意,克隆选项允许我们生成 Git 凭据,以便使用这个 Git 实例进行身份验证。 主要原因是 Azure DevOps 使用了联合实时身份验证(可能的双因素身份验证),这是 Git 本身不支持的。 因此,这个存储库的用户需要生成一个个人访问令牌(PAT),并将其用作密码。 Git for Windows 插件通过自动创建 PAT(并在 PAT 过期时更新它)来自动处理这个身份验证问题。 如果你想在 Mac 上使用 Git 和 Visual Studio, PAT 是目前唯一的解决方案。
在初始化选项中,我们还可以选择要创建的.gitignore文件(类似于 TFVC 的.tfignore文件),这样项目文件夹中不需要的用户数据就不会上传到源存储库中。
分支策略
使用 Git 和 Git 流程方法/模式,开发团队可以将本地和远程分支建立在两个主要分支上:开发分支和主分支。
开发分支被用作默认分支(也被称为主干),并代表下一个版本的源代码,直到特性集(分支)被开发团队完成并签署为止。 此时,将创建一个发布分支,下一个发布包的最后稳定阶段将使用该发布分支作为开发中所有热修复分支的基础。 每个来自更改发布版本的热修复分支的 pull 请求都需要合并回开发分支。
特性、开发和发布分支的一般流程如下图所示:

图 14.2 - Git 流分支策略
一般流程如下:
-
To safeguard the development branch as well as the release branches, developers, whether they're working on local or feature branches, will need to create a pull request that will be verified per the branch's policies.
重要提示
Pull 请求促进了同行评审过程,以及为了对开发或发布分支做出贡献而需要执行的额外静态分析。
-
在这种设置中,开发分支将有一个持续的集成构建和部署到 App Center 分发的快速环。 这允许开发和 QA 团队立即验证已合并到分支中的特性。
-
一旦当前版本分支准备好回归,它就可以合并到主分支中。 这种合并通常通过自动化 UI 测试(即自动化回归)进行验证。 主分支被用作 Visual Studio App Center 慢环部署(即登台环境)的源存储库。
-
过时的特性分支(参见外部特性分支)跨越多个版本,需要重新建立基础,以便将开发分支的历史添加到特性分支中。 这允许开发团队拥有关于提交的更清晰的元数据。
-
热修复分支用于纠正在发布分支中报告的存储提交失败或回归错误。 热修复分支可以通过自动化 UI 测试(完全自动化的回归)和手动使用慢环版本进行测试。
-
一旦主分支准备好发布,就会创建一个新的标记作为代码冻结的一部分,并且手动触发构建将为应用的 iOS 和 Android 版本准备提交包。
这个方法也可以修改为使用发布分支来进行登台和存储部署,而不是使用主分支。 这种方法为开发团队提供了更多的灵活性,并且比管理单个版本分支(即主分支)要简单一些。
管理开发分支机构
在开发阶段,保持开发和特性分支的清晰历史是很重要的——如果您正在与一个更大的团队合作的话更是如此。
在敏捷管理的项目生命周期中,分支(本地或远程)上的提交声明任务属于一个用户故事或一个 bug,而分支本身可能与用户故事或 bug 相关。 团队成员之间更大的共享分支也可以代表一个完整的特性。 在这种情况下,为了在保持源代码安全的同时减少提交的数量,而不是在每次发生更改集推入时创建一个新的提交,你可以使用Amend Previous commit特性:

图 14.3 -修改 Commit
一旦用这些更改修改了提交,本地提交将需要与远程版本合并。 在这里,避免在远程和本地分支之间进行合并提交的关键(因为远程分支具有较旧的提交版本,因此需要进行不同的提交)是在push命令中使用--force或--force-with-lease选项。 这样,本地提交(修改后的)将覆盖远程版本:

图 14.4 -启用 Push—Force
需要注意的是,当多个开发人员在同一个分支上工作时,不建议修改提交和覆盖远程分支。 这可能会为相关方的本地存储库上的分支创建不一致的历史记录。 为了避免这种情况,应该在特性分支准备好被合并后,将其分支扩展,并基于特性分支的最新版本进行重新划分。
让我们假设您已经从远程特性分支创建了一个本地分支,并推送了几次提交。 与此同时,你的团队成员向功能分支推送了几个更新:

来源:(https://www.atlassian.com/git/tutorials/merging-vs-rebasing / CC BY 2.5 AU)
图 14.5 - Git 特性分支
使用传统的同步(拉和推),将创建一个合并提交,并且特性分支的历史将类似如下:

来源:(https://www.atlassian.com/git/tutorials/merging-vs-rebasing / CC BY 2.5 AU)
图 14.6 - Git Merge Commit
然而,如果本地分支在推送之前重新基于远程分支的最新版本,历史将会是这样的:

来源:(https://www.atlassian.com/git/tutorials/merging-vs-rebasing / CC BY 2.5 AU)
抱歉
在您创建到开发或发布分支的 pull 请求之前,应该使用 rebase 策略,这样可以保留分支的干净历史记录,从而避免复杂的合并冲突。
在本节中,我们关注 Azure DevOps 上的 Git 存储库,如何创建和初始化它们,以及如何在使用最佳实践的同时使用 Visual Studio 执行众所周知的 Git 分支策略。
创建 Xamarin 应用包
一旦我们的应用准备好在实际设备上进行测试,我们就可以开始准备管道,以便我们可以编译和打包应用,并将其部署到 App Center 的 alpha 和 beta 环境中。 Azure DevOps 为这两个 Xamarin 都提供了开箱即用的模板。 Android 和 Xamarin 的。 iOS 应用。 这些管道可以进行扩展,以包括额外的测试和静态分析。
使用 Xamarin 构建模板
为 Xamarin 应用创建构建和发布模板就像为 iOS 和 Android 使用 Xamarin 模板一样简单。 此外,UWP(如果你的应用支持这个平台)模板也可以用来创建 UWP 构建:

图 14.8 - Xamarin Build Tasks
一旦创建了管道,我们将需要对两个平台做一些小的调整,以便准备应用,以便将其放到实际设备上。
Xamarin 的。 Android 构建
让我们来看看构建 Android 项目的步骤:
-
First, identify the correct Android project to be built using the wildcard designation and the target configuration:
![Figure 14.9 – Xamarin.Android Build Setup]()
图 14.9 - Xamarin。 Android 构建设置
重要提示
注意,配置和输出目录使用管道变量。 这些参数可以在Pipeline 配置页面的Variables部分中定义。
-
Select a keystore file so that you can sign the application package.
未签名的应用包不能在真正的 Android 设备上运行。 密钥存储库是一个存储库,其中包含将用于对应用包进行签名的证书。 如果你正在使用 Visual Studio 进行开发(在 Mac 或 Windows 上),生成临时发行证书的最简单方法是使用 Archive Manager:
![Figure 14.10 – Creating a distribution certificate]()
图 14.10 -创建发布证书
-
Once the store has been created, the
keystorefile can be found in the following folder on Mac:~/Library/Developer/Xamarin/Keystore/{alias}/{alias}.keystore它可以在 Windows 的以下文件夹中找到:
C:\Users\{Username}\AppData\Local\Xamarin\Mono for Android\Keystore\{alias}\{alias}.keystore -
Now, use the
.keystorefile to complete the signing step of our Android build pipeline:![Figure 14.11 – Signing the Xamarin.Android Package]()
图 14.11 -签名 Xamarin。 安卓包
注意,管道将
.keystore文件作为安全文件使用。 以类似的方式,密钥存储库密码可以(应该)存储为安全变量字符串。 -
管道现在已经准备好了。 编译 Android 版本的应用,创建一个 APK 包。
接下来,我们需要为 iOS 平台准备一个类似的管道。
Xamarin 的。 iOS 管道
类似于 Android 版本的 Xamarin。 iOS 模板创建了编译 iOS 项目的完整管道。 我们需要修改已创建任务的参数,以便成功地准备应用包。 让我们开始:
-
Before we start the pipeline configuration, head over to the Apple Developer site to generate a distribution certificate, Application ID, and an ad hoc provisioning profile:
![Figure 14.12 – Creating a Distribution Certificate]()
图 14.12 -创建发行证书
-
选择分发证书选项,并让开发人员站点指导您完成生成 CSR 和生成签名证书的步骤。
-
创建证书之后,下载并安装证书,以便您可以将其导出为一个公钥/私钥对(
.p12)。 你需要遵循以下步骤: -
打开“Keychain Access”工具。
-
标识我们已经下载和安装的发行版证书。
-
展开证书,从而显示私钥。
-
Select both the public certificate and private key so that you can use the Export option.
一旦我们有了发布证书,我们将需要一个应用 ID 来生成一个配置配置文件。 在生成应用 ID 时,重要的决策是决定是使用通配符证书(这可能是在多个应用的预发布版本中使用的一个好选项)还是使用完整的资源标识符。
-
最后,为特别分发创建应用配置文件。 特别发行是通过 App Center 进行预发行的最合适的发行选择。
-
With the P12 certificate we've exported and the mobile provisioning profile that we have generated and downloaded from the Apple Developer site, head over to Azure DevOps and modify the Install an Apple certificate and Install an Apple provisioning profile tasks:
![Figure 14.13 – Installing our Provisioning Profile and Certificate]()
图 14.13 -安装我们的配置文件和证书
最后,重要的是确保应用包是为真实的设备创建的,而不是一个模拟器。
-
在 Xamarin 中选择以下设置。 iOS 建设任务:

图 14.14 - Xamarin iOS Build Setup
这里,可以将签名标识和配置概要 UUID框留空,因为这些元素将由管道安装。 如果管道中存在多个概要文件或证书,则需要定义一个要使用的特定概要文件或证书。
重要提示
Apple 临时发布配置文件需要允许使用该应用的分布式版本的设备的 UUID。 简单地说,使用和测试这个版本的应用所涉及的任何设备都应该在这个配置文件中注册,并且应用应该与它签署。
这最终完成了 Xamarin Android 和 Xamarin iOS 构建管道的设置。 现在,我们可以生成应用包,以便准备分发它们。 然而,我们没有考虑这些设置中特定于环境的配置。 让我们看一下特定于环境配置的一些可能的解决方案。
环境相关配置
从配置的角度来看,本地应用与 web 应用不同,因为应用 CI 管道应该将配置参数嵌入到应用包中。 虽然可以使用不同的技术来管理不同环境的配置参数,例如单独的 JSON 文件、编译常量等等, 这些实现的共同点是,它们都使用条件编译或编译常量来确定在应用包中包含哪些配置参数。 换句话说,如果不重新编译应用,就不可能更改应用特定于环境的配置。
要创建指向不同服务端点的多个分发环,应用将需要构建具有多个配置的不同单管道,或者我们需要创建多个管道来为特定平台和配置构建应用。
创造和使用人工制品
为了增加跨项目的可重用性,我们可以使用 Azure DevOps 的包管理扩展。 跨 Xamarin 项目的 UI 组件,以及在 Azure 项目和客户端应用之间共享的 DTO 模型,可以通过它们自己的生命周期(开发-合并-编译-部署)合并到 NuGet 包中。
将这些模块存储在同一个 Azure DevOps 项目的单独 Git 存储库中的单独项目/解决方案中,可以更容易地集成到之前创建的构建中。
一旦 NuGet 项目准备好编译并打包一个已定义的.nuspec文件,就可以设置一个单独的 DevOps 管道来创建这个包,并将其推送到同一个团队项目的内部提要中。
一个 NuGet 构建管道示例将类似于:

图 14.15 - NuGet 管道
为了在 Xamarin iOS 和 Android 管道中包含这个提要,需要配置 NuGet Restore 步骤来包含内部提要,以及Nuget.org源。 此外,可以将Nuget.org设置为内部提要的上游源,以便将公共包缓存在内部提要中。
在本节中,我们分析了多种平台的各种管道选项,包括 Xamarin。 Android 和 Xamarin 的。 iOS,以及 NuGet 包。 这些管道是持续集成和交付管道的基石,因为它们提供了要分发的构件。
Xamarin 应用中心
Visual Studio AppCenter 在其前身 HockeyApp 及其特性集的基础上进行了扩展,是一个移动应用生命周期管理平台,用于轻松地构建、测试、分发和收集来自 iOS、Android、Windows 和 macOS 应用的遥测数据。 它与各种存储库选项和构建功能的内在集成甚至可以用于从 Azure DevOps 迁移开发和发布管道。 Visual Studio App Center,就像 Azure DevOps 一样,遵循免费订阅模式,开发者可以通过免费订阅访问它的大部分功能; 他们需要付费订阅某些功能的配额增强。
与源存储库和构建集成
尽管我们已经在 Azure DevOps 及其相关的构建管道上建立了我们的源存储库,但 App Center 也可以用于同样的目的。
例如,如果我们要建立 iOS 构建管道,我们将遵循以下步骤:
-
Start by creating an application within our organization. An application on App Center also represents a distribution ring:
![Figure 14.16 – Creating a New App on App Center]()
图 14.16 -在 App Center 上创建一个新的 App
-
Once the application has been created, in order to create a build, connect the App Center application to the target repository. Just like Azure DevOps pipelines, you are free to choose between Azure DevOps, GitHub, and Bitbucket:
![Figure 14.17 – Selecting the Source Repository]()
图 14.17 -选择源存储库
-
Now that the repository is connected, start creating a build that will retrieve the branch content from the source repository and compile our iOS package:
![Figure 14.18 – Setting up our App Center Build]()
图 14.18 -设置我们的 App Center 构建
-
一旦构建完成,就可以手动构建,也可以将其配置为 CI 构建,每次有一个推送到源分支(在本例中是主分支)时都会触发 CI 构建。
在本节中,我们创建了一个应用注册,并将来自 Azure DevOps 的源存储库与其关联起来,以便可以利用 app Center 构建。 接下来,我们将在我们的应用注册上创建分发环,这样我们在 Azure DevOps 上的 CI 管道就可以向 App Center 提供包进行分发。
设置配电环
自从我们开始为我们的应用建立 ALM 管道以来,我们就一直在中提到 App Center 的分销环。 正如我们所见,分销圈指的是在你的个人或组织账户上创建的应用。 这个环代表应用上特定于环境(例如,Dev)的编译或特定平台(例如,iOS)。
App Center 应用通过所谓的应用段塞来表示。 应用段塞可以从 App Center 页面的 URL 中提取。 URL 语法如下:
-
https://appcenter.ms/users/{username}/apps/{application}:App Slug: {username}/{application} -
https://appcenter.ms/orgs/{orgname}/apps/{application}:App Slug: {orgname}/{application}
如果我们回到我们的 Azure DevOps 管道,我们可以使用这个值来设置部署到 App Center。 然而,在我们这样做之前,我们需要创建一个带有 API 令牌的 App Center 服务连接,您可以从 App Center 检索该令牌。 这将允许 Azure DevOps 授权与 App Center 和推送应用包:

图 14.19 - Azure DevOps 上的 App Center 集成
让我们完成其余的配置参数:

图 14.20 - App Center 分发任务
使用另一个 App Center 应用为 Android 版本重复相同的步骤将完成我们应用的初始 CI 设置。 与 App Center 构建类似,这个构建可以设置为每次合并到开发或主分支时触发。
重要提示
在解决方案文件夹(即存储库中)中存储降价表(例如ReleaseNotes.md)以记录对应用的更改是非常方便的。 在每个 pull 请求中,当开发人员将更新输入到这个文件中,关于正在部署的更改的发布说明可以很容易地推送到 alpha 和 beta 发行渠道中。
在本节中,我们成功地在 App Center 中创建了应用注册,并创建了简单的构建管道,以演示其功能。 最后,我们将之前准备好的 Azure DevOps 管道集成到 App Center 应用注册中,作为分发环使用。 在下一节中,我们将重点关注 App Center 发行版。
应用中心配送
除了 App Center 的构建、测试和遥测收集功能外,App Center 的主要功能是,你可以管理预发布应用的分发,以及自动提交到公共和私有应用商店。
App Center 发布
一旦应用包从构建管道被推送到 App Center,一个应用发布就被创建了。 这个版本代表应用包的一个版本。 这个包可以分发到当前分发环内的分发组或外部分发目标:

图 14.21 - App Center 发布
当一个发行版被创建后,协作组(即对 App Center 有管理访问权的开发人员)可以访问它。
AppCenter 分发组
发布组是一组开发人员和测试人员,他们可以发布应用的版本(应用的特定环境和平台版本):

图 14.22 - App Center 分布组
分发组是非常有价值的,因为它们为不同的分发环提供了额外的阶段。 例如,一旦一个发布版本从 Azure DevOps 推送到 App Center,第一个发行组就可以在允许第二个发行组访问这个新版本之前验证该应用。 通过这种方式,来自各种管道的自动发布可以在 alpha 和 beta 通道上交付给特定的目标组。
此外,如果你导航到一个 iOS 应用环上的协作者组详细信息,你可以确定哪些设备目前包含在配置文件中:

图 14.23 - App Center 设备注册
对于注册设备,App Center 支持为 iOS 版本自动发放设备。 在这种设置中,每次在分发组中注册一个新设备时(假设配置了自动配置),App Center 将更新 Apple Developer Portal 上的配置配置文件,并使用新的配置配置文件退出发布包。
App Center 分发到生产
一旦应用通过下环认证(即 alpha 和 beta), App Center 发布就可以推进到生产阶段。 生产阶段可以是目标公共的 App Store(例如,iTunes Store,谷歌 Play Store 等),或者可以使用移动设备管理或移动应用管理(例如,Microsoft Intune)将应用发布给用户。
要将 iTunes Store 设置为目标商店,你需要在 App Center 中添加一个 Apple Developer 账户。 类似地,如果你的目标是 InTune,一个管理员帐户应该添加到你的 App Center 集成:

图 14.24 - App Center 对商店的分布
需要注意的是 App Store Connect 提交不会绕过苹果商店对应用包的验证——这只是一个通常通过 Xcode 处理的切换过程。
App 中心遥测诊断
App Center 提供先进的遥测和诊断选项。 要开始使用这些监控功能,应用 Center SDK 需要安装在应用上,并为所有目标平台初始化。 按照以下步骤来学习:
- 从公共 NuGet 商店安装 NuGet 包。 使用包管理器上下文,如下所示:
- 在本例中,我们正在创建 Xamarin。 表单应用,因此初始化不需要是特定于平台的:
- 一旦 App Center SDK 被初始化,应用将启用默认的遥测信息和崩溃跟踪。 这个遥测信息可以扩展自定义指标和事件遥测 usi 在 SDK 中可用的功能:

图 14.25 - App Center 遥测采集
在这些遥测信息之上,App Center 允许你跟踪两种类型的错误信息:应用崩溃和错误。 应用崩溃信息与导致应用崩溃的遥测事件一起记录,让开发者可以轻松地排除问题。
此外,遥测信息可以推送到 Application Insights,以便在 Azure 门户上进行分析。
总结
在本章中,我们设置了初始的构建管道,这将在接下来的章节中展开。 我们还讨论了 Azure DevOps 和 Visual Studio App Center 上可用的 ALM 特性,以及如何有效地结合使用这两个平台。 根据团队的规模和应用类型,可以在这些平台上实现许多不同的配置,从而为开发人员提供了一个自动化且易于使用的开发管道。
在下一章中,我们将学习如何监控我们的移动应用,以及与 Azure application Insights 一起使用的 Azure 资源。
十五、应用遥测与应用洞察
敏捷应用生命周期管理规定,在应用发布后,应该将性能和用户反馈引入开发周期。 反馈信息可以为如何从业务和技术角度改进应用提供重要的线索。 Application Insights 可以从使用 Azure 托管的 web 服务基础设施的 Xamarin 应用中收集遥测数据,因为它与 Azure 模块进行了内在集成,并且具有用于 App Center 遥测的持续导出功能。
在本章中,我们将分析两种不同的遥测源,即 App Center 和 Application Insights 的数据模型和利用。 我们将看看 sdk 以及这些平台为用户遥测提供了什么。 以下章节将指导你完成本章:
- 收集 Xamarin 应用的见解
- 为 Azure 服务收集遥测数据
- 分析数据
在本章的最后,您将能够将 App Center 遥测客户端集成到您的 Xamarin 应用中,并收集有价值的用户遥测数据。 您还将学习如何将这些遥测数据迁移到 Application,并使用高级查询模型和可视化对其进行分析。
收集 Xamarin 应用的见解
在本节中,我们将重点讨论 App Center 遥测技术以及如何扩展用户遥测的范围。
正如我们之前讨论和设置的,Xamarin 应用中的应用遥测是通过 App Center SDK 收集的。 该应用数据虽然提供了关于应用使用模式的关键信息,但不能在 App Center 中进一步分析。 我们首先需要将 App Center 标准以及自定义遥测数据导出到 Azure Application Insights 资源中,以便可以使用查询语言执行进一步的分析。
遥测数据模型
通过使用 App Center SDK,可以与事件一起收集遥测信息,事件可以包含关于特定用户操作或应用执行模式的附加信息。 这些额外的数据点,也被称为维度,通常用于给用户提供用于执行触发遥测事件的函数的数据的快速快照。 简单地说,遥测事件可以描述为事件名称和该事件的其他维度。 让我们从创建遥测模型开始:
-
First, create a telemetry event, as follows:
public abstract class TelemetryEvent { public TelemetryEvent() { Properties = new Dictionary<string, string>(); } public TelemetryEvent(string eventName) : this() { Name = eventName; } public string Name { get; private set; } public virtual Dictionary<string, string> Properties { get; set; } }重要提示
任何自定义事件都将包含标准的跟踪元数据,例如操作系统版本、请求的地理区域、设备模型、应用版本,等等。 不需要将这些属性记录为附加维度。 属性应用于特定于事件的数据。
-
现在,使用 App Center SDK 实现遥测 writer:
-
为了扩展事件定义以包含
Exception属性,我们可以为错误添加额外事件。 换句话说,我们可以跟踪应用中处理的异常:public void TrackEvent(TelemetryEvent @event) { if(@event.Exception != null) { TrackError(@event); return; } Analytics.TrackEvent(@event.Name, @event.Properties); } public void TrackError(TelemetryEvent @event) { Crashes.TrackError(@event.Exception, @event.Properties); } -
现在,开始创建定制事件并开始记录遥测数据。 我们的初始事件可能是登录事件,这通常是用户会话的起始位置:
public class LoginEvent: TelemetryEvent { public LoginEvent() : base("Login") { Properties.Add(nameof(Result), string.Empty); } public string Result { get { return Properties[nameof(Result)]; } set { Properties[nameof(Result)] = value; } } } -
登录后,用户将导航到主仪表板,因此我们将以类似的方式记录我们的仪表板出现事件:
protected override void OnAppearing() { base.OnAppearing(); App.Telemetry.TrackEvent(new HomePageEvent() { LoadedItems = ViewModel.Items.Count.ToString() }); } -
Additionally, define navigation to the details view, where various actions can be executed by the user:
protected override void OnAppearing() { base.OnAppearing(); App.Telemetry.TrackEvent(new DetailsPageEvent() { SelectedItem = viewModel.Title }); }重要提示
App Center 可以跟踪的事件结构有一定的限制。 自定义事件名称最多不能超过 200 个。 此外,事件名称的长度限制为 256 个字符,而属性名称不能超过 125 个字符。
-
结果元数据现在可以在 App Center 仪表盘板上显示:

图 15.1 - App Center 数据可视化
到目前为止,在本节中,我们已经使用自定义事件创建了一个小型页面跟踪器。 在本节的下一部分中,我们将尝试组织应用交付的遥测事件。
高级应用遥测
在前面的示例中,我们为遥测编写器实例使用了静态访问器。 然而,这个实现可能会导致严重的架构问题,其中最重要的是我们将应用类与 App Center SDK 的具体实现耦合。
为了解决可能出现的架构问题,让我们创建一个代理遥测容器,将遥测请求转移到目标遥测写入器。 为此,遵循以下步骤:
-
首先创建一个
ITelemetryWriter接口来抽象我们的 App Center 遥测处理程序:public interface ITelemetryWriter { string Name { get; } void TrackEvent(TelemetryEvent @event); void TrackError(TelemetryEvent @event); } -
Now, create a proxy container:
public class AppTelemetryRouter : ITelemetryWriter { // Removed for Brevity public static AppTelemetryRouter Instance { get { if(_instance == null) { _instance = new AppTelemetryRouter(); } return _instance; } } public void RegisterWriter(ITelemetryWriter telemetryWriter) { if(_telemetryWriters.Any(tw=>tw.Name == telemetryWriter.Name)) { throw new InvalidOperationException($"Already registered Telemetry Writer for {telemetryWriter.Name}"); } _telemetryWriters.Add(telemetryWriter); } public void RemoveWriter(string name) { if(_telemetryWriters.Any(tw => tw.Name == name)) { var removalItems = _telemetryWriters.First(tw => tw.Name == name); _telemetryWriters.Remove(removalItems); } } public void TrackEvent(TelemetryEvent @event) { _telemetryWriters.ForEach(tw => tw.TrackEvent(@event)); } public void TrackError(TelemetryEvent @event) { _telemetryWriters.ForEach(tw => tw.TrackError(@event)); } }同样重要的是,容器应该在定义视图模型的跨平台项目上创建,因为大多数诊断遥测实际上是在视图模型而不是视图本身上收集的。 这个实现的另一个优点是,我们现在可以为不同的平台定义多个遥测编写器,比如 Firebase、Flurry Analytics 等等。
虽然抽象的遥测事件类提供了基本数据,但它不提供关于执行时间的任何指标。 例如,如果我们正在执行一个远程服务调用或一个长时间运行的操作,执行时间可能是一个有价值的维度。
-
为了收集这个特定的度量,创建一个额外的遥测对象:
public abstract class ChronoTelemetryEvent : TelemetryEvent { public ChronoTelemetryEvent() { Properties.Add(nameof(Elapsed), 0.ToString()); } public double Elapsed { get { return double.Parse(Properties[nameof(Elapsed)]); } set { Properties[nameof(Elapsed)] = value.ToString(); } } } -
现在,创建一个跟踪器对象,它将跟踪需要执行时间度量的事件的执行时间:
public class TelemetryTracker<TEvent> : IDisposable where TEvent : ChronoTelemetryEvent { private readonly DateTime _executionStart = DateTime.Now; public TelemetryTracker(TEvent @event) { Event = @event; } public TEvent Event { get; } public void Dispose() { var executionTime = DateTime.Now - _executionStart; Event.Elapsed = executionTime.TotalMilliseconds; // The submission of the event can as well be moved out of //the tracker AppTelemetryRouter.Instance?.TrackEvent(Event); } } -
现在,使用我们的跟踪器对象,我们可以在应用中收集关于时间敏感操作的有价值的信息:
public async Task LoadProducts() { using (var telemetry = new TelemetryTracker<ProductsRequestEvent>(new ProductsRequestEvent())) { try { var result = await _serviceClient.RetrieveProducts(); Items = new ObservableCollection<ItemViewModel>( result.Select(item => ItemViewModel.FromDto(item))); } catch (Exception ex) { telemetry.Event.Exception = ex; } } } -
The results of the service call are measured with the
Elapsedmetric, which can be observed in the App Center's LogFlow:![Figure 15.2 – App Center Log Flow]()
图 15.2 - App Center 日志流程
-
类似地,跟踪器对象可以在某些视图的
OnAppearing事件上创建,并在OnDisappearing事件上处理,这样我们就可以跟踪用户在某个视图上花费了多少时间。
虽然 App Center 提供了一种方便的方法来收集遥测数据,但为了执行对用户模式的分析并排除应用问题,我们可以将遥测数据导出到 application Insights。
导出 App Center 遥测数据到 Azure
此时,应用正在收集遥测数据并将其推送到 App Center。 但是,正如我们前面讨论的,您将不能进一步分析这些数据——特别是自定义事件维度。
为了做到这一点,我们需要创建一个新的 Application Insights 资源,并设置一个连续的导出,以便 App Center 遥测数据可以导出为 Application Insights 数据。 让我们开始:
-
Start this process by creating the Application Insights resource:
![Figure 15.3 – Export to Application Insights]()
图 15.3 -导出到应用洞察
注意,对于应用类型,我们选择了App Center 应用。 作为一个资源组,我们将使用与前面创建的 Azure 服务相同的资源组,以便可以将完整的 Azure 基础设施部署在一起。
-
Once the resource is created, go to the Overview section to find the instrumentation key. This key is the only requirement for setting up the continuous export process:
![Figure 15.4 – Application Insights for Xamarin Telemetry]()
图 15.4 - Xamarin 遥测技术的应用观察
-
On App Center, in order to set up the export, navigate to the Settings section and select Export and Application Insights on the data export window.
在此视图中,您可以使用设置标准导出选项,当将 Azure 订阅配置为与 App Center 一起使用时使用该选项。 选择标准导出需要管理员访问 Azure 订阅,并创建一个新资源。 您还可以选择Customize并粘贴到我们从 Azure 门户获得的仪表键中。
-
在此设置之后,事件遥测将定期推送到Application Insights,可以在 Azure 门户中使用标准分析部分或使用查询语言进行分析:

图 15.5 -应用观察用户遥测
本节主要讨论 App Center 遥测技术。 我们从使用自定义事件的基本用户遥测开始,扩展到高级操作诊断数据。 如您所见,通过对遥测模型进行小的调整,您可以获得对常见用户流的宝贵见解,这有助于引导您的应用走向成功。 现在我们已经收集了应用中的遥测数据并导出到 application Insights,我们可以开始为 Azure 上的其余模块创建 application Insights 基础设施了。
为 Azure 服务收集遥测数据
与移动应用相比,Application Insights 与基于 azure 的服务的集成要更紧密一些。 除了可以为诸如 Azure App Service 等的服务和诸如 Azure 函数等无服务器组件收集的各种标准的开箱即开的遥测技术之外,还可以实现定制的遥测技术、跟踪和度量收集实现。
应用洞察数据模型
遥测采集可以分为三大类:
- 痕量:痕量可以识别为最简单的遥测形式。 跟踪元素通常给出事件的标称描述,并用作诊断日志,类似于其他平面文件诊断日志实现。 除了主要的遥测信息外,还可以定义严重性级别和其他属性。 跟踪消息的大小限制比其他遥测类型要大得多,并且提供了提供大量诊断数据的方便方法。 当在。net 5.0 应用中使用标准日志扩展时,也会使用跟踪模型。
- 事件:Application Insights 事件与 App Center 遥测项目非常相似,在将 App Center 数据导出到 Application Insights 后,处理方式相同。 除了名义维度之外,还可以将其他度量数据发送到 Application Insights。 一旦收集了数据,
customProperties集合提供了对描述性维度的访问,而customMeasurements字典用于访问度量。 - 度量:它们通常是预先聚合的标量度量。 如果您正在处理自定义指标,则应用有责任使其保持最新。 Application Insights 客户端为标准和定制指标提供了标准化的访问方法。
除了特定于事件的遥测数据类型外,还可以使用 Application Insights 跟踪特定于操作的数据类型,例如请求、异常和依赖关系。 这些数据被归为更广义的宏观级遥测数据,它们可以提供关于应用基础结构健康状况的有价值的信息。 默认情况下,通常会跟踪请求、异常和依赖数据,但也可以自定义/手动实现。
利用 ASP 技术采集遥测数据.NET Core
应用洞察可以很容易地使用 Visual Studio 对任何 ASP 进行初始化.NET Core web 应用。 在我们的例子中,我们将配置 web API 层来使用 Application Insights。 让我们看看如何做到这一点:
-
首先右键单击 web 应用项目,然后选择Add | application Insights Telemetry,然后单击Get Started链接。 Visual Studio 自动加载与您的帐户相关联的资源,允许您选择订阅以及资源/资源组对。
-
Use the Configure Settings... option on this page to assign an already existing resource group; otherwise, the Application Insights instance will be created on a default Application Insights resource group:
![Figure 15.6 – Application Insights Configuration]()
图 15.6 -应用洞察配置
在Application Insights配置完成后,我们可以看到如何收集遥测数据,而无需将 web API 部署到 Azure App Service 资源中。
-
In order to see the collected telemetry data, you can start a debugging session. Select View | Other Windows | Application Insights to open the live Application Insights data that is collected within the debug session:
![Figure 15.7 – Application Insights Debug Session Telemetry]()
图 15.7 -应用观察调试会话遥测
需要注意的是,数据被设置为使用调试会话遥测。 Application Insights Search工具集还可以用于读取远程遥测数据并对实时数据执行快速搜索查询。
另一个有用的工具窗口是Application Insights Trends,它是针对各种应用遥测数据类型(如请求、页面视图、异常、事件和依赖项)的快速报告工具。
同样的遥测集,即使这是一个调试会话,也应该已经在 Azure 门户上可用了。 换句话说,Application Insights 不需要部署 Azure 就可以对服务器资源进行分析,并对其进行遥测跟踪。
-
现在,如果导航到 Application Insights 概览页面,您将注意到关于请求的传入数据和收集到的遥测数据。
-
Next, navigate to the live metrics screen using the side panel navigation. The live metrics screen can provide information about the server's performance and aggregated metrics data. In order to use profiling and performance data, update the Application Insights SDK to a version higher than 2.2.0 so you can see this:
![Figure 15.8 – Application Insights Live Metrics Stream]()
图 15.8 -应用洞察实时指标流
现在已经设置了 Application Insights 基础设施,我们可以开始创建定制的遥测和跟踪数据了。
-
为产品检索 API 操作创建一个新的操作上下文,并包括一些额外的遥测数据:
using (var operationContext = _telemetryClient.StartOperation<RequestTelemetry>("getProducts")) { var result = Enumerable.Empty<Product>(); _telemetryClient.TrackTrace("Creating Document Client", SeverityLevel.Information); using (var document = GetDocumentClient()) { try { _telemetryClient.TrackTrace("Retrieving Products", SeverityLevel.Information); result = await document.Retrieve(); } catch (Exception ex) { _telemetryClient.TrackException(ex); operationContext.Telemetry.ResponseCode = "500"; throw ex; } } operationContext.Telemetry.ResponseCode = "200"; return Ok(result); } -
Now, the resultant telemetry collection that contains trace entries is automatically grouped to the operation context, thus providing more meaningful information:
![Figure 15.9 – Application Insights Operation Context]()
图 15.9 -应用观察操作上下文
我们可以通过分离依赖遥测来进一步细化这些遥测数据。 例如,在这个实现中,我们调用数据提供程序客户机来加载所有产品。
-
使用遥测客户端,为这个请求创建一个依赖遥测:
using (var document = GetDocumentClient()) { var callStartTime = DateTimeOffset.UtcNow; try { _telemetryClient.TrackTrace("Retrieving Products", SeverityLevel.Information); result = await document.Retrieve(); } finally { var elapsed = DateTimeOffset.UtcNow - callStartTime; _telemetryClient.TrackDependency( "DataSource", "ProductsDB", "Retrieve", callStartTime, elapsed, result.Any()); } } -
现在,对文档源单独跟踪应用遥测。 这个依赖甚至是在 Azure 门户上的应用地图上创建的:

图 15.10 - Application Insights Application Map
在正常情况下,对 Cosmos DB 和 SQL 等资源的依赖调用会分别自动检测和跟踪。 前面的实现适合外部依赖或遗留系统。
正如您在本部分所看到的,从 ASP 收集定制的遥测技术.NET 5 应用主要依赖于使用遥测客户端和通过 SDK 提供的特性。 大多数依赖遥测,以及异常,已经由客户端自动收集。 现在让我们转向 Azure 函数。
利用 Azure 功能采集遥测数据
当我们谈到自定义跟踪时,从 Azure 函数中收集 Application Insights 遥测数据与使用任何其他. net 应用没有什么不同。 在创建 Azure 函数时,我们在方法中注入了一个TraceWriter实例。 TraceWriter日志是诊断遥测的主要来源,是在 Azure 功能中收集的。 通过host.json设置可以根据日志级别对这些日志条目进行过滤:
{
"logger": {
"categoryFilter": {
"defaultLevel": "Information",
"categoryLevels": {
"Host.Results": "Error",
"Function": "Error",
"Host.Aggregator": "Information"
}
},
"aggregator": {
"batchSize": 1000,
"flushTimeout": "00:00:30"
}
},
"applicationInsights": {
"sampling": {
"isEnabled": true,
"maxTelemetryItemsPerSecond" : 5
}
}
}
类别级别中的 Function 部分引用在函数中收集的跟踪。 Host.Results是自动收集的请求/结果遥测数据对,而聚合器数据中充满了函数的主机默认收集的聚合指标,每 30 秒收集一次或 1,000 个结果。 然后使用它来计算聚合度量,例如计数、成功率等等。
除了基本的遥测实现之外,您还可以使用 Application Insights 遥测客户端修改默认的遥测数据。 在这种情况下,遥测客户端用于修改操作上下文,而不是创建一个新的TrackRequest。
如您所见,遥测客户机在此实现中更像中间件而不是真相源,只是修改现有操作上下文并创建额外的事件数据。
在本节中,我们研究了不同的遥测模型,以及如何在 ASP 中使用它们.NET 应用以及 Azure 无服务器组件。 我们使用遥测客户端在我们的 web 服务中提供定制的遥测数据,同时在我们的 Azure 功能中使用 Application Insights 的标准配置。 现在我们已经在移动和网络平台上收集了足够的遥测数据,我们可以开始分析这些数据集了。
数据分析
现在我们已经在服务器端和应用端建立了 Application Insights 遥测收集,我们可以尝试理解这些数据。
虽然 Azure 门户提供了对应用遥测数据的快速洞察,但如果我们想真正深入研究应用数据,应该使用 application insights 门户进行分析。 在 Application Insights 门户中,可以使用查询语言分析数据。 查询语言,也称为 Kusto 语言,提供高级只读查询特性,可以帮助组织来自多个源的数据,并提供对应用的性能和使用模式的有价值的见解。
例如,让我们看看下面的简单查询,它是在我们的 Xamarin 遥测数据上执行的。 我们将返回从 App Center 导出的前 50 个自定义事件:
customEvents
| limit 50
这些遥测条目在根目录中包含了与遥测相关的一般数据:

图 15.11 -应用观察遥测详细信息
而customDimensions对象提供更多的 xamarin 特定数据:

图 15.12 -应用观察自定义维度
最后,customDimensions的Properties节点提供使用AppCenter遥测客户端发送的实际自定义遥测数据。 经过一些预处理后,我们可以将这个数据纳入我们的分析:
-
在深入研究特定于事件的数据之前,请使用遥测事件名称
customEvents | where name == "ProductsServiceRequest"过滤遥测事件。
-
然后,根据时间戳对表进行排序:
| order by timestamp desc nulls last -
通过将属性数据反序列化为一个动态字段,在我们的查询中使用获取的数据:
-
为了扁平化表结构,将属性数据分配到其自己的字段:
| extend Duration = todouble(Properties.elapsed), OperatingSystem = customDimensions.OsName -
最后,将数据投影到一个新表中,这样我们就可以用更简单的结构来表示它:
| project OperatingSystem, Duration, timestamp -
Now, the data from our telemetry events is structured and can be presented in reports and troubleshooting:
![Figure 15.13 – Operating System Projection]()
图 15.13 -操作系统投影
-
为了更进一步,我们还可以使用最后的表格绘制一个图表,如下:
| render timechart -
这将绘制一个带有持续时间值的折线图。 您还可以通过使用
summarize函数和多个聚合函数对数据进行排序,例如将事件分组到每小时的容器中,并绘制基于时间的条形图:customEvents | order by timestamp desc nulls last | extend Properties = todynamic(tostring(customDimensions.Properties)) | summarize event_count=count() by bin(timestamp, 1h) | render barchart
Application Insights 数据和可用的查询操作符和方法为开发人员提供了无数方法,通过从登台环境或生产环境收集宝贵的应用数据并将其反馈给应用生命周期,从而主动地对应用遥测进行操作。
总结
从服务器端和客户端收集的应用遥测数据可以提供根据用户需求改进和塑造应用所需的信息。 在某种程度上,通过从实时环境中应用的各个模块收集应用遥测数据,使用实际的用户数据执行实时应用测试。 这些遥测数据可以提供其他自动化测试无法提供的应用洞察。 无论数据是真实的还是合成的,单元测试以及自动化 UI 测试都应该是应用生命周期的一部分。
在下一章中,我们将研究各种测试方法,以及如何在开发管道中包含这些测试。
十六、自动测试
大多数开发人员通常认为单元和编码 UI 测试是应用项目生命周期中最单调的部分。 然而,提高单元测试代码覆盖率和创建自动化 UI 测试可以帮助节省大量开发人员的时间,否则这些时间将花费在维护和回归上。 特别是对于具有更长的生命周期的应用项目,项目的稳定性直接与测试自动化的水平相关。 本章将讨论如何创建单元和编码 UI 测试,以及围绕它们的架构模式。 数据驱动的单元测试、模拟和 Xamarin UI 测试是将要讨论的一些概念。
在本章中,我们将重点关注为应用生命周期的不同阶段创建不同类型的测试。 在了解在 Xamarin 的范围内设置单元测试和执行策略之前,我们将从单元测试开始本章。 然后,我们将转向集成测试和自动化 UI 测试,它们与单元测试一起,使开发人员能够在整个交付管道中检查他们的应用。
下面的主题将带你了解如何实现一个自动验证的应用开发管道:
- 用测试维护应用的完整性
- 使用集成测试维护跨模块的完整性
- 自动化 UI 测试
在本章结束时,您将能够有效地使用 mock 和 fixture,同时创建战略准备的单元测试。 您还将了解集成和自动化测试背后的动机和好处。
用测试维护应用的完整性
本节的重点将放在单元测试以及用于 Xamarin 应用单元测试的策略和工具上。 我们将研究单元测试的基础知识,以及模拟和夹具的概念。
无论是开发平台还是运行时平台,单元测试都是开发管道中不可或缺的一部分。 事实上,如今,测试驱动开发(TDD)是最突出的开发方法,是任何敏捷开发团队的最佳选择。 在这个范例中,开发人员负责创建适合当前正在开发的单元的单元测试,甚至在编写实际业务逻辑实现的第一行之前就已经负责了。
安排,行动和断言
闲话少说,让我们看看应用中的第一个视图模型,并为它实现一些单元测试。 产品视图模型是一个简单的视图模型,在初始化时,它使用可用的服务客户端加载产品数据。 它暴露了两个属性; 即Items集合和ItemTapped命令。 利用这些信息,我们可以确定单位。
应用的单元可以通过实现简单的存根来识别,如下面的代码所示:
public class ListItemViewModel : BaseBindableObject
{
public ListItemViewModel(IApiClient apiClient, INavigationService
navigationService)
{
//...Load products and initialize ItemTapped command
}
public ObservableCollection<ItemViewModel> Items { get; set; }
public ICommand ItemTapped { get; }
internal async Task LoadProducts()
{
// ...
var result = await _serviceClient.RetrieveProductsAsync();
// ...
}
internal async Task NavigateToItem(ItemViewModel viewModel)
{
// ...
}
}
我们的初始单元测试将为apiClient设置模拟,构造视图模型,验证RetrieveProductsAsync是否在服务客户端上被调用,并验证ItemTapped命令是否被正确初始化。 可以执行附加检查,以查看Items属性上是否触发了PropertyChanged事件。 在单元测试的上下文中,简单单元测试的这三个步骤通常被称为 AAA 或 AAA -Arrange, Act, Assert:
-
在
Arrange部分中,准备一组结果数据,并使用模拟客户端返回数据:#region Arrange var expectedResults = new List<Product>(); expectedResults.Add(new Product { Title = "testProduct", Description = "testDescription" }); // Using the mock setup for the IApiClient _apiClientMock.Setup(client => client.RetrieveProductsAsync()).ReturnsAsync(expectedResults); #endregion -
现在,让我们通过构造视图模型来执行
Act步骤:#region Act var listViewModel = new ListItemViewModel(_apiClientMock.Object); #endregion -
最后,对视图模型目标执行断言:
#region Assert // Just checking the resultant count as an example // Foreach with checking each expected product has a // matching domain entity would improve the robustness of the test. listViewModel.Items.Should().HaveCount(expectedResults.Count()); listViewModel.ItemTapped.Should().NotBeNull() .And.Subject.Should().BeOfType<Command<ItemViewModel>>(); _apiClientMock.Verify(client => client.RetrieveProductsAsync()); #endregion -
现在运行单元测试来检查代码湾的愤怒:

图 16.1 -代码覆盖结果
有了这个简单的单元测试实现,我们已经达到了单元测试代码覆盖率的 80%。
重要提示
xUnit.net 框架被用来实现这个单元测试,或者所谓的事实。 此外,为了简化实现和断言,还使用了FluentAssertions和Moq框架。 这些框架的特性集超出了本书的范围。
实现足够好,可以检查构造函数的初始化。 我们测试的构造函数实现类似如下:
public ListItemViewModel(IApiClient apiClient)
{
_serviceClient = apiClient;
ItemTapped = new Command<ItemViewModel>(async _ => await
NavigateToItem(_));
if (_serviceClient != null)
{
LoadProducts().ConfigureAwait(false);
}
}
然而,请注意,LoadProducts方法实际上是在没有await的情况下调用的,因此它不会合并回初始同步上下文。 在多线程环境中,当并行执行多个单元测试时,可能会执行构造函数; 然而,在异步任务完成之前,断言就开始了。 这可以用穷人的线程同步——Task.Delay或Thread.Sleep来解决。
这个实现不过是一个临时的解决方案。 因为我们不能也不应该真正地在构造函数中等待任务完成,所以我们需要使用服务初始化模式:
public ListItemViewModel(IApiClient apiClient)
{
_serviceClient = apiClient;
ItemTapped = new Command<ItemViewModel>(async _ => await
NavigateToItem(_));
if (_serviceClient != null)
{
(Initialized = LoadProducts()).ConfigureAwait(false);
}
}
internal Task Initialized { get; set; }
现在,我们的Act实现看起来与类似:
#region Act
var listViewModel = new ListItemViewModel(_apiClientMock.Object);
await listViewModel.Initialized;
#endregion
重要提示
注意,我们无法在视图模型级别验证Items属性的PropertyChanged事件触发器。 其主要原因是ListItemsViewModel实例立即执行LoadProducts方法,甚至在我们有机会订阅目标事件之前,它的执行就已经完成了。 这也可以通过我们已经实现的模拟对象中的电路标志来补救,一旦监视器被附加到视图模型上,就释放任务。
要执行这些单元测试以及 IDE 扩展,请使用dotnet控制台命令:
dotnet test --collect "Code Coverage"
这个命令将执行可用的单元测试,并生成一个可以在 Visual Studio 中查看的覆盖率文件:

图 16.2 - DotNet 测试执行
在本节中,我们使用经典 AAA 设置为一个非常基本的视图模型创建单元测试。 然而,当视图模型对平台和应用服务都有大量依赖时,事情很容易变得复杂。 现在,我们已经回顾了单元测试的术语和基本单元测试知识,我们可以继续讨论 mock 了。
使用模拟创建单元测试
在实现单元测试时,隔离当前正在测试的单元是很重要的。 通过隔离,我们指的是模拟测试中当前主题的依赖关系的过程。 根据控制反转模式的实现,可以以各种方式引入这些模拟。 如果实现涉及构造函数注入,我们可以在测试的第一个 A 中模拟依赖接口,并将其传递给目标。 否则,像 NSubstitute 这样的框架可以替换接口,以及主体使用的具体类。
回顾一下我们的视图模型和我们实现的单元测试,您可能已经注意到我们使用 Moq 框架为我们的IApiClient对象创建了一个模拟接口实现。 现在,让我们学习如何使用 Moq 创建单元测试:
-
扩展构造函数,使其接受
INavigationService实例,该实例将用于导航到所选项目的详细信息视图; 换句话说,隔离我们的ItemTapped命令的执行:public ListItemViewModel(IApiClient apiClient, INavigationService navigationService) { _serviceClient = apiClient; _navigationService = navigationService; ItemTapped = new Command<ItemViewModel>(async _ => await NavigateToItem(_)); if (_serviceClient != null) { (Initialized = LoadProducts()).ConfigureAwait(false); } } -
Our navigation command will be as follows:
internal async Task NavigateToItem(ItemViewModel viewModel) { if (viewModel != null && _navigationService != null) { if (await _navigationService.NavigateToViewModel(viewModel)) { // Navigation was successful return; } } throw new InvalidOperationException("Target view model or navigation service is null"); }重要提示
在本例中,我们将抛出一个异常,只是为了演示。 在实际实现中,更好的选择可能是在内部跟踪错误和/或仅在调试模式下抛出异常。 此外,抛出与这些场景中相同类型的异常也不完全是 SOLID。
-
现在,让我们实现我们的单元测试:
[Trait("Category", "ViewModelTests")] [Trait("ViewModel", "ListViewModel")] [Fact(DisplayName = "Verify ListViewModel navigates on ItemTapped")] public async Task ListItemViewModel_ItemTapped_ShouldNavigateToItemViewModel() { #region Arrange _navigationServiceMock.Setup(nav => nav.NavigateToViewModel( It.IsAny<BaseBindableObject>())) .ReturnsAsync(true); var listViewModel = new ListItemViewModel( _apiClientMock.Object, _navigationServiceMock.Object); await listViewModel.Initialized; var expectedItemViewModel = new ItemViewModel() { Title = "Test Item" }; #endregion #region Act listViewModel.ItemTapped.Execute(expectedItemViewModel); #endregion #region Assert _navigationServiceMock.Verify( service => service.NavigateToViewModel(It.IsAny<ItemViewModel>())); #endregion } -
We have implemented the unit test to check the so-called happy path. We can also take this implementation one step further by checking whether the navigation service was called with
expectedItemViewModel:Func<ItemViewModel, bool> expectedViewModelCheck = model => model.Title == expectedItemViewModel.Title; _navigationServiceMock.Verify( service => service.NavigateToViewModel( It.Is<ItemViewModel>(_ => expectedViewModelCheck(_))));为了弥补可能的结果(请记住,我们正在处理视图模型,就好像它是一个确定性有限自动机),我们将需要实现两个场景的导航服务
null和null命令参数,这两个将抛出InvalidOperationException。 -
Now, modify the
Arrangesection of the initial set:var listViewModel = new ListItemViewModel(_apiClientMock.Object, null);在这个特定的情况下,命令(即
ICommand)是由异步任务(即NavigateToItem)构造的。 简单地在命令上调用Execute方法将吞下异常,这意味着我们将无法验证异常。 -
Because of this, modify the execution so that it uses the actual view model method so that we can assert the exception:
#region Act // Calling the execute method cannot be asserted. // Action command = () => listViewModel.ItemTapped.Execute(expectedItemViewModel); Func<Task> command = async () => await listViewModel.NavigateToItem(expectedItemViewModel); #endregion #region Assert await command.Should().ThrowAsync<InvalidOperationException>(); #endregion注意,在这两个测试用例中,我们仍然使用相同的
IApiClientmock,但没有设置方法。 我们仍然可以执行这个 mock,因为它是使用松散的 mock 行为创建的,它为集合返回类型返回一个空集合,而不是在没有进行适当设置的情况下为方法抛出异常。 -
这使得
ListViewModel上的单元测试代码覆盖率达到了大约 90%,如下面的截图所示:

图 16.3 - Visual Studio 代码覆盖结果
到目前为止,所有的测试都已为视图模型实现。 根据定义,应用中的这些模块与 UI 和平台运行时是分离的。 如果我们要编写以 Xamarin 为目标的单元测试。 表单视图,或者目标视图模型需要运行时组件,运行时和运行时特性需要模拟,因为应用实际上不会在移动运行时上执行,而是在。net Core 运行时上执行。 Xamarin.Forms.Mocks包通过提供 Xamarin 的模拟运行时来填补这一空白。 表单视图可以初始化和测试。
夹具和数据驱动测试
正如您在我们之前实现的测试中可能已经注意到的,编写单元测试最耗时的部分之一是实现的安排部分。 在这一部分中,我们实际上设置了测试目标将使用的被测试系统。 在此设置中,我们的目标是将系统置于已知状态,以便将结果与预期结果进行比较。 这种已知状态也称为fixture。
在这种背景下,固定可以简单模拟容器包含决定性的组件集,定义了被测系统(SUT),或推动的工厂可预测的行为模式。
**例如,如果我们要为我们的ListItemViewModel对象创建一个 SUT 工厂,我们可以通过向 fixture 注册两个依赖项来实现。 让我们开始:
-
通过初始化 fixture 并添加
AutoMoqCustomization:_fixture = new Fixture(); _fixture.Customize(new AutoMoqCustomization());开始实现
-
现在,为这两个服务接口设置模拟并冻结它们(也就是说,注册它们,使它们具有单例生命周期):
-
Now that the mocks have been set up, let's take a look at the
Arrangeblock of our navigation test:#region Arrange var listViewModel = _fixture.Create<ListItemViewModel>(); var expectedItemViewModel = _fixture.Create<ItemViewModel>(); #endregion如我们所见,模拟接口注入已经由
AutoMoqCustomization处理,并且已注册的冷冻样本用于实例。然而,如果我们用来执行测试目标的数据对象对结果的影响如此之大,以至于我们需要一个额外的测试用例,那么是什么呢? 例如,导航方法可以有两个不同的路径,这取决于视图模型中包含的数据:
if (viewModel.IsReleased) { if (await _navigationService.NavigateToViewModel(viewModel)) { return; } } else { await _navigationService.ShowMessage("The product has not been released yet"); return; }在本例中,我们至少需要
ItemViewModel对象的两种状态(即已释放和未释放)。 实现这一点最简单的方法是使用内联数据而不是夹具,使用提供的内联数据属性:[Trait("Category", "ViewModelTests")] [Trait("ViewModel", "ListViewModel")] [Theory(DisplayName = "Verify ListViewModel navigates on ItemTapped")] [InlineData(true, "Navigate")] [InlineData(false, "Message")] public async Task ListItemViewModel_ItemTapped_ShouldNavigateToItemViewModel( bool released, string expectedAction) -
使用数据的内联提要,创建一个使用内联数据提要创建
ItemViewModel数据项的编写器:var expectedItemComposer = _fixture.Build<ItemViewModel>() .With(item => item.IsReleased, released); var expectedItemViewModel = expectedItemComposer.Create(); -
现在,只需确保您验证了正确的
navigationService方法被执行:
这样,ItemTapped命令的两种结果实际上都包含在单元测试中。
正如我们在 AAA 描述中看到的,单元测试仅仅是建立一个单元来测试它,并将其所有的依赖项隔离,执行单元,然后验证其结果。 尽管对于快节奏的项目来说,单元测试与模拟和 fixture 的结合可能看起来像是开销,但它可以提供一个有价值的基础。 单元测试是隔离模块的第一道防线。 然而,如果不检查这些模块如何一起工作,我们就会在应用中创建竖井。 下一节提供集成测试的见解。
用集成测试维护跨模块的完整性
大多数时候,当我们处理一个移动应用时,涉及到多个平台,例如作为客户端应用本身,可能是客户端应用的本地存储,以及多个服务器组件。 这些组件可以很好地以最健壮的方式实现,并通过单元测试具有深厚的代码覆盖率。 然而,如果这些组件不能一起工作,那么投入到单个组件中的工作将是徒劳的。
为了确保两个或多个组件能够很好地一起工作,开发人员可以实现端到端或集成测试。 虽然端到端场景通常由自动化 UI 测试覆盖,但集成测试是作为目标系统的一对排列来实现的。 换句话说,我们隔离了两个相互依赖的系统(例如,移动应用和 web API facade),并准备一个 fixture 来准备其余的组件,使它们处于已知状态。 一旦夹具为集成对做好了准备,集成测试的实现与单元测试的实现没有什么不同。
为了演示集成测试的价值,让我们看几个示例。
测试客户机-服务器通信
让我们假设我们有一套单元测试来测试客户端应用的视图模型。我们还实现了单元测试来控制IApiClient实现的完整性,而IApiClient实现是我们与服务层通信的主线。 在第一个套件中,我们将模拟IApiClient,而在后一个套件中,我们将模拟 HTTP 客户机。 在这两个套件中,我们涵盖了所有层,从核心逻辑实现一直到通过传输层发送请求。
此时,下一个业务顺序是编写使用IApiClient的实际实现的集成测试,该集成测试将向服务 API facade(也称为网关)发送服务请求。 然而,我们不能真正使用实际的网关部署,因为服务器端的多个模块将参与到这个通信中,而测试中的系统将太不可预测。
在这种情况下,我们有两个选择:
- 创建一个夹具控制器,该控制器将维护数据库和处于已知状态的其他活动部件(例如,将清理样本数据库并插入需要从中检索的数据的预测试执行)。
- 创建完整网关的临时部署,可能使用模拟模块作为依赖项,并在此系统上执行集成测试。
为了简单起见,让我们使用第一个选项,并假设部署了一个完全空的文档集合来运行集成测试。 在本例中,我们可以调整夹具,以便在预定的文档集合(即服务器端希望找到的文档集合)中注册一组产品,从应用客户机执行检索调用,并清理数据库。
我们将从实现我们的定制夹具开始:
public class DataIntegrationFixture : Fixture
{
public async Task RegisterProducts(IEnumerable<Product> products)
{
var dbRepository = this.Create<IRepository<Product, string>>();
foreach (var product in products)
{
await dbRepository.AddItemAsync(product);
}
this.Register(() => products);
}
public async Task Reset()
{
var dbRepository = this.Create<IRepository<Product, string>>();
var items = this.Create<IEnumerable<Product>>();
foreach (var product in items)
{
await dbRepository.DeleteItemAsync(product.Id);
}
}
}
我们有两种初始方法RegisterProducts和Reset:
RegisterProducts用于在夹具中插入检测数据和登记产品数据。Reset用于清除插入的测试数据。 这样,测试执行将产生相同的结果——至少在数据库级别上是这样。 换句话说,测试的执行将是幂等的。
注意存储库是使用Create方法创建的,这样我们就可以将注入正确的存储库客户端的责任委托给测试计划。
现在,让我们开始进行测试:
-
首先创建测试初始化(即 xUnit 中的构造函数)和测试拆卸(即 xUnit 中的
Dispose方法)。 -
在构造函数中,注册 fixture 将使用的存储库客户端实现,并注册使用此客户端的产品:
public ClientIntegrationTests() { _fixture.Register<IRepository<Product, string>>(() => _repository); var products = _fixture.Build<Product>().With(item => item.Id, string.Empty).CreateMany(9); _fixture.RegisterProducts(products).Wait(); } -
接下来实现
IDisposable接口的Dispose方法。 这将是我们的测试拆卸功能:public void Dispose() { _fixture.Reset().Wait(); } -
现在已经设置了初始化和拆卸过程,我们可以实现我们的第一个测试:
[Fact(DisplayName = "Api Client Should Retrieve All Products")] [Trait("Category", "Integration")] public async Task ApiClient_GetProducts_RetrieveAll() { #region Arrange var expectedCollection = _fixture.Create<IEnumerable<Product>>(); #endregion #region Act var apiClient = new ApiClient(); var actualResultSet = await apiClient.RetrieveProductsAsync(); #endregion #region Assert actualResultSet.Should().HaveCount(expectedCollection.Count()); #endregion }
可以实现类似的测试来测试服务器与数据库或系统的其他组件之间的交互。 关键是控制未被测试的模块,并确保为目标交互执行测试。
实现平台测试
正如我们前面提到的,集成测试不一定是两个相互交互的独立运行时的断言。 它们还可以用于在受控环境中测试应用的两个不同的模块。 例如,在处理移动应用时,某些特性需要与移动平台交互(例如,本地存储 API 实现将使用本地平台文件系统; 甚至核心 SQLite 实现也被抽象为。net core)。
对于必须在特定移动平台(如 iOS、Android 和 UWP)上执行的集成测试,设备。 可以使用 xUnit 框架。 设备。 xUnit 框架由。net Foundation 管理。 作为 SDK 的一部分包含的多项目模板为目标平台和库项目创建测试工具项目。 一旦执行开始,测试将在提供真实或模拟目标平台的测试工具应用上执行,因此允许开发人员在特定于平台的特性上执行集成测试。
无论您是在测试模块之间的集成运行状况还是与外部服务的集成,集成测试都是交付管道中非常宝贵的成员。 在本节中,我们设计了一个示例测试场景,其中 API 客户机从一个远程服务检索数据,该服务具有由单元测试 fixture 控制的一组预先确定的数据。 虽然这种实现可以被理解为系统测试而不是集成,但完整的系统测试指的是在移动设备上执行测试而不隔离任何依赖关系的测试基础设施。 对于移动平台,自动化 UI 测试可以填补这一空白。
自动化 UI 测试
可以说,开发周期中最辛苦和最昂贵的阶段之一是人工认证测试,也称为验收测试。 在一个典型的非自动化验证周期中,认证测试所花费的时间可能比开发某个特性的时间长 2-3 倍。 此外,如果以前实现的特性存在风险,那么就必须执行这些区域的回归。 为了增加发布节奏并减少开发周期,实现自动化 UI(或端到端)测试是必要的。 通过这种方式,自动化管道可以验证一次,并重用来验证应用的 UI 和与其他系统的集成,而不是我们在每个发布周期中执行手动测试。
App Center 允许我们在几个实际设备上执行这些自动化测试,包括在开发管道中自动运行:

图 16.4 - App Center 测试结果视图
Xamarin 的。 UITests 是受支持的自动化框架中的一个,可用于创建这些自动化验收测试。
Xamarin。 UITests
Xamarin 的。 UITests 是一个自动化的 UI 测试框架,它与 Xamarin 目标平台紧密集成。 除了已经使用 Xamarin 框架创建的应用,它还可以用于为使用 Java 和 Objective-C/Swift 创建的移动应用创建自动化测试。 NUnit 与自动化框架一起用于执行断言和创建测试 fixture。
该框架允许开发人员使用查询和操作与移动平台进行交互。 查询可以描述为在IApp接口的当前实例上执行的select命令,而操作是与所选元素模拟的用户交互(即查询的结果)。 IApp接口使这种交互成为可能,它提供了目标平台之间所需的抽象,并促进了用户与它们的交互。
根据目标设备和平台,您可以以各种方式初始化IApp接口(换句话说,模拟交互平台)的实现。
以下是一些例子:
-
初始化应用使用一个 iOS 应用 bundle 可以做如下:
-
初始化它以运行在 iOS 模拟器与一个已经安装的应用可以做如下:
-
对于当前连接到 ADB 的 Android 设备,可以按照以下步骤进行初始化:
IApp app = ConfigureApp.Android.ApkFile("/path/to/android.apk") .DeviceSerial("03f80ddae07844d3") .StartApp();
一旦初始化了IApp实例,就可以使用前面提到的查询和操作执行模拟的用户交互。
可以使用各种可用的选择器编写查询。 最突出的查询如下:
- 标记:指 Xamarin 的
x:Name。 元素,或者带有给定AutomationId对象的元素。 这与本地 UI 实现的方式类似,在 iOS 上使用AccessibilityIdentifies或AccessibilityLabel,在 Android 上使用一个视图的Id、ContentDescription和Text进行查询。 - 类:查询当前 UI 中指定的类名。 它通常与
nameof(MyClass)一起使用。 - Id:这指的是我们试图定位的元素的
Id部分。 - Text:包含给定文本的任何元素。
例如,如果我们要点击一个标记为ProductsView的元素并选择列表中的第一个子元素,我们将使用以下代码:
app.Tap(c => c.Marked("ProductsView").Class("ProductItemCell").Index(0));
重要的是要注意查询的流畅执行风格,其中每个查询返回一个AppQuery对象,而应用操作使用Func<AppQuery, AppQuery>委托。
为某个视图创建结构化查询的最简单的方法是使用 Xamarin 提供的Read-Eval-Print-Loop(REPL)。 UITests 框架。 要启动 REPL,你可以使用相关的IApp方法:
app.Repl();
在终端会话上初始化 REPL 之后,tree命令可以提供完整的视图树。 您还可以使用相同的IApp实例执行应用查询和操作:
App has been initialized to the 'app' variable.
Exit REPL with ctrl-c or see help for more commands.
>>> tree
[UIWindow > UILayoutContainerView]
[UINavigationTransitionView > ... > UIView]
[UITextView] id: "CreditCardTextField"
[_UITextContainerView]
[UIButton] id: "ValidateButton"
[UIButtonLabel] text: "Validate Credit Card"
[UILabel] id: "ErrorrMessagesTestField"
[UINavigationBar] id: "Credit Card Validation"
[_UINavigationBarBackground]
[_UIBackdropView > _UIBackdropEffectView]
[UIImageView]
[UINavigationItemView]
[UILabel] text: "Credit Card Validation"
>>>
操作因所选视图元素的不同而不同,但最常用的操作如下:
Tap:用于模拟用户的点击手势。EnterText:将文本输入到所选视图中。 需要注意的是,在 iOS 上,软键盘用于输入文本,而在 Android 上,数据直接传递到目标视图。 当您与隐藏在键盘下或被键盘偏移的元素交互时,这可能会导致问题。WaitForElement:等待查询定义的元素出现在屏幕上。 有时,使用较短的超时时间,可以将此方法用作元素断言的一部分。Screenshot:这是给定标题的截图。 这表示 App Center 执行中的一个步骤。
页面对象模式
在某个测试方法中实现 UI 测试可能会变得相当乏味。 事实上,自动化平台的查询和操作将变得紧密耦合且不可维护。 为了避免这种情况,建议使用页面对象模式(POP)。
在 POP 中,屏幕上的每个视图或不同的视图元素实现其自己的页面类,该类实现与该特定页面的交互,以及该页面内视图组件的选择器。 这些交互是以一种简化的、词法的方式实现的,因此后台的复杂自动化实现不会反映在实际的测试实现中。 此外,对于交互和查询,页面对象还负责提供一种导航到另一个页面和从另一个页面导航的方法。
让我们学习如何实现我们的 POP 结构:
-
Let's start by creating our
BasePageobject:public abstract class BasePage<TPage> where TPage : BasePage<TPage> { protected abstract PlatformQuery Trait { get; } public abstract TPage NavigateToPage(); internal abstract Dictionary<string, Func<AppQuery, AppQuery>> Selectors { get; set;} protected BasePage() {} // ,.. // Additional Utility Methods for ease of execution }基类规定每个实现都应该实现一个定义页面本身(以验证应用已导航到目标视图)的
Trait对象和一个将用户(从主屏幕)带到实现视图的导航方法。 -
Now, let's implement a page object for the
Aboutview:public class AboutPage : BasePage<AboutPage> { public AboutPage() { Selectors = new Dictionary<string, Func<AppQuery, AppQuery>>() Selectors.Add("SettingsMenuItem", x => x.Marked("Settings")); Selectors.Add("SettingsMenu", x => x.Marked("CategoryView")); Selectors.Add("AboutPageMenuItem", x => x.Marked("Information")); Selectors.Add("Title", x => x.Marked("Title")); Selectors.Add("Version", x => x.Marked("Version")); Selectors.Add("PrivacyPolicyLink", x => x.Marked("PrivacyPolicyLink")); Selectors.Add("TermsOfUseLink", x => x.Marked("TermsOfUseLink")); Selectors.Add("Copyright", x => x.Marked("Copyright"); } internal override Dictionary<string, Func<AppQuery, AppQuery>> Selectors { get; set;} protected override PlatformQuery Trait => new PlatformQuery { Android = x => x.Marked("AboutPage"), iOS = x => x.Marked("AboutPage") }; public override AboutPage NavigateToPage() { // Method implemented in the base page using the App OpenMainMenu(); App.WaitForElement(Selectors["SettingsMenuItem"], "Timed out waiting for 'Settings' menu item"); App.Tap(Selectors["SettingsMenuItem"]); App.WaitForElement(Selectors["SettingsMenuItem"], "Timed out waiting for 'Settings' menu"); App.Screenshot("Settings menu appears."); App.Tap(Selectors["AboutPageMenuItem"]); if(!App.Query(Trait).Any()) { throw new Exception("Navigation Failed"); } App.Screenshot("About page appears."); return this; } public AboutPage TapOnTermsOfUseLink() { App.WaitForElement(Selectors["TermsOfUseLink"], "Timed out waiting for 'Terms Of Use' link"); App.Tap(Selectors["TermsOfUseLink"]); App.Screenshot("Terms of use link tapped"); return this; } }因此,现在,使用
AboutPage实现并在AboutPage上执行操作就像初始化Page类并导航到它一样简单:new AboutPage() .NavigateToPage() .TapOnTermsOfUseLink()
对于是否在页面中包含断言,或者仅仅公开选择器,以便将断言作为测试的一部分实现,社区存在分歧。 无论如何,实现 POP 都可以帮助开发人员和 QA 团队在短时间内轻松地创建易于维护的测试。
总结
在本章中,我们研究了自动化测试和验证过程的各种测试策略。 创建自动化测试可以帮助我们控制在开发生命周期中创建的技术债务,并保持对源代码的检查,从而提高代码和管道本身的质量。 如您所见,其中一些测试与单元测试一样简单,单元测试是在应用生命周期的开始阶段实现的,并且几乎在每个代码检查点执行,而有些测试是复杂的,例如集成和编码 UI 测试, 它们通常是在开发阶段的末尾编写的,并且只在特定的检查点执行(即,每夜构建或预发布检查)。 无论如何,目标应该始终是为代码创建一个可认证的管道,而不是为认证创建代码。
随着测试的覆盖,我们可以说我们的交付管道的持续集成部分被覆盖了。 接下来,我们将进入持续交付阶段,在此阶段,我们将通过使用 Azure Resource Manager 来管理所需状态,将基础设施作为代码来关注。**
十七、部署 Azure 模块
Azure 服务被捆绑到所谓的资源组中,以便于管理和部署。 每个资源组可以用一个Azure resource Manager(ARM)模板来表示,该模板可以用于多种配置和特定的环境部署。 在这一章,我们将配置的手臂模板 Azure 托管 web 服务,以及其他云资源(如宇宙 DB,通知中心,和其他人),我们使用以前,这样我们可以创建部署使用 Azure 开发运维构建和发布管道。 在本章中,我们主要关注的是在模板中引入配置值并准备它们来创建登台环境。
下面几节将带您创建一个参数化的、特定于环境的资源组模板:
- 创建 ARM 模板
- 手臂模板的概念
- 使用 Azure DevOps 作为 ARM 模板
- 部署.NET Core 应用
在本章结束时,你将对 ARM 模板有更深的理解,并且能够创建资源组模板并通过 Azure DevOps 管道部署它们,换句话说,将你的基础设施管理为代码。
创建 ARM 模板
在本节中,我们将讨论如何以代码的形式管理 Azure 基础设施,并创建可重用的模板来管理和重建云环境。
现代 DevOps 方法的基石之一是能够管理分布式应用的基础设施,并为其提供声明性甚至过程性的定义文件集,这些定义文件可以进行版本控制,并与应用源代码一起存储。 在这个Infrastructure-as-Code(IaC)的方法,这些文件应该创建以这样一种方式,无论当前状态的基础设施,执行这些资源应该导致相同的期望状态(即幂等性)。
在 Azure 堆栈中,在订阅中创建的基础设施资源由一个名为 ARM 的服务管理。 Azure 资源管理器(ARM**)提供一致的管理层,允许开发人员与其交互,使用 Azure PowerShell、Azure 门户和可用的 REST API 执行基础设施配置任务。 从语义上讲,ARM 提供了众多资源提供者和开发者订阅之间的桥梁。
我们使用 ARM 管理的资源使用资源组进行分组,资源组是用于标识应用亲和性组的逻辑集。 定义资源组中的一组资源的 ARM 模板是该基础设施的声明性定义,以及可用于配置应用环境的配置。
如果我们回到我们的应用,看看我们一直在为这个应用使用的资源组,你可以看到前面章节中介绍的各种类型的 Azure 资源:

图 17.1 - Azure 资源组
虽然所有这些资源都可以使用一组 Azure PowerShell 或 CLI 脚本来创建,但在不损害这些脚本的幂等性的情况下维护它们将是相当困难的。
幸运的是,我们可以将这个资源组导出为 ARM 模板,并使用生成的 JSON 清单和特定于环境的配置参数管理我们的基础设施。 为了创建初始的 ARM 模板,执行以下步骤:
-
Navigate to the target resource group and select the Export template blade on the Azure portal:
![Figure 17.2 – : Export Azure Resource Group Template]()
图 17.2 -:导出 Azure 资源组模板
-
Once the resource group template is created, head back to Visual Studio and create an Azure Resource Group project and paste the exported template. When prompted to select an Azure template, select the Blank Template option to create an empty template.
重要提示
理解可以使用所提供的模式创建模板是很重要的。 导出模板确实会产生大量冗余参数和资源属性,如果手动创建模板,或者使用 GitHub 上的基本模板,就不会产生这些冗余参数和资源属性。
当一个空的 ARM 模板被创建时,它有如下的模式,它定义了一个 ARM 模板的主要轮廓:
{ "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": {}, "variables": {}, "resources": [], "outputs": {} }在这个模式中,参数和输出定义 Azure 部署的输入和输出参数,而变量定义静态数据,我们将在整个部署模板中构造和引用这些数据。 最后,资源数组将用于定义将包含在我们的资源组中的各种 Azure 资源。
重要提示
在处理 ARM 模板时,Visual Studio 提供了 JSON 大纲视图,它可以帮助浏览模板的各个部分,以及添加和删除新资源。
-
Without further ado, let's start by copying our first resource from the exported template into our Visual Studio project. We will start by importing the Redis cache resource:
{ "type": "Microsoft.Cache/Redis", "apiVersion": "2017-10-01", "name": "[parameters('Redis_handsoncore_name')]", "location": "Central US", "properties": { "sku": { "name": "Basic", "family": "C", "capacity": 0 }, "enableNonSslPort": false, "redisConfiguration": { "maxclients": "256", "maxmemory-reserved": "2", "maxfragmentationmemory-reserved": "12", "maxmemory-delta": "2" } } }在这个资源中,
name属性引用了一个名为Redis_handsoncore_name的参数,我们需要将该参数添加到 parameters 部分。 除了名称之外,还定义了一些基本的资源元数据,如apiVersion、type和location。 此外,资源特定的配置值在properties属性中定义。 -
Let's continue by adding the parameter to the parameters section, with a number of modifications:
"resourceNameCache": { "defaultValue": "handsoncore", "type": "string", "minLength": 5, "maxLength": 18, "metadata": { "description": "Used as the resource name for Redis cache resource" } }我们添加了元数据,在某种意义上,它帮助我们记录资源模板,使其更易于维护。 除了元数据之外,我们还使用定义了
minLength和maxLength属性,以便对参数值进行一些验证(如果我们决定使用生成的或计算的值)。 此外,对于字符串类型参数,我们可以定义允许值。 -
Finally, let's add an output parameter that outputs the Redis resource connection string as an output parameter:
"redisConnectionString": { "type": "string", "value": "[concat(parameters('resourceNameCache'), '.redis.cache.windows.net:6380,abortConnect=false,ssl=true,password=', listKeys(resourceId('Microsoft.Cache/Redis', parameters('resourceNameCache')), '2017-10-01').primaryKey)]" }在这里,我们正在创建 Redis 连接字符串使用一个输入参数,以及一个给定资源的属性引用(即,Redis 缓存实例的主要访问键)。 在下一节中,我们将进一步研究 ARM 函数和引用。
-
现在我们的资源组的基本模板(也就是说,只有包含 Redis 缓存资源的部分实现)已经准备好了,开始使用 Visual Studio 部署模板。
-
After you create a new deployment profile and designate the target subscription and resource group, edit the parameters, and see how the parameter metadata that we have added reflects on the user interface:
![Figure 17.3 – Resource Group Template Parameters]()
图 17.3 -资源组模板参数
-
一旦部署完成,您可以看到部署细节上的输出参数:
09:27:19 - DeploymentName : azuredeploy-0421-0713 09:27:19 - CorrelationId : 4ec72df5-00d3-4194-9181-161a5967235b 09:27:19 - ResourceGroupName : NetCore.Web 09:27:19 - ProvisioningState : Succeeded 09:27:19 - Timestamp : 4/21/2019 7:27:20 AM 09:27:19 - Mode : Incremental 09:27:19 - TemplateLink : 09:27:19 - TemplateLinkString : 09:27:19 - DeploymentDebugLogLevel : 09:27:19 - Parameters : {[resourceNameCache, 09:27:19 - Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.DeploymentVariable]} 09:27:19 - ParametersString : 09:27:19 - Name Type Value 09:27:19 - =============== ========================= ========== 09:27:19 - resourceNameCache String handsoncore123 09:27:19 - 09:27:19 - Outputs : {[redisConnectionString, 09:27:19 - Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.DeploymentVariable]} 09:27:19 - OutputsString : 09:27:19 - Name Type Value 09:27:19 - =============== ========================= ========== 09:27:19 - redisConnectionString String handsoncore123.redis.cache.windows.net:6380 09:27:19 - ,abortConnect=false,ssl=true,password=JN6*******kg= 09:27:19 - 09:27:20 - 09:27:20 - Successfully deployed template 'azuredeploy.json' to resource group 'NetCore.Web'.
模板部署时,其中一个关键参数是部署模式。 在前面的示例中,我们使用默认的部署模式,即Incremental。 在这种类型的部署中,存在于资源组中但不存在于模板中的 Azure 资源不会从资源组中删除,并且只会根据其先前的部署状态分发或更新模板中的项目。 另一个可用的部署选项是所谓的完全部署模式。 该模式下,资源组中存在但模板中不存在的资源将被自动删除。
重要提示
模板中存在的资源,但由于某个条件而没有部署,不会从资源组中删除。
现在,我们已经使用 ARM 的模板导出特性成功地创建和部署了我们的资源组模板。 在这个过程中,我们使用了几个参数并讨论了资源属性。 这种类型的创建可以用于较小的基础设施设置,然而,随着资源组数量的增加,我们将需要使用模板的一些高级概念来重新构造我们的资源模板。
ARM 模板概念
因此,现在我们已经成功地部署了第一个资源,让我们继续扩展我们的模板以包含其他资源。 在本节中,我们将研究相同模板中的依赖资源,以及如何使用模板函数为这些资源创建配置值。
为了演示资源之间的依赖关系,接下来让我们介绍应用服务实例,它将承载用户的 API。 从技术上讲,这个应用服务只有一个依赖项——应用服务计划——它将托管应用服务(即ServerFarm资源类型):
{
"type": "Microsoft.Web/serverfarms",
"name": "[parameters('resourceNameServicePlan')]",
"kind": "app",
// removed for brevity
},
{
"type": "Microsoft.Web/sites",
"name": "[parameters('resourceNameUsersApi')]",
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms', parameters('resourceNameServicePlan'))]"
],
"kind": "app",
"properties": {
"enabled": true,
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('resourceNameServicePlan'))]",
// removed for brevity
}
}
在这个设置中,sites资源依赖于serverfarms资源类型(也就是说,部署顺序将取决于这些依赖项)。 一旦部署了serverfarms资源,所创建资源的resourceId功能将被用作sites资源的serverFarmId。
此外,从架构的角度来看,用户的 API 有两个主要依赖:Redis Cache 和 Cosmos DB。 生成的资源实例应该产生应该添加到应用服务的应用配置中的值(即连接字符串)。
因为我们已经为 Redis Cache 实例创建了一个输出参数,创建一个依赖项并添加连接字符串。
连接字符串可以作为sites资源的siteConfig属性的一部分添加,也可以通过创建专门用于站点配置的额外资源:
"properties": {
"enabled": true,
// removed for brevity
"siteConfig": {
"connectionStrings": [ {
"name": "AzureRedisCache",
"type": "custom",
"connectionString": "[concat(parameters('resourceNameCache'), '.redis.cache.windows.net:6380,abortConnect=false,ssl=true,password=', listKeys(resourceId('Microsoft.Cache/Redis', parameters('resourceNameCache')), '2017-10-01').primaryKey)]"
} ]
}
}
现在,当站点部署时,Redis 缓存连接字符串自动添加到站点配置:

图 17.4 -连接字符串
注意,当准备连接字符串,我们利用concat函数构成价值,我们使用listKeys函数得到一个值列表的资源实例检索根据使用resourceId函数类型和名称。
这些函数和其他用户定义的函数可以在整个资源模板中使用,无论是在构造参考值还是定义条件时。 根据参数类型列出了其中一些函数:

图 17.5 - Azure 模板函数
使用这些函数,可以构造复杂的变量,并使用对其他已部署资源的参数和引用来重用它们。 然后可以将这些构造作为函数公开,以便在整个模板声明中重用它们。
正如您可能已经注意到的,每次我们部署 ARM 模板时,根据我们定义的参数,azuredeploy.parameters.json文件都会更新。 这些是提供给模板的参数,并且在一个多阶段环境(即 DEV、QA、UAT、PROD)中,您会期望有多个参数的文件为这些资源分配特定于环境的值:
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environment": { "value": "DEV" },
"resourceNameCache": { "value": "dev-handsoncoreCache" },
"resourceNameServicePlan": { "value": "dev-handsoncorePlan" },
"resourceNameUsersApi": { "value": "dev-handsoncoreusers" }
}
}
使用多个参数的文件,可以构造唯一的资源名和地址,以便在部署期间避免重复的资源声明。 另一种通常用于避免资源名/地址冲突的方法是将当前资源组标识符作为资源名的一部分,确保资源是特定的和唯一的。
在本节中,我们用一个应用服务扩展了我们的模板,并使用模板函数为应用服务创建了一个配置值,这样它就可以利用包含在同一个模板中的 Redis 缓存实例。 我们最终将模板转换为依赖于环境的模板,这样我们就可以使用相同的资源模板管理多个环境,确保这些环境之间的基础结构是一致的。
使用 Azure DevOps 作为 ARM 模板
一旦准备好了模板,并且我们确信我们的应用所需的所有 Azure 资源都创建好了,我们就可以继续设置自动构建和部署。 在本节中,我们将在 Azure DevOps 上创建一个发布管道来部署我们的基础设施。
为了能够使用 Azure DevOps 进行云部署,我们的第一个行动将是创建一个用于部署资源的服务主体。 服务主体可以描述为一个服务标识,它可以访问某个订阅和/或资源组中的 Azure 资源。
所以,让我们开始:
-
Create a service principal by adding a new ARM service connection within the Azure DevOps project settings:
![Figure 17.6 – Azure Resource Manager Service Connection]()
图 17.6 - Azure 资源管理器服务连接
创建连接将为 Azure DevOps 创建应用注册,并将该服务主体指定为所选订阅的贡献者角色。
-
Another way to do this is by using the Authorize button when creating an Azure Deployment task. The Authorize button becomes available if an Azure subscription is selected, instead of a service principal:
![Figure 17.7 – Azure Resource Group Deployment with Subscription]()
图 17.7 -带有订阅的 Azure 资源组部署
-
Once the service principal is created, continue with setting up the deployment for the resource group.
Azure DevOps 为部署 Azure 资源组模板提供了多种部署选项,例如:
a)Azure PowerShell:执行内联或引用的 Azure PowerShell 脚本
b)Azure CLI:执行内联或引用的 Azure CLI 脚本
c)Azure 资源组部署:部署带有相关参数的 ARM 模板
对于本例,我们将使用Azure 资源组部署任务:
![Figure 17.8 – Azure Resource Group Deployment Task]()
图 17.8 - Azure 资源组部署任务
-
For this task, other than the service principal settings, the Template section provides the main configuration area. Here, select the ARM template and provide the parameters file designated for this environment:
![Figure 17.9 – ARM Template Parameters]()
图 17.9 - ARM 模板参数
-
Additionally, define configuration values such as deployment name and deployment outputs.
重要提示
Azure 资源组部署任务允许选择三种部署模式。 除了 Azure 部署模式(即完整的、增量的)之外,Validate选项只提供了验证模板的功能。 Validate选项可用于验证拉请求,并且只能在持续集成构建期间执行。
现在,ARM 部署可以被触发,每当一个更新被合并到主分支,以保持开发(或更高)环境与 ARM 模板定义最新:

图 17.10 - ARM 部署管道
到目前为止,我们已经创建了一个资源模板,该模板定义了基础设施所需的状态。 然后,我们创建了一个 Azure DevOps 管道,将该基础设施部署到指定的资源组。 我们的下一步是将我们的发布管道扩展到,包括实际应用的部署,这样我们就有了一个完全自动化的管道,可以从零开始创建应用和主机基础设施。
部署。NET Core 应用
一旦部署了 ARM 模板并创建了 Azure 资源,我们的下一步就是部署。NET Core 应用(即,微服务应用以及我们的函数应用)。
Azure DevOps 提供了构建和创建应用服务/web 应用部署包的所有必要任务。创建。net Core web 部署包的三步走是由恢复、构建和发布组成的。 所有这些 dotnet CLI 命令都可以使用构建和发布管道中的内置任务执行。 所以,让我们开始:
-
We will start by restoring the NuGet packages for our user's API microservice:
![Figure 17.11 – DotNet Restore Task]()
图 17.11 - DotNet 恢复任务
-
The next step is to build the application using a specific build configuration (a pipeline variable can be used for this):
![Figure 17.12 – DotNet Build Task]()
图 17.12 - DotNet Build Task
-
After the project is built, prepare the web deployment package to be able to push it to the app service resource that was created in the ARM deployment step. In order to prepare the deployment package, we will use the publish command:
![Figure 17.13 – DotNet Publish Task]()
图 17.13 - DotNet 发布任务
-
Finally, as regards deployment, make use of the Azure App Service Deploy task:
![Figure 17.14 – Azure App Service Deploy Task]()
图 17.14 - Azure App Service Deploy Task
在部署步骤中,包或文件夹参数必须与发布步骤(即
$(build.artifactstagingdirectory))中用作输出目录的文件夹匹配。在上一个任务中,我们为应用服务名称参数使用了一个硬编码的名称。 这意味着每次修改 ARM 参数文件时,我们都需要更新构建定义,或者,如果我们为构建定义使用 YAML 文件,则需要更新 YAML 定义。 这可能被证明是一场维护噩梦,因为每次创建一个新环境,构建和发布定义都需要更新。
将 ARM 部署与实际应用部署集成的一个可能的解决方案是,从 ARM 模板输出目标应用的名称,并使用它作为应用服务的名称参数:
"outputs": { "redisConnectionString": { //... }, "userApiAppResource": { "type": "string", "value": "[parameters('resourceNameUsersApi')]" } } -
Next, assign the output from the ARM deployment to a variable named
armOutputVariables, using the Deployment outputs option from the Azure Resource Group Deployment task:![Figure 17.15 – Deployment Outputs Parameter]()
图 17.15 -部署输出参数
-
现在,添加一个 PowerShell 脚本任务(也就是说,不是 Azure PowerShell)来将输出解析为 JSON 格式,并将所需的应用服务名分配给管道变量:
$outputs = ConvertFrom-Json $($env:armOutputVariables) Write-Host "##vso[task.setvariable variable=UsersApiAppService]$($outputs.userApiAppResource.value)" -
此时,应用服务资源名可以像其他管道变量一样被访问(即使用
$(UsersApiAppService)),并且可以被分配给Azure app service Deploy步骤。
剩下的构建模板可以用同样的方式创建,使用相同或类似的。net Core 和 Azure 任务。
总结
在本章中,我们已经完成了创建 ARM 模板的基本步骤,以便我们的应用所需的云基础设施能够按照 IaC 的概念供应和管理。 将云资源设置为声明性 JSON 清单后,我们可以轻松地对环境进行版本化并跟踪,而不会出现环境漂移和与基础设施相关的部署问题。 然后,作为 Azure DevOps 服务一部分的. net Core 构建和发布步骤被用于创建部署构件,这些构件与 Azure 云基础设施无缝集成。
在本章中,我们已经为。NET Core 服务之一做好了构建和发布的准备。 然而,我们实际要做的是在持续集成构建期间创建部署构件,并使用发布管道来部署基础设施,然后再部署应用服务构件。 我们将在下一章中创建发布管道。**
十八、Azure DevOps 的 CI/CD
持续集成(CI)和持续交付(CD)是两个深深植根于敏捷项目生命周期定义中的概念。 在敏捷方法中,DevOps 的工作主要花在减少 CD 周期上,以便可以定期向用户交付更小的 sprint 和更小的变更集。 反过来,变更越小,风险就越小,对用户来说就越容易采用。 为了最小化交付周期的长度,自动化的交付管道是至关重要的。 使用 Azure DevOps 提供的工具集,开发人员可以为构建、测试和部署创建完全自动化的模板。 在本章中,我们将为 Xamarin 建立与 Azure 部署管道一致的构建和发布管道。
在这一章中,我们将主要讨论如何使用 Azure DevOps 提供的工具集实现适当的应用生命周期和自动交付管道。 我们将简要介绍一下使用 Azure DevOps 的 CI/CD,以及如何使用 Git 和 GitFlow 作为分支策略来实现 CI/CD。 然后,我们将继续讨论使用自动化工具交付应用的质量保证。 最后,我们将使用发布模板发布应用包的 web 后端和我们的移动应用。 下面的主题将指导你了解这些 DevOps 概念:
- 引入 CI / CD
- 与 GitFlow CI / CD
- 分支机构的质量保证(QA
- 创建和使用发布模板
在本章结束时,您将能够使用 Azure DevOps 提供的固有工具集为您的 web 和移动应用建立一个适当的交付管道。
引入 CI/CD
让我们从 CI 和 CD 管道开始这一章。 在本节中,我们将重点关注正确设置开发和交付管道的基本概念。
在前面的章节中,我们设置了各种构建定义,以创建可以用作部署构件的应用二进制文件和包。 在准备这些工件时,我们实现了可以包含在自动化构建定义中的自动化测试。 每次团队成员引入版本控制变更时,自动化代码构建和测试的过程通常被称为 CI。 CI,加上一个成熟的版本控制系统和一个定义良好的分支策略,是鼓励开发人员在提交时更加大胆和敏捷的主要因素,有助于提高发布的节奏。
另一方面,CD 是构建、测试和配置应用的(通常)自动化过程,最后将应用的特定版本部署到登台环境中。 通常使用多个测试或登台环境,并自动创建基础设施和部署,直到生产阶段。 在一个健康的 CD 管道中,连续环境集的成功是通过集成、负载和用户验收测试等运行时间越来越长的活动来度量的。 CI 启动 CD 过程,管道在成功完成前一轮测试后对每个后续环境进行分级。
在 CD 中,发布定义由一系列环境组成。 环境是一个逻辑容器,它表示希望将应用部署在何处。 在物理上,环境可以指服务器集群、云基础设施上的资源组或移动应用分发环。 每个环境(有时被称为阶段)都有其目的,开发管道中的一部分涉众被分配为该特定环境的所有者:

图 18.1 -应用开发生命周期
每个环境的 web 服务和移动应用的配置可能因特定环境的目的而不同。 尽管如此,重要的是要记住,环境应该在 CI/CD 管道中的任何一点上托管相同的应用发布版本和二进制文件。 通过这种方式,我们可以确保,一旦应用被提升到更高的环境(即更接近生产环境),它将像在前一个阶段那样工作。
正如您所看到的,在 DevOps 术语中,部署并不总是意味着发布或生产。 作为 CD 的一部分,开发团队的提交会触发各种部署管道。 如果提交的代码经过单元测试和集成构建的验证,那么这些分支的工件将被部署到登台环境中。 在登台环境中,执行冒烟测试和验收测试。 如果在各个阶段通过这些测试验证了应用的完整性和新特性,则可以在整个生产环境中推出新版本。
在 Azure DevOps 术语中,正如您从前面的示例中看到的,CI 是使用 Git 和 Azure DevOps Build 模板实现的。 另一方面,CD 由版本定义处理,使用与触发的 CI 构建一起准备的构建工件。
重要提示
实际上,CI/CD 管道可以准备使用 TFVC 和相关的分支策略; 然而,Git 及其相关的分支策略(如 GitFlow)提供了更灵活和敏捷的设置。
使用 Azure DevOps,环境之间的转换(即,升级过程)可以通过部署前和部署后的门户来控制。 可以设置这些门以要求开发管道中特定涉众的批准(例如,环境所有者)。 除了人工审批,远程服务还可以用于 gate 审批。 例如,可以将应用模块的发布与另一个依赖模块同步,或者将部署阶段延迟到执行某些测试。
这里提供的示例只是一种可能的设计,版本控制实现、分支策略以及相关的发布管道设置应该根据开发团队的需求和业务需求来设计和执行。
在我们的示例应用中,为了创建管道,我们将分别使用 Git 和 GitFlow 作为我们的版本控制和分支策略。 对于发布管道,我们将创建一个开发版本,它将与每个提交/合并一起自动部署到开发分支(即下一个版本),而 QA、UAT 和生产环境将从发布分支(即当前版本)部署。
CI/CD with GitFlow
说明 CI/CD 最简单的方法是通过策略和过程,从 GitFlow 开始。 这里,我们处理的是两个独立的存储库,即 web 和应用,每个存储库都有自己的生命周期。
换句话说,虽然不建议这样做,但我们的 web 应用(即服务基础设施)和应用(即移动平台发布)可能有不同步的发布和版本; 因此,创建向后兼容的模块并与开发团队成员交流版本是很重要的。
下面的小节将演示一个默认的开发周期,并解释开发人员通常需要做些什么来保持应用的质量,而不影响发布的节奏。
发展
应用或 web 模块的开发从创建一个特性分支开始(例如feature/12345)。 特性分支可以在多个开发人员之间共享,也可以由单个开发人员处理。 如果特性分支由多个开发人员处理,则可以按照类似的约定user/<user identifier>/<feature id>(例如user/cbilgin/12345)创建用户分支。 一旦每个开发人员完成了他们的实现,一个 pull 请求就可以在主特性分支上执行。
决定特性分支运行状况的一个重要因素是开发分支和特性分支之间的提交差异。 理想情况下,特性分支应该总是领先于开发分支,而不管在特性分支工作时在开发分支上完成的 pull 请求的数量。 为了实现这一点,应该定期根据当前的开发树重新调整特性分支,检索来自其他特性的最新提交。
开发人员可以通过在本地运行 web 应用和在理想的模拟器/模拟器上运行移动应用来测试在特性分支上完成的工作。 虽然 iOS 和 UWP 模拟器可以使用 localhost 前缀,因为本地机器网络是由模拟器共享的,Android 模拟器使用他们自己的 NAT 表,其中 localhost 指的是移动设备,而不是主机。 为了访问主机上的托管 web 服务,您应该使用10.0.2.2IP 接口。 下表显示了不同的 IP 地址,以及如何在 Android 模拟器中使用它们:

图 18.2 - Android 模拟器 NAT
如果应用和 web 服务是在具有各自发布周期的独立存储库上处理的,那么本地 web 服务器实例应该在开发(或主)分支上使用最新的提交,确保开发是使用最新的服务基础设施完成的。
一旦特性准备好集成到下一个版本中,开发人员就负责创建一个带有此特性分支所代表的相关工作项的拉请求。
Pull request/merge
在理想的设置中,特性分支合并到开发分支的唯一方式应该是通过拉请求。 拉请求也用于执行快速的健全检查和代码审查。
由开发人员交付的代码的质量可以通过目标分支(即开发分支)的分支策略来验证。 在本例中,对于开发分支,我们将使用四个策略:
-
附加到 pull 请求的工作项(特性和/或用户故事或 bug):任务和用户故事通常附加到特性分支中的提交。 特性、用户描述和/或错误工作项(这些是任务的父工作项)必须附加到拉请求,这样,一旦创建了发布管道,这些工作项就可以从发布构建中确定。
-
Review by two team members:为了鼓励同行评审过程,至少有两名团队成员负责评审 pull request。 这些团队成员之一通常是团队领导,他是针对开发或发布分支的任何 pull 请求的强制审查者。 每个审阅者负责对代码更改进行注释,然后由 pull 请求的所有者更正这些更改。
-
团队领导的审查(包括最低数):架构师或团队领导通常是的人最终的代码质量负责引入开发或发布分支,所以他/她是一个强制性的评论家把请求。
-
Branch evaluation build: For the mobile project, the branch evaluation build can be a build of the Android project (since Android builds can be executed on a Windows build agent, as opposed to the iOS builds having to be executed on a Mac agent). This build should execute the unit tests and run static code analysis using a platform such as SonarQube or NDepend.
重要提示
在这样的设置中,明智的做法是允许团队领导或其他负责的涉众在紧急情况下对政策拥有凌驾于其之上的权力,绕过评估构建(很少)和审查需求(更常见)。
使用 Azure DevOps,为了设置策略并强制开发人员创建 pull 请求,目标分支(即开发分支)应该配置以下分支策略:

图 18.3 -分支策略
目标分支上的任何策略需求都应该在更新分支时强制使用 pull 请求。
一旦策略验证令人满意(即满足所有必需的策略),代码就可以合并到开发分支。 Azure DevOps 允许在完成期间选择合并策略。 这个合并策略应该与分支策略和设计一致。 在我们的示例中,我们将使用 Rebase; 然而,其他三个合并选项中的任何一个都可以使用:

图 18.4 -合并类型
一旦 pull 请求合并到开发分支中,CI 阶段就可以开始了,这将在下一节中讨论。
CI 期
启用了 ci 的分支上的任何更新都会触发构建来构建应用和/或 web 服务包。 例如,对于移动应用开发管道,可以使用开发阶段配置(例如 DevDroid 和 DeviOS 配置)触发目标移动平台的多个构建。 这些构建可以将应用包作为构建构件准备,并将它们与下一个应用版本和小修订一起发布。
CI 构建的触发器可以在构建属性中设置:

图 18.5 -构建触发器
除了触发分支之外,还可以设置路径过滤器,以便根据引入到应用代码基的更改触发不同的 CI 构建并准备工件。
此外,CI 构建应该执行任何可用的单元测试,以及可以在构建代理上运行的简单集成测试,这些结果可以连同代码覆盖一起发布到管道中。 使用当前工件版本注释的另一轮静态代码分析可以在静态分析平台(例如,SonarQube 服务器)上执行和发布。 这将有助于将源代码增量与此应用版本中可能出现的问题关联起来。
当 CI 构建成功完成时,根据触发器设置,可以创建一个发布管道,将准备好的工件部署到目标环境(在本例中就是开发环境):

图 18.6 -释放阶段工件触发
这个示例部署了由 CI 构建准备的微服务包,并将它们部署到开发环境中。 环境通过Azure 资源管理器(ARM)部署来更新,以目标资源组。 通常的做法是在没有任何预先批准的情况下设置开发环境的发布版本,这样集成到开发分支中的任何代码都会自动部署到开发环境中。
Azure DevOps 提供了两个任务来发布构建管道构件,创建的构件可以用作发布管道的触发器或辅助构件:

图 18.7 -工件任务
工件发布的一般经验法则是为构建使用 staging 目录:

图 18.8 -发布管道工件任务
在工件发布任务之前,对于 web 应用,. net 发布任务可以与相同的输出目录一起使用。 对于 Xamarin 包,可以创建一个复制任务,将应用包复制到 staging 目录。
来自合并的特性分支的工件已经可以部署到一个特定的环境中进行验证,或者可以将它们包含在一个发布范围中,作为发布的一部分进行验证。
发布分支
一旦验证了开发分支,并且当前特性集与预定的发布范围相匹配,就会创建一个发布分支(例如,release/1.8)。 发布分支的创建与发布构建的触发密切相关,该构建将为某个环境的发布准备所需的完整工件集。 相应地,相关的发布管道将由这个分支上的构建触发。
由于 Xamarin 应用包具有特定于环境的配置结构和多个应用构件,因此我们应该在这里专门用一段时间来介绍它。 如前所述,要让一个本地应用支持多个配置,我们需要创建多个应用包。 如果你考虑一个最小的支持场景,比如只支持 iOS 平台,为了在我们的发布管道中拥有 QA、登台和生产环境,我们将需要从相同的源代码版本创建三个独立的应用包。 如果我们也想支持 Android,这将意味着对于单一环境(例如 QA),我们将需要部署一个 IPA 和一个 APK 到 Visual Studio App Center(两个独立的应用环),并使用相同的 web 基础设施验证这些应用。 应该仔细设计和执行这些构建和发布阶段的同步。 单个平台的多配置构建可以与多代理构建模板一起使用,以创建一个包含多个包的单个工件,这样,只有在所有必需的构建完成之后,才会触发发布管道。
最后,版本工件的构建模板还可以用来检查质量,并处理要创建的版本所引入的范围。 发布管道还支持自动化测试的执行,以便在发布环境中自动化验证过程。 例如,在部署了某个服务 API 包之后,最好对部署 URL 执行功能测试,以验证部署是否成功。 以类似的方式,在将应用包部署到某个 App Center 环(例如,分段)之前,可以执行 Xamarin Test Cloud 测试来验证应用特性。
补丁分支
在使用发布管道将应用部署到快环或慢环(QA 或 UAT)之后,根据测试协议,QA 团队可以开始测试应用。
在此阶段,任何被拉入当前版本范围的错误或附加特性请求都应该由开发团队使用热修复分支引入。 热修复分支起源于当前的发布分支,并与 pull 请求的用户合并回发布分支(例如,release/1.8 -> hotfix/12324 -> release/1.8)。 一旦热修复被合并回发布分支(也就是说,它已经通过了验证),它也需要合并回开发分支,以传播代码更改并避免在接下来的发布中回归。
对发布分支的合并和拉请求遵循与开发分支相似(尽管不完全相同)的方法。 通过这种方式,我们可以通过相同的质量验证过程推动修补程序的修改。
生产
在发布管道中,工件的某个版本可以从一个环境提升到更高的环境,直到产品发布完成,并将应用的新版本交付给最终用户。
根据管道设计、生产环境还可以利用分段或阶段性发布策略使用部署与释放环槽(即在 Azure 应用服务),本机分期与 TestFlight(即为 iOS 应用),甚至增量发布,苹果和谷歌播放存储支持。 通过这种方式,可以从 beta 用户那里收集发布环境中的应用遥测数据,并将其引入开发管道中。
在本节中,我们通过讨论交付管道的“左侧”开始分析,并从开发团队的角度观察该管道:如何创建、检查、合并应用源,以及可能交付到测试环境。 然后我们讨论了发布范围和交付管道,换句话说,转向交付管道的“右边”。 在这种设置中,我们当然需要在左侧尽可能早地开始验证和质量检查,这样我们的管道中就不会出现瓶颈。 在下一节中,我们将查看整个管道中的这些不同的质量验证检查点。
QA 过程
在一个 CD 过程的每个阶段中,特性的质量应该通过自动化过程或者至少通过适当的代码评审来验证。 这就是拉请求创建和验证过程变得更加重要的地方。 然而,如上所述,工件或分支的 QA 并不局限于流程的 CI 阶段,而是贯穿于 CI/CD 管道。
如您所见,我们可以对源代码和生成的工件进行各种质量检查,例如代码检查、自动化测试,甚至静态代码分析,以识别代码气味和编码约定问题。 让我们仔细看看这些 QA 步骤。
代码评审
一个健康的开发团队应该是由协作驱动的。 在这种情况下,同行评审的概念是非常重要的,因为它为开发团队提供了对同事工作的改进提出建议和建议的机会。 Azure DevOps 有两个分支策略,直接鼓励甚至强制同行评审过程。 其中一个策略是最少的审阅者数量,第二个策略是自动代码审阅者策略:

图 18.9 -代码评审策略
使用自动代码审阅器策略,多个可选和/或强制的审阅器可以自动添加到不同源路径的拉取请求审阅过程中。
这使得开发人员可以在 Azure DevOps web 门户上协作,在拉请求中包含的提交的特定行、部分甚至文件上创建注释。
审查过程不仅限于手动开发人员的反馈,而且还可以引入一些部分自动化。 如果作为一个验证构建政策的一部分,SonarQube 和声纳 c#插件可以检测包含在拉上构建执行请求,和代码问题,发现新代码静态分析的结果,被添加到拉请求评论拉请求完成之前得到解决。
可以使用注释解析策略强制(也就是说,它们必须在 pull 请求完成之前被解析)由同行或自动化工具添加的评审注释:

图 18.10 -注释解析策略
总的来说,可以说代码评审是 CI/CD 管道中代码质量维护的重要组成部分。
测试
正如您在第 16 章自动化测试中看到的,各种测试可以自动化并引入 CD 管道。 在任何 CI 构建上执行的测试都可以显示在构建结果摘要中,并在一个单独的部分中显示聚合的报告值(考虑到之前的运行),从而使开发人员有机会在应用构件部署到目标环境之前识别问题。
任何(失败的)测试都可以作为在产品待办事项列表中创建工作项的起点(例如,一个 bug 或一个问题,取决于所使用的过程模板),并附带任何相关的调试信息(如果可用的话)。 此外,自动化测试可以与实际的工作项相关联,例如特性或用户故事,允许 CI 过程与项目管理元数据创建有意义的关联:

图 18.11 -测试和工作项的关联
因此,自动化的测试不仅可以用于早期识别问题,还可以帮助分析和分类过程。 这种方式,开发团队可以提高两个重要 DevOps kpi:平均检测时间(MTTD)和平均恢复时间(【显示】MTTR),创建和保持健康的 CD 管道。
**## 使用 SonarQube 进行静态代码分析
由于 c#和。net Core 的编译特性,源代码的静态分析和质量指标可以帮助开发团队保持一个健康的开发管道。 与更老的工具(如 StyleCop)和更流行的 Visual Studio 扩展(如 ReSharper)类似,SonarQube 是一个开源静态分析平台,提供有价值的 kpi 和关于应用源代码的历史记录。 使用 SonarQube,质量度量的某些特征和趋势,如复杂性、代码气味和重复,可以用于在开发周期的早期识别问题,帮助开发团队将应用引导到正确的方向。
SonarQube 支持许多平台和语言,包括 c#,并与 MSBuild 和 Azure DevOps 基础设施深度集成,这使它成为任何。net Core 开发项目的理想选择。 服务器组件可以在本地托管,也可以作为云设置的一部分。 另一方面,SonarCloud 是基于 java 平台的托管版本。
设置好 SonarQube 服务器并安装好所需的插件(即 SonarCSharp)后,就可以设置给定项目的质量配置文件。 质量配置文件由源代码应该遵守的质量规则组成,每个规则定义了各种警告和错误级别:

图 18.12 - SonarCloud 问题视图
使用质量概要文件,可以定义一个所谓的质量检验关,确定源代码中哪一种类型的变更会触发检验关故障,提醒开发团队可能出现的问题。 质量检验关通常定义在泄漏期(即计算新代码的时期)内引入存储库的新代码上; 然而,整个项目中的一些聚合值也可以包括在内。
在这里,需要特别指出的是 SonarQube 使用一个 Git 扩展来访问源代码修订历史,并对代码树进行注释,这样就可以很容易地识别代码增量和提交的所有者。 一个简单的质量检验关可能看起来如下所示:

图 18.13 - SonarCloud 质量之门
SonarQube 分析的执行可以在 CI 阶段和 CD 阶段进行。 AzureDevOps SonarScanner 扩展提供了方便的集成构建和分析体验,而 SonarLint 和相关的 Roslyn 分析器为 Visual Studio IDE 中的开发人员提供了洞察和帮助。
使用 SonarLint 进行本地分析
SonarLint 是一个 Visual Studio 扩展,允许开发者将本地项目绑定到指定的 SonarQube 服务器及其相关的质量配置文件。 一旦源代码与 SonarQube 项目关联起来,它就下载规则集,并使用 Roslyn 分析器将这些规则应用到源代码中,提供了具有突出显示和问题解决方案选项的完全集成的编辑器体验。
将 SonarLint 与 SonarQube 一起使用,可以集中管理编码约定和规则,并有助于在更大的开发团队中维护代码质量。 虽然规则定义是由上述分析器提供的,但质量概要定义的级别使用规则集文件包含在项目中:
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="SonarQube - App Sonar way" Description="This rule set was automatically generated from SonarQube.
http://****.northeurope.cloudapp.azure.com:9000/profiles/show?key=cs-sonar-way-35075" ToolsVersion="15.0">
<Rules AnalyzerId="SonarAnalyzer.CSharp"
RuleNamespace="SonarAnalyzer.CSharp">
<Rule Id="S100" Action="Warning" />
<Rule Id="S1006" Action="Warning" />
<!-- Removed for brevity -->
<Rule Id="S103" Action="Warning" />
<Rule Id="S4027" Action="None" />
<Rule Id="S907" Action="Warning" />
<Rule Id="S927" Action="Warning" />
</Rules>
</RuleSet>
这些规则定期与 SonarQube 服务器同步,可以与为其他分析器(如 StyleCop 分析器)定义的规则集文件组合在一起。
置信区间分析
一旦开发人员提交了他们的更改并创建了一个 pull 请求,就可以使用市场上可用的 Azure DevOps 扩展来执行 CSharp 的 SonarScanner。
在 Azure DevOps 实例上安装扩展之后,扩展的设置非常简单。 初始步骤是在你选择的 SonarQube 服务器上创建一个访问令牌(也就是说,SonarQube 或 SonarCloud 取决于所使用的变体),并使用此令牌在 Azure DevOps 上创建一个服务连接:

图 18.14 - SonarCloud 服务连接
然后,集成的构建任务将作为一对任务包括在所需的拉请求验证构建(或 CI 构建)中:准备分析和运行分析。
Prepare Analysis 任务下载所需的分析配置并准备集成的 MSBuild 执行目标。 另一方面,Run Analysis 任务收集构建执行期间收集的结果,并将它们上传到服务器。 重要的是在进行任何编译之前放置准备任务,以便 Sonar 配置在执行编译时就绪。 一个简单的构建序列可能看起来像以下的:

图 18.15 - SonarCloud CI 管道任务
最后,可选的 Publish Analysis 任务可以等待分析结果,然后在当前管道中发布它们。
重要提示
. NET Core 项目不需要ProjectGuid属性,这与经典的. net 项目不同。 然而,声纳扫描仪使用ProjectGuid来识别项目并对其进行分析。 为了确保 Sonar 扫描仪能够成功执行,应该在每个.csproj文件上手动创建ProjectGuid属性,并将其设置为随机的Guid。
通过这种设置,Sonar 规则将在构建过程中被 Sonar 分析器用于识别不同严重程度的代码问题。 然而,如果我们需要或希望包含额外的 Roslyn 分析器,我们可能会希望运行声纳分析并计算关于聚合的代码问题集的度量。
外部 Roslyn 分析仪
除了内置的分析规则集,SonarQube 还可以使用由其他 Roslyn 分析器(如可用的 StyleCop 分析器)识别的警告和错误。
为了将 StyleCop 规则包含到。net Core 项目中,只需引用公开可用的 NuGet 包:
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
</ItemGroup>
此时,与编码约定相关的问题将被识别出来,并作为警告通过构建输出刷新。 此外,IDE 将使用 Roslyn 基础设施提供注释和解决方案。
最后,一旦项目通过 SonarQube 服务器分析,StyleCop.Analyzers识别的问题也将被存储并包含在质量检查计算中。 例如,下列问题由 StyleCop 规则识别,但包含在 SonarQube 中:

图 18.16 - SonarCloud Roslyn 分析器
总之,SonarQube 提供了一个完整的代码质量管理平台,再加上 Azure DevOps 和。net Core,提供了一个理想的自动化开发管道,并确保了 CI 过程的安全。
创建和使用发布模板
如前所述,一旦 CI 完成,理想情况下,发布的构建工件应该被转移到发布管道中,开始 CD 阶段。 Azure DevOps 发布模板和基础设施提供了一个完整的发布管理解决方案,无需任何额外的平台(如 Jenkins、Octopus 或 TeamCity)就可以处理 CI/CD 管道。
在这一节中,我们将看一下基本的发布模板元素,并在不同的发布模板中为 Xamarin 和 Azure web 应用发布制定细节。
Azure DevOps 发布
发布定义由两个主要组件组成:工件和阶段。 使用触发器和门,工件到目标阶段的部署是有组织和管理的。
释放工件
发布构件是为发布任务提供组件的元素。 这些构件可以从简单编译的应用库到直接从应用库检索的源代码:

图 18.17 -释放工件
让我们仔细看看这些工件类型:
- Azure 管道:这是最常用的构件类型,它允许构建管道将编译结果作为打包组件传递给发布管道。 使用此工件类型允许发布管道检测随工件引入的工作项,从而在项目工作项和发布详细信息之间创建直接的关系。 工件新版本的创建可以用作发布的触发器。
- TFVC、Git 和 GitHub:如果来自源代码存储库的静态内容,例如配置文件、媒体内容或源代码本身,对于发布管道任务来说是必需的,那么各种存储库都可以作为工件使用。 进入存储库的提交可以用作发布的触发器。
- Jenkins:如果在一个设置中涉及多个构建和发布管道,可以为 Jenkins 部署创建一个服务连接,并且 Jenkins 构建工件可以被 Azure DevOps 发布管道使用。
- Azure Container Registry, Docker 和 Kubernetes:当处理集装箱化的应用包时,可以将准备好的图像推送到私有容器注册表中,然后在发布过程中检索这些图像。
- Azure Artifacts (NuGet、Maven 和 npm):Azure 包管理构件可以被检索并使用该源来触发新发布,允许各种打包组件包含在发布管道中。
- 外部或本地 TFS:本地 TFS 基础设施也可以包含在 Azure DevOps 发布管道中。 为了使这种类型的集成能够工作,本地 TFS 服务器应该配备一个本地自动化代理。
使用市场上可用的 Azure DevOps 扩展,可以将 TeamCity 等其他构件引入到发布管道中。
在我们的应用管道中,我们将使用构建工件类型,它将包含 ARM 定义、web API 服务包以及用于各种环境的多个配置应用包。
发布阶段
用外行的话来说,发布阶段大致地转换为我们希望部署应用的环境。 重要的是要强调一个事实,即阶段只是一个逻辑容器,不需要引用单个服务器环境。 它可以指各种环境基础设施,也可以指移动应用的托管分发环。
发布阶段包含将在发布代理上执行的发布作业。 例如,如果我们要将构建工件部署到 Azure Stack 或将移动应用包部署到 App Center,我们可以在托管代理上使用代理作业。 然而,如果部署目标是一个内部服务器,我们将需要使用一个特定的部署代理或部署组。
如前所述,发布阶段可以包含多个发布作业,这些作业可以并行或顺序执行,这取决于组件之间的依赖关系。 例如,为了将 iOS 组件与 Android 或 UWP 包同时部署到 QA 分发环,我们可以利用并行代理作业来选择要下载和发布的特定工件。 每个工作都可以定义它需要的特定工件。 另一个多任务发布设置的例子是微服务包部署设置,其中每个服务都是独立部署的:

图 18.18 -各阶段的工件下载
在本例中,API 部署可能已经配置好,以便只有在主 ARM 部署完成之后,服务才被部署到与应用服务相关的地方。
释放门和触发器
在 Azure DevOps 发行版中,阶段之间的转换由触发器和门控制。 释放序列,以及手动或外部服务门,可以使用这些组件进行配置。
发布的主要触发器是通过发布中引入的工件来设置的。 如前所述,构建工件可以被设置为触发每个新版本的新发布。
以下触发器将在每次从匹配给定通配符表达式(即/release/*)的源分支创建构建工件时执行:

图 18.19 -连续部署触发器
可以对这个场景进行扩展,以包括构建标记和其他的exclude表达式。
在主要的释放触发器之上,每个阶段都可以定义一个单独的触发器。 这些触发器可以指实际的释放触发器或另一个阶段的完成。 还包括手动部署,以将一个阶段从主发布系列中分离出来。 下面的触发器将 QA 阶段定义为 UAT 阶段的触发器,将两个版本连接起来:

图 18.20 -阶段依赖
阶段之间的自动转换可以设置为期望来自特定用户(手动批准)或外部服务器的输入。 这些所谓的门可以定义为部署前或部署后的门,其中一个验证接收发布的环境的可用性,另一个验证部署的成功。
gates 最常见的应用是针对更高环境的手动审批部署前配置,因此正在进行的测试或实际的公共 web 应用不会受到危害。 Azure DevOps 组织中的任何用户都可以进行手动审批。 审批可以设置为在特定时间后过期,所选的审批人员可以委托给其他用户。
外部门户可以是各种服务端点,比如 Azure 功能、外部 web 服务,甚至是同一个 Azure DevOps 项目中的自定义工作项查询:

图 18.21 -释放盖茨
使用固有的触发和门功能,复杂的发布工作流可以按需或以自动的方式设置和执行。
Xamarin 释放模板
在 Xamarin 发布管道中,我们将接收用于多个平台和环境的多个应用包作为构建工件。 例如,考虑以下 Xamarin Android 的 CI 构建设置,其中我们将收到三个 QA、UAT 和 PROD 包:

图 18.22 -发布 Xamarin 工件
如果我们要为 iOS 创建一个类似的多代理构建,并将这些构建设置为在任何发布分支的传入提交时触发,我们将为每个部署环境创建一个新的应用:

图 18.23 -持续集成触发器
App Center 发布的发布管道现在可以引用这些构件并将它们部署到一个特定的 App Center 环上。 App Center 能够将应用包推送到 App Store,所以我们可以在 App Center 上创建一个生产环,将应用包从 App Center 部署到生产。
不同移动平台在并行环上的部署可以并行化为并行作业,也可以并行化为收敛于同步阶段的并行阶段:

图 18.24 - Xamarin 发布模板
同步阶段(例如Beta-Start和Beta-Finish)上的闸门可用于控制部署到某些分发环。
Azure web 应用发布
对于 Azure 基础设施,我们也将接收多个包。 Azure 部署管道中最重要的包是 ARM 模板和定义应用配置的相关配置参数文件。 可以直接从源存储库检索这些资源,也可以在 CI 构建期间使用用于复制文件的基本实用程序任务对它们进行打包,并(可选地)将它们打包到 ZIP 容器中。
重要提示
在构建 CI 期间可以使用的另一个有用工具是 Azure 资源组部署任务的验证模式。 通过这种方式,CI 构建可以验证在至少一个可用环境中引入的 ARM 模板更改。
API 服务,取决于所选择的托管选项(即,容器化,或打包为 web 部署,等等),也将被创建为部署构件。
然后发布管道将 ARM 模板部署到目标资源组。 一旦资源管理器部署完成,web 应用包可以发布到目标应用服务实例或应用服务插槽,这取决于部署策略。
与 Xamarin 部署类似,发布管道可以配置为使用多个阶段来定义环境或使用多个部署的多代理阶段。 例如,将部署组件划分为多个阶段的示例发布管道如下所示:

图 18.25 - Azure 发布模板
在任何一种场景中,发布管理流程都应该是相同的:部署基础设施和配置,然后继续进行服务包部署。
总结
在本章中,我们完成了 Xamarin 存储库和 Azure web 基础设施的 CI/CD 管道。 我们已经看到 Azure DevOps 提供的工具集非常适合实现 GitFlow 分支策略。 该工具集还用于通过实现分支策略和设置 CI 触发器来管理应用生命周期。 此外,我们还了解了如何使用 CI 阶段来维护代码质量和技术债务。 最后,我们讨论了为分布式 Azure 和本地移动应用实现发布管道的策略。
通过这最后一章,我们已经达到了项目开发的最后阶段。 在本书的开头,在刷新了我们关于各种。net 概念、运行时、框架和平台的知识之后,我们转向了 Xamarin 开发。 我们使用。net 标准框架和 Xamarin 平台运行时创建和定制 Xamarin 应用。 希望您已经了解了在 Xamarin 上的何处使用哪种类型的定制。 形式框架。 一旦我们完成了 Xamarin 项目,我们的重点就转移到 Azure 云堆栈,以及我们如何在各种 Azure 服务上利用。net Core,这些服务可以与 Xamarin 移动应用一起使用。 Azure 堆栈的讨论主要集中在平台即服务(Platform as a Service)上,比如 App Service、无服务器组件,最后是数据存储服务,比如 Cosmos DB。 此外,我们还学习了如何在推送通知、Graph API 和认知服务等外部服务的帮助下更好地吸引用户。 我们慢慢地创建了移动应用和后端基础设施,最后一节是关于如何使用 Microsoft Azure DevOps 和 Microsoft Visual Studio App Center 有效地管理我们项目的生命周期。 通过使用 Azure DevOps,我们试图通过创建自动化 CI/CD 管道来实现现代 DevOps 概念。
虽然实现和实际示例非常一般化,但我们在本书中讨论的实践概念将是您和您的团队计划进行的任何跨平台开发项目的良好起点。 对于任何。net 开发人员来说,理解。net Core 和。net Standard 的其他实现是解锁多个平台并创建跨平台的用户体验的关键。**
第一部分:理解 .NET
用 Xamarin 实现跨平台应用的基本要求是了解. net 生态系统和支持微软的堆栈。 本书的这一部分具体地带领你走过。net 的发展历程,以及如何将它有效地用于移动项目。
本节由以下各章组成:
第二部分:Xamarin 和 Xamarin.Forms
Xamarin 作为一个平台提供了各种开发模型和策略。 每个模型和框架都可以用来满足不同的项目需求。 理解这些不同的开发方法将有助于开发人员应对不断变化的客户需求。 本书的这一部分主要介绍 Xamarin 平台上的实际开发示例。
本节由以下各章组成:
第三部分:Azure 云服务
创建现代移动应用通常需要健壮的服务后端和基础设施。 Azure 云基础设施为开发人员提供了广泛的服务和开发平台,以创建可与移动应用一起使用的。net Core 组件。 这些服务从简单的平台即服务(PaaS)托管组件到复杂的多模型持久性存储。
本节将涵盖以下各章:
- 第七章,Azure Services for Mobile Applications
- 第八章,使用 Cosmos DB 创建数据存储
- 第九章、创建 Azure 应用服务
- 第十章,使用。net Core for Azure 无服务器
第四部分:高级移动开发
对于那些不满足于最低要求的开发者来说,响应性、粘性和异步等术语将成为区分手机应用的关键因素。 提供模式和工具来创建一个应用,该应用将提供一个快速和流畅的用户界面,并以个性化的方式吸引用户,这是未来章节的目标之一。
本节将涵盖以下各章:
第五部分:应用生命周期管理
当我们谈到。net Core 和 Xamarin 时,Azure DevOps 和 Visual Studio App Center 是应用生命周期管理的两大支柱。 Azure DevOps,以前称为 Visual Studio Online 或 Team Services,为实现 DevOps 原则提供了一个完整的套件,而 App Center 则充当移动应用开发、测试和部署的指挥中心。 使用这些工具,开发人员和操作团队可以实现健壮且高效的交付管道,可以将应用源从存储库带到生产环境中。
本节将涵盖以下各章:






























































浙公网安备 33010602011771号