WinUI3-学习指南第二版-全-
WinUI3 学习指南第二版(全)
原文:
zh.annas-archive.org/md5/d75ac684336a7c803b35abcac34ddcac译者:飞龙
前言
WinUI 3 是 Windows 应用程序开发的最新桌面 UI 框架。它是微软的 Windows App SDK 的一部分,为开发者提供了使用 Fluent 设计系统构建美观应用程序的工具。本书将迅速帮助您熟悉 WinUI,以构建新的 Windows 应用程序,并使用 Blazor 和 Uno Platform 等技术构建跨平台的应用程序。
本书首先探讨了 Windows UI 开发框架的历史,以了解早期框架如何影响了今天存在的 WinUI。它涵盖了基于 XAML 的 UI 开发的基本知识,并探讨了 WinUI 中可用的控件,然后转向对 WinUI 开发者的模式和最佳实践的考察。为了帮助巩固这些概念,本书的前几章通过创建一个用于组织书籍、音乐和电影集合的应用程序来培养实际技能。每一章都会增强应用程序,讨论新的控件和概念。
书中的后几章探讨了开发者如何利用他们的 WinUI 知识来利用开源工具包,在 Windows 应用程序中集成网络内容,并将 WinUI 应用程序迁移到 Android、iOS 和 Web。本书最后教授了一些 Visual Studio 调试技术,并讨论了应用程序部署选项,以便将您的应用程序带给消费者和企业用户。在每一章的结尾,我都包含了一系列问题,供您自己尝试,以便评估您的理解程度。了解 WinUI 如何帮助您构建和部署现代、健壮的应用程序!
本书面向对象
本书适用于任何希望使用现代用户体验(UX)开发 Windows 应用程序的人。如果您熟悉 Windows Forms、UWP 或 WPF,并希望更新您的 Windows 开发知识或现代化现有应用程序,这本书就是为您准备的。如果您刚开始学习.NET 开发,您可以利用这本书在学习 C#和.NET 的过程中学习 XAML 开发的基础知识。
本书涵盖内容
第一章, WinUI 简介,考察了 Windows 中 UI 框架的历史和 WinUI 的起源,您将在 Visual Studio 中创建您的第一个 WinUI 3 项目。
第二章, 配置开发环境和创建项目,解释了如何安装和配置 Visual Studio 以进行 WinUI 开发,XAML 和 C#的基本知识,并通过一个将在本书中增强的项目开始了动手实践。
第三章, 可维护性和可测试性的 MVVM 模式,介绍了模型-视图-视图模型(MVVM)模式的基本知识,这是构建基于 XAML 的应用程序时最重要的设计模式之一。
第四章, 高级 MVVM 概念,在 WinUI 应用程序中学习到的 MVVM 模式基础知识的基础上,介绍了更高级的技术。您将学习如何在项目中添加新的依赖项时保持组件松散耦合和可测试性。
第五章, 探索 WinUI 控件,探讨了 WinUI 为构建 Windows 应用程序的开发者提供的许多控件和 API。本章探讨了 WinUI 2 和 UWP 中之前可用的全新控件和更新控件。
第六章, 利用数据和服务的优势,探讨了管理数据,这是软件开发的核心部分。本章涵盖了数据管理的一些关键概念,包括状态管理和服务定位器模式。
第七章, Windows 应用程序的 Fluent 设计系统,解释了微软的 Fluent 设计系统的原则以及如何在 WinUI 应用程序中实现它们。
第八章, 将 Windows 通知添加到 WinUI 应用程序中,介绍了如何利用 Windows App SDK 在 WinUI 应用程序中支持推送通知和应用程序通知。
第九章, 使用 Windows 社区工具包增强应用程序,介绍了 Windows 社区工具包和.NET 社区工具包——为 Windows 开发者提供的开源库集合。您将学习如何在 WinUI 项目中利用工具包中的控件和辅助工具。
第十章, 使用模板工作室加速应用程序开发,展示了如何利用模板工作室创建一个新的 WinUI 项目,这是一个可能令人望而却步的任务,但基于最佳的 Windows 开发模式和最佳实践。
第十一章, 使用 Visual Studio 调试 WinUI 应用程序,展示了如何利用 Visual Studio 中的 XAML 调试工具追踪 WinUI 项目中的讨厌的 bug——良好的调试技能对于开发者至关重要。
第十二章, 在 WinUI 中托管 Blazor 应用程序,探讨了 WinUI 中的 WebView2 控件,以及如何使用它来托管部署到云中的 Blazor 应用程序,从您的 Windows 应用程序内部进行托管。
第十三章, 使用 Uno Platform 将您的应用程序扩展到跨平台,解释了如何将 WinUI 项目迁移到 Uno Platform,这允许开发者在一个代码库中编写 XAML 和 C#代码,并在任何平台上运行。
第十四章, 打包和部署 WinUI 应用程序,探讨了 WinUI 开发者用于打包和部署 WinUI 应用程序的多种选项,包括通过 Microsoft Store、WinGet 和侧载应用程序进行部署。
为了充分利用本书
如果您熟悉 Windows Forms、.NET MAUI、UWP 或 WPF,并希望提高您对 Windows 开发的知识或现代化现有应用程序,本书将很有用。预期您具备 C# 和 .NET 的实践经验,但不需要对 WinUI 有先前的了解。
| 本书涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| WinUI 3 | Windows 10 版本 1809 或更高版本或 Windows 11 |
| C# | Windows、macOS 或 Linux |
| .NET 7 | Windows、macOS 或 Linux |
| Visual Studio 2022 | Windows 10 或 11 |
| Blazor | Windows、macOS 或 Linux |
| Uno Platform | Windows、macOS 或 Linux |
本书介绍了如何开始使用 WinUI 开发,但你应该安装 Visual Studio 和 .NET。请按照 Microsoft Learn: learn.microsoft.com/visualstudio/install/install-visual-studio* 的说明操作。*
如果您使用的是本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。
阅读本书后,您可以通过深入了解 Microsoft Learn: learn.microsoft.com/windows/apps/* 上的文档和示例来继续您的 Windows 开发之旅。*
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件 github.com/PacktPublishing/Learn-WinUI-3-Second-Edition。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有来自我们丰富的图书和视频目录的其他代码包,可在 github.com/PacktPublishing/ 获取。查看它们!
使用的约定
本书使用了多种文本约定。
文本中的代码: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“在 INavigationService 中,您可以更新 namespace 为 UnoMediaCollection.Interfaces 并删除 using System; 语句。”
代码块应如下设置:
using UnoMediaCollection.Enums;
using UnoMediaCollection.Interfaces;
using UnoMediaCollection.Model;
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
using UnoMediaCollection.Enums;
namespace UnoMediaCollection.Model
任何命令行输入或输出都应如下编写:
$ mkdir css
$ cd css
粗体: 表示新术语、重要单词或屏幕上显示的单词。例如,菜单或对话框中的单词会以粗体显示。以下是一个示例:“右键单击枚举文件夹,然后选择添加 | 现有项。”
小贴士或重要注意事项
看起来是这样的。
联系我们
欢迎读者反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过 customercare@packtpub.com 发送电子邮件,并在邮件主题中提及书名。
勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在此书中发现错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问 www.packtpub.com/support/errata 并填写表格。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
分享您的想法
一旦您阅读了《Power Apps 自动化测试》,我们非常乐意听到您的想法!请点击此处直接转到此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买此书!
您喜欢在路上阅读,但无法携带您的印刷书籍到处走吗?
您的电子书购买是否与您选择的设备不兼容?
不要担心,现在,每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,您还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件。
按照以下简单步骤获取优惠:
- 扫描下面的二维码或访问以下链接

packt.link/free-ebook/9781805120063
-
提交您的购买证明
-
就这些了!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件。
第一部分:WinUI 和 Windows 应用程序简介
WinUI 3 是微软为 Windows 开发者提供的最新 UI 框架。本节将从探索 XAML 和 Windows UI 框架的近期历史开始,向您介绍 WinUI。在本节的章节中,您将通过从头开始构建一个简单的项目并添加控件和功能,遵循设计模式和最佳实践来学习 WinUI 概念。这些模式和最佳实践包括模型-视图-视图模型(MVVM)设计模式、构建松散耦合、可测试的 C#类,以及使用依赖注入(DI)将服务依赖项注入到应用程序组件中。
本部分包含以下章节:
-
第一章, WinUI 简介
-
第二章, 配置开发环境和创建项目
-
第三章, MVVM 的维护性和可测试性
-
第四章, 高级 MVVM 概念
-
第五章, 探索 WinUI 控件
-
第六章, 利用数据和服务
第一章:WinUI 简介
WinUI 3是一套用户界面(UI)控件和库,Windows 开发者可以在他们的桌面应用程序中使用。它是 Windows App SDK 的 UI 部分,之前被称为Project Reunion。UWP 开发者使用Windows 软件开发工具包(Windows SDK)来构建他们的应用程序,并在项目属性中必须选择一个目标 SDK 版本。通过从 Windows SDK 中提取 UWP 控件和 UI 组件,为.NET 使用重写它们,并以 WinUI 的名称在Windows App SDK中作为一系列库发布,微软能够以比 Windows 本身更快的节奏发布版本(因为 Windows SDK 版本与 Windows 版本相关联)。这种分离还使得控件可以在较旧的 Windows 10 版本上使用。虽然使用 WinUI 构建桌面应用程序是当前的建议,但了解 WinUI 和 Windows App SDK 在更大的 Windows 开发领域中的位置是很重要的。
在这本书中,你将学习如何使用 WinUI 3 库为 Windows 构建应用程序。在整个书籍的过程中,我们将使用推荐的 Windows 应用程序开发模式和最佳实践构建一个真实世界的应用程序。
在我们开始构建 WinUI 应用程序之前,了解 Windows 客户端开发、不同类型的可扩展应用程序标记语言(XAML)UI 标记以及 WinUI 与其他 Windows 桌面开发框架的比较是很重要的。因此,在本章的第一部分,你将开始学习有关 UWP 和 WinUI 的一些背景知识。
在本章中,我们将学习以下主题:
-
UWP 是什么以及为什么微软又创建了一个新的应用程序框架
-
如何利用 XAML 在多种设备和设备系列上创建出色的 UI
-
为什么创建 WinUI 以及它与 UWP 的关系
-
WinUI 在 Windows 开发者领域中的位置
-
WinUI 3 带来了什么
别担心!覆盖背景知识不会花费很长时间,这将在你开始构建 WinUI 应用程序时提供一些上下文。在下一章中,当你创建你的第一个 WinUI 项目时,你将有机会接触一些代码。
技术要求
要跟随本章中的示例,需要以下软件:
-
Windows 10 版本 1809 或更高版本或 Windows 11。您可以在设置 | 关于中找到您的 Windows 版本。
-
需要安装 Visual Studio 2022 版本 17.0 或更高版本,并包含以下工作负载:.NET 桌面开发。在Visual Studio 安装程序的安装详情选项卡中,确保已选择Windows App SDK C#模板。
本章的源代码可在 GitHub 上通过以下网址获取:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/main/Chapter01。
注意
Microsoft Learn 上的 Windows App SDK 网站提供了关于设置 WinUI 3 开发开发工作站的最新指南:learn.microsoft.com/windows/apps/windows-app-sdk/set-up-your-development-environment。
在 UWP 之前 – Windows 8 XAML 应用程序
在 2015 年 Windows 10 发布之前,Windows 8 和 8.1 有 XAML 应用程序。XAML 语法以及许多应用程序编程接口(API)是相同的,并且这是微软实现跨桌面、移动和其他平台(Xbox、混合现实等)通用应用程序开发的下一步。可以为 Windows 8 和 Windows Phone 编写 XAML 应用程序。这些项目会生成可以在 PC 或 Windows Phone 上安装的独立二进制文件集。
这些应用程序还有许多其他限制,而现代 UWP 应用则没有。例如,它们只能全屏运行,如下面的截图所示:

图 1.1 – Windows 8 全屏应用(来源:Stack Overflow;根据 CC BY-SA 4.0 复制 – https://creativecommons.org/licenses/by-sa/4.0/)
在 UWP 应用程序开发中,许多对 Windows 8 应用的早期限制已经减少或完全取消。下述图 1.2记录了这些变化:

图 1.2 – Windows 8 和 Windows 10 应用比较表
Windows 应用程序 UI 设计
术语Metro 风格用于定义 Windows 8 应用的设计和布局。Metro 风格应用程序旨在支持触摸输入、鼠标和键盘或触控笔。微软推出首款 Windows Phone 是推动 Metro 风格设计的一个关键因素。随着 Surface 设备的推出,Metro 风格后来演变为现代 UI 设计。Metro 的某些方面至今仍存在于 UWP 应用和 Windows 10 中。
Live Tiles 与 Metro 风格一同诞生。这些在用户 Windows 8 主屏幕和 Windows 10 开始菜单上的瓷砖可以更新以显示实时更新,而无需打开应用程序。大多数微软自己的 Windows 10 应用程序都支持 Live Tiles。天气应用程序可以根据用户的当前位置在瓷砖上显示当前的天气状况更新。在 Windows 11 中,Live Tiles 已不再是操作系统的组成部分。它们已被小部件取代,应用程序开发者也可以创建小部件。我们将在第五章“探索 WinUI 控件”中进一步讨论小部件。
Windows 运行时(WinRT)
另一个根植于 Windows 8 应用程序开发的术语是 WinRT。RT 字母引起了很多混淆。WinRT 是 Windows Runtime 的缩写,它是 Windows XAML 应用使用的底层 API。还有一个名为 Windows RT 的 Windows 8 版本,它支持 Arm 处理器。第一台 Surface PC 是 Surface RT,它运行 Windows 8 RT 操作系统。
尽管 WinRT 今天仍然可以用来定义 UWP 应用消耗的 WinRT API,但你不会经常看到这个术语。我们也将避免在这本书中使用 WinRT,而是将 API 称为 UWP 或 Windows API。
用户反弹和通往 Windows 10 的道路
当微软努力通过现代 UI 设计、新的应用模型、Surface PC 以及 Windows 8 和 8.1 来赢得用户时,全屏、以触摸优先的应用体验和淡化 Windows 桌面的想法从未被客户接受。事实证明,Windows 用户真的很喜欢他们多年来在 Windows XP 和 Windows 7 中使用的开始菜单体验。
Windows 应用程序开发的下一步是一个很大的步骤——实际上,微软决定在版本号中跳过一个数字,直接从 Windows 8.1 跳到 Windows 10。
Windows 10 和 UWP 应用程序开发
在推出 Windows 10 的同时,微软也融合了之前 Windows 版本中最好的功能。它恢复了开始菜单,但其内容看起来非常像 Windows 8 的主页体验。除了所有已安装应用的字母顺序列表外,还有一个可调整大小的应用磁贴区域。实际上,当在平板模式运行 Windows 时,开始菜单可以转换为 Windows 8 风格的主页体验,以更好地适应触摸屏的使用。
当微软推出 Windows 10 时,它还向 Windows 开发者引入了 UWP 应用。虽然 UWP 应用根植于 Windows 8 的 XAML 应用,但一些关键差异为开发者构建平台应用提供了重大优势。
这些应用的一个关键优势是它们的通用性。微软为不同的设备系列构建了 Windows 10 的版本,如下所示:
-
桌面(PC)
-
Xbox
-
移动(Windows Phone)
-
HoloLens
-
IoT
-
IoT 头部无
-
团队(Surface Hub)
UWP 开发者可以构建应用以针对这些设备中的任何一个。所有这些目标共享一个单一的基础 Windows API 集合,并为一些系列的特定设备 API 提供专门的 SDK,例如,有一个用于 HoloLens 开发的混合现实工具包和 SDK。使用 UWP,可以创建一个单一的项目以针对许多设备系列——例如,你可以创建一个为桌面、Xbox 和团队系列创建应用的项目。
因为用于构建应用程序 UI 的 UWP XAML 是相同的,所以跨设备开发的难度降低,代码重用性非常高。XAML 的本质提供了 UI 灵活性,以适应不同的设备尺寸和宽高比。
使用 UWP 开发的语言选择
虽然底层的 UWP API 是用 C++ 编写的,但 UWP 开发者在为 Windows 构建应用程序时可以选择多种编程语言。可以使用以下任何流行语言创建 UWP 项目:
-
C#
-
C++
-
F#
-
Visual Basic .NET (VB.NET)
-
JavaScript
你可能会惊讶地看到 JavaScript 在列表中。在 Windows 8.x 时代,开发者可以使用名为 WinJS 应用的 API 创建 JavaScript 应用程序。今天,Microsoft 为 Windows 开发者创建了一个名为 React Native for Windows 的分支,称为 React Native for Windows。这些 JavaScript 客户端应用程序可以完全访问与其他 UWP 应用程序相同的 Windows API,并且可以通过 Windows Store 打包和部署。
注意
React Native for Windows 是一个开源项目,由 Microsoft 在 GitHub 上托管,网址为 github.com/Microsoft/react-native-windows。
虽然微软为 Windows 10 和 Windows 11 开发的许多 UWP 应用程序是用 C++ 创建的,但大多数其他开发者选择 C#。在本书的整个过程中,我们也将使用 C# 构建我们的应用程序。
提升应用程序限制
如前所述,为 Windows 8 构建的应用程序有几个限制,这些限制已被移除或放宽,使用 UWP。
首先,现代 UWP 应用程序可以在可调整大小的窗口中运行,就像任何其他 Windows 桌面应用程序一样。权衡是开发者现在需要测试并处理应用程序几乎任何大小的调整。XAML 的动态特性可以很好地处理很多调整,但低于一定最小尺寸,将需要使用滚动条。
对于最终用户来说,使用 UWP 应用程序的一个好处是它们提供的固有安全性,这是由于应用程序对 PC 文件系统的访问受限。默认情况下,每个应用程序只能访问其自己的本地存储。在 2018 年,Windows 开发团队宣布为 UWP 开发者推出一项新功能。通过添加一些应用程序配置,声明应用程序需要哪些额外的访问类型,应用程序可以请求访问文件系统的其他部分。其中以下是一些:
-
用户库,包括文档、图片、音乐和视频
-
下载
-
可移动设备
注意
可以请求额外的文件系统权限。有关完整列表,请参阅 Microsoft Learn 文档:learn.microsoft.com/windows/uwp/files/file-access-permissions。
请求的任何附加权限都将声明在 Microsoft Store 中的应用程序列表中。
现在 Windows 上的 UWP 应用可以访问一些不太常见的场景。开发者可以向应用添加一些配置和启动代码,以启用其应用的多个实例启动。虽然许多人认为 UWP 应用的特点是 XAML UI,但也可以创建 UWP 控制台应用。该应用在命令行中运行,并可以访问通用 C 运行时调用。这些现在不再受支持,因为开发者现在可以创建.NET 控制台应用并将它们打包为 MSIX,以在 Windows 中提供包标识。
注意
我们将在第十四章,打包和部署 WinUI 应用中详细讨论应用打包、MSIX 和包标识。
UWP 向后兼容性
任何 UWP 应用都与 Windows 10 之前的任何版本不兼容。除此之外,每个 UWP 应用都必须声明与其兼容的目标版本和最低版本的 Windows。目标版本是您推荐版本,它将启用应用的所有特性和功能。最低版本是,不出所料,用户必须拥有的最低 Windows 版本,才能从 Microsoft Store 安装应用。
在创建新的 UWP 项目时,Visual Studio 将提示您选择这些版本。如果两者相同,则会使事情变得简单。您将拥有该 SDK 版本的所有 API 可用。如果目标版本高于最低版本,则需要添加一些条件代码来启用高于最低版本的任何版本的特性。应用必须对运行最低版本的用户仍然有用;否则,建议提高最低版本。如果任何新的 API 或控件对应用至关重要,也建议将最低版本提高到这些 API 或控件可用的版本。
注意
有关编写条件或版本自适应代码的更多信息,请参阅以下 Microsoft Learn 文档:learn.microsoft.com/windows/uwp/debug-test-perf/version-adaptive-code.
如果您正在创建将被您的 UWP 项目引用的.NET 库,并且希望将它们跨平台共享,例如通过.NET MAUI 移动应用,则应针对共享库项目设置.NET Standard 版本。目前最常用的.NET Standard 版本是.NET Standard 2.0。要从 UWP 项目引用.NET Standard 2.0 项目,UWP 项目的目标版本应为 16299 或更高。
与 UWP 相比,WinUI 的主要优势在于它减少了 Windows 应用对特定 Windows 版本的依赖。相反,控件、样式和 API 在 Windows SDK 之外维护。在撰写本文时,WinUI 3 应用所需的最低和目标版本必须设置为 17763 或更高。请查看最新的 WinUI 3 文档以获取当前最低要求。
对于 WinUI 来说,随着项目的成熟,希望将更多的控件和功能带到更多支持的 Windows 版本中。
XAML 是什么?
XAML基于可扩展标记语言(XML)。这似乎是一件好事,因为 XML 是一种灵活的标记语言,大多数开发者都很熟悉。它确实灵活且强大,但它也有一些缺点。
微软实现 XAML 的主要问题是多年来为不同的开发平台创建了如此多的 XAML 语言变体。目前,WinUI/UWP、Windows Presentation Foundation (WPF)和.NET MAUI(以前称为 Xamarin.Forms)应用程序都使用 XAML 作为它们的 UI 标记语言,此外还有一些第三方 UI 框架。然而,这些中的每一个都使用不同的 XAML 实现或模式,并且标记无法跨平台共享。在过去,Windows 8、Silverlight 和 Windows Phone 应用程序也有额外的。
如果你以前从未使用过 XAML,你可能准备好查看一些 UI 标记的示例。以下 XAML 是一个片段,定义了包含其他几个基本 WinUI 控件的Grid(你可以从这里下载本章的代码:github.com/PacktPublishing/-Learn-WinUI-3-second-edition/tree/master/Chapter01):
<Grid Width="400" Height="250" Padding="2"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
Text=»Name:»
Margin=»0,0,2,0»
VerticalAlignment="Center"/>
<TextBox Grid.Row="0" Grid.Column="1"
Text=»»/>
<Button Grid.Row="1" Grid.Column="1" Margin="0,4,0,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Content=»Submit»/>
</Grid>
让我们在这里分解 XAML。WinUI 窗口的顶层是Window。WinUI 3 应用程序的导航是基于窗口的(与基于页面的 UWP 不同),初始导航发生在项目中的App.xaml文件中。你将在第四章中了解更多关于导航的内容,高级 MVVM 概念。一个Window必须只包含一个子元素,并且它将是某种类型的布局面板,如Grid或StackPanel。默认情况下,会插入一个StackPanel作为该子元素。我们将在下一章讨论其他类型的好父容器面板。我对它做了一些修改,并将StackPanel替换为Grid。
Height和Width属性为示例提供了静态大小,而HorizontalAlignment和VerticalAlignment属性将Grid在Window中居中。在 XAML 的此级别,固定大小并不常见,这限制了布局的灵活性,但它们展示了可用的属性。
Grid是一种布局面板,允许开发者定义行和列来排列其元素。行和列可以定义为固定大小、相对于彼此的大小,或者根据其内容自动调整大小。有关更多信息,您可以阅读 Microsoft Learn 文章使用 XAML 进行响应式布局:learn.microsoft.com/windows/uwp/design/layout/layouts-with-xaml。
Grid.RowDefinitions块定义了网格的行数和行为。我们的网格将有两个行。第一个的Height="Auto",这意味着它将根据其内容调整大小,前提是有足够的空间。第二行的Height="*",这意味着网格的剩余垂直空间将分配给这一行。如果有多个行以这种方式定义了高度,它们将平均分配可用空间。我们将在下一章中讨论更多的尺寸选项。
Grid.ColumnDefinitions块为网格的列做了RowDefinitions为行所做的事情。我们的网格定义了两个列。第一个ColumnDefinition的Height设置为Auto,第二个的Height="*"。
TextBlock在第一个Grid.Row和Grid.Column中定义了一个标签。当使用 XAML 时,所有索引都是从0开始的。在这种情况下,第一行和列的位置都是0。Text属性方便地定义了要显示的文本,而在这个例子中,VerticalAlignment将文本垂直居中。TextBlock的默认VerticalAlignment是Top。Margin属性为控件的外围添加了一些填充。具有所有边相同填充量的边距可以设置为一个单一的数值。在我们的例子中,我们只想在控件的右侧添加几个像素,以将其与TextBox分开。输入这些数值的格式是"<LEFT>,<TOP>,<RIGHT>,<BOTTOM>",或者在这里是"0,0,2,0"。
TextBox是在网格第一行的第二列中定义的文本输入字段。
最后,我们在网格的第二行的第二列中添加了一个Button控件。为了将其与上面的控件分开,添加了几像素的上边距。VerticalAlignment设置为Top(默认是Center),HorizontalAlignment设置为Right(默认是Center)。要设置Button的文本,你不会像TextBlock那样使用Text属性,正如你可能认为的那样。实际上,没有Text属性。这里使用的是Button的Content属性。Content是一个特殊属性,我们将在下一章中更详细地讨论。现在,只需知道Content属性可以包含任何其他控件:文本、Image,甚至是一个包含多个其他子控件的Grid控件。可能性几乎是无限的。
这里是前面标记生成的 UI:

图 1.3 – WinUI XAML 渲染
这是一个非常简单的例子,让你先尝尝使用 XAML 可以创建什么。随着我们继续前进,你将了解到这种语言是多么强大。
为任何设备创建自适应 UI
在前面的例子中,Grid有固定的Height和Width属性。我提到设置固定大小可能会限制 UI 的灵活性。让我们移除固定大小属性,并使用对齐属性来引导 UI 元素以不同的尺寸和宽高比渲染,如下所示:
<Grid VerticalAlignment="Top" HorizontalAlignment="Stretch" Padding="2">
标记的其余部分保持不变。结果是TextBox调整大小以适应窗口的宽度,而Button在调整大小时保持在窗口的右侧。请看以下几种调整窗口大小的不同方式:

图 1.4 – 调整大小的窗口
如果你在一个较小的 PC 上使用这个应用,比如 Surface Go 笔记本电脑,内容会自动调整大小以适应可用空间。这就是 XAML 自适应特性的力量。在构建 UI 时,你通常会想要选择相对和自适应属性,比如对固定大小和位置的定位。
正是这种自适应布局使得 XAML 在.NET MAUI 的移动设备上工作得如此出色,这也是为什么 WPF 开发者自从 Windows Vista 发布以来就喜欢使用它的原因。
强大的数据绑定
另一个原因,为什么基于 XAML 的框架如此受欢迎,就是它们数据绑定功能的简便性和强大。几乎 WinUI 控件上的所有属性都可以进行数据绑定。数据源可以是数据源上的一个对象或对象列表。在大多数情况下,这个源将是一个ViewModel类。让我们快速看一下如何使用 WinUI 的Binding语法将数据绑定到ViewModel类上的一个属性,如下所示:
-
首先,我们将创建一个简单的
MainViewModel类,它有一个Name属性,如下所示:public class MainViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private string _name; public MainViewModel() { _name = "Bob Jones"; } public string Name { get { return _name; } set { if (_name == value) return; _name = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); } } }MainViewModel类实现了一个名为INotifyPropertyChanged的接口。这个接口是 UI 在数据绑定属性更改时接收更新的关键。这个接口的实现通常被ViewModelBase类包装。现在,我们将直接在Name属性的 setter 中调用PropertyChanged事件。我们将在第三章中了解更多关于ViewModels和INotifyPropertyChanged接口的信息,可维护性和可测试性的 MVVM。 -
下一步是创建
MainViewModel类的一个实例,并将其设置为MainWindow的ViewModel。这发生在页面的代码后文件MainWindow.xaml.cs中,如下面的代码片段所示:public sealed partial class MainWindow : Window { public MainWindow() { this.InitializeComponent(); ViewModel = new MainViewModel(); } public MainViewModel ViewModel { get; private set; } }我们在
MainWindow中添加了一个ViewModel属性,并在构造函数中将其设置为MainViewModel类的新实例。
小贴士
任何添加到窗口构造函数中并与任何 UI 元素交互的代码都必须在调用InitializeComponent()之后添加。
-
现在是时候将数据绑定代码添加到
TextBox的 XAML 标记中,如下所示:<TextBox Grid.Row="0" Grid.Column="1" Text="{x:Bind Path=ViewModel.Name, Mode=TwoWay}"/>添加了一些标记来使用
x:Bind标记扩展设置Text属性。数据绑定的Path设置为ViewModel上的Name属性,该属性在代码隐藏文件中的步骤 2中分配。通过将数据绑定模式设置为TwoWay,ViewModel中的更新将在 UI 中显示,并且用户在 UI 中的任何更新也将持久保存在MainViewModel类的Name属性中。现在,运行应用程序将自动填充在ViewModel构造函数中设置的名称,如下面的截图所示:

图 1.5 – 数据绑定 TextBox
-
为了说明将数据绑定到页面上另一个 UI 元素的另一个属性,我们首先修改网格以添加一个名称,如下所示:
<Grid x:Name="ParentGrid" VerticalAlignment="Top" HorizontalAlignment="Stretch" Padding="2"> -
现在向
Grid添加另一个RowDefinition以适应页面上的新 UI 元素:<Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> -
接下来,添加一个
TextBlock元素,并使用Binding标记扩展将其Text属性绑定到设置为ParentGrid的ElementName的ActualWidth。我们还添加了一个TextBlock来标记它为实际宽度:<TextBlock Grid.Row="1" Grid.Column="0" Text="Actual Width:" Margin="0,0,2,0" VerticalAlignment="Center"/> <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding ElementName=ParentGrid, Path=ActualWidth}"/> -
接下来,更新
Grid.Row2。 -
现在新的
TextBlock控件在页面加载时显示ParentGrid的宽度。请注意,如果你调整窗口大小,它不会更新值。ActualWidth属性不会引发属性更改通知。这在FrameworkElement.ActualWidth文档中有记录:learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.frameworkelement.actualwidth:

图 1.6 – 将数据绑定到另一个元素
提交按钮目前还没有功能。你将在第五章“探索 WinUI 控件”中学习如何使用 MVVM 与事件和命令一起工作。
使用 XAML 对 UI 进行样式设计
当使用 XAML 时,可以在几乎任何范围内定义和应用样式,从全局的App.xaml应用到当前Window中的Window.Resources声明,或者在任何级别或嵌套控件上。Style元素指定了一个TargetType属性,它是要由样式针对的元素的数据类型。它可以可选地定义一个Key属性,作为唯一标识符,就像在Key属性中可以使用的类标识符一样,可以用来仅将样式应用于该类型的选定元素。与 CSS 类不同,一个元素只能分配一个Key属性。
在下一个示例中,我们将修改页面以定义一个用于窗口上所有按钮的Style属性,如下所示:
-
首先,将
StackPanel元素移动。StackPanel元素以水平或垂直方向堆叠所有子元素,其中垂直是默认方向。一些按钮的属性需要移动到StackPanel元素,因为现在它是Grid的直接子元素。在StackPanel元素中添加第二个按钮以充当StackPanel之后,StackPanel和Button元素应如下所示:<StackPanel Grid.Row="2" Grid.Column="1" Margin="0,4,0,0" HorizontalAlignment="Right" VerticalAlignment="Top" Orientation="Horizontal"> <Button Content="Submit" Margin="0,0,4,0"/> <Button Content="Cancel"/> </StackPanel>已为第一个按钮添加了一个新的
Margin属性,以在元素之间添加一些空间。 -
接下来,我们将在
Grid的嵌套Grid.Resources部分中添加一个Style块,在所有控件之前对其进行样式化。由于未为Style块分配Key,它将应用于所有没有在内部作用域中覆盖样式的Button元素。这被称为 隐式样式。此代码如下所示:<Grid.Resources> <Style TargetType="Button"> <Setter Property="BorderThickness" Value="2" /> <Setter Property="Foreground" Value="LightGray" /> <Setter Property="BorderBrush" Value="GhostWhite"/> <Setter Property="Background" Value="DarkBlue" /> </Style> </Grid.Resources> -
现在,当你运行应用程序时,你会看到新的样式已应用于 提交 和 取消 按钮,而无需直接为每个控件添加任何样式,如下面的截图所示:

图 1.7 – 样式化按钮
如果我们将 Style 块移动到 App.xaml 中的 Application.Resources 部分,则定义的样式将应用于整个应用程序中的每个按钮,除非开发人员已单独覆盖了样式中的某些属性。例如,如果将 Background 属性设置为 DarkGreen,则只有 取消 按钮会显示为深蓝色。
我们将在第七章“Windows 应用程序的流畅设计系统”中花费更多的时间来讨论样式和设计。
将展示逻辑与业务逻辑分离
在早期关于数据绑定的章节中,我们简要地讨论了 MVVM 模式。MVVM 是 WinUI 应用程序开发中展示逻辑与业务逻辑分离的关键。XAML 元素只需要知道在其数据上下文中某个地方存在一个特定名称的属性。ViewModel 类对 View(我们的 XAML 文件)没有任何了解。
这种分离提供了几个好处。首先,ViewModels 可以独立于 UI 进行测试。如果系统在测试中引用了任何 WinUI 元素,则需要 UI 线程。这将在本地或 CI 服务器上运行时导致测试失败。有关 WinUI 应用程序单元测试的更多信息,请参阅这篇 Microsoft 博客文章:devblogs.microsoft.com/ifdef-windows/winui-desktop-unit-tests/。
View/ViewModel分离的下一个好处是,对于拥有专用ViewModels的企业来说。当需要同步这两个组件时,开发者可以向 XAML 添加必要的数据绑定属性,或者 UX 设计师和开发者可能已经就共享数据上下文中的属性名称达成一致。Visual Studio 包含另一个针对设计师的工具,称为Blend for Visual Studio。Blend 最初由微软在 2006 年作为 Microsoft Expression Blend 发布,作为设计师创建 WPF UI 的工具。后来增加了对其他 XAML 语言的支持,如 Silverlight 和 UWP。当安装 Visual Studio 时,Blend 仍然包含在.NET 桌面开发工作负载中。
我们在这里要讨论的最后一个好处是,在应用程序的任何层之间良好的关注点分离将始终导致更好的可维护性。如果有多个组件参与单个责任,或者逻辑在多个地方重复,这会导致有缺陷的代码和不可靠的应用程序。遵循良好的设计模式,你将节省自己在未来大量的工作。
现在你已经很好地理解了 UWP 应用程序的历史,是时候看看 WinUI 了:它是什么,以及为什么它被创建。
什么是 WinUI?
WinUI 库是从 Windows SDK 中提取的一组控件和 UI 组件,并包含在 Windows App SDK 中。在此分离之后,许多控件得到了增强,其他控件也被添加。Windows App SDK 正在公开开发。其问题在 GitHub 上跟踪,并得到微软和 Windows 开发者社区的支持。
因此,如果你这些 WinUI 库是基于 Windows SDK 中的 UWP 库,你可能想知道为什么你应该选择 WinUI 作为你的 UI 框架而不是 UWP。UWP 自 Windows 10 发布以来一直存在,并且非常健壮和稳定。有多个非常好的理由考虑 WinUI。
选择 WinUI 带来了许多开源软件(OSS)的好处。OSS 通常非常可靠。当软件由活跃的开发者社区在公开环境中开发时,问题会被发现并迅速解决。实际上,如果你发现开源包中的问题,你可以自己修复它,并通过提交拉取请求将修复提供给整个社区。开源项目可以快速迭代,而无需与大型企业中的产品组(如 Windows 团队)保持同步。现在,Windows 定期发布功能更新,但这仍然比典型的控件库要少。尽管 Windows App SDK 和 WinUI 3 还不是开源的,但它仍然是产品路线图的一部分。
使用 WinUI 的最佳理由是其向后兼容性。当使用 UWP 控件时,特定版本的控件中的功能和修复无法部署到较旧版本的 Windows 应用程序中。使用 WinUI,只要你的目标是 WinUI 整体支持的最低 Windows 版本,你就可以在多个 Windows 版本中使用那些新控件和功能。在某个 Windows 版本中,UWP 开发者之前无法使用的控件现在作为 WinUI 控件可用。
例如,微软直到 2017 年秋季发布(版本 16299)才将 Fluent UI 设计引入 Windows。然而,WinUI 控件可以包含在针对最低 Windows 版本为 Windows 10 版本 1809 的应用程序中,即 2019 年秋季发布。WinUI 中的控件支持 Fluent UI 风格。WinUI 添加了 UWP 和 Windows SDK 中完全不可用的控件和其他功能。
WinUI 的第一个版本
WinUI 的第一个版本于 2018 年 7 月发布,作为 Windows 开发者的预览版。它作为以下两个 NuGet 包发布:
-
Microsoft.UI.Xaml:WinUI 控件和 Fluent UI 风格 -
Microsoft.UI.Xaml.Core.Direct:中间件开发者访问XamlDirectAPI 的组件
3 个月后,WinUI 2.0 发布。尽管版本号如此,但它却是 WinUI 的第一个生产版本。该版本包括超过 20 个控件和画笔。一些值得注意的控件包括以下内容:
-
TreeView:任何 UI 库的必备品。 -
ColorPicker:一个具有颜色光谱的丰富视觉颜色选择器。 -
DropDownButton:一个可以打开菜单的按钮。 -
PersonPicture:用于显示头像的图像控件。它可以将显示首字母或通用占位符图像。 -
RatingControl:允许用户为项目输入星级评分。
注意
WinUI 2.x 版本是 UWP 项目使用的库。WinUI 3 是 Windows App SDK 的一部分,并且是其自己的项目类型,尽管它与 UWP 项目共享相同的 XAML 架构。
让我们在我们的 WinUI 项目中添加一些这些控件,看看它们的外观。将 StackPanel 的内容更改为如下所示:
<StackPanel Grid.Row="1" Grid.Column="1" Margin="0,4,0,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Orientation="Horizontal">
<PersonPicture Initials="MS" Margin="0,0,8,0"/>
<DropDownButton Content="Submit" Margin="0,0,4,0">
<DropDownButton.Flyout>
<MenuFlyout Placement="Bottom">
<MenuFlyoutItem Text="Submit + Print"/>
<MenuFlyoutItem Text="Submit + Email"/>
</MenuFlyout>
</DropDownButton.Flyout>
</DropDownButton>
<Button Content="Cancel"/>
</StackPanel>
在 StackPanel 的第一个位置添加了一个带有首字母 MS 的 PersonPicture 控件,两个按钮中的第一个已被 DropDownButton 控件替换。DropDownButton 控件有一个作为下拉列表的 FlyoutMenu,并且有两个 MenuFlyoutMenuItem 元素。现在,用户可以简单地点击显示的下拉菜单:

图 1.8 – 添加 PersonPicture 和 DropDownButton 控件
我们只是刚刚触及 WinUI 为 Windows 开发者所能做的事情的表面。不用担心,我们将在接下来的章节中深入探讨。在 WinUI 3 之前,让我们简要看看后续版本中添加了什么。
WinUI 3 的道路
在 2.0 版本之后,WinUI 还发布了五个额外的次要版本,以及许多增量错误修复和预发布版本。
WinUI 2.1
WinUI 2.1 版本发布带来了几个新的控件和功能,以下是亮点:
-
TeachingTip:将TeachingTip视为一个丰富、上下文相关的工具提示。它与页面上的另一个元素相关联,并显示有关目标元素的详细信息,以帮助用户通过非侵入性内容进行引导。 -
AnimatedVisualPlayer:这个控件用于托管 Lottie 动画。Lottie 文件是由设计师在Adobe After Effects中创建的流行动画格式,被 Windows、网页和移动平台的设计师所使用。现在大多数现代开发框架都有可用的库来托管 Lottie 动画。
注意
在他们的网站上获取有关 Lottie 文件的更多信息:airbnb.design/lottie/,并查看这个 Lottie 动画文件的优秀存储库:lottiefiles.com/。
CompactDensity:将此资源字典添加到您的应用程序中可以提供在紧凑和正常显示模式之间切换的能力。CompactDensity将减少页面内和页面之间的间距,为用户提供多达 33%的更多可见内容。这个 Fluent UI 设计概念是在微软 Build 2018 大会上向开发者介绍的。
WinUI 2.2
本版本对现有功能带来了许多增强。然而,库中添加的唯一新控件是许多 Windows 开发者可能会发现有用的控件。
TabView控件在屏幕上创建了一个熟悉的标签式用户体验。每个标签可以托管 WinUI 项目中的一个页面。
WinUI 2.2 增强功能
在这里列出了版本 2.2 中一些值得注意的更新控件和库:
-
NavigationView控件得到了增强,允许在面板折叠时保持返回按钮可见。其他视觉更新最大化了控件的可视内容。 -
CornerRadius、BorderThickness、CheckBox和RadioButton。这些更新使 WinUI 视觉元素更加一致,并与 Fluent UI 设计指南保持一致。
WinUI 2.3
在 WinUI 2.3 版本中,ProgressBar控件进行了一些更新,并添加了几个新的控件到库中。
在 WinUI 应用程序中创建ProgressBar元素时,现在有两种可用模式:确定和不确定。确定进度条有一个已知任务完成量和当前任务状态。不确定控件表示任务正在进行中,但没有已知完成时间。它的目的是像忙碌指示器一样。
WinUI 2.3 中的新控件
本更新中包含了一些新的控件:
-
NumberBox:NumberBox控件是一个输入编辑器,它使得支持数字格式化、上下增减按钮和内联数学计算变得容易。它看似简单但实用且功能强大。 -
RadioButtons:你可能认为,“单选按钮一直都有。这怎么算是一个新控件呢?”RadioButtons是一个将一组RadioButton(单数)控件组合在一起的控件,使得作为单个单元处理它们变得更加容易。
WinUI 2.4
当它在 2020 年 5 月发布时,WinUI 2.4 提供了两个新功能:一个 RadialGradientBrush 视觉效果和一个 ProgressRing 控件。
该画笔在用法上与 WPF 开发者使用的 RadialGradientBrush 类似。它使得向从中心点辐射出的视觉元素添加渐变变得容易。
如其名所示,ProgressRing 控件以圆形格式重新创建了进度条功能。在版本 2.4 中,该控件提供了确定状态和不确定状态。不确定的 ProgressRing 控件显示重复动画,并且是控件的默认状态。
在版本 2.4 中更新了几个控件。TabView 控件得到了更新,以提供更多控制选项来渲染标签页,包括 TextBox 控件获得了 深色模式 增强,以保持控件内容区域为深色,默认为白色文本。最后,NavigationView 控件更新为支持分层导航,包括 Left、Top 和 LeftCompact 模式。
WinUI 2.5
WinUI 2.5 于 2020 年 12 月发布,并包含了一个新的 InfoBar 控件。该版本还包含了多个控件增强和错误修复。
InfoBar 控件提供了一种向用户显示重要状态消息的方式。该控件可以显示警报或信息图标、状态消息以及允许用户对消息采取行动的链接或按钮。还有一个选项在消息右侧显示关闭按钮。默认情况下,该控件包括图标、消息和关闭按钮。Microsoft Learn 为此新控件提供了使用指南。这是 WinUI 3 版本控件的文档:learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.infobar。
版本 2.5 中也提供了一些更新。ProgressRing 控件在确定状态方面得到了增强。NavigationView 控件更新为提供可定制的 FooterMenuItems。在 NavigationView 控件的先前版本中,页脚区域可以显示或隐藏,但不能进行自定义。
WinUI 2.x 在版本 2.6、2.7 和 2.8 中继续添加控件和功能。有关每个版本中新增功能的完整列表,请参阅以下 Microsoft Learn 上的发布说明页面:
)
)
)
我们已经看到了 WinUI 2 中为 UWP 开发者提供的内容。现在,让我们看看 WinUI 3 和 Windows App SDK 能给你带来什么。
WinUI 3 的新特性是什么?
与 WinUI 2.0 及其后续的增量版本不同,WinUI 3 是一个重大更新,它不仅提供了更多的新控件和库,用于与 Windows 桌面应用一起使用。实际上,WinUI 3 的主要目标并不是在其当前的 UWP 对应版本之外添加新的控件和功能。Windows App SDK 团队已经将 WinUI 打造成为一个完整的 UI 框架,它可以建立在 Windows 桌面.NET 平台之上。
再见,UWP?
那么,UWP 会发生什么变化?我们的 UWP 应用会停止工作吗?
如前所述,UWP UI 库的计划是继续提供重要的安全更新,但它们将不会获得任何新的功能。很可能 WinUI 2.8 将是最终的 2.x 版本。所有新的功能和更新都将为 WinUI 和 Windows App SDK 开发。新应用将使用 WinUI 开发,可以是.NET,用 C#或 VB 编写,或者使用原生 C++。这些客户端将建立在 Win32 平台之上。这一切都是因为 Windows App SDK 完全用 C++开发的。
由于它是用 C++开发的,这使得Windows 的 React Native 客户端应用能够与 Windows App SDK 平台进行交互。在 React Native 和 Uno Platform 之间,WinUI 具有很大的跨平台潜力。
开发者将为 Windows PC 和平板设备创建应用提供多条路径。其他 Windows 设备,如 Xbox 和 HoloLens,需要继续开发 UWP 应用并使用 WinUI 2.x 控件。
WinUI 3 和 Windows App SDK 的新特性
WinUI 3 有哪些新特性?
虽然听起来团队正忙于创建一个用于替换 UWP UI 库的 UI 框架,但他们确实抽出时间添加了一些新功能。WinUI 3 中可用的主要新控件是新的WebView2控件。它是一个基于新 Chromium 内核的Microsoft Edge浏览器的网页浏览器宿主控件。兼容性也是一个特性。所有在 2019 年春季 Windows SDK 中可用的 XAML 和 Composition 功能都将向后兼容,回溯到 Windows 10 1809 更新及以后版本。
Windows App SDK 和 WinUI
WinUI 3 正在将桌面应用程序开发者聚集在单一的 UI 库集合中,但这只是开始。在微软的 Build 2020 大会上,Windows 团队宣布了Project Reunion,这是一个长期计划,旨在将所有 Windows 开发者聚集在单一平台上。当 WinUI 在 2021 年发布时,Project Reunion 被更名为 Windows App SDK。WinUI 3 专注于 UI 层,而 Windows App SDK 将包括 WinUI 和整个 Windows 开发者平台。2021 年,微软发布了三个版本的 Windows App SDK 和 WinUI 3。
想了解更多关于 Windows App SDK 及其进展,您可以查看团队在 GitHub 上的仓库github.com/microsoft/WindowsAppSDK。现在,让我们看看 WinUI 与其他 Windows 开发框架相比如何。
WinUI 3 与其他 Windows 开发框架的比较
WinUI 在微软的 Windows 开发框架的整体格局中处于什么位置?让我们通过一些比较来帮助回答这个问题,从与 WinUI 最相似的那些开始。
WinUI 与 UWP
这是一个棘手的比较,因为 WinUI 应用程序今天与 UWP 应用程序共享相同的 XAML 架构。事实上,WinUI 2.x 是 UWP 应用程序的控件。它们共享相同的 XAML 架构、基础视觉元素和底层 Windows API。任何指定了相同的最小和目标 Windows 版本的 UWP 应用程序都可以添加 WinUI 2.x 库以利用新的和更新的功能。然而,UWP 将不会收到 WinUI 2.8 之后的任何功能更新。只有作为 2.8x 次要版本的补丁和安全更新将会发布。
使用 WinUI 的应用程序与传统 UWP 应用程序之间的一个关键区别是,无需更新 Windows SDK 即可访问新的和更新的控件以及其他视觉元素。这使得开发者能够将具有相同外观和功能的软件带给更多使用 Windows 10 或 Windows 11 多个版本的用户。这种差异化使得开发者和用户都更加满意。
WinUI 3 也有使用最新.NET 版本和 C#语言特性的优势。随着.NET 新版本的发布,它将继续从中受益。作为真正的.NET 桌面应用程序,它们不受 UWP 沙盒的限制。它们可以完全访问硬件和文件系统,并可以使用大多数 API。WinUI 3 应用程序比 UWP 应用程序对它们的窗口大小和外观有更多的控制。然而,想要针对 HoloLens 或 Xbox 等平台进行开发的开发者必须坚持使用 UWP 开发。
WinUI 与 WPF
WinUI 和 WPF 有许多相似之处。它们都是应用程序框架,这两种类型的应用程序都依赖于 XAML 来定义 UI 元素。这意味着在实现 MVVM 模式时,它们都提供了相同的 UI 和业务逻辑分离。WPF XAML 具有与 UI 布局相同的样式、资源、数据绑定和自适应的概念。
WinUI 优势
WinUI 的一个显著性能优势是 XAML 中 x:Bind 语法的使用,而不是 Binding。
除非您的 WinUI 应用程序使用 uap10:TrustLevel="appContainer" 进行 MSIX 打包,否则 WinUI 和 WPF 都可以完全访问用户的文件系统和使用设备。它们的访问仅受 PC 上 Windows 用户账户控制 (UAC) 的配置限制。WinUI 有使用 GPU 加速功能(如 Mica 和 Acrylic 毛刷)的优势,以支持最新的 Windows 风格,例如微软的内置应用程序。这些样式对 WPF 应用程序不可用,使它们感觉不那么现代。
WPF 优势
WPF 应用程序的主要优势是它们并不直接绑定到 Windows 的最低版本。WPF 应用程序针对 .NET 版本。任何支持目标 .NET 版本的 Windows 版本都可以运行该 WPF 应用程序。这显著增加了 WPF 应用程序的可能用户基础。实际上,WPF 应用程序可以在带有 .NET Framework 的 Windows 7 上部署和运行,这是 UWP 或 WinUI 所无法实现的。
注意
有一个名为 Uno Platform 的产品,它使 WinUI XAML 能够在 iOS、Android、macOS、Linux 以及三星 Tizen 手表上运行,还可以在带有 WebAssembly 的网络上运行。这些 WinUI 网络应用程序可以在包括 Windows 7 在内的旧版 Windows 版本的浏览器中运行。Uno Platform 的目标和口号是 WinUI 到处都是。
在 platform.uno/ 了解更多关于 Uno Platform 的信息。我们将在 第十三章 “使用 Uno Platform 将您的应用程序跨平台化”中创建一个 Uno Platform 项目。
在 webassembly.org/ 了解更多关于 WebAssembly 的信息。
新的 WPF 优势随着 .NET Core 3.x 和 .NET 5 以及后续版本的发布而出现。.NET 开发者现在可以使用 .NET Core 创建 WPF 应用程序,将现代 .NET 的性能和部署优势带给 WPF 开发者。例如,针对不同 .NET 版本的应用程序可以在不创建版本冲突的情况下在同一台机器上并行部署。然而,正如之前提到的,WinUI 3 应用程序也利用了最新的 .NET 功能和性能。
关于部署模型的差异,可以争论哪个框架具有优势。部署 WinUI 应用程序最简单的方法是通过 Microsoft Store。使用 .NET 部署 WPF 应用程序的最简单方法是使用安装程序包。WPF 应用程序可以通过添加 Windows MSIX 打包项目通过 Store 进行部署,而 WinUI 应用程序可以使用 MSIX 安装程序或 Windows 包管理器 在没有 Store 的情况下进行部署。WinUI 部署将在 第十四章 “打包和部署 WinUI 应用程序”中详细说明。
WinUI 与 Windows Forms (WinForms)
WinForms 是随着.NET Framework 1.0 一起引入的.NET UI 框架。开发者可以使用 Visual Studio 中的可视化设计表面轻松创建 WinForms UI,该表面会生成在运行时创建 UI 的 C#或 VB 代码。WPF 的大部分优势和劣势也适用于 WinForms:安全性、部署和.NET——WinForms 应用程序也可以使用.NET Core 3.x 及更高版本创建。
WinUI 优势
WinUI 与 WPF 之间的相似之处在于它们相对于 WinForms 的主要优势:数据绑定、自适应布局和灵活的样式模型。这些优势都源于对 XAML 用于 UI 布局的使用。XAML 的另一个优势是将渲染处理从中央处理器(CPU)卸载到图形处理器(GPU)。WinUI 控件默认继承 Windows 样式,比 WinForms 控件看起来更现代。WinUI 应用程序也很好地处理每英寸点数(DPI)缩放和触摸输入。WinForms UI 框架在触摸输入和 DPI 缩放成为 Windows 开发者关注的问题之前就已经成熟。本地化和 UI 性能也是 WinUI 3 相对于 WinForms 的巨大优势。
WinForms 优势
除了 WinForms 与 WinUI 共享的 Windows、.NET 应用程序和 Windows 兼容性等优势之外,WinForms 还因其快速 UI 开发而享有良好的声誉。如果你需要在最短的时间内创建一个简单的 Windows 应用程序,WinForms 的拖放式设计器既简单又直观。WinForms 最近也进行了一些更新,以改进其数据绑定支持,现在它还支持 MVVM 的 ICommand。有关这些改进的更多信息,请参阅这篇.NET 博客文章:devblogs.microsoft.com/dotnet/winforms-cross-platform-dotnet-maui-command-binding/.
许多经验丰富的 Windows 开发者仍然在需要为.NET 库创建简单的实用程序或 UI 测试框架时默认选择 WinForms。
摘要
在本章中,我们介绍了 Windows 应用程序开发的历史。我们了解了 UWP 的起源及其与 Windows 8 应用程序的渊源,并学习了在构建 Windows UI 时 XAML 的优势。我们还体验了一些简单的 WinUI 应用程序代码和 UI 的样貌。最后,我们考察了 WinUI 版本的近期历史,以及 WinUI 3 如何完全替代 UWP UI 库,并成为 WPF 开发者未来可行的选择。
这将为我们在接下来的章节中开始使用 WinUI 构建应用提供一个良好的基础。在下一章中,你将设置你的开发环境,了解我们将贯穿整本书创建的应用程序项目,并创建你的第一个 WinUI 3 项目。当我们到达第三章,可维护性和可测试性的 MVVM时,我们将重构应用以使用 MVVM 模式。这将为我们提供一个稳固、可维护的设计,以便我们在整本书的其余部分添加和扩展应用。
问题
-
哪个版本的 Windows 首次向开发者引入了 UWP 应用?
-
WinUI 和其他 XAML 开发者通常使用哪种模式来分离 UI 逻辑和业务逻辑?
-
WinUI 和 WPF 应用可以共享相同的 XAML。对还是错?
-
哪个是第一个使用 XAML 来定义 UI 的 Microsoft UI 框架?
-
首次 WinUI 发布的版本号是多少?
-
使用 WinUI 而不是 WinForms 进行开发有哪些好处?
-
WinUI 应用只能使用.NET 语言开发吗?
-
挑战:创建一个将应用于
Button元素的样式。
第二章:配置开发环境并创建项目
要开始使用 WinUI 和 Windows App SDK 进行开发,安装和配置 Visual Studio 以进行 Windows 桌面开发非常重要。WinUI 开发者还必须了解使用 可扩展应用程序标记语言(XAML)和 C# 进行应用程序开发的基础,这些内容我们在 第一章 WinUI 简介 中开始学习。然而,理解开发概念的最佳方式是亲自动手处理一个真实的项目。我们将在本章中这样做。
在设置您的 Visual Studio 开发环境后,您将创建本书其余部分将构建的项目的基础。
本章中,您将学习以下主题:
-
如何为 Windows 桌面应用程序开发设置新的 Visual Studio 安装
-
如何创建新的 WinUI 项目,添加一些控件,并首次运行项目
-
新的 WinUI 项目的结构及其每个部分的重要性
-
如何使用 XAML 构建灵活、高效的 用户界面(UI)
-
WinUI 如何与 .NET 结合以及每个层在整体应用程序架构中的作用
-
如何使用 WinUI 控件并通过更改 XAML 标记或 C# 代码来自定义它们
-
如何处理一些基本的 UI 事件
如果您是 WinUI 和其他基于 XAML 的开发平台的初学者,到本章结束时,您应该开始感到舒适地使用 WinUI 项目。
技术要求
要跟随本章中的示例,需要以下软件:
-
Windows 10 版本 1809(构建 17763)或更高版本或 Windows 11
-
安装 Visual Studio 2022 版本 17.1 或更高版本,并在安装过程中选择 .NET 桌面开发 工作负载和 Windows App SDK C# 模板
本章的源代码可在 GitHub 上通过此 URL 获取:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/main/Chapter02。
安装 Visual Studio 和 Windows 桌面开发工作负载
开始 WinUI 开发时需要遵循的第一个步骤是安装微软的 Visual Studio 集成开发环境(IDE)。您可以从 visualstudio.microsoft.com/downloads/ 下载 Visual Studio 2022 的当前版本。Visual Studio 2022 社区版对个人用户免费,并包含您构建 WinUI 应用程序所需的所有功能。
小贴士
如果您想在它们发布之前尝试新的 Visual Studio 功能,您可以从 visualstudio.microsoft.com/vs/preview/ 安装最新的 Visual Studio 预览版本。预览版本不建议用于生产应用程序的开发,因为一些功能可能不稳定。
在安装过程中,您可以选择创建任何类型应用程序的工作负载。对于 WinUI 开发,您必须首先选择.NET 桌面开发工作负载。以下截图显示了工作负载部分的概述:

图 2.1 – Visual Studio 安装程序工作负载选择
在选择.NET 桌面开发工作负载后,从安装详情面板中选择Windows App SDK C# 模板组件。这些模板,用于构建 WinUI 应用程序,默认情况下不会与工作负载一起安装。

图 2.2 – 选择安装 Windows App SDK C# 模板组件
注意
如果您是 C++开发者并且想使用 C++构建 WinUI 应用程序,您必须选择使用 C++进行桌面开发工作负载,并在使用 C++进行桌面开发工作负载内选择可选的Windows App SDK C++ 模板组件。然而,使用 C++构建 WinUI 应用程序超出了本书的范围。
在继续到下一步时,Visual Studio 安装程序将下载并安装所有选定的工作负载和组件。设置完成后,启动 Visual Studio。第一次运行 Visual Studio 时,您将被提示使用 Microsoft 账户登录。将 Visual Studio 链接到您的账户将使 Visual Studio 能够同步您的设置,链接任何可用的许可证,并在创建后链接您的 Microsoft Store 账户。我们将在第十四章 打包和部署 WinUI 应用程序 中讨论更多关于 Microsoft Store 和应用程序分发的内容。
现在,是时候开始构建我们的 WinUI 应用程序了。
介绍应用程序想法
我们将要构建的应用程序是一个名为我的媒体收藏的工具。这是一个简单的实用程序,可以编目您的整个媒体库。由于随着时间的推移,数字媒体变得越来越受欢迎,我们可以设计应用程序以支持包含实体和数字媒体。应用程序将能够编目不同类型的媒体,包括音乐、视频和书籍。我们将添加一些仅对特定媒体类型生效的功能。实体媒体(书籍、DVD 和 CD)通常会被借给朋友。这个应用程序将帮助您记住在最近的家庭聚会上谁借走了您最喜欢的书。
检查应用程序功能
在我们深入创建新项目之前,让我们先做好组织工作。了解您将要构建的内容,这样您可以在通过每一章进行时跟踪您的进度。如果您在 GitHub 上跟踪您的开发,您可以为每个功能创建一个问题。让我们从对应用程序功能的高级概述开始,如下所示:
-
查看所有媒体
-
根据以下条件过滤媒体库:媒体类型(音乐、视频或书籍)、介质(根据媒体类型的不同,可用的选项将有所不同,但将包括CD、唱片、DVD、蓝光、精装本、平装本、数字)或位置(在收藏中或已借出)
-
添加新的媒体项目
-
编辑媒体项目
-
标记项目为已借出或已归还
-
应用程序登录
-
使用 OneDrive 备份(或还原)集合数据
应用程序将使用Windows Community Toolkit的功能,这些功能可以简化诸如 Microsoft 帐户身份验证和 OneDrive 上的文件访问等问题。您可以在 Microsoft Learn 上了解更多关于工具包的信息:learn.microsoft.com/dotnet/communitytoolkit/windows/。应用程序的数据将存储在本地SQLite数据库中,允许在线或离线访问媒体集合。您甚至可以发送电子邮件提醒,如果您的朋友在归还您收藏品中的某件物品时花费了太多时间。
桌面 WinUI 项目
在本章中,我们将使用桌面 WinUI项目模板构建一个 Windows App SDK 应用程序。一个桌面 WinUI项目针对 .NET 运行时,同时使用与 UWP 项目相同的 XAML 架构。
桌面 WinUI项目还包括一个在新建解决方案中的Windows 应用程序打包项目。我们将在第十四章“打包和部署 WinUI 应用程序”中了解更多关于打包 WinUI 应用程序的内容。现在,让我们开始我们的第一个项目。
创建您的第一个 WinUI 项目
是时候开始构建项目了。为此,请按照以下步骤操作:
- 启动 Visual Studio,然后从打开屏幕中选择创建新项目,如图所示:

图 2.3 – 初始 Visual Studio 对话框
- 在搜索模板字段中的
winui,选择空白应用,打包(桌面 WinUI 3)C#模板,然后点击下一步,如图所示:

图 2.4 – 选择项目模板
提示
请确保选择C#项目模板,而不是C++。您可以通过将语言过滤器从所有语言更改为C#来过滤项目类型,以仅显示 C# 项目。
- 将项目命名为
MyMediaCollection,其余字段保留默认值,然后点击创建。您可能需要选择要针对您的应用程序的 Windows 版本。对于此项目,您可以保留这些设置为默认值。
注意
是否要更改这些版本以适应您的应用程序由您决定,但您将限制可以安装您应用程序的 Windows 版本。如果您使用的是仅在特定 Windows 版本中可用的控件或功能,您必须选择该版本作为最低版本。目标版本必须等于或高于最低版本。如果您不确定选择什么,您应该坚持使用默认值。
- 现在,项目已创建且 Visual Studio 已加载,始终是一个最佳实践来构建和运行项目。运行应用程序并查看模板为
MainWindow提供了什么。您应该看到一个空窗口托管了一些控件。接下来,我们将看到 Visual Studio 为我们创建的以帮助我们开始的内容。
注意
要在 Windows 上运行和调试 WinUI 应用程序,您必须更新您的 Windows 设置以启用开发者模式。为此,请按照以下步骤操作:
-
从开始菜单打开设置。
-
在搜索栏中输入
Developer并从搜索结果中选择开发者设置。 -
在出现的开发者页面,如果尚未开启,切换开发者模式开关。启用此功能允许开发者侧载、运行和调试未签名的应用程序,并启用一些其他面向开发者的 Windows 设置。您可以在以下链接中获取更多信息:
learn.microsoft.com/windows/apps/get-started/enable-your-device-for-development。
桌面项目中 WinUI 的结构
现在,我们已经将一个新的空 WinUI 项目加载到 Visual Studio 中,让我们检查不同的组件。在 App.xaml 和 MainWindow.xaml 中。我们将首先讨论每个文件的用途。这两个文件可以在以下 Solution Explorer 的屏幕截图中看到:

图 2.5 – Solution Explorer 中的新 WinUI 解决方案
检查 App.xaml
如其名称所示,App.xaml 文件存储了整个应用程序可用的资源。如果您有任何需要在多个窗口中使用的模板或样式,它们应该添加到 Application 级别。
新项目的 App.xaml 文件将包含一些初始标记,如下面的代码片段所示:
<Application
x:Class="MyMediaCollection.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/
presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyMediaCollection">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources
xmlns="using:Microsoft.UI
.Xaml.Controls" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>
</Application.Resources>
</Application>
在下一节中,我们将介绍一些 XAML 基础知识。目前,你应该知道 Application.Resources 部分将包含跨应用程序共享的所有资源。在这个部分中,ResourceDictionary.MergedDictionaries 部分包含了对其他 XAML 文件的引用,这些文件中的资源将与整个应用程序共享。这允许开发者将共享内容组织到多个资源文件中,从而使得 XAML 更有组织性和易于维护。这也使得应用程序可以共享第三方资源。例如,此文件合并了来自 Microsoft.UI.Xaml.Controls 命名空间的 XamlControlsResources。这些是我们将用于构建应用程序的 WinUI 控件的资源。
检查 App.xaml.cs 文件
在 App.xaml 文件的节点中,你会看到还有一个名为 App.xaml.cs 的文件嵌套在其中。这被称为 Application 类。如果你打开 C# 文件,你会看到它已经包含了一些代码。这就是你处理任何应用程序级事件的地方。这是默认添加的事件处理程序:OnLaunched。如果你需要在应用程序首次启动时执行任何特定逻辑,它应该在这里添加。这也是处理传递给应用程序的任何应用程序参数的地方。
审查 MainWindow.xaml
MainWindow.xaml 文件包含将在应用程序启动时显示的 MainWindow WinUI 窗口。你可以在 App.xaml.cs 中的 OnLaunched 事件处理程序中看到这一点,如下面的代码片段所示:
m_window = new MainWindow();
m_window.Activate();
在一个新的空白 WinUI 应用程序中,Window 将包含一个空的 StackPanel 布局控件。尝试用具有 Text 属性为 Media 的 TextBlock 替换 StackPanel 的子控件,并用 Grid 替换 StackPanel。结果应该看起来像这样:
<Window
x:Class="MyMediaCollection.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/
presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyMediaCollection"
xmlns:d="http://schemas.microsoft.com/expression/blend/
2008"
xmlns:mc="http://schemas.openxmlformats.org/
markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<TextBlock Text="Media"/>
</Grid>
</Page>
为了防止运行时出现异常,请从 MainWindow.xaml.cs 中移除 Button.Click 事件处理程序。现在使用 调试 | 开始调试 菜单项或 Visual Studio 工具栏上的 开始调试 按钮运行应用程序,你应该会看到如下内容:

图 2.6 – 添加了 TextBlock 的 MainWindow
在本章的后面部分,我们将使这个窗口功能更强大。现在,让我们完成对项目结构的审查。
审查 MainWindow.xaml.cs
MainWindow.xaml.cs 窗口的代码后文件现在在构造函数中只包含对 InitializeComponent() 的调用,因为我们移除了原始 Button 控件的事件处理程序。稍后,我们将在这里添加一些代码来填充一些示例数据并处理页面上的事件。在一个设计良好的 MVVM 应用程序中,你的页面代码后文件将包含非常少的代码。大部分代码将位于 ViewModel 类中。
审查项目引用
在 解决方案资源管理器 的 依赖项 下的 Packages 文件夹中,你项目的所有引用。你的 WinUI 项目将引用以下 NuGet 包:
-
Microsoft.WindowsAppSDK:包括 WinUI 的 Windows App SDK 组件 -
Microsoft.Windows.SDK.BuildTools:构建 WinUI 解决方案所需的 Windows SDK 组件
注意
不要修改或删除这些引用。
查看项目属性
如果你右键单击 解决方案资源管理器 中的项目并从上下文菜单中选择 属性,你可以查看和修改项目属性。你不会经常需要在此处进行任何更改。以下是一些你可能需要不时修改的属性:
-
程序集名称:你可以更改由项目编译的输出程序集的名称
-
最小版本 和 目标版本:创建项目时选择的 Windows 版本可以在此修改
新的 UWP 项目和新的 WinUI 3 项目的区别主要在于在 Windows.UI.Xaml.* 命名空间中引用的控件和其他对象现在引用了 Windows App SDK 的 Microsoft.UI.Xaml.* 命名空间中的控件。另一个区别是,在 UWP 项目的 App.xaml 文件中,不需要导入控件的资源。其余的区别主要在项目文件中对应用程序开发者隐藏。
现在你已经熟悉了 WinUI 项目,让我们开始构建 MainWindow 的 UI。我们将从一些常见的 XAML 控件和概念开始。
XAML 基础
是时候开始构建 My Media Collection 应用程序的主屏幕了。应用程序的焦点将是集合中的媒体项目。为了显示这个列表,我们需要一些东西,如下所示:
-
一个定义集合中项目的
Model类 -
一些代码用于将项目集合绑定到 UI
-
一个用于显示项目的 XAML 控件
构建模型
我们将首先为 My Media Collection 应用程序构建模型。一个 模型 定义了一个实体及其属性。在本章的早期部分,我们讨论了一些我们希望在 UI 中显示的项目属性。为了显示(最终)持久化这些信息,我们必须创建模型。
我们模型的初始版本将包括一个 Enums 文件夹中的两个枚举(ItemType 和 LocationType),以及一个 Model 文件夹中的两个类(Medium 和 MediaItem),如下面的截图所示:

图 2.7 – 解决方案资源管理器显示新的模型和枚举文件
-
要向项目中添加新文件夹,右键单击
Enums作为文件夹名称,然后按 Enter。重复此过程以添加Model文件夹。 -
接下来,右键单击项目文件并选择
ItemType,然后点击下面的 添加 按钮,如下面的截图所示:

图 2.8 – 添加新项对话框
-
ItemType的代码已创建并显示。将class定义更改为enum,并将internal关键字更改为public。ItemType枚举包含三个可能值,分别命名为Music、Video和Book。当您完成时,ItemType枚举的定义将如下所示:public enum ItemType { Music, Video, Book } -
重复步骤创建一个
LocationType枚举,定义如下:public enum LocationType { InCollection, Loaned } -
使用您所学到的知识,在
Model文件夹中创建两个名为Medium和MediaItem的类。Medium类表示特定的媒体,如精装本或平装本,而MediaType属性将ItemType分配给Medium类所属的类型。对于这些,有效的ItemType将是Book。当您完成时,这两个新类将如下所示:public class Medium { public int Id { get; set; } public string Name { get; set; } public ItemType MediaType { get; set; } } public class MediaItem { public int Id { get; set; } public string Name { get; set; } public ItemType MediaType { get; set; } public Medium MediumInfo { get; set; } public LocationType Location { get; set; } }
注意
要从两个类中引用Enums文件夹中的枚举类型,需要在每个新类的顶部添加一个using声明,如下所示:using MyMediaCollection.Enums;。
除了本章前面讨论的属性外,MediaItem类中已添加一个Id属性,用于唯一标识集合中的每个项。这将在我们开始持久化数据时非常有用。
创建示例数据
在模型类就绪后,我们准备添加一些代码来创建三个用于在 UI 中显示的媒体项。这将帮助我们在我们开始创建主屏幕上的项目列表时可视化事物。当这一步完成,我们准备继续启用通过应用程序添加项的功能时,此代码将被移除。
现在,我们将在此MainWindow.xaml.cs代码隐藏文件中添加此代码。稍后,在第三章 可维护性和可测试性的 MVVM 中,此类代码将添加到ViewModel文件中。MainWindow将只包含表示逻辑,不会负责创建或获取填充 UI 的数据:
-
首先,打开
MainWindow.xaml.cs并创建一个名为PopulateData的方法。此方法将包含创建三个MediaItem对象(一张 CD、一本书和一张蓝光光盘)并将它们添加到名为_items的私有List中的代码,如下面的代码片段所示:public void PopulateData() { if (_isLoaded) return; _isLoaded = true; var cd = new MediaItem { Id = 1, Name = "Classical Favorites", MediaType = ItemType.Music, MediumInfo = new Medium { Id = 1, MediaType = ItemType.Music, Name = "CD" } }; Var book = new MediaItem { Id = 2, Name = "Classic Fairy Tales", MediaType = ItemType.Book, MediumInfo = new Medium { Id = 2, MediaType = ItemType.Book, Name = "Book" } }; var bluRay = new MediaItem { Id = 3, Name = "The Mummy", MediaType = ItemType.Video, MediumInfo = new Medium { Id = 3, MediaType = ItemType.Video, Name = "Blu Ray" } }; _items = new List<MediaItem> { cd, book, bluRay }; }您需要将
using语句添加到MainWindow的MyMediaCollection.Model和MyMediaCollection.Enums命名空间中。 -
将
_items定义为IList和isLoaded定义为bool作为私有类成员。我们稍后将更改项目集合为ObservableCollection。ObservableCollection是一个特殊的集合,当集合中的项目被添加或删除时,它会通知 UI 中的数据绑定项。现在,IList将满足我们的需求。代码可以在以下代码片段中看到:private IList<MediaItem> _items { get; set; } private bool _isLoaded; -
接下来,在
MainWindow构造函数中,在调用InitializeComponent之后添加一行代码,以调用新的PopulateData方法。添加到Window或UserControl构造函数中的任何代码都必须在此初始化代码之后添加。这是MainWindow.xaml中所有 XAML 代码初始化的地方。如果在调用InitializeComponent之前尝试引用这些元素,将导致错误。构造函数现在应如下所示:public MainWindow() { this.InitializeComponent(); PopulateData(); }
在 XAML 文件中创建一些 UI 组件之后,我们将返回此文件以添加一些数据绑定逻辑。让我们现在就去完成这个任务。
构建初始用户界面
在本章的早期部分,我们在 MainWindow.xaml 文件中添加了一个 TextBlock,其 Text 属性设置为 Media。我们将在现有的 TextBlock 下方添加一个 ListView 控件。ListView 控件是一个强大且灵活的控件,用于在垂直列表中显示项目列表。它类似于基本列表功能(项目选择、多选和自动滚动条)中的 ListBox 控件,但每个列表项都可以进行模板化,以几乎任何可想象的方式显示。
注意
关于 ListView 类的更多信息,包括示例代码和标记的文档,可在 Microsoft Learn 上找到:learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.listview。
-
首先,在顶级
Grid控件内添加一些标记以创建两行。应在现有的TextBlock元素之前添加RowDefinitions。TextBlock将保留在第一行,我们将在第二行添加ListView控件。按照以下代码片段创建RowDefinitions:<Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions>如您所见,我们自动将第一
RowDefinition的大小设置为TextBlock的大小,并通过使用"*"将Grid内剩余的空间分配给ListView控件的RowDefinition。如果您为多个行分配"*",则这些行将平均分配剩余的可用空间。这是Height属性的默认值。如果省略"*",大小将相同,但大多数 XAML 开发者明确包含它以实现完整性和提高可读性。
注意
关于行大小的更多信息,请参阅以下 Microsoft Learn 文档:learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.rowdefinition.height。
-
接下来,将
ListView控件添加到Grid中,设置Grid.Row等于1(行编号从 0 开始,即第一行是行0),并将其命名为ItemList。命名您的 XAML 元素不是必需的。这仅在您想要通过ElementName在 XAML 数据绑定中引用它们或作为代码背后的文件中的变量时才是必需的。命名一个控件会在InitializeComponent调用中创建一个变量,为了优化性能,您应该只在需要在其他地方引用控件时命名控件。以下代码片段显示了标记:<ListView Grid.Row="1" x:Name="ItemList" Background="LightGoldenrodYellow"/>
通过将背景颜色设置为 LightGoldenrodYellow,在填充任何数据之前,我们可以看到应用程序主窗口中的 ListView。根据您是否使用 Light 或 Dark Windows 主题,您可以选择最适合您的颜色。运行应用程序,它应该看起来像这样:

图 2.9 – 已添加到 UI 的 ListView
让我们回到代码背后文件,开始设置数据绑定。
注意
如果您对 XAML 编程中的数据绑定概念不熟悉,Microsoft Learn 在以下网页上为 Windows App SDK 开发者提供了一个关于数据绑定的优秀概述:learn.microsoft.com/windows/apps/develop/data-binding/。
完成数据绑定初始化
返回到 MainWindow.xaml.cs 文件,在调用 PopulateData() 方法之前添加一行代码,如下所示:
ItemList.Loaded += ItemList_Loaded;
在键入 += 后,Visual Studio ItemList_Loaded 事件处理器,如下面的截图所示:

图 2.10 – 插入 ItemList_Loaded 事件处理器
或者,如果您输入整行并按 Ctrl +,系统会提示您创建新方法。
为了使 MainWindow 构造函数简单,并将我们的数据加载代码放在一起,将 PopulateData 调用移动到 ItemList_Loaded 方法。然后,将其他两行代码添加到新的事件处理器中,如下所示:
private void ItemList_Loaded(object sender,
Microsoft.UI.Xaml.RoutedEventArgs e)
{
var listView = (ListView)sender;
PopulateData();
listView.ItemsSource = _items;
}
此代码在 ItemsList 控件在 UI 中完成加载后调用。我们从 sender 参数获取控件的实例,并将列表的 ItemsSource 设置为在 PopulateData() 中加载的 _items 集合。现在,列表中有数据,但看起来并不完全正确,如下面的截图所示:

图 2.11 – 包含三行数据的 ListView
ListView 正在显示三个行,对应于我们的三个示例数据项,但它显示的是每个项的数据类型而不是数据。这是因为我们没有告诉 ListView 它应该显示集合中每个项的哪些属性。默认情况下,列表将显示对象 ToString() 方法返回的内容。如果 ToString() 没有重写,则返回类的数据类型名称。
创建 DataTemplate 并绑定 UI
让我们回到 MainWindow.xaml 并告诉 ListView 我们想要为列表中的每个项目显示哪些数据。尝试在应用程序仍在运行时进行此更改。多亏了 Visual Studio 的 XAML 热重载 功能,你应该会看到 UI 重新加载,而无需重新启动调试会话。
通过在 ListView.ItemTemplate 内定义 DataTemplate 来自定义每个 ListView 项的外观。DataTemplate 可以包含我们需要的任何 WinUI 控件来定义每个列表项的布局。让我们保持简单,并添加一个包含两列的 Grid。每一列将包含一个 TextBlock。现在 ListView 应该看起来像这样:
<ListView Grid.Row="1"
x:Name="ItemList"
Background="LightGoldenrodYellow">
<ListView.ItemTemplate>
<DataTemplate x:DataType="model:MediaItem">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{x:Bind
Path=MediumInfo.Name}"/>
<TextBlock Grid.Column="1"
Text="{x:Bind Path=Name}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
我们开始创建一些更复杂的 XAML,并需要考虑格式化以优化其可读性。
注意
要快速重新格式化 XAML,可以使用 Ctrl + K | D 快捷键。还有一个名为 XAML Styler 的扩展程序在 Visual Studio Marketplace 上。您可以在 Visual Studio 的 管理扩展 对话框中搜索它,或在此处获取更多信息:marketplace.visualstudio.com/items?itemName=TeamXavalon.XAMLStyler2022。
为了使每个 TextBlock 能够绑定到 MediaItem 的属性,我们必须在 DataTemplate 上设置一个 x:DataType 属性。为了解析 MediaItem,需要在 Window 定义中添加一个命名空间声明,如下所示:
xmlns:model="using:MyMediaCollection.Model"
添加此 using 语句的快捷方式是将光标放在 x:DataType 上并按 Ctrl +. Visual Studio 将建议向文件中添加缺失的命名空间。现在我们可以通过使用 model 前缀来访问 MyMediaCollection.Model 命名空间中的对象,并且 x:DataType="model:MediaItem" 在构建和运行应用程序时将解析 MediaItem。
每个 TextBlock 的 Text 属性都绑定到 MediaItem 的一个属性,使用的是 x:Bind 标记扩展。
注意
使用x:Bind而不是Binding标记扩展来将数据绑定到 UI 的好处是编译时验证和性能提升。之前在 Microsoft Learn 上提到的数据绑定概述深入探讨了这些差异。我更喜欢尽可能使用x:Bind。你应该注意Binding和x:Bind之间的重要区别是,虽然Binding默认为OneWay模式,但x:Bind默认为OneTime。这种对默认绑定行为的更改是为了性能考虑。OneWay绑定需要在幕后编写更多代码来设置更改检测,以监控源值的更改。你仍然可以显式地将你的x:Bind使用更新为OneWay或TwoWay。有关x:Bind的更多信息,请参阅这篇 Microsoft Learn 文章:learn.microsoft.com/windows/uwp/xaml-platform/x-bind-markup-extension。
有关 XAML 中标记扩展的更多信息,你可以阅读这篇.NET 文章:learn.microsoft.com/dotnet/desktop/xaml-services/markup-extensions-overview。
现在,当你运行应用程序时,你可以在ListView中看到每个项目的MediumType名称和项目名称,如下面的截图所示:

图 2.12 – 带有两列示例数据的 ListView
这已经取得了相当不错的进展!在我们扩展功能之前,让我们简要谈谈 WinUI、Windows App SDK 和.NET 在应用开发过程中的结合方式。
理解 WinUI 和 Windows App SDK
让我们回顾一下我们项目中可用的 WinUI 控件,看看它们如何帮助我们构建在解决方案资源管理器中看到的Microsoft.WindowsAppSDK包。
要查看此包的内容,请从 Visual Studio 的视图菜单中打开对象浏览器窗口。控件将在此处列出,位于Microsoft.WinUI树节点下的几个命名空间中,如下面的截图所示:

图 2.13 – 对象浏览器中的 WinUI 控件
我们将要使用的 WinUI 控件的大部分都可以在Microsoft.WinUI下的Microsoft.UI.Xaml.Controls命名空间中找到,以及其他相关类和接口。
到目前为止,我们在应用程序中使用了Grid、TextBlock和ListView控件。打开ListView类,展开ListViewBase类。这个基类包含了ListView可用的方法、属性和事件。ListViewBase的成员将在右侧窗格中显示。花些时间审查这些成员,看看你是否能从迄今为止对控件的使用中认出它们。
清除您的搜索 ListView,在 TextBlock 控件的左侧面板中向下滚动。选择它,在右侧面板中找到并选择 Text 属性。右下角的面板显示了属性的详细信息,如图下截图所示:

图 2.14 – 对象浏览器中 TextBlock.Text 属性的详细信息
对象浏览器窗口在熟悉新库或项目时可以成为一个宝贵的资源。所有引用的项目、NuGet 包和其他引用都将在此处显示。
您在此处审查的控件和其他组件构成了 Windows App SDK 应用程序的 UI 层。Windows App SDK 的底层应用程序框架是 .NET。
理解 .NET 应用程序模型
您可能听说过 WinUI 应用的底层应用程序模型可以是 C++ 应用的 Win32 或桌面应用的 .NET。那么,.NET 应用程序模型究竟是什么呢?
WinUI 应用程序中的 .NET 应用程序模型描述了应用程序的打包和部署方式。它还定义了以下行为和能力:
-
数据存储
-
状态管理
-
生命周期事件(启动和关闭)
-
多任务处理
-
资源管理
-
应用间通信
WinUI 3 是 Windows App SDK 的一个组件,是 UI 层。尽管 WinUI 控件与 Windows SDK 解耦,但在使用 Windows App SDK 时,底层 .NET 应用程序平台仍然依赖于它。选择目标操作系统和最低版本是这种依赖关系的一个副作用。
现在您已经更好地理解了 WinUI 控件以及它们与 Windows App SDK 和 .NET 应用程序平台的关系,让我们在我们的应用程序中使用更多的一些控件。
与 WinUI 控件、属性和事件一起工作
是时候增强应用程序的 UI 了。目前,主页只包含一个位于 ListView 上的 Media 标签,有媒体类型和媒体项目名称的列。以下是我们将在本节中添加的增强功能:
-
ListView的标题行 -
一个
ComboBox用于根据媒体类型过滤行 -
一个用于向集合添加新项目的
Button
我们将首先增强我们的媒体集合的 ListView。
添加 ListView 标题
在我们创建标题之前,让我们改变 ListView 的背景颜色。Aqua 颜色很好地突出了控件,但当应用程序被我们的客户使用时可能会分散注意力。我们将讨论 WinUI 主题画笔,并查看 MainWindow.xaml 文件中 ListView 定义中的 Background="Aqua"。
创建媒体集合的标题行相对简单。为了定义每个项目的行,我们创建了一个包含 DataTemplate 的 ListView.ItemTemplate 块。为了创建标题,我们在 ListView.HeaderTemplate 块内做同样的事情。
正如与项目行一样,表头行将包含一个具有两个列的 Grid,具有相同的 Width 定义。我们再次想在 Grid 中使用两个 TextBlock 控件,但为了在表头和项目之间添加一些间隔,我们将添加 Border 控件。让我们看一下表头的标记,然后更详细地讨论差异。花点时间审查以下标记:
<ListView.HeaderTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border BorderBrush="BlueViolet"
BorderThickness="0,0,0,1">
<TextBlock Text="Medium"
Margin="5,0,0,0"
FontWeight="Bold"/>
</Border>
<Border Grid.Column="1"
BorderBrush="BlueViolet"
BorderThickness="0,0,0,1">
<TextBlock Text="Title"
Margin="5,0,0,0"
FontWeight="Bold"/>
</Border>
</Grid>
</DataTemplate>
</ListView.HeaderTemplate>
如您所见,每个 TextBlock 都嵌套在一个 Border 元素中。这将用 BlueViolet 颜色的边框包裹文本。然而,通过设置 BorderThickness="0,0,0,1",边框颜色将只出现在表头行项的底部。这是它在应用程序中的样子:

图 2.15 – 添加了表头行的 ListView
注意
通过将整个 Grid 嵌套在一个 Border 中而不是围绕每个表头项放置一个 Border,可以实现相同的底部边框。然而,通过这种方式操作,我们对每个列的边框样式的外观有更多的控制。当我们稍后实现排序时,可以修改边框的颜色以突出显示已应用排序的列。
你可能也注意到了,表头行的文本与网格中的行不同。HeaderTemplate 中每个 TextBlock 内设置的 FontWeight="Bold" 属性有助于突出显示表头行。
创建 ComboBox 过滤器
应用程序的一个要求是允许用户过滤集合项的多个属性。让我们从简单开始,只添加对媒体(Book、Music 或 Movie)的过滤。列表还需要一个 All 选项,当用户打开应用程序时,它将是默认选择:
-
首先,向
MainWindow添加一些 XAML 代码,以便在Media标签的右侧添加一个过滤器。将MediaTextBlock替换为以下标记:<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <TextBlock Text="Media Collection" Margin="4" FontWeight="Bold" VerticalAlignment="Center"/> <StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right"> <TextBlock Text="Media Type:" Margin="4" FontWeight="Bold" VerticalAlignment="Center"/> <ComboBox x:Name="ItemFilter" MinWidth="120" Margin="0,2,6,4"/> </StackPanel> </Grid>单一的
TextBlock标签已被替换为两列的Grid。第一列包含TextBlock,进行了一些修改。首先,Text属性已更新为"Media Collection"。FontWeight已更改为"Bold",并添加了一些边距。最后,该元素垂直居中。第二列包含一个新的
StackPanel(一个水平或垂直堆叠其内容的容器控件)。默认方向是Vertical。在我们的情况下,我们想要一个水平堆叠,这就是为什么将Orientation属性设置为Horizontal的原因。
StackPanel 包含一个 TextBlock 标签和一个用于过滤选择的 ComboBox。ComboBox 过滤器已被赋予一个 x:Name,这样我们就可以从 C# 后台代码文件中引用它以初始化其内容。我们还配置了一个 MinWidth 值为 120。如果 ComboBox 过滤器的内容需要超过 120 像素(px),它可以变得更大,但它的宽度不能小于此处设置的值。
注意
在 XAML 中引用的像素是有效像素。要了解更多关于使用 XAML 的响应式布局和尺寸信息,请参阅这篇 Microsoft Learn 文章:learn.microsoft.com/windows/apps/design/layout/layouts-with-xaml。
-
在
MainWindow.xaml.cs文件中,添加一个新的变量来保存媒体列表,如下所示:private IList<string> _mediums { get; set; }这个集合可以是一个
IList而不是ObservableCollection,因为我们不期望在应用程序运行期间其内容发生变化。 -
在
PopulateData()方法内部,在方法末尾添加一些代码来填充_mediums列表,如下所示:_mediums = new List<string> { "All", nameof(ItemType.Book), nameof(ItemType.Music), nameof(ItemType.Video) };我们为
ItemType枚举中的每个可能值以及默认的"All"值添加一个项到集合中。 -
ComboBox过滤器将在加载后绑定到集合,因此请在MainWindow构造函数中添加一个Loaded事件处理器,就像我们之前为ItemList所做的那样,如下所示:ItemFilter.Loaded += ItemFilter_Loaded; -
ItemFilter_Loaded事件处理器将与ItemList_Loaded处理器非常相似。使用以下代码:private void ItemFilter_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { var filterCombo = (ComboBox)sender; PopulateData(); filterCombo.ItemsSource = _mediums; filterCombo.SelectedIndex = 0; }
代码将发送者转换为 ComboBox 数据类型,并将其 ItemSource 设置为我们上一步填充的列表。最后,还需要一个额外的步骤来将 ComboBox 过滤器默认设置为 "All" 项。这是通过将 SelectedIndex 设置为 0 来实现的。
让我们运行应用程序并看看现在看起来如何。您可以在以下屏幕截图中看到结果:

图 2.16 – 添加了媒体类型过滤器的媒体集合
非常锐利!如果您点击 媒体类型 过滤器,您可以看到可用的四个选择值,如图所示:

图 2.17 – 媒体类型值
注意到 ComboBox 的下拉菜单已经采用了带有一定透明度的 Windows 风格,而无需添加任何额外的代码或标记。选择列表中的其他值并查看会发生什么。什么都没有!这是因为我们没有在过滤器选择变化时添加任何代码来进行过滤。我们可以添加一些额外的代码来修复这个问题,如下所示:
-
首先,创建一个新的
_allItems集合来存储所有可用的媒体项列表,无论当前过滤器如何,如下所示:private IList<MediaItem> _allItems { get; set; } -
接下来,在
PopulateData()方法中,在填充_items集合之后,添加相同的项到_allItems,如下所示:_allItems = new List<MediaItem> { cd, book, bluRay }; -
现在,当过滤器选择变化时,我们需要进行一些过滤。我们想要处理
ComboBox控件上的SelectionChanged事件,但我们不希望在页面完全加载之前将其连接起来。这将防止在ComboBox过滤器最初被填充时处理该事件。 -
在
ItemFilter_Loaded的实现结束时,为ComboBox控件上的SelectionChanged事件添加一个事件处理器:ItemFilter.SelectionChanged += ItemFilter_SelectionChanged; -
在新的
ItemFilter_SelectionChanged事件处理器中,我们将遍历_allItems列表,并根据它们的MediaType属性确定要包含在过滤列表中的项目,如下所示:private void ItemFilter_SelectionChanged( object sender,Microsoft.UI.Xaml.Controls. SelectionChangedEventArgs e) { var updatedItems = (from item in _allItems where string.IsNullOrWhiteSpace(ItemFilter. SelectedValue.ToString()) || ItemFilter.SelectedValue.ToString() == "All" || ItemFilter.SelectedValue.ToString() == item.MediaType.ToString() select item).ToList(); ItemList.ItemsSource = updatedItems; }
如果过滤器值为空或 MediaType。否则,我们检查 MediaType 是否与 ItemFilter ComboBox 中的选择匹配。当有匹配时,我们将其添加到 updatedItems 列表中。然后,我们将 updatedItems 设置为 ListView 的 ItemsSource。
注意
过滤器永远不应该为空,除非在初始化数据时出现错误。这个条件只是为了防范不可预见的情况。
现在,再次运行应用程序,并在过滤器中选择 Book,如图所示:

图 2.18 – 仅显示书籍的媒体集合
这样就暂时处理了过滤实现。让我们用 Button 完成这部分 UI 设计。
添加新项目按钮
我们还没有准备好开始使用多个窗口或导航。你应该对当前页面的 Button 有一些了解,并添加一些代码以确保一切连接正确。
打开 MainWindow.xaml 文件,并在 Window 的顶级 Grid 上添加第三个 RowDefinition,如下所示:
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
新行将具有 Auto 高度,以便它根据 Button 的大小自动调整。我们仍然希望 ListView 占据屏幕的大部分空间。
现在,在 ListView 控件的关闭标签之后,添加新的 Button,如下所示:
<Button x:Name="AddButton" Content="Add Item"
HorizontalAlignment="Right" Grid.Row="2" Margin="8"/>
如前一章所述,Button 控件没有 Text 属性。相反,如果你想只让 Button 控件包含文本,你可以将其分配给 Content 属性。我们还将 Button 控件分配到 Grid 的第三行,设置边距,并将其对齐到 Grid 的右侧。
让我们看看现在应用程序看起来如何,如下所示:

图 2.19 – 带有添加项目按钮的我的媒体集合
按钮目前还没有做任何事情。因为我们还没有准备好向应用程序添加一个额外的窗口来添加项目,让我们打开一个消息弹出窗口来通知用户此功能不可用。
在 MainWindow.xaml 中,在 AddButton 内部连接一个新的事件处理器。你也可以移除 x:Name 属性。我们能够移除这个名称,因为我们不需要在代码-behind 文件中引用它。代码如下所示:
<Button Content="Add Item"
HorizontalAlignment="Right"
Grid.Row="2" Margin="8" Click="AddButton_Click"/>
你可以通过将光标放在处理器名称 AddButton_Click 上并按 F12 来创建事件处理器,这将创建处理器并导航到 MainWindow.xaml.cs 文件。在 AddButton_Click 事件处理器内部,我们将创建一个新的 ContentDialog,并显示我们想要向用户显示的消息。
注意
调用 dialog.ShowAsync() 必须等待,所以请记住在事件处理器中添加 async 指令,如下所示。
在以下代码片段中,async 指令被突出显示,用于新的事件处理器:
private async void AddButton_Click(object sender,
RoutedEventArgs e)
{
var dialog = new ContentDialog
{
Title = "My Media Collection",
Content = "Adding items to the collection is
not yet supported.",
CloseButtonText = "OK",
XamlRoot = Content.XamlRoot
};
await dialog.ShowAsync();
}
现在,再次运行应用程序并单击如图所示的 添加项目 按钮:

图 2.20 – 显示 ContentDialog 弹出窗口
这就是向 WinUI 页面添加功能 Button 的全部内容。正如我们讨论的那样,其中一些代码将在下一章中更改并移动到 ViewModel 中,但你现在应该已经很好地了解了如何使用一些基本的 Button 属性和事件。
摘要
在本章中,我们在 我的媒体收藏 应用程序上取得了很大的初步进展。在这个过程中,你学习了如何使用几个常见的 WinUI 控件。你还学习了如何通过使用不同的布局控件和更新 XAML 中的控件属性来更改 WinUI 控件的外观、布局和行为。最后,你看到了如何利用数据绑定和事件来添加和更新显示给用户的数据。
接下来,我们将学习如何将我们在代码隐藏文件中编写的部分逻辑解耦,以构建可测试和可维护的应用程序。
问题
-
你如何向 Visual Studio 添加或删除功能?
-
创建新的 WinUI 3 项目时,必须针对哪个最低 Windows 版本?
-
你可以在哪里添加可以被整个应用程序中的组件共享的 XAML 资源?
-
在新的 WinUI 应用程序中,第一个加载的窗口的默认名称是什么?
-
哪个 XAML 容器控件允许你定义行和列来布局其内容?
-
哪个 XAML 容器控件水平或垂直堆叠其内容?
-
WinUI 应用程序可以使用哪个消息框来向用户显示简单的消息?
-
挑战:WinUI 中哪种布局面板允许其内容绝对定位?
第三章:MVVM 的可维护性和可测试性
当构建基于 XAML 的应用程序时,最重要的设计模式之一就是 MVVM 模式。MVVM 通过数据绑定在视图的 XAML 标记和视图模型的 C#代码之间提供清晰的关注点分离。这种分离带来了易于维护和可测试性。视图模型类可以在不依赖于底层用户界面(UI)平台的情况下进行测试。对于大型团队来说,这种分离的另一个好处是,更改 XAML 使得 UI 设计师可以独立于专注于编写业务逻辑和应用程序后端的开发者工作。
在本章中,你将学习以下概念:
-
MVVM 设计模式的基础
-
流行的 MVVM 框架
-
在 WinUI 应用程序中实现 MVVM
-
在视图中处理视图模型更改
-
MVVM 中的事件处理
-
使用 MVVM Toolkit 简化视图模型实现
到本章结束时,你将了解 MVVM 设计模式的基础,对一些流行的 MVVM 框架有一些了解,并知道如何在 WinUI 应用程序中实现 MVVM。我们将通过实际操作开源的MVVM Toolkit来结束本章。
技术要求
要跟随本章中的示例,请参阅第二章的技术要求部分,配置开发环境并创建 项目。
你可以在这里找到本章的代码文件:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/main/Chapter03
理解 MVVM
MVVM 于 2005 年由微软引入,随着Windows Presentation Foundation(WPF)和Silverlight的发布而受到开发者的欢迎,因为它非常适合构建 XAML 应用程序。它类似于由 Martin Fowler 创建的展示模型模式,他是设计模式最有影响力的倡导者之一。
MVVM 模式由以下三个层组成:
-
模型:模型层包含应用程序的业务逻辑,应执行所有数据访问操作。视图模型与模型通信以检索和保存应用程序的数据。
-
视图:视图层仅负责在应用程序中展示数据。布局或结构在这里定义,以及样式定义。这是负责与用户交互、接收输入事件和数据的那一层。视图通过数据绑定表达式仅了解视图模型。
-
ViewModel:视图模型(或 ViewModel)层负责维护视图的数据状态。它有一组属性,通过数据绑定向视图提供数据,以及一组由视图在响应用户输入事件时调用的命令。视图模型类对其对应的视图没有了解。
MVVM – 整体架构
让我们看看 MVVM 的组件如何适应实现该模式的整体应用程序架构,以下图所示:

图 3.1 – 使用中的 MVVM 模式
在 图 3.1 中,你可以看到 MVVM 模式的表示以及应用程序的其他部分:服务、数据和单元测试。图中的箭头表示依赖关系,而不是数据流。你可以想象,数据需要沿着这些路径的大部分双向移动,以创建一个功能性的应用程序。
与许多设计模式一样,MVVM 模式旨在为开发者提供创建可靠、可维护应用程序的指南。然而,并非所有开发者都以相同的方式实现该模式。差异通常在于模型的实现。一些开发者会以 领域驱动设计 (DDD) 风格创建领域模型。这对于具有复杂业务逻辑的大型应用程序是有意义的。对于更简单的应用程序,模型可能只是一个简单的数据访问层,位于客户端或服务层之后。在这些情况中,前图中 服务 云会在视图模型和模型层之间移动。
MVVM 的目的是帮助你构建最适合用户的应用程序。随着你对 MVVM 和 WinUI 的经验积累,你将找到适合你应用程序的正确实现方式。一个不错的开始是找到一些使与 MVVM 一起工作变得更简单的框架。
WinUI 的 MVVM 库
在 WinUI 应用程序中使用 MVVM 时,你必须创建一些基础设施代码来促进视图和视图模型之间的数据绑定。你可以自己编写这些代码,或者选择一个将这种管道代码从你的应用程序中抽象出来的框架。虽然我们将在下一节中编写应用程序的管道代码,但让我们回顾一些流行的 WinUI MVVM 框架。
MVVM 工具包
我们将通过 Visual Studio 中的 NuGet 包管理器 讨论通过 CommunityToolkit.Mvvm NuGet 包,或者在 NuGet 网站上查看其详细信息,网址为 www.nuget.org/packages/CommunityToolkit.Mvvm/。
该库包括支持 INotifyPropertyChanged、IMessenger 和 ICommand 的基类。它还包括其他消息和 控制反转 (IoC) 辅助类。此外,MVVM Toolkit 的最新版本使用 Roslyn 源生成器 来减少开发者需要编写的样板代码量。
注意
如果您不熟悉 Roslyn,它是 .NET 编译器平台 的代码名称。它首次与 Visual Studio 2015 一起 发布到制造(RTM),并且是 C# 和 VB.NET 语言的当前编译器。Roslyn 通过 .NET 编译器平台 SDK 的可扩展性允许开发者创建诸如自定义源生成器和 Visual Studio 的源代码分析器等工具。有关该平台的信息,您可以查看 Microsoft Learn 上的文档:learn.microsoft.com/dotnet/csharp/roslyn-sdk/。
在本章的后面部分,在您学习了如何实现自己的 MVVM 基类之后,我们将在我们的示例应用程序中利用 MVVM Toolkit。
Prism 库
Prism 最初是由 Microsoft 创建和维护的库。它是 Microsoft 开发者部门用于维护的 模式与实践 指南、参考架构和库的一部分。
注意
以下由 Microsoft 维护的剩余 模式与实践 项目可以在 GitHub 上找到:github.com/mspnp。
Microsoft 决定开源 Prism 库并将所有权转让给社区。该项目托管在 GitHub 上,可以在网络上找到:prismlibrary.com/。Prism 为 WPF、Xamarin、UWP 和 WinUI 项目提供了包。
Prism 不仅仅是一个 MVVM 框架。它还包括用于松散耦合应用程序消息的 EventAggregator。Prism 可以通过 NuGet 添加到项目中。Prism 网站上还有一个安装程序,可以添加 Visual Studio 项目和项目模板。
MVVMCross
MVVMCross 是一个最初为 Xamarin 开发者创建的 MVVM 框架。现在它提供了适用于 Xamarin、.NET MAUI、WPF、UWP 和 WinUI 的 NuGet 包。与 Prism 类似,MVVMCross 不仅在 WinUI 应用程序中促进了数据绑定,它还提供了以下方面的辅助工具:
-
数据绑定
-
导航
-
日志和跟踪
-
依赖注入和 IoC
-
单元测试
有一些额外的库,但大多数都是针对 .NET MAUI 应用程序的。MVVMCross 包也可以通过 NuGet 添加到您的项目中。有关在 WinUI 中使用 MVVMCross 的更多信息,请查看他们的网站:www.mvvmcross.com/。
选择 WinUI 应用程序的框架
使用第三方框架构建生产 XAML 应用程序是一个很好的选择。这些提供了内置的日志和依赖注入支持。对于本书中的 WinUI 应用程序,我们将在本章末尾使用 MVVM Toolkit 进行数据绑定。然而,首先,我们将从头开始实现 MVVM 模式。这将帮助您了解数据绑定、依赖注入和其他与 MVVM 相关的核心概念的基础机制。
在开始编写代码之前,您应该了解 WinUI 中数据绑定是如何工作的。
理解 WinUI 中的数据绑定
在上一章中,你看到了一些使用 Binding 和 x:Bind 标记扩展的简单数据绑定示例。让我们分析一下允许视图在视图模型数据发生变化时接收更新的组件。
什么是标记扩展?
对标记扩展的深入讨论超出了本入门书的范围。简而言之,它们是一个执行一些逻辑并将值返回给 XAML 解析器的类。你可以在 XAML 中通过查找大括号内的某些标记来识别它们的使用。以下是一个 TextBlock 的 Text 属性中的 Binding 示例:
<TextBlock Text="{Binding Path=Name, Mode=TwoWay}"/>
从这个例子中,你可以推断出存在一个名为 Binding 的标记扩展类,并且它有两个属性是 Path 和 Mode。这个标记扩展会获取这些属性,解析一个值,并将其返回给 XAML 解析器,以便在应用程序的视图中显示。
一些 XAML 标记语言允许开发者编写自己的自定义标记扩展。WPF 和 .NET MAUI 有自定义标记扩展,但 WinUI 没有。如果你对标记扩展的实现感兴趣,Microsoft Learn 提供了关于如何为 .NET MAUI 创建一个的文档:learn.microsoft.com/dotnet/maui/xaml/markup-extensions/create。
现在,让我们更深入地了解 WinUI 中的 Binding 标记扩展。
标记扩展
正如你简要看到的,Binding 标记扩展将绑定源(MVVM 中的视图模型)中的数据映射到绑定目标(视图),并提供给它。这些都是 Binding 标记扩展的属性:
-
Path:数据绑定源中值的路径。对于我们的应用程序,这将是在视图模型上的属性名称。 -
Converter:如果源属性的 数据类型与视图中控件属性的 数据类型不匹配,则使用Converter属性来定义两者之间的映射。 -
ConverterLanguage:如果指定了Converter属性,可以可选地设置ConverterLanguage属性以支持国际化。 -
ConverterParameter:如果Converter属性需要一个参数,请使用ConverterParameter属性来提供它。通常不使用ConverterParameter属性,它通常是一个string值。如果你需要向Converter提供多个值,你可以将它们连接起来,然后在方法内部解析它们。 -
ElementName:当绑定到视图中的另一个元素的属性时使用此参数。 -
FallBackValue:如果数据绑定因任何原因失败,你可以指定一个FallBackValue属性在视图中显示。 -
Mode:这定义了数据绑定是OneTime(仅在 XAML 首次解析时设置值)、OneWay(在检测到更改时从 View Model 获取值)还是TwoWay(值在 View 和 View Model 之间双向流动)。Binding的默认Mode设置取决于所绑定的控件和属性。如果您不确定默认的Mode,请检查文档。 -
RelativeSource:用于定义相对于当前控件的数据绑定源。这通常与通过父元素获取数据的控件模板一起使用。 -
Source:指定数据绑定源。这通常在 WinUI 的顶级控件级别定义,作为 View Model。然而,子控件可以设置不同的Source,覆盖从其祖先继承的Source。在 View 的任何级别定义的Source值将被所有子元素继承,除非设置了新的Source。 -
TargetNullValue:指定如果数据绑定源解析但具有null值时显示的默认值。 -
UpdateSourceTrigger:指定更新TwoWay绑定源的频率。选项有PropertyChanged、Explicit和LostFocus。大多数属性的默认频率是PropertyChanged。
Path 是默认属性,在没有给出参数属性名称时假定。例如,前面的 TextBlock 示例也可以写成以下形式:
<TextBlock Text="{Binding Name, Mode=TwoWay}"/>
这里,Name 假定是提供的 Path 参数的值。提供两个参数而不指定参数名称将导致 XAML 解析器错误。
Binding 标记扩展在每种 XAML 语言中都能找到。另一种数据绑定选项 x:Bind 则不是。它仅是 UWP 和 WinUI 中的一个选项。
x:Bind 标记扩展
x:Bind 是 WinUI 的一个替代标记扩展。它比 Binding 更快,使用的内存更少,并且具有更好的调试支持。它通过在编译时生成代码以在运行时进行绑定来实现这种性能提升。相比之下,Binding 标记扩展是在运行时由 XAML 解析器执行的,这会产生额外的开销。编译时绑定还会在编译时捕获不正确的数据绑定表达式,而不是在运行时生成数据绑定失败。
Binding 和 x:Bind 之间的另一个重要区别是 Binding 需要设置 Data
Context。数据绑定到 DataContext 内对象的属性。当使用 x:Bind 时,您直接绑定到当前 Window 或 UserControl 上的属性。您还可以使用 x:Bind 直接将事件绑定到代码背后的 Window 的事件处理器。
虽然 x:Bind 的大多数属性与 Binding 相同,但让我们突出以下不同的属性:
-
ElementName:在x:Bind中不可用。您必须使用Binding来将数据绑定到其他 XAML 元素属性。如果您的应用程序必须绑定到其他元素,可以在同一视图中使用x:Bind和Binding。 -
Mode:这里唯一的区别是x:Bind的默认Mode通常为OneTime,而不是OneWay。 -
RelativeSource:在x:Bind中不可用。 -
Source:在x:Bind中不可用。相反,您通常会在每个视图的代码隐藏文件中定义一个ViewModel属性,其数据类型为相应的 ViewModel 类。您也可以为属性创建一个特定领域的名称,例如,在我们的应用程序中为MediaItems。 -
BindBack:这个属性是x:Bind独有的。它允许在调用反向数据绑定时调用一个自定义函数。这并不常用,我们不会在我们的应用程序中使用它。
x:Bind 是一个强大且复杂的标记扩展。有关更多信息,您可以阅读 Microsoft Learn 上的此页面:learn.microsoft.com/windows/uwp/xaml-platform/x-bind-markup-extension。
接下来,让我们讨论 INotifyPropertyChanged,这个接口使得 ViewModel 中数据绑定属性的变化可以在视图中反映出来。
使用 INotifyPropertyChanged 更新视图数据
那么,当 ViewModel 中的数据发生变化时,视图是如何得到通知的呢?这个魔法就隐藏在 Microsoft.UI.Xaml.Data.INotifyPropertyChanged 接口中。这个接口只有一个成员,如下所示:
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
每个 ViewModel 类都必须实现这个接口,并通过触发 PropertyChanged 事件来更新视图。通过在 PropertyChangedEventArgs 参数中传递其名称来指示哪个属性已更改。为了刷新所有属性,可以将属性名称传递为 null 或 string.Empty,但请注意这可能会对大型视图的性能产生潜在影响。
使用 INotifyCollectionChanged 更新集合数据
INotifyPropertyChanged 对于大多数属性都工作得很好,但如果集合中的项目被添加或删除,它将不会更新视图。这就是使用 Microsoft.UI.Xaml.Interop.INotifyCollectionChanged 接口的地方。同样,这个接口只有一个成员,如下所示:
public interface INotifyCollectionChanged
{
event NotifyCollectionChangedEventHandler
CollectionChanged;
}
在 .NET 中常用的一些集合或集合接口(如 List<T>、IEnumerable<T> 等)都没有实现这个接口。您可以创建一个从现有列表类型派生的自定义集合,并自行实现 INotifiedCollectionChanged,但使用已经对 WinUI 开发者可用的 ObservableCollection<T> 列表类型要容易得多。这是一个在添加或删除项目,或内容完全刷新时更新视图的集合。
ObservableCollection<T> 的 Items 属性是只读的,因此不能直接设置。你可以在创建 ObservableCollection<T> 时通过传递一个 List<T> 或 IEnumerable<T> 到构造函数来添加项目,或者使用它的 Add 或 Insert 方法(没有 AddRange 方法来添加多个项目)。你可以通过将新项目赋值给当前索引来更新集合中的单个值。你可以使用 Remove、RemoveAt、ClearItems 或 Clear 方法来删除项目。
在下一节中,当我们自己实现 MVVM 模式时,你将看到这些概念在实际中的应用。
在 WinUI 应用程序中实现 MVVM
现在是时候开始将我们的项目转换为使用 MVVM 了。为了彻底理解 MVVM 模式,我们将首先构建自己的 MVVM 基础设施。对于简单的应用程序,它不需要超过一个基类:
-
首先在项目中添加一个 ViewModels 文件夹。如果你正在使用 GitHub 上的代码,你可以继续使用上一章的项目,或者使用该章节文件夹中的 Start 项目。
-
接下来,向
BindableBase添加一个新的类。这将是项目中所有 View Model 类的基类。它将负责通知相应的视图任何属性的变化。这是通过实现INotifyPropertyChanged接口来完成的。让我们回顾一下BindableBase类的代码,如下所示:public class BindableBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged( [CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new Property ChangedEventArgs(propertyName)); } protected bool SetProperty<T>(ref T originalValue, T newValue, [CallerMemberName] string propertyName = null) { if (Equals(originalValue, newValue)) { return false; } originalValue = newValue; OnPropertyChanged(propertyName); return true; } }
通过将其作为我们 View Models 的基类,它们将有两个新的方法可供使用,如下所示:
-
OnPropertyChanged:使用此方法触发PropertyChanged事件以通知 View 数据的变化 -
SetProperty:此方法用于设置属性的值,如果值已更改,则会调用OnPropertyChanged
注意
确保在 BindableBase 类文件顶部有这两个 using 指令:
using System.ComponentModel;
using System.Runtime.CompilerServices;
现在我们已经有了基类,让我们将第一个 View Model 添加到项目中。右键单击 MainViewModel。这个 View Model 将会替换 MainWindow 中的大部分代码。以下代码是修改后的类的一部分。请参考 GitHub 仓库中该章节的 MainViewModel.txt (github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/main/Chapter03/Complete/MyMediaCollection/ViewModels) 以获取完整类的当前版本:
public class MainViewModel : BindableBase
{
private string selectedMedium;
private ObservableCollection<MediaItem> items;
private ObservableCollection<MediaItem> allItems;
private IList<string> mediums;
public MainViewModel()
{
PopulateData();
}
...
public IList<string> Mediums
{
get
{
return mediums;
}
set
{
SetProperty(ref mediums, value);
}
}
...
}
你可能已经注意到代码已经更新,在每个属性的 Set 块中使用新的 BindableBase.SetProperty 方法。这确保了当属性值发生变化时,UI 将会收到通知:
-
现在,我们需要使
MainViewModel类对MainWindow视图可用。由于在整个应用程序生命周期中将使用此视图模型的单个实例,我们将在App.xaml.cs文件中添加一个静态只读属性,使其对应用程序可用,如下所示:public static MainViewModel ViewModel { get; } = new MainViewModel(); -
我们现在可以删除
MainViewModel类中的所有代码。此外,添加一个属性使App.ViewModel对MainWindow的数据绑定可用,如下所示:public sealed partial class MainWindow : Window { public MainWindow() { this.InitializeComponent(); Loaded += MainPage_Loaded; } public MainViewModel ViewModel => App.ViewModel; private async void AddButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { var dialog = new MessageDialog("Adding items to the collection is not yet available.", "My Media Collection"); await dialog.ShowAsync(); } }目前我们只需要在代码隐藏文件中保留关于
MainViewModel的代码。现在只需要对处理新数据源进行两个更改,详细说明如下。 -
首先,更新
ComboBox以删除x:Name属性,并为ItemsSource和SelectedItem属性添加x:Bind数据绑定。SelectedItem绑定需要设置为TwoWay。这将确保当用户在 UI 中更改SelectedMedium时,MainViewModel中的数据会更新。代码可以在以下代码片段中看到:<ComboBox ItemsSource="{x:Bind ViewModel.Mediums}" SelectedItem="{x:Bind ViewModel.SelectedMedium, Mode=TwoWay}" HorizontalAlignment="Right" MinWidth="120" Margin="0,2,6,4"/> -
现在,更新
ListView以删除x:Name属性,并添加一个ItemsSourcex:Bind数据绑定,如下所示:<ListView Grid.Row="1" ItemsSource="{x:Bind ViewModel.Items}">为这些控件分配的名称不再需要,因为我们没有在代码中的任何地方引用它们。
注意
为 XAML 元素分配名称会分配额外的资源。建议仅在元素必须直接从代码隐藏文件或其他视图元素的ElementName数据绑定中引用时才命名元素。
- 现在,运行应用程序并尝试使用
ComboBox更改Medium过滤器。它应该表现得与之前完全一样,但现在我们已经将视图模型数据与 UI 解耦,这使得测试或潜在的重用变得更加容易。
接下来,我们将解决MainWindow.xaml.cs文件中仍然存在的按钮Click事件。
与事件和命令一起工作
是时候更新项目,将事件处理代码移动到MainViewModel。在本节结束时,你将删除添加到ViewModel属性的所有代码。这将有利于关注点的分离,以及项目的可维护性和可测试性。
我们可以使用与Click事件连接并连接到MainViewModel类上的方法的相同过程。这种方法有两个问题:
-
视图层和视图模型层变得更加紧密耦合,降低了可维护性
-
UI 关注点被注入到视图模型中,降低了类的可测试性
让我们另辟蹊径。MVVM 模式中的Command属性都有一个概念,即它们都期望一个System.Windows.Input.ICommand类型的类型。
实现ICommand
在项目中使用命令,我们首先需要创建一个ICommand的实现。
注意
使用像 Prism 或 MVVM Toolkit 这样的 MVVM 框架的优点之一是它们提供了ICommand的实现。
在项目中的ViewModel文件夹中添加一个新类,并将其命名为RelayCommand。这个类将实现ICommand接口。RelayCommand类将如下所示:
public class RelayCommand : ICommand
{
private readonly Action action;
private readonly Func<bool> canExecute;
public RelayCommand(Action action)
: this(action, null)
{
}
public RelayCommand(Action action, Func<bool>
canExecute)
{
if (action == null)
throw new ArgumentNullException
(nameof(action));
this.action = action;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter) => canExecute
== null || canExecute();
public void Execute(object parameter) => action();
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged() =>
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
RelayCommand有两个构造函数,它们都接受一个在命令执行时将被调用的Action。其中一个构造函数还接受一个Func<bool>。这将允许我们根据CanExecute的返回值启用或禁用 UI 操作。我们将使用此功能来启用ListView。
在视图模型中使用命令
现在,是时候更新MainViewModel以处理来自添加和删除按钮的命令了。在下一章中,我们将增强添加操作,使其能够添加或编辑项目。因此,让我们相应地命名命令和方法:
-
首先,向
MainViewModel类中添加两个新的私有变量:private MediaItem selectedMediaItem; private int additionalItemCount = 1;additionalItemCount变量是一个临时变量,我们将用它来跟踪我们添加到列表中的新项目数量。计数器将帮助我们为每个新媒体项目生成唯一的 ID 和名称。"selectedMediaItem"是新的SelectedMediaItem属性的备份变量。 -
接下来,添加
SelectedMediaItem属性:public MediaItem SelectedMediaItem { get => selectedMediaItem; set { SetProperty(ref selectedMediaItem, value); ((RelayCommand)DeleteCommand) .RaiseCanExecuteChanged(); } }除了调用
SetProperty来通知 UISelectedMediaItem已更改外,我们还需要在新的DeleteCommand上调用RaiseCanExecuteChanged。 -
接下来,让我们添加
DeleteCommand和AddEditCommand以及它们对应的行为:public ICommand AddEditCommand { get; set; } public void AddOrEditItem() { // Note this is temporary until // we use a real data source for items. const int startingItemCount = 3; var newItem = new MediaItem { Id = startingItemCount + additionalItemCount, Location = LocationType.InCollection, MediaType = ItemType.Music, MediumInfo = new Medium { Id = 1, MediaType = ItemType.Music, Name = "CD" }, Name = $"CD {additionalItemCount}" }; allItems.Add(newItem); Items.Add(newItem); additionalItemCount++; } public ICommand DeleteCommand { get; set; } private void DeleteItem() { allItems.Remove(SelectedMediaItem); Items.Remove(SelectedMediaItem); } private bool CanDeleteItem() => selectedMediaItem != null;每个 UI 操作(
AddEditCommand和Delete)都有一个ICommand属性,以及每个命令的执行方法(AddOrEditItem和DeleteItem)。还有一个CanDeleteItem方法,它返回一个bool值,以指示用户是否已选择媒体项目。 -
在
MainViewModel构造函数的末尾,添加两行代码来初始化命令,将它们连接到相应的函数:DeleteCommand = new RelayCommand(DeleteItem, CanDeleteItem); // No CanExecute param is needed for this command // because you can always add or edit items. AddEditCommand = new RelayCommand(AddOrEditItem);
上述代码是修改后的MainViewModel类的一部分。请参考 GitHub 仓库中的MainViewModel2.txt文件以获取完整类的当前版本,该文件位于github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/main/Chapter03/Complete/MyMediaCollection/ViewModels。
我们的视图模型已被更新为使用命令。接下来,我们将更新视图以绑定到它们。
更新视图
我们的视图模型已经准备好了。现在可以安全地从MainWindow代码中移除所有的事件处理代码。完成时,它应该看起来像这样:
public sealed partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
public MainViewModel ViewModel => App.ViewModel;
}
MainWindow.xaml文件需要一些更新,以便添加项目和删除项目按钮能够与临时测试数据完全功能化。执行以下步骤以完成此操作:
-
更新
ListView以将视图模型中的SelectedItem属性绑定到SelectedMediaItem:<ListView Grid.Row="1" ItemsSource="{x:Bind ViewModel.Items}" SelectedItem="{x:Bind TwoWay data binding is required to allow the UI to update the view model. -
接下来,移动
网格。然后,创建一个点击事件处理程序,并将网格和两个按钮的属性设置为以下代码片段:<StackPanel Grid.Row="2" HorizontalAlignment="Right" Orientation="Horizontal"> <Button Command="{x:Bind ViewModel.AddEditCommand}" Content="Add Item" Margin="8,8,0,8"/> <Button Command="{x:Bind ViewModel.DeleteCommand}" Content="Delete Item" Margin="8"/> </StackPanel>每个按钮的
Command属性将绑定到视图模型中的新ICommand属性。当用户点击按钮时,将调用按钮的Command属性。 -
我们现在已更新项目以使用 MVVM。运行应用程序以查看其工作情况。
-
当它首次加载时,删除项目按钮将被禁用。选择列表中的一个项目,注意按钮会自动启用。如果您点击删除项目,所选项目将从列表中删除,按钮再次被禁用。
-
最后,点击添加项目几次,看看新项目是如何创建并添加到列表中的。每个新项目都使用我们在视图模型中创建的计数器具有一个独特的名称,如下面的截图所示:

图 3.2 – 添加和删除一些项目后的我的媒体收藏
MainWindow 现在有一个完全与任何 UI 关注点解耦的视图模型。这将允许您在项目中最大化单元测试覆盖率。
在我们结束之前,让我们看看如何通过使用 MVVM 框架(如 MVVM 工具包)来减少项目中代码的数量。
利用 MVVM 工具包
我们在本章中简要介绍了 MVVM 工具包。在本节中,我们将更新 MainViewModel 以了解我们如何消除对 BindableBase 类的需求并减少视图模型本身的代码量:
-
首先,在解决方案资源管理器中右键单击解决方案文件,并选择解决方案的管理 NuGet 包。
-
在 NuGet 窗口中,选择
CommunityToolkit.Mvvm。 -
在结果中选择 CommunityToolkit.Mvvm 包,并安装最新稳定版本(8.2.0 或更高版本)。
-
关闭 NuGet 窗口并打开
MainViewModel类。我们需要做的第一件事是使用 MVVM 工具包的源生成器,将MainViewModel更新为部分类,并使其继承自CommunityToolkit.Mvvm.ComponentModel.ObservableObject而不是我们自己的BindableBase类:public partial class MainViewModel : ObservableObject
注意
要了解 MVVM 工具包如何使用 .NET 源生成器生成公共属性和命令的更多信息,请查看 Microsoft Learn 上的文档:learn.microsoft.com/dotnet/communitytoolkit/mvvm/generators/overview。如果您不熟悉 .NET 源生成器,您也可以在 Microsoft Learn 的 .NET 文档中了解它们:learn.microsoft.com/dotnet/csharp/roslyn-sdk/source-generators-overview。
-
接下来,每个只在其设置器中调用
SetProperty的简单属性将被移除,并且后备变量将被更新以具有ObservableProperty属性。移除Mediums和Items属性,以及私有的mediums和items字段现在应如下所示:[ObservableProperty] private IList<string> mediums; [ObservableProperty] private ObservableCollection<MediaItem> items;类中剩下两个公共属性。让我们从
SelectedMedium开始。在更新命令后,我们将移除SelectedMediaItem。 -
SelectedMedium的设置器在调用SetProperty之后有一些自定义逻辑。创建一个新的部分方法OnSelectedMediumChanged来包含那段代码:partial void OnSelectedMediumChanged(string value) { Items.Clear(); foreach (var item in allItems) { if (string.IsNullOrWhiteSpace(value) || value == "All" || value == item.MediaType.ToString()) { Items.Add(item); } } }注意,我们还更新了
selectedMedium的使用为value。value参数包含新的selectedMedium值。你可能也注意到了在 Visual Studio 中的PopulateData方法内的一些绿色波浪线,这表明你应该使用生成的属性而不是直接使用私有的ObservableProperty成员。你可以通过将每个变量的首字母大写来更新所有这些使用。 -
接下来,我们将更新两个命令。将
RelayCommand属性添加到AddOrEditItem和DeleteItem方法中,并将它们重命名为AddEdit和Delete,以确保生成的命令具有与旧命令相同的名称。 -
移除两个命令以及它们在
MainViewModel构造函数中初始化的代码。构造函数的代码和两个命令方法的签名现在应如下所示:public MainViewModel() { PopulateData(); } [RelayCommand] public void AddEdit() { ... } [RelayCommand(CanExecute = nameof(CanDeleteItem))] public void Delete() { ... } private bool CanDeleteItem() => SelectedMediaItem != null;DeleteItem的属性还指示应使用CanDeleteItem来检查命令是否可以调用。 -
最后,让我们将
SelectedMediaItem属性替换为 MVVM 工具包生成的源属性。现有的公共属性告诉DeleteCommand应该检查CanExecute。移除该属性,并将私有的selectedMediaItem变量更新以添加两个属性:[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(DeleteCommand))] private MediaItem selectedMediaItem;除了
ObservableProperty属性之外,我们还添加了一个NotifyCanExecuteChangedFor属性,提供了由DeleteCommand生成的ICommand属性的名称。对我们不可见的生成属性将等同于以下内容:public MediaItem SelectedMediaItem { get => selectedMediaItem; set { if (SetProperty(ref selectedMediaItem, value)) { DeleteCommand.NotifyCanExecuteChanged(); } } }
这就是你需要做的全部。在视图中不需要进行任何更改,应用程序的工作方式与之前完全相同。我们所做的就是从 MainViewModel 中移除了一大块代码。
我们将在接下来的章节中继续使用 MVVM 工具包。现在,让我们回顾一下本章学到的内容。
概述
在本章中,我们对应用程序已经取得了一些进展。虽然它还没有连接到实时数据源,但我们已经有了在内存中添加和删除媒体集合项的方法。此外,项目已经被重构,使用 MainWindow 代码隐藏文件转换为新的 MainViewModel 类。新的 MainViewModel 类不依赖于 UI。最后,我们看到了如何将 MVVM Toolkit 集成到项目中可以减少我们视图模型中的样板代码。这些良好的软件设计习惯将在我们构建更多功能到项目中的后续章节中为我们服务。
在下一章中,我们将继续学习如何使用 MVVM 模式来编写健壮、可维护的 WinUI 应用程序。我们将涵盖一些更高级的 MVVM 主题,并学习在 WinUI 项目中进行窗口管理的技巧。
问题
-
MVVM 代表什么?
-
在 MVVM 模式中,哪一层通常定义业务实体?
-
请列举本章讨论的流行 MVVM 框架之一。
-
在 MVVM 应用程序中,每个视图模型类必须实现哪个接口?
-
在 .NET 中,哪种特殊的集合类型通过数据绑定通知 UI 集合的变化?
-
ComboBox和ListView控件的哪个属性用于获取或设置控件中当前选中的项? -
实现事件绑定的命令的哪个接口被实现?
第四章:高级 MVVM 概念
在学习了 MVVM 模式的基础知识及其在 WinUI 中的实现之后,现在是时候在此基础上构建知识库,以处理一些更高级的技术了。现在,你将学习如何在向项目中添加新依赖项时,保持组件松散耦合且可测试。
现代应用很少只有单个页面或窗口。有一些 MVVM 技巧可以用来在 ViewModel 命令之间导航页面,而不与 UI 层耦合。
在本章中,你将了解以下概念:
-
理解依赖注入(DI)的基本概念
-
利用依赖注入(DI)将
ViewModel类暴露给 WinUI 视图 -
使用 MVVM 和
x:Bind在 ViewModel 中处理额外的 UI 事件 -
使用 MVVM 和 DI 在页面之间导航
到本章结束时,你将更深入地理解 MVVM 模式,并将知道如何将你的 ViewModel 与任何外部依赖项解耦。
技术要求
要跟随本章中的示例,请参考第二章中的技术要求部分,配置开发环境和创建项目。
你可以在这里找到本章的代码文件:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/main/Chapter04。
理解依赖注入(DI)的基本概念
在我们项目中使用依赖注入(DI)之前,我们应该花些时间了解 DI 是什么以及为什么它是构建现代应用的基础。你经常会看到 DI 与另一个相关概念控制反转(IoC)一起提及。让我们通过以下方式来讨论这两个概念:
-
阐明它们之间的关系
-
本章将为你准备正确使用 DI 的知识
现代开发者使用依赖注入(DI)将依赖对象注入到类中,而不是在类内部创建对象的实例。有几种方法可以注入这些对象:
-
方法注入:对象作为参数传递给类中的方法
-
属性注入:对象通过属性设置
-
构造函数注入:对象作为构造函数参数传递
依赖注入(DI)最常见的方法是构造函数注入。在本章中,我们将使用属性注入和构造函数注入。方法注入将不会使用,因为在 .NET 项目中通常不使用方法来设置单个对象的值。大多数开发者使用属性来完成这个目的。
控制反转(IoC)的概念是,一个类不应该负责(或了解)其依赖项的创建。你正在反转对象创建的控制。这听起来有点像 DI,不是吗?好吧,DI 是在代码中实现这种 IoC 的方法之一。还有其他实现 IoC 的方法,包括以下内容:
-
委托:这个委托持有对可以用来创建和返回对象的方法的引用
-
事件:与委托类似,这通常用于与用户输入或其他外部动作相关联
-
服务定位器模式:这个模式用于在运行时注入服务的实现
当你将对象创建和使用职责分离时,它有助于代码重用并提高可测试性。
本章中将利用 DI 的类是视图和 ViewModel。所以,如果我们不会在那些类中创建对象实例,它们将在哪里创建?我们不是只是将紧密耦合移动到了别处吗?从某种意义上说,这是真的,但通过将其集中到项目的某个部分,即App.xaml.cs文件,我们可以最小化耦合。如果你还记得上一章,App类是我们处理应用程序范围操作和数据的地方。
我们将使用一个App类来管理应用程序的依赖。DI 容器负责创建和维护它所管理的对象的生存期。在容器中,对象的生存期通常是实例化(每个对象请求返回对象的新实例)或单例(每个对象请求返回相同的对象实例)。容器在App类中配置,并使实例对应用程序中的其他类可用。
在.NET 6 及更高版本中,DI 现在是.NET 本身的一部分。我们将利用.NET 中的主机构建器配置来注册应用程序的依赖关系,并在需要的地方解决它们。
有许多其他可以从 MVVM 框架中利用的 DI 实现。如果你想探索其中的一些,以下是它们各自的链接:
- Unity:这个 DI 实现支持所有类型的.NET 应用程序,并具有功能齐全的 IOC 容器(
unitycontainer.org/articles/introduction.html)
)
-
DryIoc:这个小型、轻量级的 IOC 容器支持.NET Standard 2.0 和.NET 4.5 及更高版本的应用程序(
github.com/dadhi/DryIoc) -
Prism:这个 MVVM 框架不支持 WinUI 3,但开发者仍然可以利用 DI 功能(
prismlibrary.com/docs/dependency-injection/index.html)
当我们在应用程序中实现代码时,这些概念将更容易理解。现在,是时候看看 DI 和 DI 容器在实际中的应用了。
使用 ViewModel 类与 DI 结合
今天,大多数流行的 MVVM 框架都包含一个 DI 容器来管理依赖项。因为 .NET 现在包含了自己的 DI 容器,所以我们将使用它。.NET 团队已经将之前与 ASP.NET Core 一起捆绑的 DI 容器整合进来。它既轻量又易于使用。幸运的是,这个容器现在可以通过 NuGet 包供所有类型的 .NET 项目使用:
- 打开上一章的项目或使用 GitHub 仓库中本章的
Start文件夹中的项目。在MyMediaCollection项目中,打开Microsoft.Extensions.Hosting:

图 4.1 – 微软的 DI NuGet 包
-
选择包并安装最新稳定版本。安装完成后,关闭
App.xaml.cs。我们在这里将进行一些更改以开始使用 DI 容器。DI 容器通过名为
IHostBuilder和IServiceCollection的接口实现依赖注入。正如其名称所暗示的,它们旨在通过共享宿主创建应用程序的服务集合。然而,我们可以将任何类型的类添加到容器中。其使用并不局限于服务。IServiceCollection构建容器,实现IServiceProvider接口。在以下步骤中,你将为应用程序添加依赖注入的支持。 -
你应该做的第一件事是在
App类中添加一个public属性,使宿主容器对项目可用:public static IHost HostContainer { get; private set; }在这里,
get是公共的,但属性有一个private set访问器。这限制了容器创建仅限于App类。别忘了在代码中添加所需的using语句:using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -
下一步是创建一个新的方法来初始化容器,将其设置为
public属性,并添加我们的第一个依赖项:private void RegisterComponents() { HostContainer = Host.CreateDefaultBuilder() .ConfigureServices(services => { services.AddTransient<MainViewModel>(); }).Build(); }在新的
RegisterComponents方法中,我们正在创建HostContainer和其服务集合,将MainViewModel注册为Build方法以创建和返回 DI 容器。尽管这不是严格必要的,但在向容器添加多个类型时,首先将依赖对象添加到服务集合中是一种良好的做法。我们很快将向容器中添加更多项。 -
最后,你需要在
App.OnLaunched事件处理程序中创建MainWindow实例之前调用RegisterComponents:protected override void OnLaunched(LaunchActivatedEventArgs args) { RegisterComponents(); m_window = new MainWindow(); m_window.Activate(); }
这就是创建和向应用程序公开 DI 容器所需的全部代码。现在,由于我们将 MainViewModel 的创建委托给了容器,你可以从 App 类中删除公开 MainViewModel 静态实例的属性。
使用由容器控制的 ViewModel 简单。继续打开 MainWindow.xaml.cs 并更新 ViewModel 属性以删除初始化。然后,在调用 InitializeComponent 之前,使用 App 类中的 HostContainer.Services.GetService 设置 ViewModel 属性的值:
public MainWindow()
{
ViewModel = App.HostContainer.Services
.GetService<MainViewModel>();
this.InitializeComponent();
}
public MainViewModel ViewModel;
如果现在构建并运行应用程序,它将像以前一样工作。然而,现在我们的MainViewModel实例将在App类中注册并由容器管理。随着新模型、视图模型、服务和其他依赖项被添加到项目中,它们可以被添加到RegisterComponents方法中的HostContainer。
我们将在本章的后面添加页面导航到应用程序。首先,让我们讨论事件到命令模式。
利用 x:Bind 和事件
在上一章中,我们将ViewModel命令绑定到Command属性的Command属性?对于这种情况,您有两个选择:
-
在.NET MAUI 社区工具包中使用自定义行为,例如
EventToCommandBehavior。这允许您将 ViewModel 中的命令与任何事件连接起来。 -
在视图中使用
x:Bind直接绑定到视图模型上的事件处理程序。
在这个应用程序中,我们将使用x:Bind。此选项将提供编译时类型检查和性能提升。如果您想了解更多关于.NET MAUI 社区工具包的信息,可以阅读 Microsoft Learn 上的文档:learn.microsoft.com/dotnet/communitytoolkit/maui/behaviors/event-to-command-behavior。
我们希望为我的媒体收藏应用程序的用户提供双击(或双击)列表中的行以查看或编辑其详细信息的选项。新的项目详情窗口将在下一节中添加。在此之前,双击项目将调用与添加项目按钮相同的代码,因为这将成为添加/编辑项目按钮:
-
首先,向
MainViewModel类添加一个名为ItemRowDoubleTapped的事件处理程序,该处理程序调用现有的AddEdit方法:public void ListViewDoubleTapped(object sender, DoubleTappedRoutedEventArgs args) { AddEdit(); } -
接下来,将
ListView.DoubleTapped事件绑定到 ViewModel:<ListView Grid.Row="1" ItemsSource="{x:Bind ViewModel.Items}" SelectedItem="{x:Bind ViewModel.SelectedMediaItem, Mode=TwoWay}" DoubleTapped="{x:Bind ViewModel Grid inside ListView.ItemTemplate to set the IsHitTestVisible property to False:<ListView.ItemTemplate>
...
</ListView.ItemTemplate>
现在运行应用程序时,您可以通过点击添加项目按钮或双击列表中的行来添加新项目。在下一节中,您将更新添加项目按钮以成为添加/编辑****项目按钮。
使用 MVVM 和 DI 进行页面导航
到目前为止,应用程序只包含一个窗口。现在,是时候通过添加一个宿主Frame和两个Page对象来实现页面导航,这样我们就可以处理添加新项目或编辑现有项目。新的Page将通过添加/编辑项目按钮或通过在列表中双击项目来访问。
将 MainWindow 迁移到 MainPage
如果你熟悉 UWP 应用开发,你应该已经理解了页面导航。在 UWP 中,应用程序仅由一个窗口组成。窗口的根处有一个 Frame 对象,它承载页面并处理它们之间的导航。为了在桌面 WinUI 3 应用中实现相同的结果,我们将创建一个新的 MainPage,将 MainWindow 中的所有 XAML 内容移动到 MainPage 中,并将 App 类更新为创建一个 Frame 作为新的 MainWindow 内容。然后我们可以通过导航到 MainPage 来显示相同的内容。让我们开始吧:
-
首先,在项目中添加一个名为
Views的新文件夹。 -
右键点击
Views文件夹,选择 添加 | 新建项。 -
在
MainPage上点击 创建。 -
打开
MainWindow.xaml并剪切掉整个Window的 XAML 内容。 -
打开
MainPage.xaml并将MainWindow中的 XAML 粘贴进去,替换掉空的Grid控件。 -
您还需要将
MainWindow中的xmlns声明从model复制粘贴到MainPage中:xmlns:model="using:MyMediaCollection.Model" -
在
MainWindow.xaml.cs中,删除ViewModel变量和从HostContainer中获取它的构造函数代码。将此相同的代码放入MainPage.xaml.cs中:public MainPage() { ViewModel = App.HostContainer.Services.GetService <MainViewModel>(); this.InitializeComponent(); } public MainViewModel ViewModel; -
接下来,打开
App.xaml.cs并在OnLaunched方法内部添加一些代码来创建一个rootFrame,将其添加到MainWindow中,并在激活窗口之前导航到MainPage:protected override void OnLaunched (LauchActivatedEventArgs args) { m_window = new MainWindow(); var rootFrame = new Frame(); RegisterComponents(); rootFrame.NavigationFailed += RootFrame_NavigationFailed; rootFrame.Navigate(typeof(MainPage), args); m_window.Content = rootFrame; m_window.Activate(); } private void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e) { throw new Exception($"Error loading page {e.SourcePageType.FullName}"); }我们还添加了一个事件处理程序来处理
Frame的导航失败。 -
确保将必要的
using语句添加到文件中:using System; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Navigation; using MyMediaCollection.Views;
如果你现在运行应用程序,它应该看起来和之前一样,但现在的控件嵌套在 Window 中的 Page 和 Frame 内。让我们添加第二个页面并准备开始在列表页面和详情页面之间导航。
添加 ItemDetailsPage
完整的 ItemDetailsPage.xaml 代码可以在 GitHub 上找到 (github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/blob/main/Chapter04/Complete/MyMediaCollection/Views/ItemDetailsPage.xaml)。您可以跟随本节中的步骤,或者在 GitHub 上查看最终代码。
注意
在我们将新的 ViewModel 添加到项目中并将其添加到 DI 容器以供视图使用之前,项目将无法成功编译。在添加 ViewModel 之前,我们需要创建一些服务以启用视图之间的导航和数据持久性。
我们将展示 Page 控件。Page 将被设置为内容,并通过我们创建的 Frame 进行导航。有关使用 WinUI 进行页面导航的更多信息,您可以阅读这篇 Microsoft Learn 文章:learn.microsoft.com/windows/apps/design/basics/navigate-between-two-pages?tabs=wasdk。
要添加 ItemDetailsPage,请按照以下步骤操作:
-
在解决方案资源管理器中的Views文件夹上右键单击,然后选择添加 | 新项。
-
在新项目对话框中,选择
ItemDetailsPage。 -
页面上将有几个输入控件,具有一些常见的属性。首先,在顶级
Grid控件之前,在Page.Resources部分添加三个样式:<Page.Resources> <Style x:Key="AttributeTitleStyle" TargetType="TextBlock"> <Setter Property="HorizontalAlignment" Value="Right"/> <Setter Property="VerticalAlignment" Value="Center"/> </Style> <Style x:Key="AttributeValueStyle" TargetType="TextBox"> <Setter Property="HorizontalAlignment" Value="Stretch"/> <Setter Property="Margin" Value="8"/> </Style> <Style x:Key="AttributeComboxValueStyle" TargetType="ComboBox"> <Setter Property="HorizontalAlignment" Value="Stretch"/> <Setter Property="Margin" Value="8"/> </Style> </Page.Resources>在下一步中,我们可以将
AttributeTitleStyle分配给每个TextBlock,AttributeValueStyle分配给每个TextBox,以及AttributeComboValueStyle分配给每个ComboBox。如果您以后需要添加任何其他属性到输入标签,您只需更新AttributeTitleStyle,属性将自动应用于使用该样式的每个TextBlock。 -
顶级
Grid将包含三个子Grid控件,将视图分为三个区域——一个标题、输入控件和Grid.RowDefinitions如下:<Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> -
标题区域将只包含一个
TextBlock。您可以根据自己的喜好设计这个区域:<TextBlock Text="Item Details" FontSize="18" Margin="8"/> -
输入区域包含一个
Grid,有四个RowDefinitions和两个ColumnDefinitions用于标签和用户可以当前编辑的四个字段的输入控件:<Grid Grid.Row="1"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="200"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <TextBlock Text="Name:" Style="{StaticResource AttributeTitleStyle}"/> <TextBox Grid.Column="1" Style="{StaticResource AttributeValueStyle}" Text="{x:Bind ViewModel.ItemName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Text="Media Type:" Grid.Row="1" Style="{StaticResource AttributeTitleStyle}"/> <ComboBox Grid.Row="1" Grid.Column="1" Style="{StaticResource AttributeCombox ValueStyle}" ItemsSource="{x:Bind ViewModel.ItemTypes}" SelectedValue="{x:Bind ViewModel .SelectedItemType, Mode=TwoWay}"/> <TextBlock Text="Medium:" Grid.Row="2" Style="{StaticResource AttributeTitleStyle}"/> <ComboBox Grid.Row="2" Grid.Column="1" Style="{StaticResource AttributeComboxValueStyle}" ItemsSource="{x:Bind ViewModel.Mediums}" SelectedValue="{x:Bind ViewModel .SelectedMedium, Mode=TwoWay}"/> <TextBlock Text="Location:" Grid.Row="3" Style="{StaticResource AttributeTitleStyle}"/> <ComboBox Grid.Row="3" Grid.Column="1" Style="{StaticResource AttributeComboxValueStyle}" ItemsSource="{x:Bind ViewModel.LocationTypes}" SelectedValue="{x:Bind ViewModel .SelectedLocation,Mode=TwoWay}"/> </Grid> -
该项目的
名称是一个自由文本输入字段,而其他的是ComboBox控件,允许用户从绑定到ItemsSource的列表中选择值。顶级Grid的最后一个子元素是一个右对齐的水平StackPanel,包含保存和取消按钮:<StackPanel Orientation="Horizontal" Grid.Row="2" HorizontalAlignment="Right"> <Button Content="Save" Margin="8,8,0,8" Command="{x:Bind ViewModel.SaveCommand}"/> <Button Content="Cancel" Margin="8" Command="{x:Bind ViewModel.CancelCommand}"/> </StackPanel>
下一个阶段是添加接口和服务,让我们继续这个工作。
添加新的接口和服务
现在我们需要在应用程序中管理多个页面,我们需要一些服务来集中页面管理和从ViewModel代码中抽象细节。首先,在项目中创建Services和Interfaces文件夹。每个服务将实现一个接口。这个接口将用于依赖注入(DI),如果将来您要向测试项目添加单元测试,它也会被使用。
创建导航服务
我们需要的第一个服务是Interfaces文件夹中的INavigationService接口。该接口定义了获取当前页面名称、导航到特定页面或导航回上一页的方法:
public interface INavigationService
{
string CurrentPage { get; }
void NavigateTo(string page);
void NavigateTo(string page, object parameter);
void GoBack();
}
现在,在Services文件夹中创建一个NavigationService类。在类定义中,确保NavigationService实现了INavigationService接口。完整的类可以在 GitHub 上查看(github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/blob/master/Chapter04/Complete/MyMediaCollection/Services/NavigationService.cs)。让我们讨论一些亮点。
在 MVVM 中,导航服务的目的是在应用程序中存储一组可用的页面,以便当其NavigateTo方法被调用时,该服务可以找到一个与请求的名称或类型匹配的页面并导航到它。
页面集合将存储在 ConcurrentDictionary<T> 集合中。ConcurrentDictionary<T> 的功能类似于标准的 Dictionary<T>,但它可以自动添加锁,以防止多个线程同时更改字典:
private readonly IDictionary<string, Type> _pages = new
ConcurrentDictionary<string, Type>();
当你创建 NavigationService 并将其添加到 DI 容器之前,将调用 Configure 方法。这个方法不是 INavigationService 接口的一部分,并且不会对从容器中消费服务的类可用。这里有一个检查,以确保视图只添加到服务一次。我们检查字典以确定是否存在相同数据类型的任何页面。如果这个条件是 true,则页面已经注册:
public void Configure(string page, Type type)
{
if (_pages.Values.Any(v => v == type))
{
throw new ArgumentException($"The {type.Name} view
has already been registered under another
name.");
}
_pages[page] = type;
}
这些是服务中三个导航方法的实现。两个 NavigateTo 方法导航到特定的页面,第二个方法提供了向页面传递参数的能力。第三个是 GoBack,它执行你想象中的操作:导航到应用程序中的上一页。它们封装了 Frame 导航调用,以将 UI 实现从使用此服务的视图模型中抽象出来:
public void NavigateTo(string page)
{
NavigateTo(page, null);
}
public void NavigateTo(string page, object parameter)
{
if (!_pages.ContainsKey(page))
{
throw new ArgumentException($"Unable to find a page
registered with the name {page}.");
}
AppFrame.Navigate(_pages[page], parameter);
}
public void GoBack()
{
if (AppFrame?.CanGoBack == true)
{
AppFrame.GoBack();
}
}
我们已经准备好开始使用 NavigationService,但首先,让我们为应用程序创建一个数据服务。
注意
如果你愿意,可以跳到下一节实现服务。DataService 和 IDataService 代码可在 GitHub 上的完成解决方案中找到:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/master/Chapter04/Complete/MyMediaCollection.
创建数据服务
MainViewModel 的 MainPage 上的数据。这不会在多个页面之间很好地工作。通过使用数据服务,视图模型不需要知道数据是如何创建或存储的。
目前,数据仍然是样本记录,这些记录在会话之间不会保存。稍后,我们可以更新数据服务以从数据库中保存和加载数据,而无需更改使用数据的视图模型。
第一步是将名为 IDataService 的接口添加到 Interfaces 文件夹中:
public interface IDataService
{
IList<MediaItem> GetItems();
MediaItem GetItem(int id);
int AddItem(MediaItem item);
void UpdateItem(MediaItem item);
IList<ItemType> GetItemTypes();
Medium GetMedium(string name);
IList<Medium> GetMediums();
IList<Medium> GetMediums(ItemType itemType);
IList<LocationType> GetLocationTypes();
int SelectedItemId { get; set; }
}
这些方法应该让你想起前面的章节,但让我们简要回顾一下每个方法的用途:
-
GetItems: 返回所有可用的媒体项 -
GetItem: 根据提供的id查找媒体项 -
AddItem: 向集合中添加新的媒体项 -
UpdateItem: 更新集合中的媒体项 -
GetItemTypes: 获取媒体项类型的列表 -
GetMedium: 根据提供的名称获取Medium -
GetMediums: 这两个方法要么获取所有可用的媒体,要么获取提供的ItemType的任何可用媒体 -
GetLocationTypes: 获取所有可用的媒体位置 -
SelectedItemId: 在MainPage上持久化所选项的 ID
现在,在 Services 文件夹中创建 DataService 类。确保 DataService 在类定义中实现了 IDataService。
同样,我们只会审查部分代码。你可以在 GitHub 上审查整个实现(github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/blob/master/Chapter04/Complete/MyMediaCollection/Services/DataService.cs)。DataService 中的数据将保存在四个列表和 SelectedItemId 属性中:
private IList<MediaItem> _items;
private IList<ItemType> _itemTypes;
private IList<Medium> _mediums;
private IList<LocationType> _locationTypes;
public int SelectedItemId { get; set; }
从 MainViewModel 复制 PopulateItems 方法,并将其修改为使用 List<T> 集合,并为每个项目添加 Location 属性赋值。
首先创建三个 MediaItem 对象:
var cd = new MediaItem
{
Id = 1,
Name = "Classical Favorites",
MediaType = ItemType.Music,
MediumInfo = _mediums.FirstOrDefault(m => m.Name ==
"CD"),
Location = LocationType.InCollection
};
var book = new MediaItem
{
Id = 2,
Name = "Classic Fairy Tales",
MediaType = ItemType.Book,
MediumInfo = _mediums.FirstOrDefault(m => m.Name ==
"Hardcover"),
Location = LocationType.InCollection
};
var bluRay = new MediaItem
{
Id = 3,
Name = "The Mummy",
MediaType = ItemType.Video,
MediumInfo = _mediums.FirstOrDefault(m => m.Name ==
"Blu Ray"),
Location = LocationType.InCollection
};
然后,初始化 _items 列表,并添加你刚刚创建的三个 MediaItem 对象:
_items = new List<MediaItem>
{
cd,
book,
bluRay
};
还有三个其他方法用于预填充示例数据:PopulateMediums、PopulateItemTypes 和 PopulateLocationTypes。所有这些方法都从 Data 服务构造函数中调用。这些方法将在以后更新,以使用 SQLite 数据存储进行数据持久化。
大多数 Get 方法的实现都非常直接。GetMediums(ItemType itemType) 方法使用 Medium 对象来处理选定的 ItemType:
public IList<Medium> GetMediums(ItemType itemType)
{
return _mediums
.Where(m => m.MediaType == itemType)
.ToList();
}
注意
如果你不太熟悉 LINQ 表达式,Microsoft 在这个主题上有一些很好的文档:learn.microsoft.com/dotnet/csharp/programming-guide/concepts/linq/。
AddItem 和 UpdateItems 方法也很简单。它们向 _items 集合添加和更新内容:
public int AddItem(MediaItem item)
{
item.Id = _items.Max(i => i.Id) + 1;
_items.Add(item);
return item.Id;
}
public void UpdateItem(MediaItem item)
{
var idx = -1;
var matchedItem = (from x in _items
let ind = idx++
where x.Id == item.Id
select ind).FirstOrDefault();
if (idx == -1)
{
throw new Exception("Unable to update item. Item
not found in collection.");
}
_items[idx] = item;
}
AddItem 方法有一些基本的逻辑来找到最高的 Id 并将其增加 1 以用于新项目的 Id。Id 也会返回给调用方法,以防调用者需要这些信息。
所有服务都已创建。当应用程序启动时,是时候设置它们并消费它们在视图模型中。
通过消费服务提高可维护性
在视图模型中使用服务之前,打开 App.xaml.cs 中的 RegisterServices 方法,并将以下代码添加到 DI 容器中注册新服务,并注册一个新的 ItemDetailsViewModel(尚未创建)。我们还在方法中添加了一个参数,将其传递给 NavigationService 构造函数。这将提供对 Frame 的访问,以便进行页面导航:
private IServiceProvider RegisterServices(Frame rootFrame)
{
var navigationService = new NavigationService(rootFrame);
navigationService.Configure(nameof(MainPage),
typeof(MainPage));
navigationService.Configure(nameof(ItemDetailsPage),
typeof(ItemDetailsPage));
HostContainer = Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
services.AddSingleton<INavigationService>
(navigationService);
services.AddSingleton<IDataService, DataService>();
services.AddTransient<MainViewModel>();
services.AddTransient<ItemDetailsViewModel>();
}).Build();
}
INavigationService 和 IDataService 都注册为 单例。这意味着容器中将只存储每个的单个实例。这些服务中持有的任何状态都将跨所有使用它们的类共享。
你会注意到,当我们注册INavigationService时,我们正在将已创建的实例传递给构造函数。这是 Microsoft 的 DI 容器以及大多数其他 DI 容器的一个特性。它允许在实例被添加到容器之前进行初始化和配置。
我们需要对MainViewModel进行一些更改以消费IDataService和INavigationService,更新PopulateData方法,并在调用AddEdit()时导航到ItemDetailsPage:
-
首先向
MainViewModel添加INavigationService和IDataService的属性:private INavigationService _navigationService; private IDataService _dataService;不要忘记为
MyMediaCollection.Interfaces添加一个using语句。 -
接下来,更新构造函数以接收和存储服务:
public MainViewModel(INavigationService navigationService, IDataService dataService) { _navigationService = navigationService; _dataService = dataService; PopulateData(); }等等,我们已经向构造函数中添加了两个参数,但还没有更改将它们添加到 DI 容器的代码。这是怎么工作的呢?嗯,容器足够智能,可以传递它们,因为这两个接口也已经注册了。非常酷!
-
接下来,更新
PopulateData以从_dataService获取视图模型所需的数据:public void PopulateData() { items.Clear(); foreach(var item in _dataService.GetItems()) { items.Add(item); } allItems = new ObservableCollection<MediaItem>(Items); mediums = new ObservableCollection<string> { AllMediums }; foreach(var itemType in _dataService .GetItemTypes()) { mediums.Add(itemType.ToString()); } selectedMedium = Mediums[0]; }你需要向
mediums集合中添加一个名为AllMediums的字符串常量,其值为"All",因为它不是持久化数据的一部分。它仅用于 UI 过滤器。务必将此常量定义添加到MainViewModel中。 -
最后,当隐藏的
AddEditCommand调用AddEdit方法时,而不是将硬编码的项目添加到集合中,你将在导航到ItemDetailsPage时传递selectedItemId作为参数:private void AddEdit() { var selectedItemId = -1; if (SelectedMediaItem != null) { selectedItemId = SelectedMediaItem.Id; } _navigationService.NavigateTo("ItemDetailsPage", selectedItemId); }
对于MainViewModel来说,这就完成了。现在让我们来处理ItemDetailsPage。
在 ItemDetailsPage 中处理参数
要在导航期间接受从另一个页面传递的参数,你必须覆盖ItemDetailsPage.xaml.cs中的OnNavigatedTo方法。NavigationEventArgs参数包含一个名为Parameter的属性。在我们的情况下,我们传递了一个包含所选项目Id的int。将此Parameter属性转换为int并将其传递给ViewModel上的一个名为InitializeItemDetailData的方法,该方法将在下一节中创建:
protected override void OnNavigatedTo(NavigationEventArgs
e)
{
base.OnNavigatedTo(e);
var itemId = (int)e.Parameter;
if (itemId > 0)
{
ViewModel.InitializeItemDetailData(itemId);
}
}
在下一节中,我们将添加拼图的最后一块,即ItemDetailsViewModel类。
创建 ItemDetailsViewModel 类
要在应用程序中添加或编辑项目,你需要一个视图模型来绑定到ItemDetails页面。在ItemDetailsViewModel的ViewModels文件夹上右键单击。
该类将像MainViewModel一样继承自ObservableObject。完整的类可以在 GitHub 上找到:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/blob/master/Chapter04/Complete/MyMediaCollection/ViewModels/ItemDetailsViewModel.cs。让我们回顾一下该类的一些重要成员。
构造函数从容器中接收两个服务并调用PopulateLists以从数据服务中填充ComboBox数据:
public ItemDetailsViewModel(INavigationService
navigationService, IDataService dataService)
{
_navigationService = navigationService;
_dataService = dataService;
PopulateLists();
}
一个名为InitializeItemDetailData的public方法将接受ItemDetailsPage.OnNavigatedTo传递的itemId参数。它将调用方法来填充列表并初始化一个IsDirty标志来启用或禁用SaveCommand:
public void InitializeItemDetailData(int itemId)
{
_selectedItemId = itemId;
PopulateExistingItem(_dataService);
IsDirty = false;
}
PopulateExistingItem方法将在页面处于编辑模式时添加现有项目数据,而PopulateLists方法,从构造函数中调用,填充要绑定到视图的下拉数据:
private void PopulateExistingItem(IDataService dataService)
{
if (_selectedItemId > 0)
{
var item = _dataService.GetItem(_selectedItemId);
Mediums.Clear();
foreach (string medium in dataService.GetMediums
(item.MediaType).Select(m => m.Name))
Mediums.Add(medium);
_itemId = item.Id;
ItemName = item.Name;
SelectedMedium = item.MediumInfo.Name;
SelectedLocation = item.Location.ToString();
SelectedItemType = item.MediaType.ToString();
}
}
private void PopulateLists()
{
ItemTypes.Clear();
foreach (string iType in Enum.GetNames
(typeof(ItemType)))
ItemTypes.Add(iType);
LocationTypes.Clear();
foreach (string lType in Enum.GetNames
(typeof(LocationType)))
LocationTypes.Add(lType);
Mediums = new TestObservableCollection<string>();
}
这个视图模型的大多数属性都很直接,但SelectedItemType有一些逻辑,根据选定的ItemType重新填充Mediums列表。例如,如果你正在向收藏夹添加一本书,就没有必要在选择列表中看到 DVD 或 CD 媒体。我们将在OnSelectedItemTypeChanged中处理这个自定义逻辑:
partial void OnSelectedItemTypeChanged(string value)
{
IsDirty = true;
Mediums.Clear();
if (!string.IsNullOrWhiteSpace(value))
{
foreach (string med in _dataService.GetMediums
((ItemType)Enum.Parse(typeof(ItemType),
SelectedItemType)).Select(m => m.Name))
Mediums.Add(med);
}
}
最后,让我们看看SaveCommand和CancelCommand将调用来保存并导航回MainPage的代码:
private void Save()
{
MediaItem item;
if (_itemId > 0)
{
item = _dataService.GetItem(_itemId);
item.Name = ItemName;
item.Location = (LocationType)Enum.Parse
(typeof(LocationType), SelectedLocation);
item.MediaType = (ItemType)Enum.Parse(typeof
(ItemType), SelectedItemType);
item.MediumInfo = _dataService.GetMedium
(SelectedMedium);
_dataService.UpdateItem(item);
}
else
{
item = new MediaItem
{
Name = ItemName,
Location = (LocationType)Enum.Parse
(typeof(LocationType), SelectedLocation),
MediaType = (ItemType)Enum.Parse(typeof
(ItemType), SelectedItemType),
MediumInfo = _dataService.GetMedium
(SelectedMedium)
};
_dataService.AddItem(item);
}
_navigationService.GoBack();
}
private void Cancel()
{
_navigationService.GoBack();
}
在你运行应用程序以测试新页面之前,需要做的另一个更改是从ItemDetailsPage.xaml.cs中消费ItemDetailsViewModel:
public ItemDetailsPage()
{
ViewModel = App.HostContainer.Services.GetService
<ItemDetailsViewModel>();
this.InitializeComponent();
}
public ItemDetailsViewModel ViewModel;
现在,运行应用程序并尝试添加或编辑一个项目——你应该能看到新页面。如果你正在编辑,你也应该看到控件中的现有项目数据:

图 4.2 – 填充了编辑数据的项详情页面
太好了!现在当你保存时,你应该能看到任何添加的记录或编辑的数据出现在MainPage上。我们的项目开始真正成形。让我们回顾一下在本章中我们学到的关于 WinUI 和 MVVM 的知识。
摘要
在本章中,你已经学到了很多关于 MVVM 和 WinUI 页面导航的知识。你学习了如何在应用程序中创建和消费服务,并利用依赖注入(DI)和 DI 容器来保持视图模型和服务的松耦合。理解和使用 DI 是构建可测试、可维护代码的关键。此时,你应该有足够的知识来创建一个健壮、可测试的 WinUI 应用程序。
在下一章中,你将学习更多关于 WinUI 3 中可用的控件和库。
问题
-
DI 和 IoC 如何相关?
-
你如何在 WinUI 应用程序中导航到上一页?
-
我们使用什么对象来管理依赖关系?
-
使用微软的 DI 容器,你可以调用什么方法来获取对象实例?
-
查询内存中对象的框架的名称是什么?
-
你可以访问哪个事件参数属性来获取传递给另一个
Page的参数? -
哪种字典类型可以在多线程之间安全使用?
第五章:探索 WinUI 控件
WinUI 3 和 Windows App SDK 为开发 Windows 桌面应用程序的开发者提供了许多控件和 API。WinUI 控件包括一些之前未提供给 Windows 开发者的控件,以及已在 WinUI 2.x 和 UWP 中可用的更新后的控件。使用这些新控件和 WinUI 3 的更新控件,可以在之前不支持此完整组件套件的旧版 Windows 中使用它们。开发者还可以利用 Windows App SDK API 来直接访问 Windows 功能。
本章将涵盖以下主题:
-
深入了解 WinUI 3 中可用的控件
-
探索 Windows 的 WinUI 3 画廊 应用程序以了解 WinUI 控件和设计指南
-
其他 Windows App SDK API 如何为 Windows 开发者提供对电源管理、通知等功能的访问
-
如何在您的应用程序中实现
SplitButton和TeachingTip控件
到本章结束时,你将更深入地了解 WinUI 3 中的控件和库。你还将能够舒适地使用 Windows 的 WinUI 3 画廊应用程序来探索控件并找到演示如何使用它们的示例。
技术要求
要跟随本章中的示例,请参考 第二章 的 技术要求 部分,配置开发环境和创建 项目。
本章的源代码可在 GitHub 上找到:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/master/Chapter05.
了解 WinUI 为开发者提供了什么
在 第一章 中,WinUI 简介,你了解了一些关于 WinUI 和 UWP 源起的背景信息。该章节还涵盖了 WinUI 各个版本中可用的一些控件。现在,让我们更详细地探索其中的一些控件。让我们先看看 WinUI 3 中开发者可用的控件列表:

图 5.1 – WinUI 3 控件列表
这是 Windows App SDK 中开发者可用的控件相当广泛的列表。
小贴士
要获取可用控件的最新列表,你可以查看 Microsoft Learn 上的此页面:learn.microsoft.com/windows/apps/design/controls/#alphabetical-index.
如果您之前开发过 Windows 应用程序,那么这些控件名称中的大多数可能对您来说都很熟悉。在接下来的部分中,我们将概述一些您可能之前没有见过的控件。
动画视觉播放器(Lottie)
如 图 5.2 所示的 AnimatedVisualPlayer 控件是一个可以显示 Lottie 动画 的 WinUI 控件。Lottie 是一个开源库,可以在 Windows、网页、iOS 和 Android 上解析和显示动画。这些动画由设计师在如 Adobe After Effects 等工具中创建,并以 JSON 格式导出。您可以在官方网站 airbnb.io/lottie/#/ 上了解更多关于 Lottie 动画的信息:

图 5.2 – AnimatedVisualPlayer 控件
导航视图
NavigationView 提供了一个用户友好的页面导航系统。使用它为用户提供快速访问所有应用程序的最高级页面。NavigationView 可以配置为在应用程序顶部显示为一个菜单,每个页面的链接在页面顶部显示为一个标签:

图 5.3 – 在页面顶部配置的 NavigationView
NavigationView 还可以配置为出现在页面左侧。这是一个对 Windows 和 Android 用户来说应该很熟悉的视图。这种菜单格式通常被称为 汉堡菜单。这是菜单折叠状态下的视图外观:

图 5.4 – 折叠的左侧 NavigationView 控件
在任何配置中,NavigationView 都可以隐藏或显示一个返回箭头以导航到上一页,并有一个 设置 菜单项以显示应用程序的设置页面。如果您的应用程序没有设置页面,则应隐藏此项目。当通过点击返回箭头下方的 汉堡 图标展开左侧菜单时,将显示菜单文本以及相应的图标:

图 5.5 – 展开的左侧 NavigationView 控件
菜单上的所有内容都是可配置的。您可以使用 NavigationView 对页面进行分组。
垂直视差视图
ParallaxView 控件将这一概念引入您的 WinUI 应用程序。您可以将 ParallaxView 与 ListView 和背景图像链接,当 ListView 滚动时,它将提供垂直视差效果。有设置可以控制列表滚动与图像滚动量之间的关系。当不过度使用时,这种效果对用户有很大的影响:

图 5.6 – ParallaxView 控件滚动到列表顶部

图 5.7 – ParallaxView 控件部分滚动通过列表
评分控件
每个人都熟悉评分控件。您在购物网站、流媒体应用程序和在线调查中都能看到它们。WinUI 的 RatingControl 控件允许用户从 1 到 5 星对应用程序中的项目进行评分:

图 5.8 – RatingControl 显示用户的评分
该控件还可以允许您通过在控件上向左滑动来清除评分,并在用户提供自己的评分之前显示一个占位符值。应用程序通常将占位符值用作向用户显示其他用户给出的平均评分的手段。
现在我们已经讨论了一些新增的 WinUI 控件,让我们探索一个 Windows 应用程序,它使您能够自行轻松探索它们。
探索 Windows 的 WinUI 3 画廊应用程序
由于有如此多的强大且可配置的 WinUI 控件可用,微软的 WinUI 团队决定创建一个应用程序,允许 Windows 开发者探索甚至尝试这些控件。WinUI 3 画廊 是一个了解控件、决定哪些控件适合您的应用程序以及获取一些示例代码和设计指导的绝佳工具。
注意
对于 UWP 开发者,还有一个 WinUI 2 画廊 应用程序。这两个应用程序过去是单个应用程序,称为 XAML 控件画廊。
要安装 WinUI 3 画廊应用程序,您可以访问其 Microsoft Store 网页(apps.microsoft.com/store/detail/winui-3-gallery/9P3JFPWWDZRC)或启动 Windows 的 Microsoft Store 应用程序并搜索 WinUI 3 画廊。画廊应用程序本身是开源的。您可以在 GitHub 上浏览代码以了解更多信息:github.com/microsoft/WinUI-Gallery。
安装完成后,启动应用程序:

图 5.9 – WinUI 3 画廊应用程序
在 NavigationView 控件中,左侧菜单允许快速浏览或搜索画廊中的各种控件。
注意
如果您搜索上一节中显示的控件,您可能会注意到本章中提供的控件截图是从 WinUI 3 画廊应用程序中获取的。
了解 ScrollViewer 控件
假设您正在考虑向应用程序中的页面区域添加滚动功能。在画廊应用程序中,您可以点击 ScrollViewer 控件:

图 5.10 – XAML 控件画廊中的 ScrollViewer 控件详细页面
控件详细信息页面包括几个部分。标题区域提供了对控件及其用途的简要描述。右侧面板提供了有用的链接,指向在线文档、图库中的其他相关控件,以及一个链接,用于对当前图库页面提供反馈。
页面的中间区域本身包含三个部分:一个渲染控件、一个功能控件和一个属性面板。使用属性面板,你可以更新 ScrollViewer 的一些属性,并立即看到渲染控件更新。在底部中心,你会找到一个包含渲染控件源代码的面板。源代码面板对于将代码复制到用作你自己的项目起点非常方便。
图库应用程序的设计也很好地响应了调整大小。如果你将窗口的右侧拖动到尽可能窄,你会看到左侧和右侧的面板折叠,中心区域将重新对齐为一个单独的垂直列:

图 5.11 – 图库应用程序水平调整大小
你可以想象这种视图非常适合在 Microsoft Surface Go 笔记本电脑 或其他小型 PC 上使用。
花些时间探索图库中的控件。一些代码示例可能相当长。尝试更改一些控件属性,并注意 XAML 代码如何更新以反映新的属性值。这是一种学习 XAML 并熟悉 WinUI 控件的好方法。
现在,让我们来浏览一下 WinUI 3 的新特性。
查看 WinUI 3 和 Windows App SDK 的新增功能
虽然 WinUI 3 是一个主要版本,但与 WinUI 2.x 相比,新增功能并不多。这可能会让许多人感到惊讶,但仅仅创建 WinUI 3 和 Windows App SDK 作为独立发布就已经是一项相当大的任务。我们将在以下小节中查看最显著的功能。
向后兼容性
为了使 WinUI 应用程序与更多版本的 Windows 兼容(它支持 Windows 10 版本 1809 及以后的版本),WinUI 团队不得不从 Windows SDK 中提取 UWP 控件,并将它们移动到 Windows App SDK 中的新 Microsoft.UI.* 库。这项工作的成果不仅为更多版本的 Windows 提供了兼容性,还使开发者无论使用 .NET 还是 Win32 作为底层平台,都可以消费 WinUI。C# 开发者可以使用 WinUI 为桌面项目构建 .NET 应用程序,而 C++ 开发者可以在 Win32 平台上消费 WinUI。
流畅的用户界面和现代的外观
维护 Windows Presentation Foundation (WPF), WinForms 和 Microsoft Foundation Class (MFC) 应用程序的开发者不容易像在 WinUI 中那样使用 Fluent UI 来实现现代的 Windows 风格。我们将在 第七章 中深入探讨 Fluent UI,Windows 应用程序 的 流畅设计系统。
Visual Studio 工具
Visual Studio 现在可以添加 WinUI 项目模板,而无需从 Visual Studio Marketplace 安装单独的扩展。正如在开篇章节中讨论的,WinUI 支持可以与 Visual Studio 的 .NET 桌面开发 工作负载一起添加。在 Visual Studio 中以 WinUI 开始新项目实际上就像去 文件 | 新建项目 一样简单。
WebView2 控件
WinUI 3 中开发者可用的新控件是 WebView2。这个 WebView 的新版本是基于基于 Chromium 的 Microsoft Edge 网络浏览器构建的。如果你需要将一些网络内容嵌入到你的应用中,WebView2 是你应该使用的控件,以确保与现代网络标准的最大兼容性。
这里是 WebView2 在 WinUI 3 图库 应用中运行的截图:

图 5.12 – WebView2 在 WinUI 3 图库应用中运行
网络内容可以从网络或本地网络、本地存储中的文件或嵌入到应用程序的二进制文件中的文件加载到控件中。以下是从每种类型源加载到 WebView2 的示例:
<!-- Load a website. -->
<WebView2 x:Name="WebView_Web"
Source="https://www.packtpub.com"/>
<!— Load web files from local storage. -->
<WebView2 x:Name="WebView_Local" Source="ms-appdata:
///local/site/index.html"/>
<!— Load web files embedded from the app package. -->
<WebView2 x:Name="WebView_Embedded" Source="ms-appx-
web:///web/index.html"/>
如果你有一个为网络编写的现有应用程序,那么 WebView2 控件是将其集成到你的新 WinUI 客户端应用程序中的绝佳方式。
让我们暂时转换一下话题,讨论一些 Windows App SDK 中 WinUI 3 控件之外的功能。
Windows App SDK 中的新功能
到现在为止,你应该知道 WinUI 3 是 Windows App SDK 的一部分。Windows App SDK 有自己的版本,而 WinUI 3 将始终被称为 WinUI 3。在撰写本文时,Windows App SDK 的最新稳定版本是 1.4。在本节中,我们将回顾一些 Windows App SDK 的功能,这些功能位于 WinUI 控件之外。要查看当前 Windows App SDK 功能的完整列表,你可以查看 Microsoft Learn 上的此页面:learn.microsoft.com/windows/apps/windows-app-sdk/#windows-app-sdk-features。
电源管理
你的应用可以作为 Windows App SDK 应用生命周期 API 的一部分订阅并响应电源管理事件。SDK 中 PowerManager 类公开的一些事件包括以下内容:
-
BatteryStatusChanged:当系统电池状态发生变化时触发。使用BatteryStatus属性来获取当前状态。 -
DisplayStatusChanged:当运行应用程序的显示状态发生变化时触发。使用DisplayStatus属性来获取当前状态。 -
EffectivePowerModeChanged:当系统的有效电源模式发生变化时触发。这些也可以被称为电源计划,例如省电模式和性能模式。使用EffectivePowerMode属性来获取当前模式。 -
SystemSuspendStatusChanged:当系统挂起或恢复时触发。在 UWP 中,这些类型的事件是App类内置生命周期的一部分。使用SystemSuspendStatus属性来获取当前状态。 -
UserPresenceStatusChanged:当系统检测到用户状态变化时触发。使用UserPresenceStatus属性来获取当前状态。
可以在 Microsoft Learn 上查看PowerManager事件的完整列表和属性:learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.windows.system.power.powermanager。
对这些事件变化的响应可以帮助你的应用程序在何时以及如何执行资源密集型操作方面变得更加智能。你可能有一些后台处理,这些处理只有在系统连接电源或电池容量高于一定百分比时才应该执行。你也可以选择在用户屏幕变暗或关闭时暂停屏幕动画或仪表板更新,以节省电力。
接下来,让我们讨论 Windows App SDK 中的窗口功能。
窗口管理
Windows App SDK 提供了一些有限的窗口管理功能,可以通过使用AppWindow类来利用。通过使用一些互操作 API,你的应用程序可以获取当前窗口的HWND和WindowId值。在 Win32 开发中,可以使用WindowId来获取当前AppWindow对象的引用:
var appWindow = Microsoft.UI.Windowing.AppWindow
.GetFromWindowId(windowId)
AppWindow上的大多数属性都是只读的。你可以获取有关其大小、位置、可见性和其所有者的WindowId值等信息。AppWindow的一个可写属性是Title。设置Title允许你更改当前窗口标题栏中的文本。
推送通知和应用程序通知
推送通知可以用来在不通知用户的情况下与应用程序交互,或者在 Windows 中显示一个托盘通知。后者被认为是应用程序通知,并且对用户来说最为熟悉。在应用程序内部触发的其他原始通知用于唤醒应用程序从非活动状态或用于数据同步等目的。
Windows App SDK 支持两种类型的通知。这些 API 位于 Microsoft.Windows.PushNotifications 和 Microsoft.Windows.AppNotifications 命名空间中。探索通知超出了本章的范围,但您可以从 Push notifications overview 页面上的快速入门部分在 Microsoft Learn 上进行探索:learn.microsoft.com/windows/apps/windows-app-sdk/notifications/push-notifications/. 我们将在 第八章 中向我们的项目添加通知,向 WinUI 应用程序添加 Windows 通知。
现在我们已经了解了 WinUI 和 Windows App SDK 的一些新功能,让我们回到我们的项目,并添加几个新的控件。
向项目中添加一些新控件
在本节中,我们将使用仅适用于具有 WinUI 的 Windows 应用程序的两个控件。我们将更改 SplitButton 以允许用户保存并返回到项目列表,或保存并继续在项目详情页上添加另一个项目。然后,我们将添加一个 TeachingTip 控件来通知用户新的保存功能。要跟随这些步骤,您可以使用 GitHub 上的起始项目(github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/master/Chapter05/Start)。让我们先更新 Save 按钮。
使用 SplitButton 控件
按照以下步骤操作:
-
首先,在
ItemDetailsViewModel中添加一个新的SaveItemAndContinue方法,并将其绑定到我们新的SplitButton控件的Click事件:public void SaveItemAndContinue() { Save(); _itemId = 0; ItemName = string.Empty; SelectedMedium = null; SelectedLocation = null; SelectedItemType = null; IsDirty = false; }在
SaveItemAndContinue方法中,我们调用Save并重置所有项目状态数据,以便为新项目输入做好准备。这里的一个问题是Save当前会导航回上一页。让我们修复这个问题。 -
要从
Save中移除返回上一页的调用,我们需要为SaveItemAndReturn创建一个新的方法:public void SaveItemAndReturn() { Save(); _navigationService.GoBack(); }在这里,我们调用
Save并然后导航回上一页。现在可以从Save中移除对_navigationService.GoBack的调用。 -
我们目前正在使用
x:Bind直接绑定到保存方法,而不是使用ICommand。因此,您可以从Save中移除RelayCommand属性,并从ItemDetailsViewModel中移除CanSaveItem方法。您还需要从isDirty私有成员中移除NotifyCanExecuteChangedFor属性。 -
最后,打开
ItemDetailsPage并更新SplitButton控件:<SplitButton x:Name="SaveButton" Content="Save and Return" Margin="8,8,0,8" Click="{x:Bind ViewModel .SaveItemAndReturn}" IsEnabled="{x:Bind ViewModel.IsDirty, Mode=OneWay}"> <SplitButton.Flyout> <Flyout> <StackPanel> <Button Content="Save and Create New" Click="{x:Bind ViewModel .SaveItemAndContinue}" IsEnabled="{x:Bind ViewModel.IsDirty, Model=OneWay}" Background="Transparent"/> <Button Content="Save and Return" Click="{x:Bind ViewModel .SaveItemAndReturn}" IsEnabled="{x:Bind ViewModel.IsDirty, Mode=OneWay}" Background="Transparent"/> </StackPanel> </Flyout> </SplitButton.Flyout> </SplitButton>SplitButton的内容已更新为Save and Return。我们还更新了绑定,使用Click事件来调用操作,以及使用IsDirty的IsEnabled属性。还添加了一个新的子Flyout项目。Flyout包含一个StackPanel控件,其中包含用于调用SaveItemAndContinue的Click事件的Button控件。 -
就这些!现在,运行应用程序并尝试这个新功能。当你点击下拉箭头时,新按钮看起来是这样的:

图 5.13 – 使用新的 SplitButton 控件保存项目
将 TeachingTip 控件添加到保存按钮
TeachingTip 控件是教育用户了解应用程序中功能的一种很好的方式。它是一个带有标题文本和内容文本的小弹出窗口。你可能在使用的某些 Windows 应用程序中见过它们。
TeachingTip 可以链接到页面上的控件,或者它可以直接放置在页面上,并使用可选的 PreferredPlacement 属性来控制它在页面上的位置。它可以配置为用户通过 关闭 按钮或当用户开始与页面交互时自动关闭。
要为我们的 SplitButton 控件添加 TeachingTip 控件,将其添加到 ItemDetailsPage 中的 Resources 控件,如下所示:
<SplitButton x:Name="SaveButton" Content="Save and Return"
Margin="8,8,0,8" Command="{x:Bind ViewModel.SaveCommand}">
<SplitButton.Flyout>
<Flyout>
<Button Content="Save and Create New"
Command="{x:Bind ViewModel
.SaveAndContinueCommand}"
Background="Transparent"/>
</Flyout>
</SplitButton.Flyout>
<SplitButton.Resources>
<TeachingTip x:Name="SavingTip"
Target="{x:Bind SaveButton}"
Title="Save and create new"
Subtitle="Use the dropdown button
option to save your item and create
another.">
</TeachingTip>
</SplitButton.Resources>
</SplitButton>
在 TeachingTip 控件内部,我们将 Target 绑定到 SaveButton,并将 Title 和 Subtitle 设置为教育用户关于新的 保存并创建 新 功能。
在 ItemDetailsPage 构造函数中需要额外的调用才能使提示出现:
SavingTip.IsOpen = true;
如果你现在运行应用程序,TeachingTip 将会在用户每次打开 ItemDetailsPage 时出现。这可能会迅速让我们的用户感到厌烦。我们可以在 ItemDetailsPage.xaml.cs 中添加一些代码,以保存一个用户级设置,表示当前用户已经看到了这个 TeachingTip 控件。然后,下次我们加载页面时,我们会检查这个设置,以便应用程序可以跳过显示提示的代码。
我们将利用 Windows 本地存储来保存和加载用户设置:
Windows.Storage.ApplicationDataContainer localSettings =
Windows.Storage.ApplicationData.Current.LocalSettings;
// Load the user setting
string haveExplainedSaveSetting = localSettings.Values
[nameof(SavingTip)] as string;
// If the user has not seen the save tip, display it
if (!bool.TryParse(haveExplainedSaveSetting, out bool
result) || !result)
{
SavingTip.IsOpen = true;
// Save the teaching tip setting
localSettings.Values[nameof(SavingTip)] = "true";
}
现在,用户在第一次加载 ItemDetailsPage 时才会看到这个提示。
让我们看看我们的 TeachingTip 控件的实际应用。运行应用程序,选择一个项目,并点击 ItemDetailsPage:

图 5.14 – 使用新的 TeachingTip 控件
现在,应用程序有一个新功能,以及一种很好的方式来告知用户如何使用它。让我们总结一下,并讨论在本章中学到的东西。
摘要
在本章中,我们探讨了 WinUI 3 中可用的许多控件。我们了解到,WinUI 3 Gallery 应用程序是探索 WinUI 开发者可用控件的一个很好的工具。我们还探讨了 Windows App SDK 中的一些不属于 WinUI 的功能。最后,我们在我们的应用程序中添加了一些新的 WinUI 控件。
在下一章中,我们将学习更多关于服务的内容,并将开始在会话之间持久化我们的媒体项目数据。
问题
-
哪个 WinUI 控件可以显示 Lottie 动画?
-
哪个 WinUI 控件可以使用基于 Chromium 的 Microsoft Edge 浏览器显示 HTML 内容?
-
你可以使用 C++ 创建 WinUI 3 应用程序吗?
-
你会用哪个控件来教育用户关于新功能的信息?
-
你可以从 Microsoft Store 下载哪个应用来了解所有 WinUI 控件?
-
哪种控件可以用来在会话之间保存和加载用户设置?
-
你会用哪个 Windows App SDK 功能来通知用户你的应用有新消息可以查看?
第六章:利用数据和服务的优势
管理数据是大多数应用程序操作的核心。学习如何加载、维护和保存这些数据是 WinUI 开发的一个重要方面。数据管理的两个最重要的方面是 状态管理 和 服务定位器模式。我们将介绍这些概念,并在我们的应用程序中应用其中的一些。
本章我们将涵盖以下主题:
-
理解 WinUI 应用程序生命周期
-
学习使用 SQLite 存储应用程序数据
-
学习使用 对象关系映射器(ORM)Dapper 快速映射数据服务中的对象
-
继续探索服务定位器模式,并使用我们的数据服务实现它
到本章结束时,你将具备对 WinUI 应用程序生命周期的实际理解,并知道如何在项目中管理数据和状态。
技术要求
要跟随本章中的示例,需要以下软件:
-
Windows 10 版本 1803(版本 17134)或更高版本
-
Visual Studio 2022 或更高版本,已配置 .NET 桌面开发工作负载以进行 Windows App SDK 开发
本章的源代码可在 GitHub 上找到,网址为 github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/master/Chapter06。
使用应用程序生命周期事件管理应用程序状态
在任何应用程序中处理数据之前,了解目标应用程序平台的应用程序生命周期非常重要。我们简要地提到了这些概念,但现在,是时候更深入地了解桌面应用程序的 WinUI 应用程序生命周期了。
探索 Windows 应用程序生命周期事件
与其他桌面 .NET 应用程序相比,桌面应用程序的 WinUI 具有一套不同的生命周期事件。WPF 和 Windows 表单(WinForms)应用程序要么正在运行,要么没有运行。在启动和关闭 WPF 和 WinForms 应用程序时,会发生几个事件:

图 6.1 – WPF 和 WinForms 应用程序生命周期事件
注意
我们不会在这里详细介绍,因为我们的主要重点是构建 WinUI 3 应用程序。然而,对于在启动和关闭之外的两个 WPF 事件,它们的顺序如下:
1. FrameworkElement.Unloaded: 当一个元素从 WPF 可视树中移除时,此事件被触发。它不会在应用程序关闭时触发。
2. Application.SessionEnding: 当当前 Windows 用户注销或关闭 Windows 时,此事件被触发。在事件处理程序中,你可以通过将 SessionEndingCancelEventArgs.Cancel 属性设置为 true 来请求 Windows 取消进程。
WinUI 应用程序的生命周期事件
让我们谈谈 WinUI 的生命周期。生命周期事件在应用程序开始执行时为你提供了初始化任何数据和状态的机会,这允许你在应用程序关闭时清理或保存状态。在 UWP 应用程序中,你也有能力处理由于用户或操作系统操作而使应用程序挂起或恢复时的事件。WinUI,就像其他 .NET 桌面应用程序一样,没有这个能力。
在 Application 和 Window 类中,只有几个事件可以被处理。每个 Application 类都重写了 OnLaunched 方法。这个方法将在应用程序被用户或操作系统启动时恰好被调用一次。我们已经在我们的示例应用程序中使用了 OnLaunched 方法。这是创建 MainWindow 的地方,也是我们添加调用方法来配置我们的 IOC 容器的地方。在一个新的 WinUI 应用程序中,OnLaunched 方法看起来是这样的:
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
m_window = new MainWindow();
m_window.Activate();
}
Application 和 Window 类只继承自 .NET 的 Object 类,因此没有可利用的继承事件。但 Page 类的情况并非如此,我们将在稍后讨论。首先,我们将讨论 Window,它有几个我们可以在管理应用程序生命周期时利用的事件。
WinUI 中的 Window 类没有 Loaded 事件,这个事件在 WPF 的 Window 类中用来指示窗口及其内容已加载并可供交互。可以使用 Window.Activated 事件来代替 Loaded 事件,但 Activated 事件会在窗口每次获得焦点时触发。如果这是你的唯一选择,你将需要添加一个标志来检查 Activated 是否是第一次触发。
Window 中的另一个生命周期事件是 Closed 事件,当窗口关闭时触发。如果是最后一个剩余的窗口,通常是 MainWindow,则在窗口关闭后应用程序将结束。这是你应该保存任何应用程序数据和状态信息的地方。
这就是 Application 和 Window 类提供的应用程序生命周期事件的全部内容。然而,你可以利用由 FrameworkElement 提供的一些其他事件,它是 Page 和 WinUI 中所有其他控件的基础类。
FrameworkElement 对象的附加生命周期事件
WinUI 中的每个控件都继承自 Control,而 Control 继承自 FrameworkElement。即使是添加到 Control 中的 Page 控件,也是通过从 UserControl 继承而来的。
FrameworkElement 类提供了三个有用的事件,开发者可以利用这些事件来处理应用程序生命周期:
-
Loading:当加载过程开始时,将发生此事件。可以利用此事件开始从服务或其他来源获取和处理数据。你也可以在当前Window或Page的构造函数中,甚至在Application.OnLoaded中更早地开始加载数据。 -
Loaded:当当前元素及其所有子元素加载并准备好交互时,将调用Loaded事件。在它们加载之前不要尝试操作这些元素,否则应用将引发运行时异常。 -
Unloaded:当当前元素被卸载并从Page中移除时,将触发此事件。你可以使用此事件来清理资源或保存页面的任何状态。
注意
到目前为止,我们还没有讨论 WinUI 的视觉树。WinUI 中的树的概念(物理和逻辑)与其他 XAML 框架中的相同。当我们在 第十一章 中讨论使用 Visual Studio 调试 WinUI 应用程序时,我们将更详细地讨论物理树和逻辑树。要了解更多信息,请参阅 Microsoft Learn 上的关于树的优秀 WPF 文章:learn.microsoft.com/dotnet/desktop/wpf/advanced/trees-in-wpf。
你可以处理当前视图中任何控件的 Loaded 事件,无论是 Window 还是 Page,但始终要考虑性能。视觉树中最顶层的 FrameworkElement 的 Loaded 事件将在其所有子元素的 Loaded 事件完成之后才会触发。网络和文件系统操作可能很昂贵,因此尽可能最小化和合并加载以呈现视图所需数据的调用。
注意
要了解更多关于处理生命周期事件的信息,请阅读以下 Microsoft Learn 页面:learn.microsoft.com/windows/apps/windows-app-sdk/applifecycle/applifecycle。
现在你已经对 WinUI 的生命周期有了坚实的理解,让我们开始处理一些需要在用户会话之间持久化的真实数据。
创建 SQLite 数据存储
到目前为止,我的媒体收藏项目只与存储在内存集合中的数据进行交互。这意味着每次应用程序关闭时,所有用户数据都会丢失。这也意味着每次应用程序启动时,都需要调用一个方法来用硬编码的种子数据填充所有列表。
在上一章中,我们为应用程序创建了一个可维护的数据服务的第一步。通过创建一个实现 IDataService 的数据服务类,当我们开始从数据库加载数据时,不需要在 ViewModel 类中进行任何更改。本节将重点创建一个新的 SqliteDataService 类,以便我们可以使用 SQLite 进行数据访问。本章的起始代码可以在 GitHub 上找到,链接为 github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/master/Chapter06/Start。
什么是 SQLite?
SQLite(位于sqlite.org/)是一个基于 SQL 的数据库,常被移动应用和简单的桌面应用程序使用。它是一个受欢迎的选择,因为它体积小、速度快,并且包含在一个单独的文件中。几乎每个平台都有 SQLite 库可用。我们将使用 Microsoft 的Microsoft.Data.Sqlite ADO.NET 提供程序来处理 SQLite。
注意
关于 Microsoft 的 SQLite 提供程序的更多信息,您可以阅读learn.microsoft.com/dotnet/standard/data/sqlite/。要了解更多关于在 WinUI 项目中使用 SQLite 的信息,请查看这篇 Microsoft Learn 文章:learn.microsoft.com/windows/apps/develop/data-access/sqlite-data-access。
添加 SQLite 作为数据服务
按照以下步骤操作:
-
首先通过从视图 | 其他窗口 | 包管理控制台打开包管理控制台,然后运行以下命令,将Microsoft.Data.Sqlite NuGet 包添加到MyMediaCollection项目中。在运行此命令之前,请确保在包管理控制台窗口的项目下拉菜单中选择了MyMediaCollection项目:
Install-Package Microsoft.Data.Sqlite运行此命令相当于从
DataService类中的SqliteDataService查找并添加包作为起点。 -
现在,将
using语句添加到文件顶部:using Microsoft.Data.Sqlite; using System.IO; using System.Threading.Tasks; using Windows.Storage;当我们初始化 SQLite 数据库文件时,将使用
System.IO和Windows.Storage命名空间,并且我们需要导入System.Threading.Tasks命名空间以处理一些async任务。 -
接下来,向类中添加一个新的常量来保存数据库的文件名:
private const string DbName = "mediaCollectionData.db"; -
现在,让我们创建一个私有方法来创建或打开数据库文件,为数据库创建一个
SqliteConnection类,打开它,并将其返回给调用者。此方法可以在整个类中用于需要新数据库连接的任何时候。数据库文件将创建在用户的LocalFolder中,这意味着应用程序的数据将与用户的本地 Windows 配置文件数据一起保存:private async Task<SqliteConnection> GetOpenConnectionAsync() { await ApplicationData.Current.LocalFolder.CreateFileAsync(DbName, CreationCollisionOption.OpenIfExists).AsTask().ConfigureAwait(false); string dbPath = Path.Combine(ApplicationData.Current.LocalFolder.Path, DbName); var cn = new SqliteConnection($"Filename={dbPath}"); cn.Open(); return cn; }注意,我们已经将此方法声明为
async,并且在打开或创建文件时使用await关键字。当使用外部资源,如文件、网络连接或数据库时,使用 async/await 是一种良好的实践,以保持应用程序的响应性。
注意
要了解更多关于 C#和.NET 中的 async/await 的信息,Microsoft Learn 有一篇很好的文章可以帮助您入门:learn.microsoft.com/dotnet/csharp/asynchronous-programming/。
-
接下来,创建两个方法来在数据库中创建
MediaItems和Mediums表。这些方法将在每次应用程序启动时被调用,但 SQL 代码只会在表不存在时创建表。SqliteCommand对象接受tableCommand查询字符串和SqliteConnection。它有几个方法可以用来执行命令,具体取决于查询是否预期返回任何数据。在我们的情况下,不期望返回任何值,所以ExecuteNonQueryAsync是这两个方法的最佳异步选项:private async Task CreateMediumTableAsync(SqliteConnection db) { string tableCommand = @"CREATE TABLE IF NOT EXISTS Mediums (Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Name NVARCHAR(30) NOT NULL, MediumType INTEGER NOT NULL)"; using var createTable = new SqliteCommand(tableCommand, db); await createTable.ExecuteNonQueryAsync(); } private async Task CreateMediaItemTableAsync(SqliteConnection db) { string tableCommand = @"CREATE TABLE IF NOT EXISTS MediaItems (Id INTEGER PRIMARY KEY AUTOINCREMENT, Name NVARCHAR(1000) NOT NULL, ItemType INTEGER NOT NULL, MediumId INTEGER NOT NULL, LocationType INTEGER, CONSTRAINT fk_mediums FOREIGN KEY(MediumId) REFERENCES Mediums(Id))"; using var createTable = new SqliteCommand(tableCommand, db); await createTable.ExecuteNonQueryAsync(); } -
现在,为
Mediums表创建一个方法来插入一行到表中:private async Task InsertMediumAsync(SqliteConnection db, Medium medium) { using var insertCommand = new SqliteCommand { Connection = db, CommandText = "INSERT INTO Mediums VALUES (NULL, @Name, @MediumType);" }; insertCommand.Parameters.AddWithValue("@Name", medium.Name); insertCommand.Parameters.AddWithValue("@MediumType", (int)medium.MediaType); await insertCommand.ExecuteNonQueryAsync(); } -
现在,我们需要另一种方法来读取
Mediums表中的所有行:private async Task<IList<Medium>> GetAllMediumsAsync(SqliteConnection db) { IList<Medium> mediums = new List<Medium>(); using var selectCommand = new SqliteCommand("SELECT Id, Name, MediumType FROM Mediums", db); using SqliteDataReader query = await selectCommand.ExecuteReaderAsync(); while (query.Read()) { var medium = new Medium { Id = query.GetInt32(0), Name = query.GetString(1), MediaType = (ItemType)query.GetInt32(2) }; mediums.Add(medium); } return mediums; }
这些两个简单的操作需要一些代码。插入方法需要为要保存到表中的每个属性添加参数,而选择方法使用while循环将每个表的记录添加到集合中。让我们看看我们是否可以在下一节中简化这一点。
在我们实现剩余的创建、读取、更新、删除(CRUD)操作的方法之前,必须向项目中添加一个新的库来简化我们将要编写的数据库访问代码。
利用微 ORM 简化数据访问
正如你在上一节中看到的,为即使是简单的应用程序编写数据访问代码也可能需要一些时间。
ORM,如Entity Framework Core(EF Core),可以极大地简化并减少所需的代码,但对于只有几个表的简单应用程序来说可能有些过度。在本章中,我们将探讨微 ORM。微 ORM 是轻量级的框架,用于处理对象和数据查询之间的数据映射。
注意
EF Core 是.NET 开发者中流行的 ORM。如果你想了解更多关于如何在项目中使用 EF Core 的信息,你可以观看 Packt 的视频Entity Framework Core – a Full Tour,链接为www.packtpub.com/product/entity-framework-core-a-full-tour-net-5-and-up-video/9781803242231。
我们将在项目中用于数据访问的框架,Dapper,是由 Stack Overflow 的开发者创建的开源.NET 微 ORM。你可以在dapperlib.github.io/Dapper/了解更多关于 Dapper 的信息,并在 NuGet 上获取包:www.nuget.org/packages/Dapper。
Dapper 在.NET 社区中很受欢迎。虽然它不提供 EF Core 的一些功能,如模型生成或实体变更跟踪,但它确实使得编写快速、精简的数据层变得非常容易。当你将Dapper.Contrib库(www.nuget.org/packages/Dapper.Contrib)添加到其中时,编写应用程序所需的 CRUD 方法就更加容易了。
将 Dapper 添加到项目中
让我们直接进入正题:
-
首先,将
Dapper和Dapper.Contrib添加到MyMediaCollection项目中。再次打开包管理器控制台窗口,并将这两个包添加到项目中:Install-Package Dapper Install-Package Dapper.Contrib -
现在,重新审视
InsertMediaAsync方法。如果我们使用 Dapper 提供的QueryAsync方法,我们可以将原始方法中的代码减少到如下:private async Task InsertMediumAsync(SqliteConnection db, Medium medium) { var newIds = await db.QueryAsync<long>( $@"INSERT INTO Mediums ({nameof(medium.Name)}, MediumType) VALUES (@{nameof(medium.Name)}, @{nameof(medium.MediaType)}); SELECT last_insert_rowid()", medium); medium.Id = (int)newIds.First(); }我们之前编写的设置查询参数值的代码现在已删除。Dapper 会从传入其
QueryAsync方法的medium对象中为我们映射它们。你必须确保 SQLite 查询中的参数名称与我们的对象上的属性名称匹配,以便 Dapper 的自动映射能够正常工作。 -
作为额外奖励,我们还可以通过添加以下 SQLite 代码从
QueryAsync调用中获取生成的 ID,该代码在INSERT操作完成后返回它:SELECT last_insert_rowid(); -
接下来,更新
GetAllMediumsAsync的代码以使用 Dapper:private async Task<IList<Medium>> GetAllMediumsAsync(SqliteConnection db) { var mediums = await db.QueryAsync<Medium>(@"SELECT Id, Name, MediumType AS MediaType FROM Mediums"); return mediums.ToList(); }我们已经从 14 行代码减少到只有 2 行。注意,在查询的高亮部分,我们如何使用
MediaType的别名作为MediumType字段。这是一个简单地将数据映射到不匹配数据库字段名的对象属性的方法,只需简单地将作为 SQL 选择语句返回的字段重命名即可。Dapper 还帮助我们直接返回我们的Medium对象列表,而不是我们不得不使用while循环来遍历结果集。 -
接下来,创建一个查询,以获取所有媒体项以填充主
ListView控件。这个查询稍微复杂一些,因为我们根据MediumId在MediaItems和Mediums两个表上进行了连接,并将数据返回以映射到两个相应的对象,item和medium。这些类型由提供给QueryAsync方法的前两个泛型类型指示。为了执行此映射,我们给 Dapper 一个 lambda 表达式,指示它将medium设置为每个从查询返回的行的item的MediumInfo属性。返回对象的类型由提供给QueryAsync方法的第三个泛型类型定义。其余参数将由 Dapper 根据它们的属性名称自动映射:private async Task<List<MediaItem>> GetAllMediaItemsAsync(SqliteConnection db) { var itemsResult = await db.QueryAsync<MediaItem, Medium, MediaItem> ( @"SELECT [MediaItems].[Id], [MediaItems].[Name], [MediaItems].[ItemType] AS MediaType, [MediaItems].[LocationType] AS Location, [Mediums].[Id], [Mediums].[Name], [Mediums].[MediumType] AS MediaType FROM [MediaItems] JOIN [Mediums] ON [Mediums].[Id] = [MediaItems].[MediumId]", (item, medium) => { item.MediumInfo = medium; return item; } ); return itemsResult.ToList(); } -
接下来,添加创建媒体项的插入和更新方法的代码:
private async Task<int> InsertMediaItemAsync(SqliteConnection db, MediaItem item) { var newIds = await db.QueryAsync<long>( @"INSERT INTO MediaItems (Name, ItemType, MediumId, LocationType) VALUES (@Name, @MediaType, @MediumId, @Location); SELECT last_insert_rowid()", item); (int)newIds.First(); } private async Task UpdateMediaItemAsync(SqliteConnection db, MediaItem item) { await db.QueryAsync( @"UPDATE MediaItems SET Name = @Name, ItemType = @MediaType, MediumId = @MediumId, LocationType = @Location WHERE Id = @Id;", item); }InsertMediaItemAsync中的代码应该看起来很熟悉。它与我们将数据插入Mediums表时所做的操作非常相似。由于 Dapper,更新MediaItems表中一行代码现在技术上只有一行。 -
在我们的模型中,
MediaItem对象已添加了一个新的只读属性。这个属性允许 Dapper 将MediumId映射到MediaItems表:public int MediumId => MediumInfo.Id; -
现在,将
Computed属性添加到MediaItem.MediumInfo属性。这告诉 Dapper 在我们尝试在数据库中插入或更新行时忽略该属性。我们只需要保存MediumId。用户无法更改Mediums表中的行:[Computed] public Medium MediumInfo { get; set; } -
最后,让我们创建一个方法来从
MediaItems表中删除项目。这段代码因Dapper.Contrib而有所不同。我们不需要在代码中编写任何参数化 SQL,因为Dapper.Contrib有一个DeleteAsync方法,可以根据提供的MediaItem类的Id属性生成删除MediaItems的代码:private async Task DeleteMediaItemAsync(SqliteConnection db, int id) { await db.DeleteAsync<MediaItem>(new MediaItem { Id = id }); }要使这生效,您必须使用
Key属性装饰模型类的主键属性:public class MediaItem { [Key] public int Id { get; set; } ... }
确保使用 Dapper.Contrib 属性的每个模型类都添加一个 using 语句为 Dapper.Contrib.Extensions。
在我们将 SqliteDataService 类的所有公共 CRUD 方法更新为调用这些私有方法之前,我们将完成在应用程序启动时初始化服务的代码。
更新数据服务的初始化
让我们开始吧:
-
首先,在
SqliteDataService中创建DataService.PopulateMediums方法的版本,将其改为async并重命名为PopulateMediumsAsync。更新此方法以便从 SQLite 获取数据。如果这是应用程序首次为当前用户启动,该方法还将创建所需的数据:private async Task PopulateMediumsAsync(SqliteConnection db) { _mediums = await GetAllMediumsAsync(db); if (_mediums.Count == 0) { var cd = new Medium { Id = 1, MediaType = ItemType.Music, Name = "CD" }; var vinyl = new Medium { Id = 2, MediaType = ItemType.Music, Name = "Vinyl" }; var hardcover = new Medium { Id = 3, MediaType = ItemType.Book, Name = "Hardcover" }; var paperback = new Medium { Id = 4, MediaType = ItemType.Book, Name = "Paperback" }; var dvd = new Medium { Id = 5, MediaType = ItemType.Video, Name = "DVD" }; var bluRay = new Medium { Id = 6, MediaType = ItemType.Video, Name = "Blu Ray" }; var mediums = new List<Medium> { cd, vinyl, hardcover, paperback, dvd, bluRay }; foreach (var medium in mediums) { await InsertMediumAsync(db, medium); } _mediums = await GetAllMediumsAsync(db); } } -
其次,从
SqliteDataService、DataService和IDataService中移除PopulateItems。由于我们现在在会话之间持久化所有数据,所以它将不再需要。您还可以移除_items私有变量。 -
现在,将
SqliteDataService构造函数中的代码移至一个名为InitializeDataAsync的新公共方法中,并更新代码以便它使用新的私有初始化方法。别忘了移除填充项目集合的调用。SqliteConnection对象应始终作为using块的一部分,以确保连接被关闭并且对象被释放:public async Task InitializeDataAsync() { using (var db = await GetOpenConnectionAsync()) { await CreateMediumTableAsync(db); await CreateMediaItemTableAsync(db); SelectedItemId = -1; PopulateItemTypes(); await PopulateMediumsAsync(db); PopulateLocationTypes(); } } -
这个新的初始化方法需要添加到
IDataService中,以便通过我们的 DI 容器解析服务的对象可以使用它。如果您在项目中保留原始的DataService类,您将需要添加InitializeDataAsync的实现,以便项目可以编译:public interface IDataService { Task InitializeDataAsync(); ... } -
在更改初始化
SqliteDataService的代码位置后,App.xaml.cs中的RegisterComponents方法需要更新以使用新的SqliteDataService并调用InitializeDataAsync。在此过程中,我们将方法重命名为反映其新的异步状态:private async Task RegisterComponentsAsync(Frame rootFrame) { var navigationService = new NavigationService(rootFrame); navigationService.Configure(nameof(MainPage), typeof(MainPage)); navigationService.Configure(nameof(ItemDetailsPage), typeof(ItemDetailsPage)); var dataService = new SqliteDataService(); await dataService.InitializeDataAsync(); HostContainer = Host.CreateDefaultBuilder() .ConfigureServices(services => { services.AddSingleton<INavigationService>(navigationService); services.AddSingleton<IDataService>(dataService); services.AddTransient<MainViewModel>(); services.AddTransient<ItemDetailsViewModel>(); }).Build(); }
不要忘记更新 OnLaunched 以使其为 async 并等待对重命名的 RegisterComponentsAsync 的调用。
现在应用程序在启动时初始化数据服务,是时候更新公共 CRUD 方法,以便它们使用我们创建的从 SQLite 获取数据的异步私有方法了。
通过服务检索数据
让我们开始使用我们的服务方法检索和保存 SQLite 数据。只需更新创建、更新和删除操作。所有媒体项目都存储在 DataService 中的 List<MediaItem> 中,因此用于检索项目的公共方法可以保持与上一章相同。让我们开始吧:
-
首先,更新
SqliteDataService.cs中媒体项目的创建、更新和删除方法。每个方法都将从GetOpenConnectionAsync获取数据库的打开连接并异步调用其相应的私有方法:public async Task<int> AddItemAsync(MediaItem item) { using var db = await GetOpenConnectionAsync(); return await InsertMediaItemAsync(db, item); } public async Task UpdateItemAsync(MediaItem item) { using var db = await GetOpenConnectionAsync(); await UpdateMediaItemAsync(db, item); } public async Task DeleteItemAsync(MediaItem item) { using var db = await GetOpenConnectionAsync(); await DeleteMediaItemAsync(db, item.Id); } -
更新获取项目的公共方法,使其变为异步:
public async Task<MediaItem> GetItemAsync(int id) { IList<MediaItem> mediaItems; using var db = await GetOpenConnectionAsync(); mediaItems = await GetAllMediaItemsAsync(db); // Filter the list to get the item for our Id. return mediaItems.FirstOrDefault(i => i.Id == id); } public async Task<IList<MediaItem>> GetItemsAsync() { using var db = await GetOpenConnectionAsync(); return await GetAllMediaItemsAsync(db); }
注意
如果在查询数据时需要进行大量过滤,Entity Framework 是一个更健壮的 ORM,可以提供更广泛的选择。SQLite 最适合简单的应用程序。注意,在前面的代码中,GetItemAsync 查询所有项目,然后使用 lambda 表达式过滤到与提供的 ID 匹配的项目。
-
方法名称已更新为包含
Async,已移除_items集合的所有使用,并且每个方法都已更改为返回Task。因此,更新IDataService接口成员以反映相同的更改。也可以从项目中移除DataService或更新其方法以也变为异步。最好在开始时尝试预测数据访问方法需要异步,从而防止接口出现破坏性更改:Task<int> AddItemAsync(MediaItem item); Task UpdateItemAsync(MediaItem item); Task DeleteItemAsync(MediaItem item); Task<IList<MediaItem>> GetItemsAsync(); MainViewModel.cs, the Delete method will be updated to use async/await with its data service call. Don’t forget to rename it DeleteAsync to follow best practices when naming async methods. You will also need to add a using statement to the file for the System.Threading.Tasks namespace:private async Task DeleteAsync()
{
await _dataService.DeleteItemAsync(SelectedMediaItem);
Items.Remove(SelectedMediaItem);
allItems.Remove(SelectedMediaItem);
}
-
更新
PopulateData方法,将其命名为PopulateDataAsync,并使用获取项目的异步方法:public async Task PopulateDataAsync() { items.Clear(); foreach(var item in await _dataService.GetItemsAsync()) { items.Add(item); } allItems = new ObservableCollection<MediaItem>(Items); mediums = new ObservableCollection<string> { AllMediums }; foreach(var itemType in _dataService.GetItemTypes()) { mediums.Add(itemType.ToString()); } selectedMedium = Mediums[0]; } -
现在,您必须更新
MainViewModel构造函数,在构造函数末尾调用PopulateDataAsync:public MainViewModel(INavigationService navigationService, IDataService dataService) { _navigationService = navigationService; _dataService = dataService; PopulateDataAsync(); } -
在
ItemDetailsViewModel中也需要进行一些类似的更改。更新Save方法,使其变为异步并等待数据服务对AddItemAsync、GetItemAsync和UpdateItemAsync的调用。别忘了将Save重命名为SaveAsync并添加一个using语句用于System.Threading.Tasks命名空间:private async Task SaveAsync() { MediaItem item; if (_itemId > 0) { item = await _dataService.GetItemAsync(_itemId); item.Name = ItemName; item.Location = (LocationType)Enum.Parse(typeof(LocationType), SelectedLocation); item.MediaType = (ItemType)Enum.Parse(typeof(ItemType), SelectedItemType); item.MediumInfo = _dataService.GetMedium(SelectedMedium); await _dataService.UpdateItemAsync(item); } else { item = new MediaItem { Name = ItemName, Location = (LocationType)Enum.Parse(typeof(LocationType), SelectedLocation), MediaType = (ItemType)Enum.Parse(typeof(ItemType), SelectedItemType), MediumInfo = _dataService.GetMedium(SelectedMedium) }; await _dataService.AddItemAsync(item); } } -
接下来,更新
SaveItemAndReturn和SaveAndContinue方法,使它们也使用 async/await:private async Task SaveItemAndReturnAsync() { await SaveItemAsync(); _navigationService.GoBack(); } private async Task SaveItemAndContinueAsync() { await SaveItemAsync(); _dataService.SelectedItemId = 0; _itemId = 0; ItemName = ""; SelectedMedium = null; SelectedLocation = null; SelectedItemType = null; IsDirty = false; } -
最后,更新
ItemDetailsViewModel.xaml,以便保存按钮在绑定它们的Click方法时使用异步方法:<SplitButton x:Name="SaveButton" Content="Save and Return" Margin="8,8,0,8" Click="{x:Bind ViewModel.SaveItemAndReturnAsync}" IsEnabled="{x:Bind ViewModel.IsDirty, Mode=OneWay}"> ... <Button Content="Save and Create New" Click="{x:Bind ViewModel.SaveItemAndContinueAsync}" IsEnabled="{x:Bind ViewModel.IsDirty, Mode=OneWay}" Background="Transparent"/> <Button Content="Save and Return" Click="{x:Bind ViewModel.SaveItemAndReturnAsync}" IsEnabled="{x:Bind ViewModel.IsDirty, Mode=OneWay}" Background="Transparent"/>
注意
如果在添加或编辑媒体项目后,主页面上的 ComboBox 无法填充,请更新其 ItemsSource 数据绑定的 Mode 为 OneWay。GitHub 上的完整源代码已更新以反映此更改。
就这些了。运行应用程序并查看其工作情况。由于我们不再为媒体项目列表创建任何虚拟数据,当应用程序首次启动时,ListView 中的媒体集合将是空的:

图 6.2 – 首次使用数据库启动
尝试添加、更新和删除一些项目。然后,关闭应用程序并再次运行。你应该看到当你关闭它时列表上相同的项:

图 6.3 – 使用保存的数据重新启动
用户现在可以保留他们的保存数据。如果你想在应用程序之外浏览 SQLite 数据,你可以使用一些工具来连接到本地db并检查它。其中之一是DB Browser for SQLite。本书的范围不包括此工具的介绍,但你可以在sqlitebrowser.org/自行探索。
让我们总结并回顾一下我们在 WinUI 应用程序中处理数据所学到的内容。
概述
在本章中,我们涵盖了大量的重要内容。你学习了如何读取和写入本地 SQLite 数据库中的数据。然后,你学习了如何通过利用 Dapper(一个.NET 开发者的 ORM)来简化你的数据访问代码。使用 ORM 将为你节省在 WinUI 项目(或任何其他.NET 项目)的数据访问层中创建样板映射代码的时间。所有这些数据访问代码都被设置为异步,以保持用户界面的响应性。
在下一章中,我们将学习如何使用微软的 Fluent UI 设计原则创建一个美丽的Fluent UI。
问题
-
Windows 何时将 WinUI 3 应用程序置于挂起状态?
-
你应该在什么时候保存应用程序状态,以确保在应用程序关闭时不会丢失?
-
你可以在
Page类上处理哪个事件,以便在页面上的每个元素都加载完成后执行一些逻辑? -
什么是 Micro ORM?
-
添加 CRUD 辅助工具(如
Delete和DeleteAsync)的 Dapper 包叫什么名字? -
一些功能更全面的 ORM(如 Entity Framework)中哪一个功能非常强大?
-
有什么工具可以用来检查 SQLite 数据库中的数据?
第二部分:扩展 WinUI 和现代化应用程序
在这部分,你将基于你关于 WinUI 应用程序开发的所学知识进行扩展,包括设计概念、平台选项和开源库。WinUI 控件本地的 Fluent 设计系统为 Windows 应用程序用户提供了熟悉的外观和感觉。你还将学习如何将应用通知与 Windows App SDK 集成。然后,你将探索 Windows Community Toolkit 和.NET Community Toolkit,这是一套为 WinUI 开发者提供控件和辅助工具的开源包。最后,Template Studio 将为 WinUI 开发者提供在新项目开始时遵循最佳实践的起点。
本部分包含以下章节:
-
第七章,Windows 应用程序的 Fluent 设计系统
-
第八章,将 Windows 通知添加到 WinUI 应用程序
-
第九章, 使用 Windows 社区工具包增强应用
-
第十章, 使用模板工作室加速应用开发
第七章:Windows 应用程序的 Fluent Design System
Fluent Design System是由微软创建的一套应用程序设计原则,并在多个桌面、移动和 Web 平台上实现。Windows 的 Fluent Design System 是一套为 Windows 应用程序构建的控件、模式和样式。实际上,它是所有 WinUI 控件的内隐样式。
学习 Fluent Design 的原则以及如何在 WinUI 应用程序中实现它们非常重要。我们还将探索 Windows 的Fluent XAML 主题编辑器应用程序。此应用程序帮助开发者创建应用程序的主题,包括颜色方案和样式元素,如边框和角落。然后开发者可以轻松导入资源以实现主题。
在本章中,我们将涵盖以下主题:
-
学习 Fluent Design 的概念
-
如何找到关于 Fluent Design 的最新信息
-
将 Fluent Design 概念融入 WinUI 应用程序
-
使用 Fluent XAML 主题编辑器自定义和使用 UI 主题
-
探索Acrylic材质和 Fluent Design System
-
在 WinUI 应用程序中使用Mica材质
到本章结束时,你将了解 Windows 应用程序的 Fluent Design System。你还将知道如何将这些设计标准融入你的 WinUI 应用程序中。
技术要求
为了跟随本章中的示例,需要以下软件:
-
Windows 10 版本 1803(版本 17134)或更高版本
-
Visual Studio 2022 或更高版本,已安装并配置了.NET 桌面开发工作负载以进行 Windows App SDK 开发
本章的源代码可在 GitHub 上通过此 URL 获取:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/master/Chapter07。
Fluent Design System 是什么?
Fluent Design System 是一个跨平台系统,帮助开发者创建美观、直观的应用程序。Fluent Design 的网站(fluent1.microsoft.design/)为多个平台上的开发者提供了专门的页面和资源:
-
Android
-
iOS
-
macOS
-
Web
-
Windows
-
跨平台(React Native)
注意
微软已经开始发布他们的 Fluent 2 设计指南,网址为fluent2.microsoft.design/。你可以将 Fluent 1 样式视为 Windows 10 样式,而 Fluent 2 样式则类似于 Windows 11 的外观和感觉。在撰写本文时,Fluent 2 指南仅针对 React Web 应用程序发布。其他平台尚不可用。
Fluent 设计旨在简单直观。虽然它在各个平台上保持其设计理念,但它也调整其设计以在每个平台上感觉原生。在 第一章“WinUI 简介”中,我们讨论了当前 Fluent 设计概念中的一些起源,这些概念是在 Windows Phone 中引入的 Metro 设计中。虽然微软的设计在多年中有所演变,但一些原则仍然存在。Fluent 设计系统的三个核心原则如下:
-
自然适用于所有设备:软件应适应其运行的设备,无论是 PC、平板电脑、游戏机、手机还是 AR/VR 设备。
-
直观且强大:UI 预测用户的行为,并在使用应用程序时将用户带入体验中。
-
引人入胜且沉浸式:设计借鉴现实世界的元素,使用光线、阴影、纹理和深度为用户创造沉浸式体验。
设计背后的驱动哲学是适应和自然。设备和应用程序应感觉舒适,并预测用户的行为。
到目前为止,这是一个非常抽象和高级的解释。让我们在下一节中具体探讨 Windows 的 Fluent 设计。
探索 Windows 的 Fluent 设计
对于 Windows 应用程序,Fluent 设计涵盖了多个领域。与其他设计系统相比,Fluent 更加全面。苹果的 人类界面指南(developer.apple.com/design/human-interface-guidelines/) 只在苹果的平台:iOS、iPadOS 和 macOS 上得到广泛采用。谷歌的 Material Design(material.io/) 系统得到了更广泛的采用,但仅提供 Android、Flutter 和网页的工具包。
Fluent 设计最常与 Material Design 相比较,因为它们在形状和纹理方面有一些共同的概念,但 Fluent 设计在透明度方面比 Material Design 有着更显著的效果。Fluent 设计提供了一个丰富的工具集,您可以在 WinUI 和几乎任何其他开发平台上使用。
让我们探索这些设计方面的具体内容以及它们如何应用于您的 WinUI 应用程序。
控件
控件等同于单个 用户输入 或 交互 元素。我们已经在 第五章“探索 WinUI 控件”中探讨了 WinUI 中可用的许多控件。以下是一些常见的 WinUI 控件在 Windows 11 亮暗模式下的样子:

图 7.1 – 亮暗模式下的一些常见控件
默认情况下,WinUI 控件使用 Fluent 风格。我们将在本章后面看到如何覆盖 WinUI 控件中的默认 Fluent 风格。
模式
模式: 模式是一组相关的控件或一组成为单个新元素的控件组。这个组可以被添加到复合控件中以供重用。WinUI 中模式的一些示例包括以下内容:
-
搜索: 在其最简单的形式中,搜索模式需要接受输入的控件、调用搜索和显示搜索结果的控件。在接收到任何输入之前,可以添加一个用于建议搜索的额外元素。例如,亚马逊 Alexa 会根据用户的日历、联系人、新闻偏好等来执行此操作。基于用户输入添加自动建议列表是现代搜索控件的一个常见功能。你还可以将聊天控件与人工智能(AI)如微软的Bot Framework或 OpenAI 的ChatGPT集成,根据初始搜索参数提出一些后续问题。
-
表单: 表单是一种非常常见的控制模式。它们由相关标签、输入控件和命令按钮的组构成,用于收集一组相关的数据元素。一些具有重用潜力的常见表单包括用户账户创建表单和收集用户反馈的表单。表单应遵循 Fluent Design 的间距、灵活布局和利用字体排印创建层次结构的指南(Microsoft Learn 示例:
learn.microsoft.com/windows/apps/design/controls/forms)。 -
使用
ListView控件和SplitView控件将列表与所选项目的详细信息分开。根据页面宽度,这两个视图可以是垂直堆叠或并排显示(Microsoft Learn 示例:learn.microsoft.com/windows/apps/design/controls/list-details)。
这些模式中的每一个都封装了 Fluent Design 的元素,以创建一个可以在应用程序间重用的复合控件。你可能在项目中有一些可以添加到共享控件库中的控制模式,以便于重用。这样的共享库可以节省时间并确保团队遵循良好的设计实践。
布局
布局对于确保应用程序适应任何屏幕尺寸或方向非常重要。灵活性是良好设计的布局的关键原则。当一个窗口或页面被调整大小时,内容可以通过重新定位控件、添加/删除项目、更改项目流、用更适合当前可用空间的控件替换控件,或者简单地调整项目大小来适应。这通常在 XAML 中使用VisualState来处理,为页面必须适应的每个大小阈值定义VisualState,可能定义为VisualState更新控件属性以适应新的布局。Microsoft Learn 有一个很好的例子,见learn.microsoft.com/windows/apps/design/layout/layouts-with-xaml。WinUI 包括几个不同的布局面板,可以帮助开发者创建适合他们页面的布局,并响应大小、方向和分辨率的更改。
输入
对于响应用户输入,Fluent Design 有一些建议。对于反应开发者几十年来一直在处理的传统鼠标和键盘输入,也有一些指南。现代应用可以根据鼠标输入进行平移、缩放、旋转或滚动。键盘可能是一个物理键盘,也可能是移动和触摸用户的屏幕键盘。
随着今天硬件的发展,输入可以以其他形式出现:
-
笔/触控笔
-
触控
-
触控板
-
游戏手柄/控制器
-
遥控
-
Surface Dial(见
learn.microsoft.com/windows/apps/design/input/windows-wheel-interactions) -
AR/VR 手势
-
语音
用户输入也可以通过输入注入 API 进行模拟。这种功能可能在你应用中创建“展示如何”或“导览”功能时非常有用。你的代码可以执行一些预定义的步骤,引导用户在页面上执行某些操作。这个 API 超出了本书的范围。要了解使用输入注入截获鼠标输入并将其转换为触摸输入的示例,请阅读 Microsoft Learn 上的这篇文章:learn.microsoft.com/windows/apps/design/input/input-injection。
风格
风格涵盖了 Fluent Design 的多个方面:
-
图标:好的图标应该是简单的,并能传达应用程序的目的。
-
颜色:颜色选择很重要。允许用户自定义颜色也是让应用感觉个性化的好方法。WinUI 通过使用主题画刷,使适应用户的浅色或深色主题选择以及 Windows 高亮颜色变得容易。
-
字体排印:Microsoft 建议所有 Windows 应用程序都使用 Segoe UI 字体。选择字体大小可以帮助传达应用程序内的层次结构,例如书籍或文档布局。为此,Microsoft 定义了一个字体级数(可在
learn.microsoft.com/windows/apps/design/style/typography#type-ramp找到)。字体级数定义了屏幕上不同样式元素字体大小的增加,如正文、标题和副标题。WinUI 中有静态资源可以利用,以选择控件预期用途的正确大小。 -
间距:控件之间以及控件内部的间距对于可读性和可用性非常重要。WinUI 控件允许选择标准或紧凑密度。有关尺寸和 Fluent 密度的更多信息,请参阅此处:
learn.microsoft.com/windows/apps/design/style/spacing。 -
揭示焦点:在较大的显示设备上,如 Xbox 或 Surface Hub,将注意力引向可聚焦元素非常重要。这通过 Fluent 的灯光效果来实现。
-
亚克力:这是一种 WinUI 画笔类型,它通过透明度创建纹理。这种纹理给用户界面带来深度感。我们将在本章后面更详细地讨论亚克力。
-
米卡:这是一种类似于亚克力的动态材料,但与亚克力不同,它是非透明的。它通过结合当前操作系统主题和桌面壁纸的元素来创建应用程序的背景。我们将在本章后面看到如何将米卡整合到自己的 WinUI 应用程序中。
-
圆角半径:Fluent 设计推崇的观点是圆角可以促进用户的积极情绪。WinUI 控件具有与 Fluent 设计建议一致的圆角半径。
-
声音:声音可以是您应用程序中创造沉浸式体验的一个组成部分。当面板打开或关闭时,发出细微的呼啸声,以及在警报声音中使用恰当的音调和音量,可以沉浸应用程序的用户。
-
写作风格:信不信由你,写作风格是应用程序设计的一部分。业务线应用程序不应与休闲消费者应用程序或益智游戏有相同的写作风格。如果用户没有注意到写作风格,因为他们期望的应用程序类型与之相符,他们将被吸引到应用程序体验中。
这些只是 Fluent 设计定义的样式的一些方面。您可以在 Microsoft Learn 上了解更多信息:learn.microsoft.com/windows/apps/design/style/。
Fluent 风格的许多方面都通过 XAML 样式和其他静态资源提供给我们的 WinUI 应用程序。接下来,我们将探讨如何更新我们的示例应用程序以响应用户的 Windows 主题变化。
在 WinUI 应用程序中整合 Fluent 设计
是时候将一些 Fluent 设计原则融入到My Media Collection应用程序中,并对 UI 进行一些润色了。大多数 WinUI 控件已经设计得符合 Fluent 标准,但我们修改了一些属性,而没有理解 Fluent 设计。
更新标题栏
在我们开始改进 XAML 中的样式之前,让我们先修复应用程序的标题栏。到目前为止,标题栏总是显示MyMediaCollection,没有任何空格或当前页面的指示:
- 首先,为了修复应用程序打包和分发时的间距问题,从
My Media Collection中打开Package.appmanifest。如果你愿意,你还可以更改描述。

图 7.2 – 更新 Package.appmanifest 中的信息
更新AppWindow类。
-
要更新应用程序标题栏中的文本,请将以下代码添加到
MainWindow.xaml.cs中:using Microsoft.UI; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; using System; using WinRT.Interop; namespace MyMediaCollection { public sealed partial class MainWindow : Window { private AppWindow _appWindow; private const string AppTitle = "My Media Collection"; public MainWindow() { this.InitializeComponent(); _appWindow = GetCurrentAppWindow(); _appWindow.Title = AppTitle; } private AppWindow GetCurrentAppWindow() { IntPtr handle = WindowNative.GetWindowHandle(this); WindowId windowId = Win32Interop.GetWindowIdFromWindow(handle); return AppWindow.GetFromWindowId(windowId); } } } -
现在,添加一个名为
SetPageTitle的internal方法,以便每个页面都可以将其标题追加到主窗口标题中:internal void SetPageTitle(string title) { if (_appWindow == null) { _appWindow = GetCurrentWindow(); } _appWindow.Title = $"{AppTitle} – {title}"; } -
为了让每个页面都能访问
MainWindow,在App.xaml.cs中公开一个内部成员:internal Window Window => m_window; -
接下来,在
MainPage.xaml.cs中,为页面的Loaded事件添加一个事件处理器。在事件处理器中,添加一些代码以将页面标题Home追加到当前窗口的Title中。当我们启动应用程序时,标题栏应该显示My Media Collection - Home:public MainPage() { this.InitializeComponent(); Loaded += MainPage_Loaded; } private void MainPage_Loaded(object sender, RoutedEventArgs e) { var mainWindow = (Application.Current as App)?.Window as MainWindow; if (mainWindow != null) { mainWindow.SetPageTitle("Home"); } } -
最后,在
ItemDetailsView.xaml.cs中做出相同的更改,但将页面标题设置为Item Details。
现在,当你运行应用程序时,你应该会看到标题栏文本在你在项目列表和项目详情之间导航时更新。让我们接下来对MainPage的样式做一些更改。
更改 MainPage 的样式
目前,我们应用程序的主页风格并不多。我们设置了一些TextBlock控件的FontWeight为Bold,以便将它们作为重要项目区分出来,但这并不遵循字体排印的 Fluent 设计指南。同时,还有一个紫色边框将ListView的标题与其项目分开:

图 7.3 – My Media Collection 当前主页
直接编写颜色代码不是一种好习惯。即使你使用自定义颜色作为产品品牌,也可以在Application.Resources中集中管理。让我们逐步处理MainPage.xaml文件,并进行一些改进:
-
首先,将第一个
TextBlock的文本从Media Collection更新为Home,与窗口标题栏中的文本匹配。将其包裹在一个水平对齐的StackPanel中,并添加一个前置的SymbolIcon控件来显示导入SubheaderTextBlockStyle StaticResource的Style属性。这些更改应该看起来像这样:<StackPanel Orientation="Horizontal"> <SymbolIcon Symbol="Home" Margin="8"/> <TextBlock Text="Home" Style="{StaticResource SubheaderTextBlockStyle}" Margin="8"/> </StackPanel> -
我们还应该从媒体类型标签中移除
FontWeight属性,并使用 Fluent 样式资源:<TextBlock Text="Media Type:" Margin="4" Style="{StaticResource SubtitleTextBlockStyle}" VerticalAlignment="Bottom"/> -
接下来,将周围的
Grid更改为StackPanel,并删除Grid.Column定义。除了简化布局外,这还将允许主页符号和文本出现在页面上的其他控件之上,从而加强层次结构。完整的代码块将看起来像这样:<StackPanel> <StackPanel Orientation="Horizontal"> <SymbolIcon Symbol="Home" Margin="8"/> <TextBlock Text="Home" Style="{StaticResource SubheaderTextBlockStyle}" Margin="8"/> </StackPanel> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right"> <TextBlock Text="Media Type:" Margin="4" Style="{StaticResource SubtitleTextBlockStyle}" VerticalAlignment="Bottom"/> <ComboBox ItemsSource="{x:Bind ViewModel.Mediums}" SelectedItem="{x:Bind ViewModel.SelectedMedium, Mode=TwoWay}" MinWidth="120" Margin="0,2,6,4" VerticalAlignment="Bottom"/> </StackPanel> HeaderTemplate of the ListView to replace the purple BorderBrush attributes with SystemAccentColor from ThemeResource. This will make sure that the border’s color picks up the user’s preferred accent color from their selected Windows theme. Also, change each TextBlock to use a built-in Style instead of setting FontWeight and change the Width of the first column to be 120 to accommodate the larger title font:<ListView.HeaderTemplate>
<Grid.ColumnDefinitions>
</Grid.ColumnDefinitions>
<Border BorderBrush="{ThemeResource SystemAccentColor}"
边框厚度="0,0,0,1">
<TextBlock Text="Medium"
外边距="4,0,0,0"
样式设置为"{StaticResource TitleTextBlockStyle}"
<Border Grid.Column="1"
边框刷="{ThemeResource SystemAccentColor}"
边框厚度="0,0,0,1">
<TextBlock Text="标题"
外边距="4,0,0,0"
样式设置为"{StaticResource TitleTextBlockStyle}"
</ListView.HeaderTemplate>
Note that you will also need to change the first column `Width` to `120` in the `ListView.ItemTemplate`. -
最后,让我们通过在
ListView底部和命令按钮之间添加边框来定义列表区域的结束。通过将按钮的StackPanel包装在Border控件中,再次使用SystemAccentColor来实现。Margin= "4,0"是等同于Margin= "4,0,4,0"的简写:<Border Grid.Row="2" BorderBrush="{ThemeResource SystemAccentColor}" BorderThickness="0,1,0,0" Margin="4,0"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right"> <Button Command="{x:Bind ViewModel.AddEditCommand}" Content="Add/Edit Item" Margin="8,8,0,8"/> <Button Command="{x:Bind ViewModel.DeleteCommand}" Content="Delete Item" Grid.Column="1" Margin="8"/> </StackPanel> </Border> -
运行应用程序并检查重新设计的用户界面。它看起来好多了。你现在可以轻松地看到数据层次结构,尽管在我们这个简单的应用程序中可能有限:

图 7.4 – 新样式化的“我的媒体收藏”主页
在进入详细页面之前,让我们看看在 Windows 中选择暗黑模式时页面看起来如何。打开Windows 设置,转到个性化 | 颜色,并从选择你的颜色下拉菜单中选择暗黑(如果你通常使用暗黑,尝试将其更改为浅黑):

图 7.5 – “我的媒体收藏”在暗黑模式下的运行效果
页面上的所有内容都切换到暗黑模式,除了标题栏外无需任何代码更改。要了解更多关于标题栏自定义的信息,包括更改颜色或图标,甚至完全用自定义标题栏替换它,请参阅 Microsoft Learn 上的此主题:learn.microsoft.com/windows/apps/develop/title-bar。
如果你有一个很好的理由保持你的应用程序在浅色或暗黑模式,你可以更新Application.xaml,通过使用此命令向Application元素添加单个属性:
RequestedTheme="Dark"
这将把Dark主题应用到整个应用程序。如果你有理由只强制将此主题应用到应用程序的一部分,可以将RequestedTheme属性应用到单个Page或Control。现在,让我们将相同类型的样式应用到详细页面。
修改 ItemDetailsPage 的样式
我们希望更新ItemDetailsPage.xaml,使其具有与主页相同的整体外观和感觉:
-
打开文件,首先更新
Item Details。给它与Home上使用的相同的Subheader和TextBlockStyle,并在水平对齐的Stack面板中包裹它。在TextBlock前面加上一个使用Edit符号的SymbolIcon:<StackPanel Orientation="Horizontal"> <SymbolIcon Symbol="Edit" Margin="8"/> <TextBlock Text="Item Details" Style="{StaticResource SubheaderTextBlockStyle}" Margin="8"/> </StackPanel> -
接下来,修改跟随新
StackPanel的Grid,使其具有顶部和底部边框。同时,修改Margin以在Grid的两侧各有 4 px:<Grid Grid.Row="1" BorderBrush="{ThemeResource SystemAccentColor}" BorderThickness="0,1,0,1" Margin="4,0,4,8">
在这个页面上,我们只需要进行这些更改。再次运行应用程序并导航到详情页面,看看效果如何:

图 7.6 – 重新设计的项目详情页面
看起来很棒。现在两个页面的样式匹配,添加的边框线与高亮的活动输入字段的颜色相匹配。
让我们现在转换一下思路,回顾一下可以帮助设计师和开发者在实现 Fluent 设计时使用的工具。
使用 Fluent XAML 主题编辑器
我们已经看到,从用户的 Windows 设置中采用默认颜色和主题资源是多么容易,但如果你或你的公司想要为应用程序创建一个自定义主题怎么办?也许这个主题需要跨一系列应用程序共享。你可以在 Visual Studio 中创建一个包含 ResourceDictionary 的 XAML 文件,并手动编写所有标记以创建新样式。Visual Studio 的 IntelliSense 在某些方面会提供帮助。然而,有一个更简单的方法。
微软创建了一个名为 ResourceDictionary 的开源工具 XAML 文件,你可以将其拖放到你的项目中。
注意
Fluent XAML 主题编辑器是为了调整 UWP 控件的样式而创建的,但相同的样式也可以与 WinUI 3 控件一起使用。
要安装应用程序,在搜索字段中打开 fluent xaml,你将在搜索结果中找到 Fluent XAML 主题编辑器。在搜索结果中点击它以查看产品页面:

图 7.7 – 微软商店中的 Fluent XAML 主题编辑器页面
如果你已经安装了应用程序,将有一个启动按钮。如果没有安装,你可以点击安装按钮。安装完成后,你将在开始菜单中找到该应用程序。
当你第一次启动应用程序时,它将以 UWP 应用程序的默认样式启动,显示在浅色和深色主题中。在右侧面板中,你可以找到用于更改 UI 元素的颜色和形状的控件。字体列在即将推出中,但它已经承诺了多年:

图 7.8 – Windows 版本的 Fluent XAML 主题编辑器
颜色
在颜色选项卡上,您可以从颜色预设下拉列表中选择一个默认配置文件。除了默认预设外,还有薰衣草、森林和夜间选项。还有加载其他预设或保存当前颜色设置作为新预设的选项。这些颜色预设以 JSON 格式保存。
注意
您在这里指定的任何颜色都将覆盖 Windows 系统的默认强调色,该强调色通常会被 WinUI 应用程序默认拾取。除非您的应用程序有很好的理由遵循另一个主题,否则最好让 WinUI 使用用户选择的强调色。设计自定义主题应由经验丰富的设计团队承担。
点击当前预设中的任何颜色将启动颜色选择器窗口,您可以在其中调整当前颜色:

图 7.9 – 使用颜色选择器调整预设颜色
区域、基础和主要颜色可以分别独立调整,以改变它们的浅色和深色主题外观。
形状
形状面板提供了调整控件和覆盖层的圆角的控件。这也是您可以调整主题默认边框厚度的地方。
与颜色一样,形状预设也可以保存和加载。应用程序自带两个预设:默认和无圆角,较厚的边框。区别细微但明显:

图 7.10 – 应用了无圆角和较厚边框的形状
当您调整完颜色和形状设置后,使用包含您的主题数据的 ResourceDictionary。您可以将 XAML 复制并粘贴到项目中的 Resources 部分:

图 7.11 – 从 Fluent XAML 主题编辑器导出主题
接下来,让我们探索 Acrylic 材质,这是我们在本章前面提到的 Fluent 设计系统的一部分。
Acrylic 材质和 Fluent 设计系统
Acrylic 是一种 WinUI 画笔,当应用于您的应用程序时,提供半透明纹理。这种纹理在 Windows 的浅色和深色主题中都能工作,并且是给用户带来深度感的绝佳方式。AcrylicBrush 类是 Windows App SDK 中 Microsoft.UI.Xaml.Media 命名空间的一部分,其中还可以找到 Brush、SolidColorBrush 和 GradientBrush。
注意
Acrylic 材质也适用于使用 WinUI 2.8.x 的 UWP 应用程序。
如果您想在决定在自己的项目中使用 AcrylicBrush 之前先探索它,您可以在 WinUI 3 Gallery 应用程序中尝试:

图 7.12 – 在 WinUI 3 Gallery 中探索 AcrylicBrush
在画廊页面,你可以看到AcrylicBrush的默认样式以及它们在 Windows 浅色和深色主题中的显示效果。此外,画廊页面还提供了调整画笔不透明度和色调的控件。你还可以为画笔设置一个回退的纯色。回退颜色用于没有资源加载丙烯酸画笔的系统。
根据应用了哪种画笔,元素将根据背景元素或当前重叠元素背后的应用内元素来绘制丙烯酸画笔。WinUI 3 画廊中有如何应用这两种类型画笔的示例。这是一个应用内画笔的示例:
<Rectangle Fill="{ThemeResource AcrylicInAppFillColorDefaultBrush}"/>
本例展示了如何应用背景丙烯酸画笔:
<Rectangle Fill="{ThemeResource AcrylicBackgroundFillColorDefaultBrush}"/>
当云母融入你的 WinUI 应用中时,它会给用户带来质感和深度的感觉,这是流畅设计的基本原则之二。你还可以选择将另一种材料云母融入其中。
在 WinUI 应用中使用云母
云母是 WinUI 应用中可用的一种材料。你可以将云母视为没有透明度的丙烯酸背景画笔。它根据 Windows 中的当前桌面背景颜色创建画笔样式。云母材料有两种变体:云母和云母替代。云母替代创建与云母相同的半透明背景,但它具有更强的色调。
注意
云母仅在 Windows 11 及更高版本中可用。如果你的应用使用云母且安装在 Windows 10 上,则不会应用该材料。
如果你使用 WinUI 3 画廊并打开系统背景(云母/丙烯酸)页面,你可以启动一个 WinUI 窗口并循环显示应用于窗口背景的云母、云母替代和丙烯酸材料:

图 7.13 – 使用云母、云母替代和丙烯酸查看相同的窗口
设置窗口系统背景的代码比仅将画笔应用到Rectangle或其他 UI 元素的Fill属性要复杂得多。让我们在 My Media Collection 应用中尝试配置云母替代。
将云母加入我的媒体收藏
使用 Windows App SDK 1.3 或更高版本将云母引入你的应用非常简单。让我们在 My Media Collection 中使用云母替代材料。这只需要几个步骤:
-
首先,确认你的项目正在使用 Windows App SDK 1.3 或更高版本。如果不是,你可以使用NuGet 包管理器将其升级到最新版本。
-
接下来,打开
MainPage.xaml和ItemDetailsPage.xaml文件,并移除每个Page元素的Background属性。如果Page元素(或其任何子元素)不透明,你将看不到云母背景。 -
最后,打开
MainWindow.xaml.cs文件,并在调用InitializeComponent方法之后立即添加以下代码到构造函数中:SystemBackdrop = new MicaBackdrop { Kind = MicaKind.BaseAlt };
就这些了。现在运行应用,看看使用云母的背景效果如何。应用中的颜色将根据你当前的 Windows 背景而变化:

图 7.14 – 使用 Mica 样式化我的媒体收藏
注意,标题栏不会拾取 Mica 材质,就像它忽略主题更改一样。如果您想自己这样做,可以遵循 Microsoft Learn 的说明来扩展您的窗口内容到标题栏区域:learn.microsoft.com/windows/apps/develop/title-bar?tabs=wasdk#full-customization。采用这种方法,您需要创建自己的标题栏控件,包括任何按钮和图标,并在页面导航时更新标题栏文本。
现在,我们将快速浏览一些面向设计师的额外 Fluent Design 工具。
Fluent Design 的设计资源和工具包
虽然深入探讨用户界面设计超出了本书的范围,但我们将简要回顾一些适用于流行设计工具的 Fluent Design 工具包。您可以从 Microsoft Learn 下载这些工具的设计资源和示例:learn.microsoft.com/windows/apps/design/downloads/。
-
Figma: 这是一个具有免费和付费选项的设计和原型工具,具体取决于团队和项目规模。您可以在其网站上了解更多关于 Figma 的信息:
www.figma.com/。 -
草图: 这是一款流行的工具,可以单独或与团队一起设计和原型化应用程序。虽然没有免费计划,但 Sketch 提供免费试用期。Sketch 可在
www.sketch.com/获取。 -
Adobe XD: XD 是 Adobe 的设计/原型工具。与 Figma 类似,Adobe XD 提供免费和付费选项来设计应用程序。您可以查看
helpx.adobe.com/support/xd.html了解 XD。 -
Adobe Illustrator: 这是由 Adobe 推出的一款强大的矢量设计工具。提供免费试用。您可以在
www.adobe.com/products/illustrator.html下载并开始使用 Adobe Illustrator。 -
Inkscape (
inkscape.org/)是一款免费的矢量图像编辑器,也可以处理Adobe Illustrator (****AI)文件。 -
Adobe Photoshop: 这可能是最知名的栅格图像编辑器之一。Adobe 还为 Photoshop 提供免费试用。
www.adobe.com/products/photoshop.html。
Photoshop 的 Fluent Design 工具包包括几个 PSD 文件。您还可以在免费的图像编辑器中处理 PSD 文件,例如GIMP (www.gimp.org/)或Paint.NET (www.getpaint.net/)。Paint.NET 需要开源插件,可在www.psdplugin.com/获取。
摘要
在本章中,我们学习了大量关于 Fluent Design、设计资源和可供 WinUI 开发者使用的工具的知识。您将能够在您的 WinUI 应用程序设计中使用这些工具和技术,或者向您公司的设计师推荐它们。我们还更新了 My Media Collection 应用程序,使其更符合 Fluent Design 的建议,并学习了如何融入 Acrylic 和 Mica 材料。
在下一章中,我们将探讨如何使用 Windows App SDK 通知系统向 WinUI 添加通知。
问题
-
哪些平台实现了 Fluent Design?
-
控件模式是什么?
-
微软推荐使用哪种字体来实现 Fluent Design?
-
哪个风格方面是针对大屏幕设备的?
-
Fluent Design 中可用的两种间距密度分别叫什么名字?
-
在
Application.xaml中可以设置哪个属性来覆盖用户的浅色/深色主题选择? -
哪些设计工具提供了 Fluent Design 工具包?
第八章:将 Windows 通知添加到 WinUI 应用程序
Windows App SDK 为开发者提供了在 WinUI 应用程序中实现原始推送通知和应用程序通知的能力。了解每种通知类型的使用案例非常重要。它们有不同的实现方式,每种都有其自身的优点和局限性。推送通知可以显示给用户或由应用程序接收以执行内部操作。另一方面,应用程序通知用于与用户通信。我们将介绍何时使用特定通知类型的示例,并将应用程序通知添加到我的媒体收藏示例应用程序中。
在本章中,我们将涵盖以下主题:
-
了解 Windows App SDK 中不同通知类型及其使用案例
-
发现如何在 WinUI 应用程序中利用推送通知
-
探索如何使用 WinUI 中的应用程序通知
到本章结束时,您将了解推送通知与 Windows App SDK 公开的其他应用程序通知之间的区别。您将了解何时选择每种通知类型以及它们在 WinUI 3 项目中是如何处理的。
技术要求
要跟随本章中的示例,需要以下软件:
-
Windows 10 版本 1809(构建 17763)或更高版本
-
配置了.NET 桌面开发工作负载的 Visual Studio 2022 或更高版本,用于 Windows App SDK 开发
本章的源代码可在 GitHub 上通过此 URL 获取:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/master/Chapter08。
Windows App SDK 中推送通知概述
WinUI 应用程序可以利用 Windows App SDK 中的不同类型的通知。通知 API 是在 Windows App SDK 1.3 中添加的,可以根据通知类型选择本地发送或通过云服务发送。我们通常将通知与屏幕角落的小弹出窗口联系起来,这些窗口被称为托盘通知,在 Windows 中。然而,并非所有通知都需要视觉指示器。它们也可以用来指示应用程序激活并执行操作或从远程服务同步数据,而不依赖于应用程序中的计时器。
原始推送通知
这些内部通知被称为原始推送通知。它们不需要用户交互,并且不会通过托盘通知向用户发出信号。推送通知利用Windows 推送通知服务(WNS),这是Microsoft Store服务的一部分。要在商店发布应用程序或利用其任何服务,需要一个商店账户,并且您的应用程序必须在商店仪表板中注册。
注意
我们将在第十四章“打包和部署 WinUI 应用程序”中讨论将应用发布到 Microsoft Store,第十四章。本章不会涵盖商店注册过程,但如果您对此不熟悉,可以跳到第十四章查看过程。
来自 WNS 的推送通知可以直接由应用接收,以指示应用执行某些操作。实际上,您的应用不需要处于活动状态即可接收通知。Windows 将激活应用,以便它可以处理通知并执行请求的操作。使用通知可以节省设备资源,并可以减少或消除轮询和计时器的需求。
WNS 的通知也可能通知用户。这是一种应用通知类型。
基于云的应用通知
应用通知涉及通知用户某些事件已发生或需要采取行动。应用通知可以是本地的,也可以来自云端。基于云的通知,类似于原始通知,利用 WNS。
创建和发送这些应用通知的过程与创建原始推送通知的过程类似。头部和内容类型将区分应用通知,并通知 Windows 显示一个可见的、短暂的提示。任何未被用户取消或清除的通知都可以在 Windows 设置中的通知中心查看。
注意
某些类型的自包含应用或具有管理员权限运行的应用可能不符合接收通知的资格。要查看有关这些限制的更多信息,您可以查看 Microsoft Learn 上推送通知文档的此部分:learn.microsoft.com/windows/apps/windows-app-sdk/notifications/push-notifications/#limitations。
应用通知也可以是用户 PC 本地的。让我们接下来讨论这种类型的通知。
本地应用通知
本地应用通知不涉及云和 WNS,发送通知时不涉及 WNS。它们来自您的应用,显示给用户,并在用户对 toast 通知进行操作时由您的应用处理。用户通过使用 Microsoft 的应用,如 Outlook、Teams,甚至 Microsoft Store 应用,熟悉这些类型的通知。
有时,这些通知是信息性的,例如当商店应用在应用更新后显示消息时。通知还可以提示用户采取行动,例如暂停 Outlook 日历提醒。在这种情况下,通知窗口包含一个下拉控件,允许用户选择暂停的持续时间。
在本章的后面部分,我们将向 My Media Collection 应用添加本地应用通知,提示用户将新书添加到他们的收藏中。现在,我们将更深入地探讨原始推送通知的实现以及它们如何被用来静默地接收来自云的通知。
在 WinUI 应用中使用原始推送通知
如前所述,未经用户通知由应用处理的推送通知是通过 WNS 和 Azure 生成的。在本节中,我们将简要探讨如何在 WinUI 应用中利用这些通知。要开始,所需的 Azure 配置相对较长且不太有趣。由于 Azure 通知中心对 WNS 的配置已在 Microsoft Learn 上的 Azure 文档中详细说明,您应该在开始之前查看它们:learn.microsoft.com/azure/notification-hubs/notification-hubs-windows-store-dotnet-get-started-wns-push-notification。了解 Windows 设计文档中 WNS 概述也是一个好主意:learn.microsoft.com/windows/apps/design/shell/tiles-and-notifications/windows-push-notification-services--wns--overview。
注意
Azure 文档是为 UWP 应用编写的,但配置说明同样适用于 WinUI 3 应用。
完成 Azure 配置后,在 WinUI 3 应用中使用通知的步骤与 UWP 类似,但并不完全相同。有关从云中处理推送通知的详细示例,您可以阅读这篇 Microsoft Learn 文章:learn.microsoft.com/windows/apps/windows-app-sdk/notifications/push-notifications/push-quickstart。在本章中,我们专注于应用通知,将在下一节中将这些通知添加到我们的示例应用中。您需要完成的概述步骤如下:
-
将 COM 激活信息添加到您的
Package.appxmanifest文件中。以下是一个示例:<Extensions> <!--Register COM activator--> <com:Extension Category="windows.comServer"> <com:ComServer> <com:ExeServer Executable="MyApp\MyApp.exe" DisplayName="My App" Arguments="----WindowsAppRuntimePushServer:"> <com:Class Id="[Azure AppId for App]" DisplayName="WinUI Push Notify" /> </com:ExeServer> </com:ComServer> </com:Extension> </Extensions> -
在
Microsoft.Windows.PushNotifications命名空间中注册PushNotificationManager并订阅PushNotificationChannel以接收通知类型。 -
在
App类中添加代码以检查应用是否因推送通知而从后台启动或激活。 -
创建一个 WNS 通道并将该通道注册到 WNS 服务。这些是接收要推送到您应用的推送通知数据的 HTTP 端点。
-
使用像
POST请求这样的工具,并带上推送通知数据。你需要为请求获取一个包含你的 Azure 租户 ID、应用 ID 和客户端密钥的访问令牌。更多信息请参阅此页面:learn.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal#get-tenant-and-app-id-values-for-signing-in。
这些是基本步骤,但还有更多要学习。确保阅读本节中链接的所有文章,以了解在 WinUI 应用程序中使用原始推送通知的细微差别。
现在,让我们更深入地了解应用通知,以及如何将发送和接收功能添加到我们的示例应用中。
使用 Windows App SDK 添加 Windows 应用通知
在本节中,我们将向我的媒体收藏项目添加一些本地应用通知。我们将添加到项目中的代码基于微软学习团队创建的 Windows App SDK 本地应用通知示例应用。你可以在 GitHub 上下载该项目的代码:github.com/microsoft/WindowsAppSDK-Samples/tree/main/Samples/Notifications/App/CsUnpackagedAppNotifications。
我们在应用的MainPage中添加了两个按钮,用于触发两种类型的通知。一个将包含一个图像和一些文本。第二个将添加一个文本输入字段,以展示我们如何从通知托盘中接收用户输入并在我们的应用程序中对其做出反应。
注意
实现通知处理需要大量的配置和代码。如果你想要打开完成的解决方案并跟随操作,代码可以在 GitHub 上找到:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/main/Chapter08/Complete。
要开始,打开上一章中的MyMediaCollection解决方案或 GitHub 上第八章的起始解决方案:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/main/Chapter08/Start。
-
第一步是在
Package.appxmanifest文件中添加一些配置以启用应用中的通知处理。首先向Package元素添加两个命名空间声明:xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10" xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" -
接下来,在
Application节点内部添加一个Extensions部分,紧接在uap:VisualElements部分之后:<Extensions> <desktop:Extension Category="windows.toastNotificationActivation"> <desktop:ToastNotificationActivation ToastActivatorCLSID="NEW GUID HERE" /> </desktop:Extension> <com:Extension Category="windows.comServer"> <com:ComServer> <com:ExeServer Executable="MyMediaCollection\MyMediaCollection.exe" DisplayName="My Media Collection" Arguments="----AppNotificationActivated:"> <com:Class Id="SAME NEW GUID HERE" /> </com:ExeServer> </com:ComServer> </com:Extension> </Extensions>生成一个新的
Helpers。 -
现在在
Helpers文件夹中创建一个名为NotificationShared的新类。首先向这个类添加一个常量和结构体:public const string scenarioTag = "scenarioId"; public struct Notification { public string Originator; public string Action; public bool HasInput; public string Input; };Notification结构体将表示应用程序通知中接收到的数据。scenarioTag是一个常量,在构建要发送的每个通知时将需要。 -
接下来,向
NotificationShared类添加以下静态方法。这些方法将由应用程序用于在发送或接收通知时通知 UI:public static void CouldNotSendToast() { MainPage.Current.NotifyUser("Could not send toast", InfoBarSeverity.Error); } public static void ToastSentSuccessfully() { MainPage.Current.NotifyUser("Toast sent successfully!", InfoBarSeverity.Success); } public static void AppLaunchedFromNotification() { MainPage.Current.NotifyUser("App launched from notifications", InfoBarSeverity.Informational); } public static void NotificationReceived() { MainPage.Current.NotifyUser("Notification received", InfoBarSeverity.Informational); } public static void UnrecognizedToastOriginator() { MainPage.Current.NotifyUser("Unrecognized Toast Originator or Unknown Error", InfoBarSeverity.Error); }MainPage没有Current属性,所以这段代码目前还不能编译。我们很快就会解决这个问题。如果 Visual Studio 没有添加必要的using语句,请确保在NotificationShared中存在这些语句:using Microsoft.UI.Xaml.Controls; using MyMediaCollection.Views; -
现在我们将创建两个类来表示应用程序将发送和接收的两种类型的通知。首先,创建一个名为
ToastWithAvatar的新类,并首先向类中添加两个常量:using Microsoft.Windows.AppNotifications.Builder; using Microsoft.Windows.AppNotifications; using MyMediaCollection.Views; namespace MyMediaCollection.Helpers { public class ToastWithAvatar { public const int ScenarioId = 1; public const string ScenarioName = "Local Toast with Image"; } } -
接下来,向类中添加一个名为
SendToast的方法。这个方法将构建并显示一个包含一些文本、头像图像和显示我们应用程序的按钮的 Windows 通知托盘:public static bool SendToast() { var appNotification = new AppNotificationBuilder() .AddArgument("action", "ToastClick") .AddArgument(NotificationShared.scenarioTag, ScenarioId.ToString()) .SetAppLogoOverride(new System.Uri($"file://{App.GetFullPathToAsset(" Square150x150Logo.scale-200.png")}"), AppNotificationImageCrop.Circle) .AddText(ScenarioName) .AddText("This is a notification message.") .AddButton(new AppNotificationButton("Open App") .AddArgument("action", "OpenApp") .AddArgument(NotificationShared.scenarioTag, ScenarioId.ToString())) .BuildNotification(); AppNotificationManager.Default.Show(appNotification); // If notification is sent, it will have an Id. Success. return appNotification.Id != 0; } -
现在添加一个
NotificationReceived方法,当我们的应用程序从 Windows 接收到此类通知时,该方法将被调用。这个方法创建一个Notification结构体,并在稍后本节中创建的MainPage上调用NotificationReceived方法。我们还将创建ToForeground方法,以便将我们的应用程序带到前台,如果它被其他窗口隐藏或最小化了:public static void NotificationReceived(AppNotificationActivatedEventArgs notificationActivatedEventArgs) { var notification = new NotificationShared.Notification { Originator = ScenarioName, Action = notificationActivatedEventArgs.Arguments["action"] }; MainPage.Current.NotificationReceived(notification); App.ToForeground(); } -
ToastWithText类将与ToastWithAvatar类似,但它将在AppNotificationBuilder中添加对AddTextBox的调用以创建 Windows 托盘中的输入字段。它还将用户输入的结果添加到在NotificationReceived中创建的Notification类中。要查看此类的完整代码,请查看 GitHub 上的完成解决方案:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/main/Chapter08/Complete/MyMediaCollection/Helpers/ToastWithText.cs。 -
现在是时候创建
NotificationManager类了。这个类将正好做到这一点——管理通知。它将初始化和注销通知接收。它将执行实际的通知发送和接收。在Helpers文件夹中创建NotificationManager类,并首先添加构造函数和析构代码:using Microsoft.Windows.AppNotifications; using System; using System.Collections.Generic; namespace MyMediaCollection.Helpers { internal class NotificationManager { private bool isRegistered; private Dictionary<int, Action<AppNotificationActivatedEventArgs>> notificationHandlers; public NotificationManager() { isRegistered = false; notificationHandlers = new Dictionary<int, Action<AppNotificationActivatedEventArgs>> { { ToastWithAvatar.ScenarioId, ToastWithAvatar.NotificationReceived }, { ToastWithText.ScenarioId, ToastWithText.NotificationReceived } }; } ~NotificationManager() { Unregister(); } public void Unregister() { if (isRegistered) { AppNotificationManager.Default.Unregister(); isRegistered = false; } } } } -
接下来,添加
Init方法,我们将从App类中调用它:public void Init() { AppNotificationManager notificationManager = AppNotificationManager.Default; // Add handler before calling Register. notificationManager.NotificationInvoked += OnNotificationInvoked; notificationManager.Register(); isRegistered = true; } -
OnNotificationInvoked在Init方法中被连接。当应用程序接收到通知时,这将触发。它根据通知是否被识别对NotificationShared进行不同的调用:public void OnNotificationInvoked(object sender, AppNotificationActivatedEventArgs notificationActivatedEventArgs) { NotificationShared.NotificationReceived(); if (!DispatchNotification(notificationActivatedEventArgs)) { NotificationShared.UnrecognizedToastOriginator(); } }
注意
如果你的代码中有处理传入通知的未处理异常,它们也会触发对NotificationShared.UnrecognizedToastOriginator的此调用。
-
最后,在
NotificationManager中创建ProcessLaunchActivationArgs和DispatchNotification方法:public void ProcessLaunchActivationArgs(AppNotificationActivatedEventArgs notificationActivatedEventArgs) { DispatchNotification(notificationActivatedEventArgs); NotificationShared.AppLaunchedFromNotification(); } private bool DispatchNotification(AppNotificationActivatedEventArgs notificationActivatedEventArgs) { var scenarioId = notificationActivatedEventArgs.Arguments[NotificationShared.scenarioTag]; if (scenarioId.Length != 0) { try { notificationHandlersint.Parse(scenarioId); return true; } catch { // No matching handler return false; } } else { // No scenarioId provided return false; } } -
现在我们将向
App.xaml.cs添加代码以初始化NotificationManager并处理一些常见的调用。让我们首先添加新代码所需的using语句:using Microsoft.Windows.AppLifecycle; using Microsoft.Windows.AppNotifications; using MyMediaCollection.Helpers; using System.Runtime.InteropServices; using WinRT.Interop; -
接下来,添加一个私有的
notificationManager对象,添加DllImport以帮助将窗口带到前台,并使m_window静态:[DllImport("user32.dll", SetLastError = true)] static extern void SwitchToThisWindow(IntPtr hWnd, bool turnOn); private NotificationManager notificationManager; private static Window m_window;
注意
在选择在生产 WinUI 应用中使用哪些 Win32 API 时要小心。SwitchToThisWindow API 被文档标记为“不适用于通用用途”,但在我们的示例应用中它适用于我们的目的。还有其他 API 可以探索,包括 ShowWindow:learn.microsoft.com/windows/win32/api/winuser/nf-winuser-showwindow。
-
接下来,在调用
m_window.Activate之前,向OnLaunched添加以下代码。这获取传递给应用的 Windows 通知参数:var currentInstance = AppInstance.GetCurrent(); if (currentInstance.IsCurrent) { AppActivationArguments activationArgs = currentInstance.GetActivatedEventArgs(); if (activationArgs != null) { ExtendedActivationKind extendedKind = activationArgs.Kind; if (extendedKind == ExtendedActivationKind.AppNotification) { var notificationActivatedEventArgs = (AppNotificationActivatedEventArgs)activationArgs.Data; notificationManager.ProcessLaunchActivationArgs(notificationActivatedEventArgs); } } } -
接下来,在
App构造函数中添加一些代码以初始化NotificationManager并处理AppDomain.CurrentDomain.ProcessExit事件,以便在应用关闭时注销管理器:public App() { this.InitializeComponent(); notificationManager = new NotificationManager(); notificationManager.Init(); AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit; } private void CurrentDomain_ProcessExit(object sender, EventArgs e) { notificationManager.Unregister(); } -
要添加到
App类中的最后几项是三个静态辅助方法,用于获取一些应用程序相关的路径,以及ToForeground方法,当应用隐藏或最小化时将应用带到前台:public static void ToForeground() { if (m_window != null) { IntPtr handle = WindowNative.GetWindowHandle(m_window); if (handle != IntPtr.Zero) { SwitchToThisWindow(handle, true); } } } public static string GetFullPathToExe() { var path = AppDomain.CurrentDomain.BaseDirectory; var pos = path.LastIndexOf("\\"); return path.Substring(0, pos); } public static string GetFullPathToAsset(string assetName) { return $"{GetFullPathToExe()}\\Assets\\{assetName}"; } -
现在我们来处理
MainPage。从MainPage.xaml开始。我们将添加两个按钮来发送通知,以及一个InfoBar控件,在发送或接收通知时在页面底部显示消息。向最外层的Grid控件添加另一个RowDefinition:<Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> -
向包含现有
Button控件的StackPanel的开头添加两个按钮:<StackPanel HorizontalAlignment="Right" Orientation="Horizontal"> <Button Command="{x:Bind ViewModel.SendToastCommand}" Content="Send Notification" Margin="8,8,0,8"/> <Button Command="{x:Bind ViewModel.SendToastWithTextCommand}" Content="Send Notification with Text" Margin="8,8,0,8"/> ... </StackPanel> -
在最外层
Grid的关闭标签之前添加一个InfoBar控件:<InfoBar x:Name="notifyInfoBar" Grid.Row="3"/> -
接下来,打开
MainPage.xaml.cs文件,我们需要添加一些代码来处理传入的通知。我们首先要做的是添加MyMediaCollection.Helpers的using语句。 -
接下来,添加代码以公开
MainPage的当前实例,以便正确路由通知:public static MainPage Current; public MainPage() { ViewModel = App.HostContainer.Services.GetService<MainViewModel>(); this.InitializeComponent(); Current = this; Loaded += MainPage_Loaded; } -
接下来,添加一些代码,在发送或接收通知时更新
InfoBar。这是由NotificationShared类中的方法调用的代码:public void NotifyUser(string message, InfoBarSeverity severity, bool isOpen = true) { if (DispatcherQueue.HasThreadAccess) { UpdateStatus(message, severity, isOpen); } else { DispatcherQueue.TryEnqueue(() => { UpdateStatus(message, severity, isOpen); }); } } private void UpdateStatus(string message, InfoBarSeverity severity, bool isOpen) { notifyInfoBar.Message = message; notifyInfoBar.IsOpen = isOpen; notifyInfoBar.Severity = severity; }DispatcherQueue方法检查代码是否可以访问 UI 线程。如果不能,则使用TryEnqueue将工作排队,以便在 UI 线程可用时执行。否则,从后台线程访问 UI 元素时将遇到错误。 -
创建一个
NotificationReceived方法来处理传入的通知信息。此方法解析传入数据并构建要显示的消息字符串:public void NotificationReceived(NotificationShared.Notification notification) { var text = $"{notification.Originator}; Action: {notification.Action}"; if (notification.HasInput) { if (string.IsNullOrWhiteSpace(notification.Input)) text += "; No input received"; else text += $"; Input received: {notification.Input}"; } if (DispatcherQueue.HasThreadAccess) DisplayMessageDialog(text); else { DispatcherQueue.TryEnqueue(() => { DisplayMessageDialog(text); }); } }要添加到
MainPage的最后一段代码是一个简单的显示ContentDialog的方法,其中包含通知数据:private void DisplayMessageDialog(string message) { ContentDialog notifyDialog = new() { XamlRoot = this.XamlRoot, Title = "Notification received", Content = message, CloseButtonText = "Ok" }; notifyDialog.ShowAsync(); }MainViewModel是需要更新的最后一个类。我们需要为发送应用通知时新按钮调用的两个命令方法创建两个方法:SendToast和SendToastWithText:[RelayCommand] private void SendToast() { if (ToastWithAvatar.SendToast()) NotificationShared.ToastSentSuccessfully(); else NotificationShared.CouldNotSendToast(); } [RelayCommand] private void SendToastWithText() { if (ToastWithText.SendToast()) NotificationShared.ToastSentSuccessfully(); else NotificationShared.CouldNotSendToast(); }不要忘记将
using MyMediaCollection.Helpers;添加到MainViewModel中的 using 语句列表中。
我们已经准备好运行应用程序并测试通知。开始调试并点击 发送通知 按钮。你应该在主屏幕的右下角看到 toast 出现:

图 8.1 – Windows toast 通知
将另一个窗口置于 我的媒体收藏 前面,然后点击 toast 上的 打开应用。应用应该被带到屏幕的前面,ContentDialog 将显示有关接收到的通知的信息:

图 8.2 – 接收应用程序通知
注意
如果你点击其中一个按钮多次发送通知而不确认 toast 窗口,toast 不会堆叠。后续的 toast 将直接发送到 Windows 的 通知中心。一旦它们到达那里,就无法使用文本框等交互式字段。此外,如果用户已启用 勿扰 或 专注 模式,所有通知都将被抑制并直接发送到 通知中心。
现在点击 发送带文本的通知 按钮。当这个 toast 出现时,它将有一个文本框,你可以 输入 一个回复:

图 8.3 – 显示带有文本框的 toast 窗口
输入 Hello world 并点击 回复 按钮。现在,当 我的媒体收藏 显示 ContentDialog 时,它将包括在 toast 窗口中输入的回复:

图 8.4 – 在我们的应用程序中显示 toast 窗口的回复文本
现在,你已经准备好开始将通知构建到自己的 WinUI 应用程序中。让我们总结本章内容并讨论我们学到的知识。
摘要
在本章中,我们了解了 Windows App SDK 中可供 WinUI 开发者使用的 Windows 通知类型。我们讨论了如何使用通知来节省 Windows 资源、减少对计时器的需求并提示用户采取行动。我们探讨了如何配置原始推送通知,并将本地应用通知添加到 我的媒体收藏 示例应用程序中。你现在应该准备好将任何这些类型的通知添加到自己的 WinUI 应用程序中。
在下一章中,我们将探讨 Windows Community Toolkit(WCT)以及如何通过利用现有的助手、样式和控制来与 .NET Community Toolkit 一起节省开发时间。
问题
-
哪种 Windows 通知类型可以用来从云端启动数据同步?
-
哪种通知类型不依赖于 WNS?
-
在 Azure 中配置通知服务之前,您在哪里注册您的应用程序?
-
哪个 Windows App SDK 命名空间包含用于处理应用通知的对象?
-
哪个类提供了注册和注销应用程序以处理应用通知的方法?
-
如果您希望应用程序的通知在系统重启后消失,可以设置哪个属性?
-
WNS 的通知能否通过托盘通知提示用户?
第九章:使用社区工具包增强应用程序
Windows 社区工具包(WCT)和.NET 社区工具包是面向 Windows 和.NET 开发者的开源库集合。这些工具包包含可以由Windows UI 库(WinUI)、通用 Windows 平台(UWP)、.NET 多平台应用程序 UI(.NET MAUI)、Windows 表现基础(WPF)和Windows 窗体(WinForms)应用程序利用的控件和库。在Microsoft Store中,有一个 WCT 的配套示例应用程序,开发者可以安装它来探索控件并学习如何使用它们。
在本章中,我们将涵盖以下主题:
-
了解工具包的背景和目的
-
使用工具包示例应用程序来探索 WCT 中可用的控件
-
在 WinUI 项目中利用工具包控件
-
探索 WCT 中为 Windows 开发者提供的辅助工具、服务和扩展
-
发现.NET 社区工具包为 WinUI 开发者提供了什么
到本章结束时,你将了解 WCT 以及它如何在你构建 Windows 应用程序时提高你的生产力。你还将知道如何将其控件集成到你的 WinUI 应用程序中。
技术要求
要跟随本章中的示例,需要以下软件:
-
Windows 10 版本 1809(构建号 17763)或更高版本
-
配置了 Windows App SDK 开发工作负载的 Visual Studio 2022 或更高版本
本章的源代码可在 GitHub 上找到,网址为github.com/Packt
Publishing/Learn-WinUI-3-Second-Edition/tree/master/Chapter09。
介绍 WCT
WCT 是由微软创建的,作为一个开源的控件和其他辅助工具、服务和库的集合,专为 Windows 开发者设计。它主要被 UWP 开发者使用,但也为 WinUI、WinForms 和 WPF 开发者提供了价值。该工具包以 NuGet 包的形式提供给开发者。在 NuGet 上有超过一打的工具包包可供安装,可以根据项目需求独立安装。我们将在本章中探讨一些这些包。让我们先讨论 WCT 的历史。
工具包从一开始就是开源的。它已经在 GitHub 上长期可用,网址为github.com/CommunityToolkit/WindowsCommunityToolkit,但工具包的下一代托管在github.com/CommunityToolkit/Windows。这个工具包的新版本旨在帮助 WinUI 2、WinUI 3 和Uno Platform开发者,但这个新的工具包还没有任何发布版本。如果你有兴趣帮助推进项目,这个工具包欢迎社区贡献。WCT 的文档可在Microsoft Learn上找到,网址为learn.microsoft.com/windows/communitytoolkit/。
WCT 的起源
WCT 最初于 2016 年以UWP 社区工具包的形式推出。正如其名所示,它最初是一个仅针对 UWP 开发者的工具包。该工具包的创建是为了通过提供 Windows 开发者经常为其自己的常用库创建的控件和辅助工具来简化 UWP 应用开发。为 XAML 开发创建工具包的想法并不是新的。已经有一些其他类似的项目针对其他平台,包括以下内容:
-
WPF 工具包 (
github.com/dotnetprojects/wpftoolkit): 一套 WPF 开源控件和其他组件,最初由微软在CodePlex上托管。 -
扩展 WPF 工具包 (
github.com/xceedsoftware/wpftoolkit): 由Xceed Software维护的开源控件集合,旨在补充原始的 WPF 工具包。 -
Xamarin 社区工具包 (
github.com/xamarin/Xamarin CommunityToolkit): 一个开源的 Xamarin 控件、动画、行为和效果集合,用于Xamarin.Forms。随着 Xamarin 开发者转向.NET MAUI,现在也有了一个.NET MAUI 社区工具包 (github.com/CommunityToolkit/Maui)。
微软在开源社区的帮助下,定期更新工具包,每年多次添加新的和增强的组件和控件。2018 年春季,在 v3.0 发布前夕,他们宣布了工具包的新名称:Windows 社区工具包。这次更名标志着团队意图向前拥抱所有 Windows 开发者。
WCT 3.0 包含了一个基于旧版 Microsoft Edge 的WebView控件——不要与稍后在本章中介绍的WebView2混淆——用于 WPF 和 WinForms 应用。此次发布还添加了 Visual Basic 的代码示例,这在许多遗留的 Windows 桌面代码库中仍在使用。
工具包的另一个目的是让开发者能够在新控件上工作,希望其中一些控件将来会被集成到 Windows SDK 中(或者作为替代,WinUI 库)。自从工具包推出以来,已经发生了这种情况,包括 WebView 控件。
随后的工具包发布继续为 UWP 和桌面开发者增加价值,这些发布是由社区贡献推动的。
检查最近的工具包发布
自从 3.0 版本以来,WCT 已经发布了几个主要版本。是 WCT 7.0 首次增加了对 WinUI 3 的支持。
2018 年 8 月,WCT 4.0 添加了 DataGrid 控件,这是熟悉在 Silverlight 和 WPF 平台上可用的 DataGrid 控件的 UWP 开发者一直渴望的功能。很快,2018 年秋季发布了版本 5。这次发布为工具包带来了两个主要功能,如下所述:
-
WindowsXamlHost:这个控件使单个 UWP 控件能够被包装并托管在 WPF 或 WinForms 控件中。后来,WindowsXamlHost控件将被称为 XAML Islands,托管 API 被添加到 Windows SDK 中。还发布了一些wrapped controls,包括InkCanvas、MapControl以及对传统WebView控件的更新。 -
TabView:在DataGrid之后,一个丰富的TabView控件可能是 UWP 开发者最希望获得但尚未可用的控件。WCT 的TabView控件包括自定义、关闭以及拖放标签的支持。TabView也已经升级到 WinUI 2 库,并在 WinUI 2.2 及以后的版本中可用。
一年后,在 2019 年秋季,WCT 6.0 将 XAML Islands 控件引入了所有 WinForms、WPF 和 C++ Win32 开发者,并增加了对 .NET Core 3 客户端的支持。这次发布中的另一项重大改进是增加了 ARM64 开发支持。2020 年 6 月,团队宣布了 WCT 6.1,以及版本 7 和 8 的即将发布的预览版。2020 年发布了 WCT 7.0 的几个预览版,其最终版本于 2021 年 3 月发布。工具包的 7.0 版本包括主要的项目重构以及几个主要功能。最令人兴奋的功能是首次发布了 ViewModel 类。
2021 年 9 月发布了 WCT 7.1。它添加了一些用于 Microsoft Graph 和 Microsoft Identity 平台的辅助类,以及一些新的控件、行为、阴影和样式。2021 年 11 月发布了 7.1.2 版本。这被宣布为包含工具包中 .NET 库更新的最终版本,因为这些库都将迁移到 .NET Community Toolkit。所有随后的 7.x 版本都是 UWP 独有的更新。
新发布的 WCT 8.0 支持 WinUI 3。之前,使用 WCT 支持的 .NET 创建 WinUI 3 应用程序仅在新的 WCT 仓库中处于预览状态。尝试这些包需要从源代码构建工具库,因为它们在 GitHub 上没有发布版本。现在,它们作为稳定的 NuGet 包提供,您可以像添加任何其他包一样将它们添加到您的项目中。
现在我们已经介绍了 WCT 的背景和历史,我们将快速查看工具包中的一些控件和组件。
探索 WCT 图库应用
如我们本章前面提到的,WCT 图库应用 可从 Microsoft Store 获取 (apps.microsoft.com/store/detail/windows-community-toolkit-gallery/9NBLGGH4TLCQ)。它可以在 Windows 10 版本 17763 或更高版本、Windows 11 上安装,甚至可以在您的 Xbox、Surface Hub 或 HoloLens 设备上安装。与我们在 第五章 中讨论的 WinUI 3 图库 应用一样,工具包示例应用为我们提供了一个轻松导航和探索 WCT 内容的方法。
安装和启动应用
让我们开始吧:
- 在 搜索 框中打开
windows community:

图 9.1 – 在 Microsoft Store 中查找应用
- 从搜索结果中选择 Windows Community Toolkit 图库 并点击结果页面上的 安装 按钮。安装完成后,安装 按钮将变为 打开 按钮。从那里或从 Windows 开始菜单打开 WCT 应用:

图 9.2 – WCT 图库应用
应用打开到概述页面,其中有一些突出显示的控件、辅助工具和行为。顶部部分还有一些链接到 WCT 文档、GitHub 仓库和他们的 Discord 社区的有用链接。
控件和其他组件在左侧分为七个部分:动画、控件、扩展、辅助工具、布局、Xaml 和应用的 设置。由于工具包中有许多控件,我们将探索其中的一些,其余的留给您自己探索。
控件
点击应用左侧的 控件 导航项以显示控件列表。这是应用中最大的部分,控件按类别分组,如下所示:
-
输入:这些是自定义输入控件(例如,RadialGuage)
-
布局:布局面板和相关控件(例如,HeaderedTreeView)
-
媒体:用于处理媒体的控件(例如,CameraPreview)
-
尺寸调整器:这些是内容尺寸控件(例如,GridSplitter)
-
状态和信息:用于更新用户进度或状态的控件。在当前版本的 app 中,该部分只包含MetadataControl
-
文本:这些是文本输入控件类型(例如,RichSuggestBox)
你可以在以下屏幕截图中看到一些展开的导航面板中的控件:

图 9.3 – 在 WCT Gallery 应用中展开的控件菜单
选择这些控件之一将打开一个包含几个区域的页面。主面板是一个交互式区域,你可以与所选控件交互。在此面板右侧有一些下拉框,可以更新控件的行为,还有一个按钮,可以切换浅色或深色主题,这将更新面板中运行的控件。
右侧面板包含几个控件,用于修改当前控件的显示和行为。右侧面板上看到的控件数量将根据所选控件而变化。查看代码按钮将展开一个 XAML 编辑器,其中包含在主面板中运行的代码的标记。你可以在这里更改标记,你的更改将在主面板中运行的代码中反映出来。C#选项卡将显示控件的 C#代码。在页面的交互式部分下方,显示来自Microsoft Learn的控件文档:

图 9.4 – 在 WCT Gallery 应用中查看 ImageCropper 控件
在工具包的控件部分花些时间探索ImageCropper控件和MarkdownTextBlock。按照以下步骤操作:
-
打开ImageCropper控件并尝试使用它。点击裁剪形状下拉控件,选择圆形,然后观察裁剪区域变为圆形形状。
-
接下来,在左侧面板中选择RangeSelector控件。此控件的页面类似,但在主面板上有一个 RangeSelector 控件,其中两个滑块可以移动以选择范围的最小和最大点。最小和最大允许的值也可以从面板的右侧控制:

图 9.5 – RangeSelector 在 WCT Gallery 应用中运行
- 滚动查看控件的一个示例用法。
在这个 app 的部分,你可以探索更多控件。你应该花些时间找出哪些可能在你的下一个项目中有用。
现在我们已经探索了示例应用中的几个控件,让我们尝试在一个 WinUI 项目中使用它们。
使用工具包中的控件
在上一节中,我们探索了示例应用中的几个 WCT 控件。现在,是时候在 WinUI 项目中使用它们了。为了演示一些控件的实际应用,我们将创建一个新的WinUI 3 Desktop项目。
注意
在撰写本文时,WCT 控件不建议在生产应用中使用,并有一些注意事项。要了解更多关于当前限制的信息,您可以阅读这篇 Microsoft 博客文章:devblogs.microsoft.com/ifdef-windows/windows-community-toolkit-for-project-reunion-0-5/。
创建 WinUI 项目
要开始我们的 WCT 项目,您需要启动 Visual Studio 并按照以下步骤操作:
-
创建一个新的项目。然后在搜索字段中的
WinUI in Desktop。 -
将显示几个项目类型,但顶部结果之一将是 Blank App, Packaged (WinUI 3 in Desktop)。选择您选择的语言的此项目模板并点击 Next。
-
将项目命名为
HardwareSupplies并点击App.xaml和MainWindow.xaml:

图 9.6 – Visual Studio Solution Explorer 中的 HardwareSupplies 项目
-
如果您打开
MainWindow.xaml文件,您将看到一些简单的起始标记。有一个StackPanel控件,其中包含一个名为myButton的Button控件,其内容为Click Me。代码如下所示:<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> <Button x:Name="myButton" Click="myButton_Click"> Click Me </Button> </StackPanel> -
myButton控件的Click事件在MainWindow.xaml.cs中有一个名为myButton_Click的事件处理程序,它会将myButton变量的内容更改为Clicked,如下面的代码片段所示:private void myButton_Click(object sender, RoutedEventArgs e) { myButton.Content = "Clicked"; } -
在我们进行任何更改之前,运行应用程序并测试按钮以确保一切按预期工作:

图 9.7 – 首次运行 HardwareSupplies 应用
一切都按预期工作。接下来,我们将向项目中添加 WCT 包引用。
引用 WCT 包
我们需要为应用程序的主要控件是一个显示硬件项目列表的 DataGrid 控件。我们还将添加一个 HeaderedContentControl 控件和一个 DropShadowPanel 控件,以了解这些控件如何使用。大多数 WCT 控件都是 communitytoolkit.winui 的一部分,并将这两个包的最新版本添加到项目中:

图 9.8 – 添加 WCT NuGet 包
安装这两个包后,关闭包管理器窗口并编译项目以确保所有包都已下载。接下来,我们将为 DataGrid 控件设置一些数据。
向 DataGrid 控件添加数据
任何 DataGrid 控件最重要的部分是向用户展示的数据。在我们开始构建 UI 之前,我们将构建一个小型的硬件数据库存以显示。按照以下步骤操作:
-
首先向
HardwareSupplies项目添加一个名为HardwareItem的新类。该类将具有六个属性,如下所示:public class HardwareItem { public long id { get; set; } public string name { get; set; } public string category { get; set; } public int quantity { get; set; } public decimal cost { get; set; } public decimal price { get; set; } } -
接下来,打开
MainWindow.xaml.cs文件。创建一个名为HardwareItems的公共属性,并将其定义为HardwareItem的数组:public HardwareItem[] HardwareItems { get; set; } -
现在,创建一个名为
PopulateItems的新方法。此方法将初始化HardwareItems数组并用 12 个项目填充它:private void PopulateItems() { HardwareItems = new HardwareItem[] { new HardwareItem { id = 1, name = "Wood Screw", category = "Screws", cost = 0.02M, price = 0.10M, quantity = 504 }, new HardwareItem { id = 2, name = "Sheet Metal Screw", category = "Screws", cost = 0.03M, price = 0.15M, quantity = 655 }, new HardwareItem { id = 3, name = "Drywall Screw", category = "Screws", cost = 0.02M, price = 0.11M, quantity = 421 }, new HardwareItem { id = 4, name = "Galvanized Nail", category = "Nails", cost = 0.04M, price = 0.16M, quantity = 5620 }, new HardwareItem { id = 5, name = "Framing Nail", category = "Nails", cost = 0.06M, price = 0.20M, quantity = 12000 }, new HardwareItem { id = 6, name = "Finishing Nail 2 inch", category = "Nails", cost = 0.02M, price = 0.11M, quantity = 1405 }, new HardwareItem { id = 7, name = "Finishing Nail 1 inch", category = "Nails", cost = 0.01M, price = 0.10M, quantity = 1110 }, new HardwareItem { id = 8, name = "Light Switch - White", category = "Electrical", cost = 0.25M, price = 1.99M, quantity = 78 }, new HardwareItem { id = 9, name = "Outlet - White", category = "Electrical", cost = 0.21M, price = 1.99M, quantity = 56 }, new HardwareItem { id = 10, name = "Outlet - Beige", category = "Electrical", cost = 0.21M, price = 1.99M, quantity = 90 }, new HardwareItem { id = 11, name = "Wire Ties", category = "Electrical", cost = 0.50M, price = 4.99M, quantity = 125 }, new HardwareItem { id = 12, name = "Switch Plate - White", category = "Electrical", cost = 0.21M, price = 2.49M, quantity = 200 } }; }应用程序现在拥有一个不错的螺丝、钉子和电气元件的集合,可以在
DataGrid控件中展示。 -
删除
myButton_Click事件处理程序,因为它不再需要。 -
最后,在
MainWindow构造函数的末尾调用PopulateItems:public MainWindow() { this.InitializeComponent(); PopulateItems(); }
数据已经准备好了。让我们继续并定义 MainWindow 的 XAML 标记。
向 MainWindow 控件添加控件
我们应用程序的 UI 将很简单。我们将使用带有一些标题文本的阴影在 DataGrid 控件中显示数据。
注意
DropShadowPanel 控件将在未来从工具包中移除。您可以考虑使用 AttachedDropShadow 或 AttachedCardShadow 控件作为替代方案。有关更多信息,请阅读有关 附加 阴影 的内容:learn.microsoft.com/windows/communitytoolkit/helpers/attachedshadows。
按照以下步骤进行:
-
首先,在
MainWindow.xaml中的Grid控件内放置一个HeaderedContentControl控件。将Header属性设置为Hardware Inventory。这将显示在MainWindow控件内容的顶部。将Margin设置为6以在控件边缘留出一些空间:<Grid> <wct:HeaderedContentControl Header="Hardware Inventory" Margin="6"> </wct:HeaderedContentControl> </Grid> -
不要忘记为 WCT 控件添加一个命名空间定义,如下所示:
<Window x:Class="HardwareSupplies.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:HardwareSupplies" xmlns:wct="using:CommunityToolkit.WinUI.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> -
接下来,定义一个
DropShadowPanel控件作为HeaderedContentControl的内容。BlurRadius定义了阴影模糊区域的宽度。ShadowOpacity的值为1表示阴影最暗的部分将完全不透明。大部分阴影将位于DataGrid控件后面。OffsetX和OffsetY的值将使阴影向下和向右移动2像素。Color属性设置阴影的颜色。将IsMasked设置为True会创建一个更精确的阴影,但会降低性能。在我们的情况下,性能不会成为问题。最后,我们将Margin设置为6以留出一些空间来查看阴影:<wct:HeaderedContentControl Header="Hardware Inventory" Margin="6" x:Name="headerPanel"> <wct:DropShadowPanel BlurRadius="8" ShadowOpacity="1" OffsetX="2" OffsetY="2" Color="Gray" IsMasked="True" Margin="6"> </wct:DropShadowPanel> </wct:HeaderedContentControl> -
最后,将
DataGrid添加为DropShadowPanel的子控件。网格将绑定到我们创建的HardwareItems属性。AutoGenerateColumns属性将使用HardwareItem对象属性的名称创建列标题。通过将Background和AlternatingRowBackground设置为ThemeResource样式,网格将为使用DataGrid的 Windows 用户看起来很棒;DataGrid将是透明的,而灰色的阴影将遮挡网格的内容:<wct:DropShadowPanel BlurRadius="8" ShadowOpacity="1" OffsetX="2" OffsetY="2" Color="Gray" IsMasked="True" Margin="6"> <wct:DataGrid ItemsSource="{x:Bind HardwareItems}" AutoGenerateColumns="True" AlternatingRowBackground="{ThemeResource SystemControlBackgroundListLowBrush}" Background="{ThemeResource SystemControlBackgroundAltHighBrush}"/> </wct:DropShadowPanel> -
应用程序的代码已经完成。现在是时候构建并运行应用程序,看看一切看起来如何:

图 9.9 – 运行带有数据的 HardwareSupplies 应用程序
在这里,你可以看到,通过一点代码,我们就有了一个相当不错的应用程序来显示一些硬件库存数据。标题文本、阴影和丰富的DataGrid控件很好地协同工作,以创建我们的 UI。
让我们通过查看工具包中可用的其他组件来结束对 WCT 的探索。
探索工具包的辅助工具、服务和扩展
我们已经讨论了 WCT 中的许多控件,但工具包中包含的不仅仅是 UI 控件。在本节中,我们将回到 WCT 示例应用,探索工具包中的一些其他组件。我们将从一些辅助类开始。
辅助工具
在工具包中的控件旁边,辅助工具部分包含最多的组件。与控件一样,辅助工具在示例应用中按类别划分,如下所示:
-
数据: 这些辅助工具与加载数据和显示数据相关。例如包括ImageCache、ObservableGroup和Incremental Loading Collection。
-
开发者: 这些是对于开发者有用的辅助工具,包括用于从后台线程更新 UI 的DispatcherQueueHelper。
-
通知: 这些辅助工具提供了使用 Windows 通知和开始菜单通知用户的定制方式。包括LiveTile、Toast和WeatherLiveTileAndToast。然而,我们在上一章中已经看到,Windows App SDK 现在在 WinUI 3 应用程序中内置了对通知的支持。这些工具包辅助工具对于 UWP 开发者很有用。
-
使用Win2D的
CanvasGeometry类。 -
状态触发器: 工具包中目前有 10 个状态触发辅助工具,包括IsNullOrEmptyStateTrigger、FullScreenModeStateTrigger和RegexStateTrigger。
-
系统: 目前包括 14 个系统辅助工具,如CameraHelper、NetworkHelper、PrintHelper和ThemeListener。
是时候仔细看看工具包中的一个辅助工具了。让我们看看SystemInformation辅助类提供了什么。这是一个静态类,包含有关运行应用程序和用户系统的许多有用信息。以下只是可用属性中的一小部分:
-
ApplicationName: 应用程序名称 -
ApplicationVersion: 应用程序版本 -
AvailableMemory: 可用系统内存 -
文化: Windows 中当前设置的文化 -
DeviceFamily: 用户设备家族的名称 -
DeviceModel: 当前设备的型号 -
FirstUseTime: 应用程序首次启动的时间 -
IsAppUpdated: 指示这是否是应用程序更新后第一次运行 -
LaunchCount: 自系统重置以来应用程序启动的次数 -
OperatingSystem: 操作系统名称 -
OperatingSystemVersion: 操作系统版本
在示例应用中,你可以探索许多其他辅助工具。我们将通过回顾 WCT 示例应用中的扩展区域的一些其他工具来结束本次介绍。
扩展
样例应用中的扩展菜单包含几个项目,这些项目为 WinUI 控件添加扩展属性,并为其他类添加扩展方法。我们将在此处回顾FrameworkElementExtensions(以前称为鼠标扩展)和StringExtensions。
FrameworkElement控件,以便在鼠标移至该元素时设置鼠标光标:
<Button ui:FrameworkElementExtensions.Cursor="Wait"
Content="Show Wait Cursor" />
<Button ui:FrameworkElementExtensions.Cursor="Hand"
Content="Show Hand Cursor" />
<Button ui:FrameworkElementExtensions.Cursor="UniversalNo"
Content="Show UniversalNo Cursor" />
StringExtensions包含一些与字符串数据相关的扩展方法,如下所示:
-
IsEmail:确定一个字符串是否是有效的电子邮件地址格式 -
IsDecimal:确定一个字符串是否是十进制值 -
IsNumeric:确定一个字符串是否是数值 -
IsPhoneNumber:确定一个字符串是否包含有效的电话号码格式 -
IsCharacterString:确定一个字符串是否只包含字母 -
DecodeHtml:返回一个字符串,其中任何 HTML 格式、标签、注释、脚本和样式都被移除 -
FixHtml:类似于DecodeHtml,它返回一个字符串,其中包含所有 HTML 格式、注释、脚本和样式 -
Truncate:将字符串截断到指定的长度,可选地添加省略号
Truncate扩展包括两个重载。以下代码将截断name字符串,使其长度不超过 10 个字符。它将截断city字符串到七个字符,并在字符串末尾添加省略号以指示已截断,如下所示:
string name = "Bobby Joe Johnson";
string city = "San Francisco";
name.Truncate(10); // name will be "Bobby Joe "
city.Truncate(7, true); // city will be "San Fra..."
我鼓励您探索这些扩展,以及 WCT 中的所有其他扩展。样例应用是可视化探索工具包并获得将其集成到您自己的项目中的想法的好方法。
在我们结束之前,让我们简要讨论一下.NET 社区工具包。
.NET 社区工具包功能
.NET 社区工具包可以被所有.NET 开发者利用。在第三章 可维护性和可测试性的 MVVM中,我们使用了 MVVM Toolkit,它是.NET 社区工具包的一部分。此工具包还有其他一些功能,主要针对性能和诊断。
Guard和ThrowHelper。
Guard API 用于验证传递到您的.NET 方法中的参数。它们被设计成快速,对应用程序性能的影响最小。以下是一些它们使用示例:
public void TestData(decimal[] numbers, int size, string data)
{
Guard.IsNotNull(numbers);
Guard.IsInRangeFor(size, numbers);
Guard.IsNotNullOrWhitespace(data);
}
您可以在Microsoft Learn文档中查看一组完整的辅助方法:learn.microsoft.com/dotnet/api/microsoft.toolkit.diagnostics.guard#methods。
ThrowHelper类是一种高效抛出异常的方法。它旨在与Guard辅助器很好地协同工作。语法类似于.NET 中内置的抛出异常的方式。以下代码可以用来从之前显示的TestData方法中抛出ArgumentException异常:
ThrowHelper.ThrowArgumentException<int>(nameof(size));
CommunityToolkit.HighPerformance 包包含针对高性能代码的辅助程序和扩展。同样,该包适用于 .NET 和 .NET Standard 目标。
在 HighPerformance 包中可用的以下成员:
-
Span2D<T>: 此类型与Span<T>类型具有相同的功能,但支持 2D 内存。 -
Memory2D<T>: 此类型与Memory<T>类型具有相同的功能,但支持 2D 内存位置。 -
SpanOwner<T>: 此类型是一个仅使用堆栈缓冲区的类型,它利用共享内存池来借用仅在同步代码中使用的内存。 -
MemoryOwner<T>: 此类型是另一种缓冲区类型。它实现了IMemoryOwner<T>,并且是ArrayPool<T>的轻量级包装。 -
StringPool: 此类型是string对象的可配置池。当从缓冲区或流中创建大量字符串时,它可以提高性能。 -
ParallelHelper: 此辅助类包含一组用于在 .NET 中处理并行代码的 API。它具有以下辅助方法的多个重载:For、For2D和ForEach。这些辅助方法中的每一个都创建一个优化的并行循环。 -
Ref<T>: 此类型是一个仅使用堆栈的类型,它存储对值的引用。它可以用作 C# 代码中ref T值的替代,因为它们在其他情况下不受支持。还有一个ReadOnlyRef<T>类型。
对于这些类型及其使用场景的深入了解,你应该查看 Microsoft Learn 上的文档:learn.microsoft.com/dotnet/communitytoolkit/high-performance/introduction。.NET Community Toolkit 的源代码可在 GitHub 上找到:github.com/CommunityToolkit/dotnet。
现在,让我们总结并回顾本章所学的内容。
摘要
在本章中,你了解了 WinUI 开发者在 WCT 和 .NET Community Toolkit 中可用的控件、辅助程序、服务和其它组件。我们还练习了将一些 WCT 控件添加到 WinUI 3 项目中,利用强大的 DataGrid 控件。最后,我们安装并使用了 WCT 示例应用程序来发现工具包中我们可以用于我们应用程序的控件和组件。将 WCT 包添加到你的应用程序中将为你的应用程序提供具有丰富功能性和节省时间的扩展控件。
在下一章中,我们将使用 Visual Studio 的 Template Studio for WinUI 扩展来学习如何快速创建一个包含丰富控件和组件的新 WinUI 应用程序。
问题
-
WCT 的原始名称是什么?
-
哪个 WCT 旧版浏览器控件可用于 WPF 或 WinForms 应用程序?
-
哪个 WCT 控件可以渲染 Markdown 输出?
-
WCT 中哪个辅助程序可以管理和将项目分组到可观察集合中?
-
运行 WinUI 3 应用程序的可视 Studio 项目模板的名称是什么?
-
这些中哪一个被移动到了 .NET Community Toolkit:
DataGrid控件、MVVM Toolkit 还是PrintHelper? -
WCT 中的哪个扩展类包含验证字符串的方法,包括
IsEmail和IsPhoneNumber? -
哪个 WCT 扩展可以更新控制级别的 Windows 光标?
第十章:使用 Template Studio 加速应用开发
从零开始启动新项目可能会令人望而却步。应用架构和项目布局的最佳实践是什么?Template Studio for WinUI 是一个开源项目,最初是为 UWP 和 WPF 应用程序开发的 Windows Template Studio。Template Studio 的每个版本都是 Visual Studio 的扩展。现在它支持 WinUI,并创建了将生成 .NET MAUI 和 Uno Platform 项目的变体。
在本章中,我们将涵盖以下主题:
-
了解 Template Studio 是什么以及它如何帮助开发者遵循最佳模式和规范创建新的 WinUI 项目
-
查看 Windows Template Studio 和 UWP 应用程序的历史
-
使用 Template Studio for WinUI 和 MVVM Toolkit 创建一个新项目
-
了解 Template Studio 的其他变体
在本章结束时,您将了解当前 Visual Studio 的 Template Studio 扩展的起源。您还将对 Template Studio for WinUI 有足够的了解,以便在开始您的下一个 WinUI 3 项目时选择它。您还将知道您可以去哪里提出改进建议、提交问题或通过向开源项目提交自己的更改来改进项目。
技术要求
要跟随本章中的示例,请参阅 第二章 的 技术要求 部分,配置开发环境和创建项目。
本章的源代码可在 GitHub 上找到:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/master/Chapter10
Template Studio for WinUI 概述
Template Studio for WinUI 是一个 Visual Studio 的开源扩展,它增强了创建新 WinUI 3 项目的体验。目前,Microsoft 提供了三个版本的 Template Studio 扩展:
-
Template Studio for WinUI (C#) – 这是本章我们将使用的扩展。也计划为 Template Studio for WinUI 开发 C++ 版本。
-
Template Studio for WPF – Template Studio 的 WPF 版本与 WinUI 扩展类似。它创建一个 .NET WPF 项目。
-
Template Studio for UWP – 这个版本是最初命名为 Windows Template Studio 的更新扩展。
Template Studio 项目的代码和文档可在 GitHub 上找到:github.com/microsoft/TemplateStudio。团队还在 GitHub 上维护一个发布路线图,您可以进行监控:github.com/microsoft/TemplateStudio/blob/main/docs/roadmap.md。
使用 Template Studio 扩展之一创建新项目将启动一个增强的向导式体验,您可以在其中选择要创建的项目类型和要遵循的设计模式(例如 MVVM)。您还可以选择一些预定义的页面和其他要包含在项目中的功能,以及一个单元测试项目。我们将在下一节中逐步介绍整个体验,并讨论选项。
Template Studio for WinUI 的安装方式与其他 Visual Studio 扩展相同。如果您不熟悉这个过程,您可以从 Visual Studio Marketplace 下载VSIX包(marketplace.visualstudio.com/items?itemName=TemplateStudio.TemplateStudioForWinUICs)或您可以在 Visual Studio 的管理扩展对话框中搜索并添加它。

图 10.1 – Visual Studio Marketplace 中的 Template Studio for WinUI
我们将在本章中使用管理扩展对话框来安装扩展。有关在 Visual Studio 中管理扩展的详细信息,请参阅 Microsoft Learn 上的此文档:learn.microsoft.com/visualstudio/ide/finding-and-using-visual-studio-extensions。
注意
如果您对 Visual Studio 扩展包或 VSIX 的内部工作原理感兴趣,您可以在 Microsoft Learn 上了解更多信息:learn.microsoft.com/visualstudio/extensibility/anatomy-of-a-vsix-package
扩展生成的代码将继续在您构建新创建的项目时引导您。其中包含带有指导的 TODO 注释,说明您在哪里添加自己的代码,以及指向解释代码中使用的概念和控制方式的文档的有用链接。该扩展会频繁更新,以反映最新的 WinUI 功能、实践和针对 Windows 开发者的建议。
对 GitHub 上 Template Studio 项目的贡献都将经过审查,以确保它们遵循良好的编码风格、流畅的设计和全过程的帮助性注释。
现在我们已经回顾了 WinUI Template Studio 的一些基础知识,让我们直接使用扩展创建一个新项目。
使用 Template Studio 开始新的 WinUI 项目
在本节中,我们将使用 Template Studio for WinUI 创建一个新的 WinUI 项目。我们将安装扩展,创建具有多个页面和功能的 projek,然后运行项目以探索在添加任何自己的代码之前提供的内容。在下一节中,我们将更深入地研究生成的代码,看看如果我们正在构建一个生产应用程序,我们将从哪里开始扩展和增强项目:
- 要开始,打开 Visual Studio 并选择 无代码继续 以打开 IDE 而不加载解决方案:

图 10.2 – Visual Studio 2022 启动对话框
- 在菜单中,选择 扩展 | 管理扩展 以打开 管理扩展 对话框:

图 10.3 – Visual Studio 中的管理扩展对话框
- 在
template studio。在搜索结果列表中,选择 下载 WinUI (C#) 的模板工作室:

图 10.4 – 从管理扩展安装模板工作室 for WinUI
您需要重新启动 Visual Studio 以完成扩展的安装。
-
安装完成后,启动 Visual Studio 并选择 创建新项目。
-
在
template studio。新项目模板应首先出现在结果列表中:

图 10.5 – 选择 WinUI 的模板工作室以创建新项目
-
选择
TemplateStudioSampleApp。 -
点击 创建 继续操作。Visual Studio IDE 不会立即加载,而是会显示模板工作室向导:

图 10.6 – 模板工作室 for WinUI 的第一页
向导的第一页会提示您选择项目类型。选项包括我们在第五章“探索 WinUI 控件”中讨论的 NavigationView 控件。它也是 WinUI 3 画廊应用中使用的导航方法。
- 选择 下一步 继续操作。在设计模式屏幕上,唯一可用的选项是 MVVM Toolkit。一些版本的模板工作室提供其他 MVVM 框架或 后置代码 选项。对于 WinUI 3,我们只有一个选择:

图 10.7 – 设计模式屏幕
您还会注意到右侧面板包含 您的项目详细信息 标题以及迄今为止已选择的选项。当您更熟悉模板工作室时,您将能够审查这些选项,并在对默认选择满意的情况下,点击 创建 完成向导,而无需访问每个屏幕。
- 点击 下一步 并查看 页面 屏幕:

图 10.8 – 模板工作室 for WinUI 的页面屏幕
默认情况下,已选择一个 空白 页面。此页面的默认名称为 Main。您可以在 您的项目详细信息 面板中更改任何页面的名称。
- 让我们在下一节中选取几个其他页面类型进行审查。选择数据网格、列表详情、网页视图和设置。如果您喜欢,还可以选择更多:

图 10.9 – 为我们的项目选择几个附加页面
除了设置之外,任何页面都可以重命名。我们将保留默认名称。
- 点击下一步继续到功能屏幕:

图 10.10 – 探索 WinUI 的 Template Studio 功能屏幕
功能屏幕上的默认选择是设置存储、MSIX 打包和主题选择。我们将保留这些默认选择。如果您想将 Windows App SDK 依赖项包含到您的项目中,可以选择自包含。如果您打算手动使用xcopy 部署分发您的应用程序,这将很有用。我们将在第十四章中讨论部署选项,打包和部署 WinUI 应用程序。另一个未选择的选项是应用通知。我们已经在第八章中探讨了通知,向 WinUI 应用程序添加 Windows 通知。如果您想了解更多关于任何功能的信息,请选择详细信息。
- 点击下一步继续到向导的最终步骤,测试:

图 10.11 – WinUI 的 Template Studio 向导的测试屏幕
-
在测试屏幕上唯一的选项是MSTest。选择它以将单元测试项目添加到您的解决方案中。
-
我们已经到达了向导的末尾。选择创建以创建解决方案并启动 Visual Studio IDE:

图 10.12 – 加载了我们的新解决方案的 Visual Studio
- 默认情况下,将打开名为
README.md的 Markdown 文件。如果您想查看 Markdown 的格式化预览,可以在编辑器窗口的顶部选择预览按钮:

图 10.13 – 查看格式化的 README 文件
该解决方案包含三个项目:TemplateStudioSampleApp、TemplateStudioSampleApp.Core和TemplateStudioSampleApp.Tests.MSTest。我们将在下一节中讨论这些项目的目的和内容:

图 10.14 – 在解决方案资源管理器中查看项目
- 现在是运行应用程序的时候了。开始调试并确保在启动时没有编译时或运行时错误:

图 10.15 – 运行 TemplateStudioSampleApp 解决方案
所有的内容都应该按预期运行,你可以尝试使用左侧的 NavigationView 控件导航到每个页面。设置按钮将始终出现在导航栏的底部。
现在我们已经有一个可工作的应用程序,让我们花些时间来了解为我们生成的内容。
探索由 Template Studio 生成的代码
是时候回顾新生成的 README.md 文件的内容了,其中包括项目概述及其目的。
探索核心项目
TemplateStudioSampleApp.Core 项目是一个针对 .NET 7 的类库项目。这是放置任何打算在项目间共享的代码的地方。该项目包含四个文件夹:
-
Contracts:项目中服务的接口保存在Services子文件夹中。任何在这个项目中需要的新的接口都应该在这里创建。 -
Helpers:这个文件夹包含一个Json辅助类,其中包含将 JSON 字符串和 .NET 对象之间进行转换的方法。将你自己的常用辅助类添加到这个文件夹中。 -
Models:这个文件夹中有一些用于在DataGridPage和ListDetailPage视图中填充公司和订单数据的示例模型类。 -
Services:这个文件夹包含FileService和SampleDataService类。FileService类使用 .NET 的System.IO和System.Text命名空间中的类读取和写入文件。SampleDataService创建静态数据以填充 UI 的模型类。任何其他共享服务都可以添加到这里。
注意
当这本书出版时,Template Studio 扩展可能已经更新,可以创建 .NET 8 库。
接下来,让我们探索解决方案中最大的项目 TemplateStudioSampleApp。
探索主要项目
主要项目 TemplateStudioSampleApp 是解决方案中三个项目里最大的一个。它包含了所有的用户界面标记和逻辑。在与其他 WinUI 项目合作后,你可能会在项目的根目录中认出这些文件。App.xaml、MainWindow.xaml 和 Package.appxmanifest 都在那里,还有每个 XAML 文件的 C# 代码后文件。
我们在用 Template Studio 向导配置项目时选择的每个页面的 Page 和 ViewModel 类的内容。还有处理子页面之间导航的 ShellPage 和 ShellViewModel 类。NavigationView 控件是 MainWindow 的直接子项。
接下来,让我们浏览剩余文件夹的内容:
-
Activation:这个文件夹包含ActivationHandler、DefaultActivation和IActivationHandler类。它们是使用INavigationService在ShellPage内部Frame中导航到所选页面的辅助类。如果你需要更改默认激活行为,你会创建一个新的继承自ActivationHandler的处理程序。 -
Assets: 这包含项目的图形资产。Assets文件夹存在于每个 WinUI 项目中。 -
Behaviors: 此文件夹包含一个名为NavigationViewHeaderMode的枚举,它指定了导航栏的外观,以及包含实现这些模式的逻辑的NavigationViewHeaderBehavior。行为类从Behavior<NavigationView>继承。任何其他自定义 WinUI 行为都将添加到这里。 -
Contracts: 这是您保存项目接口的地方。两个子文件夹Services和ViewModels包含对应项目文件夹中八个现有类的接口。 -
Helpers: 任何特定于此项目的辅助类都保存在这里。默认提供的辅助类包括NavigationHelper、SettingsStorageExtensions和TitleBarHelper。SettingsStorageExtensions辅助类利用Windows.Storage和Windows.Storage.Streams命名空间中的成员来读取和写入用户设置。 -
Models: 示例数据模型保存在LocalSettingsOptions类中,该类存储设置文件的名称和位置。任何其他与 UI 相关的模型类都将添加到这里。 -
Services:Services文件夹中有七个服务类。您可能可以通过其名称确定每个类的用途:ActivationService、LocalSettings服务、NavigationService、NavigationViewService、PageService、ThemeSelectorService和WebViewService。您应该花些时间审查这些服务中的代码。 -
Strings: 此文件夹包含特定语言的Resources.resw文件,用于本地化应用程序中显示的任何字符串值。英语语言资源文件可在en-us文件夹中找到。有关本地化的更多信息,您应该在 Microsoft Learn 上阅读 本地化您的 WinUI 3 应用程序:learn.microsoft.com/windows/apps/winui/winui3/localize-winui3-app。 -
Styles: 此文件夹包含三个包含 XAMLResourceDictionary元素的文件:FontSizes.xaml、TextBlock.xaml和Thickness.xaml。以下以FontSizes.xaml的内容为例:<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/ 2006/xaml"> <x:Double x:Key="LargeFontSize">24</x:Double> <x:Double x:Key="MediumFontSize">16</x:Double> </ResourceDictionary>通过集中定义应用程序的大或中号字体大小,您可以在以后更改它而无需修改多个 XAML 视图。如果您正在使用
TextBlock或RichTextBlock,字体大小设置的另一种选择是使用在 XAML 主题资源中定义的 XAML 类型渐变(在 第七章,*Windows 应用程序的流畅设计系统)中讨论过):learn.microsoft.com/windows/apps/design/style/xaml-theme-resources#the-xaml-type-ramp。
在我们继续探索 App.xaml.cs 文件之前。这里的一切都应该对您来说都很熟悉。项目使用 IHostBuilder.ConfigureServices 方法添加 ActivationHandler 以及所有服务、视图和 ViewModel 类到 DI 容器中。服务和核心服务作为单例对象添加,而所有其他类都使用 AddTransient 添加。
App 类还有一个方便的辅助方法,可以从 DI 容器中获取实例:
public static T GetService<T>() T : class
{
if ((App.Current as App)!.Host.Services.GetService
(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to
be registered in ConfigureServices within
App.xaml.cs.");
}
return service;
}
这封装了一些异常处理逻辑,以防请求的类型找不到,从而减少了在整个应用程序中重复此代码的需要。
注意每个类都有有用的注释,其中包含指向 Microsoft Learn 资源的相关 API 或其他主题的链接。App 类包含指向 .NET 依赖注入、日志记录和其他学习资源的链接。
让我们通过快速查看由 Template Studio 创建的 MSTest 项目来结束本节。
探索 MSTest 项目
TestClass、一个 README.md 文件,以及 Initialize 和 Usings 类。您应该仔细阅读 README.md 文件。它包含有关项目、测试 UI 元素、依赖注入和模拟的信息。其中包含如何利用 DI 和模拟来测试 SettingsViewModel 的示例,而无需实际读取或写入文件系统中的任何设置。
TestClass 包含了在类和测试级别进行测试初始化和清理的示例,以及两个示例测试方法:TestMethod 和 UITestMethod:
[TestMethod]
public void TestMethod()
{
Assert.IsTrue(true);
}
[UITestMethod]
public void UITestMethod()
{
Assert.AreEqual(0, new Grid().ActualWidth);
}
任何带有 UITestMethod 属性装饰的测试方法都会在 UI 线程上运行。生成的代码尚未测试 TemplateStudioSampleApp 或 TemplateStudioSampleApp.Core 项目中的任何成员。添加您自己的测试方法来执行此操作取决于您。您可以通过阅读 Microsoft Learn 上的 Visual Studio 单元测试文档开始:learn.microsoft.com/visualstudio/test/getting-started-with-unit-testing。
在下一节中,我们将讨论由 Microsoft 和第三方创建的一些其他 Template Studio 扩展。
Template Studio 为其他 UI 框架提供的扩展
我们已经深入探讨了 Template Studio for WinUI 扩展,但关于 UI 框架呢?正如我们在本章前面提到的,Microsoft 还维护了 Template Studio for WPF 和 Template Studio for UWP 的扩展。实际上,他们甚至有一个 Microsoft Web Template Studio 可用:github.com/microsoft/WebTemplateStudio。它支持前端使用 React、Angular 或 Vue.js,以及后端框架使用 Node.js、Flask、Molecular 和 ASP.NET Core。如果您对 Web 开发感兴趣,您应该去看看。
此外,还有一个名为MAUI 应用加速器的扩展(marketplace.visualstudio.com/items?itemName=MattLaceyLtd.MauiAppAccelerator),这是 Template Studio 的 .NET MAUI 版本。在本章中,我们将继续关注 WinUI 和 WPF 模板。我们将最后审查的两个是 Template Studio for WPF 和与 Visual Studio 的 Uno Platform 扩展一起提供的 Template Studio 风格向导。让我们从 WPF 开始。
Template Studio for WPF
Template Studio for WPF 扩展(marketplace.visualstudio.com/items?itemName=TemplateStudio.TemplateStudioForWPF)与其 WinUI 对应版本类似。向导中有一个额外的步骤(服务),并且在一些页面上有一些不同的选项。WPF 开发者可用的项目类型之一是功能区类型。这将在标准菜单控件的位置创建一个带有 Microsoft Office 风格功能区控件的壳。
设计模式界面允许您选择代码后置或Prism,除了MVVM 工具包选项。虽然页面选项与 WinUI 相同,但功能界面有一个可以在单独窗口中显示多个视图的选项。服务界面有两个与身份相关的选项:强制登录和可选登录。最后,测试界面有七个不同的选项,而不仅仅是MSTest。提供的附加测试框架包括 NUnit、xUnit 和 Appium(用于 UI 测试),并且有为主项目和核心库项目添加测试项目的选项。
生成的项目结构与 WinUI 项目相同。以下是一个包含空功能区控件的生成 WPF 项目的示例:

图 10.16 – 由 Template Studio 创建的 WPF 项目
让我们通过查看 Uno Platform 扩展提供的 Template Studio 向导来完成。
Template Studio for Uno Platform
Uno Platform (platform.uno/) 是一个使用 WinUI XAML 和 .NET 代码的 UI 框架,它可以创建针对今天几乎每个可用设备平台的程序:Windows、iOS、Android、macOS、Tizen、Web(使用 WebAssembly),甚至 Linux。Visual Studio 的 Uno Platform 扩展(marketplace.visualstudio.com/items?itemName=unoplatform.uno-platform-addin-2022)包括一个基于 Template Studio 代码的新项目向导。
如果您在 Visual Studio 中安装扩展并使用 Uno Platform App 模板创建新项目,从 配置您的项目 屏幕中点击 创建 按钮将启动向导:

图 10.17 – Uno Platform 新项目屏幕
从这里,您可以简单地选择 创建 以接受所有默认设置并生成新解决方案。但是,如果您选择 默认 启动类型中的 自定义 按钮,您将获得完整的向导体验:

图 10.18 – Uno Platform 新项目向导
从这里,您可以从 10 个不同类别的选项中进行选择。尽管向导的样式略有不同,但您可以看到它与 Template Studio 扩展有相同的起源。当我们在 第十三章,使用 Uno Platform 将您的应用程序跨平台化 中构建 Uno Platform 应用程序时,我们将详细介绍向导的每个步骤。如果您使用默认设置创建新的 Uno Platform 项目并运行 UnoApp1.Wasm 项目,您将看到您的应用程序通过利用 WebAssembly 在浏览器中运行:

图 10.19 – 在浏览器中运行 Uno Platform 应用程序
这非常酷!现在让我们总结并回顾本章所学的内容。
摘要
在本章中,我们学习了 Template Studio for WinUI 扩展如何在新开始 WinUI 项目时节省时间并推广良好的模式和习惯。我们通过向导创建了一个新的 WinUI 项目,并探索了新解决方案中生成的代码。理解解决方案组件的结构和目的将使您更容易将其扩展到自己的项目中。我们通过讨论可用于其他 UI 框架(如 WPF 和 Uno Platform)的 Template Studio 扩展来结束讨论。
在下一章中,第十一章,使用 Visual Studio 调试 WinUI 应用程序,我们将探讨 Visual Studio 提供的工具和选项,以使 .NET 和 XAML 开发者的生活更加轻松。
问题
-
在 Template Studio for WinUI 中创建测试项目时使用哪种单元测试框架?
-
Template Studio for WinUI 中哪种项目类型实现了
NavigationView控件? -
Template Studio for UWP 的之前名称是什么?
-
Template Studio for WinUI 在生成项目时使用哪种 MVVM 框架?
-
Visual Studio 扩展使用哪种文件格式?
-
如果您需要将 Linux 作为目标平台之一,可以使用哪个 Template Studio 扩展来创建新项目?
-
哪个文件夹包含由 Template Studio for WinUI 生成的主项目所有接口?
第三部分:在 Windows 上构建和部署以及更多
本部分通过探索调试、构建和部署 WinUI 3 应用程序的技术来完善您的 WinUI 知识。您将了解 Visual Studio 为 WinUI 开发者提供的丰富调试工具。然后,您将看到如何利用 Blazor、Visual Studio Code、GitHub Actions 和 WebView2 控件在 WinUI 应用程序内托管 Web 应用程序。我们还将学习如何将 WinUI 项目迁移到 Uno Platform 以在多个平台上运行,包括 Android 和 WebAssembly。最后,您将了解使用 Visual Studio、Microsoft Store 和 Microsoft 的命令行安装程序 Windows Package Manager (WinGet)构建和部署 WinUI 应用程序的选项。
本部分包含以下章节:
-
第十一章, 使用 Visual Studio 调试 WinUI 应用程序
-
第十二章, 在 WinUI 中托管 Blazor 应用程序
-
第十三章, 使用 Uno Platform 实现应用程序跨平台
-
第十四章, 打包和部署 WinUI 应用程序
第十一章:使用 Visual Studio 调试 WinUI 应用程序
良好的调试技能对于开发者至关重要。虽然.NET 开发者需要了解如何使用包括断点和输出以及立即窗口在内的功能,但 WinUI 项目调试增加了另一套需要掌握的工具和技术。在 UI 层中可能会出现与数据绑定、布局和资源相关的问题。你将学习如何使用实时视觉树和实时属性探索器,以及如何使用 Visual Studio 的XAML 绑定****失败窗口发现数据绑定错误。
在本章中,我们将涵盖以下主题:
-
如何调试 WinUI 应用程序并与 ViewModel 和服务类中的断点一起工作
-
如何通过利用 Visual Studio 中的XAML 绑定失败窗口来调试数据绑定失败,并避免在绑定到集合时遇到常见问题
-
在 Visual Studio 中探索实时视觉树窗口以找到你的 XAML 中的布局问题
-
发现并使用实时属性探索器在运行时获取和设置你的 XAML 元素中的数据
到本章结束时,你将能够舒适地调试 WinUI 开发者在开发应用程序时通常遇到的常见问题。你将能够在构建其他 XAML 框架(如.NET MAUI、WPF 和 UWP)的应用程序时使用这些技能。
技术要求
要跟随本章中的示例,需要以下软件:
-
Windows 10 版本 1809(构建 17763)或更高版本或 Windows 11
-
配置了 Windows App SDK 开发的 Visual Studio 2022 或更高版本
本章的源代码可在以下 GitHub URL 处获取:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/master/Chapter11。
在 Visual Studio 中调试
在本节中,我们将涵盖调试 WinUI 应用程序的几个基本领域。其中一些技术适用于调试其他类型的.NET 应用程序,而其他技术则特定于 XAML 和 WinUI 开发。在整个书中,我们使用 Visual Studio 运行我们的项目,这是一个本地调试会话的例子。我们还将探讨其他调试本地应用程序以及远程应用程序的方法。
在编写 XAML 标记时,简单的错误并不总是显而易见,这种问题也不会被编译器检测到。在本节中,我们将看到如何检测和避免 XAML 标记问题,以及如何遵循最佳实践。
让我们从更仔细地查看调试本地应用程序开始。
调试本地应用程序
从 GitHub 打开Ch11-MyMediaCollection项目并编译它,以确保你已下载所有引用的 NuGet 包。这是我们之前章节中一直在构建的 WinUI 3 项目。运行应用程序以确保一切按预期工作。
注意
如果您有开发和使用 WPF 或 UWP 应用程序的调试经验,您可能熟悉 Visual Studio 中的XAML 设计器。目前 WinUI 3 应用程序没有设计器支持。不清楚这个功能是否会添加,以及何时会添加。GitHub 上关于这个话题有一些讨论:github.com/microsoft/microsoft-ui-xaml/issues/5917。问题中的一个评论建议,您可以通过利用 Visual Studio 中的XAML 热重载来尝试解决这个问题,以便在调试时更改 XAML 文件。虽然这种方法有一些限制(见github.com/microsoft/microsoft-ui-xaml/issues/7043),但大多数简单的 XAML 更改在保存时都会反映在运行的应用程序中。有关 XAML 热重载的更多信息,请参阅 Microsoft Learn 文档:learn.microsoft.com/visualstudio/xaml-tools/xaml-hot-reload。
在我们的第一次演练中,让我们看看如何调试您 PC 上安装的本地应用程序。
调试本地安装的应用程序
我们一直在本书中运行和调试我们的 Visual Studio 解决方案。现在我们将看看您如何调试已安装在 Windows 中的应用程序。
您在阅读本书时已经运行了几个项目。除非您已卸载它们,否则每个项目都应显示为可调试的已安装应用程序包。让我们从以下步骤开始:
- 在 Visual Studio 中,首先选择调试 | 其他调试目标 | 调试已安装应用程序包。将出现调试已安装应用程序包窗口:

图 11.1 – 调试已安装应用程序包窗口
此窗口将显示您 Windows PC 上安装的所有软件包。一些名称您可能很熟悉,例如Microsoft.WindowsTerminal、Fluent XAML Theme Editor或Microsoft Defender。其他名称可能只以应用程序 ID 列出。您可以选择这些其他应用程序进行调试,但没有调试符号,您将无法设置断点或逐步执行代码。让我们找到我们的一个应用程序。
-
如果您已经调试了
MyMediaCollection。MyMediaCollection应用程序将出现在搜索结果中。如果您看不到它,请确保您至少从 Visual Studio 运行了该应用程序一次。这一步是必要的,以便将应用程序打包并部署到 Windows。 -
选择它并点击开始。应用程序将运行,Visual Studio 将开始调试。
如果您不想立即开始调试,您可以选择不启动,但启动应用程序时调试我的代码复选框。现在,当您从开始菜单或任何其他方法启动应用程序时,Visual Studio 将开始调试。
另一种开始调试已安装的本地应用程序的方法是将它附加到正在运行的应用程序:
-
首先,从开始菜单运行应用程序,然后在 Visual Studio 中转到调试 | 附加 到进程。
-
在标题与您要调试的应用程序匹配的
ApplicationFrameHost.exe进程中。这是在 Windows 上托管每个打包的 WinUI 和 UWP 应用程序的过程:

图 11.2 – 附加到正在运行的过程以调试打包的应用程序
- 点击附加并像往常一样开始调试。
这些是启动本地应用程序调试会话的不同方法,但如果你想在另一台机器上调试你的应用程序怎么办?让我们接下来检查那些选项。
调试远程应用程序
您可能有很多原因想在远程机器上调试应用程序。有时您只能在某个设备上重现错误。也许存在仅在特定设备类型或屏幕尺寸上出现的问题。如果您正在处理 UWP 项目,有些设备,如 Xbox,您必须使用远程调试。
注意
在开始任何远程调试会话之前,请确保目标设备已启用开发者模式。有关激活开发者模式的更多信息,您可以阅读这篇 Microsoft Docs 文章:learn.microsoft.com/windows/apps/get-started/enable-your-device-for-development。
要调试远程已安装的应用程序,您将再次使用 Visual Studio 的调试已安装应用程序包窗口:
-
从调试 | 其他调试目标 | 调试已安装 应用程序包打开窗口。
-
将连接类型更改为远程机器:

图 11.3 – 在远程机器上调试应用程序包
-
点击连接类型部分的更改按钮以打开远程 连接窗口。
-
Visual Studio 将尝试发现其他 Windows 设备并将它们列在自动检测部分。如果您看到您想要的设备,请选择它以继续。如果您要调试的设备没有显示,请在手动配置部分的地址字段中输入其 IP 地址,然后点击选择:

图 11.4 – 为调试手动配置远程连接
-
当你返回到之前的窗口时,你可以从所选设备上安装的应用程序包列表中选择要调试的应用程序。
-
点击开始以开始调试,就像我们在上一节中在本地安装的应用程序中做的那样。
这些技术将允许您连接到其他 Windows 机器。对于 UWP 应用程序,这包括其他设备类型,例如 Xbox、HoloLens、Surface Hub 和 Windows IoT 设备。您还可以从项目的 属性 页面的 调试 选项卡访问调试选项和资源。选择 打开调试启动配置文件 UI 以打开 启动 配置文件 窗口:

图 11.5 – Visual Studio 中的启动配置文件窗口
让我们转换一下思路,来检查一些可能导致应用程序 UI 出现渲染问题的常见错误。
常见的 XAML 布局错误
XAML 开发者在编码 UI 时可能会犯许多种错误。其中一些错误不会被编译器捕获。如果您有语法错误或无效的 x:Bind 表达式,这些错误将在编译时失败,但许多其他问题则不会。
我们将要探索的第一个常见 XAML 布局错误的来源是 Grid 控件。
网格布局问题
一些与 Grid 控件相关的错误围绕着其行和列。忘记在子控件上设置 Grid.Row 或 Grid.Column 会导致运行时重叠或遮挡元素。当设置这些值不正确或处理 Grid.RowSpan 和 Grid.ColumnSpan 时,也会出现类似的问题。一个不一定立即显而易见的错误是将 Grid.Row 或 Grid.Column 设置为一个超出定义的行或列数的值。
从包含页面底部按钮控件的 Border 控件中打开 Grid.Row 属性。如果您运行应用程序,您将看到按钮移动到页面顶部并覆盖标题区域中的控件。
现在恢复 Grid.Row 属性,但将值更改为 5。如果这是网格中的最后一行,一切看起来都会很好,即使 Grid 只有三行。由于 5 大于可用的行数,控件被添加到 Grid 的最后一行。然而,因为我们有 InfoBar 在第四行的按钮下方,所以按钮和 InfoBar 发生了重叠。
应用样式时的问题
XAML 的 Style 资源是另一个常见的无意中导致 UI 变化的来源。在 Resource 中创建 Style 时,您应该意识到它将如何应用于该 Resource 范围内的控件。
打开 Page.Resources 部分。这是我们为当前页面创建了三个 Style 元素的地方。每个都有不同的 Target 类型:TextBlock、TextBox 和 ComboBox。然而,它们不会应用于 Page 上这些类型的每个控件,因为我们还给了每个 Style 一个 x:Key。这意味着只有当 Style 属性明确设置为该命名资源时,Style 才会应用于该类型的元素。这些被称为 显式样式:
<TextBlock Text="Name:" Style="{StaticResource
AttributeTitleStyle}"/>
如果您从 Page.Resources 中的 Style 移除 x:Key,那么这种 隐式样式 将应用于 Page 上指定 Target 类型的每个控件,除非这些控件已设置了另一个显式 Style。在一个具有在不同作用域(Application、Page 或控件)中声明的样式的应用程序中,有时很难确定应用于控件的样式。我们将在本章后面讨论 Visual Studio 的 实时属性浏览器 窗口时看到如何做到这一点。让隐式样式始终继承自显式样式是一种最佳实践。这使开发者能够从默认样式继承,并减少元素间重复的隐式样式属性。
接下来,我们将查看一个第三方扩展,它可以通过 静态 代码分析来帮助找到常见的 XAML 问题。
通过静态代码分析改进您的 XAML
有一个免费的、开源的 Visual Studio 扩展,它提供了对 XAML 文件进行静态代码分析的支持。Rapid XAML Toolkit (rapidxaml.dev/) 提供了针对常见问题的 XAML 分析器和代码修复,并支持添加您自己的自定义 XAML 分析器。
注意
在撰写本文时,Visual Studio 2022 的 Rapid XAML 工具包尚未发布。它正在开发中,并应在您阅读本书时可用。当前示例说明了如何使用目前适用于 Visual Studio 2019 的扩展。
让我们在 Visual Studio 中安装这个工具,看看它能在 WinUI 项目中识别出哪些问题:
-
在 Visual Studio 中,转到 扩展 | 管理扩展。
-
rapid xaml:

图 11.6 – 安装 Rapid XAML 工具包
-
点击 下载 将扩展排队以进行安装。下载完成后,您可以关闭 管理扩展 窗口,重新启动 Visual Studio 以完成安装。
-
现在,当您在 Visual Studio 中查看 错误列表 窗口时,会有一些来自 Rapid XAML 分析器的警告:

图 11.7 – 查看 Rapid XAML 工具包的新警告
- 打开 ItemDetailPage.xaml,将光标移至由代码分析器创建的绿色波浪线之一,然后点击灯泡图标:

图 11.8 – 查看代码分析器警告的快速修复
或者,您也可以在 XAML 上右键单击,选择 Rapid XAML | 将硬编码字符串移动到资源文件,以修复此类警告的所有问题。
这是工具包提供的其他分析器的列表:
-
Grid.Row值没有对应的RowDefinition -
Grid.Column值没有对应的ColumnDefinition -
Grid.RowSpan值没有对应的ColumnDefinition -
Grid.ColumnSpan值没有对应的ColumnDefinition -
TextBox没有指定InputScope -
SelectedItem绑定可能应该是TwoWay -
RXT200:应该是资源的硬编码字符串值
-
Entry没有指定Keyboard -
指定了
MaxLength -
Image缺乏无障碍考虑 -
ImageButton缺乏无障碍考虑 -
CheckBox的Checked和Unchecked事件 -
使用
MediaPlayerElement替代MediaElement -
x:Uid应该以大写字母开头 -
Name应该以大写字母开头 -
RXT999:未知错误 – 解析 XAML 文档时出错
Rapid XAML Toolkit 除了分析器和代码修复之外还有许多其他功能,并且经常添加新功能和分析器。要查看即将推出功能和修复的列表,您可以查看 GitHub 上的问题:github.com/mrlacey/Rapid-XAML-Toolkit/issues。
注意
像这样的社区驱动项目始终在寻找贡献者。这是开始参与开源社区的一个好方法。
另一个可能成为开发者烦恼来源的相关主题是调试数据绑定。让我们看看在下节中如何避免一些常见的陷阱。
精确定位数据绑定失败
虽然在 WinUI 和 UWP 中调试数据绑定问题不像在 WPF 中(如果你使用 x:Bind 编译的绑定)那么困难,但仍有一些需要注意的陷阱。在本节中,我们将探讨在视图和 ViewModels 中可能发生的问题,以及如何诊断和修复这些问题。
数据绑定中的常见错误
如果你使用 x:Bind,编译器将评估你是否绑定到有效的源,并可以让你放心地知道你的视图和 ViewModels 是否正确连接,但仍有许多可能出错的地方。让我们回顾一些最常见错误。
选择最佳绑定模式
我们在前面章节中看到,大多数带有 x:Bind 的控件默认模式是 OneTime,而 Binding 的默认模式是 OneWay。默认为 OneTime 有助于性能,因为许多只读属性仅在视图首次创建时设置。然而,如果你忘记更改绑定到用户与页面交互时数据会变化的控件,你可能不会立即意识到为什么 UI 中的数据没有更新。
当你绑定需要数据双向流动的控件时,请记住将 Mode 设置为 TwoWay。我们曾在 ComboBox.SelectedItem 属性中使用它来按媒体类型筛选集合。
触发 PropertyChanged 通知
通过绑定到一个正确实现所有公共属性INotifyPropertyChanged的 ViewModel,与PropertyChanged相关的问题并不常见。使用 MVVM Toolkit 可以使这个过程更加简单。如果 ViewModel 代码更新了这些属性之外的一个属性值,仍然可能会出现问题。这将更新属性值而不通知视图。如果你使用 MVVM Toolkit,它将在生成隐藏的派生类代码时指出这个问题。你可以通过始终使用public属性来更新值来避免这种情况。如果没有直接更新属性的好理由,那么在更新值后应该手动触发该属性的PropertyChanged事件。使用 C#中的nameof方法是最佳实践,以确保你使用存在的属性名称。拼写错误的属性名称将在编译时被捕获并在编辑器中突出显示。你还可以在.NET 中使用CallerMemberNameAttribute:learn.microsoft.com/dotnet/api/system.runtime.compilerservices.callermembernameattribute。如果你的应用程序尝试为一个不存在的属性触发属性更改通知,将引发异常。
使用ObservableCollection<T>
ObservableCollection<T>扮演着重要的角色。如果使用正确,视图中的列表将与集合保持同步。在使用可观察集合时有一些做法需要避免。
不要替换可观察集合的整个值。绑定到属性的视图元素仍然绑定到原始集合。对集合的任何后续更改都不会反映在视图中。虽然你可以通过触发PropertyChanged通知来解决这个问题,但这在大集合中可能会有性能影响。这也可能对用户造成冲击,因为大多数控件都会将当前视图重置为列表的开头。将使用ObservableCollection<T>的 ViewModel 属性设置为只读,以避免意外重置整个集合。唯一的例外是如果你知道集合将被完全重新填充。在列表中逐个删除和重新添加大量项目可能会导致用户体验不佳。
不要使用 LINQ 修改可观察集合。LINQ 表达式不是通过在可观察集合上调用Add和Remove方法来操作的。它们甚至不返回ObservableCollection<T>。如果你使用 LINQ 并将结果转换回ObservableCollection<T>,你将回到替换整个集合,这正是刚才讨论过的。
这里是一个使用 LINQ 的示例。这将导致视图停止接收CollectionChanged通知,因为 LINQ 不返回ObservableCollection:
_entryList = from entry in _entryList
where entry.Lastname.StartsWith("J")
Lastname starting with J. By using the Remove method and not changing the entire collection, the CollectionChanged events are preserved:
for (int i = _entryList.Count - 1; i >= 0; i--)
{
Entry entry = _entryList[i];
如果 (!entry.Lastname.StartsWith("J"))
{
_entryList.Remove(entry);
}
}
Finally, it’s best not to use observable collections for lists that do not change. The extra overhead they bring is not needed for static data. Bind to an `IList<T>`, `IEnumerable<T>`, or better yet, a traditional array of items instead. Check out this *C# 101* video to learn when to leverage arrays, lists, and collections in .NET: [`learn.microsoft.com/shows/csharp-101/csharp-arrays-list-and-collections`](https://learn.microsoft.com/shows/csharp-101/csharp-arrays-list-and-collections).
Using the XAML Binding Failures window
Visual Studio 2019 version 16.7 introduced the new `Binding`, not `x:Bind`, as those issues are checked and caught by the compiler. Clicking the red indicator will take you to the **XAML Binding Failures** window while debugging. Binding failures were always available in the **Output** window while debugging, but they could be difficult to find. The new, dedicated window provides sorting and searching. Similar failures are also grouped to make it easy to address related items together:

Figure 11.9 – Checking the in-app toolbar for binding failures
Let’s give it a try. Open the **XamlDebugging** solution from the GitHub repository for this chapter and follow along with these steps:
1. Open `Window` contains a `StackPanel` with two `TextBox` child elements:
```
<StackPanel DataContext="Test" Background="LightGray">
<TextBox x:Name="Text1"
Text="{Binding Path=SomeText,
Mode=TwoWay}"/>
<TextBox Text="{Binding ElementName=Text1,
Path=Text, Mode=OneWay}"/>
</StackPanel>
```cs
`DataContext` does not exist, so what do you think will be reported in the binding failures? Let’s find out!
2. Run the application and look at the in-app toolbar. There is an indication that we have one binding failure.
3. Click the red indicator on the toolbar to open the **XAML Binding Failures** window in Visual Studio:

Figure 11.10 – The XAML Binding Failures window
The **Description** text in the window tells us **BindingExpression path error: ‘SomeText’ property not found on ‘Windows.Foundation.IReference`1<String>’**. This is useful information. It would be more useful if there was something telling us directly that the data context itself is not valid. However, if you had a dozen controls using this data context, and all the bindings were failing, it would become clear that there is a problem with the data context and not the individual bindings.
1. Let’s make sure the other binding is working correctly. Enter some text into the first `TextBox`:

Figure 11.11 – Binding TextBoxes with ElementName
The text you enter should be duplicated in the second `TextBox` as your type. Our binding to `ElementName` is working as expected.
I will leave fixing the data source as an exercise for you. Add a valid data source to the code behind it and see whether the binding error is cleared up.
Now, let’s explore a few other XAML debugging tools available from the in-app toolbar.
Debugging live data with Live Visual Tree and Live Property Explorer
While the **XAML Binding Failures** window is new to Visual Studio, the in-app toolbar has been available to XAML developers since 2015\. The toolbar floats over the active window in your application while you are debugging. There are several parts to the toolbar:
* **Go to Live Visual Tree**: Opens the **Live Visual** **Tree** window.
* **Show in XAML Live Preview**: This opens the **XAML Live Preview** window in Visual Studio.
* **Select Element**: Allows you to select an element in **Live Visual Tree** by clicking on it in the active window.
* **Display Layout Adorners**: This will highlight the element in the UI that is currently selected in **Live** **Visual Tree**.
* **Track Focused Element**: While the **Live Visual Tree** window is open, toggling this on will indicate in **Live Visual Tree** which element currently has focus on the UI.
* **Binding failures**: Indicates the number of current binding failures and opens the **XAML Binding Failures** window when clicked.
* **Scan for Accessibility Issues**: This opens the **Accessibility Checker** window and runs an accessibility check on the UI elements currently visible.
* **Hot Reload**: Indicates whether **XAML Hot Reload** is currently available. Clicking it will open the documentation on Microsoft Learn.
Let’s explore a few of the most commonly used debugging tools. If you wish to get more information on the remaining tools, you can visit this page about XAML tools on Microsoft Learn: [`learn.microsoft.com/visualstudio/xaml-tools/`](https://learn.microsoft.com/visualstudio/xaml-tools/). Or, view this *Visual Studio Toolbox* episode about using the XAML tools in Visual Studio: [`learn.microsoft.com/shows/visual-studio-toolbox/new-xaml-features-in-visual-studio`](https://learn.microsoft.com/shows/visual-studio-toolbox/new-xaml-features-in-visual-studio). We’ll start with XAML Hot Reload.
Coding with XAML Hot Reload
XAML Hot Reload is a simple but powerful feature. Before Visual Studio 16.2, it was known as **XAML C# Edit & Continue**. Hot reload has been available to web developers for a while longer, and the name change for XAML helps to clear up some confusion for developers familiar with the concept in web development. The idea of hot reload is that you can make changes to your UI and the changes are reflected in the running application without having to stop debugging and recompile. Let’s try it with the **XamlDebugging** solution:
1. Start by running the application again. Make sure that the **Hot Reload** indicator in the toolbar shows that it is enabled. It should only be disabled if you are running an unsupported project type.
2. Now let’s make a change to `Background` color of `StackPanel` to `LightGray`:
```
<StackPanel DataContext="Test"
Background="LightGray">
<TextBox x:Name="Text1"
Text="{Binding Path=SomeText,
Mode=TwoWay}"/>
<TextBox Text="{Binding ElementName=Text1,
Path=Text, Mode=OneWay}"/>
</StackPanel>
```cs
3. Save the file and look at the running application:

Figure 11.12 – Changing the UI without restarting the application
The background of the window is now light gray.
This is a huge time-saver when building a new UI. For a list of known limitations, see this Microsoft Learn article: [`learn.microsoft.com/troubleshoot/developer/visualstudio/tools-utilities/xaml-hot-reload-troubleshooting#known-limitations`](https://learn.microsoft.com/troubleshoot/developer/visualstudio/tools-utilities/xaml-hot-reload-troubleshooting#known-limitations).
Now let’s look at another powerful debugging tool, the **Live Visual** **Tree** window.
Debugging with Live Visual Tree and Live Property Explorer
The **Live Visual Tree** window allows developers to explore the elements in the current window of an application’s XAML **visual tree**. It is available to UWP and WinUI applications and other XAML frameworks. Let’s step through using **Live Visual Tree** and related XAML debugging tools:
1. While debugging the **XAMLDebugging** project, open the **Live Visual Tree** window. The visual tree contains the hierarchy of controls in the window with the **Show Just My XAML** button selected by default in the **Live Visual Tree** window’s toolbar:

Figure 11.13 – Viewing Live Visual Tree for the XamlDebugging project (Show Just My XAML selected)
1. If you want to view the entire visual tree for your window, de-select **Show Just My XAML** on the toolbar and the tree will refresh:

Figure 11.14 – Viewing Live Visual Tree for the XamlDebugging project (all XAML)
There’s a lot more going on here. With `TextBox` is made up of 17 child elements with a `Grid` at the root of its template. This is a great way to learn how the controls we use are composed. Working with and modifying these templates is beyond the scope of this chapter, but I encourage you to explore them on your own. *Chapter 7*, *Fluent Design System for Windows Applications*, has additional information on using default styles and theme resources. This Microsoft Learn article on control templates is another great place to start: [`learn.microsoft.com/windows/apps/design/style/xaml-control-templates`](https://learn.microsoft.com/windows/apps/design/style/xaml-control-templates). For now, let’s switch back to the **Show Just My** **XAML** view.
1. From **Live Visual Tree**, right-click on a node to navigate to the XAML markup for the selected item by selecting **View Source**.
2. Next, right-click a node and select **Show Properties** to show the **Live Property Explorer** window, where you can inspect the current properties of the selected element. You can see how the properties are grouped based on where they have been set. Here, some are set by **Local** changes and others are set based on the **Style** for a **TextBox**. If you had multiple levels of inherited explicit styles and an implicit style, those would all be grouped here:

Figure 11.15 – Viewing the Live Property Explorer window for the Text1 TextBox
1. Look at the `Binding` markup extension. Expand the **Text** property node to view the details of the binding:

Figure 11.16 – View the binding details for the Text property of Text1
There is some very helpful information here. We can see the **Path** and **Mode** that were set in the XAML, the ones we did not set, and yet others that are read-only. **IsBindingValid** is **False** in this project, but it may be **True** if you fixed your data context from the previous section. When the binding is valid, **EvaluatedValue** will contain the current value for the **Text** property.
1. Select the second `TextBox` element in the tree and view the **Live Property Explorer** window. You can see that the default value for **Background** is **SolidColorBrush**.
2. Expand the **Background** node to view the resource details:

Figure 11.17 – Viewing the property details of Page.Background
1. Try changing the **Color** property to white and see what happens in the XAML code and to the running application. The UI in the app should update, but the XAML file will remain unchanged.
2. Next, try adding markup to `MainWindow` to set the `MaxWidth` property of the second `TextBox` to `100`. What happens in `MaxWidth` when you’re finished.
3. Returning to `Grid` is more complex than `StackPanel`. `StackPanel` is preferred for layout if you are simply laying out a few elements horizontally or vertically. This unidirectional layout is what `StackPanel` was built to handle. As a rule of thumb, you should use the right control for the job and always keep performance in mind.
4. Now select the active node from the tree in your XAML file and enable **Select Element in the Running Application** from the toolbar. The **Track Focused Element** button on the toolbar will highlight the current tree node in the application’s window at runtime. This option is also available from the in-app toolbar.
5. To toggle the in-app toolbar on/off, you can use the **Enable in-app Toolbar** button on the **Live Visual** **Tree** toolbar.
I encourage you to spend some time in these windows the next time you are debugging your application. Use them to help find issues with binding, resources, or custom control templates.
Now let’s wrap up the chapter and review what we’ve learned.
Summary
In this chapter, we have covered some essential tools and techniques for debugging your XAML applications. We learned how to debug installed applications on a local or remote PC. If you are developing WPF or UWP applications, tools such as **Live Visual Tree**, **Live Property Explorer**, and **Rapid XAML Toolkit** will all work for those projects as well. Leveraging these tools will shorten the time you spend debugging and help you deliver higher-quality software.
In the next chapter, we will explore the `WebView2` browser control. You will learn how to use `WebView2` to embed an ASP.NET Core Blazor **single-page application** (**SPA**) inside a WinUI 3 desktop application.
Questions
1. How can you debug a UWP application that is running on an Xbox?
2. How can you debug an application that is currently running on your machine?
3. How can you launch an application package installed on a remote machine for debugging?
4. What feature in Visual Studio allows you to change XAML properties at runtime and see them reflected in the running application?
5. Which window will show a hierarchy of the elements in the current window?
6. What is the default binding mode for most control properties with `x:Bind`?
7. Where can you view the runtime properties for the control currently selected in **Live** **Visual Tree**?
第十二章:在 WinUI 中托管 Blazor 应用程序
WinUI 3 中的 WebView2 控件,Windows 开发者可以在他们的 WinUI 客户端应用程序中运行云托管的 Blazor 应用程序。这些选项在 .NET 8 中随着 渲染模式 的引入有所变化。我们将讨论这些新模式及其各自的优点。
在本章中,我们将涵盖以下主题:
-
学习使用 ASP.NET Core 和 Blazor 进行客户端 .NET 开发的一些基础知识
-
使用 Visual Studio Code 和 .NET 命令行界面(CLI)创建新的 Blazor 应用程序
-
将 Blazor 应用程序部署到 Azure 静态 Web 应用 服务
-
创建一个 WinUI 应用程序以在
WebView2浏览器控件中托管 Blazor 应用程序
到本章结束时,您将了解如何创建新的 Blazor 应用程序,将其部署到云端,并使用该应用程序作为 WebView2 控件。
技术要求
要跟随本章中的示例,需要以下软件:
-
Windows 10 版本 1809(构建 17763)或更高版本或 Windows 11。
-
Visual Studio 2022 或更高版本,并已为 Windows App SDK 开发配置了 .NET 桌面开发工作负载。
-
Visual Studio Code(VS Code)以及以下扩展:C# 开发工具包和 Microsoft Edge 调试器。
-
Windows Terminal(使用 WinUI 构建)或您首选的命令行工具。您还可以使用 VS Code 中的 终端 窗口。
-
要创建 Blazor 项目,请安装 .NET 7 SDK 或更高版本。
本章的源代码可在 GitHub 上通过此 URL 获取:github.com/PacktPublishing/-Learn-WinUI-3/tree/master/Chapter12。
开始使用 ASP.NET Core 和 Blazor
Blazor 是一个 Web 开发框架,为 C# 开发者提供了在构建客户端 Web 应用程序时替代 JavaScript 的选择。Blazor 是 ASP.NET Core 的一部分,并首次在 ASP.NET Core 3.0 中引入。让我们先简要了解 ASP.NET 和 ASP.NET Core 的历史。
ASP.NET 和 ASP.NET Core 的简要历史
ASP.NET 是微软基于 .NET 的 Web 开发框架,首次于 2002 年发布。ASP.NET 的早期版本使用了一种名为 Web Forms 的客户端开发模型,旨在作为 Windows Forms(WinForms)客户端应用程序的 Web 等价物。Web Forms 在 .NET Web 开发者中很受欢迎,但并未遵循许多 Web 开发最佳实践和模式。许多开发者对每次服务器请求和响应中通过网络发送的大量 ViewState 数据表示批评。
作为对 Web Forms 批评的回应,ASP.NET 团队在 2009 年发布了 ASP.NET MVC。使用 ASP.NET MVC 构建的网络应用遵循 模型-视图-控制器(MVC)模式。这个新框架受到了 .NET 社区的欢迎,并且至今仍然是网络开发者们的热门选择。ASP.NET 也是最早被微软发布为开源的框架之一。2012 年,ASP.NET MVC 4 在 Apache License 2.0 许可下作为开源发布。
随着 .NET 团队继续拥抱开源软件,他们还决定以一个新的、开源的、跨平台的 .NET 版本开始新的起点,这个版本被称为 .NET Core。微软在 2016 年发布了 .NET Core 1.0,提供了适用于 Windows、macOS 和 Linux 的运行时。随着 .NET Core 的发布,出现了一个新的网络框架,称为 ASP.NET Core。ASP.NET Core 1.0 包含了用于构建网络应用和 Web API 项目的项目模板。这些网络应用使用 MVC 模式、用于构建丰富 UI 的 Razor 语法和 CSS 进行页面样式设计。
在接下来的几年里,ASP.NET 团队继续向 ASP.NET Core 添加更多功能,包括以下内容:
-
Razor Pages:Razor Pages 项目是在 ASP.NET Core 中引入的,提供了一个简单的 ASP.NET Core MVC 的替代方案。
-
SignalR:一个用于实时网络通信的框架;SignalR 是 Blazor 服务器应用中客户端-服务器通信的关键部分。
-
身份(之前称为 Identity Core):支持 ASP.NET Core 应用中的登录功能,并管理用户、密码、角色、令牌等认证资源。
注意
本书不会提供 ASP.NET Core 开发的详细教程。如果你想要了解更多关于使用 ASP.NET Core 构建网络应用的知识,请参阅 Andreas Helland, Vincent Maverick Durano, Jeffrey Chilberto 和 Ed Price 著,Packt 出版 的 《ASP.NET Core 5 入门》 (www.packtpub.com/product/asp-net-core-5-for-beginners/9781800567184)。
但 Blazor 在 ASP.NET Core 开发图中处于什么位置?让我们接下来探索这个问题。
什么是 Blazor?
Blazor 是一个用于使用 .NET 和 C# 构建网络应用的框架。在 .NET 8 之前,开发者在新建 Blazor 项目时可以从以下三种托管模型中选择:
-
Blazor 服务器:随着 ASP.NET Core 3.0 的发布而引入,服务器模型在服务器上执行应用逻辑,并通过 SignalR 连接将 UI 更新推送到客户端。
-
Blazor WebAssembly:在 ASP.NET Core 3.2 中推出,这种执行模型仅在客户端运行,沙盒化并在浏览器的 UI 线程上运行,通过 Wasm。
-
Blazor 混合模式:这是一种结合了 Web 和原生客户端技术的较新类型的 Blazor 应用程序。在这个模型中,Razor 组件在客户端的 .NET 上运行,并通过互操作性和 WebView 技术将 Web 用户界面渲染到原生客户端,对用户来说无缝衔接。混合应用程序可以与 .NET MAUI、WPF 和 WinForms 客户端一起使用。
在 .NET 8 及以后的版本中,Blazor 团队将这些托管模型发展成了渲染模式。现在,当使用 Blazor 进行开发时,您可以选择以下三种模式:
-
服务器模式:与之前版本中服务器托管模型等效。
-
WebAssembly 模式:利用 Wasm 的客户端模式。
-
自动模式:新的自动模式结合了前两种模式的优势。如果客户端运行时组件可以快速下载,它将在 WebAssembly 模式下运行。否则,它将回退到服务器模式。
那么,您应该为您的下一个 Blazor 应用程序选择哪种模式呢?幸运的是,随着 .NET 8 的推出,您不再需要为整个应用程序选择相同的模式。可以通过设置 @rendermode 属性在组件级别选择模式。这个组件级别的决策将取决于您项目的需求,但许多应用程序可能会开始利用自动模式。
注意
在撰写本文时,.NET 8 仍然仅作为开发者预览版提供。我们将使用 .NET 7 和 Blazor WebAssembly 托管模型在本章中构建应用程序。您将能够使用 .NET 8 和 WebAssembly 模式构建相同类型的应用程序。
这里列出了服务器和 Wasm 托管模型的一些优缺点:

图 12.1 – Blazor 托管模型优缺点
Blazor 服务器托管模型首先发布,拥有最成熟的工具和调试支持。如果您计划在支持 ASP.NET Core 的服务上托管服务器,并且您的用户可能正在使用不支持 Wasm 的浏览器,那么这是一个很好的选择。
在本章中,我们将重点关注客户端 Blazor 应用程序。那么,为什么选择这种模式,它是如何工作的呢?
WebAssembly 和客户端 .NET 开发
使用 Wasm 的客户端托管模型的主要优势是可选的无服务器部署和客户端离线工作的能力。离线支持意味着您的 Blazor 应用程序可以被配置为渐进式 Web 应用程序(PWA),并下载到 PC、平板电脑和手机上。您可以从 Mozilla 的开发者文档中了解更多关于 PWAs 的信息:developer.mozilla.org/en-US/docs/Web/Progressive_web_apps。
Blazor 客户端应用程序可以作为 PWA 运行的原因,也是我们想在 WinUI 应用程序中的 WebView2 控件中使用它的原因。一旦网络应用程序在浏览器宿主中加载,所有内存执行和交互都可以发生,无论网络连接是否中断。如果你的项目中不关心连接性、可扩展性和服务器托管,那么 Blazor 服务器模型当然可以使用。
简单的 Blazor Wasm 应用程序可以作为 静态资源 在 Web 服务器上托管。你还可以在 ASP.NET Core Web 托管解决方案上托管 Blazor Wasm 应用程序。这样做可以让你在服务器上与其他 Web 解决方案共享代码,并支持更高级的路由场景,以支持应用程序内的深度链接。使用 .NET 8,使用具有 ASP.NET Core 支持的主机可以让你利用新的自动模式,或者选择在某些适合离线使用的组件中仅使用 Wasm 模式。
在 Wasm 模型中,当客户端向服务器发出第一个请求时,整个应用程序和 .NET 运行时都会在响应中发送到浏览器,并且整个应用程序在客户端端运行。此模式中没有共享的服务器端代码。然后,运行时和应用程序在 UI 线程上加载到 Wasm 之上:

图 12.2 – Blazor Wasm 模型在浏览器中运行
现在你已经对 ASP.NET Core 和 Blazor 应用程序有了些许了解,让我们创建一个 Blazor Wasm 项目,并亲身体验一下这个框架。
创建 Blazor Wasm 应用程序
是时候开始构建我们将在 WinUI 应用程序中运行的 Blazor 应用程序了。我们将使用 .NET CLI 和 VS Code 来创建 Blazor 项目。如果你更喜欢全功能的 IDE 体验,也可以使用 Visual Studio 2022:
- 首先,使用你选择的终端应用程序打开命令提示符。我将使用带有 PowerShell 7.3 的 Windows Terminal(https://apps.microsoft.com/store/detail/windows-terminal/9N0DX20HK701):

图 12.3 – 在 Windows Terminal 中运行 PowerShell 7.3
-
使用终端将当前文件夹更改为你保存项目的地方。我的位置将是
C:\Users\alash\source\repos。 -
使用以下命令创建一个名为
BlazorTasks的新 Blazor WebAssembly 项目,并按 Enter:dotnet new blazorwasm -o BlazorTasks。.NET CLI 将创建新项目,你应该会看到一个消息表明它已成功完成:

图 12.4 – .NET CLI 成功创建 Blazor WebAssembly 应用程序项目
- 导航到 .NET 刚创建的
BlazorTasks文件夹。如果你已安装 VS Code (code.visualstudio.com/),你可以在命令行中输入code .以在 VS Code 中打开当前文件夹:

图 12.5 – VS Code 中的 BlazorTasks 项目
你可能会在 输出 窗口中看到一些活动,因为 VS Code 正在下载与项目相关的调试和编辑工具。如果你看到一个 你信任此文件夹中文件的作者吗? 对话框,请选择 是的,我信任作者 按钮,以继续。
-
切换到 VS Code 中的 终端 窗口。如果窗口在编辑器的底部不可见,你可以从菜单中选择 终端 | 新建终端。
-
在终端中输入
dotnet run。你还可以使用 F5 在 VS Code 中运行,就像你在 Visual Studio 中习惯使用的那样。当编译完成后,你可以在浏览器中导航到https://localhost:5240/来查看正在运行的BlazorTasks应用程序(端口号将在 终端 窗口中显示):

图 12.6 – 首次运行 BlazorTasks 项目
默认项目模板在左侧面板中有三个导航选项:主页、计数器 和 获取数据。当你从一个页面导航到另一个页面时,所有执行逻辑都在浏览器中运行。没有往返到 ASP.NET Core 服务器实例。
- 你可以通过按 F12 在浏览器中打开开发者工具。你将看到,在导航到应用程序中的 计数器 选项卡并多次点击 点击我 按钮时,开发者工具的 网络 选项卡上没有活动:

图 12.7 – 在 BlazorTasks 应用程序中查看网络活动
- 最后,当你完成对应用程序的探索后,你可以关闭浏览器,并在 VS Code 的 终端 窗口中按 Ctrl + C 来停止调试应用程序。
现在我们已经创建并测试了项目,让我们开始为应用程序编写一个新的任务页面。
构建一个简单的应用程序以跟踪任务
在本节中,我们将为应用程序创建一个新的任务页面,该页面将出现在左侧导航中的 获取数据 项下方。如果你愿意,你可以从项目中删除其他组件。我将保留它们以测试在 WinUI 中部署的应用程序中的导航:
-
首先向项目中添加一个
Tasks组件。通过在 VS Code 的Pages文件夹中的TasksRazor 组件中输入dotnet new razorcomponent -n Tasks -o Pages来完成此操作。 -
双击 资源管理器 窗口中的
Pages文件夹中的Tasks.razor以在编辑器中打开它。作为对资源管理器窗口的替代,C# Dev Kit 扩展应该已经将 解决方案资源管理器 视图添加到左侧窗格的底部。如果您喜欢,请使用此视图。该文件包含以下代码:<h3>Tasks</h3> @code { }Razor 文件包含 HTML 标记和 C# 代码的组合,HTML 在文件顶部,C# 代码在文件底部的
@code块中。我们将随着我们的进展看到这两个部分如何交互。 -
将
@page "/tasks"作为Tasks.razor文件的第一行。这将允许应用程序通过 URL 上的/tasks路由到该页面。 -
在我们添加页面内容之前,让我们为它添加一个新的导航项。从 资源管理器 中的
Shared文件夹打开NavMenu.razor。 -
在
<nav>元素内部,在关闭</nav>标签之前添加一个新的<div>:<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu"> <nav class="flex-column"> ... <div class="nav-item px-3"> <NavLink class="nav-link" href="tasks"> <span class="oi oi-list-rich" aria- hidden="true"></span> Tasks </NavLink> </div> </nav> </div> -
使用
dotnet run运行应用程序,以确保新的菜单选项出现,并且您可以通过带有 Tasks 标题的新页面进行导航:

图 12.8 – 导航到新的任务页面
-
接下来,使用
TaskItem.cs。这将作为任务的模型类。向新文件添加以下代码:namespace BlazorTasks { public class TaskItem { public string? Name { get; set; } public bool IsComplete { get; set; } } } -
打开
Tasks.razor并添加以下代码,通过迭代@code块中包含的任务列表来创建一个任务的无序列表:@page "/tasks" <h3>Tasks</h3> <ul> @foreach (var task in taskList) { <li>@task.Name</li> } </ul> <input placeholder="Enter new task..." /> <button>Add task</button> @code { private IList<TaskItem> taskList = new List<TaskItem>(); }注意到 Razor 文件如何允许您混合 C# 代码和 HTML 标记。我们在
<ul>中有一个 C#foreach循环,并在foreach循环内部添加<li>元素,这些元素再次包含获取每个task.Name的 C# 代码。这是强大功能。我们还添加了一个输入字段来输入新任务和一个按钮来添加任务。我们将在下一个步骤添加一些代码来使button功能化。 -
在
@code块中添加一个名为AddTask的新私有变量和新方法。此方法将向taskList集合添加一个新任务:private string newTask; private void AddTask() { if (!string.IsNullOrWhiteSpace(newTask)) { taskList.Add(new TaskItem { Name = newTask }); newTask = string.Empty; } } -
最后,向页面上的
input和button元素添加一些数据绑定代码。input将绑定到newTask变量,而button的onclick事件将触发AddTask方法来运行:<input placeholder="Enter new task..." @bind="newTask" /> <button @onclick="AddTask">Add task</button> -
现在,运行应用程序并测试控件。您应该能够将一些任务添加到列表中:

图 12.9 – 在 BlazorTasks 中向任务列表添加一些任务
这效果很好,但现在我们有一些任务要做,我们没有标记它们为完成的方法。让我们在下一步处理这个问题。
-
第一步是使每个列表项成为用户可以勾选的
checkbox,以便在完成任务时进行检查。我们还将task.Name绑定到一个input字段,以便用户可以编辑每个任务的名称:<ul> @foreach (var task in taskList) { <li> <input type="checkbox" @bind="task.IsComplete" /> <input @bind="task.Name" /> </li> } </ul> -
接下来,以防列表过长,让我们使用一些数据绑定来显示作为页面标题的一部分的不完整任务数量:
<h3>Tasks - (@taskList.Count(task => !task.IsComplete)) Incomplete</h3> -
再次运行应用程序,并开始处理您的任务列表:

图 12.10 – 在 BlazorTasks 应用程序中添加和完成任务
你可能已经注意到,任务在会话之间不会保存。目前taskList是一个内存中的集合。要跨会话持久化它,你需要添加服务调用以将数据保存到服务器端数据存储中。创建此服务超出了本章的范围,我将把它留作你的练习。
注意
如果你更喜欢使用功能齐全的 IDE,所有这些步骤也可以在 Visual Studio 或 Visual Studio for Mac 中完成。Microsoft Learn 文档页面提供了如何在两种工具以及 VS Code 中调试 Blazor Wasm 应用程序的信息:https://learn.microsoft.com/aspnet/core/blazor/debug。
现在我们已经有一个功能齐全的任务跟踪 Web 客户端,我们可以继续下一步。是时候将我们的 Blazor 应用程序部署到云中了。
探索 Blazor Wasm 部署选项
在开发解决方案时,在本地运行和调试 Blazor 项目是很好的,但当是时候将你的应用程序与全世界分享时,我们需要将其托管在云中。对于典型的 ASP.NET Core 应用程序,有众多云托管选项,Blazor Wasm 应用程序更是如此。完全在客户端运行的网站可以作为静态文件托管在服务器上,这意味着服务器在收到请求时只需简单地提供文件。不需要服务器端执行。
让我们先回顾一下 Blazor WebAssembly 部署可用的托管选项。
Blazor Wasm 项目的部署选项
我们的项目有几个托管选项。今天我们将讨论一些最受欢迎的解决方案:GitHub Pages、Azure App Service、Azure Static Web Apps以及Amazon Web Services(AWS)上的两个选项。关于托管在 ASP.NET Core 或作为静态文件深入探索选项,Microsoft Learn 有一篇很好的文章:https://learn.microsoft.com/aspnet/core/blazor/host-and-deploy/webassembly。
亚马逊云服务
使用 AWS,Blazor Wasm 网站可以使用 ASP.NET Core 在弹性容器服务(ECS)(aws.amazon.com/ecs/)和Fargate上托管。ECS 解决方案使用Docker创建要托管在云中的容器。然后网站通过 Fargate(aws.amazon.com/fargate/),AWS 的容器计算引擎,进行服务。要了解更多关于此 ASP.NET Core 项目解决方案的信息,AWS 博客有一篇很好的文章详细说明了步骤:aws.amazon.com/blogs/compute/hosting-asp-net-core-applications-in-amazon-ecs-using-aws-fargate/。
对于 AWS 的静态托管选项,可以使用 wwwroot 文件夹将其复制到 S3 存储中,CloudFront 负责从 S3 存储桶中提供静态文件。本文详细介绍了如何在 AWS 中创建和部署 Blazor Wasm 应用程序:aws.amazon.com/blogs/developer/run-blazor-based-net-web-applications-on-aws-serverless/。
现在,让我们看看如何通过 GitHub 提供静态文件。
GitHub Pages
GitHub Pages (pages.github.com/) 是直接从 GitHub 仓库提供静态网站的托管服务。您可以在 GitHub 上维护您的站点,并配置 GitHub Actions 将站点部署到 GitHub Pages。Microsoft MVP Niels Swimburghe 在他的个人博客上提供了将 Blazor Wasm 项目部署到 GitHub Pages 的分步指南:https://swimburger.net/blog/dotnet/how-to-deploy-aspnet-blazor-webassembly-to-github-pages。GitHub Pages 是免费的,但 标准 用户账户只能托管来自 默认 GitHub 分支的页面。
在下一节中,我们将使用 GitHub Actions 与我们的项目一起部署到 Azure。但在此之前,让我们回顾一下可用的两个 Azure 托管解决方案。
Azure App Service
Azure App Service (https://azure.microsoft.com/products/app-service/) 是如果您希望将 Blazor 应用程序托管在 ASP.NET Core Web 服务器上时的一个很好的选择。App Service 提供了 Windows 和 Linux 服务器,但目前仅支持 Windows 实例用于 Blazor WebAssembly 应用程序。Microsoft Learn 提供了关于将 ASP.NET Core 应用程序部署到 App Service 的详细文档:learn.microsoft.com/aspnet/core/host-and-deploy/azure-apps/。
现在,让我们看看另一个 Azure 选项。这个选项专门用于部署静态站点,如 Blazor Wasm。
Azure Static Web Apps
Azure Static Web Apps (azure.microsoft.com/products/app-service/static/) 是一个用于托管和提供静态 Web 应用程序(如 Blazor Wasm)的服务。它通过 GitHub Actions 提供简单的部署,免费 SSL 证书,自定义域名,以及与 Azure Functions 的轻松集成。
关于 Static Web Apps 的完整文档,包括与其他 SPA 网站使用的信息,Microsoft Learn 提供了文档、指南和 培训 (learn.microsoft.com/training/) 内容,可在 learn.microsoft.com/azure/static-web-apps/ 找到。
我们将使用 Static Web Apps 来托管我们的 Blazor 应用程序。现在让我们来做这件事!
将 Blazor 发布到 Azure Static Web Apps 托管
在本节中,我们将通过将源代码推送到 GitHub、创建 Azure 静态 Web 应用应用程序并配置 GitHub Actions 以在主分支每次提交时将应用程序发布到 Azure,来在云中托管我们的BlazorTasks应用程序。让我们首先将我们的代码推送到 GitHub。
将项目推送到 GitHub
要将代码推送到 GitHub 仓库,你可以使用 Git CLI (git-scm.com/downloads) 或GitHub Desktop (desktop.github.com/)应用程序。在这个例子中,我们将使用 GitHub Desktop:
-
下载并安装 GitHub Desktop。安装完成后,启动应用程序。
-
如果你本地项目还不是 Git 仓库的一部分,请选择文件 | 新建仓库。如果你已经为你的项目创建了一个本地仓库,你可以跳到下一步:

图 12.11 – 为 BlazorTasks 应用程序创建一个新的本地 GitHub 仓库
将仓库命名为类似BlazorTasksWasm或BlazorTasks的东西,可选地添加一个描述,并浏览到项目的本地路径。拥有一个 README、一个Git 忽略文件和一个许可证是一个好习惯。因此,选择这些选项中的每一个。完成这些后,点击创建仓库。完成此步骤后,继续进行第 4 步。
- 如果你在一个本地 Git 仓库中创建了你的 Blazor 项目,你可以选择
BlazorTasks项目并选择它。如果你那里还没有 Git 仓库,应用程序将提示你创建一个:

图 12.12 – 添加本地仓库
- 在这一步,我们将把本地仓库发布到 GitHub。如果你还没有 GitHub 账户,你可以在
github.com/创建一个。准备好后,确保你的BlazorTasksWasm仓库被选为当前仓库,然后点击发布 仓库按钮:

图 12.13 – 将本地仓库发布到 GitHub
如果你出现在对话框中的保留此代码为私有选项,你可以取消选中它。
- 在 GitHub 上查看仓库,以确保它已正确发布:

图 12.14 – GitHub 上的 BlazorTasks 代码
现在,代码已经准备好发布到 Azure 了。让我们接下来进行这一步。
创建 Azure 静态 Web 应用资源
让我们一步步创建一个新的 Azure 静态 Web 应用应用程序:
-
要开始,如果你还没有 Azure 账户,你可以在
azure.microsoft.com/创建一个免费试用账户。网站将引导你完成创建新账户的步骤。 -
登录到与您的 Azure 账户关联的 Microsoft 账户,网址为
portal.azure.com/。 -
从门户主页,点击 Azure 服务 顶部下的创建资源。
-
在
static下选择静态 Web 应用:

图 12.15 – 创建新的静态 Web 应用
- 点击
BlazorTasksWasm。为资源命名,选择对您或您的用户有意义的区域选项,并选择免费选项作为SKU。选择GitHub作为部署详情。我们将在下一步链接到 GitHub:

图 12.16 – 配置新的静态 Web 应用资源
- 接下来,点击
BlazorTasksWasm仓库:

图 12.17 – 为静态 Web 应用资源输入 GitHub 详细信息
-
在
wwwroot。 -
点击审阅 + 创建。审阅摘要页面,确保一切看起来正确,然后点击创建。Azure 将花费几分钟创建新的资源。完成后,您可以点击转到资源。
静态 Web 应用资源已准备就绪。Azure 已经为我们创建了 GitHub Actions 部署。让我们回顾一下它做了什么,然后审阅网站。
使用 GitHub Actions 发布应用程序
通常,我们会在这里配置 GitHub Actions 以在 GitHub 仓库中构建我们的项目并将其发布到 Azure 资源。然而,Azure 静态 Web 应用配置已经为我们处理了这一步骤。让我们回顾一下它做了什么:
- 导航到您的 GitHub 项目并点击操作标签。您将看到 Azure 创建了一个名为 Azure Static Web Apps CI/CD 的工作流程:

图 12.18 – 查看 BlazorTasks 的工作流程
-
选择出现在
.yml文件下方编辑器中的.yml文件超链接。 -
审阅文件内容。您将看到一个
build_and_deploy_job部分。此步骤将获取最新提交的代码,构建它并将其部署到我们在 Azure 中配置的应用服务。 -
您可以通过在 Azure 门户中导航到
BlazorTasksWasm资源并点击网站的 URL 来验证网站是否已发布到 Azure:

图 12.19 – BlazorTasks 资源在 Azure 门户中的主页
- Blazor 网站将在您的浏览器中打开新标签页。点击导航菜单中的任务项,并验证应用程序是否按预期工作:

图 12.20 – 在静态云中运行 BlazorTasks
现在,我们有一个面向公众的静态网站正在运行 Blazor Wasm 应用程序。现在,我们准备在 WinUI 项目中运行 Web 应用程序。
在 WinUI WebView2 中托管 Blazor 应用程序
我们已经完成了最后的冲刺。我们创建了一个 Blazor Wasm 应用程序,将源代码推送到 GitHub,并配置 Azure GitHub Actions 以在每次提交时将应用程序发布到 Azure 静态 Web 应用。最后一步是创建一个简单的 WinUI 3 项目,并将WebView2控件添加到MainWindow:
-
你可以通过在 Visual Studio 中创建一个新的
BlazorTasksHost或者从 GitHub 打开入门项目来开始:github.com/PacktPublishing/-Learn-WinUI-3/tree/master/Chapter12/Start/BlazorTasksHost。 -
打开
MainWindow.xaml并更新窗口以托管包含WebView2控制的Grid。将Source属性设置为你的BlazorTasksWasm站点的 URL:<Grid> <WebView2 Source="https://you-custom-url- 0af06780d.azurestaticapps.net/"/> </Grid> -
在
MainWindow.xaml.cs中移除未使用的按钮点击事件处理程序,以防止编译错误。 -
运行应用程序,你会看到
BlazorTasksWasm应用程序像 Windows 应用程序一样加载:

图 12.21 – 在 WinUI 应用程序中运行 BlazorTasksWasm
你可以在网页视图中测试应用程序。因为它是全部客户端代码,即使断开网络连接,你也可以继续使用应用程序。任务页面将继续离线工作。
现在,当你向 GitHub 提交更改时,你对 Blazor 应用程序所做的任何更新都将立即推送到所有用户。这对于想要接触更多 Windows 用户的 Web 开发者来说是一个很有吸引力的方式。
注意
如果你想要进一步探索 Blazor 和 WinUI 的集成,你可以查看托马斯·克劳迪乌斯·休伯的这篇博客文章。在文章中,他通过执行脚本通过WebView2控件从 WinUI 宿主应用程序调用 Blazor 应用程序中的方法进行了实验:www.thomasclaudiushuber.com/2020/02/18/hosting-blazor-app-in-winui-3-with-webview-2-and-call-blazor-component-method-from-winui/。
让我们总结一下本章我们所学到的内容。
摘要
在本章中,我们学习了 ASP.NET Core Blazor。你使用 Blazor Wasm 创建了一个简单的任务跟踪应用程序,并通过 GitHub Actions 将其发布到 Azure Static Web Apps。从这里,你可以使用 ASP.NET Core Identity 来集成应用程序登录,并将任务数据保存到Azure SQL、Azure Cosmos DB或另一个基于云的数据存储。这将允许为每个用户个性化任务列表并保存其状态。我们创建了一个 WinUI 3 应用程序来在 Windows 上运行 Blazor 客户端,但你也可以直接将用户发送到你的网站或为桌面和移动客户端创建基于 JavaScript 的 PWA。有关使用 Blazor WASM 创建 PWA 的更多信息,请参阅这篇 Microsoft 博客文章:devblogs.microsoft.com/visualstudio/building-a-progressive-web-app-with-blazor/。
备注
要了解更多关于使用 Blazor 构建 Web 应用程序的信息,你可以阅读 Jimmy Engstrom 的《Web Development with Blazor》。这里是亚马逊链接:www.amazon.com/dp/1803241497/
在下一章,第十三章,使用 Uno Platform 跨平台开发你的应用,我们将探讨Uno Platform可以为 WinUI 开发者做什么。
问题
-
.NET 7 及更早版本中,名为什么的 Blazor 托管模型在浏览器中运行所有应用程序逻辑?
-
哪个 Blazor 托管模型的可扩展性较差?
-
Blazor UI 文件中使用的语法叫什么名字?
-
哪个.NET CLI 命令可以编译并运行当前文件夹中的项目?
-
GitHub 提供托管静态网站的产品叫什么名字?
-
哪个 Azure 产品托管静态网站?
-
哪个 WinUI 3 控件可以在基于 Chromium 的浏览器控件中加载网页内容?
-
GitHub 的持续集成/持续交付(CI/CD)解决方案叫什么名字?
第十三章:使用 Uno Platform 将您的应用程序跨平台化
Uno Platform 允许开发者在一个代码库中编写 XAML 标记和 C# 代码,并将应用程序部署到多个平台。目前 Uno 支持 iOS、Android、Windows、macOS、Linux、Tizen 以及网页(通过 WebAssembly)。Uno Platform 使用与 WinUI 相同的 XAML 语法,使得 WinUI 开发者可以轻松地跳转到 Uno,同时重用大量现有的 XAML 和 C# 代码。我们将探讨如何将 My Media Collection 示例应用程序适配到使用 Uno Platform 在这些其他平台上运行。
在本章中,我们将涵盖以下主题:
-
Uno Platform 的历史及其当前功能讨论
-
如何配置 Visual Studio 以创建 Uno Platform 项目
-
将现有的 WinUI 视图和 ViewModels 适配以在 Uno 项目中重用
-
使用 Windows Subsystem for Android (WSA)在 Android 上运行和调试 Uno Platform 应用程序
-
使用 WebAssembly 在浏览器中本地运行您的应用程序
到本章结束时,你将了解如何将使用 Windows App SDK 构建的 WinUI 应用程序移植到 Windows 生态系统之外的其他多个平台使用 Uno Platform。
技术要求
要跟随本章中的示例,需要以下软件:
-
Windows 11 版本 22000.0 或更高版本,并已从 Microsoft Store 安装 WSA。要使用 WSA,建议至少 16 GB 的 RAM。
-
Visual Studio 2022 或更高版本,并已配置 .NET Desktop Development 工作负载以进行 Windows App SDK 开发。
-
如果你想要构建和运行 iOS 或 macOS 版本的示例应用程序,你需要一台运行 macOS 12.5 或更高版本的 Mac,并已从 App Store 安装 Xcode 14 或更高版本。
-
要针对 Android 设备,你可以在 Visual Studio 安装程序中安装 .NET Multi-platform App UI development 工作负载。这将安装一个受支持的 Android SDK 版本。
Uno Platform 概述
Uno Platform 是一个开源的 UI 框架,作为 Visual Studio 的扩展进行安装。它是跨平台的,能够针对 Windows、iOS、Android、macOS、Linux 和 WebAssembly。使用单个 C# 和 WinUI XAML 代码库,你可以针对所有这些平台。虽然 Uno Platform 团队推荐使用 Visual Studio 以获得最佳体验,但你也可以使用 Visual Studio Code 或 JetBrains Rider IDE 来构建 Uno 应用程序。每个开发环境的优缺点在 Uno Platform 的 入门 文档中有讨论:platform.uno/docs/articles/get-started.html?tabs=windows#select-your-development-environment。
Uno Platform 首次发布于 2018 年 5 月,近年来其受欢迎程度一直在增长。他们还增加了支持的平台,并在 XAML 支持中从 UWP 转向 WinUI 3。今年,在他们的 4.10.13 版本中,他们甚至增加了在 Uno Platform 应用程序中嵌入 .NET MAUI 控件的支持,并支持大量第三方控件。我们不会在本章中介绍 .NET MAUI 的嵌入,但您可以在他们的文档中探索这个令人兴奋的功能:platform.uno/docs/articles/external/uno.extensions/doc/Overview/Maui/MauiOverview.html。
谈到 .NET MAUI,您可能想知道为什么开发者会选择 Uno Platform 而不是 Microsoft 对 Xamarin.Forms 的跨平台继任者。WinUI 开发者选择 Uno Platform 的一个原因是 熟悉度。Uno 应用程序使用 WinUI XAML 创建,因此没有学习曲线。.NET MAUI XAML 与 WinUI 略有不同。如果您认为针对 Linux 和网页浏览器很重要,.NET MAUI 应用程序目前无法针对这两个平台,而 Uno Platform 可以。如果您或您公司的设计师使用 Figma 创建用户界面,您会发现 Uno Platform Figma 插件将在构建下一个应用程序时为您的团队提供一个很好的起点。
Uno Platform 是开源的。您可以在他们的 GitHub 仓库 (github.com/unoplatform/uno) 中跟踪开放问题、提交拉取请求以改进框架,或了解他们最新的发布信息。如果您想在安装并构建您的第一个应用程序之前尝试 Uno Platform,您可以在浏览器中打开他们的交互式 Uno Playground,网址为 playground.platform.uno/#wasm-start:

图 13.1 – 在网页浏览器中探索 Uno Playground
Uno Playground 示例应用程序在浏览器中以 WebAssembly 的形式交互式运行。您可以在左侧面板中修改 XAML,并实时观察右侧预览的更新。
您还可以探索的其他基于浏览器的资源是 Uno Gallery (gallery.platform.uno/)。在 Uno Gallery 中,您可以探索 Uno Platform 的控件、主题功能以及其他 UI 和非 UI 功能。例如,在 按钮 控件的画廊页面中,您可以看到该控件将以不同的样式呈现 – Material、Fluent 或 Cupertino 设计:

图 13.2 – 在 Uno Gallery 中探索按钮控件
在我们继续使用 Uno Platform 创建第一个项目之前,花些时间查看这些在线资源。
创建您的第一个 Uno Platform 项目
在本节中,我们将创建一个新的 Uno Platform 项目,这将是我们跨平台版本的 My Media Collection 应用程序的基础,该应用程序在本书的几个早期章节中创建。在我们能够创建一个新的 Uno Platform 项目之前,我们需要安装扩展:
-
首先打开 Visual Studio 并转到 扩展 | 管理扩展 以打开 管理扩展 窗口。
-
在
Uno Platform。 -
Uno Platform 扩展应该是第一个结果。点击 安装 并重新启动 Visual Studio 以完成安装。
-
当你再次打开 Visual Studio 时,选择 创建一个 新项目。
-
在
Uno Platform中。你将得到不同 Uno Platform 项目类型的结果。 -
选择 Uno Platform App 模板并点击 下一步。
-
将项目命名为
UnoMediaCollection并选择 创建。这将启动 Uno Platform 模板向导:

图 13.3 – Uno Platform 模板向导
-
在 选择启动类型 页面上,选择 默认 类型下的 自定义 按钮。
这将打开向导的详细步骤。从这里,你可以配置所有可用的 Uno Platform 选项。我们将保留大多数默认设置,但让我们逐页浏览。
-
在 框架 页面上,当前默认为 .NET 7.0,但当你阅读这本书时,它可能是 .NET 8.0。你可以保留默认选择。
-
在 平台 页面上,我们将只使用 Windows、Android 和 WebAssembly。你可以取消选择其他平台。
-
在
MyMediaCollection应用程序。 -
在 主题 页面上,选择 Fluent 以使用原始应用程序相同的 Fluent 设计。
-
在 扩展 页面上,你可以移除 本地化 并将 导航 改为 空白。
-
你可以在 测试 页面上取消选中 单元测试 和 UI 测试 选项。我们不会在本章中涉及测试。
-
你可以在 项目、功能、身份验证 和 应用程序 页面上保留默认设置。要了解更多关于这些选项的信息,你可以查看 Uno Platform 文档:
platform.uno/docs/articles/get-started-vs-2022.html#create-an-application。 -
点击 创建 以生成项目并开始在 Visual Studio 中使用它们。如果 Visual Studio 提示你重新加载任何项目,请点击 重新加载。
-
在 Visual Studio 的 Uno 欢迎屏幕上遵循 验证您的开发环境 步骤:

图 13.4 – Uno Platform 欢迎屏幕
uno-check命令行工具在诊断潜在的开发环境问题并自动修复它们方面做得很好。您应该允许它在继续之前修复它识别出的任何问题。完成之后,您可能需要重新启动系统。完成此操作并重新打开项目,然后再继续下一步。
- 确保将
UnoMediaCollection.Windows项目设置为启动项目并开始调试。应用程序应该启动并显示一个包含Hello Uno Platform消息的窗口:

图 13.5 – 以 Windows 应用程序运行 UnoMediaCollection
就这样!我们已经有一个运行的应用程序作为我们的起点。在下一节中,我们将通过从MyMediaCollection项目重用代码来增强UnoMediaCollection,我们将学习更多关于项目结构的信息。
将 WinUI XAML 标记和代码迁移到 Uno 平台
在本节中,我们将从上一节创建的UnoMediaCollection解决方案中提取代码,从MyMediaCollection的早期版本迁移代码。这将给我们一个跨平台的应用程序版本,我们将在接下来的部分中在 Windows、Android 和 WebAssembly 上运行。
为了使我们的第一个项目保持简单,我们将从第五章“探索 WinUI 控件”中找到的已完成的MyMediaCollection解决方案迁移代码。如果您没有该代码的副本,您可以从 GitHub 这里获取:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/main/Chapter05/Complete。遵循说明的最简单方法是下载解决方案,但您也可以在 Uno 项目解决方案中创建每个类,并将 GitHub 编辑器中的 WinUI 项目代码复制粘贴。
我们将使用第五章中的代码,因为在这个阶段还没有添加 SQLite 数据库。向 Uno 平台项目添加具有文件访问权限的代码是可能的,但更复杂。这需要编写一些平台特定代码。此代码将根据应用程序当前运行的平台进行条件执行。在大多数平台上使用 SQLite 相对简单,但在 WebAssembly 上本地文件访问则不那么容易。您可以在 Uno Platform 的文档中了解更多关于平台特定代码的信息:platform.uno/docs/articles/platform-specific-csharp.html。
如果不关心离线工作,那么在这些所有平台上进行数据访问的最佳选项是创建一个轻量级的 Web 服务来处理您的数据访问。这样做也意味着添加一个身份解决方案以确保每个用户都在访问自己的数据。如果您对自行探索此选项感兴趣,Uno Platform 有一些关于消费 Web API 的文档:platform.uno/docs/articles/howto-consume-webservices.html。
现在,让我们开始将 WinUI 项目迁移到 Uno Platform。
迁移 WinUI 项目代码
是时候开始我们的 MyMediaCollection 的跨平台转换了。我们将首先从旧项目中导入 C# 类:
- 首先,从上一节打开
UnoMediaCollection解决方案,并在UnoMediaCollection项目中创建四个新的文件夹:Enums、Interfaces、Model和ViewModels。我们还将从Services文件夹添加类,但该文件夹已存在于新项目中:

图 13.6 – 添加到 UnoMediaCollection 项目的新的文件夹
-
右键单击
Enums文件夹,在 WinUIMyMediaCollection项目中选择Enums文件夹,选择ItemType.cs和LocationType.cs文件,然后点击 添加。 -
打开每个新添加的文件,并将
namespace改为UnoMediaCollection.Enums。 -
我们将为剩余的每个文件夹重复这些步骤。接下来,将现有的
IDataService.cs和INavigationService.cs文件添加到Interfaces文件夹。 -
在
IDataService中,将namespace改为UnoMediaCollection.Interfaces并更新using语句为以下内容:using UnoMediaCollection.Enums; using UnoMediaCollection.Model;您可以删除任何其他
using语句,因为它们是INavigationService的一部分,您可以更新namespace为UnoMediaCollection.Interfaces并删除usingSystem;语句。 -
将现有的
MediaItem.cs和Medium.cs文件添加到Model文件夹。 -
修改
MediaItem以如下所示(变更已突出显示):using UnoMediaCollection.Enums; namespace UnoMediaCollection.Model { public class MediaItem { public int Id { get; set; } public string? Name { get; set; } public ItemType MediaType { get; set; } public Medium? MediumInfo { get; set; } public LocationType Location { get; set; } } } -
修改
Medium类以如下所示:using UnoMediaCollection.Enums; namespace UnoMediaCollection.Model { public class Medium { public int Id { get; set; } public string? Name { get; set; } public ItemType MediaType { get; set; } } } -
将现有的
DataService.cs和NavigationService.cs文件添加到Services文件夹。 -
在
DataService中,更新namespace为UnoMediaCollection.Services并更新您的using语句,使其仅包含以下三个语句:using UnoMediaCollection.Enums; using UnoMediaCollection.Interfaces; using UnoMediaCollection.Model; -
在
NavigationService中,更新namespace为UnoMediaCollection.Services并将AppFrame变量的访问器从private static更改为internal static。我们稍后需要从App.cs设置此值。同时,更新using语句,使其仅包含以下两个语句:using UnoMediaCollection.Interfaces; using System.Collections.Concurrent; -
将现有的
ItemDetailsViewModel.cs和MainViewModel.cs文件添加到ViewModels文件夹。 -
在
ItemDetailsViewModel中,将namespace改为UnoMediaCollection.ViewModels并修改using语句以包含以下四个语句:using UnoMediaCollection.Enums; using UnoMediaCollection.Interfaces; using UnoMediaCollection.Model; using System.Collections.ObjectModel; -
在
MainViewModel中,将namespace更改为UnoMediaCollection.ViewModels并更新using语句,使其只包含以下四个语句:using Microsoft.UI.Xaml.Input; using UnoMediaCollection.Interfaces; using UnoMediaCollection.Model; using System.Collections.ObjectModel;到目前为止的所有更改都比较简单。Uno Platform 项目已经使用了我们在 WinUI 项目中引用的 NuGet 包,所以代码非常兼容。
-
在我们继续到两个视图之前,让我们对
App.cs进行必要的修改。我们需要设置NavigationService并将我们的服务和 ViewModel 类注册到 IoC 容器中。首先,将以下三个using语句添加到App类中:using UnoMediaCollection.Interfaces; using UnoMediaCollection.Services; using UnoMediaCollection.ViewModels; -
将
Host变量重命名为HostContainer以匹配我们的 WinUI 项目的名称,并使其为internal static:internal static IHost? HostContainer { get; private set; } -
在
OnLaunched方法开始处,在创建builder对象之前,添加以下代码:var navigationService = new NavigationService(new Frame()); navigationService.Configure(nameof(MainPage), typeof(MainPage)); navigationService.Configure(nameof(ItemDetailsPage), typeof(ItemDetailsPage));这创建了
navigationService类,稍后它将被注册到 IoC 容器中,并注册两个视图以进行导航。我已经突出显示了与原始 WinUI 项目代码的一个不同之处。我们暂时将new Frame()传递到构造函数中。在方法中稍后,我们将AppFrame设置为OnLaunched方法末尾创建的rootFrame。 -
接下来,更新
OnLaunched中的ConfigureServices块,使其看起来像这样:.ConfigureServices((context, services) => { services.AddSingleton<INavigationService>(navigationService); services.AddSingleton<IDataService, DataService>(); services.AddTransient<MainViewModel>(); services.AddTransient<ItemDetailsViewModel>(); })这就像我们在 WinUI 项目中所做的那样,将我们的类注册到 IoC 容器中。
-
将
OnLaunched中的Host = builder.Build();代码行更新为HostContainer = builder.Build();. -
最后,在
OnLaunched结尾的MainWindow.Activate();调用之前,添加以下代码行以更新NavigationService中的AppFrame静态变量:NavigationService.AppFrame = rootFrame;
除了两个视图之外,我们还需要添加和更新的内容就这些了。代码还不能成功编译,因为我们已经在 OnLaunched 中引用了 ItemsDetailsPage,但还没有添加它。我们将在下一节中处理这个问题。
迁移 WinUI XAML 视图
在本节中,我们将完成对 UnoMediaCollection 项目的添加和更改,并运行应用程序的 Windows 版本。让我们从 ItemDetailsView 开始:
-
右键点击
UnoMediaCollection项目,选择 添加 | 新建项. -
在
ItemDetailsPage.xaml中,并点击 添加:

图 13.7 – 将 ItemDetailsPage 添加到项目中
-
打开
ItemDetailsPage.xaml.cs并将类的全部内容替换为 WinUI 项目中的以下代码:public ItemDetailsPage() { ViewModel = App.HostContainer.Services.GetService<ItemDetailsViewModel>(); this.InitializeComponent(); } public ItemDetailsViewModel ViewModel; protected override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); var itemId = (int)e.Parameter; if (itemId > 0) { ViewModel.InitializeItemDetailData(itemId); } }注意,我们在构造函数中的
InitializeComponent调用之后没有迁移任何用户设置代码。为了我们的第一次迁移尝试,我们将保持简单。 -
您还可以从
ItemDetailsPage类中删除所有using语句,除了以下这些:using UnoMediaCollection.ViewModels; -
打开
ItemDetailsPage.xaml并将Page的子内容替换为 WinUI 项目中ItemDetailsPage.xaml的内容。不要替换Page本身,因为命名空间不同。 -
删除此
SplitButton.Resources块,因为我们不会使用TeachingTip。我们删除了依赖于将用户首选项保存到文件系统的代码。没有它,每次页面打开时都会出现:<SplitButton.Resources> <TeachingTip x:Name="SavingTip" Target="{x:Bind SaveButton}" Title="Save and create new" Subtitle="Use the dropdown button option to save your item and create another."> </TeachingTip> </SplitButton.Resources> -
打开
MainPage.xaml.cs并添加以下using语句:using UnoMediaCollection.ViewModels; -
MainPage类的内容将与ItemDetailsPage相似:public MainPage() { ViewModel = App.HostContainer.Services.GetService<MainViewModel>(); this.InitializeComponent(); } public MainViewModel ViewModel; protected override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); if (e.NavigationMode == NavigationMode.Back) { ViewModel.PopulateData(); } }这与 WinUI 项目中
MainPage的实现略有不同。我们添加了对OnNavigatedTo的重写。在某些平台上,在ItemDetailsPage上添加项目后,MainPage上的项目列表没有更新。当用户导航到Back时调用MainViewModel上的PopulateData()解决了这种行为。这是 Android 和 WebAssembly 上的一个问题。Windows 项目按预期工作。 -
打开
MainPage.xaml并替换Page中的Page的整个子内容。就像我们在ItemDetailsPage.xaml中做的那样,由于命名空间的不同,请小心不要替换Page本身。 -
最后,将以下
using声明添加到MainPage中的Page元素:xmlns:model="using:UnoMediaCollection.Model"MediaItem模型类在ListView中的DataTemplate中被引用,并需要此using声明。
这些就是使应用程序准备好使用 Uno Platform 运行的所有更改。我们根本不需要更改我们的 XAML 控件,除了删除 TeachingTip。
让我们运行 Windows 版本的应用程序,以确保一切按预期工作。确保将 UnoMediaCollection.Windows 设置为启动项目并运行应用程序。当它启动时,应该看起来像这样:

图 13.8 – 使用 Uno Platform 运行 Windows 项目
它看起来与我们的 WinUI 应用程序的 第五章 版本完全一样。如果您尝试添加、编辑或删除项目,一切都应该按预期工作。做得好!这很简单。让我们继续前进,尝试使用 WSA 在 Android 上使用该应用程序。
使用 WSA 在 Android 上运行
使用 WSA 在 Windows 上运行和调试 Android 应用程序既快又简单。在 Windows 11 上安装 WSA 的最简单方法是安装来自 Microsoft Store 的 Amazon Appstore。您可以通过此处获取应用程序:aka.ms/AmazonAppstore。

图 13.9 – Microsoft Store 中的 Amazon Appstore 应用
安装应用程序并按照提示将 WSA 作为过程的一部分进行安装。在完成初始安装后,您需要重新启动计算机以完成 WSA 的安装和配置:

图 13.10 – 完成 Amazon Appstore 的安装
重启完成后,在您的 Windows 开始菜单中找到 Amazon Appstore 应用并启动它。您会看到 WSA 首先启动:

图 13.11 – 启动 WSA 进程
请保持 Amazon Appstore 在后台运行,以确保您的 Android 系统保持活跃。如果您喜欢,可以最小化窗口。您还需要做的另一件事是确保 开发者模式 已开启。从开始菜单启动 Windows Subsystem for Android。这将打开 WSA 系统 设置页面。从左侧导航面板中选择 高级设置 以打开 高级 设置 页面:

图 13.12 – WSA 系统设置
如果 开发者模式 没有开启,请现在开启它。最后的准备步骤是将 Android SDK 连接到 WSA 以进行我们的调试会话。为此,您需要找到您的 Android SDK 安装位置。如果它是与 Visual Studio 安装一起安装的,它应该在这个路径上:
c:\Program Files (x86)\Android\android-sdk\
您需要从命令提示符运行以下命令。在您的 SDK 位置的 platform-tools 子文件夹中打开一个终端或命令窗口。如果您在 PowerShell 中运行,这是该命令:我使用 Windows Terminal 中的 PowerShell 窗口:
.\adb connect 127.0.0.1:58526
如果您收到 failed to authenticate to 127.0.0.1:58526 的消息,请检查是否有 WSA 弹出对话框在您的其他窗口后面。每次您使用 WSA 开始调试时,都点击 adb connect 命令。
现在是时候使用 WSA 运行我们应用程序的 Android 版本了。将启动项目更新为 UnoMediaCollection.Mobile:

图 13.13 – 更新启动项目以在移动设备上运行
如果 WSA 仍然处于活动状态,您应该在 调试 按钮上看到 Microsoft Corporation Subsystem for Android 以及一个 Android 版本号。开始调试并等待几分钟。编译、部署和运行 Android 应用程序可能比 Windows 版本花费的时间要长。当应用程序启动时,它应该看起来像这样:

图 13.14 – 使用 WSA 在 Android 上运行 UnoMediaCollection 应用程序
所有的功能应该和在 Windows 上一样,但根据您系统的性能,UI 可能会有轻微的延迟。使用 WSA 的酷之处在于您可以将应用程序的窗口大小调整到测试不同宽高比下的 UI 布局。试试看。
如果您在系统上的 Android SDK 中配置了 Android 模拟器,您也可以尝试在 调试 按钮上选择它并在那里运行。在传统手机模拟器图像上,它看起来可能像这样:

图 13.15 – 在 Android 模拟器上运行应用程序
我们已经在 Android 上运行起来了。在许多情况下,切换平台就像更改启动项目一样简单。让我们通过在 Web 上使用 WebAssembly 尝试我们的应用程序来结束这次尝试。
使用 WebAssembly 在浏览器中运行
在本节的最后,我们将尝试在浏览器中使用 WebAssembly 运行应用程序。Uno Platform 使得这变得很容易,但就像 Android 一样,编译和部署可能需要一点时间。这是因为整个应用程序需要在浏览器中作为客户端运行。这意味着除了部署我们的应用程序外,所有其依赖项(甚至包括 .NET 运行时的一个版本)也需要被部署。
这就是为什么 WebAssembly 的采用速度没有像许多人预期的那样快的原因之一。这些应用程序的首次加载性能在最理想的情况下也可能比较慢。Uno Platform 发布了一篇关于使用 Uno Platform 优化 WebAssembly 性能的博客文章。如果你打算追求这个选项,你应该阅读这篇文章:platform.uno/blog/optimizing-uno-platform-webassembly-applications-for-peak-performance/。
将启动项目更改为 UnoMediaCollection.Wasm 并开始调试。你会在后台看到一个命令窗口启动,它承载着部署 WebAssembly 应用程序的 Web 服务器。接下来,将打开一个浏览器窗口。在应用程序部署和加载的过程中,你会看到一个 Uno Platform 标志作为启动画面。当应用程序加载完成后,它看起来会是这样:

图 13.16 – 在浏览器中使用 WebAssembly 运行 Uno Platform 应用程序
尝试使用该应用程序。它应该与在其他平台上运行时完全一样。虽然一些视觉元素可能在不同平台上有所不同,但 Uno Platform 承诺在每个支持平台上都能实现接近像素级的完美应用程序。
如果你想要进一步探索调试,调试 WebAssembly 应用程序时有一些不同之处。Uno Platform 文档中关于这方面的信息非常出色:platform.uno/docs/articles/external/uno.wasm.bootstrap/doc/debugger-support.html。
在本节中,我们将使用 WebAssembly 和 Uno Platform 完成所有操作。让我们总结一下,回顾一下本章所学的内容。
摘要
在本章中,我们学习了关于 Uno Platform 的所有内容。WinUI 开发者可以利用他们的 Windows 开发经验,将其用于构建适用于每个平台的应用程序。虽然我们在这里专注于 Visual Studio 和 Windows 开发,但使用 Visual Studio、VS Code 和 JetBrains Rider,你可以在任何平台上构建你的 Uno Platform 应用程序。我们从基本的 Hello World 风格的应用程序开始,将代码和 XAML 从我们的 WinUI 项目导入,以极少的更改创建了一个跨平台版本的应用程序。我们还学习了如何利用 WSA 在 Windows 上调试可调整大小的 Android 应用程序,而无需配置模拟器。最后,我们使用 Uno Platform 和 WebAssembly 在浏览器中运行了我们的应用程序。你现在可以亲自尝试 Uno Platform 并测试它支持的其他平台。
在下一章(也是最后一章)中,我们将了解如何通过 WinGet 或企业部署选项将 WinUI 3 应用程序部署到 Microsoft Store。
问题
-
使用 Visual Studio 调试 Android 应用程序时,有哪些两种部署选项?
-
Uno Platform 支持哪两种应用程序设计模式?
-
Uno Platform 支持哪些 XAML 架构?
-
第一版 Uno Platform 是何时发布的?
-
.NET 和 Uno Platform 开发者可以利用哪种客户端 Web 技术在浏览器中本地运行应用程序?
-
Uno Platform 提供哪两种在线资源,以便你在浏览器中测试其控件和组件?
-
你可以利用哪种设计工具在设计你喜欢的 IDE 中开发 Uno Platform 应用程序之前设计它们?
第十四章:打包和部署 WinUI 应用程序
WinUI 开发者有几种选项来打包和部署他们的应用程序。开发者可以在 Microsoft Store 上创建一个账户,并将打包的应用程序上传到 Microsoft Partner Center 以供公众消费。应用程序包也可以创建,以便通过 Microsoft Endpoint Manager 和 Microsoft Intune 由组织分发,或者由个人在 Windows PC 上侧载。
在本章中,我们将涵盖以下主题:
-
探索应用程序打包和 MSIX 基础知识
-
在 Visual Studio 中开始应用程序打包
-
使用 Windows Package Manager 部署应用程序
-
使用 Microsoft Store 分发应用程序
-
使用 MSIX 侧载 WinUI 应用程序
到本章结束时,您将了解可用于打包和分发 WinUI 应用程序的方法,以及如何使用每种方法。
技术要求
要跟随本章中的示例,需要以下软件:
-
Windows 10 版本 1809(构建 17763)或更高版本,或 Windows 11
-
Visual Studio 2022 或更高版本,已配置 .NET Desktop Development 工作负载以进行 Windows App SDK 开发
本章的源代码可在 GitHub 上通过此 URL 获取:github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/master/Chapter14。
探索应用程序打包和 MSIX 基础知识
在本书的大部分内容中,我们都在本地构建和运行我们的 WinUI 3 应用程序。现在,是时候学习打包和部署 WinUI 应用程序的概念,并将这些知识付诸实践。
为什么要打包应用程序?嗯,应用程序包是 WinUI 应用程序及其依赖项在 Windows 上安装的最简单方式。今天,当你在 Visual Studio 中运行 WinUI 项目时,集成开发环境(IDE)会创建一个包并将其本地部署。打包还有其他几个重要的用途,如下所述:
-
提供干净的卸载:打包系统确保在应用程序卸载时,任何由应用程序安装或更新的文件都会被删除或恢复到其之前的状态
-
捆绑依赖项:应用程序包将捆绑并交付所有应用程序的依赖项,通过在可能的情况下共享安装应用程序中的文件来优化磁盘空间
-
简化更新:差异更新已优化,仅根据原始包和更新包的清单交付需要更新的文件
-
声明能力:通过在清单中声明应用程序的能力,用户在选择安装之前就会知道应用程序需要哪些类型的访问权限
-
验证完整性和真实性:为了在其他人的设备上安装 Windows 应用程序,应用程序包必须使用来自 受信任的 签名机构的有效证书进行数字签名。
如果你打算分发你的 WinUI 应用程序,你将使用的打包格式是 MSIX。什么是 MSIX?让我们来了解一下。
MSIX
MSIX 是 Microsoft 引入的最新打包应用程序的标准。它不仅限于 Windows。MSIX SDK(learn.microsoft.com/windows/msix/msix-sdk/sdk-overview)是一个开源项目,可以用于为任何平台创建应用程序包。您可以使用 SDK 为 Windows、Linux、macOS、iOS、Android 以及甚至网页浏览器创建应用程序包。在本章中,我们将专注于向 Windows 用户提供 WinUI 应用程序,但您可以在其 GitHub 仓库 github.com/Microsoft/msix-packaging 上了解更多关于 MSIX SDK 的信息。
在 Windows 上,以下平台目前支持 MSIX 格式:
-
Windows 10 版本 1709 及以后的版本,以及 Windows 11
-
Windows Server 2019 长期服务渠道(LTSC)及以后的版本
-
Windows Enterprise 2019 LTSC 及以后的版本
Windows 10 的早期版本需要 APPX 包,它是 MSIX 包的前身。然而,WinUI 3 支持的所有 Windows 10 版本也支持 MSIX 包。Microsoft 于 2018 年引入了 MSIX 包,作为 APPX 的发展,旨在满足 APPX 以及传统的 Windows 安装程序(MSI)包的需求。MSIX 是一个开放标准,因此它可以用于向任何平台分发应用程序。自 1999 年以来,MSI 一直是打包和安装 Windows 桌面应用程序的标准。在 Windows 95 及以后的操作系统上支持使用 MSI 包安装 Windows 应用程序。
使用新的 MSIX 打包标准,发送给 Windows 用户的 通用 Windows 平台(UWP)应用程序将在一个轻量级的应用容器内运行。WinUI 3 应用程序也可以配置为在应用容器中运行,以提供额外的安全性。Windows 应用容器为应用程序的执行提供了一个沙箱,限制了对其注册表、文件系统和其他系统功能(如摄像头)的访问。任何应用程序需要访问的功能都必须在清单文件中指定。打包为在应用容器中运行并使用 MSIX 安装的程序默认具有对 Windows 注册表的 读取访问权。在这种配置下,如果应用程序被卸载或重置,写入虚拟注册表中的任何数据都将被完全删除。对写入虚拟文件系统的数据也是如此。
如 Microsoft Learn 上所述(learn.microsoft.com/windows/msix/overview#inside-an-msix-package),MSIX 包的内容被分为 应用程序文件 和 脚印文件,如下面的图示所示:

图 14.1 – MSIX 包的内容
应用程序文件是代码文件和其他资产的有效负载,这些文件被交付给用户。脚印文件是包需要的元数据和其它资源,以确保应用程序文件按预期交付。此元数据包括以下内容:
-
AppManifest:清单(AppxManifest.xml)包括有关应用程序标识、依赖项、功能、扩展点和视觉元素的信息。这是从 WinUI 项目的Package.appxmanifest文件生成的。 -
AppBlockmap:AppxBlockMap.xml文件包括一个索引和加密散列的文件列表,这些文件位于包中,并在签名时进行数字签名,以确保包签名时的完整性。 -
AppSignature:当包被签名时,包中的AppxSignature.p7x文件被生成。这允许操作系统在安装过程中验证签名。 -
CodeIntegrity:通过验证AppxManifest.xml、AppxBlockMap.xml和AppxSignature.p7x中的包信息来确保代码完整性。
包中的应用程序文件将被安装到 C:\Program Files\WindowsApps<package_name>,应用程序的可执行文件位于 C:\Program Files\WindowsApps\<package_name>\<app_name>.exe。请注意,您不能直接执行此 EXE 文件,并且对 WindowsApps 文件夹的访问受到 Windows 的限制。应用程序在安装期间和安装后创建的数据将存储在 C:\Users\<user_name>\AppData\Local\Packages\<package_name> 下。当应用程序被卸载时,所有应用程序文件、依赖项和数据都将被删除。
既然我们已经了解了 MSIX 的背景和历史,让我们回顾一下可供开发者和 IT 专业人员使用的工具。
检查 MSIX 工具和资源
在我们开始使用 MSIX 打包自己的应用程序之前,我们将回顾一些其他可用的工具和资源,如下所示:
-
MSIX 工具包:MSIX 工具包是由 Microsoft 在 GitHub 上维护的 MSIX 工具和脚本的开源集合:
github.com/microsoft/MSIX-Toolkit。 -
MSIX 实验室:Microsoft 为对利用 MSIX 感兴趣的开发者和 IT 专业人员维护了一套动手教程,用于打包和分发他们的应用程序:
github.com/Microsoft/msix-labs。 -
MSIX 打包工具:MSIX 打包工具是一个用于将经典应用程序重新打包为 MSIX 格式的应用程序。现有的 EXE、MSI 和应用程序虚拟化(App-V)安装包可以通过工具的交互式用户界面或命令行工具转换为 MSIX 包。它可在 Microsoft Store 中找到:
apps.microsoft.com/store/detail/msix-packaging-tool/9N5LW3JBCXKF。 -
MSIX 视频教程:Microsoft Learn 在这里提供了一系列关于 MSIX 打包的入门视频:
learn.microsoft.com/windows/msix/resources#msix-videos。这是开始您的 MSIX 之旅的好方法。 -
MSIX 社区:Microsoft Tech Community 有一个专门讨论 MSIX 打包和部署的讨论空间。加入社区并参与其中:
techcommunity.microsoft.com/t5/msix/bd-p/MSIX-Discussions。
这些工具和资源将帮助您在学习 WinUI 应用程序部署的细节过程中。重要的是要记住,MSIX 是微软持续投资的一个领域。它是打包所有应用程序的向前策略和推荐。WinUI 开发者不需要对 MSIX 有深入的了解。您只需要对 MSIX 以及与我们应用程序相关的属性有基本的知识。在我们动手操作之前,我们将简要讨论 Windows 中 打包应用程序、包标识和应用容器的概念。
打包应用程序和应用程序标识
在 Windows App SDK 中有一些特定的部署概念。在我们开始打包和部署自己的应用程序之前,让我们先在这里介绍这些基础知识。
需要理解的最重要概念之一是打包应用程序。我们迄今为止讨论的概念适用于打包的 WinUI 应用程序。这些应用程序使用 MSIX 打包和安装,具有包标识,并且默认情况下是框架依赖的。我们在那个句子中提到了一些重要的概念;让我们首先检查包标识。
所有打包的应用程序,无论是 WinUI、UWP 还是其他桌面 Windows 应用程序类型,都受益于包标识符。包标识符是 Windows 用来区分和验证应用程序身份的唯一标识符。有一些 Windows App SDK 功能仅适用于具有包标识符的应用程序。我们在第八章,添加 Windows 通知到 WinUI 应用程序中使用的新的 Windows App SDK 通知 API,对任何打包应用程序都可用,因为这些应用程序具有包标识符。有关需要包标识符的其他功能的当前列表,请参阅此 Microsoft Learn 主题:learn.microsoft.com/windows/apps/desktop/modernize/modernize-packaged-apps。
现在,让我们讨论打包应用程序和未打包应用程序。我们已经审查了打包应用程序的一些方面。大多数打包应用程序都是通过 MSIX 打包和安装的。然而,有一种打包应用程序的特殊变体,称为具有外部位置的打包应用程序。这些应用程序使用 MSIX 打包,因此具有包标识符,但它们使用不同的安装程序机制进行安装。我们不会部署具有外部位置的打包应用程序,但你可以在 Microsoft Learn 上了解更多关于它们的信息:learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps。
未打包的应用程序没有包标识符,因此它们可以访问的 API 受到限制,正如之前所讨论的。在大多数情况下,你只会选择这条路线来处理不需要任何需要包标识符的功能的旧版应用程序,并且通常对它们的功能限制较少,例如文件系统或注册表访问。UWP 应用程序不能进行解包,但 WinUI 应用程序可以。
最后一个要审查的概念是框架依赖型应用程序和自包含应用程序。依赖框架的 WinUI 应用程序依赖于 Windows App SDK 运行时在目标机器上安装。这减少了应用程序安装程序的大小,并允许应用程序在运行时接收安全漏洞修复更新,而无需每个应用程序都进行更新。缺点是在安装期间必须检查运行时的存在,以及用户在应用程序安装后卸载运行时的潜在风险。
自包含部署将 Windows App SDK 运行时与 MSIX 打包在一起。这增加了安装程序的大小,但提供了对应用程序使用的运行时版本的完全控制。自包含应用程序还支持 Xcopy 部署。你可以将应用程序及其依赖项从项目的输出文件夹复制到任何支持的 PC。除了包大小之外,此选项的主要缺点是性能。自包含应用程序加载较慢,并且由于它们的依赖项不与其他 WinUI 应用程序共享,因此它们使用更多的系统内存。它们被隔离在你的应用程序进程中。
关于这些概念的更详细信息,Microsoft Learn 上有两个优秀主题:
-
Windows App SDK 部署概述 (
learn.microsoft.com/windows/apps/package-and-deploy/deploy-overview) 讨论了自包含部署的优缺点 -
部署概述 主题 (
learn.microsoft.com/windows/apps/package-and-deploy/) 深入探讨了打包应用与未打包应用之间的区别
现在,让我们开始动手,在 Visual Studio 中创建我们的 MSIX 包。
在 Visual Studio 中开始应用程序打包
在本节中,我们将了解如何使用 Visual Studio 将我们的应用程序打包为 MSIX。Visual Studio 2022 包含两个能够创建 MSIX 部署包的 WinUI 项目模板,具体如下:
-
使用
package.appxmanifest文件生成 MSIX 包 -
package.appxmanifest文件
我们将使用来自 第八章 的完成后的 MyMediaCollection 解决方案。你可以使用该章节中自己的解决方案,或者从该章节的 GitHub 仓库下载副本,网址为 github.com/PacktPublishing/Learn-WinUI-3-Second-Edition/tree/master/Chapter14。让我们看看如何为应用程序生成 MSIX 包,如下所示:
-
首先在 Visual Studio 中打开解决方案。
-
如果你想查看项目的清单数据,可以打开
Package.appxmanifest文件并查看每个选项卡上的设置,如图所示:

图 14.2 – 查看应用程序清单
- 然后,在 解决方案资源管理器 中右键单击
MyMediaCollection项目,并选择 包和发布 | 创建应用包。将出现 创建应用包 窗口,如图所示:

图 14.3 – 创建应用包窗口
-
选择侧载,保留启用自动更新选项选中,并点击下一步。这是 MSIX 的ClickOnce 部署(
learn.microsoft.com/visualstudio/deployment/clickonce-security-and-deployment)的等效操作。 -
在选择签名方法页面,如果出现当前证书,请将其删除。您将看到从Azure Key Vault、存储或本地文件添加证书的选项,或者创建一个新的证书,如以下截图所示:

图 14.4 – 选择包的签名方法
-
我们在这里将创建一个自签名证书。如果您想使用受信任机构的证书创建一个包,您可以选择一个
.pfx文件。对于这个练习,请选择创建。 -
在创建自签名测试证书对话框中输入并确认一个安全密码,然后点击确定。如果您被提示覆盖现有证书,您可以这样做。选择是以继续。您还将收到一条关于证书被导入到证书存储以进行包签名存储的消息。您可以在该消息对话框中点击确定。
-
当您返回到选择签名方法页面时,新的证书详细信息将出现。确保在继续之前,您点击页面上的信任按钮以信任您机器上的证书。然后点击下一步。
-
在选择和配置包页面,有选项可以更改版本号、生成应用程序包,或者添加、删除和更改解决方案配置映射。还有一个复选框可以包含与您的包一起的公共符号文件。保留所有默认设置并点击下一步。
-
在配置更新设置页面,需要一个安装位置路径。对于您将在内部网络上安装工作站的程序,您可以在此处输入包的网络路径。对于我们测试的目的,我们将输入一个本地路径,如以下截图所示。为了使应用程序在其他机器上正确更新,需要存在相同的本地路径到安装程序:

图 14.5 – 配置包的更新设置
-
点击创建。解决方案将被构建,包将被创建。当它完成后,点击复制并关闭按钮将安装程序复制到您选择的安装位置。
-
打开
MyMediaCollection_1.0.0.0_x64_Debug_Test文件夹以查看生成的 MSIX 文件,如以下截图所示:

图 14.6 – 为 MyMediaCollection 创建的 MSIX 包
- 导航到
MyMediaCollection的父文件夹,并双击index.html文件来安装软件包。它将在你的默认浏览器中打开文件,并显示在打包过程中选择的任何配置的安装选项。注意,在下面的屏幕截图中,我已经第二次打包了应用程序,因此版本号已自动增加到1.0.1.0:

图 14.7 – MyMediaCollection 的安装页面
- 如果你尝试在已经安装并运行软件包的同一台机器上安装该软件包,系统会提示你更新应用程序。继续在你的系统上安装或更新应用程序。
使用 Visual Studio 创建软件包是大多数 IDE 用户选择生成安装程序的方式。还有命令行工具可以创建 MSIX 软件包和捆绑包,这些工具在微软学习文档中有描述:learn.microsoft.com/windows/msix/package/manual-packaging-root。
现在,让我们看看另一种替代的发行方法,Windows 软件包管理器。
使用 Windows 软件包管理器部署应用程序
Windows 软件包管理器,也被称为其命令名 WinGet 命令来安装已发布的软件包以及将你的 MSIX 软件包添加到 WinGet 的步骤。微软商店是 WinGet 可用的软件包源之一。因此,如果你计划将你的应用程序发布到商店,就没有必要将其也发布到 WinGet 仓库。
首先,让我们回顾一下将软件包添加到社区仓库的步骤。
将软件包添加到社区仓库
要通过 WinGet 命令使你的应用程序可供 Windows 用户使用,它们必须发布到微软的软件包管理器社区仓库或微软商店。任何发布到社区仓库的应用程序都可以通过 Windows 中的 WinGet 命令被发现和安装。
注意
与通过微软商店的发行方式相比,这种发行方式在本质上要安全得多。理论上,任何能够访问公共仓库的人都可以提取软件包并反编译你的应用程序。
要将你的现有 MSIX 捆绑包添加到仓库,我们需要使其公开可用,创建一个 WinGet 清单,并向 GitHub 提交一个 pull request (PR) 以将清单添加到社区仓库,具体操作如下:
-
首先,将我们创建的安装程序文件夹(
C:\Installers\MyMediaCollection)的内容推送到一个公开的 URL。为此,你可以在 Azure 中创建一个静态网站。这个 Microsoft Learn 主题将介绍这个过程:learn.microsoft.com/azure/static-web-apps/get-started-portal。这将需要一个 GitHub 或 Azure DevOps 仓库来托管要部署的文件。文件可以被放在任何公开的 URL 上。另一个选项是将文件托管在 Azure Blob Storage 中(
learn.microsoft.com/azure/storage/blobs/storage-blob-static-website-how-to?tabs=azure-portal)。我们将选择这个选项。 -
在我们将上传为索引文档名称的
index.html文件名之后。保存文件后,将出现 主端点 字段,如下面的截图所示:

图 14.8 – 在 Azure Blob 存储中设置静态网站
-
点击该页面上的 $web 链接以导航到门户页面,在那里你可以上传文件到静态网站文件夹。
-
点击
C:\Installers\MyMediaCollection文件夹。
一旦你的包在云端,你就可以为社区仓库创建你的清单文件了。该文件以 YAML 格式创建。YAML 文件是当前 DevOps 工作流程的标准。
有关创建清单的详细说明以及有关 YAML 的更多信息,请查看此 Microsoft Learn 主题:learn.microsoft.com/windows/package-manager/package/manifest:
-
将你的 YAML 文件命名为
YourCompany.MyMediaCollection.yaml;内容应该看起来像这样:PackageIdentifier: YourCompany.MyMediaCollection PackageVersion: 1.0.0.0 PackageLocale: en-US Publisher: Your Company Name PackageName: My Media Collection License: MIT ShortDescription: My Media Collection helps you get your media collection organized. Installers: - Architecture: x64 InstallerType: msix InstallerUrl: https://mymediacollectiondeploy.z20.web.core.windows.net/MyMediaCollection_1.0.0.0_x64_Debug_Test/MyMediaCollection_1.0.0.0_x64_Debug.msix InstallerSha256: 712f139d71e56bfb306e4a7b739b0e1109abb662dfa164192a5cfd6adb24a4e1 SignatureSha256: e53f48473621390c8243ada6345826af7c713cf1f4bbdf0d030599d1e4c175ea ManifestVersion: 1.0.0 -
要获取你的
.msix文件的Sha256信息,你可以使用此命令:certUtil –hashfile C:\Installers\MyMediaCollection\ MyMediaCollection_1.0.0.0_x64_Debug_Test\ MyMediaCollection_1.0.0.0_x64_Debug.msix SHA256 -
然后,使用 WinGet 测试你的清单。要安装 WinGet,你可以从 Microsoft Store 安装 App Installer 应用程序,链接为
apps.microsoft.com/store/detail/app-installer/9NBLGGH4NNS1。 -
应用程序从商店安装后,使用以下命令测试你的清单中的语法:
winget validate <manifest-file-name> -
然后,使用此命令测试从你的清单中安装应用程序:
publisher name must be unique. If you have an account on package is the name of your application:manifests<字母表中的字母><发布者><包><版本><文件名>.yaml
注意
如果你对 GitHub 分支和 PR 工作流程不熟悉,Package Manager 文档中有更详细的步骤:learn.microsoft.com/windows/package-manager/package/repository#step-3-clone-the-repository.
在 PR 被批准并合并后,任何拥有 WinGet 的用户都将能够从命令行安装你的应用程序。让我们看看如何使用 WinGet。
使用 WinGet 进行包管理
WinGet 是 Windows 包管理器的命令行客户端。如果你熟悉其他应用程序包管理器,例如 Windows 的Chocolatey(chocolatey.org/)或 macOS 的Homebrew(brew.sh/),WinGet 会让你感到熟悉。包管理器允许你在操作系统中安装、列出和更新应用程序,并可以编写脚本安装多个应用程序,例如在设置新电脑时。在本节中,我们将了解如何使用 WinGet 安装Windows Terminal,这是微软使用 WinUI 构建的现代命令行应用程序!
注意
要了解 Windows Terminal 团队如何使用 WinUI 创建应用程序,请查看这篇博客文章:devblogs.microsoft.com/commandline/building-windows-terminal-with-winui/。
现在我们来看一下步骤:
-
你已经安装了应用程序安装器预览版,因此请打开命令提示符。
-
使用以下命令测试 WinGet:
search command to find the application we want to install, as follows:winget search windowsterminal
You will see two results, as illustrated here:

图 14.9 – 使用 WinGet 进行搜索
注意
在继续之前,你可能需要同意 Microsoft Store 的交易条款。
-
要获取有关包的更多信息,请使用
show命令,如下所示:winget show Microsoft.WindowsTerminal这将返回应用程序清单中的以下信息:

图 14.10 – 查看 WinGet 包的清单信息
-
然后,输入以下命令来安装 Windows Terminal:
Microsoft.PowerToys (you can find out more about Microsoft PowerToys at https://learn.microsoft.com/windows/powertoys/).
这就是使用 WinGet 的全部内容。因为它是一个命令行工具,你可以构建脚本,安装所有需要的软件,以便在新电脑或虚拟机(VM)上运行。在 Microsoft Learn 上获取有关使用 WinGet 进行脚本编写的更多信息:learn.microsoft.com/windows/package-manager/winget/#scripting-winget。
让我们继续到最后一个部分,我们将学习如何在 Microsoft Store 中分发应用程序。
通过 Microsoft Store 分发应用程序
我们已经看到如何通过可以侧载的包将 WinUI 应用程序传递给用户,以及如何使用 WinGet。对于 Windows 开发者来说,还有其他几个分发渠道可用——Microsoft Intune用于企业应用程序分发,以及Microsoft Store用于消费类应用程序。
对Microsoft Endpoint Configuration Manager和 Intune 的深入探讨超出了本书的范围,但如果您想了解如何通过它们分发业务线(LOB)应用程序,您可以阅读这个 Microsoft Learn 主题:learn.microsoft.com/windows/apps/publish/distribute-lob-apps-to-enterprises。
Microsoft 商店是 Windows 用户的消费应用商店。商店接受免费和付费应用的提交。还可以配置额外的货币化选项,例如应用内购买、销售定价和带免费试用期的付费应用。
在本节中,我们将介绍提交免费应用程序到商店的基本知识。如果您想了解更多关于货币化您的应用的信息,您可以从这里开始:learn.microsoft.com/windows/apps/publish/publish-your-app/price-and-availability。
让我们看看如何将MyMediaCollection提交到 Microsoft 商店。
准备免费应用程序提交到 Microsoft 商店
在本节中,我们将使用 Visual Studio 准备和提交MyMediaCollection应用程序到 Microsoft 商店。Microsoft 商店是消费应用程序的主要分发渠道。在开始此过程之前,您需要在商店中拥有一个开发者账户。我们现在就来做,如下所示:
-
首先,访问 Microsoft 商店的注册页面(
developer.microsoft.com/microsoft-store/register/)并点击注册。 -
使用 Microsoft 账户登录,并选择您或您的公司所在的国家或地区。
注意
此账户将是商店账户的所有者,并且无法轻易更改。如果您为组织创建商店账户,建议使用一个与组织或公司中任何个人无关的单独的 Microsoft 账户。
-
选择您的账户类型为个人或公司。
-
输入您的发布者显示名称。这是用户在您的公共应用商店列表中看到的名称,因此请仔细选择。
-
输入您的联系信息——如果您的商店列表有任何问题或更新,Microsoft 将使用这些信息与您联系。点击下一步继续并输入付款信息。
-
支付一次性的商店注册费。Microsoft 在您首次在商店注册账户时收取少量费用。这有助于防止欺诈和恶意账户的创建。在大多数国家,费用大约为个人约19 美元(USD)和公司账户约99 美元(USD)。
各国的完整费用列表可在以下链接查看:
learn.microsoft.com/windows/apps/publish/partner-center/account-types-locations-and-fees#developer-account-and-app-submission-markets。 -
完成后,点击审查。
-
查看您的账户详情和应用开发者协议,然后点击完成以确认您的注册。此时将处理付款,并且您将收到确认电子邮件。
现在您已经在 Microsoft Store 上有了账户,我们可以继续提交我们的第一个应用,具体步骤如下:
-
返回 Visual Studio,在解决方案资源管理器中右键单击
MyMediaCollection项目。 -
选择包和发布 | 创建应用包。这个过程与创建用于旁加载的包的过程相同。
-
在选择分发方式屏幕上,选择以新应用名称在 Microsoft Store 下,然后点击下一步。
-
在选择应用名称屏幕上,确保选择与您的商店账户链接的 Microsoft 账户,并在预留新应用名称字段中输入您希望的应用名称。点击预留以检查名称是否可用。每个应用名称在 Microsoft Store 中必须是唯一的。应用名称将出现在您的应用列表中,如以下截图所示:

图 14.11 – 为 Microsoft Store 列表选择应用名称
-
在您的现有应用名称列表中选择应用名称,然后点击下一步。
-
您可以在选择和配置包屏幕上保留默认值,除非您想更新版本或解决方案配置映射。如以下截图所示,在屏幕底部点击创建:

图 14.12 – 选择和配置包屏幕
-
Visual Studio 将构建您的项目并为商店准备包。它还将对您的应用清单数据进行一些验证。如果在打包过程中遇到任何错误,请修复它们并再次尝试。如果您在审查提交清单(
learn.microsoft.com/windows/apps/publish/publish-your-app/create-app-submission#app-submission-checklist)后认为您的提交已准备好,您可以在完成创建包屏幕上勾选在 Windows 应用认证工具包验证后自动提交到 Microsoft Store复选框。这将使您的包在验证运行成功后提交到商店。 -
点击启动 Windows 应用程序认证工具包按钮。当认证工具包启动时,保留所有测试,然后点击下一步。测试可能需要几分钟才能完成,应用程序在验证过程中可能会启动几次。以下截图说明了验证****应用程序的进度:

图 14.13 – 微软商店应用程序验证过程
- 此过程应在查看最终报告页面上提供通过结果。完成后,您可以点击点击此处查看结果选项,以查看哪些测试通过或失败。在继续商店提交之前,应解决所有失败。这些测试将在微软商店的审批过程中运行,任何失败都可能导致提交被拒绝。有关测试和针对失败可以采取的纠正措施的详细信息,您可以查看此微软学习主题:
learn.microsoft.com/windows/uwp/debug-test-perf/windows-app-certification-kit-tests。
当您有一个准备提交到商店的验证应用程序时,您可以继续此过程。让我们通过微软合作伙伴中心网站来了解如何提交 WinUI 应用程序。
上传包到商店
在本节中,我们将通过合作伙伴中心仪表板将 Visual Studio 创建的包提交到微软商店。为此,请按照以下步骤操作:
-
首先,在以下 URL 使用您的微软账户登录到合作伙伴中心:
partner.microsoft.com/dashboard/home。 -
点击应用程序和游戏,您将被带到概览页面,以将应用程序和游戏提交到 Windows 和 Xbox。
-
在这里,您将看到您账户中预留的应用程序名称和提交的应用程序列表。如果您还没有预留应用程序名称,您可以点击新产品 | MSIX 或 PWA 应用程序。我将从我的应用程序中选择我的媒体收藏来提交此应用程序的初始版本。以下截图说明了您如何开始预留新名称:

图 14.14 – 为您的应用程序预留新名称
- 在您所选应用程序的应用程序概览页面,点击开始您的提交,如图下所示:

图 14.15 – 开始您的新的应用程序提交
-
通过选择定价和可用性部分开始提交。
-
我们将在这个部分保留所有默认设置,除了基础价格选项。这里必须进行选择。请选择免费选项或从列表中选择一个基础价格。完成后,选择保存草稿。
-
然后,选择属性部分。选择一个类别和子类别(如果需要),并输入您的支持信息数据。在显示模式、产品声明和系统要求部分输入您应用的任何相关数据,然后点击保存。
属性部分可以在以下屏幕截图中看到:

图 14.16 – 应用提交的属性部分
-
在年龄分级页面上完成问卷并点击预览分级。此页面根据收集或与其他用户交换的数据确定您的应用是否应限制在特定年龄组。如果生成后一切看起来都很好,点击保存,然后点击继续。
-
选择
.msixupload或.msix文件并将其上传到提交网站。选择兼容的设备系列并点击保存。 -
然后,在商店列表部分下选择一种语言。支持多种语言并在此处指定它们会使您的应用在不同国家安装的可能性更大。添加应用描述和至少一张应用截图。其余字段是可选的,但您完成的越多,客户找到您的应用就越容易,他们尝试它的可能性也越大。完成所有内容后,点击保存。
以下屏幕截图中显示了某些示例语言选项:

图 14.17 – 选择应用提交的语言
-
完成提交选项页面是可选的。默认情况下,您的应用在通过认证后将被立即发布。我将选择在我选择现在发布之前不发布此提交,因为我希望在做出一些额外更改之前不希望此应用在商店中可用。
-
最后,点击提交到商店。您的应用将提交进行认证。如果通过认证,当您在提交选项页面上指定时,它将在 Microsoft Store 中可用。如果您的应用未通过验证,您将收到一个问题列表,您需要在尝试另一次提交之前解决这些问题。
这些是将新应用提交到 Microsoft Store 的基本步骤。有关更详细的情况,以及有关更新和附加信息的说明,您可以查看 Microsoft Learn 的应用提交文档:learn.microsoft.com/windows/apps/publish/publish-your-app/create-app-submission。
现在,我们将介绍如何在 Windows 中使用 MSIX 侧载应用。
使用 MSIX 旁路加载 WinUI 应用
在本节中,我们将为 WinUI 项目创建一个 MSIX 包,并学习如何在 Windows 10 上旁路加载它。当您旁路加载一个应用时,您可以直接使用 MSIX UI 或 PowerShell 命令来安装它。这种安装方法非常重要,因为企业经常用它来内部分发应用。
注意
您也可以创建一个自包含的应用程序包,并使用 Xcopy 部署进行分发,但这种方法有一些性能缺点,我们已在本章前面讨论过。
我们将首先创建一个用于旁路加载的包。
创建用于旁路加载的 MSIX 包
在本节中,我们将使用 Visual Studio 创建一个 WinUI 项目的包。您可以从打开一个现有的 WinUI 项目或创建一个新的、空的开始。我已创建了一个名为ProjectTracker的新项目。按照以下步骤操作:
-
首先,在解决方案资源管理器中右键单击项目,然后选择打包和发布 | 创建 应用包。
-
在创建应用包窗口的选择分发方法屏幕上,保持旁路加载单选按钮和启用自动更新复选框选中状态。点击下一步。
-
在下一页上,选择签名方法。选择是,选择一个证书,然后点击创建按钮。在这里,您将创建一个自签名证书。使用自签名证书,任何安装应用的用户都需要信任您的包并从 MSIX 包中导入证书。我们将在下一节中解释此过程,当我们在旁路加载包时。输入证书的名称和密码,如下截图所示:

图 14.18 – 为包创建自签名证书
-
选择信任以信任证书,然后点击下一步。
-
在选择和配置包页面上保留默认设置。点击下一步。
-
输入安装程序路径。这可以是一个本地文件路径或网络位置。点击创建。您的项目将编译,包将在指定位置创建。
现在我们已经有了项目的 MSIX 包,我们准备旁路加载它。让我们一步步来走这个过程。
旁路加载 MSIX 包
在本节中,我们将学习如何使用 MSIX 旁路加载 WinUI 应用。在前一节中,我们已创建了一个新的 MSIX 包。导航到项目文件夹内创建包的文件夹,并检查包文件夹中的文件——在我的例子中,文件名为ProjectTracker_1.0.0.0_x64_Test。以下截图展示了这一过程:

图 14.19 – 查看包文件
现在,让我们看看旁路加载应用程序的步骤:
-
要在另一台 Windows 设备上安装此包,首先将
ProjectTracker_1.0.0.0_x64.msix` 文件复制到机器上。此文件包含安装应用程序所需的所有信息和文件。 -
然后,我们需要安装用于签名包的自签名证书。您可以通过运行
Install.ps1PowerShell 脚本来安装它,但如果您在同一台机器上安装,您也可以在创建时安装它。我们将通过右键单击 MSIX 文件并选择 属性 来安装它。 -
点击 数字签名 选项卡,并在 签名列表 框中选中证书,如图所示以下屏幕截图:

图 14.20 – 在 MSIX 包属性中选择证书
-
点击 详细信息 以打开 数字签名 详细信息 屏幕。
-
点击 查看证书 按钮。在打开的 证书 页面上,点击 安装证书。
-
在完成 证书导入向导 时,将证书导入到本地计算机,并在 证书存储 页上选择 将所有证书放置在以下存储中,如图所示以下屏幕截图:

图 14.21 – 导入包证书
-
点击 浏览,然后选择 受信任的根证书颁发机构 文件夹。在对话框中点击 确定,并在向导中点击 下一步。
-
点击 完成 后,证书将被导入。如果一切顺利,您将收到一条消息,表明证书已成功导入。您可以关闭 属性 页面并继续安装包。
-
现在,双击
MSIX包文件。安装程序将打开一个窗口,显示应用程序的一些清单信息。点击 安装,并选中 准备就绪时启动 复选框,如图所示:

图 14.22 – 从其 MSIX 包安装受信任的应用程序
应用程序将安装并启动,您就可以开始了。请注意,由于我们已将证书导入到 受信任的根证书颁发机构 文件夹,因此包是受信任的。
如果目标机器上已经信任了 MSIX 证书,则可以使用 PowerShell 自动化此安装。使用 Add-AppPackage 命令从 PowerShell 提示符安装 MSIX 包或 MSIX 打包,如图所示以下命令:
Add-AppPackage -path c:\Installers\ProjectTracker\ProjectTracker_1.0.0.0_x64.msix
如果你有多份软件包要分发,你可以创建一个自定义 PowerShell 脚本来遍历给定文件夹中的所有 MSIX 软件包。有关 MSIX 的 PowerShell 脚本信息,请参阅以下 Microsoft Learn 主题:learn.microsoft.com/windows/msix/desktop/powershell-msix-cmdlets。
让我们总结并回顾一下本章学到的内容。
摘要
在本章中,我们回顾了将 WinUI 应用程序提供给消费者的各种方法。我们学习了 MSIX 软件包的基础知识以及如何创建软件包以侧载我们的应用程序。我们还介绍了在 Microsoft Partner Center 上创建账户以在 Microsoft Store 上创建应用程序提交的过程。
然后,我们验证并提交了一个 MSIX 应用程序包到商店。最后,我们学习了如何手动侧载 MSIX 软件包以及如何利用 PowerShell 自动化侧载过程。这些概念将帮助你在准备创建自己的 WinUI 应用程序用于企业或消费者使用时。
这是本书的最后一章。我希望每一章中涵盖的概念能帮助你成功实现成为 WinUI 应用程序开发者的目标。
问题
-
在 MSIX 之前,有哪些应用程序安装格式?
-
MSIX 是否仅适用于 UWP 和 WinUI 应用程序?
-
在一个 WinUI 项目中,哪个文件包含应用程序清单数据?
-
使用 Windows 包管理器安装软件包时,使用哪个命令?
-
你如何使用 WinGet 使你的应用程序可用?
-
提交应用程序到 Microsoft Store 的在线门户名称是什么?
-
在 Microsoft Store 列表中需要多少张截图?


浙公网安备 33010602011771号