使用-UNO-平台创建跨平台-C--应用-全-

使用 UNO 平台创建跨平台 C# 应用(全)

原文:zh.annas-archive.org/md5/1FD2D236733A02B9975D919E422AEDD3

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

开发人员越来越多地被要求构建在多个操作系统和浏览器上运行的本机应用程序。过去,这意味着学习新技术并制作多个应用程序副本。但是 Uno 平台允许您使用已经熟悉的用于构建 Windows 应用程序的工具、语言和 API 来开发也可以在其他平台上运行的应用程序。本书将帮助您创建面向客户以及业务线的应用程序,这些应用程序可以在您选择的设备、浏览器或操作系统上使用。

这本实用指南使开发人员能够利用他们的 C#和 XAML 知识,使用 Uno 平台编写跨平台应用程序。本书充满了技巧和实际示例,将帮助您构建常见场景的应用程序。您将首先通过逐步解释基本概念来了解 Uno 平台,然后开始为不同的业务线创建跨平台应用程序。在本书中,您将使用示例来教您如何结合您现有的知识来管理常见的开发环境并实现经常需要的功能。

通过本 Uno 平台开发书的学习,您将学会如何使用 Uno 平台编写自己的跨平台应用程序,并使用其他工具和库来加快应用程序开发过程。

本书适合的读者

本书适用于熟悉 Windows 应用程序开发并希望利用其现有技能构建跨平台应用程序的开发人员。要开始阅读本书,需要具备 C#和 XAML 的基本知识。任何具有使用 WPF、UWP 或 WinUI 进行应用程序开发的基本经验的人都可以学会如何使用 Uno 平台创建跨平台应用程序。

本书涵盖内容

第一章《介绍 Uno 平台》介绍了 Uno 平台,解释了它的设计目的以及何时使用它。之后,本章将介绍如何设置开发机器并安装必要的工具。

第二章《编写您的第一个 Uno 平台应用程序》介绍了创建您的第一个 Uno 平台应用程序,并涵盖了应用程序的结构。通过本章结束时,您将已经编写了一个可以在不同平台上运行并根据应用程序运行的操作系统显示内容的小型 Uno 平台应用程序。

第三章《使用表单和数据》将带您开发一个以数据为重点的虚构公司 UnoBookRail 的业务线应用程序。本章涵盖了显示数据,对表单进行输入验证以及将数据导出为 PDF。

第四章《使您的应用程序移动化》介绍了使用 Uno 平台开发移动应用程序。除此之外,本章还涵盖了在具有不稳定互联网连接的设备上使用远程数据,根据应用程序运行的平台对应用程序进行样式设置,以及使用设备功能,如相机。

第五章《使您的应用程序准备好迎接现实世界》涵盖了编写面向外部客户的移动应用程序。作为其中的一部分,它涵盖了在设备上本地持久化数据,本地化您的应用程序,并使用 Uno 平台编写可访问的应用程序。

第六章《在图表和自定义 2D 图形中显示数据》探讨了在 Uno 平台应用程序中显示图形和图表。本章涵盖了使用诸如 SyncFusion 之类的库以及使用 SkiaSharp 创建自定义图形。最后,本章介绍了编写响应屏幕尺寸变化的用户界面。

第七章测试您的应用程序,向您介绍了使用 Uno.UITest 进行 UI 测试。此外,本章还涵盖了使用 WinAppDriver 编写自动化 UI 测试,为应用程序的 Windows 10 版本编写单元测试,以及测试应用程序的可访问性。

第八章部署您的应用程序并进一步,将指导您将 Xamarin.Forms 应用程序带到 Uno 平台上,并将 WASM Uno 平台应用程序部署到 Azure。之后,本章将介绍部署 Uno 平台应用程序并加入 Uno 平台社区。

充分利用本书

在本书中,我们将使用 Windows 10 上的 Visual Studio 2019 和.NET CLI 来开发 Uno 平台应用程序。我们将介绍安装必要的扩展和 CLI 工具;但是,安装 Visual Studio 和.NET CLI 将不在范围之内。要安装所需的软件,您需要一个正常的互联网连接。

如果您使用本书的数字版本,我们建议您自己输入代码或从书的 GitHub 存储库中访问代码(下一节中提供了链接)。这样做将帮助您避免与复制和粘贴代码相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub 上的github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform下载本书的示例代码文件。如果代码有更新,将在 GitHub 存储库中更新。

我们还提供了来自我们丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。快来看看吧!

代码实战

本书的“代码实战”视频可在bit.ly/3yHTfYL上观看

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图和图表的彩色图像。您可以在此处下载:static.packt-cdn.com/downloads/9781801078498_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

文本中的代码:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这是一个例子:“在UnoAutomatedTestsApp文件夹内,创建一个名为UnoAutomatedTestsApp.UITests的文件夹。”

代码块设置如下:

private void ChangeTextButton_Click(object sender,
                                    RoutedEventArgs e)
{
    helloTextBlock.Text = "Hello from code behind!";
}

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

<skia:SKXamlCanvas 
xmlns:skia="using:SkiaSharp.Views.UWP" 
PaintSurface="OnPaintSurface" />

任何命令行输入或输出都以以下方式编写:

dotnet new unoapp -o MyApp

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。这是一个例子:“单击菜单栏中的View,然后单击Test Explorer,打开Test Explorer。”

提示或重要说明

以这种方式出现。

第一部分:了解 Uno 平台

本书的这一部分将为您提供关于 Uno 平台的所有信息,以及如何确定哪些项目适合使用它的知识。然后,它将详细介绍如何设置开发环境以构建 Uno 平台的应用程序,并指导您创建您的第一个应用程序。然后,它将探讨使用 Uno 平台构建的应用程序的基础知识,并展示您如何使用您已经熟悉的工具和技能。此外,它将向您展示开发人员在大多数应用程序中需要执行的一些最常见任务。

在本节中,我们包括以下章节:

  • 第一章,介绍 Uno 平台

  • 第二章,编写您的第一个 Uno 平台应用程序

第一章:介绍 Uno 平台

Uno 平台是一个跨平台、单一代码库解决方案,用于开发在各种设备和操作系统上运行的应用程序。它在丰富的 Windows 开发 API 和工具基础上构建。这使您可以利用您已经拥有的 Windows 应用程序开发技能,并将其用于构建 Android、iOS、macOS、WebAssembly、Linux 等应用程序。

本书将是您学习 Uno 平台的指南。它将向您展示如何使用 Uno 平台的功能来构建各种解决现实场景的不同应用程序。

在本章中,我们将涵盖以下主题:

  • 了解 Uno 平台是什么

  • 使用 Uno 平台

  • 设置您的开发环境

通过本章结束时,您将了解为什么要使用 Uno 平台开发应用程序,以及它最适合帮助您构建哪些类型的应用程序。您还将能够设置您的环境,以便在阅读本书后续章节时准备开始构建应用程序。

技术要求

在本章中,您将被引导完成设置开发机器的过程。要在本书中的所有示例中工作,您需要运行以下任何一种操作系统的机器:

  • Windows 10(1809)或更高版本

  • macOS 10.15(Catalina)或更高版本

如果您只能访问一个设备,您仍然可以跟随本书的大部分内容。本书将主要假设您正在使用 Windows 机器。我们只会在绝对必要时展示使用 Mac 的示例。

本章没有源代码。但是,其他章节的代码可以在以下网址找到:github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform

了解 Uno 平台是什么

根据网站(platform.uno/),Uno 平台是“用于 Windows、WebAssembly、iOS、macOS、Android 和 Linux 的单一代码库应用程序的第一个和唯一 UI 平台。”

这是一个复杂的句子,让我们分解一下关键元素:

  • 作为一个 UI 平台,它是一种构建具有用户界面UI)的应用程序的方式。这与那些基于文本并从命令行(或等效方式)运行、嵌入在硬件中或以其他方式进行交互的平台形成对比,比如通过语音。

  • 使用单一代码库意味着您只需编写一次代码,即可在多个设备和操作系统上运行。具体来说,这意味着相同的代码可以为应用程序将在的每个平台编译。这与将代码转换或转译为另一种编程语言然后编译为另一个平台的工具形成对比。它也是唯一的单一代码库,而不是输出。一些可比较的工具在每个操作系统上创建一个独特的包,或者在 HTML 和 JavaScript 中创建所有内容,并在嵌入式浏览器中运行。Uno 平台都不这样做。相反,它为每个平台生成本机应用程序包。

  • Windows 应用程序基于 Windows 10 的Universal Windows PlatformUWP)。微软目前正在进行工作,将WinUI 3作为 UWP 的继任者。Uno 平台已与微软合作,以确保 Uno 平台可以在 WinUI 3 达到可比较的操作水平时轻松过渡。

  • Windows 支持还包括由 SkiaSharp 提供支持的Windows Presentation FoundationWPF),用于需要在较旧版本的 Windows(7.1 或 8.1)上运行的应用程序。

  • 在 WebAssembly 中运行的应用程序将所有代码编译为在 Web 浏览器中运行。这意味着它们可以在任何兼容浏览器的设备上访问,而无需在服务器上运行代码。

  • 通过支持 iOS,创建的应用程序可以在 iPhone 和 iPad 上运行。

  • 对于 macOS 的支持,应用程序可以在 MacBook、iMac 或 Mac Mini 上运行。

  • 对 Android 的支持适用于运行 Android 操作系统的手机和平板电脑。

  • Linux 支持适用于特定的 Linux PC 等价发行版,并由 SkiaSharp 提供支持。

Uno 平台通过重用 Microsoft 为构建 UWP 应用程序创建的工具、API 和 XAML 来完成所有这些工作。

回答“Uno 平台是什么?”的另一种方式是,它是一种一次编写代码,到处运行的方式。 “到处”这个确切的定义并不精确,因为它不包括每个能够运行代码的嵌入式系统或微控制器。然而,许多开发人员和企业长期以来一直希望一次编写代码,并在多个平台上轻松运行。Uno 平台使这成为可能。

微软的 UWP 早期的批评之一是它只在 Windows 上是通用的。有了 Uno 平台,开发人员现在可以真正地使他们的 UWP 应用程序变得真正通用。

Uno 平台的简要历史

随着当今跨平台工具的多样化,很容易忘记 2013 年的选择有多有限。那时,没有通用工具可以轻松构建在多个操作系统上运行的本地应用程序。

就在那时,加拿大软件设计和开发公司nventive(nventive.com/)面临着一个挑战。他们在为 Windows 和 Microsoft 工具构建应用程序方面拥有大量知识和经验,但他们的客户也要求他们为 Android 和 iOS 设备创建应用程序。他们发明了一种方法,将他们为 Windows Phone(后来是 UWP)应用程序编写的代码编译并转移到其他平台,而不是重新培训员工或通过为不同平台构建多个版本的相同软件来复制工作。

到 2018 年,很明显这种方法对他们来说是成功的。然后他们做了以下两件事:

  1. 他们将他们创建的工具转变为一个开源项目,称之为 Uno 平台。

  2. 他们增加了对 WebAssembly 的支持。

作为一个开源项目,这使得其他开发人员解决同样的问题可以共同合作。Uno 平台自那时以来已经看到了来自 200 多名外部贡献者的数千次贡献,并且参与度已经扩展到支持更多平台,并为最初支持的平台添加额外功能。

作为一个开源项目,它是免费使用的。此外,它得到了一家商业模式由 Red Hat 广泛采用的公司的支持。使用是免费的,并且有一些免费的公共支持。然而,专业支持、培训和定制开发只能通过付费获得。

Uno 平台的工作原理

Uno 平台以不同的方式工作,并使用多种基础技术,具体取决于您要构建的平台。这些总结在图 1.1中:

  • 如果您正在为 Windows 10 构建应用程序,Uno 平台不会做任何事情,而是让所有 UWP 工具编译和执行您的应用程序。

  • 如果您正在为 iOS、macOS 或 Android 构建应用程序,Uno 平台会将您的 UI 映射到本机平台等效,并使用本机Xamarin库调用其正在运行的操作系统。它会为每个操作系统生成适当的本机包。

  • 如果您正在构建一个 WebAssembly 应用程序,Uno 平台会将您的代码编译成mono.wasm运行时,并将 UI 映射到 HTML 和 CSS。然后,它将其打包成一个.NET库,作为静态 Web 内容与 Uno 平台 Web 引导程序一起启动。

  • 为了创建 Linux 应用程序,Uno 平台将您的代码转换为.NET等效,并使用GTK3.NET5应用程序来呈现 UI。

  • Uno 平台通过将编译后的代码包装在一个简单的WPFNETCore 3.1)应用程序中,并使用SkiaSharp来渲染 UI,从而创建了 Windows 7 和 8 的应用程序。

请参考以下图表:

图 1.1 - Uno 平台的高级架构

图 1.1 - Uno 平台的高级架构

无论您要构建的操作系统或平台是什么,Uno 平台都使用该平台的本机控件。这使您的应用程序能够获得完全本机应用程序的体验和性能。唯一的例外是它使用 SkiaSharp。通过使用 SkiaSharp,Uno 平台在画布上绘制所有 UI 内容,而不是使用平台本机控件。Uno 平台不会向正在运行的应用程序添加额外的抽象层(就像使用容器的跨平台解决方案可能会在外壳应用程序中使用嵌入的 WebView 一样)。

Uno 平台使您能够使用单个代码库做很多事情。但它能做到一切吗?

它是灵丹妙药吗?

编写代码一次并在所有地方运行该代码的原则既强大又吸引人。然而,有必要意识到以下两个关键点:

  • 并非所有应用程序都应该为所有平台创建。

  • 这并不是不了解应用程序将在哪些平台上运行的借口。

此外,并非所有事情都需要应用程序。假设您只想分享一些不经常更新的信息。在这种情况下,静态网页的网站可能更合适。

只是因为你能做某事并不意味着你应该这个教训也适用于应用程序。当您看到创建可以在多个平台上运行的应用程序是多么容易时,您可能会被诱惑在您可以的所有地方部署您的应用程序。在这样做之前,您需要问一些重要的问题:

  • 应用程序是否在所有平台上都需要或想要?人们是否希望并需要在您提供的所有平台上使用它?如果不是,您可能会浪费精力将其放在那里。

  • 应用程序在所有平台上都有意义吗?假设应用程序的关键功能涉及在户外捕捉图像。在 PC 或 Mac 上提供它是否有意义?相反,如果应用程序需要输入大量信息,这是人们愿意在手机的小屏幕上做的吗?您对应用程序在哪里可用的决定应该由其功能和将使用它的人员决定。不要让您的决定仅基于可能性。

  • 您能在所有平台上支持它吗?通过在平台上发布、维护和支持应用程序来获得的价值是否能够证明在该平台上释放、维护和支持应用程序的时间和精力?如果只有少数人在特定类型的设备上使用应用程序,但他们产生了许多支持请求,重新评估您对这些设备的支持是可以的。

没有技术能为所有场景提供完美的解决方案,但希望您已经看到 Uno 平台提供的机会。现在让我们更仔细地看看为什么以及何时您可能希望使用它。

使用 Uno 平台

现在您知道了 Uno 平台是什么,我们将看看在选择是否使用它时需要考虑什么。有四个因素需要考虑:

  • 您已经知道的知识。

  • 您希望针对哪些平台?

  • 应用程序所需的功能。

  • 与其他选择相比如何。

让我们探讨 Uno 平台与这些因素的关系。

Uno 平台允许您使用您已经知道的知识

Uno 平台最初是为在Visual Studio中使用 C#和 XAML 的开发人员创建的。如果这对您来说很熟悉,那么开始使用 Uno 平台将会很容易,因为您将使用您已经知道的软件。

如果您已经熟悉 UWP 开发,差异将是最小的。如果您熟悉 WPF 开发,XAML 语法和可用功能会有轻微差异。在阅读本书的过程中,您将学到构建 Uno 平台所需的一切。只要您不期望一切都像 WPF 中那样工作,您就会没问题。此外,由于 WinUI 和 Uno 平台团队正在努力消除存在的轻微差异,您可能永远不会注意到差异。

如果您不了解 C#或 XAML,Uno 平台可能仍然适合您,但是由于本书假定您熟悉这些语言,您可能会发现先阅读* C# 9 and .NET 5 – Modern Cross-Platform Development – Fifth Edition, Mark J. Price, Packt PublishingLearn WinUI 3.0, Alvin Ashcraft, Packt Publishing*会有所帮助。

Uno 平台支持许多平台

Uno 平台的一个伟大之处在于它允许您为多个平台构建应用程序。Uno 平台支持最常见的平台,但如果您需要构建在小众平台或专用设备上运行的应用程序,那么它可能不适合您。此外,如果您需要支持旧版本的平台或操作系统,您可能需要找到解决方法或替代方案。以下表格显示了您可以使用 Uno 平台构建的受支持平台的版本:

图 1.2 - Uno 平台支持的最低受支持平台版本

图 1.2 - Uno 平台支持的最低受支持平台版本

支持多个平台也可能是有利的,即使您希望在不同平台上实现非常不同的应用行为或功能。可以通过创建多个解决方案来支持多个平台,而不是将所有内容合并到单个解决方案中。

Uno 平台声称可以实现高达 99%的代码和 UI 重用。当您需要在所有设备上使用相同的内容时,这非常有用。但是,如果您需要不同的行为或针对不同平台高度定制的 UI(这是我们将在未来章节中探讨的内容),则在不同的解决方案中构建不同的应用程序可能会更容易,而不是在代码中放置大量的条件逻辑。对于有多少条件代码是太多,没有硬性规定,这取决于项目和个人偏好。只需记住,如果您发现您的代码充满了使其难以管理的条件注释,那么这仍然是一个选择。

因此,也可以使用 Uno 平台为单个平台构建应用程序。您可能不希望创建一个可以在任何地方运行的应用程序。您可能只对单个平台感兴趣。如果是这种情况,您也可以使用 Uno 平台。如果您的需求发生变化,未来还可以轻松添加其他平台。

Uno 平台是否能够满足您的应用程序的所有需求?

Uno 平台能够重用 UWP API 构建其他平台的核心在于它具有将 UWP API 映射到其他平台上的等效代码。由于时间、实用性和优先级的限制,并非所有 API 都适用于所有平台。一般指导方针是,最常见的 API 在最广泛的平台上都是可用的。假设您需要使用更专业的功能或针对的不是 Android、iOS、Mac 或 WebAssembly 的其他内容,建议您检查您所需的功能是否可用。

提示

我们建议在开始编写代码之前确认您的应用程序所需的功能是否可用。这将使您能够避免在开发过程的后期出现任何不愉快的惊喜。

由于印刷书籍的永久性以及新功能的频繁添加和更多 API 的支持,不适合在此列出支持的内容。相反,您可以在以下 URL 查看支持功能的高级列表:platform.uno/docs/articles/supported-features.html。还有一个支持的 UI 元素列表,位于以下 URL:platform.uno/docs/articles/implemented-views.html。当然,确认可用和不可用的最终方法是检查以下 URL 的源代码:github.com/unoplatform/uno

如果您尝试使用不受支持的 API,您将在 Visual Studio 中看到提示,如图 1.3所示。如果您在运行时尝试使用此 API,您要么什么也不会得到(NOOP),要么会得到NotSupported异常:

图 1.3 - Visual Studio 中指示不受支持的 API 的示例

图 1.3 - Visual Studio 中指示不受支持的 API 的示例

如有必要,您可以使用Windows.Foundation.Metadata.ApiInformation类在运行时检查支持的功能。

作为一个开源项目,您也可以选择自己添加任何当前不受支持的功能。将这样的添加贡献回项目总是受到赞赏的,团队也始终欢迎新的贡献者。

Uno Platform 与其他替代方案相比如何?

如前所述,有许多工具可用于开发在多个平台上运行的应用程序。我们不打算讨论所有可用的选项,因为它们可以与前面的三个要点进行评估和比较。但是,由于本书旨在面向已经熟悉 C#、XAML 和 Microsoft 技术的开发人员,因此适当提及Xamarin.Forms

Xamarin.Forms是在大约与 Uno Platform 同时创建的,并且有几个相似之处。其中两个关键点是使用 C#和 XAML 来创建在多个操作系统上运行的应用程序。两者都通过提供对包含 C#绑定的Xamarin.iOSXamarin.Android库的抽象来实现这一点。

Uno Platform 与Xamarin.Forms之间的两个最大区别如下:

  • Uno Platform 支持构建更多平台的应用。

  • Uno Platform 重用了 UWP API 和 XAML 语法,而不是构建自定义 API。

第二点对于已经熟悉 UWP 开发的开发人员来说很重要。许多Xamarin.Forms元素和属性的名称听起来相似,因此记住这些变化可能是具有挑战性的。

Xamarin.Forms的第 5 版于 2020 年底发布,预计将是Xamarin.Forms的最后一个版本。它将被.NET 多平台应用 UIMAUI)所取代,作为.NET 6 的一部分。.NET MAUI 将支持从单个代码库构建 iOS、Android、Windows 和 Mac 的应用程序。但是,它将不包括构建 WebAssembly 的能力。微软已经拥有 Blazor 用于构建 WebAssembly,因此不打算将此功能添加到.NET MAUI 中。

.NET 6 将带来许多新的功能。其中一些功能是专门为.NET MAUI 添加的。一旦成为.NET 6 的一部分,这些功能将不仅限于.NET MAUI。它们也将适用于 Uno Platform 应用。其中最明显的新功能之一是拥有一个可以为不同平台生成不同输出的单个项目。这将大大简化所需的解决方案结构。

重要提示

在我们撰写本书时,微软正在准备发布WinUI 3作为下一代 Windows 开发平台。这将建立在 UWP 之上,是Project Reunion努力的一部分,旨在使所有 Windows 功能和 API 对开发人员可用,无论他们使用的 UI 框架或应用程序打包技术如何。

由于 WinUI 3 是 UWP 开发的继任者,Uno 平台团队已经公开表示,计划和准备正在进行中,Uno 平台将过渡到使用 WinUI 3 作为其构建基础。这是与微软合作完成的,允许 Uno 平台团队获取 WinUI 代码并修改以在其他地方工作。您可以放心,您现在制作的任何东西都将有过渡路径,并利用 WinUI 带来的好处和功能。

另一个类似的跨平台解决方案,使用 XAML 来定义应用程序的 UI 的是 Avalonia (avaloniaui.net/)。然而,它不同之处在于它只专注于桌面环境的应用程序。

现在您已经对 Uno 平台是什么以及为什么要使用它有了扎实的了解,您需要设置好您的机器,以便编写代码和创建应用程序。

设置您的开发环境

现在您已经熟悉 Uno 平台,无疑渴望开始编写代码。我们将在下一章开始,但在那之前,您需要设置好开发环境。

Visual Studio 是开发 Uno 平台应用程序最流行的集成开发环境IDE)。其中一个重要原因是它具有最广泛的功能集,并且对构建 UWP 应用程序的支持最好。

使用 Visual Studio 进行开发

使用 Visual Studio 构建 Uno 平台应用程序,您需要做以下三件事:

  • 确保您有Visual Studio 2019版本16.3或更高版本,尽管建议使用最新版本。

  • 安装必要的工作负载。

  • 安装项目和项目模板。

安装所需的工作负载

作为 Visual Studio 的一部分可以安装的许多工具、库、模板、SDK 和其他实用程序统称为组件。有超过 100 个可用的组件,相关组件被分组到工作负载中,以便更容易选择所需的内容。您可以在Visual Studio 安装程序中选择工作负载,这些显示在图 1.4中:

图 1.4 - Visual Studio 安装程序显示各种工作负载选项

图 1.4 - Visual Studio 安装程序显示各种工作负载选项

要构建 Uno 平台应用程序,您需要安装以下工作负载:

  • 通用 Windows 平台开发

  • 使用.NET 进行移动开发

  • ASP.NET 和 Web 开发

  • .NET Core 跨平台开发

从市场安装所需的模板

为了更容易构建您的 Uno 平台应用程序,提供了多个项目和项目模板。这些作为Uno 平台解决方案模板扩展的一部分安装。您可以从 Visual Studio 内部安装这个,或者直接从市场安装。

从 Visual Studio 内部安装模板

要安装包含模板的扩展,请在 Visual Studio 中执行以下操作:

  1. 转到扩展>管理扩展

  2. 搜索Uno。它应该是第一个结果。

  3. 点击下载按钮。

  4. 点击关闭,让扩展安装程序完成,然后重新启动Visual Studio

图 1.5 - 在“管理扩展”对话框中显示的 Uno 平台解决方案模板

图 1.5 - 在“管理扩展”对话框中显示的 Uno 平台解决方案模板

从市场安装模板

按照以下步骤从市场安装扩展:

  1. 转到marketplace.visualstudio.com并搜索Uno。它应该是返回的第一个结果。

或者,直接转到以下网址:marketplace.visualstudio.com/items?itemName=nventivecorp.uno-platform-addin

  1. 点击下载按钮。

  2. 双击下载的.vsix文件以启动安装向导。

  3. 按照向导中的步骤操作。

安装了工作负载和模板后,您现在可以开始构建应用程序了。但是,如果您想要开发 iOS 或 Mac 应用,您还需要设置 Mac 设备,以便您可以从 Windows 上的 Visual Studio 连接到它。

使用其他编辑器和 IDE

在 Windows PC 上使用 Visual Studio 2019 并不是强制的,Uno 平台团队已经努力使构建 Uno 平台应用程序尽可能灵活。因此,您可以在现有的工作模式和偏好中使用它。

使用命令行安装所需的模板

除了在 Visual Studio 中使用模板外,还可以通过命令行安装它们以供使用。要以这种方式安装它们,请在命令行或终端中运行以下命令:

dotnet new -i Uno.ProjectTemplates.Dotnet

完成此命令后,它将列出所有可用的模板。您应该看到多个以uno开头的短名称条目。

使用 Visual Studio for Mac 构建 Uno 平台应用程序

要使用 Visual Studio for Mac 构建 Uno 平台应用程序,您将需要以下内容:

  • Visual Studio for Mac 版本 8.8 或更高(建议使用最新版本)。

  • Xcode 12.0或更高(建议使用最新版本)。

  • Apple ID。

  • .NET Core 3.15.0 SDK

  • GTK+3(用于运行Skia/GTK项目)。

  • 安装的模板(参见上一节)。

  • 通过打开首选项菜单选项,然后选择其他>预览功能并选中在新项目对话框中显示所有.NET Core 模板,可以使模板在 Visual Studio for Mac 中可见。

所有这些的链接都可以在以下网址找到:platform.uno/docs/articles/get-started-vsmac.html

使用 Visual Studio Code 构建 Uno 平台应用程序

您可以使用 Visual Studio Code 在 Windows、Linux 或 Mac 上构建 WebAssembly 应用程序。目前尚不支持使用它构建其他平台的应用程序。

要使用 Visual Studio Code 构建 Uno 平台应用程序,您将需要以下内容:

  • Visual Studio Code(建议使用最新版本)

  • Mono

  • .NET Core 3.15.0 SDK

  • 安装的模板(参见上一节)

  • Visual Studio CodeC#扩展

  • Visual Studio CodeJavaScript Debugger(夜间版)扩展

所有这些的链接都可以在以下网址找到:platform.uno/docs/articles/get-started-vscode.html

使用 JetBrains Rider 构建 Uno 平台应用程序

可以在 Windows、Mac 和 Linux 上使用JetBrains Rider,但并非所有版本都可以构建所有平台。

要使用 JetBrains Rider 构建 Uno 平台应用程序,您将需要以下内容:

  • Rider 版本 2020.2或更高,建议使用最新版本

  • Rider Xamarin Android Support Plugin

  • .NET Core 3.1 和 5.0 SDK

  • 安装的模板(参见上一节)

在使用 JetBrains Rider 时,还有一些额外的注意事项,如下所示:

  • 目前还无法从 IDE 内部调试 WebAssembly 应用程序。作为一种解决方法,可以使用 Chromium 浏览器中的调试器。

  • 如果在 Mac 上构建Skia/GTK项目,还需要安装GTK+3

  • 如果您希望在 Windows PC 上构建 iOS 或 Mac 应用程序,您将需要连接的 Mac(就像使用 Visual Studio 一样)。

所有这些链接和更多详细信息都可以在以下 URL 中找到:platform.uno/docs/articles/get-started-rider.html

重要提示

还可以使用 Blend for Visual Studio(在 Windows 上)来处理代码,就像对常规 UWP 应用程序一样。但是,Blend 不支持 Uno Platform 解决方案包含的所有项目类型。您可能会发现,有一个不包含这些项目的解决方案的单独版本,并且可以在 Blend 中访问该版本,这是有益的。

检查您的设置

Uno Platform 有一个dotnet 全局工具,可以检查您的机器是否设置正确,并引导您解决它发现的任何问题。它被称为uno-check,非常简单易用,如下所示:

  1. 打开开发人员命令提示符、终端或 PowerShell 窗口。

  2. 通过输入以下内容安装该工具:

dotnet tool install --global Uno.Check
  1. 通过输入以下内容运行该工具:
uno-check
  1. 按照它给出的任何提示,并享受查看以下消息:恭喜,一切看起来都很棒!

调试您的设置

无论您使用哪种 IDE 或代码编辑器,都会有许多部分,使用多个工具、SDK 甚至机器可能会让人难以知道在出现问题时从何处开始。以下是一些通用提示,可帮助找出问题所在。其中一些可能看起来很明显,但我宁愿因为提醒您检查一些明显的东西而显得愚蠢,也不愿让您浪费时间在未经检查的假设上:

  • 尝试重新启动您的机器。是的,我知道,如果它经常不起作用,那将会很有趣。

  • 仔细阅读任何错误消息,然后再次阅读。它们有时可能会有所帮助。

  • 检查您是否已正确安装所有内容。

  • 有什么改变了吗?即使您没有直接做,也可能已经自动或在您不知情的情况下进行了更改(包括但不限于操作系统更新、安全补丁、IDE 更新、其他应用程序的安装或卸载以及网络安全权限更改)。

  • 如果有一个东西已经更新了,所有依赖项和引用的组件也已经更新了吗?通常情况下,当事物相互连接、共享引用或通信时,它们必须一起更新。

  • 任何密钥或许可证已过期吗?

  • 如果以前创建的应用程序出现问题,您可以创建一个新的应用程序并编译和运行吗?

  • 您可以创建一个新的应用程序,并确认它在每个平台上都可以编译和运行吗?

  • 如果在 Windows 上,您可以创建一个新的空白 UWP 应用程序,然后编译和调试它吗?

尝试使用其他工具进行等效操作或创建等效应用程序通常会产生不同的错误消息。此外,您还可能找到解决方案的路径,可以修复 Uno Platform 项目设置中的问题:

  • 如果使用 WebAssembly 应用程序,您可以创建一个新的空白ASP.NET Web 应用程序或Blazor项目,并编译和调试吗?

  • 如果 WebAssembly 应用程序在一个浏览器中无法工作,浏览器日志或调试窗口中是否显示错误消息?它在另一个浏览器中工作吗?

  • 对于Xamarin.Forms应用程序?

  • 如果存在特定于 Android 的问题,您可以使用 Android Studio 创建和调试应用程序吗?

  • 如果使用 Mac,您可以使用 Xcode 创建和调试空白应用程序吗?

有关解决常见设置和配置问题的其他提示可以在以下两个 URL 中找到:

如果问题来自从 PC 连接到 Mac,Xamarin 文档可能会有所帮助。它可以在以下 URL 找到:docs.microsoft.com/en-us/xamarin/ios/get-started/installation/windows/connecting-to-mac/。这也可以帮助识别和解决 Uno Platform 项目中的问题。

有关特定 Uno 平台相关问题答案的详细信息可以在第八章中找到,部署您的应用程序并进一步

总结

在本章中,我们了解了 Uno 平台是什么,它设计解决的问题以及我们可以将其用于哪些项目类型。然后,我们看了如何设置开发环境,使其准备好以便使用 Uno 平台构建第一个应用程序。

在下一章中,我们将构建我们的第一个 Uno 平台应用程序。我们将探索生成解决方案的结构,看看如何在不同环境中进行调试,并在这些不同环境中运行应用程序时进行自定义。我们将看看如何创建可在未来的 Uno 平台项目中使用的可重用库。最后,我们将看看创建 Uno 平台应用程序的其他可用选项。

进一步阅读

本章前面提到了以下标题,如果您对使用 C#和 XAML 不熟悉,这些标题可能提供有用的背景信息:

  • C# 9 and .NET 5 – Modern Cross-Platform Development – Fifth Edition,Price,Packt Publishing(2020 年)

  • 学习 WinUI 3.0,Ashcraft,Packt Publishing(2021 年)

第二章:编写您的第一个 Uno 平台应用程序

在本章中,您将学习如何创建新的 Uno 平台应用程序,并了解典型的 Uno 平台应用程序的结构。首先,我们将介绍默认的 Uno 平台应用程序模板,包括不同的项目,并让您在 Windows 10 上运行您的第一个 Uno 平台应用程序。之后,我们将深入探讨在不同平台上运行和调试应用程序的方法,包括如何使用模拟器和调试应用程序的 WebAssembly(Wasm)版本。

由于 Uno 平台支持众多平台,并且越来越多的平台被添加到支持的平台列表中,因此在本书中,我们将只开发一部分支持的平台。以下平台是最突出和广泛使用的平台,因此我们将以它们为目标:Windows 10,Android,Web/Wasm,macOS 和 iOS。

虽然在本章中我们提到了其他平台以保持完整性,但其他章节将只包括前面提到的平台。这意味着我们不会向您展示如何在LinuxTizenWindows 7/8上运行或测试您的应用程序。

在本章中,我们将涵盖以下主题:

  • 创建 Uno 平台应用程序并了解其结构

  • 运行和调试您的应用程序,包括使用XAML 热重载C#编辑和继续

  • 使用 C#编译器符号和XAML前缀的特定于平台的代码

  • 除了 Uno 平台应用程序之外的其他项目类型

在本章结束时,您将已经编写了您的第一个 Uno 平台应用程序,并根据运行平台进行了定制。除此之外,您将能够使用不同的 Uno 平台项目类型。

技术要求

本章假设您已经设置好了开发环境,包括安装了项目模板,就像在第一章中介绍的那样,介绍 Uno 平台。您可以在此处找到本章的源代码:github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/Chapter02

查看以下视频以查看代码的实际操作:bit.ly/37Dt0Hg

注意

如果您使用本书的数字版本,我们建议您自己输入代码或从书的 GitHub 存储库中访问代码。这样做将有助于避免与复制和粘贴代码相关的任何潜在错误。

创建您的第一个应用程序

创建项目的不同方式,因此我们将从使用 Visual Studio 的最常见方式开始。

使用 Uno 平台解决方案模板创建您的项目

创建 Uno 平台应用程序项目的过程与在 Visual Studio 中创建其他项目类型的过程相同。根据安装的扩展和项目模板,当过滤Uno 平台时,您将看到图 2.1中的选项列表。请注意,对于图 2.1,只安装了Uno 平台解决方案模板扩展:

图 2.1 - 新项目对话框中 Uno 平台项目模板的列表

图 2.1 - 新项目对话框中 Uno 平台项目模板的列表

使用多平台应用程序(Uno 平台)项目模板是开始使用 Uno 平台的最简单方式,因为它包含了构建和运行 Uno 平台应用程序所需的所有项目。

让我们通过选择多平台应用程序(Uno Platform)项目类型并单击下一步来开始创建您的应用程序。 请注意,您不要选择多平台库(Uno Platform)选项,因为那将创建一个不同的项目类型,我们将在超越默认跨平台应用程序结构部分中介绍。 现在,您需要选择项目的名称、位置和解决方案名称,如图 2.2中所示:

图 2.2 - 配置多平台应用程序(Uno Platform)

在我们的案例中,我们将称我们的项目为HelloWorld,并将其保存在D:\Projects下,这意味着项目将存储在D:\Projects\HelloWorld中,而HelloWorld.sln解决方案将是顶级元素。 当然,您可以在任何您想要的文件夹中创建项目; D:\Projects只是一个例子。 请注意,您应尽可能靠近驱动器根目录创建项目,以避免路径过长的问题。 单击创建后,Visual Studio 将为您创建项目并打开解决方案。 您将在Solution Explorer中看到所有生成的项目。

如果您在 Visual Studio for Mac 中创建项目,生成的解决方案将包括Windows Presentation FoundationWPF)和Universal Windows PlatformUWP)应用程序的项目头。 项目或平台头是在为特定平台编译应用程序时将被编译的相应项目。 因此,在 Windows 10 的情况下,将编译 UWP 头。 您需要使用 Windows PC 来构建这些应用程序。 如果您不想为这些平台构建,可以从解决方案中删除这些项目。 如果您将在 Windows 机器上单独构建这些项目,请在 Mac 上工作时从解决方案中卸载它们。

由于您的应用程序可能不针对 Uno Platform 支持的每个平台,您可能希望为您的应用程序删除那些头。 要做到这一点,请通过右键单击项目视图中的项目并单击删除来从解决方案中删除这些项目,如图 2.3所示:

图 2.3 - 从解决方案中删除 Skia.Tizen 头

图 2.3 - 从解决方案中删除 Skia.Tizen 头

从解决方案中删除项目后,项目仍然存在于磁盘上。 要完全删除它,您必须打开项目文件夹并删除相应的文件夹。 由于我们只针对 Windows 10、Android、Web、macOS 和 iOS,您可以从解决方案中删除Skia.GTKSkia.TizenSkia.WpfSkia.WpfHost项目。

使用.NET CLI 创建您的项目

当然,您不必使用 Visual Studio 来创建您的 Uno Platform 应用程序。 您还可以使用 Uno Platform 的dotnet new模板。 您可以通过打开终端并输入以下内容来创建新项目:

dotnet new unoapp -o MyApp

这将创建一个名为MyApp的新项目。 您可以在 Uno Platform 的模板文档中找到所有 dotnet new 模板的概述(platform.uno/docs/articles/get-started-dotnet-new.html)。

当然,并非每个人都希望将其应用程序针对每个平台,也不是每个应用程序都适合在每个平台上运行。 您可以通过在命令中包含特定标志来选择不为特定平台创建目标项目(下一节将详细介绍这些内容)。 例如,使用以下命令,您将创建一个不在 Linux 和其他 Skia 平台上运行的新项目,因为我们排除了 Skia 头:

dotnet new unoapp -o MyApp -skia-wpf=false -skia-gtk=false     -st=false

要获取unoapp模板的所有可用选项列表,可以运行dotnet new unoapp -h

项目结构和头

在 Windows 上使用 Uno 平台解决方案模板在 Visual Studio 中创建项目时,Platforms文件夹和HelloWorld.Shared共享 C#项目中有两个不同的顶级元素。请注意,在解决方案视图中,这些是两个顶级元素,但是Platforms文件夹在磁盘上不存在。相反,所有项目,包括共享项目,都有自己的文件夹,如图 2.4所示:

图 2.4 - 文件资源管理器中的 HelloWorld 项目

图 2.4 - 文件资源管理器中的 HelloWorld 项目

在生成的解决方案的根目录中有一个名为.vsconfig的文件。该文件包含了与生成的项目一起使用所需的所有 Visual Studio 组件的列表。如果您按照第一章中介绍 Uno 平台的方式设置了您的环境,那么您将拥有所需的一切。但是,如果您看到图 2.5中的提示,请单击安装链接并添加缺少的工作负载:

图 2.5 - 在 Visual Studio 中缺少组件的警告

图 2.5 - 在 Visual Studio 中缺少组件的警告

Platforms解决方案文件夹下,您将找到每个受支持平台的C#项目:

  • HelloWorld.Droid.csproj 用于 Android

  • HelloWorld.iOS.csproj 用于 iOS

  • HelloWorld.macOS.csproj 用于 macOS

  • HelloWorld.Skia.Gtk.csproj 用于带有 GTK 的 Linux

  • HelloWorld.Skia.Tizen.csproj 用于 Tizen

  • HelloWorld.Skia.Wpf.csproj:用于 Windows 7 和 Windows 8 的基本项目

  • HelloWorld.Skia.Wpf.WpfHost.csproj:用于 Windows 7 和 Windows 8 上的HelloWorld.Skia.Wpf项目的主机

  • HelloWorld.UWP.csproj 用于 Windows 10

  • HelloWorld.Wasm.csproj 用于 WebAssembly(WASM)

这些项目也被称为 iOS 的UIApplication,在 macOS 上创建和显示NSApplication,或在 WASM 上启动应用程序。

基于平台的一些特定设置和配置,例如应用程序所需的权限,将根据平台而异。一些平台允许您无任何限制地使用 API。相反,其他平台更加禁止,并要求您的应用程序事先指定这些 API 或要求用户授予权限,这是您必须在头项目中配置的内容。由于这些配置需要在各个头项目中完成,因此在不同平台上的体验将有所不同。在第三章中配置平台头时,我们将仅涵盖部分这些差异,使用表单和数据(Mac、WASM 和 UWP),以及第四章使您的应用程序移动(Android 和 iOS)作为为这些平台开发应用程序的一部分。

与头项目相比,共享项目是几乎所有应用程序代码的所在地,包括页面和视图、应用程序的核心逻辑以及任何资源或图像等资产,这些资产将在每个平台上使用。共享项目被所有平台头引用,因此放在那里的任何代码都将在所有平台上使用。如果您不熟悉 C#共享项目,共享项目只不过是一个在编译引用共享项目的项目时将被包含的文件列表。

像我们的Hello World应用程序这样的新创建的跨平台应用程序已经在共享项目中带有一些文件:

  • App.xaml.cs:这是应用程序的入口点;它将加载 UI 并导航到MainPage。在这里,您还可以通过取消注释InitializeLogging函数中的相应行来配置事件的日志记录。

  • App.xaml:这包含了常见的 XAML 资源列表,如资源字典和主题资源。

  • MainPage.xaml.cs:这个文件包含了你的MainPage的 C#代码。

  • MainPage.xaml:这是您可以放置MainPage的 UI 的地方。

  • Assets/SharedAssets.md:这是一个演示资产文件,用于展示在 Uno 平台应用程序中如何使用资产。

  • Strings/en/Resources.resw:这也是一个演示资产文件,您可以使用它来开始在 Uno 平台应用程序中进行本地化。

现在您已经熟悉了您的第一个 Uno 平台应用程序的项目结构,让我们深入了解如何构建和运行您的应用程序。

构建和运行您的第一个 Uno 平台应用程序

既然您熟悉了 Uno 平台应用程序的结构,我们可以开始构建和运行您的第一个 Uno 平台应用程序了!在本节中,我们将介绍构建和运行应用程序的不同方法。

在 Windows 上使用 Visual Studio 运行和调试您的应用程序

从 Visual Studio 中运行您的 Uno 平台应用程序与运行常规的 UWP、Xamarin.Forms或 WASM 应用程序完全相同。要在特定设备或模拟器上构建和运行应用程序,可以从启动项目下拉菜单中选择相应的头。请注意,根据所选的配置、目标平台和架构,不是每个项目都会编译成预期的输出,甚至可能根本不会被编译。例如,UWP 项目始终针对明确的架构进行编译,因此在选择任意 CPU架构时将编译为 x86。这意味着并非所有目标架构和项目的组合都会编译成指定的内容,而是会退回到默认架构,例如在 UWP 的情况下是 x86。

要运行 UWP 应用程序,如果尚未选择HelloWorld.UWP项目作为启动项目,请从启动项目下拉菜单中选择HelloWorld.UWP,如图 2.6所示:

图 2.6 - Visual Studio 中的配置、架构、启动项目和目标机器选项

图 2.6 - Visual Studio 中的配置、架构、启动项目和目标机器选项

之后,选择适合您的计算机架构和要运行的运行配置、调试或发布。由于我们将在下一节中调试应用程序,请暂时选择调试。之后,您可以选择要部署到的目标设备,即本地计算机、连接的设备或模拟器。要做到这一点,请使用图 2.7中项目列表右侧的下拉菜单:

图 2.7 - Visual Studio 中的 Android 模拟器列表

图 2.7 - Visual Studio 中的 Android 模拟器列表

然后,您可以通过单击绿色箭头或按下F5来启动项目。应用程序将构建,然后您应该会看到类似图 2.8的东西:

图 2.8 - 运行在 Windows 10 上的 HelloWorld 应用程序的屏幕截图

图 2.8 - 运行在 Windows 10 上的 HelloWorld 应用程序的屏幕截图

恭喜,您刚刚运行了您的第一个 Uno 平台应用程序!当然,在 Windows 上运行应用程序并不是开发跨平台应用程序的唯一部分。在 Android、iOS 和其他平台上运行和调试您的应用程序对于编写跨平台应用程序来说是至关重要的,以确保您的应用程序在所有支持的平台上都能正常运行。

对于 Android 开发,有多种不同的方法可以尝试和运行您的应用程序。一种可能性是使用 Visual Studio 附带的 Android 模拟器。为此,只需从目标列表下拉菜单中选择 Android 模拟器,如图 2.7所示。

注意

如果您还没有添加 Android 模拟器设备映像,您将只看到Android 模拟器作为选项。要了解如何添加和配置设备,Visual Studio 文档(docs.microsoft.com/en-us/xamarin/android/get-started/installation/android-emulator/device-manager)介绍了如何创建新设备并根据您的需求进行配置。

如果您已将 Android 手机连接到计算机,它将显示在可用目标设备列表中。可以在图 2.7中看到 Samsung 设备的示例。

注意

为了获得与 Visual Studio 的最佳开发体验,在编辑 C#或 XAML 文件时,请确保 Visual Studio 将使用 UWP 头进行智能感知,否则智能感知可能无法正常工作。为此,在打开 C#或 XAML 文件时,从下拉菜单中选择已打开文件的选项卡名称下方的 UWP 头。

将 Windows 的 Visual Studio 与 Mac 配对

对于测试和调试 iOS 头,您可以直接在 Mac 上开发,我们将在下一节中介绍,或者您可以将 Windows 的 Visual Studio 与 Mac 配对,以远程调试 iOS 头。

在 Visual Studio 中的使用.NET 进行移动开发工作负载包括连接到 Mac 所需的软件。但是,需要三个步骤才能完全配置它:

  1. 在 Mac 上安装XcodeVisual Studio for Mac,并打开这些应用程序以确保安装了所有依赖项。

  2. 在 Mac 上启用远程登录

  3. 从 Visual Studio 连接到 Mac。

在 Mac 上启用远程登录需要以下步骤:

  1. 系统偏好设置中打开共享窗格。

  2. 检查远程登录并指定允许访问的用户:

  3. 根据提示更改防火墙设置。

要从 Visual Studio 连接,请执行以下操作:

  • 转到工具>iOS>配对到 Mac

  • 如果您是第一次这样做,请选择添加 Mac…并输入 Mac 名称或 IP 地址,然后在提示时输入用户名和密码。

  • 如果 Mac 已列出,请选择它并单击连接

该工具将检查 Mac 上安装和可用的所有必需内容,然后打开连接。

如果出现问题,它将告诉您如何解决。

注意。

有关将 Visual Studio 配对到 Mac 以及解决可能遇到的任何问题的更详细说明,请访问docs.microsoft.com/xamarin/ios/get-started/installation/windows/connecting-to-mac/

现在,Visual Studio 已成功与您的 Mac 配对,您可以从 Windows 机器调试应用程序,并在远程 iOS 模拟器上运行它。

使用 Visual Studio for Mac 运行和调试应用程序

如果您主要在 Mac 上工作,使用 Visual Studio for Mac 是开发 Uno 平台应用程序的最简单方法。

使用 Visual Studio for Mac 运行 Uno 平台应用程序与运行其他应用程序相同。您需要在启动项目列表中选择正确的头项目(例如,HelloWorld.macOSHelloWorld.iOS),选择正确的目标架构来运行应用程序,并选择设备或模拟器来运行应用程序。

当然,除了在本地机器上运行应用程序之外,您还可以在模拟器上运行 Android 或 iOS 应用程序。您可以在 Windows 的 Visual Studio 中将任何适用的设备作为目标,包括任何模拟器或仿真器。

由于 Uno 平台应用程序的 WASM 版本的调试将在 Visual Studio 和 Visual Studio for Mac 之外进行,我们将在下一节中介绍这一点。

调试应用程序的 WASM 头

在撰写本文时,从 Visual Studio 或 Visual Studio for Mac 内部调试 WASM 的支持并不是很好,但是有替代选项。因此,当使用 Visual Studio for Windows 或 Visual Studio for Mac 时,WASM 的调试体验将在浏览器中进行。为了获得最佳的调试体验,我们建议使用最新的 Google Chrome Canary 版本。这可以从www.google.com/chrome/canary/获取。由于 WASM 的调试仍处于实验阶段,因此可能会发生变化,我们强烈建议访问官方文档(platform.uno/docs/articles/debugging-wasm.html)获取最新信息。您可以在这里了解有关使用 Visual Studio 调试 WASM 头的更多信息:platform.uno/blog/debugging-uno-platform-webassembly-apps-in-visual-studio-2019/

或者,您可以使用 Visual Studio Code 来调试应用程序的 WASM 版本。为了获得最佳体验,您应该使用dotnet newCLI 创建 Uno Platform 应用程序。您必须包括–vscodeWasm标志,如下所示,因为它将添加您可以在 Visual Studio Code 中使用的构建配置:

dotnet new unoapp -o HelloWorld -ios=false -android=false 
 -macos=false -uwp=false --vscodeWasm

请注意,通过前面的dotnet new命令,我们选择了不使用其他头部,因为在撰写本文时,只有 WASM 版本可以在 Visual Studio Code 中进行调试。

如果您已经创建了应用程序,请按照文档中显示的步骤进行操作platform.uno/docs/articles/get-started-vscode.html#updating-an-existing-application-to-work-with-vs-code。当您的项目中已经存在其他平台的头部时,这也适用。

要使用 Visual Studio 启动应用程序并进行调试,首先使用dotnet restore恢复 NuGet 包。之后,您需要启动开发服务器。要做到这一点,打开 Visual Studio Code 左侧的三角形图标,显示运行和调试面板,如图 2.9所示:

图 2.9 - Visual Studio Code 的运行和调试视图

图 2.9 - Visual Studio Code 的运行和调试视图

单击箭头,将运行.NET Core Launch配置,该配置将构建应用程序并启动开发服务器。开发服务器将托管您的应用程序。检查终端输出,以查看您可以在本地计算机上访问 WASM 应用程序的 URL,如图 2.10所示:

图 2.10 - 开发服务器的终端输出

图 2.10 - 开发服务器的终端输出

如果您只想启动应用程序并在没有调试功能的情况下继续,那么您已经完成了。但是,如果您想利用调试和断点支持,您还需要选择在 Chrome 中的.NET Core Debug Uno Platform WebAssembly配置。在运行和调试面板中选择启动配置后,启动它,这将启动调试服务器。然后,调试服务器会打开一个浏览器窗口,其中包含您的 Uno Platform WASM 应用程序。

注意

默认情况下,调试服务器将使用最新的稳定版 Google Chrome 启动。如果您没有安装稳定版的 Google Chrome,服务器将无法启动。如果您希望改用最新的稳定版 Edge,可以更新.vscode/launch.json文件,并将pwa-chrome更改为pwa-msedge

调试服务器启动并准备好接收请求后,它将根据您的配置在 Chrome 或 Edge 中打开网站。您在 Visual Studio Code 中放置的任何断点都将被浏览器所尊重,并暂停您的 WASM 应用程序,类似于在非 WASM 项目上使用 Visual Studio 时断点的工作方式。

成功完成这些步骤后,您可以在所选的浏览器中打开应用程序,它将看起来像图 2.11

图 2.11–在浏览器中运行的 HelloWorld 应用程序

图 2.11–在浏览器中运行的 HelloWorld 应用程序

现在我们已经介绍了运行和调试应用程序,让我们快速介绍一下使用 Uno Platform 进行开发的两个非常有用的功能:XAML Hot Reload 和 C#编辑和继续。

XAML Hot Reload 和 C#编辑和继续

为了使开发更加简单和快速,特别是 UI 开发,Uno Platform 在使用 Visual Studio 进行开发时支持 XAML Hot Reload 和 C#编辑和继续。XAML Hot Reload 允许您修改视图和页面的 XAML 代码,运行的应用程序将实时更新,而 C#编辑和继续允许您修改 C#代码,而无需重新启动应用程序以捕获更改。

由于您的应用程序的 UWP 头部是使用 UWP 工具链构建的,因此您可以使用 XAML Hot Reload 和 C#编辑和继续。由于在撰写本文时,UWP 是唯一支持两者的平台,因此我们将使用 UWP 来展示它。其他平台不支持 C#编辑和继续,但是支持 XAML Hot Reload。

XAML Hot Reload

要尝试 XAML Hot Reload,请在共享项目中打开MainPage.xaml文件。页面的内容将只是一个Grid和一个TextBlock

<Grid Background="{ThemeResource 
                   ApplicationPageBackgroundThemeBrush}">
    <TextBlock Text="Hello, world!"
        Margin="20" FontSize="30" />
</Grid>

现在让我们通过用Hello from hot reload!替换文本来更改我们的页面,保存文件(Ctrl + S),我们的应用程序现在看起来像图 2.12所示,而无需重新启动应用程序!

图 2.12–我们的 HelloWorld 应用程序使用 XAML Hot Reload 更改

图 2.12–我们的 HelloWorld 应用程序使用 XAML Hot Reload 更改

XAML Hot Reload 可以在 UWP、iOS、Android 和 WebAssembly 上运行。但是,并非所有类型的更改都受支持,例如,更改控件的事件处理程序不受 XAML Hot Reload 支持,需要应用程序重新启动。除此之外,更新ResourceDictionary文件也不会更新应用程序,需要应用程序重新启动。

C#编辑和继续

有时,您还需要对“code-behind”进行更改,这就是 C#编辑和继续将成为您的朋友的地方。请注意,您需要使用应用程序的 UWP 头部,因为它是唯一支持 C#编辑和继续的平台。在继续尝试 C#编辑和继续之前,您需要添加一些内容,因为我们的 HelloWorld 应用程序尚不包含太多 C#代码。首先,您需要关闭调试器和应用程序,因为 C#编辑和继续不支持以下代码更改。通过将MainPage内容更改为以下内容,更新您的页面以包含具有Click事件处理程序的按钮:

<StackPanel Background="{ThemeResource 
                   ApplicationPageBackgroundThemeBrush}">
    <TextBlock x:Name="helloTextBlock"
         Text="Hello from hot reload!" Margin="20"
         FontSize="30" />
    <Button Content="Change text"
        Click="ChangeTextButton_Click"/>
</StackPanel>

现在,在您的MainPage类中,添加以下代码:

private void ChangeTextButton_Click(object sender,
                                    RoutedEventArgs e)
{
    helloTextBlock.Text = "Hello from code behind!";
}

当您运行应用程序并单击按钮时,文本将更改为Hello from code behind!。现在单击图 2.13中突出显示的全部中断按钮,或按Ctrl + Alt + Break

图 2.13–全部中断按钮

图 2.13–全部中断按钮

您的应用程序现在已暂停,您可以对 C#代码进行更改,当您通过单击Click事件处理程序来恢复应用程序时,这些更改将被捕获为Hello from C# Edit and Continue!

private void ChangeTextButton_Click(object sender,
                                    RoutedEventArgs e)
{
    helloTextBlock.Text = 
        "Hello from C# Edit and Continue!";
}

然后恢复应用程序。如果现在点击按钮,文本将更改为Hello from C# Edit and Continue!

但是,对于编辑和继续,有一些限制;并非所有代码更改都受支持,例如,更改对象的类型。有关不受支持更改的完整列表,请访问官方文档(docs.microsoft.com/en-us/visualstudio/debugger/supported-code-changes-csharp)。请注意,在撰写本文时,C#编辑和继续仅在 UWP 和 Skia 头部的 Windows 上运行。

现在我们已经讨论了构建和运行应用程序,让我们谈谈条件代码,即特定于平台的 C#和 XAML。

特定于平台的 XAML 和 C#

虽然 Uno 平台允许您在任何平台上运行应用程序,而无需担心底层特定于平台的 API,但仍然存在一些情况,您可能希望编写特定于平台的代码,例如访问本机平台 API。

特定于平台的 C#

编写特定于平台的 C#代码类似于编写特定于架构或特定于运行时的 C#代码。Uno 平台附带了一组编译器符号,这些符号将在为特定平台编译代码时定义。这是通过使用预处理器指令实现的。预处理器指令只有在为编译设置了符号时,编译器才会尊重它们,否则编译器将完全忽略预处理器指令。

在撰写本文时,Uno 平台附带了以下预处理器指令:

  • NETFX_CORE用于 UWP

  • __ANDROID__用于 Android

  • __IOS__用于 iOS

  • HAS_UNO_WASM(或__WASM__)用于使用 WebAssembly 的 Web

  • __MACOS__用于 macOS

  • HAS_UNO_SKIA(或__SKIA__)用于基于 Skia 的头

请注意,WASM 和 Skia 有两个不同的符号可用。两者都是有效的,除了它们的名称之外没有区别。

您可以像使用DEBUG一样使用这些符号,甚至可以组合它们,例如if __ANDROID__ || __ MACOS__。让我们在之前的示例中尝试一下,并使用 C#符号使TextBlock元素指示我们是在桌面、网络还是移动设备上:

private void ChangeTextButton_Click(object sender,
                                    RoutedEventArgs e)
{
#if __ANDROID__ || __IOS__
    helloTextBlock.Text = "Hello from C# on mobile!";
#elif HAS__UNO__WASM
    helloTextBlock.Text = "Hello from C# on WASM!";
#else
    helloTextBlock.Text = "Hello from C# on desktop!";
#endif
}

如果您运行应用程序的 UWP 头并单击按钮,然后文本将更改为设置的NETFX_CORE符号。现在,如果您在 Android 或 iOS 模拟器(或设备)上运行应用程序并单击按钮,它将显示__ANDROID____IOS__符号。

特定于平台的 XAML

虽然特定于平台的 C#代码很棒,但也有一些情况需要在特定平台上呈现控件。这就是特定于平台的 XAML 前缀发挥作用的地方。XAML 前缀允许您仅在特定平台上呈现控件,类似于 UWP 的条件命名空间。

在撰写本文时,您可以使用以下 XAML 前缀:

图 2.14 - 命名空间前缀表,支持的平台及其命名空间 URI

图 2.14 - 命名空间前缀表,支持的平台及其命名空间 URI

要在 XAML 中包含特定的 XAML 前缀,您必须在 XAML 文件的顶部与所有其他命名空间声明一起添加xmlns:[prefix-name]=[namespace URI]前缀名称是 XAML 前缀(图 2.14中的第 1 列),而命名空间 URI是应与之一起使用的命名空间的 URI(图 2.14中的第 3 列)。

对于将从 Windows 中排除的前缀,您需要将前缀添加到mc:Ignorable列表中。这些前缀是androidioswasmmacosskiaxamarinnetstdrefnot_netstdrefnot_win,因此所有不在http://schemas.microsoft.com/winfx/2006/xaml/presentation中的前缀。

现在让我们尝试一下通过更新我们的 HelloWorld 项目来使用一些平台 XAML 前缀,使TextBlock元素仅在 WASM 上呈现。为此,我们将首先将前缀添加到我们的MainPage.xaml文件中(请注意,我们省略了一些定义):

<Page
    x:Class="HelloWorld.MainPage"
    ... 
    xmlns:win="http ://schemas.microsoft.com/winfx/2006/xaml/
             presentation"
    xmlns:android="http ://uno.ui/android"
    xmlns:ios="http ://uno.ui/ios"
    xmlns:wasm="http ://uno.ui/wasm"
    xmlns:macos="http ://uno.ui/macos"
    xmlns:skia="http ://schemas.microsoft.com/winfx/2006/xaml/
              presentation"
    ...
    mc:Ignorable="d android ios wasm macos skia">
    ...
</Page>

由于 Android、iOS、WASM、macOS 和 Skia XAML 前缀将在 Windows 上被排除,因此我们需要将它们添加到mc:Ignorable列表中。这是因为它们不是标准 XAML 规范的一部分,否则将导致错误。添加它们后,我们可以添加仅在应用程序在特定平台上运行时呈现的控件,例如 WASM 或 iOS。要尝试这一点,我们将添加一个TextBlock元素来欢迎用户:

<StackPanel>
     <TextBlock x:Name="helloTextBlock"
         Text="Hello World!" Margin="20"
         FontSize="30" />
     <win:TextBlock Text="Welcome on Windows!"/>
     <android:TextBlock Text="Welcome on Android!"/>
     <ios:TextBlock Text="Welcome on iOS!"/>
     <wasm:TextBlock Text="Welcome on WASM!"/>
     <macos:TextBlock Text="Welcome on Mac OS!"/>
     <skia:TextBlock Text="Welcome on Skia!"/>
     <Button Content="Change test"
         Click="ChangeTextButton_Click"/>
</StackPanel>

现在,如果您启动应用程序的 WASM 头并在浏览器中打开应用程序(如果尚未打开),应用程序将显示TextBlock元素,如图 2.15左侧所示。如果您现在启动应用程序的 UWP 头,应用程序将显示欢迎使用 Windows!,如图 2.15右侧所示:

图 2.15 - 使用 WASM(左)和使用 UWP(右)运行的 HelloWorld 应用程序

图 2.15 - 使用 WASM(左)和使用 UWP(右)运行的 HelloWorld 应用程序

如果您在跨目标库中使用 XAML 前缀,例如跨目标库(Uno 平台)项目模板,这将在下一节中介绍,XAML 前缀的行为会略有不同。由于跨目标库的工作方式,wasmskia前缀将始终计算为 false。跨目标库的一个示例是跨运行时库项目类型,我们将在下一节中介绍。这是因为两者都编译为.NET Standard 2.0,而不是 WASM 或 Skia 头。除了这些前缀,您还可以使用netstdref前缀,其命名空间 URI 为http://uno.ui/netstdref,如果在 WASM 或 Skia 上运行,则计算为 true。此外,还有not_netstdref前缀,其命名空间 URI 为http://uno.ui/not_netstdref,它与netstdref完全相反。请注意,您需要将这两个前缀都添加到mc:Ignorable列表中。现在您已经了解了使用 C#编译器符号和 XAML 前缀编写特定于平台的代码,让我们来看看其他项目类型。

超越默认的跨平台应用程序结构

到目前为止,我们已经创建了一个包含每个平台头的跨平台应用程序。但是,您还可以使用不同的项目类型来编写 Uno 平台应用程序,我们将在本节中介绍。

注意

如果您现在使用dotnet CLI,请打开终端并运行dotnet new -i Uno.ProjectTemplates.Dotnet,因为我们将在本章的其余部分中使用这些内容。

多平台库项目类型

除了多平台应用程序(Uno 平台)项目类型之外,最重要的项目类型之一是跨平台库(Uno 平台)类型。跨平台库(Uno 平台)项目类型允许您编写可以被 Uno 平台应用程序使用的代码。了解项目类型的最简单方法是创建一个新的跨平台库。我们将通过在现有的 HelloWorld 解决方案中创建一个新项目来实现这一点。

注意

为了能够使用dotnet new CLI 安装的所有项目模板,您需要允许 Visual Studio 在项目类型列表中包含dotnet new模板。您可以通过在工具 > 选项下打开环境下的预览功能部分,勾选在新项目对话框中显示所有.NET Core 模板来实现这一点。之后,您需要重新启动 Visual Studio 以使更改生效。

启用该选项后,重新启动 Visual Studio 并通过右键单击解决方案视图中的解决方案并单击添加 > 新建项目来打开新项目对话框。对话框将如图 2.16所示:

图 2.16 - Visual Studio 中的添加新项目对话框

图 2.16 - Visual Studio 中的添加新项目对话框

接下来,选择HelloWorld.Helpers。输入名称后,单击创建

这将在您的解决方案中创建一个新的跨平台 Uno 平台库。在磁盘上,该库有自己的文件夹,以自己的名称命名,您的解决方案视图将如图 2.17所示:

图 2.17 - HelloWorld 解决方案视图

图 2.17 - HelloWorld 解决方案视图

现在让我们向我们的跨平台库添加一些代码。我们将把类Class1重命名为Greetings,并引入一个新的公共静态函数,名为GetStandardGreeting,它将返回字符串"Hello from a cross-platform library!"

public class Greetings
{
    public static string GetStandardGreeting()
    {
        return "Hello from a cross-platform library!";
    }
}

除了创建库之外,您还必须在要在其中使用该项目的每个头项目中添加对它的引用。添加对库的引用的过程对所有头项目都是相同的,这就是为什么我们只会向您展示如何向 UWP 头添加引用。

要向 UWP 头添加引用,请在“解决方案资源管理器”中右键单击 UWP 项目。在上下文菜单中,您将找到添加类别,其中包含引用…选项,该选项也显示在图 2.18中:

图 2.18 - 添加|引用…选项,用于 UWP 头

图 2.18 - 添加|引用…选项,用于 UWP 头

单击引用…后,将打开一个新对话框,您可以在其中选择要添加的引用。在我们的情况下,您需要选择该项目,如图 2.19所示:

图 2.19 - UWP 头的参考管理器

图 2.19 - UWP 头的参考管理器

在检查了HelloWorld.Helpers项目后,单击确定以保存更改。现在我们可以在应用程序的 UWP 版本中使用我们的库。让我们更新平台条件代码部分的事件处理程序,以使用 Greetings 辅助类,如下所示:

private void ChangeTextButton_Click(object sender,
                                    RoutedEventArgs e)
{
#if __ANDROID__ || __IOS__
    helloTextBlock.Text = "Hello from C# on mobile!";
#elif __WASM__
    helloTextBlock.Text = "Hello from C# on WASM!";
#else
    helloTextBlock.Text=
        HelloWorld.Helpers.Greetings.GetStandardGreeting();
#endif
}

如果现在运行 UWP 版本的应用程序并单击按钮,应用程序将在HelloWorld 命名空间中显示Helpers 命名空间。这是因为我们尚未从 macOS 头添加对库的引用。对于您计划在其中使用库的任何平台,您都需要在该平台的头中添加对库的引用。该过程也适用于作为 NuGet 包引用的库;您需要在要在其中使用库的每个平台头中添加对 NuGet 包的引用。与 Uno 平台应用程序项目不同,其中大部分源代码位于共享项目中,跨平台库项目类型是一个多目标项目。

其他项目类型

除了跨平台库项目类型,还有其他 Uno 平台项目模板。我们将在本节中广泛介绍它们。要能够从 Visual Studio 中创建它们,请按照上一节所示,启用在 Visual Studio 中显示dotnet新模板。

如果您已经熟悉使用 XAML 和 MVVM 模式进行应用程序开发,您可能已经了解 Prism (prismlibrary.com/),这是一个框架,用于构建“松散耦合、可维护和可测试的 XAML 应用程序”。Uno 平台模板中还包括跨平台应用程序(Prism)(Uno 平台)模板,它将创建一个 Prism Uno 平台应用程序。创建 Prism Uno 平台应用程序与创建“普通”多平台 Uno 应用程序相同。

除了 Uno 平台 Prism 应用程序模板之外,还有一个 Uno 平台模板,用于构建WinUI 3应用程序。但是,您可以创建一个使用 Windows 10 预览版 WinUI 3 的 Uno 平台应用程序。要使用 WinUI 3 创建 Uno 平台应用程序,在新项目对话框中,选择跨平台应用程序(WinUI)(Uno 平台)模板。

另一个将会很有用的项目类型,特别是在开发将使用 NuGet 进行发布的库时,是跨运行时库(Uno 平台)项目类型,它将创建一个跨运行时库。与跨平台库不同,Skia 和 WASM 版本不会分别构建,也无法区分,跨运行时库将为 WASM 和 Skia 分别编译项目,允许使用 XAML 前缀和编译器符号编写特定于 WASM 和 Skia 的代码。

除此之外,我们还有跨平台 UI 测试库。跨平台 UI 测试库允许您编写可以在多个平台上运行的 UI 测试,只需使用一个代码库。由于我们将在《第七章》中更全面地介绍测试,即《测试您的应用程序》,我们将在那里介绍该项目类型。

最后但并非最不重要的是,我们将在《第八章》中涵盖使用 WebAssembly 和 Uno 平台将Xamarin.Forms应用程序部署到 Web 上,即《部署您的应用程序并进一步》。

总结

在本章中,您学会了如何创建、构建和运行您的第一个 Uno 平台应用程序,并了解了一般解决方案结构以及平台头的工作原理。我们还介绍了如何使用 Visual Studio 和 Visual Studio Code 在不同平台上构建、运行和调试应用程序。除此之外,您还学会了如何使用 XAML 热重载和 C#编辑和继续功能,以使开发更加轻松。

在接下来的部分中,我们将为 UnoBookRail 编写应用程序,该公司运营 UnoBookCity 的公共交通。我们将从《第三章》开始,即《使用表单和数据》,为 UnoBookRail 编写一个任务管理应用程序,该应用程序允许在桌面和 Web 上输入、过滤和编辑数据。

第二部分:编写和开发 Uno 平台应用程序

在接下来的四章中,我们将介绍四个不同的应用程序,展示 Uno 平台构建的应用程序可用的不同功能。这些应用程序是为同一个虚构的业务(UnoBookRail)创建的,该业务是虚构城市(UnoBookCity)的公共交通管理局的一部分。

该业务负责城市轻轨网络中使用的所有技术。轻轨网络是电力驱动的只运载乘客的火车,在世界许多城市存在。它们被称为地铁、快速交通、地铁、地铁、地下铁路、地铁和许多其他名称。

不用担心,你不需要了解这些火车或它们是如何运行的。下图显示了网络地图,让你了解我们在谈论什么。你会看到主线从机场向西沿着河流前进。当它到达城市中心时,它会沿着海岸向北和向南分支。

UnoBookRail 网络站点的地图

这四个应用程序将展示 Uno 平台如何用于创建不同场景的应用程序,并展示在适当场景中使用不同功能。

在本节中,我们包括以下章节:

  • 第三章, 使用表单和数据

  • 第四章, 使您的应用程序移动化

  • 第五章, 使您的应用程序准备好面对现实世界

  • 第六章, 在图表中显示数据和使用自定义 2D 图形

第三章:使用表单和数据

在这一章中,我们将为虚构公司 UnoBookRail 编写我们的第一个应用程序,该应用程序将针对桌面和 Web 进行定位。我们将编写一个典型的业务线(LOB)应用程序,允许我们查看、输入和编辑数据。除此之外,我们还将介绍如何以 PDF 格式导出数据,因为这是 LOB 应用程序的常见要求。

在本章中,我们将涵盖以下主题:

  • 编写以桌面为重点的 Uno 平台应用程序

  • 编写表单并验证用户输入

  • 在您的 Uno 平台应用程序中使用 Windows 社区工具包

  • 以编程方式生成 PDF 文件

到本章结束时,您将创建一个以桌面为重点的应用程序,也可以在 Web 上运行,显示数据,允许您编辑数据,并以 PDF 格式导出数据。

技术要求

本章假设您已经设置好了开发环境,并安装了项目模板,就像我们在第一章中介绍的那样,介绍 Uno 平台。本章的源代码可以在github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/Chapter03找到。

本章的代码使用了以下库:github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/SharedLibrary

查看以下视频以查看代码的运行情况:bit.ly/3fWYRai

介绍应用程序

在本章中,我们将构建 UnoBookRail ResourcePlanner应用程序,该应用程序将在 UnoBookRail 内部使用。UnoBookRail 的员工将能够使用这个应用程序来管理 UnoBookRail 内部的任何资源,比如火车和车站。在本章中,我们将开发应用程序的问题管理部分。虽然这个应用程序的真实版本会有更多的功能,但在本章中,我们只会开发以下功能:

  • 创建一个新问题

  • 显示问题列表

  • 以 PDF 格式导出问题

由于这个应用程序是一个典型的业务线应用程序,该应用程序将针对 UWP、macOS 和 WASM。让我们继续创建这个应用程序。

创建应用程序

让我们开始创建应用程序的解决方案:

  1. 在 Visual Studio 中,使用多平台应用程序(Uno 平台)模板创建一个新项目。

  2. 将项目命名为ResourcePlanner。如果您愿意,也可以使用其他名称,但在本章中,我们将假设项目名为ResourcePlanner

  3. 删除除UWPmacOSWASM之外的所有项目头。

  4. 为了避免写更多的代码,我们需要从github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/SharedLibrary下载共享库项目,并添加引用。为此,在UnoBookRail.Common.csproj文件中右键单击解决方案节点,然后单击打开

  5. 现在我们已经将项目添加到解决方案中,我们需要在特定于平台的项目中添加对库的引用。为此,在解决方案资源管理器中右键单击UWP项目节点,选择添加 > 引用... > 项目,选中UnoBookRail.Common条目,然后单击确定。对 macOS 和 WASM 项目重复此过程

  6. 最后,在LinkerConfig.xml文件的闭合链接标签之前添加以下代码,在LinkerConfig.xml文件中告诉 WebAssembly 链接器包括编译源代码中的类型,即使这些类目前没有被使用。如果我们不指定这些条目,那么程序集中定义的类型将不会被包括,因为链接器会删除代码。这是因为它找不到直接的引用。当使用其他包或库时,您可能还需要为这些库指定条目。不过,在本章中,前面的条目就足够了。

对于我们的应用程序,我们将使用Model-View-ViewModelMVVM)模式。这意味着我们的应用程序将主要分为三个区域:

  • ModelModel包含应用程序的数据和业务逻辑。例如,这将处理从数据库加载数据或运行特定业务逻辑。

  • ViewModelViewModel充当视图和模型之间的层。它以适合视图的方式呈现应用程序的数据,提供视图与模型交互的方式,并通知视图模型的更改。

  • ViewView代表用户的数据,并负责屏幕上的表示内容。

为了使开发更容易,我们将使用Microsoft.Toolkit.MVVM包,现在我们将添加它。这个包帮助我们编写我们的 ViewModel,并处理 XAML 绑定所需的样板代码:

  1. 首先,在解决方案视图中右键单击解决方案节点,然后选择管理解决方案的 NuGet 包...

  2. 现在,搜索Microsoft.Toolkit.MVVM,并从列表中选择该包。

  3. 从项目列表中选择macOSUWPWASM项目,然后单击安装

  4. 由于我们稍后会使用它们,还要创建三个名为ModelsViewModelsViews的文件夹。为此,在ResourcePlanner.Shared共享项目中右键单击,选择添加 > 新文件夹,并命名为Models。对于ViewModelsViews,重复此过程。

现在我们已经设置好了项目,让我们从向我们的应用程序添加第一部分代码开始。与业务应用程序一样,我们将使用MenuBar控件作为切换视图的主要方式:

  1. 首先,在ViewModels文件夹中创建一个名为NavigationViewModel的新类。

  2. 现在,用以下代码替换NavigationViewModel.cs文件中的代码:

using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using System.Windows.Input;
using Windows.UI.Xaml;
namespace ResourcePlanner.ViewModels
{
    public class NavigationViewModel :
        ObservableObject
    {
        private FrameworkElement content;
        public FrameworkElement Content
        {
            Get
            {
                return content;
            }
            Set
            {
                SetProperty(ref content, value);
            }
        }
        public ICommand Issues_OpenNewIssueViewCommand
            { get; }
        public ICommand Issues_ExportIssueViewCommand 
            { get; }
        public ICommand Issues_OpenAllIssuesCommand {
            get; }
        public ICommand Issues_OpenTrainIssuesCommand
            { get; }
        public ICommand 
            Issues_OpenStationIssuesCommand { get; }
        public ICommand Issues_Open OtherIssuesCommand
            { get; }
        public NavigationViewModel()
        {
            Issues_OpenNewIssueViewCommand = 
                new RelayCommand(() => { });
            Issues_ExportIssueViewCommand = 
                new RelayCommand(() => { });
            Issues_OpenAllIssuesCommand =
                new RelayCommand(() => { });
            Issues_OpenAllTrainIssuesCommand = 
                new RelayCommand(() => { });
            Issues_OpenAllStationIssuesCommand =
                new RelayCommand(() =>{ });
            Issues_OpenAllOtherIssuesCommand = 
                new RelayCommand(() =>{ });
        }
    }
}

这是处理导航到不同控件的类。随着我们在本章后面实现更多视图,我们将更新Command对象,使其指向正确的视图。

  1. 现在,在MainPage类中添加以下代码:
using ResourcePlanner.ViewModels;
...
private NavigationViewModel navigationVM = new NavigationViewModel();

这将在MainPage类中添加一个NavigationViewModel对象,我们可以在 XAML 中绑定它。

  1. 最后,用以下内容替换您的MainPage.xaml文件的内容:
    ...
    xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
    <Grid Background="{ThemeResource 
        ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <muxc:MenuBar>
            <muxc:MenuBar.Items>
                <muxc:MenuBarItem Title="Issues">
                    <MenuFlyoutItem Text="New" 
                        Command="{x:Bind
                        navigationVM.Issues_
                        OpenNewIssueViewCommand}"/>
                    <MenuFlyoutItem Text="Export to 
                        PDF" Command="{x:Bind 
                        navigationVM.Issues_
                        ExportIssueViewCommand}"/>
                    <MenuFlyoutSeparator/>
                    <MenuFlyoutItem Text="All" 
                        Command="{x:Bind 
                        navigationVM.Issues_
                        OpenAllIssuesCommand}"/>
                    <MenuFlyoutItem Text="Train 
                        issues" Command="{x:Bind 
                        navigationVM.Issues_
                        OpenTrainIssuesCommand}"/>
                    <MenuFlyoutItem Text="Station 
                        issues" Command="{x:Bind 
                        navigationVM.Issues_
                        OpenStationIssuesCommand}"/>
                    <MenuFlyoutItem Text="Other 
                         issues" Command="{x:Bind 
                         navigationVM.Issues_
                         OpenOtherIssuesCommand}"/>
                </muxc:MenuBarItem>
                <muxc:MenuBarItem Title="Trains"
                    IsEnabled="False"/>
                <muxc:MenuBarItem Title="Staff"
                    IsEnabled="False"/>
                <muxc:MenuBarItem Title="Depots"
                    IsEnabled="False"/>
                <muxc:MenuBarItem Title="Stations"
                    IsEnabled="False"/>
            </muxc:MenuBar.Items>
        </muxc:MenuBar>
        <ContentPresenter Grid.Row="1"
            Content="{x:Bind navigationVM.Content,
                Mode=OneWay}"/>
    </Grid>

此代码添加了MenuBar,用户可以使用它导航到不同的视图。底部的ContentPresenter用于显示导航到的内容。

现在,如果启动应用程序,您将看到类似以下的内容:

图 3.1 - 运行带有 MenuBar 导航的 ResourcePlanner 应用程序

图 3.1 - 运行带有 MenuBar 导航的 ResourcePlanner 应用程序

在下一节中,我们将向应用程序添加第一个视图,允许用户创建新问题。

输入和验证数据

业务应用程序的典型要求是输入数据并为所述数据提供输入验证。Uno 平台提供了各种不同的控件,允许用户输入数据,除了支持 Uno 平台的数十个库。

注意

在撰写本文时,尚无内置的输入验证支持,但 Uno 平台计划支持输入验证。这是因为目前 UWP 和 WinUI 3 都不完全支持输入验证。要了解有关即将到来的输入验证支持的更多信息,请查看 WinUI 存储库中的以下问题:github.com/microsoft/microsoft-ui-xaml/issues/179。Uno 平台正在跟踪此问题的进展:github.com/unoplatform/uno/issues/4839

为了使我们的开发过程更加简单,首先让我们添加对 Windows 社区工具包控件的引用:

  1. 首先,在解决方案视图中右键单击解决方案节点,然后选择管理解决方案的 NuGet 包…

  2. 搜索Microsoft.Toolkit.UI.Controls并选择该包。

  3. 在项目列表中选择UWP头,并单击安装

  4. Microsoft.Toolkit.UI.Controls.DataGrid包重复步骤 23

  5. 现在,搜索Uno.Microsoft.Toolkit.UI.Controls并选择该包。

注意

虽然 Windows 社区工具包仅支持 UWP,但由于 Uno 平台团队的努力,我们也可以在所有支持的平台上在 Uno 平台应用程序中使用 Windows 社区工具包。Uno 平台团队根据原始包维护了与 Uno 平台兼容的 Windows 社区工具包版本,并相应地更新它们。

  1. 从项目列表中选择macOSWASM头,并单击安装

  2. 最后,对Uno.Microsoft.Toolkit.UI.Controls.DataGrid包重复步骤 56

这使我们能够在应用程序中使用 Windows 社区工具包控件。由于我们还希望在 macOS 和 WASM 上使用这些控件,因此我们还安装了这两个包的 Uno 平台版本。由于我们添加了Windows 社区工具包控件包,我们可以开始创建“创建问题”视图:

  1. 首先,在Models文件夹内创建IssueRepository.cs类,并将以下代码添加到其中:
using System.Collections.Generic;
using UnoBookRail.Common.Issues;
namespace ResourcePlanner.Models
{
    public class IssuesRepository
    {
        private static List<Issue> issues = new
            List<Issue>();
        public static List<Issue> GetAllIssues()
        {
            return issues;
        }
        public static void AddIssue(Issue issue)
        {
            issues.Add(issue);
        }
    }
}

这是收集问题的模型。在现实世界的应用程序中,此代码将与数据库或 API 通信以持久化问题,但为简单起见,我们只会将它们保存在列表中。

  1. 接下来,在ViewModels文件夹中创建CreateIssueViewModel.cs类,并使用来自 GitHub 的以下代码:github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/blob/main/Chapter03/ResourcePlanner.Shared/ViewModels/CreateIssueViewModel.cs

现在我们已经创建了必要的模型和视图模型,接下来我们将继续添加用户界面以创建新问题。

对于用户界面,我们将实现输入验证,因为这在业务应用程序的数据输入表单中是典型的。为此,我们将实现以下行为:如果用户单击“创建问题”按钮,我们将使用代码后台中的函数验证数据。如果我们确定数据有效,我们将创建一个新问题;否则,我们将在每个未通过自定义验证的字段下方显示错误消息。除此之外,我们将在输入更改时验证输入字段。

让我们继续创建用户界面:

  1. Views文件夹内创建一个名为CreateIssueView.xaml的新UserControl,并用以下内容替换 XAML:
<UserControl
    x:Class="ResourcePlanner.Views.CreateIssueView"
     xmlns="http://schemas.microsoft.com/winfx/2006
           /xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/
              winfx/2006/xaml" 
    xmlns:local="using:ResourcePlanner.Views"
    xmlns:d="http://schemas.microsoft.com/
            expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/
             markup-compatibility/2006"
    xmlns:wctcontrols="using:Microsoft.Toolkit.
                       Uwp.UI.Controls"
    xmlns:wctui="using:Microsoft.Toolkit.Uwp.UI"
    xmlns:ubrcissues="using:UnoBookRail.Common.Issues"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">
    <StackPanel Orientation="Vertical" Padding="20">
        <TextBlock Text="Create new issue"
            FontSize="24"/>
        <Grid ColumnSpacing="10">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="200"/>
                <ColumnDefinition Width="200"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition />
            </Grid.RowDefinitions>
            <TextBox x:Name="TitleTextBox"
                Header="Title"
                Text="{x:Bind createIssueVM.Title,
                       Mode=TwoWay}"
                HorizontalAlignment="Stretch" 
                TextChanged="FormInput_TextChanged"/>
            <TextBlock x:Name="titleErrorNotification" 
                Grid.Row="1"Foreground="{ThemeResource
                    SystemErrorTextColor}"/>
            <ComboBox Header="Type" Grid.Column="1"
                ItemsSource="{wctui:EnumValues 
                    Type=ubrcissues:IssueType}"
                HorizontalAlignment="Stretch"
                SelectedItem="{x:Bind 
                    createIssueVM.IssueType, 
                    Mode=TwoWay}"/>
        </Grid>
        <TextBox Header="Description"
            Text="{x:Bind createIssueVM.Description,
                Mode=TwoWay}"
            MinWidth="410" MaxWidth="800" 
            HorizontalAlignment="Left"/>
        <Button Content="Create new issue"
            Margin="0,20,0,0" Width="410" 
            HorizontalAlignment="Left"
            Click="CreateIssueButton_Click"/>
    </StackPanel>
</UserControl>

这是一个基本的用户界面,允许用户输入标题和描述,并让用户选择问题的类型。请注意,我们在文本输入下方添加了一个TextBlock控件,以便在提供的输入无效时向用户显示错误消息。除此之外,我们还为Title添加了一个TextChanged监听器,以便在文本更改时更新错误消息。

  1. 现在,用以下代码替换CreateIssueView.xaml.cs文件的内容:
using ResourcePlanner.ViewModels;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace ResourcePlanner.Views
{
    public sealed partial class CreateIssueView :
        UserControl
    {
        private CreateIssueViewModel createIssueVM;
        public CreateIssueView(CreateIssueViewModel
            viewModel)
        {
            this.createIssueVM = viewModel;
            this.InitializeComponent();
        }
        private void FormInput_TextChanged(object 
            sender, TextChangedEventArgs args)
        {
            EvaluateFieldsValid(sender);
        }
        private bool EvaluateFieldsValid(object
            sender)
        {
            bool allValid = true;
            if(sender == TitleTextBox || sender ==
               null)
            {
                if (TitleTextBox.Text.Length == 0)
                {
                    allValid = false;
                    titleErrorNotification.Text = 
                        "Title must not be empty.";
                }
                Else
                {
                    titleErrorNotification.Text = "";
                }
            }
            return allValid;
        }
        private void CreateIssueButton_Click(object
            sender, RoutedEventArgs args)
        {
            if (EvaluateFieldsValid(null))
            {                
                createIssueVM.CreateIssueCommand.
                    Execute(null);
            }
        }
    }
}

使用这段代码,我们现在在输入字段的文本更改或用户点击CreateIssueCommand时,将运行输入验证。

  1. 最后,在NavigationViewModel.cs文件中,用以下代码替换Issues_OpenNewIssueViewCommand对象的创建,并添加必要的using语句。这样,当命令被调用时,CreateIssueView将被显示:
Issues_OpenNewIssueViewCommand = new RelayCommand(() =>
{
     Content = new CreateIssueView(new 
         CreateIssueViewModel(this));
});

现在,如果您启动应用程序并单击问题下拉菜单中的新问题选项,您将看到类似以下图 3.2的内容:

图 3.2 - 创建新问题界面

图 3.2 - 创建新问题界面

如果您尝试单击创建新问题按钮,您将在标题输入字段下方看到一条简短的消息,指出“标题不能为空”。在标题字段中输入文本后,消息将消失。虽然我们已经添加了简单的输入,但现在我们将使用 Windows Community Toolkit 添加更多的输入选项。

使用 Windows Community Toolkit 控件

到目前为止,用户只能输入标题和描述,并选择问题的类型。但是,我们还希望允许用户根据问题的类型输入特定的数据。为此,我们将使用 Windows Community Toolkit 提供的控件之一:SwitchPresenterSwitchPresenter控件允许我们根据已设置的属性呈现 UI 的特定部分,类似于 C#中的 switch case 的工作方式。

当然,SwitchPresenter不是来自 Windows Community Toolkit 的唯一控件;还有许多其他控件,例如GridSplitterMarkdownTextBlockDataGrid,我们将在使用 DataGrid 显示数据部分中使用。由于我们在本章的早些时候已经安装了必要的软件包,我们将向用户界面添加控件。让我们开始吧:

  1. CreateIssueView.xaml的描述TextBox控件下方添加以下 XAML 代码:
<wctcontrols:SwitchPresenter Value="{x:Bind createIssueVM.IssueType, Mode=OneWay}">
    <wctcontrols:SwitchPresenter.SwitchCases>
        <wctcontrols:Case Value="{x:Bind
            ubrcissues:IssueType.Train}">
            <StackPanel Orientation="Horizontal"
                Spacing="10">
                <StackPanel MinWidth="410" 
                    MaxWidth="800">
                    <TextBox x:Name=
                        "TrainNumberTextBox" 
                        Header="Train number" 
                        Text="{x:Bind
                          createIssueVM.TrainNumber,
                            Mode=TwoWay}"
                        HorizontalAlignment="Stretch"
                        TextChanged=
                          "FormInput_TextChanged"/>
                    <TextBlock x:Name=
                        "trainNumberErrorNotification"
                        Foreground="{ThemeResource 
                          SystemErrorTextColor}"/>
                </StackPanel>
            </StackPanel>
        </wctcontrols:Case>
        <wctcontrols:Case Value="{x:Bind 
            ubrcissues:IssueType.Station}">
            <StackPanel MinWidth="410" MaxWidth="800"
                HorizontalAlignment="Left">
                <TextBox x:Name="StationNameTextBox"
                  Header="Station name" Text="{x:Bind
                    createIssueVM.StationName,
                      Mode=TwoWay}"
                    HorizontalAlignment="Stretch"
                        TextChanged=
                            "FormInput_TextChanged"/>
                <TextBlock x:Name=
                    "stationNameErrorNotification" 
                        Foreground="{ThemeResource
                            SystemErrorTextColor}"/>
            </StackPanel>
        </wctcontrols:Case>
        <wctcontrols:Case Value="{x:Bind 
            ubrcissues:IssueType.Other}">
            <StackPanel MinWidth="410" MaxWidth="800"
                HorizontalAlignment="Left">
                <TextBox x:Name="LocationTextBox" 
                    Header="Location" Text="{x:Bind
                        createIssueVM.Location, 
                            Mode=TwoWay}"
                    HorizontalAlignment="Stretch"
                        TextChanged=
                            "FormInput_TextChanged"/>
                <TextBlock x:Name=
                    "locationErrorNotification"
                        Foreground="{ThemeResource 
                            SystemErrorTextColor}"/>
            </StackPanel>
        </wctcontrols:Case>
    </wctcontrols:SwitchPresenter.SwitchCases>
</wctcontrols:SwitchPresenter>

这使我们能够根据用户选择的问题类型显示特定的输入字段。这是因为SwitchPresenter根据已设置的Value属性呈现特定的Case。由于我们将其绑定到 ViewModel 的IssueType属性,所以每当用户更改问题类型时,它都会相应地更新。请注意,只有在我们将模式指定为OneWay时,此绑定才有效,因为x:Bind的默认绑定模式是OneTime,因此不会更新。

  1. 现在,在CreateIssueViewModel.xaml.cs中的EvaluateFields函数的返回语句之前添加以下代码:
if (sender == TrainNumberTextBox || sender == null)
{
    if (TrainNumberTextBox.Text.Length == 0)
    {
        if (createIssueVM.IssueType ==
            UnoBookRail.Common.Issues.IssueType.Train)
        {
            allValid = false;
        }
        trainNumberErrorNotification.Text = 
            "Train number must not be empty.";
    }
    else
    {
        trainNumberErrorNotification.Text = "";
    }
}
if (sender == StationNameTextBox || sender == null)
{
    if (StationNameTextBox.Text.Length == 0)
    {
        if (createIssueVM.IssueType ==
          UnoBookRail.Common.Issues.IssueType.Station)
        {
            allValid = false;
        }
        stationNameErrorNotification.Text = 
            "Station name must not be empty.";
    }
    else
    {
        stationNameErrorNotification.Text = "";
    }
}
if (sender == LocationTextBox || sender == null)
{
    if (LocationTextBox.Text.Length == 0)
    {
        if (createIssueVM.IssueType == 
            UnoBookRail.Common.Issues.IssueType.Other)
        {
            allValid = false;
        }
        locationErrorNotification.Text = 
            "Location must not be empty.";
    }
    else
    {
        locationErrorNotification.Text = "";
    }
}

现在,我们的输入验证也将考虑到新增的输入字段。请注意,只有当与问题相关的输入不符合验证过程时,我们才会阻止创建问题。例如,如果问题类型是Train,我们将忽略位置文本是否通过验证,用户可以创建新问题,无论位置输入是否通过验证阶段。

现在,如果您启动应用程序并导航到创建新问题视图,您将看到类似以下图 3.3的内容:

图 3.3 - 更新的问题创建视图。左:选择了问题 Train 类型;右:选择了问题 Station 类型

图 3.3 - 更新的问题创建视图。左:选择了问题 Train 类型;右:选择了问题 Station 类型

当您更改问题类型时,您会注意到表单会更改,并根据问题类型显示正确的输入字段。虽然我们允许用户创建新问题,但我们目前无法显示它们。在下一节中,我们将通过添加新视图来改变这一点,以显示问题列表。

使用 DataGrid 显示数据

由于 UnoBookRail 员工将使用此应用程序来管理现有问题,对于他们来说,查看所有问题以便轻松了解其当前状态非常重要。虽然没有内置的 UWP 和 Uno Platform 控件可以轻松实现这一点,但幸运的是,Windows Community Toolkit 包含了适合这种情况的正确控件:DataGrid

DataGrid控件允许我们将数据呈现为表格,指定要显示的列,并允许用户根据列对表格进行排序。然而,在开始使用DataGrid控件之前,我们需要创建 ViewModel 并准备视图:

  1. 首先,在ViewModels Solution文件夹中创建一个名为IssueListViewModel.cs的新类,并向其中添加以下代码:
using System.Collections.Generic;
using UnoBookRail.Common.Issues;
namespace ResourcePlanner.ViewModels
{
    public class IssueListViewModel
    {
        public readonly IList<Issue> Issues;
        public IssueListViewModel(IList<Issue> issues)
        {
            this.Issues = issues; 
        }
    }
}

由于我们只想显示问题的一个子集,例如导航到列车问题列表时,要显示的问题列表将作为构造函数参数传递。

  1. 现在,在Views文件夹中创建一个名为IssueListView.xaml的新UserControl

  2. 最后,在NavigationViewModel类的构造函数中,用以下代码替换创建Issues_OpenAllIssuesCommandIssues_OpenTrainIssuesCommandIssues_OpenTrainIssuesCommandIssues_OpenTrainIssuesCommand对象:

Issues_OpenAllIssuesCommand = new RelayCommand(() =>
{
    Content = new IssueListView(new IssueListViewModel
        (IssuesRepository.GetAllIssues()), this);
});
Issues_OpenTrainIssuesCommand = new RelayCommand(() =>
{
    Content = new IssueListView(new IssueListViewModel
        (IssuesRepository.GetAllIssues().Where(issue
            => issue.IssueType == 
                IssueType.Train).ToList()), this);
});
Issues_OpenStationIssuesCommand = new RelayCommand(() =>
{
    Content = new IssueListView(new IssueListViewModel
        (IssuesRepository.GetAllIssues().Where(issue
            => issue.IssueType == 
                IssueType.Station).ToList()), this);
});
Issues_OpenOtherIssuesCommand = new RelayCommand(() =>
{
    Content = new IssueListView(new IssueListViewModel
        (IssuesRepository.GetAllIssues().Where(issue 
            => issue.IssueType == 
                IssueType.Other).ToList()), this);
});

这使用户可以在用户从导航中单击相应元素时导航到问题列表,同时确保我们只显示与导航选项相关的列表中的问题。请注意,我们选择使用内联 lambda 创建命令。但是,您也可以声明函数并使用它们来创建RelayCommand对象。

现在我们已经添加了必要的 ViewModel 并更新了NavigationViewModel以允许我们导航到问题列表视图,我们可以继续编写我们的问题列表视图的 UI。

使用 DataGrid 控件显示数据

在我们实现问题列表视图之前,让我们快速介绍一下我们将使用的 DataGrid 的基本功能。有两种方法可以开始使用 DataGrid:

  • 让 DataGrid 自动生成列。这样做的缺点是,列标题将使用属性名称,除非您在AutoGeneratingColumn内部更改它们。虽然它们对于开始使用 DataGrid 控件是很好的,但通常不是最佳选择。此外,使用此方法,您无法选择要显示的列;相反,它将显示所有列。

  • 通过手动指定要包含的属性来指定要包含的属性。这种选项的优点是我们可以控制要包含的属性,并且还可以指定列名。当然,这也意味着我们必须确保我们的绑定是正确的,这是潜在的错误原因。

通过设置 DataGrid 的Columns属性并提供DataGridColumn对象的集合来指定 DataGrid 的列。对于某些数据类型,已经有内置的列可以使用,例如DataGridTextColumn用于基于文本的数据。每列都允许您通过指定Header属性以及用户是否可以通过CanUserSort属性对列进行排序来自定义显示的标题。对于没有内置DataGridColumn类型的更复杂数据,您还可以实现自己的DataGridColumn对象。或者,您还可以使用DataGridTemplateColumn,它允许您基于指定的模板呈现单元格。为此,您可以指定一个CellTemplate对象,用于呈现单元格,并一个CellEditTemplate对象,用于让用户编辑当前单元格的值。

除了指定列之外,DataGrid 控件还有更多您可以自定义的功能。例如,DataGrid 允许您选择行并自定义行和单元格背景。现在,让我们继续编写我们的问题列表。

现在我们已经介绍了 DataGrid 的基础知识,让我们继续编写我们的问题列表显示界面:

  1. 为此,请将以下代码添加到IssueListView.xaml.cs文件中:
using Microsoft.Toolkit.Uwp.UI.Controls;
using ResourcePlanner.ViewModels;
using UnoBookRail.Common.Issues;
using Windows.UI.Xaml.Controls;
namespace ResourcePlanner.Views
{
    public sealed partial class IssueListView :
        UserControl
    {
        private IssueListViewModel issueListVM;
        private NavigationViewModel navigationVM;
        public IssueListView(IssueListViewModel
            viewModel, NavigationViewModel 
                navigationViewModel)
        {
            this.issueListVM = viewModel;
            this.navigationVM = navigationViewModel;
            this.InitializeComponent();
        }
        private void IssueList_SelectionChanged(object
            sender, SelectionChangedEventArgs e)
        {
            navigationVM.SetSelectedIssue((sender as 
                DataGrid).SelectedItem as Issue);
        }
    }
}

这允许我们从 DataGrid 创建到问题列表的绑定。请注意,我们还将添加一个SelectionChanged处理程序函数,以便我们可以通知NavigationViewModel是否已选择问题。我们这样做是因为某些选项只有在选择问题时才有意义。其中一个选项是导出为 PDF选项,我们将在以 PDF 格式导出问题部分中实现。

  1. 将以下 XAML 命名空间定义添加到IssueListView.xaml文件中:
xmlns:wct="using:Microsoft.Toolkit.Uwp.UI.Controls"
  1. 现在,请用以下 XAML 替换IssueListView.xaml文件中的Grid
<wct:DataGrid
    SelectionChanged="IssueList_SelectionChanged"
    SelectionMode="Single"
    AutoGenerateColumns="False"
    ItemsSource="{x:Bind 
        issueListVM.Issues,Mode=OneWay}">
    <wct:DataGrid.Columns>
        <wct:DataGridTextColumn Header="Title"
            Binding="{Binding Title}" 
           IsReadOnly="True" CanUserSort="True"/>
        <wct:DataGridTextColumn Header="Type"
            Binding="{Binding IssueType}" 
            IsReadOnly="True" CanUserSort="True"/>
        <wct:DataGridTextColumn Header="Creator" 
            Binding="{Binding OpenedBy.FormattedName}"
            IsReadOnly="True" CanUserSort="True"/>
        <wct:DataGridTextColumn Header="Created on" 
            Binding="{Binding OpenDate}" 
            IsReadOnly="True" CanUserSort="True"/>
        <wct:DataGridCheckBoxColumn Header="Open" 
            Binding="{Binding IsOpen}" 
            IsReadOnly="True" CanUserSort="True"/>
        <wct:DataGridTextColumn Header="Closed by" 
            Binding="{Binding ClosedBy.FormattedName}"
            IsReadOnly="True" CanUserSort="True"/>
        <wct:DataGridTextColumn Header="Closed on" 
            Binding="{Binding CloseDateReadable}" 
            IsReadOnly="True" CanUserSort="True"/>
    </wct:DataGrid.Columns>
</wct:DataGrid>

在这里,我们为问题的最重要字段添加了列。请注意,我们只允许更改标题,因为其他字段需要比 DataGrid 表格布局更容易显示的更多逻辑。由于在这种情况下不支持x:Bind,我们使用Binding将属性绑定到列。

现在,如果您启动应用程序并创建一个问题,您将看到类似于以下图 3.4的内容:

图 3.4 - DataGrid 显示演示问题

图 3.4 - DataGrid 显示演示问题

在本节中,我们只涵盖了使用 Windows Community Toolkit DataGrid 控件的基础知识。如果您希望了解更多关于 DataGrid 控件的信息,官方文档包含了涵盖不同可用 API 的实际示例。您可以在这里找到更多信息:docs.microsoft.com/en-us/windows/communitytoolkit/controls/datagrid。现在我们可以显示现有问题列表,接下来我们将编写问题的 PDF 导出。作为其中的一部分,我们还将学习如何编写一个自定义的 Uno Platform 控件,我们将仅在 Web 上使用。

以 PDF 格式导出问题

除了能够在业务应用程序的界面中查看数据之外,通常还希望能够导出数据,例如作为 PDF,以便可以打印或通过电子邮件发送。为此,我们将编写一个允许用户将给定问题导出为 PDF 的接口。由于没有内置的 API 可用,我们将使用iText库。请注意,如果您想在应用程序中使用该库,您需要遵循 AGPL 许可证或购买该库的商业许可证。但是,在我们编写生成 PDF 的代码之前,我们需要准备项目:

  1. 首先,我们需要安装iText NuGet 包。为此,请右键单击解决方案并搜索iText。选择该包。然后,从项目列表中选择macOSUWPWASM头,并单击安装

  2. 现在,在ViewModels文件夹中创建一个名为ExportIssueViewModel.cs的类,其中包含以下代码:

using iText.Kernel.Pdf;
using iText.Layout;
using iText.Layout.Element;
using Microsoft.Toolkit.Mvvm.Input;
using System;
using System.IO;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Windows.Input;
using UnoBookRail.Common.Issues;
namespace ResourcePlanner.ViewModels
{
    public class ExportIssueViewModel
    {
        public readonly Issue Issue;
        public ICommand SavePDFClickedCommand;
        public ExportIssueViewModel(Issue issue)
        {
            Issue = issue;
            SavePDFClickedCommand = 
               new RelayCommand(async () => { });
        }
    }
}

请注意,我们现在添加这些using语句,因为我们稍后在本节中会用到它们。

  1. 现在,在Views文件夹中创建一个名为ExportIssueView.xaml的新UserControl

  2. 请用以下内容替换ExportIssueView.xaml.cs中的代码:

using ResourcePlanner.ViewModels;
using Windows.UI.Xaml.Controls;
namespace ResourcePlanner.Views
{
    public sealed partial class ExportIssueView : 
        UserControl
    {
        private ExportIssueViewModel exportIssueVM;
        public ExportIssueView(ExportIssueViewModel 
            viewModel)
        {
            this.exportIssueVM = viewModel;
            this.InitializeComponent();
        }
    }
}
  1. 请用 GitHub 上的代码替换ExportIssueView.xaml中的代码:

github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/blob/main/Chapter03/ResourcePlanner.Shared/Views/ExportIssueView.xaml

  1. 最后,在NavigationViewModel.cs文件中用以下代码替换Issue_ExportIssueViewCommand的创建:
Issues_ExportIssueViewCommand = new RelayCommand(() =>
{
    Content = new ExportIssueView(new 
        ExportIssueViewModel(this.selectedIssue));
});

现在我们已经添加了必要的接口,接下来我们将编写将问题导出为 PDF 的代码。由于桌面上的行为与网络上的行为不同,我们将先介绍桌面版本。

在桌面上导出

由于我们已经编写了用户界面,允许用户导出问题,唯一剩下的就是更新ExportIssueViewModel以生成 PDF 并为用户提供访问方式。在桌面上,我们将 PDF 文件写入本地文件系统并打开它。由于应用程序也是 UWP 应用程序,我们将文件写入应用程序的本地文件夹。现在,让我们更新ExportIssueViewModel

  1. 首先,在ExportIsseuViewModel类内创建一个名为GeneratePDF的新函数,代码如下:
public byte[] GeneratePDF()
{
    byte[] bytes;
    using (var memoryStream = new MemoryStream())
    {       
        bytes = memoryStream.ToArray();
    }
    return bytes;
}
  1. 现在,在using块内的赋值之前添加以下代码:
var pdfWriter = new PdfWriter(memoryStream);
var pdfDocument = new PdfDocument(pdfWriter);
var document = new Document(pdfDocument);
document.Close();

这将创建一个新的PdfWriterPdfDocument,它将使用MemoryStream对象写入到字节数组中。

  1. 在添加PDFWriterPDFDocumentDocument之后,添加以下代码来编写文档的标题:
var header = new Paragraph("Issue export: " +
    Issue.Title)
     .SetTextAlignment(
        iText.Layout.Properties.TextAlignment.CENTER)
     .SetFontSize(20);
document.Add(header);

这将创建一个新的段落,其中包含文本“问题导出:”和问题的标题。它还设置了文本对齐和字体大小,以便更容易区分为文档的标题。

  1. 由于我们还想导出有关问题的信息,请在调用document.Close()之前添加以下代码:
var issueType = new Paragraph("Type: " + Issue.IssueType);
document.Add(issueType);
switch (Issue.IssueType)
{
    case IssueType.Train:
        var trainNumber = new Paragraph("Train number: "
             + Issue.TrainNumber);
        document.Add(trainNumber);
        break;
    case IssueType.Station:
        var stationName = new Paragraph("Station name: "
             + Issue.StationName);
        document.Add(stationName);
        break;
    case IssueType.Other:
        var location = new Paragraph("Location: " + 
            Issue.Location);
        document.Add(issueType);
        break;
}
var description = new Paragraph("Description: " + Issue.Description);
document.Add(description);

这将根据问题的类型向 PDF 文档添加必要的段落。除此之外,我们还将问题的描述添加到 PDF 文档中。

注意

由于在向文档添加第一个元素时出现NullReferenceException的错误。不幸的是,在撰写本书时,没有已知的解决方法。这只会在调试器附加时发生,并且不会在应用程序运行时造成任何问题。在调试器附加时运行应用程序,您可以通过工具栏点击继续来继续调试应用程序。

  1. 最后,用以下代码替换SavePDFClickedCommand的创建:
SavePDFClickedCommand = new RelayCommand(async () =>
{
#if !__WASM__
    var bytes = GeneratePDF();
    var tempFileName = 
        $"{Path.GetFileNameWithoutExtension
            (Path.GetTempFileName())}.pdf";
    var folder = Windows.Storage.ApplicationData.
        Current.TemporaryFolder;
    await folder.CreateFileAsync(tempFileName, 
        Windows.Storage.CreationCollisionOption.
            ReplaceExisting);
    var file = await
        folder.GetFileAsync(tempFileName);
    await Windows.Storage.FileIO.WriteBufferAsync
        (file, bytes.AsBuffer());
    await Windows.System.Launcher.LaunchFileAsync
        (file);
#endif
});

这将创建一个 PDF,将其保存到apps临时文件夹,并使用默认的 PDF 处理程序打开它。

注意

在本章中,我们将文件写入临时文件夹,并使用默认的 PDF 查看器打开它。根据您的应用程序和用例,FileSavePicker和其他文件选择器可能非常合适。您可以在这里了解更多关于FileSavePicker和其他可用文件选择器的信息:platform.uno/docs/articles/features/windows-storage-pickers.html

要尝试问题导出,请启动应用程序并创建一个新问题。之后,从问题列表中选择问题,并从顶部的问题下拉菜单中单击导出为 PDF。现在,如果单击创建 PDF,PDF 将被创建。之后不久,PDF 将在您的默认 PDF 查看器中打开。PDF 应该看起来像这样:

图 3.5 - 演示问题导出 PDF

图 3.5 - 演示问题导出 PDF

由于在 WASM 上运行应用程序时无法将文件写入用户的本地文件系统,因此在接下来的部分中,我们将通过编写自定义 HTML 元素控件来更新我们的应用程序,以在 WASM 上提供下载链接,而不是使用创建 PDF按钮。

通过下载链接在网络上导出

Uno Platform 的主要功能是运行在所有平台上的代码,它还允许开发人员编写特定于平台的自定义控件。您可以利用这一点来使用特定于平台的控件。在我们的情况下,我们将使用它来创建一个 HTML a-tag,为我们应用程序的 WASM 版本提供下载链接。我们将使用Uno.UI.Runtime.WebAssembly.HtmlElement属性来实现这一点:

  1. 首先,在Views文件夹中创建一个名为WasmDownloadElement.cs的新类,并添加以下代码:
using System;
using System.Collections.Generic;
using System.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace ResourcePlanner.Views
{
#if __WASM__
    [Uno.UI.Runtime.WebAssembly.HtmlElement("a")]
    public class WasmDownloadElement : ContentControl
    {
    }
#endif
}

这将是我们的a标签,我们将使用它来允许用户下载问题导出的 PDF。由于我们只希望在 WASM 上使用此控件,因此我们将其放在#if __WASM__预处理指令内。

  1. 为了能够自定义下载的 MIME 类型和下载文件的名称,请将以下代码添加到WasmDownloadElement类中:
public static readonly DependencyProperty MimeTypeProperty = DependencyProperty.Register(
    "MimeType", typeof(string),
        typeof(WasmDownloadElement), new
        PropertyMetadata("application/octet-stream",
        OnChanged));
public string MimeType
{
    get => (string)GetValue(MimeTypeProperty);
    set => SetValue(MimeTypeProperty, value);
}
public static readonly DependencyProperty FileNameProperty = DependencyProperty.Register(
    "FileName", typeof(string),
        typeof(WasmDownloadElement), new 
        PropertyMetadata("filename.bin", OnChanged));
public string FileName
{
    get => (string)GetValue(FileNameProperty);
    set => SetValue(FileNameProperty, value);}
private string _base64Content;
public void SetBase64Content(string content)
{
    _base64Content = content;
    Update();
}
private static void OnChanged(DependencyObject dependencyobject, DependencyPropertyChangedEventArgs args)
{
    if (dependencyobject is WasmDownloadElement wd)
    {
        wd.Update();
    }
}
private void Update()
{
    if (_base64Content?.Length == 0)
    {
        this.ClearHtmlAttribute("href");
    }
    else
    {
        var dataUrl =
           $"data:{MimeType};base64,{_base64Content}";
        this.SetHtmlAttribute("href", dataUrl);
        this.SetHtmlAttribute("download", FileName);
    }
}

尽管这是很多代码,但我们只在WasmDownloadElement类上创建了两个DependencyProperty字段,即MimeTypeFileName,并允许它们设置将要下载的内容。其余的代码处理在底层控件上设置正确的属性。

  1. 最后,在ExportIssueView的构造函数中添加以下代码,调用this.InitializeComponent()后:
#if __WASM__
    this.WASMDownloadLink.MimeType =
       "application/pdf";
    var bytes = exportIssueVM.GeneratePDF();
    var b64 = Convert.ToBase64String(bytes);
    this.WASMDownloadLink.SetBase64Content(b64);
#endif

这将在下载链接上设置正确的 MIME 类型,并设置正确的内容进行下载。请注意,我们在本章前面在ExportIssueView.xaml文件中定义了WASMDownloadLink元素。

要测试这一点,请启动应用程序的 WASM 头。加载完成后,创建一个问题,然后从问题列表中选择它,然后通过问题选项点击导出为 PDF。现在,您应该看到下载 PDF选项,而不是创建 PDF按钮,如图 3.6所示:

图 3.6 - 在 WASM 上导出 PDF

图 3.6 - 在 WASM 上导出 PDF

点击链接后,PDF 导出将被下载。

总结

在本章中,我们构建了一个桌面应用程序,可以在 Windows、macOS 和 Web 上使用 WASM。我们介绍了如何编写带有输入验证的数据输入表单以及如何使用 Windows Community Toolkit。之后,我们学习了如何使用 Windows Community Toolkit DataGrid 控件显示数据。最后,我们介绍了如何以 PDF 格式导出数据,并通过编写自定义 HTML 控件提供了下载链接。

在下一章中,我们将构建一个移动应用程序。虽然它也将被设计用于 UnoBookRail 的员工使用,但主要重点将放在在移动设备上运行应用程序。除其他事项外,我们将利用这个应用程序来研究如何处理不稳定的连接以及使用设备功能,如相机。

第四章:使您的应用程序移动化

本章将向您展示如何使用 Uno 平台为移动设备开发应用程序。这样的应用程序可能与在桌面设备或 Web 上运行的应用程序有很大的不同,并带来了您必须考虑的挑战。

在本章中,我们将涵盖以下主题:

  • 为运行 iOS 和 Android 的移动设备构建

  • 在偶尔连接的环境中使用远程数据

  • 为其运行的平台设计应用程序的样式

  • 利用应用程序所在设备的功能

在本章结束时,您将创建一个在 Android 和 iOS 设备上运行的移动应用程序,每个平台上的外观都不同,并与远程服务器通信以检索和发送数据。

技术要求

本章假设您已经设置好了开发环境,并安装了必要的项目模板,就像我们在第一章 介绍 Uno 平台中所介绍的那样。本章的源代码可以在github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/Chapter04找到。

本章的代码使用以下库:github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/SharedLibrary

本章还从远程 Web 服务器检索数据,您可以使用github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/WebApi的代码重新创建。

查看以下视频以查看代码的运行情况:bit.ly/3jKGRkI

介绍应用程序

我们将在本章中构建的应用程序称为Network Assist。这是一个将提供给所有员工使用的应用程序。对于在公共场合工作的人来说,这是特别有用的。这个应用程序的真实版本将有许多功能,但我们只会实现两个:

  • 显示下一班火车将到达每个车站的时间

  • 记录和报告发生在网络周围的事件的细节。

由于这个应用程序将被员工在整个网络上执行工作时使用,它将被构建为在 Android 和 iOS 设备上运行。

“移动”是什么意思?

很容易认为“移动”只是关于应用程序所在的设备,但这样做是有限制的。“移动”可以是“Android 和 iOS 设备”的一个有用的简称。然而,重要的是要记住,移动不仅仅是指手机(或平板电脑)。使用设备的人也是移动的。考虑将使用应用程序的人通常比运行应用程序的设备更重要。设备只是要考虑的一个因素。一个人可能在过程中使用多个设备,因此需要体验在他们在设备之间移动时也是移动的 - 也许在一个设备上开始一个任务,然后在另一个设备上完成它。

我们构建 Network Assist 应用程序为移动应用程序的主要原因是因为将使用它的人将整天四处旅行。正因为人是移动的,我们才构建了一个在“移动”设备上运行的“移动”应用程序。

与其花费大量时间事先解释功能,不如开始构建应用程序。我们将在编写代码时扩展需求。

创建应用程序

我们将从创建应用程序的解决方案开始:

  1. 在 Visual Studio 中,使用多平台应用程序(Uno 平台)模板创建一个新项目。

  2. 将项目命名为NetworkAssist。你可以使用不同的名称,但需要相应地调整所有后续的代码片段。

  3. 删除所有平台头项目,除了 AndroidiOSUWP

始终保留 UWP 头在解决方案中

即使您不打算发布应用程序的 UWP 版本,保留 UWP 头在解决方案中也有两个原因。首先,当诊断任何编译错误时,这可能是有帮助的,以检查代码是否存在基本问题,或者问题是否与 Uno 特定的工具有关。其次,更重要的是,当选择 UWP 头时,Visual Studio 可以提供额外的工具和智能感知。通过在项目中添加 UWP 头,您的 Uno 平台开发体验将更加简单。

  1. 为了避免写更多的代码,我们将添加对共享库项目的引用。在UnoBookRail.Common.csproj文件中,右键单击解决方案节点,然后点击打开

  2. 对于每个特定平台的项目,我们需要添加对通用库项目的引用。在解决方案资源管理器中右键单击Android项目节点,然后选择添加 > 引用... > 项目。然后,选中UnoBookRail.Common的条目,然后点击确定。现在,重复此过程用于 iOS 和 UWP 项目

基本解决方案结构现在已经准备就绪,我们可以向主页添加一些功能。

创建主页

由于这将是一个简单的应用程序,我们将把所有功能放在一个页面上。设计要求是应用程序在屏幕底部有选项卡或按钮,以便在不同功能区域之间进行切换。我们将把不同的功能放在单独的控件中,并根据用户按下的按钮(或选项卡)来更改显示的控件。

这是合适的,因为用户不需要通过他们已经查看过的选项卡后退。

允许相机凹口、切口和安全区域

在添加任何自己的内容之前,您可能希望运行应用程序,以检查是否一切都可以编译和调试。根据您运行应用程序的设备或模拟器,您可能会看到图 4.1左侧的内容,显示了在 iPhone 12 模拟器上运行的默认应用程序。在这个图中,您可以看到Hello, World!文本重叠(或撞到)时间,并且在相机凹口后面。

如果您没有设备可以测试这个功能,一些模拟器可以模拟这个凹口。其他模拟器将有一个可配置的选项,允许在有或没有切口的情况下进行测试。在设置 > 系统 > 开发人员选项 > 模拟具有切口的显示下查找:

图 4.1 - 显示允许状态栏和相机凹口的内容的前后截图

图 4.1 - 显示允许状态栏和相机凹口的内容的前后截图

我们的应用程序不会有Hello, World!文本,但我们不希望我们的内容被遮挡。幸运的是,Uno 平台带有一个辅助类,可以为相机凹口留出空间,无论它们在哪种设备上或者它们的位置如何。

要使用这个辅助类,我们需要做以下几步:

  1. MainPage.xaml的根元素Page中添加xmlns:toolkit="using:Uno.UI.Toolkit"

  2. Page元素内部的Grid元素中添加toolkit:VisibleBoundsPadding.PaddingMask="All"。通过设置All的值,如果设备横向旋转,辅助类将提供适当的空间,并且凹口将显示在屏幕的侧面。

现在运行应用程序,你会看到类似于图 4.1右侧图像的东西,它展示了布局已经添加了足够的空间。这样可以防止状态栏或相机凹口遮挡我们的内容。

现在我们已经处理了屏幕上的切口,我们可以实现应用程序所需的功能。

实现主页面的内容

由于应用程序中只有一个页面,我们现在将实现它:

  1. 用以下内容替换Grid的现有内容:
<Grid.RowDefinitions>
    <RowDefinition Height="*" />
    <RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<CommandBar VerticalAlignment="Bottom" Grid.Row="1">
    <CommandBar.PrimaryCommands>
        <AppBarButton Icon="Clock" Label="Arrivals" 
            Click="ShowArrivals" />
        <AppBarButton Label="Quick Report" 
            Click="ShowQuickReport">
            <AppBarButton.Icon>
                <FontIcon Glyph="&#xE724;" />
            </AppBarButton.Icon>
        </AppBarButton>
    </CommandBar.PrimaryCommands>
</CommandBar>

网格的顶行将包含不同功能元素的控件。底行将承载选择不同控件的按钮。

我们使用CommandBar,因为这是最适合在应用程序中提供选择功能区域按钮的 UWP 控件。这只是我们希望在 iOS 和 Android 上看到的外观的近似值,我们将很快解决这些问题。

注意

XAML 提供了多种方法来实现相似的结果。在本章的代码中,我们使用了最简单的方法来在所有平台上提供一致的输出。

  1. 现在我们需要自定义控件来显示不同的功能。首先右键单击Views,以匹配存储 UI 相关控件的约定。

如果您愿意,可以将MainPage文件移入Views文件夹,但这对应用程序的功能并不重要。

  1. 在新文件夹中,右键单击并选择ArrivalsControl。重复此操作以添加名为QuickReportControl的控件。

  2. 现在我们将控件添加到MainPage.xaml。在页面级别声明一个新的 XML 命名空间别名,值为xmlns:views="using:Network Assist.Views"。在Grid标签的开头和CommandBar之前,添加以下内容以创建我们新控件的实例:

<views:ArrivalsControl x:Name="Arrivals" Visibility="Visible" />
<views:QuickReportControl x:Name="QuickReport" Visibility="Collapsed" />
  1. 在代码后台文件(MainPage.xaml.cs)中,我们需要添加处理 XAML 中AppBarButtons引用的Click事件的方法:
public void ShowArrivals(object sender, RoutedEventArgs args) 
{
    Arrivals.Visibility = Visibility.Visible; 
    QuickReport.Visibility = Visibility.Collapsed;
}
public void ShowQuickReport(object sender, RoutedEventArgs args) 
{
    Arrivals.Visibility = Visibility.Collapsed; 
    QuickReport.Visibility = Visibility.Visible;
}

我们将在这里使用点击事件和代码后台,因为逻辑与 UI 紧密耦合,并且不会受益于编写的测试。可以使用ICommand实现和绑定来控制每个控件何时显示,但如果您希望这样实现,可以自行实现。

MVVM 和代码后台

在本章中,我们将使用代码后台文件和Model-View-ViewModelMVVM)模式的组合。有三个原因。首先,它使我们可以使代码更短,更简单,这样您就更容易跟随。其次,它避免了解释特定的 MVVM 框架或实现的需要,而我们可以专注于与应用程序相关的代码。最后,它表明 Uno 平台不会强迫您以特定方式工作。您可以使用您喜欢的编码风格、模式或框架。

主页面已经运行,现在我们可以添加显示即将到达的详细信息的功能。

显示即将到达的详细信息

显示即将到达的要求如下:

  • 显示站点列表,并在选择一个站点时,显示每个方向的下三列火车的到达时间。

  • 数据可以刷新以确保始终有最新的信息可用。

  • 显示检索到最后一条数据的时间。

  • 如果未选择站点或检索数据时出现问题,则会显示提示。

  • 应用程序指示正在检索数据时。

您可以在本章结束时创建的最终功能示例中看到以下图示:

图 4.2 - iPhone 上显示的即将到达的详细信息(左)和 Android 设备上(右)

图 4.2 - iPhone 上显示的即将到达的详细信息(左)和 Android 设备上(右)

用于显示即将到达的用户控件将是应用程序中最复杂的 UI 部分。看起来可能有很多步骤,但每一步都很简单:

  1. 首先在ArrivalsControl.xaml中的Grid中添加两个列定义和四个行定义:
<Grid.ColumnDefinitions>
    <ColumnDefinition Width="*" />
    <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="Auto" />
    <RowDefinition Height="Auto" />
    <RowDefinition Height="*" />
</Grid.RowDefinitions>
  1. 顶部行将包含一个用于选择车站的ComboBox控件和一个用于请求刷新数据的Button元素:
<ComboBox x:Name="StationList"
    HorizontalAlignment="Stretch" 
    VerticalAlignment="Stretch"
    ItemsSource="{x:Bind VM.ListOfStations}"
    SelectedItem="{x:Bind VM.SelectedStation, 
        Mode=TwoWay}"
    SelectionChanged="OnStationListSelectionChanged"
    SelectionChangedTrigger="Always">
    <ComboBox.ItemTemplate>
        <DataTemplate xmlns:network="using:UnoBookRail.Common.Network".
  1. 接下来的两行将使用TextBlocks来显示上次检索数据的时间以及检索数据时是否出现问题:
<TextBlock 
    Grid.Row="1" 
    Grid.ColumnSpan="2" 
    Margin="4"
    HorizontalAlignment="Stretch"
    HorizontalTextAlignment="Right"
    Text="{x:Bind VM.DataTimestamp, Mode=OneWay}" />
<TextBlock 
    Grid.Row="2" 
    Grid.ColumnSpan="2"
    Margin="4"
    HorizontalAlignment="Stretch"
    HorizontalTextAlignment="Right"
    Foreground="Red" 
    TextWrapping="WrapWholeWords"
    Text="Connectivity issues: data may not be up to 
          date!"
    Visibility="{x:Bind VM.ShowErrorMsg, 
        Mode=OneWay}"/>
  1. ListView将使用我们在控件级别定义的一些数据模板。在打开的UserControl标签之后添加以下内容:
<UserControl.Resources>
  <DataTemplate x:Key="HeaderTemplate">
       <Grid HorizontalAlignment="Stretch" 
           Background="{ThemeResource 
               ApplicationPageBackgroundThemeBrush}">
      <TextBlock 
          Margin="0" 
          FontWeight="Bold"
          Style="{StaticResource 
                  SubheaderTextBlockStyle}"
          Text="{Binding Platform}" />
    </Grid>
  </DataTemplate>
  <DataTemplate x:Key="ItemTemplate">
    <Grid Margin="0,10">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="100" />
        <ColumnDefinition Width="*" />
      </Grid.ColumnDefinitions>
      <TextBlock 
          Margin="0,10"
          Style="{StaticResource TitleTextBlockStyle}"
          Text="{Binding DisplayedTime}" />
      <TextBlock 
          Grid.Column="1" 
          Margin="0,10"
          Style="{StaticResource TitleTextBlockStyle}"
          Text="{Binding Destination}" />
    </Grid>
  </DataTemplate>
</UserControl.Resources>
  1. 第四行,也是最后一行,包含一个ListView,显示即将到达的到站时间:
<ListView Grid.Row="3" 
    Grid.ColumnSpan="2"
    ItemTemplate="{StaticResource ItemTemplate}"
    ItemsSource="{x:Bind VM.ArrivalsViewSource}"
    SelectionMode="None">
    <ListView.GroupStyle>
        <GroupStyle HeaderTemplate="{StaticResource 
            HeaderTemplate}" />
    </ListView.GroupStyle>
</ListView>
  1. 第四行还包含一个Grid,其中包含其他信息控件,根据需要显示在ListView上或替代ListView
<Grid Grid.Row="3" Grid.ColumnSpan="2">
    <TextBlock HorizontalAlignment="Stretch"
        VerticalAlignment="Center"
        HorizontalTextAlignment="Center"
        Style="{StaticResource 
                SubheaderTextBlockStyle}"
        Text="Select a station" TextWrapping="NoWrap"
        Visibility="{x:Bind VM.ShowNoStnMsg,
            Mode=OneWay}" />
    <ProgressRing Width="100" Height="100"
        IsActive="True" IsEnabled="True"
        Visibility="{x:Bind VM.IsBusy, Mode=OneWay}"
    />
</Grid>
  1. 我们在这里添加了相当多的 XAML。看看它的外观的第一步是连接 ViewModel,以便我们可以访问相关属性和命令。将ArrivalsControlxaml.cs的内容更改为以下内容:
public sealed partial class ArrivalsControl : UserControl {
    private ArrivalsViewModel VM to help keep the code concise) in the constructor, and it's this class that contains most of the logic.The code-behind also includes a method to handle the `SelectionChanged` event on the `ComboBox`. This is currently necessary as a workaround for a bug due to the order that `ComboBox` events are raised in. The bug is logged at [`github.com/unoplatform/uno/issues/5792`](https://github.com/unoplatform/uno/issues/5792). Once fixed, it should be possible to bind to a `Command` on the ViewModel to perform the equivalent functionality.
  1. 将以下using声明添加到文件顶部,以便编译器可以找到我们刚刚添加的类型:
using NetworkAssist.ViewModels;
using UnoBookRail.Common.Network;
  1. 现在我们准备创建一个包含剩余功能逻辑的 ViewModel。我们将首先创建一个名为ViewModels的文件夹。在该文件夹中,创建一个名为ArrivalsViewModel的类。

  2. 为了避免在遵循 MVVM 模式时编写常见的代码,需要在每个平台头项目中添加对Microsoft.Toolkit.Mvvm NuGet 包的引用*:

Install-Package Microsoft.Toolkit.Mvvm -Version 7.0.2
  1. 更新ArrivalsViewModel类,使其继承自Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObject

  2. ArrivalsViewModel将使用来自不同位置的类型,因此我们需要引用以下命名空间:

using Microsoft.Toolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows.Input;
using UnoBookRail.Common.Network;
using Windows.UI.Xaml.Data;
  1. 首先,在类中添加以下字段:
private static DataService _data = DataService.Instance;
private List<Station> _listOfStations;
private ObservableCollection<StationArrivalDetails> 
_arrivals = 
    new ObservableCollection<StationArrivalDetails>();
private Station _selectedStation = null;
private string _dataTimestamp;
private bool _isBusy;
private bool _showErrorMsg;
  1. 我们的ViewModel需要以下属性,因为它们在我们之前定义的 XAML 绑定中被引用。它们将使用我们刚刚添加的后备字段:
public List<Station> ListOfStations 
{
    get => _listOfStations;
    set => SetProperty(ref _listOfStations, value);
}
public bool ShowErrorMsg 
{
    get => _showErrorMsg;
    set => SetProperty(ref _showErrorMsg, value);
}
public Station SelectedStation 
{
    get => _selectedStation;
    set {
        if (SetProperty(ref _selectedStation, value)) 
        {
            OnPropertyChanged(nameof(ShowNoStnMsg));
        }
    }
}
public ObservableCollection<StationArrivalDetails> Arrivals 
{
    get => _arrivals;
    set => SetProperty(ref _arrivals, value);
}
public string DataTimestamp 
{
    get => _dataTimestamp;
    set => SetProperty(ref _dataTimestamp, value);
}
public bool IsBusy 
{
    get => _isBusy;
    set => SetProperty(ref _isBusy, value);
}
public IEnumerable<object> ArrivalsViewSource => new CollectionViewSource() 
{
    Source = Arrivals,
    IsSourceGrouped = true
}.View;
public bool ShowNoStnMsg => SelectedStation == null;
public ICommand RefreshCommand { get; }
public ICommand SelectionChangedCommand { get; }
  1. 我们将使用构造函数来初始化车站列表和命令:
public ArrivalsViewModel() 
{
    ListOfStations = _data.GetAllStations();
    RefreshCommand = new AsyncRelayCommand(async () =>
        { await LoadArrivalsDataAsync(); });
    SelectionChangedCommand = new AsyncRelayCommand(
        async () => { await LoadArrivalsDataAsync(); 
            });
}
  1. 现在,添加处理检索和显示数据的方法:
public async Task LoadArrivalsDataAsync(int stationId = 0)
{
  if (stationId < 1) 
  {
    // if no value passed use the previously selected 
    // Id.
    stationId = SelectedStation?.Id ?? 0;
  }
  else 
  { 
    // We've changed station so clear current details
    Arrivals.Clear();
    DataTimestamp = string.Empty;
    ShowErrorMsg = false;
  }
  if (stationId > 0) 
  {
    IsBusy = true;
    try {
      var arr = await 
          _data.GetArrivalsForStationAsync(stationId);
      ShowErrorMsg = false;
      if (arr.ForStationId == stationId) 
      {
        DataTimestamp = 
            $"Updated at {arr.Timestamp:t}";
        Arrivals.Clear();
        if (!string.IsNullOrEmpty(
            arr.DirectionOneName)) 
        {
          var d1details = new StationArrivalDetails
              (arr.DirectionOneName);
          d1details.AddRange(arr.DirectionOneDetails);
          Arrivals.Add(d1details);
        }
        if (!string.IsNullOrEmpty(
            arr.DirectionTwoName)) 
        {
          var d2details = new StationArrivalDetails(
              arr.DirectionTwoName);
          d2details.AddRange(arr.DirectionTwoDetails);
          Arrivals.Add(d2details);
        }
      }
    }
    catch (Exception exc) {
      // Log this or take other appropriate action
      ShowErrorMsg = true;
    }
    finally {
      IsBusy = false;
    }
  }
}
  1. 您可能已经注意到数据是从单例DataService类中检索的。我们将首先创建一个简单版本,稍后再扩展。通常约定将此类放在名为Services的目录中,尽管您也可以将其放在ViewModels文件夹中:
using System.Linq;
using System.Threading.Tasks;
using UnoBookRail.Common.Network;
public class DataService 
{
    private static readonly Lazy<DataService> ds =
        new Lazy<DataService>(() => new
            DataService());
    private static readonly Lazy<Stations> stations =
        new Lazy<Stations>(() => new Stations());
    public static DataService Instance => ds.Value;
    private DataService() { }
    public List<Station> GetAllStations() => 
        stations.Value.GetAll().OrderBy(s => 
            s.Name).ToList();
    public async Task<Arrivals> 
        GetArrivalsForStationAsync method may seem overly complex.
  1. 现在我们有了DataService类,可以检索到达详情,但是我们需要做更多工作来显示它们。我们还需要另一个类。这是StationArrivalDetails,它允许我们按站台和列车行驶方向对信息进行分组。在ViewModels目录中创建这个类:
using UnoBookRail.Common.Network;
public class StationArrivalDetails : 
List<ArrivalDetail> 
{
    public StationArrivalDetails(string platform) 
    {
        Platform = platform;
    }
    public string Platform { get; set; }
}

Uno 中使用分组数据的 CollectionViewSource

在 Uno 平台上显示分组列表比在 UWP 上更复杂。如果您以前在 UWP 应用程序中使用过CollectionViewSource,那么您可能已经在 XAML 中定义了它,而不是作为IEnumerable<object>。不幸的是,为了 Uno 平台能够正确渲染 Android 和 iOS 上的所有组和标题,我们需要将我们的CollectionViewSource定义为IEnumerable<IEnumerable>。如果不这样做,我们将在 iOS 上看到缺少组标题,而在 Android 上只能看到第一组的内容。

现在我们有一个可用的应用程序,但在接下来的两个部分中,我们将进行两项改进。在那里,我们将改善应用程序的外观并使用一些本机控件,但在此之前,我们将切换到使用来自远程源的“实时”数据,而不是应用程序自带的数据。

检索远程数据

很少有应用程序仅使用其自带的数据。网络辅助提供的价值是基于提供实时信息。知道火车实际到达的时间比知道计划到达时间更有价值。为了收集这些信息,应用程序必须连接到远程实时数据源。

大多数移动应用程序连接到外部数据源,最常见的方式是通过 HTTP(S)。如果您只开发运行在桌面上的应用程序,您可能可以假设始终有可用的连接。对于移动应用程序,必须考虑设备为偶尔连接

由于不可能假设应用程序始终可用连接或连接速度很快,因此在设计应用程序时必须考虑这一点。这些问题适用于所有移动应用程序,并不是 Uno 平台开发中的独特问题。正确处理偶尔的连接性和数据可用性的方式因应用程序而异。这个问题太大,我们无法在这里完全覆盖,但重要的是提出来。至少,考虑偶尔的连接性意味着需要考虑重试失败的连接请求和管理数据。我们之前在LoadArrivalsDataAsync方法中编写的代码已经以一种粗糙的缓存形式,通过在刷新数据时不丢弃当前信息,直到成功请求并有新数据可用于显示。虽然应用程序中显示的信息可能会很快过时,但相对于不显示任何内容,显示应用程序承认为几分钟前的内容更为合适。

在另一个应用程序中,将数据保存在文件或数据库中可能更合适,以便在远程数据不可用时检索和显示。第五章使您的应用程序准备好面对现实,展示了如何使用 SQLite 数据库来实现这一点。

我们将很快看到应用程序如何处理连接到远程数据的失败,但首先,我们将看看如何连接到远程数据。

连接到远程数据源

本书的 GitHub 存储库位于github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform,其中包括一个WebAPI项目,该项目将为应用程序返回火车到站数据。

您可以选择运行代码并通过本地机器访问,或者您可以连接到unobookrail.azurewebsites.net/上提供的版本。如果连接到托管版本,请注意它基于服务器的本地时间,并且这可能与您所在的地方不同。如果服务器不断表示下一班火车还有很长时间,因为服务器所在地的凌晨,如果您自己运行项目,您将看到更多不同的数据:

  1. 我们将使用System.Net.Http.HttpClient连接到服务器。为了能够做到这一点,我们必须在Android 和 iOS项目中添加对System.Net.Http的包引用:
Install-Package System.Net.Http -Version 4.3.4
  1. 由于 API 返回的数据是 JSON 格式,因此我们还将在所有平台项目中添加对Newtonsoft.Json库的引用,以便我们可以对响应进行反序列化:
Install-Package Newtonsoft.Json -Version 12.0.3
  1. 我们现在准备检索远程数据。所有更改都将在DataService.cs文件中进行。首先添加一个HttpClient的实例。我们将使用这个实例进行所有请求:
using System.Net.Http;
private static readonly HttpClient _http = new HttpClient();
  1. 要连接到服务器,我们需要指定它的位置。由于我们最终将进行多个请求,因此在一个地方定义服务器域是明智的。我们将通过__ANDROID__常量来实现这一点,该常量可用于#if预处理指令。有关更多信息,请参见第二章**,编写您的第一个 Uno 平台应用程序

如果您从 Android 模拟器连接到本地托管的 WebAPI 实例,则需要使用 IP 地址10.0.2.2进行连接。这是模拟器用来指代主机机器的特殊 IP 地址。您可以使用条件编译来指定这一点,就像前面的代码片段中所示。如果您连接到外部服务器,您可以直接设置地址,不需要任何条件代码。

  1. 现在我们可以更新GetArrivalsForStationAsync方法以获取实时数据。用以下内容替换当前的实现:
using Newtonsoft.Json;
public async Task<Arrivals> GetArrivalsForStationAsync(int stationId) 
{
  var url = $"{WebApiDomain}/stations/?stationid=
      {stationId}";
  var rawJson = await _http.GetStringAsync(url);
  return JsonConvert.DeserializeObject<Arrivals>
      (rawJson);
}

如果现在运行应用程序,数据将来自远程位置。您可能会注意到数据检索不再是瞬间完成的,等待时会显示一个忙指示器。我们在应用程序的原始版本中添加了显示进度指示器的代码,但直到现在才看到它显示出来。这突显了在处理需要时间检索的数据时可能出现的另一个潜在问题。在发生某事时让用户了解情况至关重要。我们在这里使用ProgressRing来指示发生了某事。如果没有这个,用户可能会想知道是否有任何事情发生,并变得沮丧或反复按刷新按钮。

到目前为止,我们已经从远程源检索到数据,并在此过程中让用户了解情况,但是当事情出错时,我们需要做更多。所以,我们接下来会看看这一点。

使用 Polly 处理异常并重试请求

处理异常并重试失败的请求的需求几乎适用于所有应用程序。幸运的是,有许多解决方案可以帮助我们处理一些复杂性。Polly (github.com/App-vNext/Polly)是一个流行的开源库,用于处理瞬态错误,我们将在我们的应用程序中使用。让我们来看一下:

  1. 我们将首先向所有平台项目添加对Polly.Extensions.Http包的引用:
Install-Package Polly.Extensions.Http -Version 3.0.0

这扩展了标准的 Polly 功能,并简化了处理与 HTTP 相关的故障。

  1. 我们现在将再次更新GetArrivalsForStationAsync方法,使其使用 Polly 的HandleTransientHttpError。这告诉 Polly 如果 HTTP 响应是服务器错误(HTTP 5xx)或超时错误(HTTP 408),则重试请求。

WaitAndRetryAsync的调用告诉 Polly 最多重试三次。我们还使用policy.ExecuteAsync指定每个请求之间的延迟,并将其传递给我们希望应用策略的操作。

  1. 如果请求因我们策略未覆盖的原因而失败,我们之前创建的代码会导致屏幕顶部显示一条消息,如下面的屏幕截图所示,指示问题所在。其他应用可能需要以不同方式记录或报告此类问题,但通常不适合什么都不做:

图 4.3 - 应用程序显示连接问题的消息

图 4.3 - 应用程序显示连接问题的消息

现在我们有了一个可以可靠地从远程源提供有用数据的应用程序。我们想要做的最后一件事是改善它在不同平台上的外观。

使您的应用程序看起来像属于每个平台

到目前为止,应用程序中的所有内容都使用了 Uno Platform 提供的默认样式。因为 Uno Platform 基于 UWP 和 WinUI,我们的应用程序的样式是基于 Fluent Design 系统的,因为这是 Windows 的默认样式。如果我们希望我们的应用程序看起来这样,这是可以的,但是如果我们希望我们的应用程序使用 Android 或 iOS 的默认样式怎么办?幸运的是,Uno Platform 为我们提供了解决方案。它为我们提供了MaterialCupertino样式的库,我们可以应用到我们的应用程序中。虽然这些库分别是为 Android 和 iOS 设备本地化的,但它们可以在任何地方使用。

现在,我们将使用这些库提供的资源,将 Material Design 样式应用于我们应用程序的 Android 版本,将 Cupertino 样式应用于 iOS 版本。

将 Material 样式应用于应用程序的 Android 版本

让我们开始吧:

  1. 我们将首先向Android 项目添加对Uno.Material软件包的引用。请注意,这是一个预发布软件包,因此如果您通过 UI 搜索,请启用此软件包:
Install-Package Uno.Material -Version 1.0.0-dev.790
  1. 虽然Uno.Material库知道如何为控件设置样式,但它并不包含所有资产和引用以使用它们。为此,在 Android 项目添加Xamarin.AndroidX.Lifecycle.LiveDataXamarin.AndroidX.AppCompat.AppCompatResources软件包:
Install-Package Xamarin.AndroidX.AppCompat.AppCompatResources -Version 1.2.0.5
Install-Package Xamarin.AndroidX.Lifecycle.LiveData -Version 2.3.1
  1. 要在 Android 库中使用样式,我们必须通过在App.xaml中引用它们来将它们添加到应用程序中可用的样式中:
<Application
    x:Class="NetworkAssist.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/
           xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/
             xaml"
    xmlns:android="http://uno.ui/android"
    xmlns:local="using:NetworkAssist"
    xmlns:mc="http://schemas.openxmlformats.org/
             markup-compatibility/2006"
    mc:Ignorable="android">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <XamlControlsResources xmlns=
                "using:Microsoft.UI.Xaml.Controls" />
                <android:MaterialColors xmlns=
                    "using:Uno.Material" />
                <android:MaterialResources xmlns=
                     "using:Uno.Material" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>
  1. 一些控件将自动应用 Material 样式,而其他控件将需要直接应用样式。为了展示这一点,我们将为刷新Button应用特定样式。

ArrivalsControl.xaml中,在文件顶部添加 Android 命名空间别名。我们只在 Android 上运行时才会使用这个。然后,将样式应用于Button元素:

Button control looks on the arrivals control, but it hasn't improved the buttons in CommandBar at the bottom of the shell page. Let's address this now.
  1. 与使用 Windows CommandBar不同,Material Design 系统具有一个单独的控件,更适合在屏幕底部显示与导航相关的按钮。这称为BottomNavigationBar。我们将首先将其添加到MainPage.xaml中,并将现有的CommandBar包装在一个Grid中,该Grid仅在 Windows 上显示:
Click events as before. It's only the control that's displaying them that we're changing.NoteAfter adding the `Xamarin.AndroidX` packages, you may get a compilation error related to a file called `abc_vector_test.xml`. This error is due to compatibility inconsistencies between different preview versions of the packages and Visual Studio. This error can be addressed by opening the **Properties** section of the **Android** project, selecting **Android Options**, and unchecking the **Use incremental Android packaging system (aap2)** option. This may lead to a separate build warning and slightly slower builds, but the code will now compile. Hopefully, future updates that are made to these packages will help us avoid this issue.
  1. 如果现在运行应用程序,您会看到按钮和导航栏是紫色的。这是Uno.Material库中定义的颜色方案的一部分。您可以通过包含提供预定义 Material 颜色的不同值的ResourceDictionary来使用自己的颜色方案。然后,当您添加步骤 2中显示的资源时,您可以引用它。有关如何执行此操作的指南,请参阅platform.uno/docs/articles/features/uno-material.html#getting-started

现在我们已经改善了 Android 上应用程序的外观,让我们为 iOS 做同样的事情。

将 Cupertino 样式应用于应用程序的 iOS 版本

让我们开始吧:

  1. 单独的软件包包含 Cupertino 样式,因此我们必须在 iOS 项目中添加对Uno.Cupertino的引用:
Install-Package Uno.Cupertino -Version 1.0.0-dev.790

与上一节中的 Material 软件包一样,我们需要通过添加以下内容在App.xaml中加载此软件包的资源:

xmlns:ios="http://uno.ui/ios"
mc:Ignorable="android ios">
<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <XamlControlsResources xmlns=
                "using:Microsoft.UI.Xaml.Controls" />
            <android:MaterialColors xmlns=
                "using:Uno.Material" />
            <android:MaterialResources xmlns=
                "using:Uno.Material" />
            <ios:CupertinoColors xmlns=
                "using:Uno.Cupertino" />
            <ios:CupertinoResources xmlns=
               "using:Uno.Cupertino" />
       </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>
  1. 此软件包尚未包含原生选项卡栏控件(UITabBar),但我们可以轻松创建与苹果的人机界面指南相匹配的内容。

MainPage.xaml添加*以下内容,添加到win:Grid元素之后:

Click events that we did previously, but we're using a new converter for ForegroundColor of the Buttons. For this, you'll need to *create a folder* called Converters and *create a file* called CupertinoButtonColorConverter.cs containing the following code:

使用 Windows.UI.Xaml.Data;

public class CupertinoButtonColorConverter:IValueConverter

{

public object Convert(object value, Type targetType,

对象参数,字符串语言)

{

如果(value?.ToString() == parameter?.ToString())

{

return App.Current.Resources[

"CupertinoBlueBrush"];

}

否则

{

return App.Current.Resources[

"CupertinoSecondaryGrayBrush"];

}

}

public object ConvertBack(object value, Type

targetType,对象参数,字符串语言)

=> 抛出未实现的异常();

}


  1. 与 Android 项目一样,Cupertino 样式不会自动应用于应用程序中的按钮。但是,我们可以创建一个隐式样式,将其应用于整个应用程序中的所有Button元素,而不是直接将样式应用于每个Button元素。要做到这一点,修改 App.xaml以添加样式,如下所示:
<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <XamlControlsResources xmlns=
                "using:Microsoft.UI.Xaml.Controls" />
            <android:MaterialColors xmlns=
                "using:Uno.Material" />
            <android:MaterialResources xmlns=
                "using:Uno.Material" />
            <ios:CupertinoColors xmlns=
                "using:Uno.Cupertino"  />
            <ios:CupertinoResources xmlns=
                "using:Uno.Cupertino" />
        </ResourceDictionary.MergedDictionaries>
        <ios:Style TargetType="Button"
BasedOn="{StaticResource 
                CupertinoButtonStyle}" />
    </ResourceDictionary>
</Application.Resources>

隐式样式可以用于任何平台,因此,如果您愿意,您可以在应用程序的 Android 版本中执行类似的操作。

现在我们有一个看起来属于每个平台的应用程序,并且它可以显示我们从外部服务器检索的内容。现在,让我们看看如何使用设备的功能来创建数据并将其发送到远程源。

访问设备功能

我们将向应用程序添加的最后一个功能与我们迄今为止所做的不同。到目前为止,我们已经研究了消耗数据,但现在我们将研究如何创建数据。

公司对应用程序的要求是,它提供了一种让员工在发生事故时捕获信息的方式。所谓的“事故”可以是企业可能需要记录或了解的任何事情。它可能是一些小事,比如顾客在公司财产上绊倒,也可能是一起重大事故。所有这些事件都有一个共同点:捕获详细信息比依靠人们以后记住细节更有益。目标是让员工尽可能快速、简单地捕获图像或一些文本,以增加捕获的信息量。软件将使用事件发生的时间和位置以及记录者的信息来增强捕获的信息。这些信息将被汇总并在一个单独的后端系统中进一步记录。

让我们创建一种简单的方式来满足这些要求,以演示 Uno 平台如何提供一种在不同平台上使用 UWP API 的方式:

  1. 使用相机并获取设备位置,我们需要指示应用程序将需要必要的权限来执行此操作。我们在每个平台上指定权限的方式略有不同。

在 Android 上,打开项目的info.plist并使用Package.appxmanfiest打开它,转到CameraCaptureUI

  1. 我们可以通过在QuickReportControl.xamlGrid中添加以下内容来创建 UI:
Button elements on Android. This is to highlight the importance of each button.
  1. QuickReportControl.xaml.cs中,让我们添加处理用户单击按钮添加照片时发生的情况的代码:
using Windows.Media.Capture;
using Windows.UI.Xaml.Media.Imaging;
Windows.Storage.StorageFile capturedPhoto;
private async void CaptureImageClicked(object sender, RoutedEventArgs e) 
{
    try 
    {
         var captureUI = new CameraCaptureUI and call CaptureFileAsync to ask it to capture a photograph. When that returns successfully (it isn't canceled by the user), we display the image on the screen and store it in a field to send it to the server later.
  1. 现在我们将创建一个方法来封装检索设备位置的逻辑:
using Windows.Devices.Geolocation;
using System.Threading.Tasks;
private async Task<string> GetLocationAsync() 
{
    try 
    {
        var accessStatus = await 
            Geolocator.RequestAccessAsync();
        switch (accessStatus) 
        {
            case GeolocationAccessStatus.Allowed:
                 var geolocator = new Geolocator();
                 var pos = await 
                     geolocator.GetGeopositionAsync();
                 return $"{pos.Coordinate.Latitude},
                    {pos.Coordinate.Longitude},
                        {pos.Coordinate.Altitude}";
            case GeolocationAccessStatus.Denied:
                return "Location access denied";
            case GeolocationAccessStatus.Unspecified:
                return "Location Error";
        }
    }
    catch (Exception ex) 
    {
        // Log the exception as appropriate
    }
    return string.Empty;
}
  1. 最后一步是为“成功”提交有效数据时添加事件处理程序。应用程序会检查这一点,并向用户显示适当的消息。

注意

您可能认为允许用户与应用程序交谈并记录他们的声音会更方便。这是一个明智的建议,也是可以很容易在将来添加的内容。我们在这里没有包括它,因为大多数设备都具有内置功能,可以使用语音转文字来输入详细信息。使用设备的现有功能可能比复制已有功能更快捷、更容易。

现在,我们的应用程序已经完成了这最后一部分功能。您可以在下图中看到它的运行效果:

图 4.4-快速报告屏幕在 iPhone 上运行(左)并显示所选图像,以及 Android 设备(右)显示输入的一些口述文本

图 4.4-快速报告屏幕在 iPhone 上运行(左)并显示所选图像,以及 Android 设备(右)显示输入的一些口述文本

总结

在本章中,我们构建了一个可以在 iOS 和 Android 设备上运行的应用程序。这使您了解了创建“移动”应用程序的含义,处理远程数据,将本机平台主题应用于应用程序,并使用本机设备功能。

在下一章中,我们将构建另一个移动应用程序。这将与迄今为止制作的应用程序不同,因为它旨在供客户使用,而不是公司员工使用。除其他事项外,我们将利用这个应用程序来研究可访问性、本地化和使用 SQLite 数据库。

第五章:使您的应用程序准备好面向现实世界

在上一章中,我们介绍了使用 Uno Platform 编写面向 UnoBookRail 员工的第一个移动应用程序。在本章中,我们也将编写一个移动应用程序;但是,我们将专注于使其准备好供客户使用。在本章中,您将编写一个在设备上持久保存用户偏好和更大数据集的应用程序。此外,您还将学习如何通过自定义应用程序图标使您的应用程序对用户更具吸引力,以及如何编写可以供使用辅助技术的人使用的应用程序。

为了做到这一点,我们将在本章中涵盖以下主题:

  • 介绍应用程序

  • 使用ApplicationData API 和 SQLite 在本地持久化数据

  • 使您的应用程序准备好供客户使用

  • 本地化您的应用程序

  • 使用自定义应用程序图标和启动画面

  • 使您的应用程序适用于所有用户

在本章结束时,您将创建一个在 iOS 和 Android 上运行的移动应用程序,该应用程序已准备好供客户使用,并且已进行本地化和可访问。

技术要求

本章假设您已经设置好了开发环境,包括安装了项目模板,就像在第一章中介绍的那样,介绍 Uno Platform。本章的源代码位于github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/Chapter05

本章的代码使用了来自github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/SharedLibrary的库。

查看以下视频以查看代码的实际操作:bit.ly/3AywuqQ

介绍应用程序

在本章中,我们将构建 UnoBookRail DigitalTicket 应用程序,这是一个面向想要使用 UnoBookRail 从 A 到 B 的 UnoBookRail 客户的应用程序。虽然这个应用程序的真实版本可能有很多功能,但在本章中,我们只会开发以下功能:

  • 预订 UnoBookRail 网络两个站点之间的行程车票

  • 查看所有预订的车票以及车票的 QR 码

  • 本地化应用程序,并允许用户选择用于应用程序的语言

作为其中的一部分,我们还将确保我们的应用程序是可访问的,并允许不同能力水平的更多人使用我们的应用程序。现在让我们开始创建应用程序并添加第一部分内容。

创建应用程序

首先,我们需要为我们的应用程序设置解决方案:

  1. 首先,使用Multi-Platform App (Uno Platform) 模板创建一个新的应用程序。

  2. 将项目命名为DigitalTicket。当然,您也可以使用不同的名称;但是,在本章中,我们将假设该应用程序被命名为 DigitalTicket,并使用相应的命名空间。

  3. 删除除AndroidiOSUWP之外的所有平台头。请注意,即使在网络上提供此功能可能会有好处,我们也会删除 WASM 头。虽然 WASM 在移动设备上运行得相当不错,但并不理想,为了简单起见,我们将继续不使用应用程序的 WASM 版本。

  4. 将 UnoBookRail 共享库添加到解决方案中,因为我们稍后将需要其功能。为此,请右键单击解决方案文件,选择UnoBookRail.Common.csproj文件,然后单击打开

  5. 在每个头项目中引用共享库项目。为此,请右键单击头项目,选择添加 | 引用… | 项目,选中UnoBookRail.Common,然后单击确定。由于我们需要在每个头中引用该库,请为每个头重复此过程,即 Android、iOS 和 UWP。

由于我们的应用程序还将遵循Microsoft.Toolkit.MVVM包,您还需要添加对其的引用:

  1. 在解决方案视图中右键单击解决方案节点,然后选择管理解决方案的 NuGet 包…

  2. 搜索Microsoft.Toolkit.MVVM并选择NuGet包。

  3. 在项目列表中选择 Android、iOS 和 UWP 头部,然后点击安装

与上一章类似,我们还需要修改我们的应用程序以留出相机刘海的空间,以避免应用程序的内容被遮挡:

  1. 为此,在MainPage.xaml文件中添加以下命名空间:xmlns:toolkit="using:Uno.UI.Toolkit"

  2. 之后,在我们的MainPage.xaml文件内的网格中添加toolkit:VisibleBoundsPadding.PaddingMask="All"

创建主导航和预订流程

由于我们的应用程序将包含不同的功能,我们将把应用程序的功能拆分成不同的页面,我们将导航到这些页面。在MainPage内,我们将有我们的导航和相关代码:

  1. 首先,通过右键单击Views创建一个 views 文件夹。

  2. 现在,在JourneyBookingPage.xamlOwnedTicketsPage.xamlSettingsPage.xaml内添加以下三个页面。

  3. 由于我们以后会需要它,创建一个Utils文件夹,并添加一个LocalizedResources类,其中包含以下代码:

public static class LocalizedResources
{
    public static string GetString(string key) {
        return key;
    }
}

目前,这个类只会返回字符串,这样我们就可以引用该类,而不必以后更新代码。不过,在本章的后面,我们将更新实现以返回提供的键的本地化版本。

  1. 之后,在共享项目中创建一个ViewModels文件夹,并创建一个NavigationViewModel类。

  2. 将以下内容添加到您的NavigationViewModel类:

using DigitalTicket.Views;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Controls;
using System;
namespace DigitalTicket.ViewModels
{
    public class NavigationViewModel : 
        ObservableObject
    {
        private Type pageType;
        public Type PageType
        {
            get
            {
                return pageType;
            }
            set
            {
                SetProperty(ref pageType, value);
            }
        }
        public void NavigationView_SelectionChanged(
          NavigationView navigationView, 
            NavigationViewSelectionChangedEventArgs
              args)
        {
            if (args.IsSettingsSelected)
            {
                PageType = typeof(SettingsPage);
            }
            else
            {
                switch ((args.SelectedItem as 
                   NavigationViewItem).Tag.ToString())
                {
                    case "JourneyPlanner":
                        PageType = 
                          typeof(JourneyBookingPage);
                        break;
                    case "OwnedTickets":
                        PageType = 
                          typeof(OwnedTicketsPage);
                        break;
                }
            }
        }
    }
}

此代码将公开MainPage应该导航到的页面类型,并提供选择更改侦听器以在应用程序导航更改时更新。为了确定正确的页面类型,我们将使用所选项的Tag属性。

  1. 现在,用以下内容替换MainPage的内容:
    ...
    xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
    <Grid toolkit:VisibleBoundsPadding.PaddingMask=
        "All">
        <muxc:NavigationView x:Name="AppNavigation"
            PaneDisplayMode="LeftMinimal"             
            IsBackButtonVisible="Collapsed" 
            Background="{ThemeResource 
                ApplicationPageBackgroundThemeBrush}"
            SelectionChanged="{x:Bind 
                navigationVM.NavigationView_
                     SelectionChanged, Mode=OneTime}">
            <muxc:NavigationView.MenuItems>
                <muxc:NavigationViewItem 
                    x:Name="JourneyBookingItem" 
                    Content="Journey Booking"
                    Tag="JourneyPlanner"/>
                <muxc:NavigationViewItem 
                    Content="Owned tickets"
                    Tag="OwnedTickets"/>
                <muxc:NavigationViewItem Content="All 
                    day tickets - soon" 
                    Tag="AllDayTickets" 
                    IsEnabled="False"/>
                <muxc:NavigationViewItem 
                    Content="Network plan - soon" 
                    IsEnabled="False"/>
                <muxc:NavigationViewItem 
                    Content="Line overview - soon"
                    IsEnabled="False"/>
            </muxc:NavigationView.MenuItems>
            <Frame x:Name="ContentFrame" 
                Padding="0,40,0,0"/>
             </muxc:NavigationView>
    </Grid>

这是我们应用程序的主要导航。我们使用NavigationView控件来实现这一点,它允许我们轻松地拥有一个可以使用汉堡按钮打开的侧边窗格。在其中,我们提供不同的导航选项,并将Tag属性设置为NavigationViewModel使用。由于在本章中我们只允许预订行程和拥有的票证列表,我们暂时禁用了其他选项。

  1. 用以下内容替换您的MainPage类:
using DigitalTicket.ViewModels;
using DigitalTicket.Views;
using System;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
namespace DigitalTicket
{
    public sealed partial class MainPage : Page
    {
        public NavigationViewModel navigationVM = new 
            NavigationViewModel();
        public MainPage()
        {
            InitializeComponent();
            if (navigationVM.PageType is null)
            {
                AppNavigation.SelectedItem = 
                    JourneyBookingItem;
                navigationVM.PageType = 
                    typeof(JourneyBookingPage);
                navigationVM.PageTypeChanged += 
                    NavigationVM_PageTypeChanged;
            }
        }
        protected override void OnNavigatedTo(
            NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
            if (e.Parameter is Type navigateToType)
            {
                if (navigateToType == 
                    typeof(SettingsPage))
                {
                    AppNavigation.SelectedItem = 
                        AppNavigation.SettingsItem;
                }
                navigationVM.PageType = 
                    navigateToType;
                ContentFrame.Navigate(navigateToType);
            }
        }
        private void NavigationVM_PageTypeChanged(
           object sender, EventArgs e)
        {
            ContentFrame.Navigate(
                navigationVM.PageType);
        }
    }
}

通过这样,MainPage在创建时将创建必要的视图模型,并根据此更新显示的内容。MainPage还监听OnNavigatedTo事件,以根据传递给它的参数更新显示的项目。最后,我们还监听NavigationViewModels属性更改事件。

请注意,我们重写了OnNavigatedTo函数,以便允许导航到MainPage,以及在MainPage内导航到特定页面。虽然我们现在不需要这个,但以后我们会用到。让我们继续填充行程预订页面的内容:

  1. ViewModels文件夹内创建JourneyBookingOption类。

  2. 将以下代码添加到JourneyBookingOption类:

using DigitalTicket.Utils;
using UnoBookRail.Common.Tickets;
namespace DigitalTicket.ViewModels
{
    public class JourneyBookingOption
    {
        public readonly string Title;
        public readonly string Price;
        public readonly PricingOption Option;
        public JourneyBookingOption(PricingOption 
            option)
        {
            Title = LocalizedResources.GetString(
              option.OptionType.ToString() + "Label");
            Price = option.Price;
            Option = option;
        }
    }
}

由于这是一个用于显示选项的数据对象,它只包含属性。由于标题将显示在应用程序内并且需要本地化,我们使用LocalizedResources.GetString函数来确定正确的值。

  1. 现在在ViewModels文件夹中创建JourneyBookingViewModel类,并添加 GitHub 上看到的代码(github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/blob/main/Chapter05/DigitalTicket.Shared/ViewModels/JourneyBookingViewModel.cs)。请注意,有几行被注释掉,那是因为我们稍后会需要这些行;但是,现在我们还没有添加必要的代码。

  2. 更新JourneyBookingPage.xaml.csJourneyBookingPage.xaml,使它们与 GitHub 上看到的一样。

  3. 将以下条目复制到Strings/en文件夹中的Strings.resw文件中。请注意,您不必逐字复制Comments列,因为它只是为其他两列提供指导和上下文:

表 5.1

您可能会注意到,一些控件设置了x:Uid属性,这就是为什么需要Strings.resw文件中的条目。我们将在本地化您的应用程序部分介绍这些工作原理;现在,我们只会添加代码和相应的条目到我们的资源文件中。现在,如果您启动应用程序,您应该会看到图 5.1中显示的内容:

图 5.1 - Android 上的旅程预订页面

图 5.1 - Android 上的旅程预订页面

现在您的用户可以配置他们的旅程,选择车票并预订,尽管车票名称不够理想。我们将在本地化您的应用程序部分中解决这个问题。为简单起见,我们将不处理实际付款,并假设付款信息与用户帐户关联。

在本节中,我们添加了应用程序的初始代码和导航。我们还添加了旅程预订页面,尽管目前实际上还没有预订车票,但我们稍后会更改。在下一节中,我们将介绍如何使用两种不同的方法在用户设备上本地持久化数据,即ApplicationData API 和 SQLite。

使用 ApplicationData API 和 SQLite 在本地持久化数据

虽然在许多情况下,数据可以从互联网上获取,就像我们在第四章中看到的那样,移动化您的应用程序,通常需要在用户设备上持久化数据。这可能是需要在没有互联网连接时可用的数据,或者是设备特定的数据,例如设置。我们将首先使用ApplicationData API 持久化小块数据。

使用 ApplicationData API 存储数据

由于我们将本地化我们的应用程序,我们还希望用户能够选择应用程序的语言。为此,首先在我们的共享项目中创建一个Models文件夹,并添加一个SettingsStore类。现在,将以下代码添加到SettingsStore类中:

using Windows.Storage;
public static class SettingsStore
{
    private const string AppLanguageKey = 
        "Settings.AppLanguage";
    public static void StoreAppLanguageOption(string 
         appTheme)
    {
        ApplicationData.Current.LocalSettings.Values[
            AppLanguageKey] = appTheme.ToString();
    }
    public static string GetAppLanguageOption()
    {
        if (ApplicationData.Current.LocalSettings.Values.
            Keys.Contains(AppLanguageKey))
        {
            return ApplicationData.Current.LocalSettings.
                Values[AppLanguageKey].ToString();
        }
        return "SystemDefault";
    }
}

访问应用程序的默认本地应用程序存储,我们使用ApplicationData.Current.LocalSettings对象。ApplicationData API 还允许您访问存储数据的不同方式,例如,您可以使用它来访问应用程序的本地文件夹,使用ApplicationData.Current.LocalFolder。在我们的情况下,我们将使用ApplicationData.Current.LocalSettings来持久化数据。LocalSettings对象是一个ApplicationDataContainer对象,您可以像使用字典一样使用它。请注意,LocalSettings对象仅支持字符串和数字等简单数据类型。现在我们已经添加了一种存储要显示应用程序语言的方法,我们需要让用户更改语言:

  1. 首先,在我们的ViewModels文件夹中创建一个名为SettingsViewModel的新类。您可以在此处找到此类的代码:github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/blob/main/Chapter05/DigitalTicket.Shared/ViewModels/SettingsViewModel.cs

  2. 现在,我们更新我们的设置页面,以包括更改应用程序语言的 UI。为此,请将SettingsPage.xaml中的Grid元素替换为以下内容:

<StackPanel Padding="10,0,10,10">
    <ComboBox x:Name="LanguagesComboBox"
        Header="Choose the app's language"
        SelectedIndex="{x:Bind 
            settingsVM.SelectedLanguageIndex,
                Mode=TwoWay}"/>
</StackPanel>
  1. 除此之外,我们还需要更新SettingsPage.xaml.cs。请注意,我们将在代码后台设置ComboBoxItemsSource,以确保在ComboBox创建并准备就绪后设置ItemsSource,以便ComboBox能够正确更新。为此,请添加以下代码:
using DigitalTicket.ViewModels;
...
private SettingsViewModel settingsVM = new SettingsViewModel();
public SettingsPage()
{
    InitializeComponent();
    LanguagesComboBox.ItemsSource = 
        settingsVM.LanguageOptions;
}
  1. 最后,为了确保在应用程序启动时将尊重所选的语言,将以下代码添加到App.xaml.csOnLaunched函数中,并为DigitalTicket.ModelsDigitalTicket.ViewModels添加导入:
ApplicationLanguages.PrimaryLanguageOverride = 
SettingsViewModel.GetPrimaryLanguageOverrideFromLanguage(
SettingsStore.GetAppLanguageOption());

现在我们已经添加了语言选项,让我们试一下。如果您现在启动应用程序并使用左侧的导航转到设置页面,您应该会看到类似于图 5.2左侧的内容。现在,如果您选择SettingsViewModel重新加载MainPage和所有其他页面,并设置ApplicationLanguages.PrimaryLanguageOverride属性,我们将在本地化您的应用程序部分更多地讨论此属性,并且还将更新应用程序,以便所有当前可见的文本也根据所选择的语言进行更新:

图 5.2–左:设置页面;右:切换语言为德语后的导航

图 5.2–左:设置页面;右:切换语言为德语后的导航

使用 SQLite 存储数据

虽然ApplicationData API 适用于存储小数据块,但是如果要持久化更大的数据集,则ApplicationData API 并不理想,因为使用ApplicationData.Current.LocalSettings对象存储的条目存在空间限制。换句话说,对象键的长度只能为 255 个字符,UWP 上的条目大小只能为 8 千字节。当然,这并不意味着您不能在应用程序中存储更大或更复杂的数据集。这就是sqlite-net-pcl库的作用,因为该库适用于我们应用程序支持的每个平台。sqlite-net-pcl包括 SQLite 的跨平台实现,并允许我们轻松地将对象序列化为 SQLite 数据库。

让我们首先向我们的应用程序添加对sqlite-net-pcl的引用。为此,请在解决方案视图中右键单击解决方案,单击sqlite-net-pcl。由于在编写本书时,最新的稳定版本是1.7.335,请选择该版本并在项目列表中选择 Android、iOS 和 UWP 头。然后,单击安装。现在,我们需要添加代码来创建、加载和写入 SQLite 数据库:

  1. 首先,我们需要添加一个类,我们希望使用 SQLite 持久化其对象。为此,在ViewModels文件夹中添加一个名为OwnedTicket的新类。您可以在 GitHub 上找到此类的源代码:github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/blob/main/Chapter05/DigitalTicket.Shared/ViewModels/OwnedTicket.cs

有两件重要的事情需要知道:

由于每个 SQLite 表都需要一个主键,我们添加了带有 PrimaryKey 和 AutoIncrement 属性的DBId属性。使用这些属性,我们让sqlite-net-pcl为我们管理主键,而无需自己处理。

将对象传递给sqlite-net-pcl以将它们持久化到 SQLite 数据库中,只有属性将被持久化。由于我们不想持久化ShowQRCodeCommand(实际上也不能),这只是一个字段,而不是属性。

  1. 现在在Models文件夹中创建OwnedTicketsRepository类,并向其中添加以下代码:
using DigitalTicket.ViewModel;
using SQLite;
using System;
using System.IO;
using System.Threading.Tasks;
using Windows.Storage;
namespace DigitalTicket.Models
{
    public class OwnedTicketsRepository
    {
        const string DBFileName = "ownedTickets.db";
        private static SQLiteAsyncConnection database;
        public async static Task InitializeDatabase()
        {
            if(database != null)
            {
                return;
            }
            await ApplicationData.Current.LocalFolder.
                CreateFileAsync(DBFileName, 
                CreationCollisionOption.OpenIfExists);
            string dbPath = Path.Combine(
                ApplicationData.Current.LocalFolder
                    .Path, DBFileName);
            database = 
                new SQLiteAsyncConnection(dbPath);
            database.CreateTableAsync<
                OwnedTicket>().Wait();
        }
        public static Task<int> SaveTicketAsync(
            OwnedTicket ticket)
        {
            if (ticket.DBId != 0)
            {
                // Update an existing ticket.
                return database.UpdateAsync(ticket);
            }
            else
            {
                // Save a new ticket.
                return database.InsertAsync(ticket);
            }
        }
    }
}

InitializeDatabase函数处理创建我们的 SQLite 数据库文件和创建表(如果不存在),但也会在文件已经存在时加载现有数据库。在SaveTicketsAsync函数中,我们更新并保存传递的车票到数据库,或者如果数据库中已存在该车票,则更新该车票。

  1. 更新App.xaml.cs以在OnLaunched函数的开头包含以下代码,并将OnLaunched函数更改为异步:
await OwnedTicketsRepository.InitializeDatabase();

这将在应用程序启动时初始化 SQLite 连接,因为按需创建连接并不理想,特别是在加载所拥有的车票页面时。

  1. 现在更新JourneyBookingViewModel以将车票保存到OwnedTicketsRepository。为此,请删除当前创建BookJourney并取消注释文件顶部的using语句以及JourneyBookingViewModel构造函数中的代码。

现在让我们谈谈我们刚刚做的步骤。首先,我们创建了我们的OwnedTicket对象,我们将在下一节中将其写入 SQLite 并从 SQLite 中加载。

然后我们添加了OwnedTicketsRepository,我们用它来与我们的 SQLite 数据库交互。在可以向 SQLite 数据库发出任何请求之前,我们首先需要初始化它,为此我们需要一个文件来将 SQLite 数据库写入其中。使用以下代码,我们确保我们要将数据库写入的文件存在:

await ApplicationData.Current.LocalFolder.CreateFileAsync(DBFileName, CreationCollisionOption.OpenIfExists);

之后,我们为我们的数据库创建了一个SQLiteAsyncConnection对象。SQLiteAsyncConnection对象将处理与 SQLite 的所有通信,包括创建表和保存和加载数据。由于我们还需要一个表来写入我们的数据,我们使用SQLiteAsyncConnection为我们的OwnedTickets对象创建一个表,如果该表在我们的 SQLite 数据库中不存在。为了确保在对数据库进行任何请求之前执行这些步骤,我们在我们的应用程序构造函数中调用OwnedTicketsRepository.InitializeDatabase()

最后一步是更新我们的JourneyBookingViewModel类,以便将数据持久化到 SQLite 数据库中。虽然我们只向数据库中添加新项目,但我们仍然需要注意是否正在更新现有条目或添加新条目,这就是为什么SavedTicketAsync函数确保我们只在没有 ID 存在时才创建项目。

从 SQLite 加载数据

现在我们已经介绍了如何持久化数据,当然,我们也需要加载数据;否则,我们就不需要首先持久化数据。让我们通过添加用户预订的所有车票的概述来改变这一点。由于 UnoBookRail 的客户需要在登上火车或检查车票时出示他们的车票,我们还希望能够为每张车票显示 QR 码。由于我们将使用ZXing.Net.Mobile来实现这一点,请立即将该NuGet包添加到您的解决方案中,即 Android、iOS 和 UWP 头。请注意,在撰写本文时,版本2.4.1是最新的稳定版本,我们将在本章中使用该版本。

在我们想要显示所有车票之前,我们首先需要从 SQLite 数据库中加载它们。为此,向我们的OwnedTicketsRepository类添加以下方法:

using System.Collections.Generic;
...
static Task<List<OwnedTicket>> LoadTicketsAsync()
{
    //Get all tickets.
    return database.Table<OwnedTicket>().ToListAsync();
}

由于sqlite-net-pcl,这就是我们需要做的一切。该库为我们处理了其余部分,包括读取表并将行转换为OwnedTicket对象。

现在我们也可以加载票了,我们可以更新本章开头创建的OwnedTicketsPage类,以显示用户预订的所有票。在我们的应用程序中,这意味着我们只会显示在此设备上预订的票。在真实的应用程序中,我们还会从远程服务器访问票务并将其下载到设备上;但是,由于这超出了本章的范围,我们不会这样做:

  1. 在更新拥有的票页面之前,首先在ViewModels文件夹中添加一个OwnedTicketsViewModel类。该类的源代码在此处可用:github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/blob/main/Chapter05/DigitalTicket.Shared/ViewModels/OwnedTicketsViewModel.cs

  2. 现在,更新OwnedTicketsPage.xamlOwnedTicketsPage.xaml.cs。您可以在 GitHub 上找到这两个文件的源代码:github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/Chapter05/DigitalTicket.Shared/Views

现在,如果启动应用程序并导航到拥有的票页面,您应该会看到一个空页面。如果您已经预订了一张票,您应该会看到图 5.3左侧的内容。如果您点击票下方的小、宽、灰色框,您应该会看到图 5.3右侧的内容:

图 5.3 - 左:拥有单张票的票务列表;右:拥有的票和已预订票的 QR 码

图 5.3 - 左:拥有单张票的票务列表;右:拥有的票和已预订票的 QR 码

当然,这还不是最终的 UI;用户应该看到指示他们尚未预订票的文本,而不是空白屏幕。不过,目前预计文本缺失,按钮也没有标签,因为它们使用的是x:Uid,而不是设置了TextContent属性。在下一节中,我们将看看x:Uid是什么,并更新我们的应用程序,以便所有标签都能正确显示。

使您的应用程序准备好迎接客户

在本节中,我们将更新我们的应用程序,以便为我们的客户做好准备,包括本地化支持,使应用程序更易于客户使用。添加本地化支持后,我们将更新应用程序的图标和启动画面,以便用户更容易识别。

本地化您的应用程序

如果您正在开发一个面向客户的应用程序,能够以客户的母语提供翻译非常重要,特别是针对来自不同国家的客户的应用程序。在上一节中,我们已经添加了x:Uid属性并向Strings.resw文件添加了条目;但是,还有其他本地化资源的方法,我们将在后面介绍。我们将从x:Uid开始本地化文本。

使用 x:Uid 本地化您的 UI

使用x:Uid和资源文件(.resw 文件)是本地化应用程序的最简单方法,特别是因为添加新的翻译(例如,为新语言)非常容易。但是如何使用x:Uid和.resw 文件本地化您的应用程序呢?

x:Uid属性可以添加到你的 XAML 代码的任何元素上。除了在你想要提供翻译的控件上设置x:Uid属性之外,你还需要添加这些翻译。这就是.resw文件发挥作用的地方。简而言之,resw文件是包含必要条目的 XML 文档。然而,最容易想到的方法是将它们视为一张包含三个属性的条目列表,通常表示为表格。这些属性(或列)如下:

  • Name:你可以用来查找资源的名称。这个路径也将用于确定要设置哪个控件上的哪个属性。

  • Value:设置的文本或查找此资源时返回的文本。

  • Comment:你可以使用这一列提供解释该行的注释。当将应用程序翻译成新语言时,这是特别有用的,因为你可以使用注释找出最佳翻译是什么。查看图 5.4中的Comment列,了解它们可能如何使用。

在 Visual Studio 中打开.resw文件时,显示将如图 5.4所示:

图 5.4 - 在 Visual Studio 中查看.resw 文件

图 5.4 - 在 Visual Studio 中查看.resw 文件

当在 XAML 代码中使用x:Uid属性与.resw文件结合使用时,你需要注意如何编写资源的名称条目。名称条目需要以控件的x:Uid值开头,后跟一个点(.)和应该设置的属性的名称。因此,在前面的示例中,如果我们想要本地化TextBlock元素的文本,我们将添加一个名称值为ButtonTextBlock.Text的条目,因为我们想设置TextBlock元素的Text属性。

“但是本地化是如何运作的呢?”你可能会问。毕竟,我们只添加了一个条目;它怎么知道选择哪种语言呢?这就是为什么你放置.resw文件的文件夹很重要。在你的项目中,你需要有一个Strings文件夹。在该文件夹中,对于你想要将应用本地化的每种语言,你需要创建一个文件夹,比如en-GB,而对于德语(德国),你需要创建一个名为de-DE的文件夹。在你为每种想要支持的语言创建的文件夹中,你需要放置.resw文件,以便本地化能够正常工作。请注意,如果某种语言不可用,资源查找将尝试找到下一个最佳匹配。你可以在这里了解更多关于这个过程的信息,因为你的 Uno 平台应用在每个平台上的行为都是相同的:docs.microsoft.com/windows/uwp/app-resources/how-rms-matches-lang-tags

重要提示

要小心命名这些文件夹。资源查找将根据文件夹的名称进行。如果文件夹的名称有拼写错误或不符合 IETF BCP 47 标准,资源查找可能会失败,你的用户将看到缺少标签和文本,或者资源查找将退回到已翻译文本的语言混合。

我们已经有一个用于英文文本资源的文件夹;然而,我们也想支持德语翻译。为此,在Strings文件夹内创建一个名为de-DE的新文件夹。现在,添加一个名为Resources.resw的新.resw文件,并添加以下条目:

表 5.2

如果现在启动应用并将应用的语言切换为德语,你会看到预订页面现在已本地化。如果你的设备语言已设置为德语,而不是以英语显示页面,现在应该显示为德语,即使你现在不切换到德语选项。

从代码后台访问资源

使用x:Uid并不是本地化应用程序的唯一方法;我们现在将看到如何可以从代码后台访问资源。例如,当您想要本地化集合中的项目时,例如我们应用程序中拥有的票证列表。要访问字符串资源,您可以使用ResourceLoader类。我们在本章开头添加了LocalizedResources类;然而,直到现在,它还没有访问任何资源。现在通过添加以下导入并替换GetString函数来更新LocalizedResources

using Windows.ApplicationModel.Resources;
...
private static ResourceLoader cachedResourceLoader;
public static string GetString(string name)
{
    if (cachedResourceLoader == null)
    {
        cachedResourceLoader = 
            ResourceLoader.GetForViewIndependentUse();
    }
    if (cachedResourceLoader != null)
    {
        return cachedResourceLoader.GetString(name);
    }
    return null;
}

由于我们将经常使用加载的资源,我们正在缓存该值,以避免调用GetForViewIndependentUse,因为这是昂贵的。

现在我们已经介绍了x:Uid的工作原理以及如何从代码后台访问本地化资源,让我们更新应用程序的其余部分以进行本地化。首先,通过向我们的.resw文件添加必要的条目来开始。以下是您需要为MainPage.xaml文件及其英语和德语条目的条目表:

Table 5.3

现在,请将MainPage.xaml文件中的NavigationViewItems属性替换为以下内容:

<muxc:NavigationViewItem x:Name="JourneyBookingItem" x:Uid="JourneyBookingItem" Tag="JourneyPlanner"/>
<muxc:NavigationViewItem x:Uid="OwnedTicketsItem" Tag="OwnedTickets"/>
<muxc:NavigationViewItem x:Uid="AllDayTicketsItem" Tag="AllDayTickets" IsEnabled="False"/>
<muxc:NavigationViewItem x:Uid="NetworkPlanItem" IsEnabled="False"/>
<muxc:NavigationViewItem x:Uid="LineOverViewItemItem" IsEnabled="False"/>

要将应用程序的其余部分本地化,请查看 GitHub 上的源代码。您还可以在那里找到英语和德语的更新的Resources.resw文件。请注意,我们选择不本地化车站名称,因为本地化街道和地名可能会让客户感到困惑。

重要提示

您还可以本地化其他资源,如图像或音频文件。为此,您需要将它们放在正确命名的文件夹中。例如,如果您想本地化名为Recipe.png的图像,您需要将该图像的本地化版本放在Assets/[语言标识符]文件夹中,其中语言标识符是图像所属语言的 IETF BCP 47 标识符。您可以在这里了解有关自定义和本地化资源的更多信息:docs.microsoft.com/windows/uwp/app-resources/images-tailored-for-scale-theme-contrast

在本节中,我们介绍了如何使用x:Uid和资源文件本地化您的应用程序。随着您的应用程序变得更大并提供更多语言,使用多语言应用程序工具包可能会有所帮助。它使您更容易地检查哪些语言键未被翻译,并集成到 Visual Studio 中。您可以在这里了解更多信息:developer.microsoft.com/en-us/windows/downloads/multilingual-app-toolkit/

自定义应用程序的外观

在将应用程序发布到商店时,您希望您的应用程序能够被用户识别并传达您的品牌。然而,到目前为止,我们开发的所有应用程序都使用了标准的 Uno 平台应用程序图标。幸运的是,Uno 平台允许我们更改应用程序的图标,并允许我们为应用程序设置启动图像。

更新应用程序的图标

让您的应用程序被用户识别的最重要的事情之一就是为您的应用程序添加图标。更新应用程序的图标很容易。您可以在这里找到我们将使用的图像:github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/blob/main/Chapter05/DigitalTicket.Shared/Assets/AppIcon.png

更新 Android 应用程序的图标

要更新 Android 应用程序的应用程序图标,您只需将 Android 项目的 drawable 文件夹中的Icon.png文件替换为所需的应用程序徽标。请注意,您还需要在项目属性中选择正确的图像。为此,请双击Appicon,您将在Properties节点内的AndroidManifest.xml文件中选择android:icon条目。

更新 iOS 应用程序的图标

更新我们的 iOS 应用程序图标需要更多的工作。对于 iOS 应用程序,您需要根据应用程序安装的设备不同的尺寸来设置应用程序图标。要查看尺寸列表并更新 iOS 应用程序的应用图标,只需展开 iOS 项目的Assets Catalog节点,然后双击其中的Media条目。在AppIcons选项卡中,您可以选择不同设备和类别的图像和尺寸。并不需要为每个尺寸提供图像;但是,您至少应该为每个类别提供一个图标。

更新 UWP 应用程序的图标

更新 UWP 头部的应用程序图标的最简单方法是使用Package.appxmanifest文件。为此,请双击Package.appxmanifest,然后在Visual Assets选项卡中选择App icon选项。要更新应用程序的图标,请选择源图像,选择目标文件夹,然后单击Generate。这将生成不同尺寸的应用程序图标,并因此将您的应用程序图标更新为指定的图像。

更新其他项目的图标

虽然我们的应用程序将不会在其他平台上可用,并且我们已经删除了相应平台的头部,但您可能希望在其他项目中更新其他平台的图标:

  • Assets/xcassets/AppIcon.appiconset文件夹。如果重命名图像,请确保还更新Contents.json文件。

  • 基于 Skia 的项目:在 Visual Studio 中右键单击项目并选择属性。在应用程序选项卡中,您可以使用资源部分中的浏览按钮选择一个新图标。

  • favicon.ico在项目的Assets文件夹中。

自定义您的应用程序启动画面

更新您的应用程序图标并不是使您的应用程序更具识别性的唯一方法。除了应用程序图标之外,您还可以自定义应用程序的启动画面。请注意,目前只有 Android、iOS、UWP 和 WASM 应用程序支持设置启动画面。与图标一样,您可以在 GitHub 上找到此类图像资源。

更新 Android 启动画面

要向 Android 应用程序添加启动画面,您首先需要添加您的启动画面图像。在我们的情况下,我们将其命名为SplashScreen.png。之后,将以下条目添加到Resource/values/Styles.xml文件中:

<item name="android:windowBackground">@drawable/splash</item>

然后,您需要在Resources/drawable文件夹中添加splash.xml文件,并添加以下代码:

<?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android=
        "http://schemas.android.com/apk/res/android">
    <item>
        <!-- background color -->
        <color android:color="#008cff"/>
    </item>
    <item>
    <!-- splash image -->
        <bitmap android:src="img/splashscreen"
                android:tileMode="disabled"
                android:gravity="center" />
    </item>
</layer-list>

更新 iOS 应用程序的启动画面

与任何 iOS 应用程序一样,启动画面需要是一个故事板。Uno Platform 使得很容易显示一个单一图像作为启动画面。只需这些简单的步骤:

  1. 在解决方案资源管理器中,选择 iOS 项目并按下显示所有文件按钮。

  2. 现在您将能够看到一个名为LaunchScreeen.storyboard的文件。右键单击此文件并选择包含在项目中。这将在启动应用程序时自动使用。

如果运行应用程序,您将看到 Uno Platform 标志在启动应用程序时显示。您可以通过替换图像来轻松更改此内容。

  1. SplashScreen@2x.pngSplashScreen@3x.png中。这些是故事板使用的文件。用您想要的图像替换它们。

  2. 要更改用于背景的颜色,您可以在 Xcode Interface Builder 中打开故事板并更改颜色。或者,您可以在 XML 编辑器中打开故事板文件,并更改颜色的redgreenblue属性,使用backgroundColor键。

可以使用任何您希望的内容作为启动屏幕的故事板文件。要做到这一点,您需要使用 Xcode Interface Builder。在版本16.9之前,Visual Studio 包括一个 iOS 故事板编辑器,但现在不再可用。要现在编辑故事板,您需要在 Visual Studio for Mac 中打开项目,右键单击文件,然后选择打开方式 | Xcode Interface Builder

更新 UWP 应用程序的启动画面

与更新 UWP 应用程序的应用程序图标类似,使用Package.appxmanifest文件和#008CFF。现在,单击生成以生成 UWP 应用程序的启动画面图像。

更新 WASM 应用程序的启动画面

要更新 WASM 头的启动画面,请将新的启动画面图像添加到 WASM 项目的AppManifest.js文件中的WasmScripts文件夹中,以引用该图像,并在必要时更新启动画面颜色。

如果您已成功按照我们的应用程序步骤进行操作,您应该能够在 Android 应用程序列表中看到应用程序,就像图 5.5左侧所示。一旦启动应用程序,您的应用程序在显示旅程预订页面之前应该看起来像图 5.5右侧所示。请注意,此处提供的图标和启动画面仅为示例。在真实的应用程序中,您应该确保您的应用程序图标即使很小也看起来不错:

图 5.5 – 左:应用程序列表中的 DigitalTicket;右:DigitalTicket 的启动画面

图 5.5 – 左:应用程序列表中的 DigitalTicket;右:DigitalTicket 的启动画面

确保每个人都能使用您的应用程序

为确保每个人都能使用您的应用程序,您需要使其具有无障碍性。在开发应用程序时,无障碍性至关重要。所有能力水平的人都会使用您的应用程序;如果您的应用程序不具有无障碍性,将使您的客户生活更加困难,甚至可能使他们无法使用您的应用程序。

在考虑无障碍性时,大多数人首先想到的是通过为屏幕阅读器添加标签和替代文本来使应用程序对盲人无障碍。然而,无障碍性涉及的远不止这些。例如,视力较低但不是盲人的人可能不使用屏幕阅读器,而是选择使用高对比度主题使应用程序更易于使用,或者选择增大字体大小以便更容易阅读文本。提供暗色主题通常被视为纯粹的美学方面;然而,它在无障碍性方面也很重要。一些人可能能够更好地阅读文本,而某些残障人士可能会更难使用您的应用程序。

如果您已经熟悉 UWP 中可用的 API 来制作应用程序,那么在使您的 Uno 平台应用程序具有无障碍性时会有一些不同之处。由于您的应用程序将在不同的平台上运行,而这些平台都有不同的 API 来提供无障碍应用程序,Uno 平台在无障碍性方面只提供了一部分可用属性。在撰写本文时,只支持以下属性,并且在每个平台上都可以使用:

  • AutomationProperties.AutomationId: 您可以设置此属性以便使用辅助技术更轻松地导航到控件。

  • AutomationProperties.Name: 辅助技术将使用此属性向用户宣布控件。

  • AutomationProperties.LabeledBy: 设置此属性时,将使用此属性指定的控件来宣布设置了此属性的控件。

  • AutomationProperties.AccessibilityView: 使用此属性,您可以指示控件不应该被辅助技术向用户宣布,或者您想要包括通常不会被宣布的控件。

除了之前列出的属性外,Uno 平台还在每个平台上支持高对比度主题。由于我们使用 Uno 平台提供的标准控件,我们不需要特别关注这一点,因为 Uno 平台已经为我们的应用程序提供了正确的高对比度外观。但是,如果您编写自己的控件,您还应该检查您的应用程序的高对比度版本,以确保其可接受。

重要提示

您应该始终本地化辅助技术将使用的资源。不这样做可能会使您的应用程序无法访问,因为用户可能会遇到语言障碍,特别是如果辅助技术期望从一种语言中读出单词,而实际上却是另一种语言的单词。

为了确保您的应用程序对使用辅助技术的人员可访问,您需要使用辅助技术测试您的应用程序。在下一节中,您可以找到启动平台默认屏幕阅读器的说明。

在不同平台上启动屏幕阅读器

由于激活系统辅助技术的步骤因平台而异,我们将逐一进行介绍,从 Android 开始。

Android 上的 TalkBack

启动设置应用程序,打开辅助功能页面。按下TalkBack,并点击开关以启用 TalkBack。最后,按下确定关闭对话框。

iOS 上的 VoiceOver

打开设置应用程序,打开通用下的辅助功能选项。然后,在视觉类别中点击VoiceOver,并点击开关以启用它。

macOS 上的 VoiceOver

启动系统偏好设置,点击辅助功能。然后,在视觉类别中点击VoiceOver。勾选启用 VoiceOver以使用VoiceOver

Windows 上的 Narrator(适用于 UWP 和 WASM)

要在 Windows 上启动Narrator屏幕阅读器,您只需同时按下 Windows 徽标键、CtrlEnter

更新我们的应用程序以实现可访问性

在本章中,我们尚未确保我们的应用程序是可访问的。虽然许多控件已经可以自行访问,例如,按钮控件将宣布其内容,但仍然有一些控件需要我们在可访问性方面进行改进。如果用户使用辅助技术使用应用程序,不是所有内容都会以有意义的方式进行宣布。让我们通过更新应用程序的 UI 来改变这一点,以设置所有必要的属性。为此,我们将首先更新我们的旅程预订页面。

我们旅程预订页面上的两个ComboBox控件目前只会被宣布为ComboBox控件,因此使用辅助技术的用户不知道ComboBox控件实际用途。由于我们已经添加了描述其目的的TextBlock元素,我们将更新它们以使用AutomationProperties.LabeledBy属性:

<TextBlock x:Name="StartPointLabel" x:Uid="StartPointLabel" FontSize="20"/>
<ComboBox ItemsSource="{x:Bind journeyBookingVM.AllStations}" x:Uid="StartPointComboBox"
    AutomationProperties.LabeledBy="{x:Bind 
        StartPointLabel}"
    SelectedItem="{x:Bind 
        journeyBookingVM.SelectedStartpoint,Mode=TwoWay}"
    HorizontalAlignment="Stretch" 
        DisplayMemberPath="Name"/>
<TextBlock x:Name="EndPointLabel" x:Uid="EndPointLabel" FontSize="20"/>
<ComboBox ItemsSource="{x:Bind journeyBookingVM.AvailableDestinations, Mode=OneWay}" x:Uid="EndPointComboBox"
    AutomationProperties.LabeledBy="{x:Bind EndPointLabel}"
    SelectedItem="{x:Bind 
        journeyBookingVM.SelectedEndpoint,Mode=TwoWay}"
    HorizontalAlignment="Stretch" 
    DisplayMemberPath="Name"/>

现在,当用户使用辅助技术导航到ComboBox控件时,ComboBox控件将使用由AutomationProperties.LabeledBy引用的TextBlock元素的文本进行宣布。由于该页面上的其余控件已经为我们处理了可访问性,让我们继续进行所拥有的车票页面。

在所拥有的车票页面上,存在两个潜在问题:

  • 车站名称旁边的图标将被宣布为空白图标。

  • QR 码将只被宣布为一个图像。

由于图标仅用于视觉表示,我们指示辅助技术不应使用AutomationProperties.AccessibilityView属性进行通告,并将其设置为Raw。如果您想为辅助技术包括一个控件,可以将该属性设置为Content

为了确保 QR 码图像能够以有意义的方式进行通告,我们将为其添加一个描述性名称。为简单起见,我们将宣布它是当前选定车票的 QR 码。首先,您需要按照以下方式更新图像元素:

<Image x:Name="QRCodeDisplay" x:Uid="QRCodeDisplay"
    Source="{x:Bind ownedTicketsVM.CurrentQRCode,
             Mode=OneWay}"
    Grid.Row="4" MaxWidth="300" MaxHeight="300" 
        Grid.ColumnSpan="2"/>

之后,将以下条目添加到Resources.resw文件中:

英语

表格 5.4

德语

表格 5.5

通过添加这些条目,我们现在为显示的 QR 码提供了一个描述性名称,同时确保此文本将被本地化。

最后,我们还需要更新设置页面。由于它只包含一个单独的ComboBox控件,缺少名称,因此将以下条目添加到Resources.resw文件中:

英语

表格 5.6

德语

表格 5.7

在本节中,我们简要介绍了 Uno 平台中的可访问性;但是,我们没有提到还有一些限制和需要注意的事项。您可以在官方文档中阅读更多关于这些限制的信息:platform.uno/docs/articles/features/working-with-accessibility.html。如果您希望了解更多关于一般可访问性的信息,您可以查看以下资源:

摘要

在本章中,我们构建了一个在 iOS 和 Android 上运行的面向客户的应用程序。我们介绍了如何使用 SQLite 存储数据,如何使您的应用程序具有可访问性,并使其为客户准备好。作为其中的一部分,我们介绍了如何本地化您的应用程序,让用户选择应用程序的语言,并为您的应用程序提供自定义启动画面。

在下一章中,我们将为 UnoBookRail 编写一个信息仪表板。该应用程序将面向 UnoBookRail 的员工,并在桌面和 Web 上运行。

第六章:显示图表和自定义 2D 图形中的数据

本章将介绍需要显示图形、报告和复杂图形的应用程序。应用程序通常包括某种图形或图表。还越来越常见的是在 UI 中包含无法轻松使用标准控件制作的元素。

随着我们在本章的进展,我们将为我们虚构的业务构建一个仪表板应用程序,显示适合业务不同部分的信息。这样的应用程序在管理报告工具中很常见。您可以想象不同的屏幕显示在每个部门墙上安装的监视器上。这使员工可以立即看到他们所在业务部门的情况。

在本章中,我们将涵盖以下主题:

  • 显示图形和图表

  • 使用 SkiaSharp 创建自定义图形

  • 使 UI 布局对屏幕尺寸的变化做出响应

在本章结束时,您将创建一个仪表板应用程序,显示在 UWP 和 Web 上运行的财务、运营和网络信息。它还将适应不同的屏幕比例,因此每个页面的内容都会考虑不同的屏幕尺寸和纵横比。

技术要求

本章假设您已经设置好了开发环境,包括安装了项目模板,就像在第一章中介绍的那样,介绍 Uno Platform。本章的源代码位于github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/Chapter06

本章的代码使用了来自github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/SharedLibrary的库。

查看以下视频,以查看代码的实际运行情况:bit.ly/3iDchtK

介绍应用程序

本章中我们将构建的应用程序名为Dashboard。这是一个显示业务部门内当前活动的应用程序。这不是所有员工都可以使用的东西,但为了让我们专注于本章的特性和兴趣领域,我们不会关心访问权限是如何控制的。这个应用程序的真实版本将有许多功能,但我们只会实现三个:

  • 显示当前的财务信息

  • 显示实时的运营信息

  • 显示火车目前在网络中的位置

由于这个应用程序将被办公室工作人员使用,它将在桌面上(通过 UWP)和在 Web 浏览器上(使用 WASM 版本)可用。

创建应用程序

我们将从创建应用程序的解决方案开始。

  1. 在 Visual Studio 中,使用多平台应用程序(Uno Platform)模板创建一个新项目。

  2. 给项目命名为Dashboard。您可以使用不同的名称,但需要相应调整所有后续的代码片段。

  3. 删除所有平台头项目,除了 UWPWASM

  4. 为了避免写更多的代码,我们现在将添加对共享库项目的引用。右键单击UnoBookRail.Common.csproj文件中的解决方案节点,然后单击打开

  5. 对于每个特定于平台的项目,我们需要添加对共享库项目的引用。右键单击UnoBookRail.Common,然后单击确定。现在重复此过程以进行 WASM 项目

现在基本的解决方案结构已经准备好,我们可以向主页面添加一些功能。

创建各个页面

我们将为要显示的每个功能区域使用单独的页面:

  1. Views中创建一个新文件夹。

  2. Views文件夹中,添加名为FinancePage.xamlOperationsPage.xamlNetworkPage.xaml三个新页面。

现在我们将更新主页面以在这些新页面之间进行导航。

创建主页面

该应用程序已经包含了文件MainPage.xaml,我们将使用它作为在其他页面之间导航的容器:

  1. 用包含每个我们将实现的单独页面选项的以下NavigationView控件替换MainPage.xaml中的网格:
<NavigationView
    PaneDisplayMode="Top"
    SelectionChanged="NavItemSelected"
    IsBackEnabled="{Binding Path=CanGoBack, 
                    ElementName=InnerFrame}"
    BackRequested="NavBackRequested"
    IsSettingsVisible="False">
    <NavigationView.MenuItems>
        <NavigationViewItem Content="Finance" />
        <NavigationViewItem Content="Operations" />
        <NavigationViewItem Content="Network" />
    </NavigationView.MenuItems>
    <Frame x:Name="InnerFrame" />
</NavigationView>
  1. 我们现在需要添加NavItemSelected事件的处理程序,以执行页面之间的实际导航。在MainPage.xaml.cs中添加以下内容:
using Dashboard.Views;
private void NavItemSelected(NavigationView sender, NavigationViewSelectionChangedEventArgs args) 
{
  var item = (args.SelectedItem as 
              NavigationViewItem).Content.ToString();
  Type page = null;
  switch (item) {
    case "Finance":
      page = typeof(FinancePage);
      break;
    case "Operations":
      page = typeof(OperationsPage);
      break;
    case "Network":
      page = typeof(NetworkPage);
      break;
  }
  if (page != null && InnerFrame.CurrentSourcePageType
      != page) {
    InnerFrame.Navigate(page);
  }
}
  1. 我们还需要实现NavBackRequested方法来处理用户按下返回按钮导航回页面。添加以下内容来实现这一点:
private void NavBackRequested(object sender, NavigationViewBackRequestedEventArgs e) 
{
    InnerFrame.GoBack();
}

导航

该应用程序使用自定义定义的框架和基于堆栈的导航样式。这允许用户按下内置的返回按钮返回到上一页。虽然这可能不被认为是这个应用程序最合适的方式之一,但这是开发人员在 UWP 应用程序中实现导航的最流行方式之一。因此,我们认为将其包含在本书中并展示它可以被整合到 Uno 平台应用程序中是合适的。

  1. 前面的内容将允许我们在菜单中选择项目时在页面之间进行导航,但我们也希望在应用程序首次打开时显示一个页面。为此,在MainPage构造函数的末尾添加以下调用:
InnerFrame.Navigate(typeof(FinancePage));

重要提示

本节中的代码显示了在NavigationView控件中启用页面之间导航的最简单方法。这当然不是唯一的方法,也不是应该总是这样做的建议。

现在所有基础都已就绪,我们现在可以向财务页面添加一个图表。

使用来自 SyncFusion 的控件显示图表

SyncFusion 是一家为 Web、桌面和移动开发制作 UI 组件的公司。他们的 Uno 平台控件在撰写本文时处于测试阶段,并且在预览期间可以免费使用,通过他们的社区许可证(www.syncfusion.com/products/communitylicense)。有许多不同的图表类型可用,但我们将使用线图来创建一个类似于图 6.1所示的页面。图表显示在一些箭头旁边,提供一些一般趋势数据,以便查看它们的人可以快速了解数据的摘要。想象它们代表数据与上周、上个月和去年同一天相比较的情况:

图 6.1-包括来自 SyncFusion 的图表的财务信息

图 6.1-包括来自 SyncFusion 的图表的财务信息

更新引用以包括 SyncFusion 控件

SyncFusion Uno 图表控件的测试版本可在 GitHub 上获得完整的源代码:

  1. github.com/syncfusion/Uno.SfChart下载或克隆代码。

  2. 通过右键单击解决方案并选择添加 | 现有项目…,将Syncfusion.SfChart.Uno.csproj项目添加到解决方案中。

  3. 更新Syncfusion.SfChart.Uno项目以使用最新版本的Uno.UI包。这是为了避免在解决方案中的不同项目中使用不同版本的库时出现任何问题。

  4. UWPWASM项目中引用Syncfusion.SfChart.Uno项目。

我们现在可以在应用程序中使用这些控件。

重要提示

由于 SyncFusion 控件仅从源代码中获取,虽然不太可能,但当您阅读本文时它们可能已经发生了变化。希望可以获得编译版本的控件,但如果您需要达到与本文撰写时相当的状态,请使用提交43cd434

绘制线图

我们可以通过以下步骤绘制一个简单的线图:

  1. 首先将此命名空间添加到FinancePage.xaml中:
xmlns:sf="using:Syncfusion.UI.Xaml.Charts"
  1. 现在用以下内容替换网格:
<RelativePanel HorizontalAlignment="Center">
  <sf:SfChart class we can specify. We define a PrimaryAxis class (for the X-axis), which reflects the hours of the day, with a SecondaryAxis class (for the Y-axis) representing the numeric values and a set of data as a LineSeries class.We also specify a `TextBlock` element to appear below the chart but be horizontally aligned. This will display arrows indicating trend information relating to the graph.
  1. 为了提供数据,我们需要在FinancePage.xaml.cs中的类中添加以下内容:
public List<HourlySales> DailySales
    => FinanceInfo.DailySales
       .Select(s => new HourlySales(s.Hour, 
            s.Sales)).ToList();
public string TrendArrows => FinanceInfo.TrendArrows;
  1. 这些属性需要您添加此using声明:
using UnoBookRail.Common.DashboardData;
  1. 我们还必须创建以下类,SfChart对象将使用它来查找我们在 XAML 中引用的命名属性:
public class HourlySales
{
    public HourlySales(string hour, double totalSales) 
    {
        Hour = hour;
        TotalSales = totalSales;
    }
    public string Hour { get; set; }
    public double TotalSales { get; set; }
}

显然,我们在这里只创建了一个简单的图表,但关键是要注意它是多么容易。一个真正的仪表板可能会显示不止一个图表。您可以在存储库中包含的示例应用程序中看到您可以包含的图表的示例github.com/syncfusion/Uno.SfChart

我们已经看到了如何轻松地包含来自一个供应商的图表来显示财务信息。现在让我们添加另一个供应商的图表,以显示一些不同的信息。

使用 Infragistics 控件显示图表

Infragistics 是一家为各种平台提供 UI 和 UX 工具的公司。他们还有一系列控件可供 Uno 平台应用程序使用,在预览期间免费使用。

您可以在www.infragistics.com/products/uno-platform了解更多关于这些控件的信息,或者跟随我们为应用程序添加图表,以显示与 UnoBookRail 业务的当前操作相关的信息,并创建一个看起来像图 6.2的页面:

图 6.2 - 来自 Infragistics 的图表上显示的网络操作详细信息

图 6.2 - 来自 Infragistics 的图表上显示的网络操作详细信息

更新引用

为了能够在我们的应用程序中使用这些控件,我们必须首先进行以下修改:

  1. UWP项目中引用Infragistics.Uno.Charts NuGet 包:
Install-Package Infragistics.Uno.Charts -Version 20.2.59-alpha
  1. WASM项目中引用Infragistics.Uno.Wasm.Charts NuGet 包:
Install-Package Infragistics.Uno.Wasm.Charts -Version 20.2.59-alpha
  1. WASM项目中引用Uno.SkiaSharp.ViewsUno.SkiaSharp.Wasm NuGet 包。这是必要的,因为 Infragistics 控件使用 SkiaSharp 来绘制控件。这与我们之前使用的 SyncFusion 控件不同,后者使用 XAML:
Install-Package Uno.SkiaSharp.Views -Version 2.80.0-uno.493
Install-Package Uno.SkiaSharp.Wasm -Version 2.80.0-uno.493

通过这些简单的修改,我们现在可以将图表添加到我们的应用程序中。

重要提示

如果在进行上述更改后注意到任何奇怪的编译行为,请尝试清理解决方案,关闭所有打开的 Visual Studio 实例,然后重新打开解决方案。这不应该是必要的,但我们发现在某些情况下需要这样做。

您可能还会在 SyncFusion 项目的错误列表中看到条目,尽管它成功编译。这些错误可以安全地忽略。

绘制柱状图

现在我们将为应用程序的Operations页面添加内容。为简单起见,我们只添加两条信息。我们将添加一个图表,显示今天每小时使用了多少张票的类型。此外,我们将根据持票进入车站但随后没有出站的人数,显示目前在火车上或车站中的人数:

  1. 将以下命名空间添加到OperationsPage.xamlPage元素中:
xmlns:ig="using:Infragistics.Controls.Charts"
  1. 现在将以下 XAML 添加为页面的内容:
<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="*" />
    <RowDefinition Height="*" />
  </Grid.RowDefinitions>
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="*" />
    <ColumnDefinition Width="*" />
  </Grid.ColumnDefinitions>
  <ig:XamDataChart class. Within this, we specify the *x* and *y* axes and the data to display as a StackedColumnSeries element. Within the series, we detail the paths to the data for each fragment of the stack.Finally, we added the `TextBlock` element that displays the current passenger count.
  1. OperationsPage.xaml.cs中添加以下using指令:
using UnoBookRail.Common.DashboardData;

这些是我们将添加到此文件的属性所需的。

  1. 将以下内容添加到OperationsPage类中,提供图表中显示的数据:
public string PsngrCount => OperationsInfo.CurrentPassengers;
private List<PersonCount> Passengers
   => OperationsInfo.Passengers.Select(p 
       => new PersonCount(p.Hour, p.Children,
           p.Adults, p.Seniors)).ToList();
  1. 现在我们需要添加刚刚引用的PersonCount类:
public class PersonCount 
{
    public PersonCount(string hour, double child,
        double adult, double senior) 
    {
        Hour = hour;
        Children = child;
        Adults = adult;
        Seniors = senior;
    }
    public string Hour { get; set; }
    public double Children { get; set; }
    public double Adults { get; set; }
    public double Seniors { get; set; }
}

有了这个,我们现在有一个简单的页面图表,显示每小时旅行的乘客数量。

与 SyncFusion 图表一样,Infragistics 还有许多其他图表和控件可用。您可以在github.com/Infragistics/uno-samples找到这些示例。

现在我们已经看到了使用第三方库显示更复杂控件的不同方法,让我们来看看如何自己绘制更复杂的东西。

使用 SkiaSharp 绘制自定义图形

UWP 和 Uno 平台包括支持创建形状并提供基本绘图功能。然而,有时您需要在应用程序中显示一些无法轻松使用标准控件完成的东西,您需要精细的控制,或者在操作大量 XAML 控件时遇到性能问题。在这些情况下,可能需要直接在 UI 上进行绘制。其中一种方法是使用 SkiaSharp。SkiaSharp 是一个基于 Google 的 Skia 图形库的跨平台 2D 图形 API,我们可以在 Uno 平台应用程序中使用。为了展示使用起来有多简单,我们将创建我们应用程序的最后一部分,显示网络中火车当前位置的地图。只需几行代码,我们就可以创建出类似图 6.3中显示的屏幕截图的东西:

图 6.3-在浏览器中运行时应用程序中显示的网络地图

图 6.3-在浏览器中运行时应用程序中显示的网络地图

现在你已经看到我们要创建的东西了,让我们开始做吧。

更新项目引用

我们在应用程序中需要使用 SkiaSharp 的引用已经作为我们添加到使用 Infragistics 控件的引用的一部分添加。如果您已经进行了这些更改,这里就没有什么要做的了。

如果您在上一节中跟着做,并且没有添加 Infragistics 控件,您需要对解决方案进行以下更改:

  • WASM项目中引用Uno.SkiaSharp.ViewsUno.SkiaSharp.Wasm NuGet 包:
Install-Package Uno.SkiaSharp.Views -Version 2.80.0-uno.493
Install-Package Uno.SkiaSharp.Wasm -Version 2.80.0-uno.493

在添加相关引用之后,我们现在准备绘制网络地图。

绘制网络地图

要在应用程序中绘制网络地图,我们需要采取以下步骤:

  1. NetworkPage.xaml中,添加以下作为唯一的内容。这是将显示我们绘制的控件:
<skia:SKXamlCanvas xmlns:skia="using:SkiaSharp.Views.UWP" PaintSurface="OnPaintSurface" />
  1. 要在SKXamlCanvas控件上绘制地图,我们需要在NetworkPage.xaml.cs中添加以下使用声明:
using SkiaSharp;
using SkiaSharp.Views.UWP;
using UnoBookRail.Common.Mapping;
using UnoBookRail.Common.Network;
  1. 接下来,我们必须添加我们在 XAML 中引用的OnPaintSurface方法。每当控件需要重新绘制图像时,该方法将被控件调用。这将在控件首次加载时以及控件的渲染大小发生变化时发生:
private void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e) 
{
    var canvas = SetUpCanvas(e);
    DrawLines(canvas);
    DrawStations(canvas);
    DrawTrains(canvas);
}
  1. 添加SetUpCanvas方法来正确初始化和定位图像:
private SKCanvas SetUpCanvas(SKPaintSurfaceEventArgs e) 
{
  var canvas = e.Surface.Canvas;
  var relativeWidth = e.Info.Width / ImageMap.Width;
  var relativeHeight = 
      e.Info.Height / ImageMap.Height;
  canvas.Scale(Math.Min(relativeWidth, 
      relativeHeight));
  var x = 0f;
  var y = 0f;
  if (relativeWidth > relativeHeight) 
  {
    x = (e.Info.Width - (ImageMap.Width * 
         relativeHeight)) / 2f / relativeHeight;
  }
  else {
    y = (e.Info.Height - (ImageMap.Height * 
         relativeWidth)) / 2f / relativeWidth;
  }
  canvas.Translate(x, y);
  canvas.Clear();
  return canvas;
}

SetUpCanvas方法调整我们的绘图区域尽可能大,而不会扭曲或拉伸它,并确保它始终水平和垂直居中。最后,它清除画布并返回它,准备让其他方法在其上绘制。

  1. 添加DrawLines方法来在画布上绘制支线:
void DrawLines(SKCanvas canvas) 
{
    var paint = new SKPaint 
    {
        Color = SKColors.Black, 
        StrokeWidth = 1,
    };
    var northPnts = 
        ImageMap.GetStations(Branch.NorthBranch);
    var mainPnts = 
        ImageMap.GetStations(Branch.MainLine);
    var southPnts = 
        ImageMap.GetStations(Branch.SouthBranch);
    SKPoint[] ToSKPointArray(List<(float X, float Y)> 
        list)
        => list.Select(p => new SKPoint(p.X, 
            p.Y)).ToArray();
    void DrawBranch(SKPoint[] stnPoints)
        => canvas.DrawPoints(SKPointMode.Polygon, 
            stnPoints, paint);
    DrawBranch(ToSKPointArray(northPnts));
    DrawBranch(ToSKPointArray(mainPnts));
    DrawBranch(ToSKPointArray(southPnts));
}

在上面的代码中,库返回的站点位置被转换为 Skia 特定的数组,用于绘制连接所有点的多边形。

  1. 添加DrawStations方法来在支线上绘制站点位置:
void DrawStations(SKCanvas canvas) 
{
    var paint = new SKPaint 
    {
        Color = SKColors.Black,
        Style = SKPaintStyle.Fill,
    };
    foreach (var (X, Y) in ImageMap.Stations) 
    {
        canvas.DrawCircle(new SKPoint(X, Y), 2, 
            paint);
    }
}

DrawStations方法很简单,因为它只是为每个站点绘制一个圆圈。

  1. DrawTrains方法添加到地图上显示火车当前位置的方法:
void DrawTrains(SKCanvas canvas) 
{
    var trainPaint = new SKPaint 
    {
        Color = SKColors.Cyan,
        Style = SKPaintStyle.Fill,
    };
    foreach (var train in ImageMap.GetTrainsInNetwork()) 
    {
        canvas.DrawCircle(new SKPoint(
            train.MapPosition.X, train.MapPosition.Y),
                1.8f, trainPaint);
    }
}

DrawTrains方法同样简单,因为它循环遍历提供的数据,并在每个位置绘制一个青色的圆圈。因为这是在站点圆圈之后绘制的,所以当火车在站点时,它会出现在站点上方。

重要提示

在本章中,我们只使用了一些圆圈和线条来创建我们的地图。然而,SkiaSharp 能够做的远不止我们在这里介绍的。您可能希望通过扩展我们刚刚创建的地图来探索其他可用的功能,包括包括站点名称或添加显示火车行驶方向或是否在站点的其他细节。

现在我们已经实现了应用程序的所有页面,但我们可以通过根据屏幕或窗口的大小调整内容来进一步改进。

响应 UI 的变化

您的应用程序将需要在不同大小的屏幕和窗口上运行。其中一些差异是由应用程序运行的不同设备引起的,但您可能还需要考虑用户可以调整大小的窗口。

可以设计页面的多个版本,并在运行时加载适当的版本。但通常更容易创建一个根据可用尺寸调整的单个页面。我们将看看如何使用可用的功能来实现这一点。

更改页面布局

Uno 平台允许您通过在VisualStates之间切换来创建响应式 UI。

可以创建AdaptiveTrigger元素,根据其附加的控件的大小触发。现在我们将使用自适应触发器来调整财务运营页面,以更好地根据可用宽度布置其内容:

  1. 将以下内容添加为FinancePage.xamlRelativePanel的第一个子元素:
<VisualStateManager.VisualStateGroups>
  <VisualStateGroup>
    <VisualState>
      <VisualState.StateTriggers>
        <AdaptiveTrigger element that's applied when the panel is at least 1,200 relative pixels wide. When this visual state is triggered, the TextBlock element is set to the right of the chart and has its alignment adjusted accordingly. The left-hand side of *Figure 6.4* shows how this looks.
  1. 现在我们可以在OperationsPage.xaml页面的网格中做类似的事情。在行和列定义下方立即添加以下内容:
<VisualStateManager.VisualStateGroups>
  <VisualStateGroup>
    <VisualState>
      <VisualState.StateTriggers>
        <AdaptiveTrigger MinWindowWidth="1200" />
      </VisualState.StateTriggers>
      <VisualState.Setters>
        <Setter Target="PassengerChart.
            (Grid.ColumnSpan)" Value="1"/>
        <Setter Target="PassengerChart.(Grid.RowSpan)"
             Value="2"/>
        <Setter Target="CurrentCount.(Grid.Row)" 
             Value="0"/>
        <Setter Target="CurrentCount.(Grid.Column)" 
            Value="1"/>
        <Setter Target="CurrentCount.
            (Grid.ColumnSpan)" Value="1"/>
        <Setter Target="CurrentCount.
            (Grid.RowSpan)" Value="2"/>
      </VisualState.Setters>
    </VisualState>
  </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

通过这些设置器,我们正在利用之前创建的行和列定义。初始代码将控件放在不同的行中,而在这里我们正在更改控件,使它们位于不同的列中,并在窗口更宽时跨越行。如图 6.4所示,这意味着当前在火车上的人数显示在图表旁边,而不是下方:

图 6.4-以横向布局显示的财务和运营页面

图 6.4-以横向布局显示的财务和运营页面

通过这两个示例,我们已经看到了改变页面上元素重新定位以更改布局的不同方法。没有一种适合所有不同可用空间量的页面的正确方法。状态触发器可用于更改元素上的任何属性,还可以使用多个触发器,因此您可以为小型、中型和大型屏幕制定不同的布局,例如。

更改屏幕上元素的布局不是调整显示内容的唯一方法。控件本身也可以调整、调整大小和重绘以适应空间。

拉伸和缩放内容以适应可用空间

XAML 的一个优点是其能够动态布局控件,而不依赖于为每个元素提供特定大小。可以通过设置HorizontalAlignmentVerticalAlignment属性来调整单个 XAML 控件的大小,以控制它们如何利用可用空间。将这些属性的值设置为Stretch将允许它们占用其父元素中的所有可用空间。对于更复杂的情况,还可以使用ViewBox元素以不同的方式和方向拉伸控件。

如果您想了解如何使用 XAML 元素创建布局的更多信息,您可以在platform.uno/docs/articles/winui-doc-links-development.html#layouting找到一些有用的链接。

许多控件也会自动调整以使用所有或尽可能多的可用空间。我们在 SkiaSharp 绘制的地图上做到了这一点。地图被绘制得尽可能大,而不会扭曲。它被放置在可用空间的中心,无论窗口是纵向还是横向纵横比。

现在所有页面都已调整到可用空间,我们的应用程序和本章已经完成。

总结

在这一章中,我们构建了一个可以在 UWP 和 Web 浏览器上运行的应用程序。该应用程序使用了 SyncFusion 和 Infragistics 的图形控件。我们还使用 SkiaSharp 创建了一个自定义地图。最后,我们看了如何根据不同和变化的屏幕尺寸调整 UI 布局。

这一章是本书的这一部分的最后一章。在接下来的部分中,我们将从构建应用程序转向如何测试和部署它们。在下一章中,我们将看看如何在更广泛的测试策略中使用Uno.UITest库。在构建可以在多个平台上运行的应用程序时,自动化跨平台的测试可以节省大量时间并提高生产率。

第三部分:测试、部署和贡献

本书的最后部分侧重于代码编写后的应用程序开发。具体来说,它侧重于您如何测试您创建的应用程序的用户界面,然后将它们部署到云端(在 WebAssembly 的情况下)或应用商店。最后,它向您展示了在结束之前可以去哪里获取更多资源、帮助或信息,然后看一下您如何为更广泛的项目做出贡献。

在这一部分,我们包括以下章节:

  • 第七章测试您的应用程序

  • 第八章部署您的应用程序并进一步发展

第七章:测试您的应用

在之前的章节中,我们介绍了使用 Uno 平台开发多种不同类型的应用。Uno 平台不仅允许编写应用程序,还允许使用 Uno.UITest 框架编写自动化 UI 测试,这些测试将在 Android、iOS 和 WebAssembly 上运行。在本章中,我们将使用 Uno.UITest 编写我们的第一个测试,并在不同平台上运行它,包括使用模拟器。之后,您还将学习如何使用 WinAppDriver 为 Windows 编写测试。

在本章中,我们将涵盖以下主题:

  • 为您的应用设置Uno.UITest项目

  • 为您的 Uno 平台应用编写Uno.UITest测试

  • 针对您的应用的 WASM、Android 和 iOS 版本运行您的测试

  • 为您的 Uno 平台应用编写单元测试

  • 使用 WinAppDriver 为您的应用的 UWP 版本编写自动化测试

  • 为什么手动测试仍然很重要

通过本章结束时,您将学会如何使用Uno.UITest和 WinAppDriver 为您的应用编写测试,如何在不同平台上运行这些测试,以及为什么手动测试您的应用仍然很重要。

技术要求

本章假设您已经设置好了开发环境,包括安装了项目模板,就像在第一章中介绍的那样,介绍 Uno 平台。本章的源代码可在github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/Chapter07上找到。

本章中的代码使用了来自github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/SharedLibrary的库。

查看以下视频以查看代码示例:bit.ly/3iBFZ2e

开始使用 Uno.UITest

在我们开始使用 Uno.UITest 之前,让我们先了解一下 Uno.UITest 是什么,以及它的目标是什么。Uno.UITest 是由 Uno 平台团队开发和维护的库,允许开发人员为他们的 Uno 平台应用编写统一的 UI 测试。这些 UI 测试允许您模拟用户与您的应用进行交互,并验证您的应用的 UI,以确保用户交互正常工作,并且您的应用行为符合设计。使用 Uno.UITest,您可以编写 UI 测试(有时也称为交互测试),并针对您的应用的 Android、iOS 和 WASM 版本运行这些测试。

在内部,Uno.UITest 使用Xamarin.UITest来针对应用的 Android 和 iOS 版本运行测试。对于应用的 WASM 版本,Uno.UITest 使用SeleniumGoogle Chrome。使用这些库,Uno.UITest 允许您编写模拟用户与应用 UI 交互的测试,包括模拟鼠标输入(如点击)和键盘输入(如输入文本)。

但是何时应该使用 UI 测试?在编写复杂的应用程序时,确保代码更改没有破坏现有功能通常很难测试,特别是因为某些更改只有在使用应用程序时才会变得显而易见,而不是在单独测试组件或类时。UI 测试非常适合这种情况,因为您可以编写测试来模拟普通用户使用您的应用程序,而无需手动执行数十甚至数百个步骤。编写 UI 测试的常见场景是检查用户是否可以成功完成应用程序中的某些任务,例如登录到您的应用程序或搜索特定内容。虽然 UI 测试非常适合测试这些类型的场景,但 UI 测试并非万能药,也有其缺点。由于 UI 测试模拟用户输入,因此与仅测试单个对象或类的常规单元测试相比,它们运行速度较慢。此外,由于 UI 测试框架或库需要找到一种与您的应用程序交互的方法,因此在更新应用程序的 UI 或更改应用程序中的文本或名称时,UI 测试有时可能会出现问题。

然而,在开发应用程序时,编写 UI 测试非常重要,尤其是在尝试确保没有错误进入应用程序时。这在编写将在各种不同设备上运行的应用程序时尤其有用,这些设备具有不同的屏幕尺寸、功能和操作系统版本,因此可以更容易地在许多不同的配置上测试应用程序,因为手动测试速度慢且容易出错。

在使用 Uno.UITest 之前,我们首先需要一个可以用来编写测试的应用程序。为此,让我们首先创建一个新的解决方案,用于编写测试:

  1. 使用多平台应用程序(Uno 平台)模板创建一个新项目。

  2. 将项目命名为UnoAutomatedTestsApp。当然,您可以使用不同的名称;但是,在本章中,我们将假定项目命名为UnoAutomatedTestsApp

  3. 删除除 Android、iOS、UWP 和 WASM 之外的所有平台头项目。

  4. 现在我们需要向我们的共享库添加引用。要做到这一点,右键单击解决方案文件,选择UnoBookRail.Common.csproj文件,然后单击打开

  5. 在每个头项目中引用共享库项目。为此,在头项目上右键单击,选择添加 > 引用… > 项目,选中UnoBookRail.Common,然后单击确定。由于我们需要在每个头中引用库,请为每个头重复此过程,换句话说,Android、iOS、UWP 和 WASM。

现在我们已经创建了项目,让我们向我们的应用程序添加一些内容,以便我们可以进行测试:

  1. xmlns:toolkit="using:Uno.UI.Toolkit"添加到MainPage.xaml中。

  2. 用以下内容替换MainPage.xaml文件中的Grid控件:

<StackPanel Spacing="10" Padding="10" 
    toolkit:VisibleBoundsPadding.PaddingMask="All" 
    Background="{ThemeResource 
        ApplicationPageBackgroundThemeBrush}">
    <StackPanel x:Name="SignInForm" Spacing="10">
        <TextBox x:Name="UsernameInput" 
            AutomationProperties.AutomationId=
                "UsernameInput"
            TextChanged="Username_TextChanged" 
                Header="Username"/>
        <PasswordBox x:Name="PasswordInput" 
            AutomationProperties.AutomationId=
                "PasswordInput"
            PasswordChanged="Password_PasswordChanged"
                Header="Password"/>
        <TextBlock x:Name=
            "SignInErrorMessageTextBlock" 
                AutomationProperties.AutomationId="
                    SignInErrorMessageTextBlock"
            Foreground="{ThemeResource 
                SystemErrorTextColor}" 
                    Visibility="Collapsed"/>
        <Button x:Name="SignInButton" 
            AutomationProperties.AutomationId=
                "SignInButton"
            Click="SignInButton_Click" 
                Content="Sign in" IsEnabled="False"
            HorizontalAlignment="Center" 
                BorderThickness="1"/>
    </StackPanel>
    <TextBlock x:Name="SignedInLabel" 
        AutomationProperties.AutomationId=
            "SignedInLabel"
        Text="Successfully signed in!" 
            Visibility="Collapsed"/>
</StackPanel>
  1. 这是一个简单的登录界面,我们将在本章后面为其编写测试。它包括登录控件、登录按钮和在登录时显示的标签。

  2. 现在,向MainPage类添加以下两个方法:

using UnoBookRail.Common.Auth;
...
private void Username_TextChanged(object sender, TextChangedEventArgs e)
{
    SignInButton.IsEnabled = UsernameInput.Text.Length
        > 0 && PasswordInput.Password.Length > 0;
}
private void Password_PasswordChanged(object sender, RoutedEventArgs e)
{
    SignInButton.IsEnabled = UsernameInput.Text.Length
        > 0 && PasswordInput.Password.Length > 0;
}
private void SignInButton_Click(object sender, RoutedEventArgs args)
{
    var signInResult = Authentication.SignIn(
        UsernameInput.Text, PasswordInput.Password);
    if(!signInResult.IsSuccessful && 
        signInResult.Messages.Count > 0)
    {
        SignInErrorMessageTextBlock.Text = 
            signInResult.Messages[0];
        SignInErrorMessageTextBlock.Visibility = 
            Visibility.Visible;
    }
    else
    {
        SignInErrorMessageTextBlock.Visibility = 
            Visibility.Collapsed;
        SignInForm.Visibility = Visibility.Collapsed;
        SignedInLabel.Visibility = Visibility.Visible;
    }
}

此代码添加了处理程序,允许我们在用户输入用户名和密码后立即启用登录按钮。否则,登录按钮将被禁用。除此之外,我们还处理登录按钮的点击,并相应地更新 UI,包括在登录失败时显示错误消息。

现在,如果您启动应用程序的 UWP 头,您将看到类似图 7.1的东西:

图 7.1 - 运行应用程序的屏幕截图,带有登录表单

图 7.1 - 运行应用程序的屏幕截图,带有登录表单

现在我们已经添加了一个简单的测试应用程序,我们可以再次进行测试,现在我们可以添加我们的Uno.UITest测试项目:

  1. 如果您想对应用程序的 WASM 头运行测试,请确保已安装 Google Chrome。

  2. 首先,您需要更新 Android、iOS 和 WASM 头的项目文件。为此,在这些项目的.csproj文件的最后一个闭合项目标签之前添加以下条目:

<PropertyGroup Condition="'$(Configuration)'=='Debug' or '$(IsUiAutomationMappingEnabled)'=='True'"> 
    <IsUiAutomationMappingEnabled>
        True</IsUiAutomationMappingEnabled>
    <DefineConstants>$(DefineConstants);
        USE_UITESTS</DefineConstants>
 </PropertyGroup>
  1. 对于 iOS 项目,添加对Xamarin.TestCloud.Agent NuGet 包的引用。由于在撰写时,最新的稳定版本是OnLaunched方法的App.xaml.cs文件,因此在该方法的开头添加以下内容:
#if __IOS__ && USE_UITESTS
      // Launches Xamarin Test Cloud Agent
      Xamarin.Calabash.Start();
 #endif

由于 Uno.UITest 库在底层使用 Xamarin.UITest,因此对于 iOS 应用程序,我们需要添加前面的代码。否则,Xamarin.UITest 无法与正在运行的 iOS 应用程序交互,测试将无法正常工作。

  1. 由于 Uno.UITest 项目类型未包含在 Uno Platform Visual Studio 模板扩展中,请确保已安装了 Uno Platform dotnet new模板。您可以在Chapter 1中找到此说明,介绍 Uno 平台

  2. UnoAutomatedTestsApp文件夹内,创建一个名为UnoAutomatedTestsApp.UITests的文件夹。

  3. 在新创建的文件夹内,运行以下命令:

dotnet new unoapp-uitest

这将在文件夹内创建一个新的 Uno.UITest 项目,并将该项目添加到解决方案文件中。

  1. 更新 Android 和 iOS 应用程序的包名称。对于 Android,请使用UnoBook.UnoAutomatedTestsApp替换 Android 项目的Properties/AndroidManifest.xml文件。要替换 iOS 包名称,请打开 iOS 项目内的Info.plist文件,并将Bundle Identifier替换为UnoBook.UnoAutomatedTestsApp

  2. 现在我们需要更新 Uno.UITests 应用项目内的Constants.cs文件,以指向正确的应用程序。为此,请使用以下内容替换第 13、14 和 15 行:

public readonly static string iOSAppName = "UnoBook.UnoAutomatedTestsApp";
public readonly static string AndroidAppName = "UnoBook.UnoAutomatedTestsApp";
public readonly static string WebAssemblyDefaultUri = "http://localhost:[PORT]/";

由于 WASM 应用的端口是随机生成的,因此请使用前面代码中的以下信息替换[PORT]

注意

我们需要更新Constants.cs文件,因为 Uno.UITest 需要能够通过应用程序名称或在 WASM 情况下的应用程序 URI 找到应用程序。要找出您的 WASM head 正在运行的 URI,请打开 WASM head 内的Properties/launchSettings.json。在那里,根据您是否将使用[ProjectName].Wasm目标,可以使用[Project name].Wasm配置文件中的applicationUrl来确定端口。在本章中,我们将使用位于 iOS 项目内的Info.plist文件。有关 Android 应用程序名称,请参考 Android 项目的Properties/AndroidManifest.xml文件中的包属性。

UnoAutomatedTestsApp.UITests项目内,您将找到三个文件:

  • Constants.cs:这包含了使用应用程序包名称或应用程序的 URL 来找到正在运行的应用程序的配置,如前所述。

  • Given_MainPage.cs:这是一个包含小型测试的示例测试文件,显示了如何编写测试。

  • TestBase.cs:此文件包含所有启动和关闭应用程序的引导代码,并公开一个IApp实例(在下一节中详细介绍)。该文件还导出了一个TakeScreenshot函数,您可以使用它来对正在测试的运行中的应用程序进行截图。

现在我们已经介绍了如何设置 Uno.UITest 项目及其结构,让我们继续编写我们的第一个 Uno.UITest,并学习如何运行这些测试。

编写和运行您的第一个测试

在我们开始编写第一个测试之前,我们将介绍如何使用 Uno.UITest 与您的应用程序进行交互。为此,我们将首先介绍使用 Uno.UITests 查询功能对象来定位元素的基础知识。

Uno.UITest 的工作原理

由于 UI 测试需要处理应用程序的 UI 元素,因此每个 UI 测试库都需要一种方式让开发人员处理这些元素。Uno.UITest使用IAppQuery接口来定义查询和IApp接口来运行这些查询和注入输入。

IApp接口为您提供了与应用程序交互所需的 API,包括单击元素、模拟滚动和注入文本输入。在创建Uno.UITest项目的过程中,TestBase类将为您提供一个IApp实例。由于IApp接口允许您模拟对应用程序的输入,并且大多数交互需要特定的控件作为交互的目标,因此IApp接口上的大多数方法都要求您指定控件的AutomationID属性或使用IAppQuery接口。

在下面的示例中,我们将使用AutomationID来单击按钮,如以下 XAML 中所定义的:

<!-- Setting AutomationId to reference button from UI test -->
<Button AutomationProperties.AutomationId="SignInButton Content="Sign in"/>

在编写 Uno.UITest 测试时,我们可以使用以下代码按下按钮:

App.Tap("SignInButton");

与使用x:Name/AutomationID来指定元素的控件不同,通过使用IAppQuery接口,您可以根据其他属性(例如它们的类型)或基于控件上设置的特定属性来寻址控件。在使用IAppQuery时,您会注意到IApp接口不希望得到IAppQuery类型的元素,而是希望得到Func<IAppQuery,IAppQuery>类型的元素。由于IApp接口在很大程度上依赖于此,因此您经常会看到以下using-alias语句:

using Query=System.Func<Uno.UITest.IAppQuery,Uno.UITest.IAppQuery>;

这使开发人员更容易编写查询,因为您可以简单地使用Query类型别名,而不必每次都写出来。为简单起见,在本章中,我们还将使用此using语句并使用Query类型。

如果我们从之前的 XAML 中获取,使用IAppQuery接口按下按钮可以按照以下方式完成:

Query signInButton = q => q.Marked("SignInButton");
App.Tap(signInButton);

当我们创建 Uno.UITest 项目时,您可能还注意到已添加了对 NUnit NuGet 包的引用。默认情况下,Uno.UITest 使用 NUnit 进行断言和测试。当然,这并不意味着您必须在测试中使用 NUnit。但是,如果您希望使用不同的测试框架,您将需要更新TestBase.cs文件,因为它使用 NUnit 属性来连接测试的设置和拆卸。

现在我们已经介绍了Uno.UITest的基本工作原理,现在我们将继续为我们的登录界面编写测试。

编写您的第一个测试

我们将从编写我们在本章开头添加的登录界面的第一个测试开始。为简单起见,我们将使用NUnit,因为在创建新的Uno.UITest项目时,默认情况下会使用Uno.UITest,这意味着我们不必更新TestBase类。我们首先创建一个新的测试文件:

  1. 首先,删除现有的Given_MainPage.cs文件。

  2. 创建一个名为Tests的新文件夹。

  3. Tests文件夹内创建一个名为SignInTests.cs的新类。

  4. 使用以下代码更新SignInTests.cs

using NUnit.Framework;
using Query = System.Func<Uno.UITest.IAppQuery, Uno.UITest.IAppQuery>;
namespace UnoAutomatedTestsApp.UITests.Tests
{
    public class SignInTests : TestBase
    {
    }
}

我们继承自TestBase以访问当前测试运行的IApp实例并能够向我们的应用程序发送输入。除此之外,我们还添加了一个使用NUnit库的using语句,因为我们稍后将使用它,并添加了我们在Uno.UITest 工作原理部分中介绍的命名使用语句。

现在,让我们添加我们的第一个测试。让我们首先简单地检查电子邮件和密码输入字段以及登录按钮是否存在。在本节的其余部分,我们将只在SignInTests.cs文件中工作,因为我们正在为登录用户界面编写测试:

  1. 首先添加一个新的公共函数,这将是我们的测试用例。我们将函数命名为VerifySignInRenders

  2. 添加NUnit知道该函数是一个测试。

  3. 现在,在函数内添加以下代码:

App.WaitForElement("UsernameInput");
App.WaitForElement("PasswordInput");
App.WaitForElement("SignInButton");

您的SignInTests类现在应该看起来像这样:

public class SignInTests : TestBase
{
    [Test]
    public void VerifySignInRenders()
    {
        App.WaitForElement("UsernameInput", "Username input 
            wasn't found.");
        App.WaitForElement("PasswordInput", "Password input 
            wasn't found.");
        App.WaitForElement("SignInButton", "Sign in button 
            wasn't found.");
    }
}

现在我们的测试所做的是尝试查找具有UserNameInputPasswordInputSignInButton的自动化 ID 的元素,并且如果找不到这些元素,则测试失败。

现在我们已经编写了第一个测试,让我们试一下!为此,我们首先来看如何运行这些测试。

在 Android、iOS 和 WASM 上运行您的测试

针对您的应用的 Android、iOS 和 WASM 头运行您的Uno.UITest测试非常简单,尽管具体过程因您尝试启动的平台而略有不同。

针对 WASM 头运行测试

让我们首先针对我们应用的 WASM 头运行我们的测试:

  1. 首先,您需要部署应用的 WASM 头。为此,请选择UnoAutomatedTestsApp.Wasm作为启动项目,并选择IIS Express目标,如图 7.2所示。然后,按下Ctrl + F5,这将部署项目。图 7.2 - 选择了 IIS Express 的 WASM 项目

图 7.2 - 选择了 IIS Express 的 WASM 项目

  1. 更新Constants.cs并将Constants.CurrentPlatform更改为Platform.Browser。如果您尚未更新Constants.WebAssemblyDefaultUri属性,请按照开始使用 Uno.UITest部分中的说明进行操作。

  2. 通过单击菜单栏中的View,然后单击Test Explorer来打开Test Explorer。现在,展开树并右键单击VerifySignInRenders测试。从弹出菜单中选择Run选项。现在,测试将针对在 Chrome 中运行的应用运行。

重要提示

在撰写本文时,由于 Uno.UITest 存在已知的 bug,针对 WASM 头运行测试可能无法正常工作,因为 Chrome 可能无法启动。不幸的是,目前还没有已知的解决方法。要了解有关此错误当前状态的更多信息,请参考以下 GitHub 问题:github.com/unoplatform/Uno.UITest/issues/60

一旦测试开始,Chrome 将以无头模式启动,一旦测试完成,测试将在 Visual Studio Test Explorer 中标记为通过。

针对您的应用的 Android 版本运行测试

除了针对 WASM 头运行测试外,您还可以针对在模拟器上运行的 Android 版本或在 Android 设备上运行的应用运行测试。要做到这一点,请按照以下步骤操作:

  1. 确保Android 模拟器正在运行,并且应用已部署。要部署应用的 Android 版本,请选择 Android 项目作为启动项目,然后按下Ctrl + F5。如果您想对在 Android 设备上运行的应用运行测试,请确保应用已部署在设备上,并且您的设备已连接到计算机。

  2. 更新Constants.cs并将Constants.CurrentPlatform更改为Platform.Android。如果您尚未更新Constants.AndroidAppName属性,请按照开始使用 Uno.UITest部分中的说明进行操作。

  3. 与 WASM 一样,现在在Test Explorer中右键单击测试,然后单击Run。应用将在模拟器内或在 Android 设备上启动,并且测试将针对正在运行的 Android 应用运行。

针对 iOS 版本的应用运行测试

您还可以针对在模拟器上运行的 iOS 版本或在 iOS 设备上运行的应用运行 UI 测试。请注意,这需要 macOS。要针对 iOS 头运行测试,请按照以下步骤操作:

  1. 确保 iOS 模拟器正在运行,并且应用已部署。要部署应用的 iOS 版本,请选择 iOS 项目作为启动项目并运行应用。如果您想对在 iOS 设备上运行的应用运行测试,请确保应用已部署在设备上,并且已连接到计算机。

  2. 更新Constants.cs并将Constants.CurrentPlatform更改为Platform.iOS。将iOSDeviceNameOrId设置为您希望使用的模拟器或连接设备的名称。

如果使用连接设备,您可能还需要更改iOSAppNameinfo.plist,以使其与您的开发者证书兼容。

  1. 现在,在Tests窗口中右键单击测试项目,然后单击Run Test。应用将启动,并且测试将运行。

附加信息

在 Mac 上运行 UI 测试需要具有兼容的测试库、工具和操作系统版本。如果在运行测试时遇到错误,请确保您拥有 OS X、Xcode、Visual Studio for Mac 和测试项目中使用的 NuGet 包的最新版本。您还可能需要确保您正在运行的设备或模拟器是最新的 iOS 版本(包括任何更新)。

在模拟器上运行 UI 测试可能会消耗大量资源。如果测试无法在模拟器上启动,可能需要在连接的设备上运行测试。

如果在物理设备上进行测试,必须启用 UI 自动化。在设置 > 开发人员 > UI 自动化中启用此功能。

希望会添加更多文档,使在 Mac 上进行测试和调试测试更加容易。有关此问题的进展,请参见github.com/unoplatform/Uno.UITest/issues/66

现在我们已经介绍了如何针对 Android、iOS 和 WASM 版本的应用程序运行测试,我们将通过为我们的登录界面编写更多 UI 测试来深入了解如何编写测试。

编写更复杂的测试

到目前为止,我们只测试了登录界面呈现的基本示例。但是,我们还希望确保我们的登录界面实际上可以正常工作并允许用户登录。为此,我们将编写一个新的测试,以确保在提供用户名和密码时,登录按钮是可点击的:

  1. SignInTests.cs文件中创建一个名为VerifyButtonIsEnabledWithUsernameAndPassword的新函数,并为其添加Test属性。

  2. 由于我们将经常使用这些查询,因此将以下Query对象添加到SignInTests类中:

Query usernameInput = q => q.Marked("UsernameInput");
Query passwordInput = q => q.Marked("PasswordInput");
Query signInButton = q => q.Marked("SignInButton");
  1. 现在,让我们通过将以下代码插入VerifyButtonIsEnabledWithUsernameAndPassword测试来模拟在用户名和密码字段中输入文本:
App.ClearText(usernameInput);
App.EnterText(usernameInput, "test"); App.ClearText(passwordInput);
App.EnterText(passwordInput, "test");

重要提示

由于 Xamarin.UITest 存在一个 bug,Uno.UITest 用于 Android 和 iOS 的测试库,清除和输入测试无法在每台 Android 设备或模拟器上正常工作。您可以在这里找到有关此错误的更多信息:github.com/microsoft/appcenter/issues/1451。作为解决方法,您可以使用 API 版本为 28 或更低的 Android 模拟器,因为这些 Android 版本不受此 bug 影响。

这将模拟用户在用户名输入字段和密码输入字段中输入文本test。请注意,在这个和后续的测试中,我们将始终在输入文本之前清除文本,以确保输入了正确的文本。

注意

在作为一组运行多个测试时,例如通过选择多个测试或它们的根节点在测试资源管理器中,Uno.UITest 不会在各个测试之间重置应用程序。这意味着如果测试依赖于特定的初始应用程序状态,则需要为您的测试编写初始化代码。

  1. 现在,让我们使用以下代码验证登录按钮是否已启用:
var signInButtonResult = App.WaitForElement(signInButton);
Assert.IsTrue(signInButtonResult[0].Enabled, "Sign in button was not enabled."); 

对于此操作,我们确保按钮存在,并获取该查询的IAppResult[]对象。然后,我们通过IAppResult.Enabled属性检查按钮是否已启用。请注意,我们通过提供第二个参数向断言添加了一条在断言失败时显示的消息。

现在,如果在 Android 上运行测试,应用程序将在您的 Android 设备或模拟器上启动。Uno.UITest然后会在用户名密码输入字段中输入文本,您应该会看到登录按钮变为可点击状态。

现在让我们测试无效的登录凭据是否提供了有意义的错误消息。为此,我们将编写一个新的测试:

  1. SignInTests.cs文件中创建一个名为VerifyInvalidCredentialsHaveErrorMessage的新函数,并为其添加Test属性。

  2. 现在,在SignInTests类中添加一个新的查询,我们将用它来访问错误消息标签:

Query errorMessageLabel = q => q.Marked("SignInErrorMessageTextBlock");
  1. 现在,让我们输入绝对无效的凭据并使用以下代码按下登录按钮:
App.ClearText(usernameInput);
App.EnterText(usernameInput, "invalid");
App.ClearText(passwordInput);
App.EnterText(passwordInput, "invalid");
App.Tap(signInButton);
  1. 由于我们将在测试中使用Uno.UITest扩展方法和Linq,请添加以下using语句:
using System.Linq;
using Uno.UITest.Helpers.Queries;
  1. 最后,我们需要使用以下代码验证错误消息。通过这样做,我们检查错误标签是否显示适当的错误消息:
var errorMessage = App.Query(q => errorMessageLabel (q).GetDependencyPropertyValue("Text").Value<string>()).First();
Assert.AreEqual(errorMessage, "Username or password invalid or user does not exist.", "Error message not correct."); 
  1. 如果您现在运行此测试,您将看到用户名“invalid”和密码“invalid”将被输入。之后,测试点击登录按钮,您将看到错误消息用户名或密码无效或用户不存在

最后,我们希望验证具有有效凭据的用户可以登录的事实。为此,我们将使用用户名demo和密码1234,因为这些对身份验证代码来说是已知的演示用户:

  1. 与以前的测试一样,创建一个名为VerifySigningInWorks的新函数,并在SignInTests.cs文件中添加Test属性。

  2. 由于我们将使用SignedInLabel来检测我们是否已登录,因此添加以下查询,因为我们稍后将使用它来检测标签是否可见。

  3. 添加以下代码以输入演示用户凭据并登录:

App.ClearText(usernameInput);
App.EnterText(usernameInput, "demo");
App.ClearText(passwordInput);
App.EnterText(passwordInput, "1234");
App.Tap(signInButton);
  1. 最后,通过以下代码验证我们是否已登录,以验证已登录标签是否可见并显示正确的文本:
var signedInMessage = App.Query(q => signedInLabel(q).GetDependencyPropertyValue("Text").Value<string>()).First();
Assert.AreEqual(signedInMessage, "Successfully signed in!", "Success message not correct."); 
  1. 如果您运行此测试,您将看到用户名demo和密码1234已被输入。测试点击登录按钮后,登录表单将消失,您将看到文本成功登录

虽然我们介绍了使用 Uno.UITest 编写测试,当然,我们并没有涵盖所有可用的 API。图 7.3显示了 Uno.UITest 作为一部分提供的不同 API 列表以及您可以如何使用它们:

图 7.3 - Uno.UITest 的其他可用 API 列表

图 7.3 - Uno.UITest 的其他可用 API 列表

现在我们已经介绍了使用Uno.UITest编写测试,让我们看看您可以使用的工具来为您的应用程序编写自动化测试,包括使用 WinAppDriver 为应用程序的 UWP 头部编写 UI 测试。

除了 Uno.UITest 之外的测试工具

Uno.UITest并不是您可以用来为 Uno 平台应用编写自动化测试的唯一工具。在本节中,我们将介绍使用 WinAppDriver 和 Selenium 为项目的 UWP 头部编写 UI 测试,并为项目的 UWP 头部编写单元测试。

使用 WinAppDriver 测试应用程序的 UWP 头部

在撰写本文时,Uno.UITest 不支持针对应用程序的 UWP 头部运行测试。但是,您可能还想针对应用程序的 UWP 版本运行 UI 测试。幸运的是,WinAppDriverAppium允许我们实现这一点。WinAppDriver 是微软开发的一个工具,允许开发人员模拟对 Windows 应用程序(包括 UWP 应用程序)的输入。虽然 WinAppDriver 允许您与 Windows 应用程序交互,但它是通过在本地启动一个 Web 服务器,并通过与 WinAppDriver 通过基于 Web 的协议进行通信来允许与应用程序交互的。为了使开发过程对我们更加容易,我们将使用Appium.WebDriver作为编写 UI 测试的库。我们将首先创建我们的测试项目并添加必要的测试。请注意,我们将创建一个新项目,因为我们不希望 Appium.WebDriver 干扰 Uno.UITest,并且我们无法从 UWP 项目内部使用 Appium 和 WinAppDriver,这意味着我们无法重用我们的 UWP 单元测试项目:

  1. 首先,您需要安装 WinAppDriver。为此,请转到 WinAppDriver 的发布页面(github.com/Microsoft/WinAppDriver/releases)并下载最新的 MSI 安装程序。在撰写本文时,最新的稳定版本是版本WinAppDriver.exe文件,如果您在不同的文件夹中安装了 WinAppDriver,您应该记下安装文件夹。

  2. 打开UnoAutomatedTestsApp解决方案并创建一个新的单元测试项目。要做到这一点,右键单击解决方案节点,然后点击添加 > 新建项目

  3. 在对话框中搜索Unit Test App并选择图 7.4中突出显示的选项:图 7.4 - 新项目对话框中的单元测试项目模板

图 7.4 - 新项目对话框中的单元测试项目模板

  1. UnoAutomatedTestsApp.UWPUITests。当然,您可以以不同的名称命名项目;但是,在本章中,我们将假定项目命名为UnoAutomatedTestsApp.UWPUITests。然后,点击下一步

  2. 现在,选择目标框架;我们将使用.NET 5.0。现在,点击创建来创建项目。

  3. 创建项目后,在解决方案视图中右键单击项目,然后在浏览部分点击Appium.WebDriver并安装该包。

现在我们已经创建了单元测试项目,我们可以使用Appium.Webdriver编写我们的第一个 UI 测试。我们将只介绍如何使用 Appium 和 WinAppDriver 编写您的第一个测试。您可以在它们的官方文档中找到有关 WinAppDriver 和编写测试的更多信息:

  1. 在编写我们的第一个测试之前,首先将UnitTest1.cs文件重命名为SignInTests.cs。还将UnitTest1类重命名为SignInTests

  2. 打开位于应用程序 UWP 头部内的Package.appxmanifest文件,并更改UnoAutomatedTestsApp。现在,通过选择 UWP 头部并按下Ctrl + F5来部署您的应用程序的 UWP 头部。由于我们已更改了包名称,我们希望测试使用更新后的包名称启动应用程序。

  3. 将以下using语句添加到SignInTests类中:

using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
  1. 现在,将以下代码添加到SignInTests类中。
private static WindowsDriver<WindowsElement> session;
[AssemblyInitialize]
public static void InitializeTests(TestContext _)
{
    AppiumOptions appiumOptions = new AppiumOptions();
    appiumOptions.AddAdditionalCapability("app", 
       "WindowsDriver object to interact with the app, we store a reference to it. Note that the highlighted section will be different for your app. To get the correct value, open the Package.appxmanifest file and open the Packaging tab. Then, replace the highlighted part with the Package family name value.
  1. 现在,删除现有的TestMethod1测试,并添加以下测试:
[TestMethod]
public void VerifyButtonIsEnabledWithUsernameAndPasswordUWP()
{
    var usernameInput = 
        session.FindElementByAccessibilityId(
            "usernameInput");
    usernameInput.SendKeys("test");
    var passwordInput = 
        session.FindElementByAccessibilityId(
            "passwordInput");
    passwordInput.SendKeys("test");
    var signInButton = 
        session.FindElementByAccessibilityId(
            "signInButton");
    Assert.IsTrue(signInButton.Enabled, "Sign in 
        button should be enabled.");
}

与我们在 Uno.UITest 部分编写的VerifyButtonIsEnabledWithUsernameAndPassword测试一样,此测试验证了当输入用户名和密码时,登录按钮是否已启用。

现在我们已经编写了我们的第一个测试,让我们运行它!要做到这一点,您首先需要启动 WinAppDriver。如果您已将 WinAppDriver 安装在默认文件夹中,您将在C:\Program Files (x86)\Windows Application Driver文件夹中找到WinAppDriver.exe文件。如果您之前选择了不同的安装文件夹,打开该文件夹并在其中启动WinAppDriver.exe文件。启动后,您应该看到如图 7.5所示的内容。

图 7.5 - 运行 WinAppDriver 的窗口

图 7.5 - 运行 WinAppDriver 的窗口

现在,通过右键单击测试资源管理器中的VerifyButtonIsEnabledWithUsernameAndPasswordUWP测试并点击运行来启动测试。测试将启动应用程序,输入文本,然后检查登录按钮是否已启用。

使用 Axe.Windows 进行自动化可访问性测试

除了编写常规 UI 测试之外,您还可以添加Axe.Windows是由 Microsoft 开发和维护的旨在检测应用程序中的可访问性问题的库。将Axe.Windows添加到您的 UI 测试非常简单:

  1. Axe.Windows和安装包中添加对Axe.Windows包的引用。

  2. 现在,将以下两个using语句添加到SignInTests.cs文件中:

using Axe.Windows.Automation;
using System.Diagnostics;
  1. 最后,将以下测试添加到SignInTests类中:
[TestMethod]
public void VerifySignInInterfaceIsAccessible()
{
    var processes = Process.GetProcessesByName(
        "UnoAutomatedTestsApp");
    Assert.IsTrue(processes.Length > 0);
    var config = Config.Builder.ForProcessId(
        processes[0].Id).Build();
    var scanner = ScannerFactory.CreateScanner(
        config);
    Assert.IsTrue(scanner.Scan().ErrorCount == 0, 
        "Accessibility issues found.");
}

由于Axe.Windows需要知道进程 ID,因此我们首先使用Axe.Windows配置获取正在运行的应用程序的进程 ID,然后使用该进程 ID 创建一个新的Axe.Windows扫描程序,允许我们使用Scan()扫描我们的应用程序以查找辅助功能问题,返回一个扫描结果对象告诉我们已经找到了所有辅助功能问题,我们断言我们已经找到了零个辅助功能错误。在编写更复杂应用程序的 UI 测试时,您将更频繁地扫描应用程序,以确保此辅助功能扫描覆盖了应用程序内的每种情景和视图。例如,您可以在导航到不同视图时每次扫描应用程序以查找辅助功能问题。如果现在运行测试,测试应用程序将启动,几秒钟后,测试将被标记为Axe.Windows

在本节中,我们只是浅尝辄止地介绍了使用 WinAppDriver 和 Axe.Windows 进行测试,还有很多内容可以涵盖。如果您想了解更多关于使用 WinAppDriver 编写测试的信息,可以在其编写测试脚本文档中找到更多信息(github.com/microsoft/WinAppDriver/blob/master/Docs/AuthoringTestScripts.md),或者查看它们的示例代码:github.com/microsoft/WinAppDriver/tree/master/Samples/C%23。如果您想了解更多关于 Axe.Windows 的信息,可以访问它们的 GitHub 存储库:github.com/microsoft/axe-windows

在下一节中,我们将介绍如何为 Uno 平台应用程序编写单元测试,包括不同的方法。

为 Uno 平台应用程序编写单元测试

随着应用程序复杂性的增加,确保应用程序逻辑的正常工作变得越来越难以在没有测试的情况下验证。虽然您可以使用 UI 测试来验证逻辑,但您只能验证作为 UI 的一部分公开的逻辑。然而,诸如网络访问或错误处理之类的事情很难使用 UI 测试来验证,因为这些事情通常是通过 UI 公开的。除此之外,UI 测试速度较慢,因为它们模拟用户交互并依赖于渲染的 UI 进行更新。

这就是单元测试的作用。单元测试是验证代码的单个单元的小测试。最常见的是,类或函数被视为单独的单元,并且测试是根据它们正在测试的类或函数进行分组的;换句话说,对于每个要测试的类,都有一组仅针对该类而不是任何其他类的测试。随着应用程序复杂性的增加,单元测试允许您验证单个类仍然按预期工作。

重要说明

单元测试并非万能药!虽然单元测试允许您验证单个功能片段的行为,但更大更复杂的应用程序除了单元测试之外还需要更多的测试,换句话说,还需要 UI 测试来确保整个应用程序按预期工作。仅仅因为单个类在隔离状态下工作正确,并不意味着整个构造体按预期工作且没有错误!

由于在撰写本文时,只有针对 UWP 头部创建单元测试得到了很好的支持,因此我们将重点关注这一点。我们现在将介绍创建单元测试项目的不同方法。

添加单元测试项目的不同方法

由于大多数,如果不是全部,您的应用程序逻辑都位于共享项目中,编写单元测试会更加复杂。由于共享项目实际上并不生成您可以引用的程序集,因此有不同的方法来测试应用程序的逻辑,它们都具有各自的优点和缺点。

第一个选项是创建一个包含要在其上运行测试的平台的单元测试的项目,并在该项目中引用共享项目。这是最简单的入门方式,因为您只需要创建一个新项目并引用共享项目。其中一个缺点是,由于共享项目不允许添加诸如 NuGet 包之类的引用,您在共享项目中使用的任何库也需要被测试项目引用。此外,由于共享项目不创建二进制文件,而是编译到引用它的项目中,对共享项目所做的更改将始终导致测试项目重新编译。

下一个选项是将代码留在共享项目中,并在单元测试项目中引用平台头项目;例如,创建一个 UWP 单元测试项目,并在其中引用您的应用程序的 UWP 头。这个选项比第一个选项更好,因为您不会遇到需要添加到测试项目的库引用的问题,因为平台头为我们引用了库。我们将在本章中使用这种方法。

最后一个选项是将共享项目中的代码移动到跨平台库(Uno Platform)项目中,并在平台头和单元测试项目中引用该库。这种方法的好处是您可以单独为库项目添加库引用,而无需手动添加到各个项目的引用。其中一个缺点是,您必须切换到跨平台库项目类型,而不能使用现有的共享项目。这种方法还有一个缺点,即跨平台库将始终为所有平台编译,从而在只需要特定平台时增加构建时间。

现在,通过使用先前讨论的第二个选项,即添加对平台头项目的引用,向我们的应用程序添加一个单元测试。

添加您的第一个单元测试项目

由于我们将引用 UWP 平台头,我们需要一个 UWP 单元测试应用程序。为此,我们首先需要添加一个新项目:

  1. 右键单击解决方案,点击添加 > 新建项目

  2. 在对话框中,搜索Unit Test App (Universal Windows)文本,并选择Unit Test App (Universal Windows)项目类型,如图 7.6所示:图 7.6 - 新项目对话框中的 Unit Test App (Universal Windows)项目类型

图 7.6 - 新项目对话框中的 Unit Test App (Universal Windows)项目类型

  1. 点击UnoAutomatedTestsApp.UWPUnitTests。当然,您可以给项目取不同的名称;但是,在本节和接下来的几节中,我们将假设项目名称如前所述。

  2. 选择最小和目标版本。我们将使用18362,因为应用程序的 UWP 头也使用这些版本。不使用与 UWP 头相同的最小和目标版本可能会导致构建错误,因此您应该始终努力匹配 UWP 头。

  3. 现在,为 Unit Test App 项目添加对 UWP 头的引用。为此,在解决方案视图中右键单击UnoAutomatedTestsApp.UWPUnitTests项目,点击添加 > 引用… > 项目,勾选UnoAutomatedTestsApp.UWP,然后点击确定

  4. 由于对 UWP 头的引用还会将Properties/Default.rd.xml文件复制到构建输出文件夹中,这将导致构建问题,因为编译器希望将两个Default.rd.xml文件复制到同一个文件夹中。因此,将单元测试应用程序的Default.rd.xml文件重命名为TestsDefault.rd.xml。然后,还要更新UnoAutomatedTestsApp.UWPUnitTests.csproj文件指向该文件。如果您从解决方案视图重命名文件,只需选择项目并按下Ctrl + S

  5. 除此之外,我们还需要重命名单元测试项目的图像资产。为此,请在Assets文件夹中的所有图像前加上UWPUnitTestApp-

我们现在能够为 UWP 头部内的所有内容编写和运行单元测试,包括共享项目内的类。对于还包含平台条件代码的较大应用程序,您只能引用为 UWP 头部编译的类和代码。现在我们已经创建了项目,让我们编写一个小的单元测试。与 Uno.UITest 测试项目相比,单元测试应用程序(Universal Windows)项目类型使用MSTest作为测试框架。当然,您可以更改这一点,但为了简单起见,我们将坚持使用 MSTest。请注意,您不能在 UWP 单元测试中使用 NUnit,因为它不支持 UWP。

  1. 由于我们现在没有太多可以测试的类,让我们向共享项目添加一个新类。为此,请创建一个名为DemoUtils的新类。

  2. 用以下内容替换文件的代码:

namespace UnoAutomatedTestsApp
{
    public class DemoUtils
    {
        public static bool IsEven(int number)
        {
            return number % 2 == 0; 
        }
    }
}

我们将只使用这段代码,以便我们有一些简单的单元测试可供编写。

  1. 现在,将DemoUtilsTests.cs文件重命名为UnitTest.cs

  2. 现在,用以下内容替换DemoUtilsTests.cs文件的内容:

using UnoAutomatedTestsApp;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnoAutomatedTests.UWPUnitTests
{
    [TestClass]
    public class DemoUtilsTests
    {
        [TestMethod]
        public void VerifyEvenNumberIsEven()
        {
            Assert.IsTrue(DemoUtils.IsEven(2), 
                "Number 2 should be even");
        }
    }
}

这是一个小的单元测试,用于验证我们的DemoUtils.IsEven函数成功确定数字2是偶数。

我们现在已经添加了我们的第一个单元测试。与 UI 测试一样,您可以通过打开测试资源管理器,展开树,右键单击VerifyEvenNumberIsEven测试,然后单击运行来运行测试。然后将编译测试应用程序,部署它并启动它。您的测试将运行,然后单元测试应用程序将关闭。

在本章的最后一节中,我们将介绍手动测试,为什么它很重要以及如何使用Accessibility Insights手动测试可访问性。

进行手动测试及其重要性

虽然自动化测试有助于发现错误和问题,但它们无法覆盖某些仍需要手动测试的内容。在开发利用摄像头、蓝牙或其他设备功能的应用程序时,编写自动化测试是困难的,有时甚至是不可能的。在这些情况下,手动测试是必要的。这在使用连接功能方面尤为重要,以查看您的应用程序如何处理不稳定的连接,以及您的应用程序是否仍然提供良好的用户体验,特别是在连接质量不同的情况下。更重要的是,使用模拟器进行测试很难验证应用程序在实际设备上的感觉,特别是在考虑用户体验时,例如元素的大小是否合适,并且在屏幕上是否容易点击。

除了测试特定功能,这些功能很难作为自动化测试的一部分进行模拟,例如 GPS 或漫游数据访问,手动测试也是确保您的应用在可用性方面表现出色的关键。在开发过程中,将应用程序在模拟器中运行是可以的,但随着开发的进行,手动测试变得越来越重要。

除了通过在设备或模拟器上使用应用程序手动测试应用程序之外,另一个重要方面是手动测试应用程序的可访问性。确保您的应用程序对用户是可访问的在开发应用程序时至关重要,虽然自动化测试,如Axe.Windows测试,可以帮助发现问题,但它们并不完美。由于可能有各种能力水平的人使用您的应用程序,使您的应用程序不可访问会使您的应用程序对这些客户更难甚至不可能使用。由于每个人都应该能够使用您的应用程序,无论他们的能力水平如何,因此在测试应用程序的可访问性时有不同的工具。然而,在本节中,我们将专注于使用辅助技术和使用Accessibility Insights扫描工具。

Accessibility Insights 是一个允许你手动扫描应用程序的无障碍问题的工具,类似于Axe.Windows所做的。事实上,Axe.Windows就是其内部实现。与Axe.Windows相比,Accessibility Insights 还允许测试你的 Web 应用程序和 Android 应用程序的无障碍问题。在本章中,你将学习如何使用 Accessibility Insights for Windows。如果你想了解更多关于 Accessibility Insights 的信息,包括使用 Accessibility Insights for Web 和 Accessibility Insights for Android,你可以查看官方网站:accessibilityinsights.io/

现在,让我们开始使用 Accessibility Insights for Windows,在 UnoAutomatedTestsApp 的 UWP 头上使用它:

  1. 首先,你需要从accessibilityinsights.io/docs/en/windows/overview/点击Download for Windows来下载 Accessibility Insights for Windows。如果你已经安装了 Accessibility Insights for Windows,你可以继续进行步骤 4

  2. 一旦下载完成,运行 MSI 安装程序安装 Accessibility Insights。

  3. 安装过程完成后,Accessibility Insights for Windows应该会启动,在关闭遥测对话框后,你会看到类似于图 7.7所示的内容:图 7.7 – Accessibility Insights 的屏幕截图

图 7.7 – Accessibility Insights 的屏幕截图

  1. 一旦你关闭了弹出窗口,启动UnoAutomatedTestsApp的 UWP 头。

现在,如果你将鼠标悬停在应用程序上,你会注意到你悬停的区域和该区域的控件将被一个深蓝色区域所包围。在 Accessibility Insights 中,你可以看到控件的不同 UI 自动化属性,例如控件的“控件类型”或它们是否可以通过键盘进行焦点控制。要扫描一个控件,你可以从Live Inspect tree中选择控件,或者点击蓝色矩形框右上角的扫描按钮,如图 7.8所示:

图 7.8 – 控件上突出显示的扫描图标

图 7.8 – 控件上突出显示的扫描图标

虽然 Accessibility Insights 是一个发现无障碍问题的有用工具,但通过使用辅助技术来测试你的应用程序对于确保你的应用程序可以被所有能力水平的用户使用至关重要。为此,我们将使用 Narrator 手动测试 UWP 头。然而,类似的测试也可以在 Android、iOS 和 macOS 上进行。要了解如何在不同平台上启动辅助技术,请参考第五章中的在不同平台上启动屏幕阅读器部分,使你的应用程序准备好迎接现实世界

现在让我们使用 Narrator 来浏览我们的应用程序。要做到这一点,按下Windows 标志键CtrlEnter同时启动 Narrator,并打开Axe.Windows和 Accessibility Insights for Windows 没有捕捉到的内容。为此,通过导航到用户名输入字段,输入文本invalid,然后重复这个过程来到密码字段。在导航到登录按钮并按下空格键时,你会注意到你没有收到任何错误消息的通知。这是一个无障碍问题,因为依赖辅助技术的用户将不会收到错误消息的通知,也不会知道发生了什么。

对于更大的应用程序,浏览应用程序将会更加复杂。虽然我们的测试应用程序很小,所有控件都是可访问的,但对于使用此测试的更大的应用程序,你可以发现关键的无障碍问题,例如对辅助技术来说没有帮助甚至误导的控件表示。在开发过程中早期发现这些问题会使它们更容易修复,并防止它们影响用户。

在本节中,我们只是浅尝辄止地介绍了手动测试以及为什么它是必要的。我们还介绍了如何使用 Accessibility Insights 和辅助技术进行辅助功能测试的方法。

总结

在本章中,我们学习了如何使用 Uno.UITest 和 Selenium 为您的应用程序编写自动化 UI 测试。然后,我们学习了如何在不同平台上运行这些测试,包括在模拟器上运行应用程序上的测试。之后,我们介绍了如何使用 WinAppDriver 为应用程序的 UWP 头部编写 UI 测试,并为 UWP 头部编写单元测试。最后,我们介绍了手动测试以及如何测试辅助功能问题。

在下一章中,我们将讨论部署您的应用程序以及如何使用 Uno 平台将您的 Xamarin.Forms 应用程序带到 Web 上。我们还将介绍如何为其他平台构建,并介绍如何加入甚至为 Uno 社区做出贡献。

第八章:部署您的应用程序并进一步操作

本章结束了我们对 Uno 平台的介绍,但在结束之前还有很多内容需要涵盖。您已经知道 Uno 平台允许创建在多个环境中运行的应用程序。这不仅适用于新应用程序。Uno 平台的吸引力很大一部分在于它使开发人员能够将现有的应用程序在新环境中运行。因为它是建立在 UWP 和 WinUI 之上的,Uno 平台为您提供了一个出色的方式,可以将现有的应用程序在新环境中运行。

在本章中,我们将涵盖以下主题:

  • Xamarin.Forms应用程序带到 WebAssembly

  • 将 Wasm Uno 平台应用部署到 Web

  • 将您的应用程序部署到商店

  • 与 Uno 平台社区互动

通过本章结束时,您将知道如何部署您的应用程序,并且您将对 Uno 平台的后续步骤感到自信。

技术要求

本章假定您已经设置好了开发环境,包括安装项目模板。这在第一章 介绍 Uno 平台中已经涵盖了。

本章还将使用在第六章 显示图表数据和自定义 2D 图形中创建的源代码。这可以在以下网址找到:github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/Chapter06

查看以下视频以查看代码的运行情况:bit.ly/3xDJDwT

将 Xamarin.Forms 应用程序带到 WebAssembly

如果您使用.NET 进行开发,并且以前创建过移动(iOS 和/或 Android)应用程序,您可能已经使用过Xamarin.Forms。如果您有使用Xamarin.Forms构建的移动应用程序,现在希望在 WebAssembly 上运行,您可能担心需要重写代码,但实际上并非如此。

Xamarin.Forms可以创建 UWP 应用程序。Uno 平台允许 UWP 应用程序在其他平台上运行。因此,可以使用Xamarin.Forms生成的 UWP 应用程序,并将其作为 Uno 平台用来创建 Wasm 应用程序的输入。幸运的是,为了简化起见,所有项目输入和输出的连接都由提供的模板处理。

提示

还可以在 Xamarin 应用程序中使用 Uno 平台控件。这样做很简单,有一个指南显示在以下网址:platform.uno/docs/articles/howto-use-uno-in-xamarin-forms.html

为了展示Xamarin.Forms创建的 UWP 应用程序如何被 Uno 平台用来创建 Wasm 应用程序,让我们创建一个新的Xamarin.Forms应用程序,并使用 Uno 添加一个 Wasm 头。当然,您也可以对现有的Xamarin.Forms应用程序执行相同的操作,但前提是它具有 UWP 头。如果您有一个没有 UWP 头的现有Xamarin.Forms应用程序,您需要先添加一个,然后才能创建一个 Wasm 头:

  1. Visual Studio中,使用移动应用(Xamarin.Forms)项目模板创建一个新项目。

  2. 给项目(和解决方案)命名为UnoXfDemo。当然,您也可以使用不同的名称,但您需要相应地调整所有后续引用。

  3. 勾选将解决方案和项目放在同一个目录中框。

  4. 选择Xamarin.Forms特定内容应该可以正常工作。但建议您在流程的早期进行测试,以确定您可能遇到的任何可能问题,特别是自定义 UI 或第三方控件。

  5. 解决方案资源管理器中右键单击解决方案节点,然后选择在终端中打开

  6. 开发人员 PowerShell窗口将在解决方案目录中打开。在其中,输入以下内容:

dotnet new -i Uno.ProjectTemplates.Dotnet::*

这将确保您安装了最新版本的模板。

  1. 现在输入以下内容:
dotnet new wasmxfhead

这将向解决方案中添加新项目。

  1. 选择重新加载,在提示时重新加载解决方案,您将看到UnoXfDemo.Wasm作为解决方案中的新项目。

  2. 将所有Xamarin.Forms的引用降级为5.0.0.1931版本,因为这是 Uno 项目支持的最新版本。

  3. 在 Wasm 项目中添加对Xamarin.Forms包的引用,如下所示:

Xamarin.Forms referenced from the projects in the solution should be the same, and they must also match the version supported by the Uno.Xamarin.Forms.Platform package. If they don't, you'll get an error explaining the different versions referenced and how to address them.
  1. 更新Xamarin.Forms的版本,目前写作时,最新模板中引用的版本。

  2. UnoXfDemo.Wasm项目设置为启动项目,然后开始调试。您将看到类似图 8.1的东西:

图 8.1 - 通过 WebAssembly 运行的默认(空白)Xamarin.Forms 应用程序

图 8.1 - 通过 WebAssembly 运行的默认(空白)Xamarin.Forms 应用程序

当然,您可以继续开发应用程序,添加或更改功能,然后像解决方案中的任何其他项目一样将最新版本部署到 WebAssembly。

现在,我们已经看到了使用 Uno 平台使Xamarin.Forms应用程序作为 Wasm 应用程序运行是多么简单。

重要提示

除了能够使用 Uno 平台使现有的Xamarin.Forms应用程序能够运行外,还可以将现有的 UWP 应用程序转换为使用 Uno 平台以针对其他操作系统。Uno 平台团队已经在以下网址发布了一份官方指南:platform.uno/docs/articles/howto-migrate-existing-code.html

创建了应用程序的 Wasm 版本(无论它最初是作为Xamarin.Forms应用程序还是其他),您希望将其放在 Web 上,以便其他人可以使用。我们现在来看看这个过程。

将 Wasm Uno 平台应用程序部署到 Web

构建 Wasm 应用程序并在本地计算机上运行是一个令人兴奋的步骤,它展示了 Uno 平台的强大潜力。但是,在本地计算机上运行使其他人难以使用。您需要将应用程序托管在所有人都可以访问的地方。

托管基于.NET 的 Web 应用程序最受欢迎的选择可能是 Azure。您可以在任何地方托管您的应用程序,对所有服务来说,流程都非常相似,因为不需要服务器端处理。假设您可能想要托管您的应用程序在 Azure 上,现在让我们看看如何做到这一点。如果您以前从未部署过 Web 应用程序或使用过 Azure,可能会感到害怕,但您会发现这是多么容易,没有什么可害怕的。

免费试用 Azure

如果您还没有 Azure 帐户,可以通过访问以下网址注册免费试用:azure.microsoft.com/free/

与其创建一个新应用程序纯粹是为了展示它被部署,不如使用我们在第六章中创建的应用程序,在图表中显示数据和自定义 2D 图形

  1. 打开之前创建的仪表板应用程序(或从github.com/PacktPublishing/Creating-Cross-Platform-C-Sharp-Applications-with-Uno-Platform/tree/main/Chapter06下载版本)。

  2. 右键单击WASM项目,然后选择发布...

  3. 您会看到有许多地方可以发布您的应用程序,但是因为我们想要将应用程序发布到 Azure,所以选择Azure选项,然后点击下一步

  4. 对于特定的目标,我们将选择Azure App Service (Windows)选项,尽管您也可以使用其他选项。

重要提示

静态 Web 应用程序是托管 Wasm 应用程序的另一种合适方式。有关更多详细信息,请参见azure.microsoft.com/services/app-service/static/

  1. 如果您还没有这样做,请登录到您的 Azure 关联帐户。

  2. 我们将创建一个新的应用服务来托管该应用程序,因此点击创建 Azure 应用服务加号

  3. 将自动分配一个默认名称给您的应用程序。由于这将被用作应用程序将被提供的子域,因此这个名称必须是唯一的。默认名称将根据当前日期和时间基于项目名称附加一个数字。如果您希望更改此名称,但指定的值不是唯一的,您将看到一个警告,指出该名称不可用,您必须选择另一个名称。

  4. 如果您的帐户链接了多个订阅,请选择要为此应用程序使用的订阅。

  5. 选择或创建新的资源组和托管计划。出于演示目的,您现在可以使用免费托管计划。如果您的应用程序的需求意味着这是不够的,您可以在将来更改这一点。

重要提示

当您从免费试用转为生产应用程序时,非常重要的是,您充分了解为您的 Web 应用程序配置的选项和与计费相关的选择。这将避免您的信用卡出现任何意外费用,或者在信用用完时禁用关键应用程序。适合您和您的应用程序的适当设置将取决于您的应用程序和个人要求。有关计费选项的详细信息可以在以下 URL 找到:azure.microsoft.com/pricing/details/app-service/windows/

  1. 单击创建按钮,服务将为您创建。这可能需要几秒钟,屏幕角落将显示一条消息,说明正在进行此操作。

  2. 现在您将看到类似于图 8.2的东西。这显示了我使用了名称UnoBookRailDashboard,因此该应用程序将在以下 URL 上可用:unobookraildashboard.azurewebsites.net/。现在单击完成,应用程序将准备好进行部署:图 8.2 - Azure 发布对话框准备发布应用程序

图 8.2 - Azure 发布对话框准备发布应用程序

  1. 现在,您已经设置好了您的 Web 应用程序,准备发布应用程序。单击窗口右上角的发布按钮。

可能需要一两分钟,但最终,浏览器将打开一个新标签页,其中包含从 Azure 运行的应用程序。这应该看起来类似于图 8.3

图 8.3 - 在 Azure 上运行的仪表板应用程序

图 8.3 - 在 Azure 上运行的仪表板应用程序

如果您没有在 Azure 上托管您的应用程序,您可以通过搜索如何部署Blazor应用程序来找到有用的指导,因为该过程可能是类似的。最终,基于 Uno 平台的 WebAssembly 应用程序只是静态文件,可以部署到任何能够托管静态内容的服务器上。

从 Visual Studio 发布很方便。但是,从创建一个被跟踪的、可重复的过程来看,这并不理想。理想情况下,您应该设置一个自动化过程来部署您的应用程序。接下来我们将看一下持续集成和部署CI/CD)过程。

自动构建、测试和分发

理想情况下,您将使用自动化过程来构建、测试和部署您的应用程序,而不是依赖手动完成所有这些,因为手动过程更容易出错。

这就是 CI/CD 过程至关重要的地方。因为我们刚刚手动将 Wasm 应用程序部署到了 Azure,让我们从自动化这个过程开始。幸运的是,Visual Studio 工具使这变得简单。

如果您浏览配置了工作流程的YAML文件:

图 8.4 - 创建 GitHub 操作以通过发布向导发布您的 Wasm 应用程序

图 8.4 - 创建 GitHub 操作以通过发布向导发布您的 Wasm 应用程序

生成的文件只需要进行单个修改,以适应 Uno 平台模板使用的解决方案结构。工作目录需要更改为Dashboard\Dashboard.Wasm

一旦您进行了任何更改并将其推送到 GitHub,代码将自动构建和部署。

您可以在以下 URL 看到一个 GitHub Actions 工作流文件的示例,该文件部署了一个基于 Uno 平台的 Wasm 应用程序:github.com/mrlacey/UnoWasmGithubActions/blob/main/.github/workflows/UnoWasmGithubActions.yml

GitHub 不是您可能存储代码的唯一位置,GitHub Actions 也不是唯一的 CI/CD 流水线选项。对于使用.NET 的开发人员,Azure DevOps(以前称为 Visual Studio Online)是一个流行的解决方案。

Nick Randolph 创建了一个全面的指南,介绍了如何为 Uno 平台应用程序创建基于 Azure DevOps 的构建流水线,网址如下:nicksnettravels.builttoroam.com/uno-complete-pipeline/

Lance McCarthy 还创建了一个示例存储库,展示了在 GitHub 托管的存储库中使用多个 Azure DevOps 构建流水线。如果您需要进行类似操作,这可以作为一个有用的参考,并且可以在以下 URL 找到:github.com/LanceMcCarthy/UnoPlatformDevOps

由于 Uno 平台允许您创建多种平台,并且您可以以多种方式构建和部署这些应用程序,因此不可能提供所有场景的操作指南。幸运的是,由于 Uno 平台是建立在其他知名技术之上的,因此构建这些其他技术的过程也与构建基于 Uno 平台的应用程序时使用的过程相同。例如,因为 Android、iOS 和 macOS 应用程序是基于 Xamarin 构建的,所以构建和部署过程可能与直接使用 Xamarin 构建时相同。

我们通过查看部署使用 Uno 构建的应用程序的 Wasm 版本开始了 CI/CD 的这一部分。这不是您可能需要部署应用程序的唯一位置。应用商店是您可能需要部署您构建的一些应用程序的地方,因此我们将在下一步中查看它们。

将您的应用程序部署到商店

假设您正在为公共使用构建应用程序。在这种情况下,您可能需要通过该操作系统的适当应用商店部署它。商店为使用 Uno 平台构建的应用程序所适用的规则、政策和限制与使用任何其他工具集构建的应用程序相同。

每个商店的政策可能会经常更改(通常每年至少几次),而且也相当长。因此,我们认为在这里重复它们没有价值。相反,您应该查看以下列表中的官方文档:

)

Uno 基于应用程序的分发过程与任何其他应用程序相同。您需要为希望通过部署的每个商店创建开发人员帐户,然后根据需要将相关文件、软件包和捆绑包上传到商店。

由于 Android、iOS 和 Mac 应用程序都是构建在特定平台的 Xamarin 技术之上,您可能会发现它们与发布相关的文档也很有用:

前面的链接指向每个商店的一般信息。如果您遇到任何特定的与 Uno 相关的问题,有一个庞大的社区准备好帮助您。

与 Uno Platform 社区互动

Uno Platform 作为一个开源项目的吸引力之一。与许多开源项目一样,一个核心团队帮助领导贡献者社区。正是这个广泛的社区,您可以寻求信息、帮助并成为其中的一部分。

信息来源

除了这本书(显然!),获取信息的中心地带是官方网站,网址如下:platform.uno/。在网站上,您会找到文档、指南、示例和博客。订阅博客是跟上所有未来公告的绝佳方式,还可以关注官方的 Twitter 账号,网址如下:twitter.com/unoplatform

官方网站还包括超出本书范围的主题的信息,例如使用 Uno Platform 来针对 Windows 7 或 Linux(参见platform.uno/uno-platform-for-linux/)。

官方网站充满了信息,但是在您的应用程序中可能有很多功能和事情,您会遇到需要回答的问题。

帮助来源

有四个地方可以寻求与 Uno Platform 相关的帮助:

  • Stack Overflow

  • Discord

  • GitHub

  • 专业支持

Stack Overflow是互联网上与软件开发相关的问题和答案的存储库。这是您关于如何使用 Uno Platform 的问题的第一个求助地点。您会发现许多核心团队和常规贡献者在那里回答问题。确保您的问题标记有uno-platform,并在以下网址提问:stackoverflow.com/questions/tagged/uno-platform

如何寻求帮助

与大多数事物一样,您付出的努力越多,您得到的回报就越多。这也适用于寻求帮助。如果您不熟悉它,Stack Overflow 有一个关于如何提问问题的指南,网址如下:stackoverflow.com/help/how-to-ask

有两个请求帮助的一般原则。首先,记住您是在寻求帮助,而不是让别人替您做工作。其次,让别人帮助您变得更容易,这增加了他们能够和愿意这样做的可能性。

一个很好的求助请求包括提供回答所需的所有必要具体信息,而不包括其他信息。对问题的模糊描述或您的代码不如提供您尝试过的细节或简单的最小化重现问题的方式有帮助。

如果你的问题涉及 Uno 平台的内部,或者你正在使用最新的预览版本,最好在Discord上提问。UWP 社区服务器有一个uno-platform频道,包括许多热情的社区成员和核心团队成员。你可以通过以下网址加入:discord.com/invite/eBHZSKG

使用 Uno 平台,就像任何开源项目一样,都需要一定程度的责任感。开源软件是一个集体过程,每个人都在努力使每个人都拥有更好的软件。这意味着你应该报告一个 bug,即使你自己无法修复它。如果你认为在平台、示例或文档中发现了 bug,你应该在 GitHub 的以下网址上提交问题:github.com/unoplatform/uno/issues/new/choose。与请求帮助一样,你应该提供尽可能多的适当信息,包括重现问题的最小方式,以便于找到并修复你发现的问题。务必提供所有请求的信息,这有助于快速解决问题,避免浪费精力,或者需要人们再次要求更多信息。

最后,如果你需要及时解决问题,或者你有比在 Stack Overflow 或 Discord 上处理的更深层次的支持需求,Uno 平台背后的公司也提供专业付费支持。请访问platform.uno/contact讨论你的需求。

贡献

有一个普遍的误解,即对开源项目的贡献意味着添加代码,但与任何软件项目一样,使其成功和有价值的工作远不止于代码。当然,如果你想帮助贡献代码,你将受到热烈欢迎。首先看看标记为good first issue的问题,并查看以下网址的贡献指南:platform.uno/docs/articles/uno-development/contributing-intro.html。但请记住,还有很多其他事情可以做。

这是一个陈词滥调,但事实证明,无论大小,一切都有所帮助。分享你的经验是你可以做的最简单但也最有价值的事情之一。这可以是提供正式的操作指南或代码示例。或者,它可能只是回答某人想知道你已经做过的事情的问题。

无论大小,我们都期待看到你的贡献。

总结

在本章中,我们已经看到了各种领域,以补充你对 Uno 平台的介绍。你已经看到了 Uno 平台如何扩展现有的Xamarin.Forms应用程序,使其可以通过 WebAssembly 运行。你看到了如何将你的应用程序的 Wasm 版本部署到 Azure。我们看了持续集成和部署。你知道了去哪里进一步学习,我们看了你如何与 Uno 平台开发者社区互动。

通过这本书,我们已经来到了尽头。如果你已经逐章阅读,现在你将拥有知识和信心,可以使用 Uno 平台构建在多个操作系统上运行的应用程序。我们期待看到你的创作。

感谢阅读!

posted @ 2024-05-17 17:51  绝不原创的飞龙  阅读(96)  评论(0编辑  收藏  举报