构建桌面和移动应用程序
由于本书涉及使用 C#和.NET 进行现代跨平台开发,因此不包括使用 Windows Forms、Windows Presentation Foundation (WPF)或 WinUI 3 应用程序构建桌面应用程序的内容,因为它们仅限于 Windows。
如果您需要为 Windows 构建应用程序,那么以下链接将会对您有所帮助:
- 官方文档,帮助您开始为 Windows 构建应用程序: https://learn.microsoft.com/zh-cn/windows/apps/get-started/
- WPF 死了吗?: https://avaloniaui.net/Blog/is-wpf-dead
- WPF 在 2024 年与 WinUI、MAUI 相比有多受欢迎?: https://twitter.com/DrAndrewBT/status/1759557538805108860
- 在 64 位世界中的 WinForms - 我们的未来战略: https://devblogs.microsoft.com/dotnet/winforms-designer-64-bit-path-forward/
移动应用平台
有两个主要的移动平台,苹果的 iOS 和谷歌的 Android,每个平台都有其自己的编程语言和平台 API。还有两个主要的桌面平台,苹果的 macOS 和微软的 Windows,每个平台也都有其自己的编程语言和平台 API,如下所示:
- iOS:Objective C 或 Swift 和 UIKit
- 安卓:Java 或 Kotlin 和安卓 API
- macOS:Objective C 或 Swift 和 AppKit 或 Catalyst
- Windows:C、C++或许多其他语言,以及 Win32 API 或 Windows 应用程序 SDK
由于需要学习如此多的组合来进行原生移动开发,如果有一种技术能够针对所有这些移动平台,那将是非常有用的。
.NET MAUI
跨平台的移动和桌面应用可以一次为 .NET 多平台应用用户界面 (MAUI) 平台构建,然后可以在许多移动和桌面平台上运行。
.NET MAUI 通过共享用户界面组件和业务逻辑,使得开发这些应用变得简单。它们可以针对与控制台应用、网站和 Web 服务相同的 .NET API。应用将在移动设备上由 Mono 运行时执行,在桌面设备上由 CoreCLR 运行时执行。与普通的 .NET CoreCLR 运行时相比,Mono 运行时对移动设备进行了更好的优化。Blazor WebAssembly 也使用 Mono 运行时,因为它像移动应用一样,资源受限。
这些应用可以独立存在,但它们通常会调用服务,以提供跨越所有计算设备的体验,从服务器和笔记本电脑到手机和游戏系统。
我在我的伴侣书《使用 .NET 8 的应用程序和服务》中介绍了 .NET MAUI,Packt 还有许多其他书籍更深入地探讨 .NET MAUI,因此如果您认真想学习 MAUI,请查看以下 Packt 书籍:
- .NET MAUI 跨平台应用程序开发: https://www.packtpub.com/en-us/product/net-maui-cross-platform-application-development-9781835080597
- .NET MAUI 中的 MVVM 模式: https://www.packtpub.com/en-us/product/the-mvvm-pattern-in-net-maui-9781805125006
- .NET MAUI 项目: https://www.packtpub.com/zh-cn/product/net-maui-projects-9781837634910
在微软创建 .NET MAUI 之前,第三方创建了开源项目,以使 .NET 开发人员能够使用 XAML 构建跨平台应用程序,这些项目名为 Uno 和 Avalonia。
警告!我自己没有在任何实际项目中尝试过 Uno 或 Avalonia,因此无法为它们提供基于证据的推荐。我在本书中提到它们只是为了让您了解它们。
Uno platform
Uno 是一个“用于快速构建单一代码库的原生移动、网页、桌面和嵌入式应用的开源平台”,正如他们的网站所述,网址为:https://platform.uno/.
开发人员可以在原生移动、网页和桌面之间重用 99%的业务逻辑和用户界面层。
Uno 平台使用 Xamarin 原生平台,但不使用 Xamarin.Forms。对于 WebAssembly,Uno 使用 Mono-WASM 运行时。对于 Linux,Uno 使用 Skia 在画布上绘制用户界面。
阿瓦隆
Avalonia 是一个“用于从单一.NET 代码库构建美丽的跨平台应用程序的开源框架”,正如他们网站上所述,网址为:https://avaloniaui.net/。
您可以将 Avalonia 视为 WPF 的精神继承者。熟悉 WPF 的 WPF、Silverlight 和 UWP 开发人员可以继续受益于他们多年来积累的知识和技能。
它被 JetBrains 用于现代化他们基于 WPF 的工具,并使其跨平台。
Avalonia 扩展为 Visual Studio 和与 Rider 的深度集成使开发变得更简单、更高效。
项目结构化
你应该如何构建你的项目?到目前为止,我们主要构建了小型的独立控制台应用程序,以说明语言或库的特性,偶尔会有类库和单元测试项目来支持它们。在本书的其余部分,我们将构建多个使用不同技术的项目,这些项目协同工作以提供一个单一的解决方案。
对于大型复杂的解决方案,浏览所有代码可能会很困难。因此,构建项目的主要原因是为了更容易找到组件。为您的解决方案起一个反映应用程序或解决方案的整体名称是很好的。
我们将为一个名为 Northwind 的虚构公司构建多个项目。我们将把解决方案命名为 ModernWeb ,并使用名称 Northwind 作为所有项目名称的前缀。
有很多方法可以构建和命名项目和解决方案,例如,使用文件夹层次结构以及命名约定。如果您在团队中工作,请确保您了解您的团队是如何做的。
在解决方案中构建项目
在解决方案中为项目制定命名约定是很好的,这样任何开发人员都可以立即了解每个项目的功能。一个常见的选择是使用项目类型,例如,类库、控制台应用程序、网站等。
由于您可能希望同时运行多个网络项目,并且它们将托管在本地网络服务器上,我们需要通过为它们的端点分配不同的端口号来区分每个项目,包括 HTTP 和 HTTPS。
常用的本地端口号是 5000 用于 HTTP 和 5001 用于 HTTPS。我们将使用 5
因此,我们将使用以下项目名称和端口号,如表 12.2 所示:
名称 | 端口 | 描述 |
Northwind.Common | 不适用 | 一个用于多个项目的公共类型的类库项目,如接口、枚举、类、记录和结构体。 |
Northwind.EntityModels | 不适用 | 一个用于通用 EF Core 实体模型的类库项目。实体模型通常在服务器端和客户端都使用,因此最好将对特定数据库提供程序的依赖分开。 |
Northwind.DataContext | 不适用 | A class library project for the EF Core database context, with dependencies on specific database providers. |
Northwind.UnitTests | 不适用 | 一个用于该解决方案的 xUnit 测试项目。 |
Northwind.Web | http 5130 和 https 5131 | 一个用于简单网站的 ASP.NET Core 项目,使用静态 HTML 文件和 Blazor 静态服务器端渲染(SSR)的混合。 |
Northwind.Blazor | http 5140 和 https 5141 | 一个 ASP.NET Core Blazor 项目。 |
Northwind.WebApi | http 5150 和 https 5151 | 一个用于 Web API 的 ASP.NET Core 项目,也称为 HTTP 服务。由于它可以使用任何 JavaScript 库或 Blazor 与服务进行交互,因此是与网站集成的良好选择。 |
表 12.2:各种项目类型的示例项目名称
中央包管理
在本书的所有先前项目中,如果我们需要引用一个 NuGet 包,我们会直接在项目文件中包含对包名称和版本的引用。
中央包管理(CPM)是一项功能,简化了在解决方案中多个项目之间的 NuGet 包版本管理。这对于拥有多个项目的大型解决方案特别有用,因为单独管理包版本可能变得繁琐且容易出错。
CPM 的关键特性和优势包括:
- 集中控制:CPM 允许您在一个文件中定义包版本,通常是 Directory.Packages.props ,该文件放置在您解决方案的根目录中。此文件集中管理您解决方案中所有项目使用的 NuGet 包的版本信息。
- 一致性:它确保多个项目之间的包版本一致。通过拥有一个包版本的单一真实来源,CPM 消除了不同项目指定相同包的不同版本时可能出现的差异。
- 简化更新:在大型解决方案中更新包版本变得简单明了。您只需在中央文件中更新版本,所有引用该包的项目将自动使用更新后的版本。这大大减少了维护开销。
- 减少冗余:它消除了在单个项目文件中指定软件包版本的需要( .csproj )。这使得项目文件更简洁,更易于管理,因为它们不再包含重复的版本信息。
让我们为本书接下来的章节设置一个将要使用的 CPM 解决方案:
- 在 cs13net9 文件夹中,创建一个名为 ModernWeb 的新文件夹。
- 在 ModernWeb 文件夹中,创建一个名为 Directory.Packages.props 的新文件。
- 在 Directory.Packages.props 中,修改其内容,如下所示的标记:
true
警告!
对于我们在包含此文件的文件夹下添加的任何项目,我们可以引用这些包而无需明确指定版本,如下所示的标记所示:
您应定期检查和更新 Directory.Packages.props 文件中的软件包版本,以确保您使用的是最新的稳定版本,其中包含重要的错误修复和性能改进。
我建议您在日历中设置每个月的第二个星期三的月度事件。这将在每个月的第二个星期二之后发生,第二个星期二是补丁星期二,微软会在这一天发布 .NET 及相关软件包的错误修复和补丁。
例如,在 2024 年 12 月中旬,可能会有新版本,因此您可以访问所有包的 NuGet 页面,然后在必要时更新版本,如下所示:
在更新软件包版本之前,请检查软件包的发布说明中是否有任何重大更改。更新后请彻底测试您的解决方案以确保兼容性。
教育您的团队,并记录 Directory.Packages.props 文件的目的和使用方法,以确保每个人都理解如何集中管理软件包版本。
您可以通过在
这在新版本引入回归错误时可能会很有用。
更多信息:您可以通过以下链接了解有关 CPM 的更多信息:https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management。
为本书其余部分构建实体模型
网站和网络服务通常需要与关系数据库或其他数据存储中的数据进行交互。在本节中,我们将为存储在 SQL Server 或 SQLite 中的 Northwind 数据库定义一个实体数据模型。它将在我们后续章节中创建的大多数应用程序中使用。
创建 Northwind 数据库
创建 Northwind 数据库的脚本文件对于 SQLite 和 SQL Server 是不同的。SQL Server 的脚本创建了 13 个表以及相关的视图和存储过程。SQLite 的脚本是一个简化版本,仅创建 10 个表,因为 SQLite 不支持那么多功能。本书中的主要项目只需要这 10 个表,因此您可以使用任一数据库完成本书中的所有任务。
SQL 脚本可以在以下链接找到:https://github.com/markjprice/cs13net9/tree/main/scripts/sql-scripts。
有多个 SQL 脚本可供选择,如下所述:
- Northwind4Sqlite.sql script: To use SQLite on a local Windows, macOS, or Linux computer. This script could probably also be used for other SQL systems, like PostgreSQL or MySQL, but has not been tested for use with those!
- Northwind4SqlServer.sql 脚本:在本地 Windows 计算机上使用 SQL Server。该脚本检查 Northwind 数据库是否已经存在,如果数据库存在,则删除(即删除)该数据库,然后重新创建它。
- Northwind4AzureSqlDatabaseCloud.sql 脚本:在 Azure 云中使用与 Azure SQL 数据库资源创建的 SQL Server。这些资源只要存在就会产生费用!该脚本不会删除或创建 Northwind 数据库,因为您应该使用 Azure 门户用户界面手动创建 Northwind 数据库。
- Northwind4AzureSqlEdgeDocker.sql 脚本:在本地计算机上使用 Docker 中的 SQL Server。该脚本创建 Northwind 数据库。如果数据库已经存在,则不会删除它,因为 Docker 容器应该是空的,每次都会启动一个新的容器。
安装 SQLite 的说明可以在第 10 章《使用 Entity Framework Core 处理数据》中找到。在该章节中,您还会找到安装 dotnet-ef 工具的说明,您将使用该工具从现有数据库中生成实体模型。
在您的本地 Windows 计算机上安装 SQL Server 开发者版(免费)的说明可以在本书的 GitHub 仓库中找到,链接如下:https://github.com/markjprice/cs13net9/blob/main/docs/sql-server/README.md。
在以下链接中可以找到在 Windows、macOS 或 Linux 上通过 Docker 设置 Azure SQL Edge 的说明,链接为本书的 GitHub 存储库:https://github.com/markjprice/cs13net9/blob/main/docs/sql-server/sql-edge.md。
使用 SQLite 创建实体模型的类库
您现在将在类库中定义实体数据模型,以便它们可以在其他类型的项目中重用,包括客户端应用程序模型。
良好实践:您应该为实体数据模型创建一个单独的类库项目,而不是与数据上下文的类库合并。这使得在后端 Web 服务器和前端桌面、移动设备及 Blazor 客户端之间更容易共享实体模型,并且只有后端需要引用数据上下文类库。
我们将使用 EF Core 命令行工具自动生成一些实体模型:
- 使用您首选的代码编辑器创建一个新项目和解决方案,如下列表所示:项目模板:类库 / classlib项目文件和文件夹: Northwind.EntityModels.Sqlite解决方案文件和文件夹: ModernWeb
- 在 Northwind.EntityModels.Sqlite 项目中,添加 SQLite 数据库提供程序和 EF Core 设计时支持的包引用,如下所示:
all
runtime; build; native; contentfiles; analyzers; buildtransitive
- 删除 Class1.cs 文件。
- 构建 Northwind.EntityModels.Sqlite 项目以恢复包。
- 将 Northwind4Sqlite.sql 文件复制到 ModernWeb 解决方案文件夹中(不是项目文件夹!)。
- 在 ModernWeb 文件夹的命令提示符或终端中,输入命令以为 SQLite 创建 Northwind.db 文件,如以下命令所示:
sqlite3 Northwind.db -init Northwind4SQLite.sql
请耐心等待,因为此命令可能需要一些时间来创建数据库结构。
- 要退出 SQLite 命令模式,在 Windows 上按两次 Ctrl + C,或在 macOS 或 Linux 上按 Cmd + D。
- 在 ModernWeb 文件夹的命令提示符或终端中,输入命令以列出当前目录中的文件,如以下命令所示:
dir
您应该看到一个名为 Northwind.db 的新文件已被创建,如下输出所示:
Directory: C:\cs13net9\ModernWeb
Length Name
------ ----
Northwind.EntityModels.Sqlite
382 Directory.Packages.props
1193 ModernWeb.sln
557056 Northwind.db
480790 Northwind4SQLite.sql
更改到项目文件夹:
cd Northwind.EntityModels.Sqlite
在
Northwind.EntityModels.Sqlite 项目文件夹(包含 .csproj 项目文件的文件夹)的命令提示符或终端中,为所有表生成实体类模型,如以下命令所示:
dotnet ef dbcontext scaffold "Data Source=../Northwind.db" Microsoft.EntityFrameworkCore.Sqlite --namespace Northwind.EntityModels --data-annotations
请注意以下事项:
- 执行的命令: dbcontext scaffold
- 连接字符串指的是解决方案文件夹中的数据库文件,该文件夹位于当前项目文件夹的上一级: "Data Source=../Northwind.db"
- 数据库提供者: Microsoft.EntityFrameworkCore.Sqlite
- 命名空间: --namespace Northwind.EntityModels
- 使用数据注解以及流式 API: --data-annotations
- 警告! dotnet-ef 命令必须全部在一行中输入,并且在包含项目的文件夹中;否则,您将看到以下错误: No project was found. Change the current working directory or use the --project option. 请记住,所有命令行都可以在以下链接找到并复制:https://github.com/markjprice/cs13net9/blob/main/docs/command-lines.md。
- 如果您使用 SQLite,那么您将看到关于表列与实体类模型中的属性之间不兼容类型映射的警告。例如, The column 'BirthDate' on table 'Employees' should map to a property of type 'DateOnly', but its values are in an incompatible format. Using a different type 。这是因为 SQLite 使用动态类型。我们将在下一节中解决这些问题。
使用 SQLite 创建数据库上下文的类库
您现在将定义一个数据库上下文类库:
- 将新项目添加到解决方案中,如下列表所定义:项目模板:类库 / classlib项目文件和文件夹: Northwind.DataContext.Sqlite解决方案文件和文件夹: ModernWeb
- 在 Northwind.DataContext.Sqlite 项目中,静态和全局导入 Console 类,添加对 SQLite 的 EF Core 数据提供程序的包引用,并添加对 Northwind.EntityModels.Sqlite 项目的项目引用,如下标记所示:
警告!项目文件中的项目引用路径不应有换行。
- 在 Northwind.DataContext.Sqlite 项目中,删除 Class1.cs 文件。
- 构建 Northwind.DataContext.Sqlite 项目以恢复包。
- 在 Northwind.DataContext.Sqlite 项目中,添加一个名为 NorthwindContextLogger.cs 的类。
- 修改其内容以定义一个静态方法,命名为 WriteLine ,该方法将一个字符串附加到桌面上名为 book-logs 的文件夹中名为 northwindlog-
.txt 的文本文件的末尾,如以下代码所示:
using static System.Environment;
namespace Northwind.EntityModels;
public class NorthwindContextLogger
{
public static void WriteLine(string message)
{
string folder = Path.Combine(GetFolderPath(
SpecialFolder.DesktopDirectory), "book-logs");
if (!Directory.Exists(folder))
Directory.CreateDirectory(folder);
string dateTimeStamp = DateTime.Now.ToString(
"yyyyMMdd_HHmmss");
string path = Path.Combine(folder,
$"northwindlog-{dateTimeStamp}.txt");
StreamWriter textFile = File.AppendText(path);
textFile.WriteLine(message);
textFile.Close();
}
}
- 将 NorthwindContext.cs 文件从 Northwind.EntityModels.Sqlite 项目/文件夹移动到 Northwind.DataContext.Sqlite 项目/文件夹。
在 Visual Studio 解决方案资源管理器中,如果您在项目之间拖放文件,它将被复制。如果在拖放时按住 Shift 键,它将被移动。在 VS Code EXPLORER 中,如果您在项目之间拖放文件,它将被移动。如果在拖放时按住 Ctrl 键,它将被复制。
- 在 NorthwindContext.cs 中,请注意第二个构造函数可以将 options 作为参数传递,这使我们能够在任何项目中覆盖默认的数据库连接字符串,例如需要与 Northwind 数据库一起工作的网页,如以下代码所示
public NorthwindContext(DbContextOptions options)
: base(options)
{
}
在 NorthwindContext.cs 中,在 OnConfiguring 方法中,移除编译器的#warning 关于连接字符串的警告,然后添加语句以检查当前目录的结尾,以适应在 Visual Studio 中运行与在 VS Code 的命令提示符中运行的情况,如以下代码所示:
protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
string database = "Northwind.db";
string dir = Environment.CurrentDirectory;
string path = string.Empty;
if (dir.EndsWith("net9.0"))
{
// In the \bin\\net9.0 directory.
path = Path.Combine("..", "..", "..", "..", database);
}
else
{
// In the directory.
path = Path.Combine("..", database);
}
path = Path.GetFullPath(path); // Convert to absolute path.
try
{
NorthwindContextLogger.WriteLine($"Database path: {path}");
}
catch (Exception ex)
{
WriteLine(ex.Message);
}
if (!File.Exists(path))
{
throw new FileNotFoundException(
message: $"{path} not found.", fileName: path);
}
optionsBuilder.UseSqlite($"Data Source={path}");
optionsBuilder.LogTo(NorthwindContextLogger.WriteLine,
new[] { Microsoft.EntityFrameworkCore
.Diagnostics.RelationalEventId.CommandExecuting });
}
}
抛出异常是重要的,因为如果数据库文件缺失,SQLite 数据库提供者将创建一个空的数据库文件,因此如果你测试连接,它是有效的。但是如果你查询它,你将看到与缺失表相关的异常,因为它没有任何表!在将相对路径转换为绝对路径后,你可以在调试时设置一个断点,以更容易地查看数据库文件预期的位置,或者添加一条语句来记录该路径。