Blazor-WebAssembly-示例-全-

Blazor WebAssembly 示例(全)

原文:Blazor WebAssembly by Example

协议:CC BY-NC-SA 4.0

零、前言

Blazor WebAssembly 是一个框架,建立在流行且健壮的 ASP.NET 框架之上,它允许您构建在客户端使用 C#而不是 JavaScript 的单页 web 应用。Blazor WebAssembly 不依赖插件或附加组件。它只要求浏览器支持 WebAssembly——所有现代浏览器都支持它。

在这本书里,你将完成实用的项目,这些项目将教会你 Blazor WebAssembly 框架的基础。每一章都包括一个独立的项目和详细的逐步说明。每个项目都旨在突出一个或多个与 Blazor WebAssembly 相关的重要概念。到本书结束时,您将会有使用 SQL Server 后端构建简单的独立 web 应用和托管 web 应用的经验。

这本书是给谁的

这本书是为经验丰富的网络开发人员编写的,他们厌倦了不断学习最新的新 JavaScript 框架,并希望利用他们的经验.NET 和 C#来构建可以在任何地方运行的 web 应用。

这本书是为任何想通过强调实践而不是理论来快速学习 Blazor WebAssembly 的人准备的。它使用完整的、易于遵循的分步示例项目来教您使用 Blazor WebAssembly 框架开发 web 应用所需的概念。

您不需要成为专业的开发人员就能从本书的项目中受益,但您确实需要一些 C#和 HTML 方面的经验。

这本书涵盖了什么

第 1 章【Blazor WebAssembly 简介】介绍了 Blazor web assembly 框架。它解释了使用 Blazor 框架的好处,并描述了两种托管模型之间的区别:Blazor 服务器和 Blazor WebAssembly。在强调了使用 Blazor WebAssembly 框架的优势之后,讨论了 WebAssembly 的目标和支持选项。最后,它将指导您完成设置计算机的过程,以完成本书中的项目。到本章结束时,您将能够继续阅读本书的任何其他章节。

第二章 【构建你的第一个 Blazor WebAssembly 应用】通过创建一个简单的项目来介绍 Razor 组件。本章分为三节。第一部分解释 Razor 组件、路由和 Razor 语法。第二部分通过使用微软提供的 Blazor WebAssembly App 项目模板,逐步引导您完成创建第一个 Blazor WebAssembly 应用的过程。最后一节将逐步引导您创建自己的自定义 Blazor WebAssembly 项目模板。第 3-7 章中的项目将使用该自定义项目模板。到本章结束时,您将能够创建一个空的 Blazor WebAssembly 项目。

第 3 章 【使用模板化组件构建模态对话框】通过创建模态对话框组件介绍了模板化组件。本章分为三节。第一节讲解RenderFragment参数、EventCallback参数、CSS 隔离。第二部分将逐步引导您完成创建模态对话框组件的过程。最后一节将逐步引导您创建自己的 Razor 类库,并将模态对话框组件移动到其中。到本章结束时,您将能够创建一个模态对话框组件,并通过 Razor 类库与多个项目共享它。

第 4 章 【使用 JavaScript 互用性(JS Interop) 构建本地存储服务】介绍了如何通过创建本地存储服务将 JavaScript 与 Blazor WebAssembly 结合使用。本章分为两节。第一部分解释了仍然需要偶尔使用 JavaScript 的原因,以及如何从. NET 调用 JavaScript 函数。为了完整起见,还介绍了如何从 JavaScript 调用. NET 方法。最后,介绍了项目使用的网络存储应用编程接口。在最后一节中,它将逐步引导您完成创建和测试服务的过程,该服务将读写浏览器的本地存储。到本章结束时,您将能够通过使用 JS Interop 从 Blazor WebAssembly 应用调用 JavaScript 函数来创建本地存储服务。

第五章 将天气应用构建为渐进式网络应用(PWA) ,通过创建一个简单的天气网络应用来介绍渐进式网络应用。本章分为两节。第一部分解释什么是 PWA 以及如何创建 PWA。它包括清单文件和服务人员。此外,它还描述了如何使用本章项目所需的缓存存储应用编程接口、地理定位应用编程接口和开放天气一次调用应用编程接口。第二部分通过添加徽标、清单文件和服务人员,逐步引导您完成创建 5 天天气预报应用并将其转换为 PWA 的过程。最后,它向您展示了如何安装和卸载 PWA。到本章结束时,您将能够通过添加徽标、清单文件和服务人员,将 Blazor WebAssembly 应用转换为 PWA。

第 6 章 ,使用应用状态构建购物车,通过创建购物车 web app 介绍应用状态。本章分为两节。第一部分解释应用状态和依赖注入。最后一部分将逐步引导您完成创建购物车应用的过程。为了维护应用中的状态,您将创建一个服务,该服务将在 DI 容器中注册并注入到您的组件中。到本章结束时,您将能够使用 DI 来维护 Blazor WebAssembly 应用中的应用状态。

第 7 章 【使用事件构建看板】通过创建看板 web 应用介绍了事件处理。本章分为两节。第一部分讨论事件处理、任意参数和属性规划。最后一部分将逐步引导您创建看板板应用,该应用使用DragEventArgs类使您能够在拖放区之间拖放任务。到本章结束时,您将能够在您的 Blazor WebAssembly 应用中处理事件,并且可以轻松使用属性拆分和任意参数。

第 8 章 【使用 ASP.NET Web API 构建任务管理器】通过创建任务管理器 Web 应用,介绍托管 Blazor WebAssembly 应用。这是使用 SQL Server 的第一章。它分为两个部分。第一部分描述了托管的 Blazor WebAssembly 应用的组件。它还解释了如何使用 HttpClient 服务和各种 JSON 助手方法来操作数据。最后一节将逐步引导您完成创建任务管理器应用的过程,该应用将其数据存储在 SQL Server 数据库中。您将使用实体框架创建一个带有动作的应用编程接口控制器。到本章结束时,您将能够创建一个托管的 Blazor WebAssembly 应用,该应用使用 ASP.NET Web API 来更新 SQL Server 数据库中的数据。

第 9 章 ,使用编辑表单组件构建费用跟踪器,通过创建费用跟踪器网络应用介绍了EditForm组件。本章使用的是 SQL Server。它分为两个部分。第一部分介绍EditForm组件、内置输入组件和内置验证组件。最后一部分将逐步引导您创建费用跟踪应用,该应用使用EditForm组件和一些内置组件来添加和编辑存储在 SQL Server 数据库中的费用。到本章结束时,您将能够结合内置组件使用EditForm组件来输入和验证存储在 SQL Server 数据库中的数据。

为了充分利用这本书

我们建议您阅读本书的前两章,了解如何设置您的计算机以及如何使用空的 Blazor WebAssembly 项目模板。之后,您可以按任何顺序完成其余章节。当你继续阅读这本书时,每一章中的项目会变得更加复杂。最后两章需要一个 SQL Server 数据库来完成项目。

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

这本书假设你是一个有经验的网络开发者。你应该对 C#和 HTML 有一些经验。此外,所有的项目都使用 Bootstrap 4 作为 CSS 框架。如果您从未使用过 Bootstrap 4,我们建议您在继续之前先熟悉一下,网址为https://getbootstrap . com/docs/4.6/入门/简介

有一些项目使用了 JavaScript 和 CSS,还有两个项目使用了 Entity Framework,但是所有的代码都在书中提供了。

下载示例代码文件

可以从https://GitHub . com/packt publishing/Blazor-web assembly by Example下载本书的示例代码文件。如果代码有更新,它将在现有的 GitHub 存储库中更新。

我们还有来自 https://github.com/PacktPublishing/丰富的书籍和视频目录的其他代码包。看看他们!

行动中的代码

本书的《行动中的代码》视频可在(https://bit.ly/3f1rJ0R)观看。

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:https://static . packt-cdn . com/downloads/9781800567511 _ color images . pdf

使用的约定

本书通篇使用了许多文本约定。

Code in text:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“将DeleteProduct方法添加到@code块中。”

代码块设置如下:

private void DeleteProduct(Product product)
{
    cart.Remove(product);
    total -= product.Price;
}

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

public class CartService : ICartService
{
 public IList<Product> Cart { get; private set; }
 public int Total { get; set; }
 public event Action OnChange;
}

任何命令行输入或输出都编写如下:

Add-Migration Init
Update-Database

粗体:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如,菜单或对话框中的单词像这样出现在文本中。这里有一个例子:“从构建菜单中,选择构建解决方案选项。”

提示或重要注意事项

像这样出现。

取得联系

我们随时欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在留言主题中提及书名,并通过customercare@packtpub.com发邮件给我们。

勘误表:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata,选择您的图书,点击勘误表提交链接,并输入详细信息。

盗版:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请联系我们在copyright@packt.com与材料的链接。

如果你有兴趣成为一名作者:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问authors.packtpub.com

评论

请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家!

更多关于 Packt 的信息,请访问packt.com

一、介绍 BlazorWebAssembly

Blazor WebAssembly 是微软全新的单页应用(SPA) 框架,用于在上构建 web 应用.NET 框架。它使开发人员能够在客户端运行 C#代码。因此,我们现在可以在浏览器上使用 C#而不是被迫在浏览器上使用 JavaScript

在本章中,我们将为您准备使用 Blazor WebAssembly 开发 web 应用。我们将讨论两种不同的 Blazor 托管模型,并展示使用 Blazor WebAssembly 相对于 Blazor Server 的优势。最后,我们将指导您完成设置计算机的过程,以完成本书中的项目。

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

  • 使用 Blazor 框架的好处
  • 两种托管模式的区别
  • 什么是 WebAssembly
  • 设置您的电脑

使用 Blazor 框架的好处

使用 Blazor 框架有几个好处。首先,它是一个建立在微软强大基础上的自由开源框架.NET 框架。此外,它是一个 SPA 框架,使用 Razor 语法,可以使用微软的特殊工具进行开发。

.NET 框架

Blazor 建立在之上。 NET 框架。因为Blazor是建立在.NET 框架,任何熟悉的人.NET 框架可以使用 Blazor 框架快速变得高效。Blazor 框架利用了强大的生态系统.NET 库和 NuGet 包.NET 框架。此外,由于客户机和服务器代码都是用 C#编写的,它们可以共享代码和库,例如用于数据验证的应用逻辑。

Blazor 是开源的。由于 Blazor 是 ASP.NET 框架的一个特性,所以 Blazor 的所有源代码都可以在 GitHub 上作为 T2 拥有的存储库的一部分获得。网络基金会。.NET Foundation 是一个独立的非营利组织,旨在支持全球创新的、商业友好的开源生态系统.NET 平台。那个.NET 平台有一个强大的社区,有来自 3700 多家公司的 10 多万份投稿。

Blazor 是自由的。从.NET Framework 是免费的,这意味着 Blazor 也是免费的。使用 Blazor 没有任何费用或许可成本,包括商业用途。

SPA 框架

Blazor 框架是一个 SPA 框架。顾名思义,SPA 是一个由单个页面组成的网络应用。应用动态重写单个页面,而不是加载一个全新的页面来响应每次用户界面更新。目标是更快的过渡,让网络应用感觉更像一个原生应用。

渲染页面时,Blazor 会创建一个渲染树,它是页面上组件的图形。它类似于浏览器创建的文档对象模型 ( DOM )。然而,它是一个虚拟的 DOM。对用户界面的更新应用于虚拟 DOM,只有 DOM 和虚拟 DOM 之间的差异由浏览器更新。

Razor 语法

Blazor 框架的名字有一个有趣的起源故事。术语Blazor是单词浏览器和单词剃刀的组合。 Razor 是用来用 C#创建动态网页的ASP.NET视图引擎。Razor 是一种将 HTML 标记与 C#代码相结合的语法,旨在提高开发人员的工作效率。它允许开发人员在同一个文件中同时使用 HTML 标记和 C#。

Blazor 网络应用是使用剃刀组件构建的。Razor 组件是可重用的 UI 元素,包含 C#代码、标记和其他 Razor 组件。剃刀组件是 Blazor 框架的基本构件。有关剃须刀组件的更多信息,请参考 第 2 章构建您的第一个 Blazor WebAssembly 应用

重要说明

剃刀页面和 MVC 也使用剃刀语法。与呈现整个页面的 Razor Pages 和 MVC 不同,Razor Components 只呈现 DOM 更改。一种容易区分它们的方法是 Razor 组件使用RAZOR文件扩展名,而 Razor Pages 使用CSHTML文件扩展名。

牛逼的工装

可以使用微软 Visual Studio 或者微软 Visual Studio 代码来开发 Blazor WebAssembly 应用。微软 Visual Studio 是一个集成开发环境 ( IDE ),而微软 Visual Code 是一个轻量级但功能强大的编辑器。它们都是构建企业应用的不可思议的工具。此外,它们都是免费的,并且有在视窗、Linux 和苹果电脑上运行的版本。

使用 Blazor 框架开发网络应用有很多好处。既然是建立在成熟的基础上.NET 框架,它使开发人员能够使用他们已经掌握的技能,如 C#和工具,如 Visual Studio。此外,由于它是一个 SPA 框架,Blazor 网络应用感觉像本地应用。

托管模型

Blazor 有两种不同的托管模式。微软发布的第一个托管模型是 Blazor Server 模型。在这个托管模型中,web 应用在服务器上执行。微软发布的第二个托管模型,也是本书的主题,是 Blazor WebAssembly 模型。在这种托管模式下,web 应用在浏览器上执行。

每种托管模式都有自己的优缺点。然而,它们都使用相同的底层架构。因此,可以独立于宿主模型编写和测试代码。这两种托管模式的主要区别在于延迟、安全性、数据访问和离线支持。

Blazor 服务器

正如我们刚刚提到的,Blazor Server 托管模型是微软发布的第一个托管模型。它是作为.NET Core 3 于 2019 年 9 月发布。

下图说明了 Blazor 服务器托管模型:

Figure 1.1 – Blazor Server

图 1.1–Blazor 服务器

在这种托管模式下,web 应用在服务器上执行,只有对 UI 的更新才会发送到客户端的浏览器。浏览器被视为瘦客户端,所有处理都在服务器上进行。当使用 Blazor 服务器时,用户界面更新、事件处理和 JavaScript 调用都通过 ASP.NET Core信号器连接来处理。

重要说明

signal是一个软件库,允许网络服务器向浏览器推送实时通知。Blazor 服务器使用它向浏览器发送用户界面更新。

Blazor 服务器的优势

使用 Blazor 服务器与使用 Blazor 网络组件相比,有一些优势。然而,的关键优势是一切都发生在服务器上。由于 web 应用运行在服务器上,因此它可以访问服务器上的所有内容。因此,安全性和数据访问得到了简化。此外,由于一切都发生在服务器上,所以包含 web 应用代码的程序集(dll)仍保留在服务器上。

使用 Blazer Server 的另一个优点是,它可以在瘦客户端和不支持 WebAssembly 的旧浏览器(如 Internet Explorer)上运行。

最后,第一次使用使用 Blazor Server 的 web 应用的初始加载时间可能比使用 Blazor WebAssembly 的 web 应用要短得多,因为要下载的文件很少。

Blazor 服务器的缺点

与 Blazor WebAssembly 相比,Blazor 服务器托管模型有许多缺点,因为浏览器必须保持与服务器的持续连接。由于没有离线支持,每个用户交互都需要网络往返。所有这些往返的结果是,Blazor Server 网络应用比 Blazor WebAssembly 网络应用具有更高的延迟,并且会感觉迟钝。

小费

延迟是用户界面操作和用户界面更新之间的时间。

使用 Blazor Sever 的另一个缺点是每次用户界面更新都依赖 SignalR。微软对 SignalR 的支持一直在改善,但扩展起来可能很有挑战性。

最后,一个 Blazor Server 网络应用必须由一个ASP.NET Core服务器提供。

Blazor WebAssembly

Blazor WebAssembly 托管模型是微软最近发布的托管模型,也是本书的主题。 Blazor WebAssembly 3.2.0 于 2020 年 5 月发布。 Blazor WebAssembly in.NET 5 作为的一部分发布了.NET 5.0 发布于 2020 年 11 月,不是长期支持 ( LTS )发布。这本书将在中使用 Blazor WebAssembly.NET 5 为所有的项目。

小费

LTS 版本在首次发布后至少 3 年内受微软支持。WebAssembly 中的 Blazor.NET 5 不是 LTS 版本。如果你用 Blazor WebAssembly 开始一个新项目,你应该使用最新的版本..

下图说明了 Blazor WebAssembly 宿主模型:

Figure 1.2 – Blazor WebAssembly

图 1.2–Blazor 网络组件

在这种托管模式下,web 应用在浏览器上执行。为了让网络应用和.NET 运行时要在浏览器上运行,浏览器必须支持 WebAssembly 。WebAssembly 是所有现代浏览器都支持的 web 标准,包括移动浏览器。虽然 Blazor WebAssembly 本身不需要服务器,但 web 应用可能需要一个服务器来进行数据访问和身份验证。

过去,在浏览器上运行 C#代码的唯一方法是使用插件,比如 Silverlight 。Silverlight 是微软提供的一个免费浏览器插件。它非常流行,直到苹果决定不允许在 iOS 上使用浏览器插件。由于苹果的决定,Silverlight 被微软放弃了。Blazor 不依赖插件或者将代码重新编译成其他语言。取而代之的是,它基于开放的网络标准,并得到所有现代浏览器的支持,包括移动浏览器。

Blazor WebAssembly 的优势

Blazor WebAssembly 有很多优点。首先,由于它运行在浏览器上,所以它依赖于客户端资源而不是服务器资源。因此,与 Blazor 服务器不同,由于每个用户界面交互都需要往返于服务器之间,因此没有延迟。

Blazor WebAssembly 可用于创建进步 Web App ( PWA )。PWA 是一个网络应用,看起来和感觉都像一个原生应用。它们提供离线功能、后台活动、本机 API 层和推送通知。它们甚至可以在各种应用商店中列出。通过将您的 Blazor WebAssembly 应用配置为 PWA,您的应用可以在任何地点、任何设备上通过单一代码库访问任何人。有关创建 PWA 的更多信息,请参考第 5 章**将天气应用构建为渐进式网络应用(PWA)* 。*

*最后,一个 Blazor WebAssembly web app 不依赖于一个ASP.NET Core服务器。事实上,在没有服务器的情况下部署一个Blazor web assemblylyweb 应用是可能的。

Blazor WebAssembly 的缺点

平心而论,使用 Blazor WebAssembly 有一些缺点需要考虑。首先,当使用 Blazor WebAssembly 时.NET 运行时,dotnet.wasm文件和您的程序集都需要下载到浏览器中,您的 web 应用才能工作。因此,Blazor WebAssembly 应用的初始加载时间通常比 Blazor Server 应用长。但是,有一些策略可以加快应用的加载时间,例如将某些程序集的加载推迟到需要的时候。

调试 Blazor 服务器应用时,可以使用标准.NET 调试器。但是,要调试 Blazor WebAssembly 应用,您需要使用浏览器的调试器。要启用浏览器的调试器,您需要在启用远程调试的情况下启动浏览器,然后使用 Alt+Shift+D 启动位于浏览器和编辑器之间的代理组件。不幸的是,由于在浏览器上调试的复杂性,有一些特定的场景,例如在调试代理运行之前遇到断点,以及在未处理的异常上中断,这些都是调试器当前无法处理的。微软正在积极改善调试体验。

Blazor WebAssembly web 应用的另一个缺点是,它们的功能只和它们运行的浏览器一样强大。因此,不支持瘦客户端。Blazor WebAssembly 只能在支持 WebAssembly 的浏览器上运行。幸运的是,由于万维网联盟 ( W3C )和来自苹果、谷歌、微软和 Mozilla 的工程师之间的大量协调,所有现代浏览器都支持 WebAssembly。

Blazor 框架提供了两种不同的托管模型,Blazor 服务器和 Blazor WebAssembly。一个 Blazor Server 网络应用运行在服务器上,并使用信号向浏览器提供 HTML。相反,Blazor WebAssembly web 应用直接在浏览器中运行。它们各有利弊。然而,如果你想创建一个可以离线工作的响应性的、类似于原生的网络应用,你需要使用 Blazor WebAssembly。

什么是 WebAssembly?

WebAssembly 是一种二进制指令格式,允许用 C#编写的代码以接近本机的速度在浏览器上运行。跑步.NET 二进制文件,它使用.NET 运行时,该运行时已编译为网络程序集。您可以将其视为在浏览器中执行本机编译的代码。

WebAssembly 是由 W3C 社区小组开发的开放标准。它最初是在 2015 年宣布的,第一个支持它的浏览器是在 2017 年发布的。

WebAssembly 目标

最初开发 WebAssembly 时,项目有四个主要设计目标:

  • 快速高效
  • 安全的
  • 打开
  • 不要破坏网络

WebAssembly 快速高效。它旨在允许开发人员用任何语言编写代码,然后可以编译这些代码在浏览器中运行。由于代码是编译的,因此速度很快,并且以接近本机的速度运行。

WebAssembly 是安全的。它不允许与浏览器的 DOM 直接交互。相反,它在自己的内存安全的沙盒执行环境中运行。您必须使用 JavaScript 互操作来与 DOM 交互。 第四章使用 JavaScript 互操作性(JS interop) 构建本地存储服务的项目,将教你如何使用 JavaScript interop。

WebAssembly 已打开。虽然是低级汇编语言,但是可以手工编辑调试。

WebAssembly 没有破坏网络。这是一个网络标准,旨在与其他网络技术协同工作。此外,WebAssembly 模块可以访问相同的网络应用编程接口,这些接口可以从 JavaScript 中访问。

WebAssembly 支持

如前所述,WebAssembly 运行在所有现代浏览器上,包括移动浏览器。从下表中可以看出,所有当前最流行的浏览器版本都与网络组件兼容:

Figure 1.3 – WebAssembly browser compatibility

图 1.3–WebAssembly 浏览器兼容性

重要说明

Microsoft Internet Explorer 当前不支持网络程序集,并且永远不会支持网络程序集。所以,如果你的网络应用必须能够在微软浏览器上运行,不要使用 Blazor WebAssembly。

WebAssembly 是一个 web 标准,允许开发人员在浏览器中运行任何语言编写的代码。所有现代浏览器都支持它。

设置您的电脑

对于本书中的项目,我们使用 Visual Studio 2019,.NET 5.0 和 SQL Server 2019。

所有项目都是使用 Visual Studio 2019 社区版16 . 9 . 5 版构建的,具有 ASP.NET 和 Web 开发工作负载。如果您需要安装 Visual Studio 2019,请遵循本章后面的安装 Visual Studio 社区版一节中的说明。

小费

虽然我们使用的是 Visual Studio 2019 社区版,但是任何版本的 Visual Studio 2019 都可以用来完成本书中的项目。微软 Visual Studio 代码也可以使用。

WebAssembly 中的 Blazor.NET 5 需要.NET 5.0。检查的版本.NET,打开命令提示符并输入以下命令:

dotnet –-version

如果您的计算机没有运行.NET 5.0,按照中的说明进行安装.NET 5.0 一节将在本章后面介绍。

本书最后两个项目使用 SQL Server 2019 快速版作为后端数据库。如果您需要安装 SQL Server 2019 快速版,请按照本章后面的安装 SQL Server 快速版一节中的说明进行操作。

小费

虽然我们使用的是 SQL Server 2019 速成版,但是任何一年或版本的 SQL Server 都可以用来完成本书的项目。

安装 Visual Studio 社区版

Visual Studio 社区版是 Visual Studio 的免费版。要安装 Visual Studio 社区版,请执行以下步骤:

  1. https://visualstudio.microsoft.com下载 Visual Studio 安装程序

  2. 下载完成后运行安装程序完成安装。安装过程的第一步是让 Visual Studio 安装程序检查系统中现有的 Visual Studio 版本。

  3. Once the installer has finished checking for installed versions, it will open the following installation dialog:

    Figure 1.4 – The Visual Studio installer

    图 1.4–Visual Studio 安装程序

  4. 选择ASP.NET 和 web 开发工作量,点击安装按钮,完成安装。

安装.NET 5.0

安装.NET 5.0 ,执行以下步骤:

  1. https://dotnet.microsoft.com/download/dotnet/5.0下载安装程序。
  2. 下载完成后,运行安装程序以完成的安装.NET 5.0。
  3. 打开命令提示符,输入以下命令以验证您的计算机是否正在运行.NET 5.0:
dotnet –-version

下面的截图来自一台正在运行的计算机.NET 5.0:

Figure 1.5 – .NET version

图 1.5–.NET 版本

安装 SQL Server Express

SQL Server Express 是 SQL Server 的免费版。要安装 SQL Server Express ,请执行以下操作:

  1. https://www . Microsoft . com/en-us/SQL-Server/SQL-Server-downloads下载 SQL Server 安装程序。

  2. 下载完成后,运行 SQL Server 安装程序。

  3. Select the Basic installation type:

    Figure 1.6 – The SQL Server installer

    图 1.6–SQL Server 安装程序

  4. 点击接受按钮接受微软 SQL Server 许可条款。

  5. 点击安装按钮完成安装。

以下屏幕截图显示了成功安装 SQL Server Express 后出现的对话框:

Figure 1.7 – SQL Server Express Edition

图 1.7–SQL Server 快速版

要完成本书中的所有项目,您将需要一个代码编辑器,例如 Visual Studio 2019,.NET 5.0 或 SQL Server。在本章中,我们向您展示了如何安装 Visual Studio 2019 社区版.NET 5.0 和 SQL Server 2019 快速版。

总结

完成本章后,您应该了解使用 Blazor WebAssembly 相对于其他 web 开发框架的优势,并准备好完成本书中的项目。

在本章中,我们介绍了 Blazor 框架。Blazor 框架是建立在.NET 框架,并允许开发人员在 web 应用的前端和后端都使用 C#。

之后,我们比较了 Blazor 服务器和 Blazor WebAssembly。Blazor WebAssembly 比 Blazor Server 有很多优势,因为它运行在浏览器上,而 Blazor Server 运行在服务器上。Blazor WebAssembly web 应用可以脱机运行,感觉更像一个本地应用,因为所有代码都直接在浏览器上运行。最后,一个 Blazor WebAssembly 应用可以很容易地转换成一个 PWA。

在本章的最后一部分,我们解释了如何使用 Visual Studio 2019 社区版设置您的计算机.NET 5.0 和 SQL Server 2019 Express,所有这些都是完成本书中的项目所必需的。

现在,您的计算机已经设置好创建一个 Blazor WebAssembly web 应用,是时候开始了。在下一章中,您将创建第一个 Blazor WebAssembly web 应用。

问题

以下问题供您考虑:

  1. 使用 Blazor WebAssembly 是否意味着您再也不需要编写 JavaScript 了?
  2. Blazor WebAssembly 是否要求在浏览器上安装任何插件?
  3. 开始使用 Blazor WebAssembly 开发需要花费多少钱?

进一步阅读

以下资源提供了有关本章主题的更多信息:

二、构建你的第一个 Blazor WebAssembly 应用

Razor 组件是 Blazor WebAssembly 应用的构建块。Razor 组件是可以共享、嵌套和重用的用户界面块。Razor 组件是普通的 C#类,可以放在项目中的任何地方。

在本章中,我们将了解 Razor 组件。我们将学习如何使用它们,如何应用参数,以及它们的生命周期和结构。我们将学习如何使用@page指令来定义路由。我们还将学习如何使用 Razor 语法将 C#代码与 HTML 标记相结合。

本章中的 Blazor WebAssembly 项目将使用微软提供的 Blazor WebAssembly App 项目模板创建。创建项目后,我们将对其进行检查,以进一步熟悉 Razor 组件。我们将学习如何使用组件,如何添加参数,如何应用路由,如何使用 Razor 语法,以及如何将 Razor 标记和代码分离到单独的文件中。最后,我们将配置自己的自定义项目模板,创建一个空的 Blazor WebAssembly 项目。

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

  • 剃刀组件
  • 选择途径
  • 剃刀语法
  • 使用 Blazor 应用项目模板
  • 创建一个空的 Blazor WebAssembly 项目模板

技术要求

要完成此项目,您需要在电脑上安装 Visual Studio 2019。关于如何安装 Visual Studio 2019 免费社区版的说明,请参考 第 1 章Blazor web assembly 简介

本章的源代码可在以下 GitHub 存储库中获得:https://GitHub . com/PacktPublishing/Blazor-web assembly by Example/tree/main/chapter 02

行动中的代码视频可在此获得:https://bit.ly/3bEZrrg

剃须刀组件

Blazor WebAssembly 是一个组件驱动的框架。Razor 组件是 Blazor WebAssembly 应用的基本构件。它们是使用 C#、HTML 和 Razor 标记的组合实现的类。当 web 应用加载时,类会像往常一样下载到浏览器中。. NET 程序集(dll)。

重要说明

在本书中,术语剃刀组件组件可互换使用。

使用组件

HTML 元素语法用于将一个组件添加到另一个组件。标记看起来像一个 HTML 标记,其中标记的名称是组件类型。

我们将在本章中创建的Demo项目的Pages\Index.razor文件中的以下标记将呈现一个SurveyPrompt实例:

<SurveyPrompt Title="How is Blazor working for you?" />

前面的SurveyPrompt元素包含一个名为Title的属性参数。

参数

组件参数用于使组件动态化。参数是用Parameter属性或CascadingParameter属性修饰的组件的公共属性。参数可以是简单类型、复杂类型、函数、RenderFragments或事件回调。

以下名为HelloWorld的组件代码包含名为Text的参数:

地狱世界剃刀

<h1>Hello @Text!</h1>
@code {
    [Parameter] public string Text { get; set; }
}

要使用HelloWorld组件,请在另一个组件中包含以下 HTML 语法:

<HelloWorld Text="World" />

在上例中,HelloWorld组件的Text属性是Text参数的来源。此屏幕截图显示了使用组件的结果,如下所示:

Figure 2.1 – HelloWorld component

图 2.1–hello world 组件

组件还可以从其路由或查询字符串中接收参数。在本章的后面,您将了解更多关于不同类型参数的信息。

命名组件

剃刀组件的名称必须在标题大小写中。因此,helloWorld不是 Razor 组件的有效名称,因为 h 没有大写。另外,Razor 组件使用的是RAZOR扩展,而不是 Razor Pages 使用的CSHTML扩展。

重要说明

剃须刀组件必须以大写字母开头。

组件生命周期

剃刀组件继承自ComponentBase类。ComponentBase类包括用于管理组件生命周期的异步和同步方法。在本书中,我们将使用方法的异步版本,因为它们在执行时不会阻塞其他操作。这是组件生命周期中方法的调用顺序:

  1. SetParameterAsync:该方法在渲染树中设置由组件的父组件提供的参数。
  2. OnInitializedAsync:首次渲染组件后调用此方法。
  3. OnParametersSetAsync:该方法在组件每次重新渲染初始化后调用。
  4. OnAfterRenderAsync:组件渲染完毕后调用此方法。这个方法是针对使用 JavaScript 的的,因为 JavaScript 需要文档对象模型 ( DOM )元素在它们可以做任何工作之前被渲染。

构件结构

下面的图显示了我们将在本章中创建的Demo项目的Counter组件的代码:

Figure 2.2 – Component structure

图 2.2–组件结构

前面示例中的代码分为三个部分:

  • 指令
  • 标记
  • 代码块

每个部分都有不同的目的。

指令

指令用于添加特殊功能,如路由、布局和依赖注入。它们是在 Razor 中定义的,您不能定义自己的指令。

在前面的例子中,只使用了一个指令-@page指令。@page指令用于路由。在本例中,以下网址将用户路由到Counter组件:

/counter

一个典型的页面可以在页面顶部包含许多指令。而且,很多页面都有不止一个@page指令。

Razor 中的大部分指令都可以在 Blazor WebAssembly 应用中使用。以下是 Blazor 中使用的 Razor 指令,按字母顺序排列:

  • @attribute: This directive adds a class-level attribute to the component. The following example adds the [Authorize] attribute:

    @attribute [Authorize]

  • @code:该指令向组件中添加类成员。在示例中,它用于区分代码块。

  • @implements:这个指令实现了指定的类。

  • @inherits:这个指令提供了对视图继承的类的完全控制。

  • @inject: This directive is used for dependency injection. It enables the component to inject a service from the dependency injection container into the view. The following example injects HttpClient defined in the Program.cs file into the component:

    @inject HttpClient Http

  • @layout:该指令用于指定 Razor 组件的布局。

  • @namespace:这个指令设置组件的命名空间。如果不想使用组件的默认命名空间,只需使用该指令。默认命名空间基于组件的位置。

  • @page:本指令用于路由。

  • @typeparam:该指令为组件设置类型参数。

  • @using:本指令控制范围内的部件。

利润

这是带有 Razor 语法的 HTML。Razor 语法可用于呈现文本,并允许 C#用作标记的一部分。我们将在本章的后面介绍更多关于 Razor 语法的内容。

码组

代码块包含页面的逻辑。它始于@code指令。按照惯例,@code指令位于页面底部。它是唯一没有放在页面顶部的文件级指令。

代码块是我们向组件添加 C#字段、属性和方法的地方。在本章的后面,我们将把代码块移到一个单独的代码隐藏文件中。

Razor 组件是 Blazor WebAssembly 应用的构建块。它们很容易使用,因为它们只是 HTML 标记和 C#代码的组合。在下一节中,我们将看到如何使用路由在每个组件之间导航。

Blazor 网络组件中的路由

在 Blazor WebAssembly 中,路由是在客户端处理的,而不是在服务器端。当您在浏览器中导航时,Blazor 拦截该导航,并使用匹配的路线渲染组件。

相对于wwwroot/index.html文件中指定的基本路径来解析 URL。使用以下语法在head元素中指定:

 <base href="/" />

与您可能使用过的其他框架不同,路径不是从其文件的位置推断出来的。例如,在Demo项目中,Counter组件位于/Pages/Counter文件夹中,但它使用以下路径:

@page "/counter"

路线参数

Router组件使用路线参数来填充相应组件的参数。组件和路由的参数必须具有相同的名称,但不区分大小写。

由于不支持可选路线参数,您可能需要向组件提供多个@page指令来模拟可选参数。以下示例显示了如何包含多个@page参数:

路线举例:剃刀

@page "/routing"
@page "/routing/{text}"
<h1>Blazor WebAssembly is @Text!</h1>
@code {
    [Parameter] public string Text { get; set; }
    protected override void OnInitialized()
    {
        Text = Text ?? "fantastic";
    }
}

在前面的代码中,第一个@page指令允许导航到没有参数的组件,第二个@page指令允许路线参数。如果提供了text的值,它将被分配给组件的Text属性。如果组件的Text属性为null,则设置为fantastic

以下网址将用户路由到RoutingExample组件:

/routing

以下网址也将用户路由到RoutingExample组件,但这次Text参数将由路由设置:

/routing/amazing

此屏幕截图显示了使用指定路线的结果:

Figure 2.3 – RoutingExample component

图 2.3–布线示例组件

重要说明

路由参数不区分大小写。

捕捉所有路线参数

捕捉所有路径参数用于捕捉跨越多个文件夹边界的路径。这种类型的路由参数是string类型,只能放在 URL 的末尾。

这是一个使用全面路线参数的示例组件:

剃刀

@page "/{*path}"
<h1>Catch All</h1>
Route: @Path
@code {
    [Parameter] public string Path { get; set; }
}

对于/error/type/3 URL,前面的代码将Path参数的值设置为error/type/3:

Figure 2.4 – Catch-all route parameter example

图 2.4–全面路线参数示例

路线限制

路线约束用于实施路线参数的数据类型。若要定义约束,请在参数中添加一个冒号,后跟约束类型。在下面的示例中,路线需要名为Increment的路线参数,其类型为int:

@page "/counter/{increment:int}"

支持以下路线约束:

Figure 2.5 – Supported route constraints

图 2.5–支持的路线限制

以下类型是当前不支持的约束:

  • 正则表达式
  • 枚举数
  • 自定义约束

路由在客户端处理。我们可以使用路由参数和总括路由参数来启用路由。路由约束用于确保路由参数属于所需的数据类型。Razor 组件使用 Razor 语法将 HTML 与 C#代码无缝合并,这是我们将在下一节中看到的内容。

Razor 语法

Razor 语法由 HTML、Razor 标记和 C#组成。从 Razor 组件呈现 HTML 与从 HTML 文件呈现 HTML 是一样的。Razor 组件中的 HTML 由服务器不变地呈现。Razor 语法同时使用内联表达式和控制结构。

内联表达式

内联表达式以@符号开始,后跟变量或函数名。这是一个内联表达式的示例:

<h1>Blazor is @Text!</h1>

控制结构

控制结构也以@符号开始。大括号内的内容将被评估并呈现到输出中。这是来自Demo项目中FetchData组件的if声明的示例:

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}

Razor 代码块中的每个代码语句都必须以分号结束。C#代码区分大小写,字符串必须用引号括起来。

条件式

以下类型的条件句包含在 Razor 语法中:

  • if陈述
  • switch陈述

这是一个if语句的例子:

@if (DateTime.Now.DayOfWeek.ToString() != "Friday")
{
    <p>Today is not Friday.</p>
}
else if (DateTime.Now.Day != 13)
{
    <p>Today is not the 13th.</p>
}
else
{
    <p>Today is Friday the 13th.</p>
}

上面的代码使用 if 语句来检查一周的当前日期是星期五和/或一个月的当前日期是 13 号。

这是switch语句的一个例子:

@switch (value)
{
    case 1:
        <p>The value is 1!</p>
        break;
    case 42:
        <p>Your number is 42!</p>
        break;
    default:
        <p>Your number was not 1 or 42.</p>
        break;
}
@code {
    private int value = 2;
}

前面的switch语句将value变量与142进行了比较。

以下类型的循环包含在 Razor 语法中:

  • for循环
  • foreach循环
  • while循环
  • do while循环

以下每个示例都使用了一个WeatherForecast类型的数组。WeatherForecast包括一个Summary物业,在Demo项目中定义。

这是一个for循环的例子:

@for (var i = 0; i < forecasts.Count(); i++)
{
   <div> forecasts[i].Summary</div>
};
@code {
    private WeatherForecast[] forecasts;
}

这是一个foreach循环的例子:

@foreach (var forecast in forecasts)
{
    <div>@forecast.Summary</div>
};
@code {
    private WeatherForecast[] forecasts;
}

这是一个while循环的例子:

@while (i < forecasts.Count())
{
    <div>@forecasts[i].Summary</div>
    i++;
};
@code {
    private WeatherForecast[] forecasts;
    private int i = 0;
}

这是一个循环的例子:

@do
{
    <div>@forecasts[i].Summary</div>
    i++;
} while (i < forecasts.Count());
@code {
    private WeatherForecast[] forecasts;
    private int i = 0;
}

如果你已经知道 C#的话,Razor 语法很容易学习。它包括内联表达式和控制结构,如条件和循环。

项目概述

我们将在本章中构建的 Blazor WebAssembly 应用是一个简单的三页应用。每一页都将用于演示 Razor 组件的一个或多个功能。

这是已完成的Demo项目截图:

Figure 2.6 – Home page of the Demo project

图 2.6–演示项目的主页

等完成Demo项目后,我们将把它转换成一个空的 Blazor WebAssembly 项目。空的 Blazor WebAssembly 项目将用作自定义 Blazor WebAssembly 应用项目模板的基础。

创建演示 Blazor WebAssembly 项目

我们正在创建的Demo项目是基于 Blazor WebAssembly App 项目模板提供的众多示例项目之一。在我们使用模板创建项目之后,我们将检查示例项目中的文件,并更新一些文件来演示如何使用 Razor 组件。最后,我们将其中一个组件的代码块分离到一个单独的文件中,以演示如何使用代码隐藏技术将标记与代码分离。

创建演示项目

Visual Studio 附带了相当多的项目模板。我们将使用 Blazor WebAssembly 应用项目模板来创建我们的第一个 Blazor WebAssembly 项目。由于此项目模板可用于创建许多不同类型的 Blazor 项目,因此仔细遵循说明非常重要:

  1. 打开 Visual Studio 2019

  2. 点击新建项目按钮。

  3. In the Search for templates (Alt+S) textbox, enter Blazor and hit the Enter key.

    下面的截图显示了我们将要使用的 Blazor WebAssembly App 项目模板:

    Figure 2.7 – Blazor WebAssembly App project template

    图 2.7–Blazor web assembly App项目模板

  4. 选择 Blazor WebAssembly App 项目模板,点击下一步按钮。

  5. Enter Demo in the Project name textbox and click the Next button.

    这是用于配置我们新项目的对话框截图:

    Figure 2.8 – The Configure your new project dialog

    图 2.8–配置您的新项目 ct 对话框

    小费

    在前面的例子中,我们将Demo项目放入E:\Blazor文件夹中。然而,这个项目的位置并不重要。

  6. Select .NET 5.0 as the version of the .NET Framework to use.

    这是用于创建我们新的 Blazor WebAssembly 应用的对话框截图:

    Figure 2.9 – Additional information for the Blazor WebAssembly App dialog

    图 2.9–Blazor WebAssembly 应用对话框的附加信息

  7. 点击创建按钮。

您已经创建了Demo Blazor WebAssembly 项目。

运行演示项目

一旦创建了项目,您需要运行它来了解它的功能。Demo项目包含三页:首页计数器取数据:

  1. From the Debug menu, select the Start Without Debugging (Ctrl+F5) option to run the Demo project.

    这是来自Demo项目的主页页面截图:

    Figure 2.10 – The Home page

    图 2.10–主页

    主页页面分为两部分。导航菜单位于页面左侧,正文位于页面右侧。主页的正文由一些静态文本和一个调查链接组成。

  2. Click the Counter option on the navigation menu to navigate to the Counter page.

    这是Demo项目中计数器页面的截图:

    Figure 2.11 – The Counter page

    图 2.11–计数器页面

    计数器页面的主体包括一个计数器和一个点击我按钮。每次点击计数器页面上的按钮,计数器都会递增,而不会刷新页面。

    重要说明

    由于这是一个单页应用 ( SPA ),所以只更新页面中需要更新的部分。

  3. Click the Fetch data option on the navigation menu to navigate to the Fetch data page.

    这是从Demo项目获取数据页面的截图:

Figure 2.12 – The Fetch data page

图 2.12–提取数据页面

获取数据页面的正文包括一个显示 2018 年几天天气预报的表格。如您所见,表格中显示的数据是来自wwwroot\sample-data\weather.json文件的静态数据。

检查演示项目的结构

现在让我们返回 Visual Studio 到检查Demo项目中的文件。

下图显示了项目的结构:

Figure 2.13 – Project structure

图 2.13–项目结构

该项目包括相当多的文件,其中一些文件被分成自己的文件夹。让我们检查一下。

wwwroot 文件夹

wwwroot文件夹是应用的网络根目录。只有该文件夹中的文件可以通过网络访问。wwwroot文件夹包含层叠样式表 ( CSS )文件的集合、示例数据文件、图标文件和index.html。在本书的后面,除了这些类型的文件,我们还将把这个文件夹用于公共静态资源,如图像和 JavaScript 文件。

index.html文件是 web 应用的根页面。每当最初请求一个页面时,index.html页面的内容被呈现并在响应中返回。index.html文件的head元素包括到css文件夹中每个 CSS 文件的链接,并指定用于 web 应用的基本路径。index.html文件的body元素包括两个div元素和对Blazor.webassembly.js文件的引用。

这是index.html文件的body元素中的代码:

<body>
    <div id="app">Loading...</div>
    <div id="Blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">x</a>
    </div>
    <script 
      src="_framework/Blazor.webassembly.js"></script>
</body>

在前面的代码中高亮显示的div元素加载App组件。

Blazor-error-ui div元素用于显示未处理的异常。这个div元素的样式在wwwroot\css\app.css文件中。Blazor.webassembly.js文件是下载的脚本.NET 运行时、应用的程序集以及应用的依赖项。它还初始化运行时来运行 web 应用。

应用组件

App组件是在App.razor文件中定义的:

App .剃须刀

<Router AppAssembly="@typeof(Program).Assembly" 
        PreferExactMatches="@true">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" 
                   DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

App组件是 Blazor web assembly 应用的根组件。它使用Router组件为网络应用设置路由。在前面的代码中,如果找到了路线,RouteView组件接收RouteData并使用指示的DefaultLayout渲染指定的组件。如果未找到路线,则使用NotFound模板,并使用指示的Layout渲染LayoutView

如您所见,在Demo项目中,Found模板和NotFound模板使用相同的布局。他们都在使用MainLayout组件。但是,它们不需要使用相同的布局组件。

共享文件夹

Demo项目中的Shared文件夹包括共享用户界面 Razor 组件,包括MainLayout组件。这些组件中的每一个都可以被其他 Razor 组件使用一次或多次。

页面文件夹

Pages文件夹包括项目使用的可传递剃刀组件。可路由的组件有CounterFetchDataIndex。这些组件中的每一个都包含一个@page指令,用于将用户路由到页面。

导入文件

该文件包括 Razor 指令,例如名称空间的@using指令。您的项目可以包含多个_Imports.razor文件。每个文件都应用于其当前文件夹和子文件夹。_Imports.razor文件中的任何@using指令仅适用于 Razor ( RAZOR)文件。它们不适用于 C# ( CS)文件。当使用代码隐藏技术时,这种区别很重要。

程序文件

Program.cs文件是申请的入口点。

检查共享的剃须刀组件

共享剃刀组件在Shared文件夹中。Demo项目中有三个共享剃须刀组件:

  • MainLayout组件
  • NavMenu组件
  • SurveyPrompt组件

主布局组件

MainLayout组件用于定义网络应用的页面布局:

页面/主布局。剃刀

@inherits LayoutComponentBase
<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>
    <div class="main">
        <div class="top-row px-4">
            <a href="http://Blazor.net" 
               target="_blank" 
               class="ml-md-auto">About</a>
        </div>
        <div class="content px-4">
            @Body
        </div>
    </div>
</div>

MainLayout组件继承自LayoutComponentBase类。LayoutComponentBase代表一个布局,只有一个属性,就是Body属性。Body属性获取要在布局中呈现的内容。

下图说明了由MainLayout组件定义的页面布局:

Figure 2.14 – Screen layout

图 2.14–屏幕布局

小费

Blazor WebAssembly App 项目模板使用 Bootstrap 4 来设计页面样式。如果您不熟悉 Bootstrap 4,您应该参考https://getbootstrap.com来熟悉它的语法。

导航菜单组件

NavMenu组件为Demo项目定义导航菜单。它使用多个NavLink组件来定义各种菜单选项。这是NavMenu组件中引用用于项目导航的NavLink组件的部分:

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" 
                     Match="NavLinkMatch.All">
                <span class="oi oi-home" 
                      aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" 
                      aria-hidden="true"></span> Counter
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" 
                      aria-hidden="true"></span> Fetch data
            </NavLink>
        </li>
    </ul>
</div>

NavLink组件是在Microsoft.AspNetCore.Components.Routing命名空间中定义的。它的行为就像一个a元素,只是它增加了突出显示当前网址的功能。这是选择Counter组件时NavLinkCounter组件呈现的 HTML:

<a href="counter" class="nav-link active">
    <span class="oi oi-plus" aria-hidden="true"></span> 
    Counter
</a>

nav-link类使用的样式来自 Bootstrap。用于active类的样式是在wwwroot\css\app.css文件中定义的:

.sidebar .nav-item a.active {
    background-color: rgba(255,255,255,0.25);
    color: white;
}

调查提示组件

SurveyPrompt组件创建一个到 Blazor 上的简短调查的链接。

检查可布线剃刀组件

可传递剃刀组件在Pages文件夹中。在Demo项目中有三个可传递的剃刀组件:

  • Index组件
  • Counter组件
  • FetchData组件

索引组件

Demo项目的Home页面使用在Pages\Index.razor文件中定义的Index组件:

页面\索引.剃刀

@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />

前面的代码包括一个@page指令,该指令引用了 web 应用的根和一些标记。标记包括一个SurveyPrompt组件。

计数器组件

Counter组件比Index组件更复杂。类似于Index组件,它包含一个@page指令,用于路由和一些标记。但是,它还包含一个 C#代码块:

页面\计数器.剃刀

@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">
    Click me
</button>
<a href="counter" class="nav-link active">
    <span class="oi oi-plus" aria-hidden="true"></span> 
    Counter
</a>
@code {
    private int currentCount = 0;
    private void IncrementCount()
    {
        currentCount++;
    }
}

在前面的代码块中,一个私有的currentCount变量用于保存点击次数。每次点击Counter按钮,就会调用Counter组件注册的@onclick处理程序。在这种情况下,就是IncrementCount法。

IncrementCount方法增加currentCount变量的值,Counter组件重新生成其渲染树。Blazor 将新的渲染树与前一个进行比较,并对浏览器的 DOM 进行修改。这将导致显示的计数被更新。

FetchData 组件

FetchData组件是Demo项目中最复杂的组件。

这些是Pages\FetchData.razor文件中的指令:

@page "/fetchdata"
@inject HttpClient Http

@page指令用于路由,@inject指令用于依赖注入。在该组件中,在Program.cs文件中定义的HttpClient被注入到视图中。有关依赖注入的更多信息,请参考 第 6 章使用应用状态构建购物车

下面的标记演示了一种非常重要的模式的使用,在开发 Blazor WebAssembly 应用时,您将经常使用这种模式。因为应用在浏览器上运行,所以所有数据访问都必须是异步的。这意味着当页面第一次加载时,数据将为空。因此,在尝试处理数据之前,您需要测试 null 情况。

这是Pages\FetchData.razor文件中的标记:

<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                   <td>@forecast.Date.ToShortDateString()
                      </td>
                   <td>@forecast.TemperatureC</td>
                   <td>@forecast.TemperatureF</td>
                   <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

前面的标记包括一个if语句和一个foreach循环。当forecasts值为null时,显示加载信息。如果不处理forecasts值为null的情况,框架会抛出异常。一旦forecasts的值不再是null,数组中的所有项目都呈现在一个表中。

重要说明

forecasts的值将是第一次渲染页面时的null

如前所述,Blazor 组件有一个定义明确的生命周期。渲染组件时调用OnInitializedAsync方法。OnInitializedAsync方法完成后,组件被重新渲染。

这是Pages\FetchData.razor文件中的代码块:

@code {
    private WeatherForecast[] forecasts;
    protected override async Task OnInitializedAsync()
    {
        forecasts = await 
            Http.GetFromJsonAsync<WeatherForecast[]>
            ("sample-data/weather.json");    
    }
    public class WeatherForecast
    {
        public DateTime Date { get; set; }
        public int TemperatureC { get; set; }
        public string Summary { get; set; }
        public int TemperatureF => 
            32 + (int)(TemperatureC / 0.5556);
    }
}

首先,前面的代码块声明了一个包含类型为WeatherForecast的数组的参数。接下来,它使用OnInitializedAsync异步方法填充数组。为了填充数组,使用了HttpClient服务的GetFromJsonAsync方法。有关HttpClient的更多信息,请参考 第 8 章使用 ASP.NET 网络应用编程接口构建任务管理器。

使用组件

剃刀组件通过包含在另一个组件的标记中来使用。我们将在Home页面添加一个Counter组件。我们按如下方式进行:

  1. 返回到 Visual Studio。

  2. 打开Pages\Index.razor文件。

  3. 删除所有标记。确保您没有删除文件顶部的@page指令。

  4. @page指令下添加以下标记:

    <Counter />
    
  5. 构建菜单中,选择重建解决方案选项。

  6. 返回浏览器,导航至主页页面。如果Demo项目没有继续运行,从调试菜单中,选择开始不调试(Ctrl+F5)选项运行。

  7. Use Ctrl + R to refresh the browser.

    小费

    每当更新 C#代码时,都需要刷新浏览器,以便浏览器加载更新后的 DLL。

  8. 点击点击我按钮测试Counter组件。

向组件添加参数

大多数组件需要参数。要向组件添加参数,请使用Parameter属性。我们将添加一个参数来指定IncrementCount方法使用的增量。我们按如下方式进行:

  1. 返回到 Visual Studio。

  2. 打开Pages\Counter.razor文件。

  3. 在代码块顶部添加以下代码,定义新参数:

    [Parameter] public int? Increment { get; set; }
    private int increment = 1;
    
  4. IncrementCount方法更新为如下:

    private void IncrementCount()
    {
        currentCount += increment;
    }
    
  5. 添加以下OnParametersSet方法将increment的值设置为Increment参数的值:

    protected override void OnParametersSet()
    {
        if (Increment.HasValue)
            increment = Increment.Value;
    }
    
  6. 将高亮显示的文本添加到的标记中点击我按钮,显示increment变量的当前值:

    <button class="btn btn-primary" 
            @onclick="IncrementCount">
        Click me (@increment)
    </button>
    

使用带有属性的参数

我们将向使用新参数的Home页面添加另一个Counter组件的实例。我们按如下方式进行:

  1. 打开Pages\Index.razor文件。

  2. Add the following markup to the bottom of the Index.razor file:

    <Counter Increment="5"/>
    

    添加标记时,为新的Increment参数提供了智能感知:

    Figure 2.15 – IntelliSense

    图 2.15–智能感知

  3. 构建菜单中,选择构建解决方案选项。

  4. 返回浏览器。

  5. 使用 Ctrl + R 刷新浏览器。

  6. Navigate to the Home page.

    主页页面现在包含两个Counter组件实例。如果点击第一个点击我按钮,第一个计数器将增加 1;如果点击第二个点击我按钮,第二个计数器将增加 5:

    Figure 2.16 – The Home page

    图 2.16–主页

  7. 单击每个点击我按钮以验证它们是否都按预期工作。

添加路线参数

组件可以有多个指令。我们将向使用参数的Counter组件添加一个@page指令。我们按如下方式进行:

  1. 返回到 Visual Studio。

  2. 打开Pages/Counter.razor文件。

  3. Add the following @page directive to the top of the file:

    @page "/counter/{increment:int}"
    

    Counter组件现在包括两个@page指令。

  4. 构建菜单中,选择构建解决方案选项。

  5. 返回浏览器。

  6. 导航至计数器页面。

  7. Update the URL to the following:

    /counter/4
    

    重要说明

    由于当您更改网址时,页面会自动重新加载,因此您不需要刷新浏览器来重新加载页面。

  8. Click the Click me button.

    计数器现在应该增加4

  9. Update the URL to an invalid route:

    /counter/a
    

    由于这不是有效的路线,您将被引导至App组件中定义的NotFound内容:

Figure 2.17 – Page not found

图 2.17–找不到页面

小费

如果需要在代码中导航到某个 URL,应该使用NavigationManagerNavigationManager提供了一种NavigateTo方法,用于在不强制页面加载的情况下将用户导航到指定的 URI。

使用分部类从代码中分离标记

许多开发人员更喜欢将他们的标记与他们的 C#字段、属性和方法分开。因为 Razor 组件是常规的 C#类,所以它们支持分部类。partial关键字用于创建分部类。我们将使用分部类将代码块从RAZOR文件移动到CS文件。我们按如下方式进行:

  1. 返回 Visual Studio

  2. 右键单击页面文件夹,从菜单中选择添加,类别

  3. 命名新类Counter.razor.cs

  4. 使用partial关键字

    public partial class Counter{}
    

    Counter类更新为分部类

  5. 打开Pages/Counter.razor文件。

  6. 将代码块中的所有代码复制到Counter.razor.cs文件中的部分Counter类。

  7. Counter.razor文件中删除代码块。

  8. 将以下using语句添加到Counter.razor.cs文件中:

    using Microsoft.AspNetCore.Components;
    
  9. 构建菜单中,选择构建解决方案选项。

  10. 返回浏览器。

  11. 使用 Ctrl + R 刷新浏览器。

  12. 导航至计数器页面。

  13. 点击点击我按钮,验证是否仍然有效。

  14. 关闭浏览器。

使用部分类可以让您灵活地将代码块中的代码移动到单独的文件中,从而允许您使用代码隐藏技术。

我们使用微软提供的 Blazor WebAssembly 应用项目模板创建了一个Demo项目。我们向Counter组件添加了一个参数,并将Counter组件的代码块中的代码移动到一个单独的文件中。

创建自定义 Blazor WebAssembly 项目模板

如您所见, Blazor WebAssembly App 项目模板创建的Demo Blazor WebAssembly 项目包含相当多的文件。在后面的章节中,我们将希望从一个空的 Blazor 项目开始。因此,我们将创建自己的项目模板,创建一个空的 Blazor WebAssembly 项目。

创建一个空的 Blazor 项目

我们需要创建一个空的 Blazor WebAssembly 项目来作为我们新项目模板的基础。我们按如下方式进行:

  1. 返回到 Visual Studio。

  2. 删除wwwroot\sample-data文件夹。

  3. 删除Pages文件夹中的所有组件,除了Index组件。

  4. 打开Index.razor文件。

  5. Index组件中删除所有标记。确保您没有删除页面顶部的@page指令。

  6. 删除Shared\SurveyPrompt.razor文件。

  7. 打开Shared\MainLayout.razor文件。

  8. 通过删除以下标记,从布局的第一行中删除About链接:

    <a href="http://Blazor.net" target="_blank"
       class="ml-md-auto">
            About
    </a>
    
  9. 打开Shared\NavMenu.razor文件。

  10. 删除CounterFetch data页面的li元素。

  11. 构建菜单中,选择构建解决方案选项。

  12. From the Debug menu, select the Start Without Debugging (Ctrl+F5) option to run the Demo project.

`Demo`项目现在是空的。它只包含一个空白的`Home`页面。

创建项目模板

导出模板向导用于创建自定义项目模板。我们将使用我们刚刚创建的空项目作为自定义项目模板的基础。我们按如下方式进行:

  1. 返回到 Visual Studio。

  2. 项目菜单中,选择导出模板选项,打开导出模板向导窗口

  3. Select Project template on the Choose Template Type dialog and click the Next button:

    Figure 2.18 – The Choose Template Type dialog

    图 2.18–选择模板类型对话框

  4. 如下图所示完成选择模板选项对话框,点击完成按钮:

Figure 2.19 – The Select Template Options dialog

图 2.19–选择模板选项对话框

单击完成按钮后,您的新项目模板将保存到选择模板选项对话框中输出位置字段指示的文件夹中,该文件夹将自动打开。组成新项目模板的文件被压缩成一个名为EmptyBlazorProject.zip的文件。

更新自定义项目模板

我们需要在自定义项目模板准备使用之前对其进行一些更新。首先,我们将为项目名称声明一个模板参数,然后我们将更新元数据。我们按如下方式进行:

  1. Extract all of the files from the EmptyBlazorProject.zip file.

    EmptyBlazorProject.zip文件包含空的Demo项目的所有文件以及包含项目模板的所有元数据的MyTemplate.vstemplate文件。

  2. Open the Shared/NavMenu.razor file and replace the word Demo with $projectname$:

    <a class="navbar-brand" href="">$projectname$</a>
    

    $projectname$参数将被用户在创建项目时提供的项目名称替换。

    打开_Imports.razor文件,将Demo替换为$projectname$:

    @using $projectname$
    @using $projectname$.Shared
    
  3. 打开MyTemplate.vstemplate文件。

  4. Name元素的值更新为Empty Blazor WebAssembly App :

    <Name>Empty Blazor WebAssembly App</Name>
    
  5. Description元素后添加以下元素:

        <LanguageTag>C#</LanguageTag>
        <ProjectTypeTag>Web</ProjectTypeTag>
    
  6. Icon元素替换为以下Icon Package元素:

    <Icon Package="{AAB75614-2F8F-4DA6-B0A6-763C6DBB2969}" ID="13"/>
    
  7. NavMenu.razor ProjectItem :

    <ProjectItem ReplaceParameters="true" 
                 TargetFileName="NavMenu.razor">
        NavMenu.razor
    </ProjectItem>
    

    ReplaceParameters属性更改为true

  8. _Imports.razor ProjectItem :

    <ProjectItem ReplaceParameters="true" 
                 TargetFileName="_Imports.razor">
        _Imports.razor
    </ProjectItem>
    

    ReplaceParameters属性更改为true

  9. 保存所有更新文件。

  10. 用更新后的文件更新EmtpyBlazorProject.zip文件。

  11. EmtpyBlazorProject.zipVisual Studio 2019\MyExportedTemplates文件夹复制到Visual Studio 2019\Templates\ProjectTemplates文件夹。

使用自定义项目模板

我们可以像使用任何内置项目模板一样使用自定义项目模板。我们按如下方式进行:

  1. 文件菜单中,选择新建,项目选项。

  2. Enter Blazor in the Search for templates textbox to locate your new template:

    Figure 2.20 – Empty Blazor WebAssembly App template

    图 2.20–空 Blazor WebAssembly 应用模板

  3. 选择空 Blazor WebAssembly App 模板,点击下一步按钮。

  4. 将项目名称更新为Sample,点击创建按钮。

  5. 构建菜单中,选择构建解决方案选项。

  6. 调试菜单中,选择开始不调试(Ctrl+F5)

我们使用自定义项目模板通过创建了一个新的Sample项目。Sample项目中唯一的页面是Home页面。

我们从上一节创建的Demo项目中删除了一些组件和代码,从而创建了一个空项目。然后,我们使用导出模板向导基于空项目创建自定义项目模板。在我们更新了自定义项目模板中的一些文件后,我们将它们复制到ProjectTemplates文件夹中。最后,我们使用自定义项目模板来创建Sample项目。

总结

现在,您应该能够创建一个 Blazor WebAssembly 应用了。

在本章中,我们介绍了 Razor 组件、路由和 Razor 语法。

之后,我们使用微软提供的 Blazor WebAssembly App 项目模板,创建了Demo Blazor WebAssembly 项目。我们向Counter组件添加了一个参数,并研究了路由是如何工作的。

在本章的最后一部分,我们创建了一个空的 Blazor WebAssembly 项目,我们自己的自定义项目模板就是基于这个项目。我们使用导出模板向导创建了一个自定义项目模板。完成自定义项目模板的配置后,我们使用它创建了一个空的 Blazor WebAssembly 项目。

我们将在本书的下一章中使用空 Blazor WebAssembly App 项目模板来创建项目。

问题

以下问题供您考虑:

  1. Razor 组件可以包含 JavaScript 吗?
  2. Razor 语法支持哪些类型的循环?
  3. Blazor 应用项目模板可以用来创建 Blazor WebAssembly 应用和 Blazor Server 应用吗?
  4. 使用自定义项目模板有什么好处?
  5. 您如何创建自己的自定义项模板来为每个新组件自动创建代码隐藏页?

进一步阅读

以下资源提供了有关本章主题的更多信息:

三、使用模板化组件构建模态对话框

模态对话框是出现在窗口中所有其他内容之上的对话框,需要用户交互才能关闭它。模板化组件是接受一个或多个用户界面模板作为参数的组件。用户界面模板可以包含任何 Razor 标记。

在本章中,我们将学习RenderFragment参数、EventCallback参数和 CSS 隔离。当父组件需要与子组件共享信息时,使用RenderFragment参数,反之,当子组件需要与其父组件共享信息时,使用EventCallback参数。我们还将学习如何通过使用 CSS 隔离将样式仅应用于单个组件。

在本章中,我们将创建一个模态对话框组件。该组件将是一个模板化组件,可以根据其参数的内容呈现不同的 HTML。它将使用事件回调将事件返回给调用组件。它将使用 CSS 隔离来添加格式,这将使它的行为像一个模态对话框。最后,我们将组件移动到一个 Razor 类库中,这样就可以和其他项目共享了。

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

  • RenderFragment参数
  • EventCallback参数
  • CSS 隔离
  • 创建 Razor 类库
  • 创建模态对话框项目

技术要求

要完成此项目,您需要在电脑上安装 Visual Studio 2019。关于如何安装 Visual Studio 2019 免费社区版的说明,请参考 第 1 章 、Blazor WebAssembly 简介。您还需要我们在 第 2 章 中创建的空 Blazor WebAssembly App 项目模板,构建您的第一个 Blazor WebAssembly 应用

本章的源代码可在以下 GitHub 存储库中获得:https://GitHub . com/PacktPublishing/Blazor-web assembly by Example/tree/main/chapter 03

行动中的代码视频可在此获得:https://bit.ly/33X2Zkc

渲染片段参数

一个RenderFragment参数是一段 UI 内容。一个RenderFragment参数用于将用户界面内容从父母传递给孩子。用户界面内容可以包括纯文本、HTML 标记、Razor 标记或其他组件。

以下代码用于Alert组件。当Show属性的值为true时,显示Alert组件的用户界面内容:

警惕剃刀

@if (Show)
{
    <div class="dialog-container">
        <div class="dialog">
            <div>
                @ChildContent
            </div>
            <div>
                <button @onclick="OnOk">
                    OK
                </button>
            </div>
        </div>
    </div>
}
@code {
  [Parameter] public bool Show { get; set; }
  [Parameter] public EventCallback OnOk { get; set; }
  [Parameter] public RenderFragment ChildContent { get;
    set; } 
}

前面的代码针对Alert组件,包括三种不同类型的参数:布尔、RenderFragmentEventCallback:

  • 第一个参数是Show属性。它是布尔类型,这是一个简单的类型。有关使用简单类型作为参数的更多信息,请参见第 2 章**,构建您的第一个 Blazor WebAssembly 应用
    ** 第二个参数是OnOk属性。是EventCallback型。我们将在下一节中了解更多关于EventCallback参数的信息。* 最后一个参数是ChildContent属性。是RenderFragment型。*

*当点击显示提醒按钮时,以下标记使用Alert组件在对话框中显示一周的当前日期。Alert元素的开始标记和结束标记之间的 Razor 标记绑定到Alert组件的ChildContent属性:

索引剃刀

@page "/"
<Alert Show="showAlert" OnOk="@(() => showAlert = false)">
    <h1>Alert</h1>
    <p>Today is @DateTime.Now.DayOfWeek.</p>
</Alert>
<button @onclick="@(() => showAlert = true)">
    Show Alert
</button>
@code {
    private bool showAlert = false;
}

以下截图显示了点击显示提醒按钮时显示的对话框:

Figure 3.1 – Sample alert

图 3.1–示例警报

如果要在不明确指定参数名称的情况下使用元素内容,RenderFragment参数的名称必须为ChildContent。例如,以下标记产生的输出与前面没有明确指定ChildContent的标记相同:

<Alert Show="showAlert" OnOk="@(() => showAlert = false)">
    <ChildContent>
        <h1>Alert</h1>
        <p>Today is @DateTime.Now.DayOfWeek.</p>
    </ChildContent>
</Alert>
<button @onclick="@(() => showAlert = true)">
    Show Alert
</button>
@code {
    private bool showAlert = false;
}

在前面的标记中,ChildContent元素突出显示为。

重要说明

按照惯例,用于捕获父元素内容的RenderFragment参数的名称必须是ChildContent

通过在标记中明确指定每个参数的名称,可以在组件中包含多个RenderFragment参数。我们将使用多个RenderFragment参数来完成本章中的项目。

RenderFragment参数使父组件能够传达其子组件要使用的用户界面内容,而EventCallback参数用于从子组件传达回父组件。在下一节中,我们将看到如何使用EventCallback参数。

事件回调参数

事件回调是您传递给另一个方法的方法,当特定事件发生时调用。例如,当点击Alert组件上的按钮时,@onclick事件使用OnOk参数来确定应该调用的方法。OnOK参数引用的方法在父组件中定义。

EventCallback参数用于从子组件向父组件共享信息。他们与他们的父母分享信息,并在发生事情时通知他们的父母,如按钮点击。父组件只是指定事件被触发时要调用的方法。

这是一个EventCallback参数的例子:

[Parameter] public EventCallback OnOk { get; set; }

以下示例使用λ表达式作为OnOk方法。调用OnOk方法时,showAlert属性的值设置为false:

<Alert Show="showAlert" OnOk="@(() => showAlert = false)">
        <h1>Alert</h1>
        <p>Today is @DateTime.Now.DayOfWeek.</p>
</Alert>
@code {
    private bool showAlert = false;
}

lambda 表达式允许您创建匿名函数。您不需要使用匿名函数。以下示例显示了如何为OnOk方法使用方法,而不是匿名函数:

<Alert Show="showAlert" OnOk="OkClickHandler)">
        <h1>Alert</h1>
        <p>Today is @DateTime.Now.DayOfWeek.</p>
</Alert>
@code {
    private bool showAlert = false;
    private void OkClickHandler()
    {
        showAlert = false;
    }
}

编写Alert组件时,您可能会尝试直接从组件上的OnOk事件更新Show参数。您不能这样做,因为如果您直接在组件中更新值,并且组件必须重新呈现,任何状态更改都将丢失。如果需要维护组件中的状态,应该向组件添加一个私有字段。

重要说明

组件永远不应该写入自己的参数。

Alert组件在页面上显示文本,但它还不像模态对话框那样工作。为了使它像模态对话框一样工作,我们需要更新组件使用的样式表。我们可以通过使用 CSS 隔离来做到这一点。在下一节中,我们将看到如何使用 CSS 隔离。

CSS 隔离

用来为我们的 Blazor WebAssembly 应用设计风格的级联样式表 ( CSS )的位置通常是wwwroot文件夹。通常,那些 CSS 文件中定义的样式会应用于网络应用中的所有组件。然而,有时您需要对应用于特定组件的样式进行更多控制。为了实现这一点,我们使用 CSS 隔离。使用 CSS 隔离,指定 CSS 文件中的样式将覆盖全局样式。

启用 CSS 隔离

为了给某个组件添加一个与隔离的CSS文件,在与该组件相同的文件夹中创建一个与该组件同名的CSS文件,但文件扩展名为CSS。例如,Alert.razor组件的CSS文件将被称为Alert.razor.css

以下标记用于更新版本的Alert组件。在这个版本中,我们添加了两个突出显示的类:

警惕剃刀

@if (Show)
{
    <div class="dialog-container">
        <div class="dialog">
            <div>
                @ChildContent
            </div>
            <div>
                <button @onclick="OnOk">
                    OK
                </button>
            </div>
        </div>
    </div>
} 
@code {
  [Parameter] public bool Show { get; set; }
  [Parameter] public EventCallback OnOk { get; set; }
  [Parameter] public RenderFragment ChildContent { get;
    set; }
}

以下CSS文件定义了新类使用的样式:

Alert.razor.css

.dialog-container {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    background-color: rgba(0,0,0,0.6);
    z-index: 2000;
}
.dialog {
    background-color: white;
    margin: auto;
    width: 15rem;
    padding: .5rem
}

前面的CSS包括dialog-container类和dialog类的样式:

  • dialog-container:这个类将元素的背景颜色设置为 60%不透明度的黑色,并通过将其 z-index 设置为 2,000 将其放置在其他元素的顶部。
  • dialog:这个类将元素的背景颜色设置为白色,在父元素内水平居中,宽度设置为 15 雷姆。

下面的截图显示了使用前面的CSS文件的Alert组件:

Figure 3.2 – Alert component

图 3.2–警报组件

支持子组件

默认情况下,使用 CSS 隔离时,CSS 样式仅适用于当前组件。如果希望 CSS 样式应用于当前组件的子组件,则需要在样式中使用::deep组合子。这个组合器选择元素标识符的后代元素。

例如,以下样式将应用于具有当前组件的任何H1标题,以及当前组件的子组件中的任何H1标题:

::deep h1 { 
    color: red;
}

如果你不想让你的组件使用全局样式,或者你想通过一个 Razor 类库共享你的组件,并且你需要避免样式冲突,CSS 隔离是非常有用的。

项目概述

在本章中,我们将构建一个模态对话框组件。您将能够使用 Razor 标记自定义模态对话框的TitleBody

这是模态对话框的截图:

Figure 3.3 – Modal dialog

图 3.3–模式对话框

最后,在我们完成模态对话框组件后,我们将把它移动到一个 Razor 类库中,这样它就可以与其他项目共享了。

该项目的构建时间约为 90 分钟。

创建模态对话框项目

将使用空 Blazor WebAssembly App 项目模板创建ModalDialog项目。我们将添加一个包含多个部分的Dialog组件,并使用 CSS 隔离来应用样式,使其表现得像一个模态对话框。单击按钮时,我们将使用EventCallback参数从组件返回到父组件。我们将使用RenderFragment参数来允许 Razor 标记从父组件传递到组件。最后,我们将创建一个 Razor 类库,并将我们的Dialog组件移动到其中,以便与其他项目共享。

开始项目

我们需要创建一个新的 Blazor WebAssembly 应用。我们按如下方式进行:

  1. 打开 Visual Studio 2019

  2. 点击新建项目按钮。

  3. In the Search for templates (Alt + S) textbox, enter Blazor and hit the Enter key.

    下面的截图展示了我们在 第二章 中创建的空 Blazor WebAssembly App 项目模板,构建你的第一个 Blazor WebAssembly 应用:

    Figure 3.4 – Empty Blazor WebAssembly App project template

    图 3.4–空 Blazor WebAssembly 应用项目模板

  4. 选择空 Blazor WebAssembly App 项目模板,点击下一步按钮。

  5. 项目名称文本框中输入ModalDialog,点击创建按钮:

图 3.5–配置您的新项目对话框

小费

在前面的例子中,我们将ModalDialog项目放入E:/Blazor文件夹中。然而,这个项目的位置并不重要。

我们已经创建了ModalDialog Blazor WebAssembly 项目。

添加对话框组件

Dialog组件将被共享。因此,我们将其添加到Shared文件夹中。我们按如下方式进行:

  1. 右键单击Shared文件夹,从菜单中选择添加,剃刀组件

  2. 命名新组件Dialog

  3. 点击添加按钮。

  4. Replace the markup with the following markup:

    @if (Show)
    {
        <div class="dialog-container">
            <div class="dialog">
                <div class="dialog-title">Title</div>
                <div class="dialog-body">Body</div>
                <div class="dialog-buttons">
                    <button class="btn btn-dark mr-2">
                        OK
                    </button>
                    <button class="btn btn-danger">
                        Cancel
                    </button>
                </div>
            </div>
        </div>
    }
    @code {
        [Parameter] public bool Show { get; set; }
    }
    

    Show属性用于显示和隐藏组件的内容。我们已经添加了一个Dialog组件,但是在适当的样式被添加到项目中之前,它不会表现得像一个模态对话框。

添加 CSS

前面的标记包括五个类,我们将使用它们来设置Dialog组件的样式,使其表现得像一个模态对话框:

  • dialog-container:这个类用于将元素的背景颜色设置为黑色,不透明度为 60%,并通过将其 z-index 设置为 2,000 将其放置在其他元素之上。

  • dialog:此类用于将元素的背景颜色设置为白色,在其父元素内水平居中,宽度设置为 25 REM。

  • dialog-title:这个类用来将背景颜色设置为深灰色,将文字设置为白色,并添加一些填充。

  • dialog-body:这个类用来添加一些填充。

  • dialog-buttons: This class is used to set the background color to silver and add some padding.

    小费

    Dialog组件使用的其余类,如btn,在wwwroot\css\bootstrap\bootstrap.min.css文件中定义,该文件包含由 Bootstrap 框架提供的样式。

我们需要创建一个CSS文件来定义如何为每个类设置样式。我们按如下方式进行:

  1. 右键单击Shared文件夹,从菜单中选择添加,新项目
  2. 搜索框中输入css
  3. 选择样式表
  4. 命名样式表Dialog.razor.css
  5. 点击添加按钮。
  6. 输入以下样式:

Dialog.razor.css

.dialog-container {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    background-color: rgba(0,0,0,0.6);
    z-index: 2000;
}
.dialog {
    background-color: white;
    margin: auto;
    width: 25rem;
}
.dialog-title {
    background-color: #343a40;
    color: white;
    padding: .5rem;
}
.dialog-body {
    padding: 2rem;
}
.dialog-buttons {
    background-color: silver;
    padding: .5rem;
}

由于 CSS 隔离,Dialog.razor.cs文件中的样式将仅由Dialog组件使用。

测试对话框组件

为了测试Dialog组件,我们需要将其添加到另一个组件中。我们将把它添加到用作应用的主页Index组件中。我们按如下方式进行:

  1. 打开Pages\Index.razor文件。

  2. Add the following markup to the Index.razor file:

    <Dialog Show="showDialog"></Dialog>
    <button @onclick="OpenDialog">Show Dialog</button>
    @code {
        private bool showDialog = false;
        private void OpenDialog()
        {
            showDialog = true;
        }
    }
    

    确保您没有删除文件顶部的@page指令。

  3. 调试菜单中,选择开始不调试 ( Ctrl + F5 )选项以运行项目。

  4. Click the Show Dialog button:

    Figure 3.6 – Simple modal dialog

    图 3.6–简单模式对话框

  5. 点击确定按钮。

当您单击确定按钮时,不会发生任何事情,因为我们尚未添加@onclick事件。

添加事件回调参数

我们需要为添加确定按钮和取消按钮。我们按如下方式进行:

  1. 返回 Visual Studio

  2. 打开Shared\Dialog.razor文件。

  3. @onclick事件添加到每个按钮中:

    <button class="btn btn-dark mr-2" @onclick="OnOk">
        OK
    </button>
    <button class="btn btn-danger" @onclick="OnCancel">
        Cancel
    </button>
    
  4. Add the following parameters to the code block:

    [Parameter] 
    public EventCallback<MouseEventArgs> OnOk { get; set; }
    [Parameter] 
    public EventCallback<MouseEventArgs> OnCancel { get; set; }
    

    小费

    Parameter装饰器不需要与其应用的属性在同一行。

  5. 打开Pages\Index.razor文件。

  6. 通过添加突出显示的标记来更新Dialog元素的标记:

    <Dialog Show="showDialog" 
            OnCancel="DialogCancelHandler" 
            OnOk="DialogOkHandler">
    </Dialog>
    
  7. 在代码块中添加以下方法:

    private void DialogCancelHandler(MouseEventArgs e)
    {
        showDialog = false;
    }
    private void DialogOkHandler(MouseEventArgs e)
    {
        showDialog = false;
    }
    
  8. 构建菜单中,选择构建解决方案选项。

  9. 返回浏览器。

  10. 使用 Ctrl + R 刷新浏览器。

  11. 点击显示对话框按钮。

  12. 点击确定按钮。

点击确定按钮,对话框关闭。现在让我们更新Dialog组件,允许我们自定义它创建的模态对话框的TitleBody属性。

添加渲染片段参数

我们将对Dialog组件的TitleBody属性使用RenderFragment参数。我们按如下方式进行:

  1. 返回 Visual Studio

  2. 打开Shared\Dialog.razor文件。

  3. dialog-title的标记更新为如下:

    <div class="dialog-title">@Title</div>
    
  4. dialog-body的标记更新为如下:

    <div class="dialog-body">@Body</div>
    
  5. 向代码块添加以下参数:

    [Parameter]
    public RenderFragment Title { get; set; }
    [Parameter]
    public RenderFragment Body { get; set; }
    
  6. 打开Pages\Index.razor文件。

  7. Update the markup for the Dialog element to the following:

    <Dialog Show="showDialog"
            OnCancel="DialogCancelHandler"
            OnOk="DialogOkHandler">
        <Title>Quick List [@(Items.Count + 1)]</Title>
        <Body>
            Enter New Item: <input @bind="NewItem" />
        </Body>
    </Dialog>
    

    前面的标记将对话框的标题改为Quick List,并为用户输入列表项目提供一个文本框。

  8. Add the following markup under the Dialog element:

    <ol>
        @foreach (var item in Items)
        {
            <li>@item</li>
        }
    </ol>
    

    前面的代码将以有序列表的形式显示Items列表中的项目。

  9. 将以下变量添加到代码块的顶部:

    private string NewItem;
    private List<string> Items = new List<string>();
    
  10. Update DialogCancelHandler to the following:

```cs
private void DialogCancelHandler(MouseEventArgs e)
{
    NewItem = "";
    showDialog = false;
}
```

前面的代码将清除文本框并隐藏`Dialog`。
  1. Update DialogOkHandler to the following:
```cs
private void DialogOkHandler(MouseEventArgs e)
{
    if (!string.IsNullOrEmpty(NewItem))
    {
        Items.Add(NewItem);
        NewItem = "";
    };
    showDialog = false;
}
```

前面的代码将把`NewItem`添加到`Items`列表中,清除文本框,并隐藏`Dialog`。
  1. 构建菜单中,选择构建解决方案选项。
  2. 返回浏览器。
  3. 使用 Ctrl + R 刷新浏览器。
  4. 点击显示对话框按钮。
  5. 输入新项目字段输入一些文本。
  6. 点击确定按钮。
  7. Repeat.
每次点击**确定**按钮时,**输入新项目**字段中的文本将被添加到列表中。下面的屏幕截图显示了一个列表,其中已经添加了三个项目,第四个项目将使用模态对话框添加:

![](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/blazor-webasm-exam/img/Figure_3.07_B16786.jpg)

图 3.7–快速列表示例
  1. 关闭浏览器。

为了与其他项目共享这个新组件,我们需要将其添加到一个 Razor 类库中。

创建 Razor 类库

我们可以通过使用 Razor 类库跨项目共享组件。为了创建一个 Razor 类库,我们将使用 Razor 类库项目模板。我们按如下方式进行:

  1. 右键单击解决方案,并从菜单中选择添加,新项目选项。

  2. Enter Razor Class Library in the Search for templates textbox to locate the Razor Class Library project template:

    Figure 3.8 – Razor Class Library project template

    图 3.8–Razor 类库项目模板

  3. 选择剃刀类库项目模板。

  4. 点击下一步按钮。

  5. 命名项目MyComponents并点击创建按钮。

  6. 接受默认值,点击创建按钮。

  7. 右键单击ModalDialog项目,从菜单中选择添加,项目参考选项。

  8. 勾选MyComponents复选框,点击确定按钮。

我们已经创建了一个示例 Razor 类库。

测试 Razor 类库

我们刚刚创建的示例 Razor 类库包含一个名为Component1的组件。在我们继续之前,我们应该测试新的 Razor 类库是否正常工作。我们按如下方式进行:

  1. 打开ModalDialog\Pages\ Index.razor文件。

  2. Add the following @using statement right below the @page directive:

    @using MyComponents;
    

    小费

    如果您将在多个页面上使用该项目,您应该将@using语句直接添加到ModalDialog\_Imports.razor文件中。

  3. @using语句下面添加以下标记:

    <Component1 />
    
  4. From the Debug menu, select the Start Without Debugging (Ctrl + F5) option to run the project.

    下面的截图显示了Component1组件应该如何渲染:

    Figure 3.9 – Component1

    图 3.9–组件 1

    重要说明

    如果Component1组件缺少样式,那是因为 CSS 文件被缓存了。使用以下组合键Ctrl+Shift+R,清空缓存并重新加载页面。

  5. 返回 Visual Studio

  6. 删除Component1元素。

我们已经完成了对示例 Razor 类库的测试。

向 Razor 类库添加组件

为了共享Dialog组件,我们需要将其移动到我们刚刚创建和测试的 Razor 类库中。我们按如下方式进行:

  1. 右键单击ModalDialog\Shared\Dialog.razor文件,从菜单中选择复制选项。

  2. 右键单击MyComponents项目,从菜单中选择粘贴选项。

  3. 右键单击MyComponents\Dialog.razor文件,从菜单中选择重命名选项。

  4. Rename the file to BweDialog.razor.

    在这种情况下,Bwe代表Blazor web assembly by Example

    小费

    在给 Razor 类库中的组件命名时,应该给它们唯一的名称,以避免不明确的引用错误。大多数组织都用相同的文本作为所有共享组件的前缀。以为例,一家名为一站式设计 ( OSD )的公司可能会在所有共享组件前加上Osd

  5. 打开ModalDialog\Pages\Index.razor文件。

  6. Dialog元素重命名为BweDialog

  7. 构建菜单中,选择构建解决方案选项。

  8. 返回浏览器。

  9. 使用 Ctrl + R 刷新浏览器。

  10. 点击显示对话框按钮。

  11. 输入新项目字段输入一些文本。

  12. 点击确定按钮。

  13. 重复一遍。

BweDialog组件现在正从MyComponents项目中使用。由于它现在包含在剃刀类库中,您可以很容易地与其他项目共享它。

总结

现在,您应该能够创建一个模态对话框,并通过使用 Razor 类库与多个项目共享它。

本章我们介绍了RenderFragment参数、EventCallback参数以及 CSS 隔离。

之后,我们使用空 Blazor App 项目模板创建了一个新项目。我们添加了一个Dialog组件,它就像一个模态对话框。Dialog组件使用RenderFragment参数和EventCallback参数在它和它的父组件之间共享信息。此外,它的样式使用了 CSS 隔离。

在本章的最后一部分,我们创建了一个 Razor 自定义库,并将Dialog组件移动到了新的库中。

到目前为止,在本书中,我们避免使用 JavaScript。不幸的是,仍然有一些功能我们只能用 JavaScript 来完成。我们将在本书的下一章看到如何在 Blazor WebAssembly 中使用 JavaScript 互操作来使用 JavaScript。

问题

以下问题供您考虑:

  1. 如何用模板化组件替换表?
  2. 如何为Dialog组件的Title属性和Body属性添加默认值?
  3. 如果希望 Razor 类库中的所有组件都使用相同的 CSS 文件,可以将样式移动到共享的 CSS 文件中吗?
  4. 你能使用 NuGet 包分发你的Dialog组件吗?

进一步阅读

以下资源提供了有关本章主题的更多信息:

四、使用 JavaScript 互操作性构建本地存储服务

Blazor WebAssembly 框架让我们可以在浏览器上运行 C#代码。然而,有一些场景它无法处理,在这些情况下,我们需要使用 JavaScript 函数来填补空白。

在本章中,我们将学习如何将 JavaScript 与 Blazor WebAssembly 一起使用。我们将学习如何在有和没有返回值的情况下从 Blazor 调用一个 JavaScript 函数。相反,我们将学习如何调用.NET 方法。我们将通过使用 JavaScript 互操作 ( JS 互操作)来完成这两个场景。最后,我们将学习如何使用本地存储在浏览器上存储数据。

我们将在本章中创建的项目将是一个本地存储服务,它将读写浏览器的本地存储。为了访问浏览器的本地存储,我们需要使用 JavaScript。JS 互操作用于从. NET 调用 JavaScript

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

  • 为什么要用 JavaScript?
  • 探索 JS 互操作
  • 了解本地存储
  • 从 Blazor 调用一个 JavaScript 函数
  • 从 JavaScript 调用. NET 方法
  • 创建本地存储服务

技术要求

要完成此项目,您需要在电脑上安装 Visual Studio 2019。关于如何安装 Visual Studio 2019 免费社区版的说明,请参考 第 1 章 、Blazor WebAssembly 简介。您还需要我们在 第 2 章 中创建的空 Blazor WebAssembly App 项目模板,构建您的第一个 Blazor WebAssembly 应用

本章的源代码可在以下 GitHub 存储库中获得:https://GitHub . com/PacktPublishing/Blazor-web assembly by Example/tree/main/chapter 04

行动中的代码视频可在此获得:https://bit.ly/3tXVMeg

为什么要用 JavaScript?

有了 Blazor WebAssembly,你可以创建完整的应用,而无需直接使用 JavaScript。但是,您可能需要使用 JavaScript,因为有些场景没有它是无法完成的。没有 JavaScript,您就不能操纵 DOM 或调用我们在 web 开发中所依赖的任何 JavaScript APIs。

这是您不能直接从 Blazor WebAssembly 框架访问的内容的示例:

  • DOM 操作
  • 媒体捕获和流应用编程接口
  • 网络图形接口(网络的 2D 和 3D 图形)
  • 网络存储应用编程接口(本地存储和会话存储)
  • 地理定位应用编程接口
  • JavaScript 弹出框(提醒确认提示)
  • 浏览器的在线状态
  • 浏览器的历史
  • Chart.js
  • 其他第三方 JavaScript 库

前面的列表一点也不全面,因为目前有数百个 JavaScript 库可用。但是,要记住的关键一点是,不使用 JavaScript 就无法操纵 DOM。因此,我们可能总是需要在网络应用中使用一些 JavaScript。幸运的是,通过使用 JS 互操作,这很容易做到。

探索 JS 互操作

从调用一个 JavaScript 函数.NET,我们使用IJSRuntime抽象。这个抽象表示框架可以调用的 JavaScript 运行时的一个实例。要使用IJSRuntime,我们必须首先使用依赖注入将其注入到我们的组件中。有关依赖注入的更多信息,请参考 第 6 章 【使用应用状态构建购物车】

@inject指令用于向组件注入依赖关系。以下代码将IJSRuntime注入到当前组件中:

@inject IJSRuntime js

IJSRuntime抽象有两种方法可以用来调用 JavaScript 函数:

  • InvokeVoidAsync
  • InvokeAsync

这两种方法都是异步的。这两种方法的区别在于,其中一种方法返回值,而另一种方法不返回值。我们可以将IJSRuntime的一个实例降级为IJSInProcessRuntime的一个实例,同步运行该方法。最后,我们可以通过用JsInvokable修饰方法,从 JavaScript 中调用一个. NET 方法。

InvokeVoidAsync

InvokeVoidAsync方法用于调用不返回值的 JavaScript 函数。它异步调用指定的 JavaScript 函数。

这是IJsRuntimeInvokeVoidAsync法:

InvokeVoidAsync(string identifier, params object[] args);

第一个参数是被调用的 JavaScript 方法的标识符,第二个参数是 JSON 可序列化参数的数组。第二个参数是可选的。

在 JavaScript 中,Document对象代表 HTML 文档的根节点。Document对象的title属性用于指定浏览器标题栏中显示的文本。假设我们想要在我们的 Blazor WebAssembly 应用中的组件之间导航时更新浏览器的标题。为此,我们需要使用 JavaScript 更新title属性。

下面的 JavaScript 代码定义了一个名为setDocumentTitle的方法,该方法将Document对象的title属性设置为title参数提供的值:

bweinterop . js

var bweInterop = {};
bweInterop.setDocumentTitle = function (title) {
    document.title = title;
}

小费

在本书中,我们将为我们的 JavaScript 代码使用bweInterop命名空间,以构建我们的代码并最小化命名冲突的风险。

在我们可以访问前面的 JavaScript 代码之前,我们需要从wwwroot/index.html文件中添加对它的引用。下面突出显示的代码是对 JavaScript 文件的引用。它假设它已经被放入名为scripts的文件夹中:

<script src="scripts/bweInterop.js"></script>
<script src="_framework/Blazor.webassembly.js"></script> 

新的脚本标签应该添加在引用wwwroot/index.html文件的body元素中的_framework/Blazor.webassembly.js文件的脚本标签之前。

以下Document组件使用setDocumentTitle JavaScript 函数更新浏览器的标题栏:

文档剃刀

@inject IJSRuntime js
@code {
    [Parameter] public string Title { get; set; }
    protected override async Task OnAfterRenderAsync(bool 
      firstRender)
    {
        if (firstRender)
        {
            await js.InvokeVoidAsync(
                "bweInterop.setDocumentTitle",
                Title);
        }
    }
} 

在前面的代码中,IJSRuntime被注入到组件中。然后,OnAfterRenderAsync方法使用IJSRuntimeInvokeVoidAsync方法在第一次渲染组件时调用setDocumentTitle JavaScript 函数。

以下标记使用Document组件将浏览器的标题栏更新为Home – My App:

<Document Title="Home - My App" />

以下截图显示了更新后的文档标题:

Figure 4.1 – Updated document title

图 4.1–更新的文档标题

InvokeVoidSync方法用于调用不返回值的 JavaScript 函数。如果我们需要返回值,我们需要使用InvokeAsync方法来代替。

调用同步

当我们想要调用一个返回值的 JavaScript 函数时,使用InvokeAsync方法。它异步调用指定的 JavaScript 函数。

这是IJSRuntimeInvokeAsync法:

ValueTask<TValue> InvokeAsync<TValue>(string identifier, 
                                     params object[] args);

就像InvokeVoidAsync方法一样,第一个参数是 JavaScript 方法的标识符,第二个参数是 JSON 可序列化参数的数组。第二个参数是可选的。InvokeAsync方法返回一个TValue类型的ValueTaskTValue是 JavaScript 返回值的 JSON 反序列化实例。

在 JavaScript 中,Window对象代表浏览器的窗口。如果我们需要确定当前窗口的宽度和高度,我们可以使用Window对象的innerWidthinnerHeight属性。

下面的 JavaScript 代码定义了一个名为getWindowSize的方法,该方法返回Window对象的宽度和高度:

bweinterop . js

var bweInterop = {};
bweInterop.getWindowSize = function () {
    var size = {
        width: window.innerWidth,
        height: window.innerHeight
    }
    return size;
}

这是用于存储窗口大小的WindowSize类的定义.NET:

public class WindowSize
{
    public int? Width { get; set; }
    public int? Height { get; set; }
}

以下Index组件从bweInterop.js文件调用GetWindowSize方法:

索引剃刀

@page "/"
@inject IJSRuntime js
@if (windowSize.Width != null)
{
    <h2>
        Window Size: @windowSize.Width x @windowSize.Height
    </h2>
}
<button @onclick="GetWindowSize">Get Window Size</button>
@code{
    private WindowSize windowSize = new WindowSize();
    private async Task GetWindowSize()
    {
        windowSize = await js.InvokeAsync<WindowSize>(
            "bweInterop.getWindowSize");
    }
}

在前面的代码中,IJSRuntime被注入到组件中。点击获取窗口大小按钮后,GetWindowSize方法使用IJSRuntimeInvokeAsync方法调用getWindowSize JavaScript 函数。GetWindowSize JavaScript 函数将窗口的宽度和高度返回到windowSize属性。最后,组件重新生成其渲染树,并将任何更改应用于浏览器的 DOM。

这是点击获取窗口大小按钮后的页面截图:

Figure 4.2 – Window size example

图 4.2–窗口大小示例

从调用 JavaScript.NET 同步

到目前为止,在本章中,我们只关注异步调用 JavaScript 函数。但是我们也可以同步调用 JavaScript 函数。我们通过将IJSRuntime下调至IJSInProcessRuntime来实现。IJSInProcessRuntime表示 JavaScript 运行时的一个实例,调用可以被分派到该实例。

IJSInProcessRuntime允许我们的.NET 代码来同步调用 JS 互操作调用。这可能是有利的,因为这些调用比异步调用具有更少的开销。这些方法类似于异步方法:

  • InvokeVoid
  • Invoke

以下代码是本章前面Document组件的同步版本。它使用IJSInProcessRuntime同步调用 JavaScript 函数:

DocumentSync.razor

@inject IJSRuntime js
@code {
    [Parameter] public string Title { get; set; }
    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            ((IJSInProcessRuntime)js).InvokeVoid(
                "bweInterop.setDocumentTitle",
                Title);
        }
    }
}

在前面的代码中,IJsRuntime实例已经向下转换为IJSInProcessRuntime实例。IJSInProcessRuntime实例的InvokeVoid方法是用来调用setDocumentTitle JavaScript 方法的。

以下标记使用了DocumentSync组件:

<DocumentSync Title="Home - My App" />

调用.NET 从 JavaScript

我们可以调用一个公共.NET 方法,方法是用JSInvokable属性修饰该方法。

下面的方法用JSInvokable属性修饰,使其能够从 JavaScript 中调用:

[JSInvokable]
public void GetWindowSize(WindowSize newWindowSize)
{
    windowSize = newWindowSize;
    StateHasChanged();
}

在前面的代码中,windowSize属性在每次从 JavaScript 中调用GetWindowSize方法时都会更新。组件的StateHasChanged方法被调用来通知组件它的状态已经改变,组件应该被重新渲染。

小费

组件的StateHasChanged方法仅对EventCallback方法自动调用。在其他情况下,必须手动调用它来通知用户界面它可能需要重新呈现。

要从 JavaScript 调用. NET 方法,必须创建一个DotNetObjectReferenece类供 JavaScript 使用,以便定位.NET 方法。DotNetObjectReferenece类包装了一个 JS 互操作参数,指示值不应该序列化为 JSON,而应该作为引用传递。

重要说明

为了避免内存泄漏并允许在创建DotNetObjectReference类的组件上进行垃圾收集,我们必须努力处理DotNetObjectReference的每个实例。

下面的代码创建了一个包装Resize组件的DotNetObjectReference实例。然后,该引用被传递给 JavaScript 方法:

private DotNetObjectReference<Resize> objRef;
protected async override Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        objRef = DotNetObjectReference.Create(this);
        await js.InvokeVoidAsync(
            "bweInterop.registerResizeHandler",
             objRef);
    }
}

您可以使用对使用DotNetObjectReference创建的组件的引用,从 JavaScript 调用. NET 组件中的方法。在下面的 JavaScript 中,registerResizeHandler函数创建了resizeHandler,在初始化时和每次调整窗口大小时都会调用它。

您可以使用invokeMethodinvokeMethodAsync函数来调用.NET 实例方法来自 JavaScript。以下示例使用invokeMethodAsync函数调用用JSInvokable属性修饰的GetWindowSize方法:

bweinterop . js

bweInterop.registerResizeHandler = function (dotNetObjectRef) {
    function resizeHandler() {
        dotNetObjectRef.invokeMethodAsync('GetWindowSize',
            {
                width: window.innerWidth,
                height: window.innerHeight
            });
    };
    resizeHandler();
    window.addEventListener("resize", resizeHandler);
}

这就是的完整。Resize组件的净代码:

调整大小。剃刀

@page "/resize"
@inject IJSRuntime js
@implements IDisposable
@if (windowSize.Width != null)
{
    <h2>
        Window Size: @windowSize.Width x @windowSize.Height
    </h2>
}
@code {
    private DotNetObjectReference<Resize> objRef;
    private WindowSize windowSize = new WindowSize();
    protected async override Task OnAfterRenderAsync(bool 
      firstRender)
    {
        if (firstRender)
        {
            objRef = DotNetObjectReference.Create(this);
            await js.InvokeVoidAsync(
                "bweInterop.registerResizeHandler",
                    objRef);
        }
    }
    [JSInvokable]
    public void GetWindowSize(WindowSize newWindowSize)
    {
        windowSize = newWindowSize;
        StateHasChanged();
    }
    public void Dispose()
    {
        objRef?.Dispose();
    }
}

Resize组件的上述代码显示了浏览器的当前宽度和高度。调整浏览器大小时,显示的值会自动更新。此外,DotNetObjectReference对象在部件被处理时被处理掉。

IJSRuntime抽象为我们提供了一种从调用 JavaScript 函数的方法.NET 并调用.NET 方法。我们将使用 JavaScript 的网络存储应用编程接口来完成本章中的项目。但是在我们使用它之前,我们需要了解它是如何工作的。

了解本地存储

JavaScript 的 Web 存储 API 为浏览器提供了存储键/值对的机制。对于每个网络浏览器,可以存储在网络存储器中的数据大小至少为每个源 5 MB。本地存储是在 JavaScript 的网络存储应用编程接口中定义的。我们需要使用 JS 互操作来访问浏览器上的 localStorage。

浏览器的本地存储范围是特定的网址。如果用户重新加载页面或关闭并重新打开浏览器,本地存储的内容将被保留。如果用户打开多个选项卡,每个选项卡共享同一个本地存储。本地存储中的数据会一直保留到明确清除为止,因为它没有到期日期。

小费

当最后一个选项卡关闭时,使用 InPrivate 窗口或 Incognito 窗口创建的本地存储对象中的数据将被清除。

这些是本地存储的方法:

  • key:该方法根据指定键在 localStorage 中的位置返回指定键的名称。

  • getItem:该方法从 localStorage 返回指定键的值。

  • setItem:这个方法获取一个键值对,并将它们添加到 localStorage。

  • removeItem:此方法从 localStorage 中移除指示的键。

  • clear: This method clears localStorage.

    小费

    会话存储也是在网络存储应用编程接口中定义的。与在多个浏览器选项卡之间共享其值的本地存储不同,会话存储的范围仅限于单个浏览器选项卡。因此,如果用户重新加载页面,数据仍然存在,但是如果用户关闭选项卡(或浏览器),数据将被清除。

要查看浏览器本地存储器的内容,打开开发者工具(F12) 并选择应用选项卡。在左侧菜单的存储部分查找本地存储。以下截图显示了开发工具对话框的应用选项卡:

Figure 4.3 – Local Storage

图 4.3–本地存储

通过使用网络存储应用编程接口,可以很容易地在浏览器中存储数据并检索数据。现在,让我们快速了解一下我们将在本章中构建的项目。

项目概述

在本章中,我们将构建一个本地存储服务。该服务将写入和读取浏览器的本地存储。我们将使用 JS 互操作来实现这一点。最后,我们将创建一个组件来测试我们的服务:

Figure 4.4 – Local Storage Service test page

图 4.4–本地存储服务测试页面

这个项目的构建时间大约为 60 分钟。

创建本地存储服务

将使用空 Blazor WebAssembly 应用项目模板创建LocalStorage项目。首先,我们将添加一个带有 JavaScript 函数的 JavaScript 文件,我们的服务将需要使用这些函数来更新浏览器的 localStorage。接下来,我们将使用.NET 方法调用 JavaScript 函数。最后,我们将测试我们的服务。

创建本地存储服务项目

我们需要创建一个新的 Blazor WebAssembly 应用。我们按如下方式进行:

  1. 打开 Visual Studio 2019

  2. 点击新建项目按钮。

  3. In the Search for templates (Alt+S) textbox, enter Blazor and hit the Enter key.

    以下截图显示了我们在 第二章 中创建的空 Blazor WebAssembly App 项目模板,构建您的第一个 Blazor WebAssembly 应用:

    Figure 4.5 – Empty Blazor WebAssembly App project template

    图 4.5–空 Blazor WebAssembly 应用项目模板

  4. 选择空 Blazor WebAssembly App 项目模板,点击下一步按钮。

  5. 项目名称文本框中输入LocalStorage,然后点击创建按钮:

Figure 4.6 – Configure your new project dialog

图 4.6–配置新项目对话框

小费

在前面的例子中,我们将LocalStorage项目放入E:/Blazor文件夹中。然而,这个项目的位置并不重要。

我们现在已经创建了 Blazor WebAssembly 项目。

编写 JavaScript 访问本地存储

我们需要编写 JavaScript 函数,这些函数将读写浏览器的 localStorage。我们按如下方式进行:

  1. 右键单击wwwroot文件夹,从菜单中选择添加,新文件夹选项。

  2. 命名新文件夹scripts

  3. 右键单击scripts文件夹,从菜单中选择添加,新项目选项。

  4. 搜索框中输入javascript

  5. 选择 JavaScript 文件

  6. 命名文件bweInterop.js

  7. 点击添加按钮。

  8. 输入以下 JavaScript:

    var bweInterop = {};
    bweInterop.setLocalStorage = function (key, data) {
        localStorage.setItem(key, data);
    }
    bweInterop.getLocalStorage = function (key) {
        return localStorage.getItem(key);
    }
    
  9. 打开wwwroot\index.html文件。

  10. body元素中添加以下引用:

```cs
<script src="scripts/bweInterop.js"></script>
```
  1. 请确保您将添加到引用_framework/Blazor.webassembly.js之前。

添加 ILocalStorageService 接口

我们需要为我们的服务创建一个接口。我们按如下方式进行:

  1. 右键单击LocalStorage项目,从菜单中选择添加,新文件夹选项。

  2. 命名新文件夹Services

  3. 右键单击Services文件夹,然后从菜单中选择添加,新项目选项。

  4. 搜索框中输入interface

  5. 选择界面

  6. 命名文件ILocalStorageService

  7. 点击添加按钮。

  8. 用以下高亮显示的代码更新【T0:

    interface ILocalStorageService
    {
     Task SetItemAsync<T>(string key, T item);
     Task<T> GetItemAsync<T>(string key);
    }
    

创建本地存储服务类

我们需要基于我们刚刚创建的接口创建一个新的类。我们按如下方式进行:

  1. 右键单击Services文件夹,从菜单中选择添加,类别选项。

  2. 命名新类LocalStorageService

  3. 将代码更新到LocalStorageService类以从ILocalStorageSerivce继承:

    public class LocalStorageService : ILocalStorageService
    {
    }
    
  4. 右键单击ILocalStorageService,从菜单中选择执行界面选项。

  5. Add the following code to the LocalStorageService class:

    private IJSRuntime js;
    public LocalStorageService(IJSRuntime JsRuntime)
    {
        js = JsRuntime;
    }
    

    前面的代码定义了LocalStorageService类的构造函数。

  6. 增加以下using语句:

    using Microsoft.JSInterop;
    
  7. Update the SetItemAsync method to the following:

    public async Task SetItemAsync<T>(string key, T item)
    {
        await js.InvokeVoidAsync(
            "bweInterop.setLocalStorage",
            key,
            JsonSerializer.Serialize(item));
    }
    

    SetItemAsync方法调用bweInterop.setLocalStorage JavaScript 函数,该函数带有要存储在 localStorage 中的项的密钥和序列化版本。

  8. using语句后添加:

    using System.Text.Json;
    
  9. Update the GetItemAsync method to the following:

    public async Task<T> GetItemAsync<T>(string key)
    {
        var json = await js.InvokeAsync<string>(
            "bweInterop.getLocalStorage",
            key);
        return string.IsNullOrEmpty(json)
                ? default
                : JsonSerializer.Deserialize<T>(json);
    }
    

    GetItemAsync方法用一个键调用bweInterop.getLocalStorage JavaScript 函数。如果bweInterop.getLocalStorage返回一个值,该值将被反序列化并返回。

我们已经完成了服务。现在我们需要测试一下。

写入本地存储

我们需要使用我们的本地存储服务测试写入浏览器的本地存储。我们按如下方式进行:

  1. 打开Pages\Index.razor文件。

  2. Add the following markup:

    @using LocalStorage.Services
    @inject IJSRuntime js
    <h2>Local Storage Service</h2>
    <div>
        Data:
        <input type="text"
               @bind-value="data"
               size="50" />
    </div>
    <div class="pt-2">
        <button class="btn btn-primary"
                @onclick="SaveToLocalStorageAsync">
            Save to Local Storage
        </button>
    </div>
    @code {
    }
    

    前面的标记为要保存到浏览器的 localStorage 中的数据添加了一个文本框和一个用于调用SaveToLocalStorageAsync方法的按钮。

  3. Add the following code to the code block:

    private string data;
    private LocalStorageService localStorage;
    protected override void OnInitialized()
    {
        localStorage = new LocalStorageService(js);
    }
    async Task SaveToLocalStorageAsync()
    {
        await localStorage.SetItemAsync<string>(
            "localStorageData", 
            data);
    }
    

    前面的代码初始化了组件并定义了SaveToLocalStorageAsync方法。将数据保存到本地存储时,SaveToLocalStorageAsync方法使用localStorageData作为密钥。

    调试菜单中,选择不调试启动 ( Ctrl + F5 )选项运行项目:

    Figure 4.7 – Local Storage Service test page

    图 4.7–本地存储服务测试页面

  4. 数据文本框中输入字Test

  5. 点击保存到本地存储按钮。

  6. 点击 F12 打开开发工具。

  7. 选择应用选项卡。

  8. 打开本地存储

以下截图显示了localStorageData的值:

Figure 4.8 – Local storage

图 4.8–本地存储

我们已经使用 本地存储应用编程接口将数据保存到浏览器的本地存储。接下来,我们需要学习如何读取浏览器的 localStorage。

从本地存储器读取

我们需要使用我们的本地存储服务测试从浏览器的本地存储读取。我们按如下方式进行:

  1. 返回 Visual Studio

  2. 打开Pages\Index.razor文件。

  3. Add the following button beneath the existing button:

    <button class="btn btn-primary"
            @onclick="ReadFromLocalStorageAsync">
        Read from Local Storage
    </button>
    

    前面的标记添加了一个用于调用ReadFromLocalStorageAsync方法的按钮。

  4. Add the following method to the code block:

    async Task ReadFromLocalStorageAsync()
    {
        data = await localStorage.GetItemAsync<string>(
            "localStorageData");
    }
    

    前面的代码定义了ReadFromLocalStorageAsync方法。ReadFromLocalStorageAsync在访问浏览器的本地存储时使用localStorageData键。

  5. 构建菜单中,选择构建解决方案选项。

  6. 返回浏览器。

  7. 使用 Ctrl + R 刷新浏览器。

  8. 点击从本地存储器读取按钮。

我们现在已经完成了本地存储服务的测试。

总结

现在,您应该能够通过使用 JS 互操作从 Blazor WebAssembly 应用中调用 JavaScript 函数来创建本地存储服务。

在这一章中,我们解释了为什么您仍然需要使用 JavaScript,以及如何使用 IJSRuntime 抽象从调用 JavaScript 函数.NET,同步和异步。相反,我们解释了如何调用.NET 方法。最后,我们解释了如何使用 localStorage 在浏览器中存储数据。

之后,我们使用空 Blazor App 项目模板创建了一个新项目。我们添加了一些 JavaScript 函数来读写本地存储。然后,我们添加了一个类来调用这些 JavaScript 函数。

在本章的最后一部分,我们测试了本地存储服务。

使用 Blazor WebAssembly 的最大好处之一是所有代码都在浏览器上运行。这意味着使用 Blazor WebAssembly 构建的 web 应用可以脱机运行。在下一章中,我们将利用这一优势来创建一个进步的 web 应用。

问题

以下问题供您考虑:

  1. IJSRuntime可以用来渲染一个 UI 吗?
  2. 您如何将我们的本地存储服务添加到 Razor 类库中?
  3. 在哪些情况下,您会使用会话存储而不是本地存储?
  4. 本地存储安全吗?
  5. 如何验证浏览器支持 localStorage 并且可以使用?

进一步阅读

以下资源提供了有关本章所涵盖主题的更多信息:

五、做为渐进式的网络应用(PWA)的天气应用

作为网络开发人员,我们开发各种令人惊叹的网络应用,但直到最近,网络应用能做什么和本地应用能做什么之间一直存在分歧。一种叫做渐进式网络应用 ( PWAs )的新类别的应用正在通过在我们的网络应用中实现类似本机的功能、可靠性和可安装性来帮助弥合这一鸿沟。PWA 是一个网络应用,它利用了原生应用的功能,同时保留了网络应用的所有功能。

在本章中,我们将学习什么定义了 PWA,以及如何通过向现有 web 应用添加清单文件服务人员来创建 PWA。

我们在本章中创建的项目将是一个本地 5 天天气预报应用,可以作为本机应用安装和运行在 Windows、MAC、iPhones、Android 手机等上,并可以通过各种应用商店分发。我们将使用 JavaScript 的地理定位 API 来获取设备的位置,并使用开放天气一呼 API 来获取该位置的天气预报。我们将通过添加一个清单文件和一个服务人员将应用转换成一个 PWA。服务人员将使用缓存存储应用编程接口来缓存信息,以便 PWA 可以脱机工作。

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

  • 了解普华永道
  • 使用清单文件
  • 与服务人员一起工作
  • 使用缓存存储 api
  • 使用地理定位应用编程接口
  • 使用开放天气一键呼叫应用编程接口
  • 创建 PWA

技术要求

要完成这个项目,你需要在你的电脑上安装 Visual Studio 2019 。关于如何安装 Visual Studio 2019 免费社区版的说明,请参考 第 1 章 、Blazor WebAssembly 简介。您还需要我们在 第 2 章 中创建的空 Blazor WebAssembly App 项目模板,构建您的第一个 Blazor WebAssembly 应用

我们将使用外部天气应用编程接口来访问我们项目的天气预报数据。我们将使用的应用编程接口是开放天气一键调用应用编程接口,用于获取当前、预测和历史天气数据。这是由开放天气(https://openweathermap.org提供的免费 API。为了开始使用这个应用编程接口,您需要创建一个帐户并获取一个应用编程接口密钥。如果您不想创建帐户,可以使用我们在 GitHub 存储库中为本章提供的weather.json文件。

本章的源代码可在以下 GitHub 存储库中获得:https://GitHub . com/PacktPublishing/Blazor-web assembly by Example/tree/main/chapter 05

行动中的代码视频可在此获得:https://bit.ly/3u2CrbX

了解 PWAs

PWA 是一个网络应用,它使用现代网络功能向用户提供类似应用的体验。它们看起来和感觉上都像一个原生应用,因为它们运行在自己的应用窗口而不是浏览器窗口中,并且它们可以从开始菜单或任务栏启动。由于使用了缓存,pwa 提供了离线体验和即时加载。它们可以接收推送通知,并在后台自动更新。最后,尽管它们不需要在应用商店中列出才能分发,但它们可以通过应用商店分发。

许多大公司,如 Pinterest、星巴克、Trivago 和推特,都已经接受了 PWAs。公司被普华永道吸引是因为他们可以一次开发它们,并在任何地方使用它们。

由于技术的结合,PWA 感觉就像一个本地应用。为了将网络应用转换为 PWA,它必须使用超文本传输协议安全 ( HTTPS )并包括清单文件和服务人员。

HTTPS

要转换成 PWA,网络应用必须使用 HTTPS,并且必须通过安全的网络提供服务。这应该不是问题,因为大多数浏览器将不再通过 HTTP 提供页面。因此,即使您不打算将 Blazor WebAssembly 应用转换为 PWA,您也应该始终使用 HTTPS。

小费

启用 HTTPS 需要安全套接字层 ( SSL )证书。免费 SSL 证书的一个很好的来源是让我们加密(https://letsencrypt.org)。它是一个免费的、自动化的、开放的证书颁发机构 ( CA )。

清单文件

清单文件是一个简单的 JavaScript 对象符号 ( JSON )文档,它包含应用的名称、默认值和网络应用启动时的启动参数。它描述了应用的外观和感觉。

这是一个简单清单文件的示例:

{
  "name": "My Sample PWA",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#03173d",
  "icons": [
    {
      "src": "icon-512.png",
      "type": "img/png",
      "sizes": "512x512"
    }
  ]
}

清单文件必须包括应用的名称和至少一个图标。在下一节中,我们将更仔细地研究清单文件。

服务人员

服务人员是一个 JavaScript 文件,它定义了 PWA 的离线体验。它拦截并控制网络浏览器如何处理其网络请求和资产缓存。

这是微软提供的 Blazor WebAssembly PWA 项目模板中包含的service-worker.js文件的内容:

self.addEventListener('fetch', () => { });

它只有一行代码,正如您所看到的,它实际上没有做任何事情,但它目前被视为一名服务人员,是将应用转换为 PWA 所需的全部技术。我们将在本章后面详细介绍更强大的服务人员。

PWA 是一个网络应用,可以像本地应用一样安装在设备上。如果一个网络应用使用 HTTPS,并包括一个清单文件和一个服务人员,它可以转换成一个 PWA。让我们仔细看看清单文件。

处理清单文件

清单文件以 JSON 格式提供关于应用的信息。它通常位于应用的根文件夹中。下面的代码片段显示了如何将名为manifest.json的清单文件添加到index.html文件中:

<link href="manifest.json" rel="manifest" />

下面是一个包含许多可能字段的示例清单文件:

{
  "dir": "ltr",
  "lang": "en",
  "name": " 5-Day Weather Forecast",
  "short_name": "Weather",
  "scope": "/",
  "display": "standalone",
  "start_url": "./",
  "background_color": "transparent",
  "theme_color": "transparent",
  "description": "This is a 5-day weather forecast.",
  "orientation": "any",
  "related_applications": [],
  "prefer_related_applications": false,
  "icons": [
    {
      "src": "icon-512.png",
      "type": "img/png",
      "sizes": "512x512"
    }
  ],
  "url": "https://bweweather.azurewebsites.net",
  "screenshots": []
}

如前所述,清单文件必须包括应用的名称和至少一个图标。除此之外,其他都是可选的,尽管强烈建议您至少包括descriptionshort_namestart_url

这些是manifest.json文件中的键:

  • dir:第nameshort_namedescription的基向。要么是ltrrtl,要么是auto
  • lang:第一语言nameshort_name,description
  • name:应用的名称。最大长度为45个字符。
  • short_name:应用的简称。最大长度为12个字符。
  • scope:app 的导航范围。
  • display:app 的显示方式,设置为fullscreenstandaloneminimal-UIbrowser
  • start_url:应用的统一资源定位器 ( 网址)。
  • background_color:安装在闪屏时应用背景使用的颜色。
  • theme_color:默认主题颜色。
  • description:app 的简短描述。
  • orientation:默认屏幕方向,设置为anylandscapeportrait
  • related_applications:开发者希望突出显示的任何相关应用。这些通常是原生应用。
  • prefer_related_applications:通知用户代理相关应用优于 web 应用的值。
  • icons:应用使用的一张或多张图片。
  • url:app 的地址。
  • screenshots:正在运行的应用的图像数组。

manifest.json文件最大的部分往往是图像列表。原因是许多设备更喜欢不同大小的图像。

小费

快速生成相同图像的许多不同大小的一个简单方法是使用 PWABuilder 网站上的图像 生成器工具,该工具位于https://www.pwabuilder.com/generate

清单文件控制 PWA 对用户的显示方式,并且是将网络应用转换为 PWA 所必需的。服务人员还需要将网络应用转换为 PWA。让我们仔细看看服务人员。

与服务人员一起工作

服务人员提供 PWAs 背后的魔力。它们用于缓存、后台同步和推送通知。服务工作者是一个 JavaScript 文件,它拦截并修改导航和资源请求。它让我们完全控制哪些资源被缓存,以及我们的 PWA 在不同情况下的行为。

服务人员只是浏览器在后台运行的一个脚本。它与应用分离,没有文档对象模型 ( DOM )访问。它运行在一个不同的线程上,而不是主 JavaScript 所使用的线程上,主 JavaScript 为你的应用提供了动力,所以它不是阻塞的。它被设计成完全异步的。

服务人员生命周期

当与服务人员一起工作时,了解他们的生命周期非常重要,因为离线支持会给网络应用增加大量复杂性。服务人员的生命周期有三个步骤——安装激活获取,如下图所示:

Figure 5.1 – Service worker life cycle

图 5.1–服务人员生命周期

安装

在安装步骤中,服务人员通常会缓存网站的一些静态资产,例如你离线了闪屏。如果文件缓存成功,则安装服务工作器。但是,如果任何文件下载和缓存失败,服务人员将不会安装,也不会进入激活步骤。

如果服务人员没有成功安装,它将在下次运行 web 应用时尝试安装。因此,开发人员可以确信,如果服务工作人员已经成功安装,缓存将包含所有指定要缓存的静态资产。安装步骤成功完成后,启动激活步骤。

使活动

在激活步骤中,服务人员处理旧缓存的管理。由于之前的一次安装可能已经创建了一个缓存,这是我们删除它的机会。激活步骤成功完成后,服务工作人员准备开始处理提取事件。

取得

在获取步骤中,服务人员控制属于其范围内的所有页面。它将处理从 PWA 发出网络请求时发生的提取事件。服务工作器将继续提取,直到它被终止。

更新服务人员

为了更新为我们网站运行的服务人员,我们需要更新服务人员的JavaScript文件。每次用户导航到我们的站点,浏览器都会下载当前的服务人员,并将其与已安装的服务人员进行比较。如果他们不同,它将尝试替换旧的服务人员。

然而,这不会立即发生。新的服务工作人员必须等到旧的服务工作人员不再处于控制状态时才能被激活。在所有打开的页面关闭之前,旧的服务人员将保持控制。当新的服务人员取得控制权时,其激活事件将会触发。

activate回调期间处理缓存管理。我们在activate回调期间管理缓存的原因是,如果您要在安装步骤中清除任何旧的缓存,旧的服务工作人员(控制所有当前页面)将突然停止从该缓存提供文件。

下面的截图显示的是一个正在等待激活的服务人员:

Figure 5.2 – Service worker waiting to activate

图 5.2–等待激活的服务人员

小费

在用户在所有选项卡中导航离开应用之前,服务人员不会被激活。重装标签是不够的。但是,您可以通过单击技能等待链接来激活等待激活的服务人员。

服务人员类型

有许多不同类型的服务工作者,从简单到复杂。下图显示了从简单到复杂排序的一些不同类型的服务人员:

Figure 5.3 – Types of service workers

图 5.3–服务人员的类型

离线页面

这是创建的最简单类型的功能服务工作者。为了创建这种类型的服务人员,我们只需要一个指示应用离线的超文本标记语言(T2)页面。每当应用无法连接到网络时,我们只需显示该 HTML 页面。

页面的脱机副本

有了这种类型的服务人员,当我们的访问者查看页面时,我们会在缓存中存储每个页面的副本。当应用脱机时,它从缓存中提供页面。这种方法可能只适用于页面数量有限的应用,因为如果用户想要查看的页面尚未被该用户查看,则该页面将不会在缓存中,应用将会失败。

带有脱机页的脱机副本

这种类型的服务工作者是页面离线副本服务工作者的改进版本。它结合了前面两种类型的服务工作者。有了这种类型的服务人员,当我们的访问者查看它们时,我们会在缓存中存储每个页面的副本。当应用脱机时,它从缓存中提供页面。如果用户想要查看的页面不在缓存中,我们将显示指示应用脱机的 HTML 页面。

缓存优先网络

这种类型的服务工作者总是首先使用缓存。如果请求的页面在缓存中,它在向服务器请求页面之前服务于该页面,并用新页面更新缓存。使用这个服务工作器,我们总是在向服务器请求页面之前提供缓存中的页面版本,因此无论用户在线还是离线,都可以获得相同的数据。

小费

微软更喜欢缓存优先的网络服务工作者类型。

高级缓存

这种类型的服务人员是上述每种类型的组合。对于这种类型的服务工作者,我们使用不同的规则来指定要缓存的不同文件和路由。例如,一些数据,如股票价格,永远不应该被缓存,而其他不经常变化的数据应该被缓存。

背景同步

这是最复杂的服务工作者类型。它允许用户在脱机时继续使用应用添加和编辑数据。然后,当他们重新联机时,应用会将他们的数据与网络同步。

这不是所有可用的不同类型的服务人员的完整列表。但是,它应该让您了解服务人员的能力和灵活性以及缓存的重要性。我们列表中的所有服务人员都依赖缓存存储应用编程接口进行缓存。

使用缓存存储应用编程接口

缓存存储 API 用于缓存request / response对象对,其中request对象是键,response对象是值。它是为服务人员提供离线功能而设计的。一个caches对象是缓存的一个实例。它是位于window对象中的全局对象。

我们可以使用以下代码来测试它在浏览器上是否可用:

 const hasCaches = 'caches' in self;

一个caches对象用于维护一个特定网络应用的缓存列表。缓存不能与其他网络应用共享,并且与浏览器的 HTTP 缓存隔离。它们完全通过我们编写的 JavaScript 来管理。

以下是缓存的一些方法:

  • delete(cacheName):此方法删除指示的缓存,返回true。如果没有找到指示的缓存,则返回false
  • has(cacheName):如果指示的缓存存在,则返回true,否则返回false
  • keys:这个方法返回所有缓存名称的字符串数组。
  • open(cacheName):此方法打开指示的缓存。如果它不存在,则创建它,然后打开它。

当我们打开缓存的实例时,会返回一个Cache对象。以下是Cache对象的一些方法:

  • add(request):这个方法接受一个请求,并将结果响应添加到缓存中。

  • addAll(requests):这个方法接受一个请求数组,并将所有得到的响应添加到缓存中。

  • delete(request):如果能够找到并删除指示的请求,则返回true,否则返回false

  • keys():这个方法返回一个键数组。

  • match(request):此方法返回与匹配请求相关联的响应。

  • put(request, response): This method adds the request and response pair to the cache.

    小费

    除非我们明确要求更新,否则Cache对象不会更新。此外,这些对象不会过期。我们需要删除它们,因为它们已经过时了。

服务人员使用缓存存储应用编程接口来允许 PWA 在脱机时继续运行。接下来,我们将解释如何使用地理定位应用编程接口。

使用地理定位应用编程接口

JavaScript 的地理定位 API 为我们提供了一种获取用户位置的机制。使用地理定位应用编程接口,我们可以获得运行浏览器的设备的坐标。

地理定位应用编程接口通过navigator.geolocation对象访问。当我们调用navigator.geolocation对象时,用户的浏览器请求用户允许访问他们的位置。如果他们接受,浏览器会使用设备的定位硬件,例如智能手机上的全球定位系统 ( 全球定位系统)来确定其位置。

在我们尝试使用navigator.geolocation对象之前,我们应该验证浏览器是否支持它。以下代码测试浏览器是否支持地理定位:

if (navigator.geolocation) {
    var position = await getPositionAsync();  
} else {
    throw Error("Geolocation is not supported.");
};

对于本章中的项目,我们将使用getCurrentPosition方法来检索设备的位置。这个方法采用两个回调函数。success回调函数返回一个GeolocationPosition对象,而error回调函数返回一个GeolocationPositionError对象。如果用户拒绝我们访问他们的位置,将在GeolocationPositionError对象中报告。

这些是GeolocationPosition对象的属性:

  • coords.latitude:该属性返回一个双精度值,代表设备的纬度。
  • coords.longitude:该属性返回一个代表设备经度的双精度值。
  • coords.accuracy:该属性返回一个双精度值,表示纬度和经度的精度,单位为米。
  • coords.altitude:该属性返回一个双精度值,代表设备的高度。
  • coords.altitudeAccuracy:该属性返回一个双精度值,表示高度的精度,单位为米。
  • coords.heading:该属性返回一个 double,表示设备面对的方向,以度数表示。
  • coords.speed:该属性返回一个代表设备速度的 double 值,单位为米/秒。
  • timestamp:该属性返回响应的日期和时间。

GeolocationPosition对象总是返回coords.latitudecoords.longitudecoords.accuracytimestamp属性。其他属性只有在可用时才会返回。

通过使用 JavaScript 的地理定位 API,我们可以确定设备的经纬度。我们需要这些信息,以便使用开放天气一次呼叫应用编程接口为我们的项目请求本地天气预报。

使用 OpenWeather 一次调用 API

本章项目的数据来源为开放天气提供的免费 API。它被称为开放天气一次呼叫应用编程接口(https://openweathermap.org/api/one-call-api)。该应用编程接口能够返回当前、预测和历史天气数据。我们将使用它来访问未来 5 天的本地预报。这是使用开放天气一次调用应用编程接口的应用编程接口调用格式:

https://api.openweathermap.org/data/2.5/onecall?lat={lat}&lon={lon}&appid={API key}

这些是开放天气一次呼叫应用编程接口的参数:

  • lat:纬度。此参数是必需的。
  • lon:经度。此参数是必需的。
  • appid : API 键。此参数是必需的。在账户页面的应用编程接口键标签下。
  • units:计量单位。这被设置为标准公制英制
  • exclude:排除数据。这用于简化返回的数据。由于我们将只使用每日预测,我们将排除当前、分钟和小时数据,并为我们的项目发出警报。这是一个逗号分隔的列表。
  • lang:输出的语言。

这是来自 OpenWeather One Call API 的响应的片段:

weather.json 片段

{
  "dt": 1616436000,
  "sunrise": 1616416088,
  "sunset": 1616460020,
  "temp": {
    "day": 58.5,
    "min": 54.75,
    "max": 62.6,
    "night": 61.29,
    "eve": 61.25,
    "morn": 54.75
  },
  "feels_like": {
    "day": 49.69,
    "night": 51.91,
    "eve": 50.67,
    "morn": 47.03
  },
  "pressure": 1011,
  "humidity": 85,
  "dew_point": 54.01,
  "wind_speed": 17.83,
  "wind_deg": 168,
  "weather": [
    {
      "id": 502,
      "main": "Rain",
      "description": "heavy intensity rain",
      "icon": "10d"
    }
  ],
  "clouds": 98,
  "pop": 1,
  "rain": 27.91,
  "uvi": 2.34
},

在前面的【JSON 片段中,我们已经强调了我们在本章项目中使用的字段。

OpenWeather One Call API 是一个简单的 API,我们将使用它来获取给定位置的每日预报。现在,让我们快速了解一下我们将在本章中构建的项目。

项目概述

在本章中,我们将构建一个 Blazor WebAssembly 应用来显示当地的 5 天天气预报,然后将其转换为 PWA。

我们将构建的 web 应用使用 JavaScript 的地理定位 API 来确定设备的当前经纬度。它使用 OpenWeather One Call API 获取本地天气预报,并使用各种 Razor 组件向用户显示天气预报。在我们完成 web 应用后,我们将通过添加徽标、清单文件和服务人员将其转换为 PWA。最后,我们将安装、运行和卸载 PWA。

这是完整应用的屏幕截图:

图 5.4–天气预报应用

这个项目的构建时间大约为 120 分钟。

创建 PWA

一个WeatherForecast项目将使用空 Blazor WebAssembly 应用项目模板创建。首先,我们将使用 JS 与地理定位 API 的互操作来获取设备的坐标。然后,我们将使用 OpenWeather One Call API 获取这些坐标的天气预报。接下来,我们将创建几个 Razor 组件来显示预测。

为了将 web 应用转换为 PWA,我们将添加一个徽标、一个清单文件和一个离线页面服务工作人员。在测试服务人员之后,我们将安装、运行和卸载 PWA。

开始项目

我们需要来创建一个新的 Blazor WebAssembly 应用。我们将通过以下步骤来做到这一点:

  1. 打开 Visual Studio 2019

  2. 点击新建项目按钮。

  3. In the Search for templates (Alt + S) textbox, enter Blazor and hit the Enter key.

    以下截图显示了我们在 第二章 中创建的空 Blazor WebAssembly App 项目模板,构建您的第一个 Blazor WebAssembly 应用:

    Figure 5.5 – Empty Blazor WebAssembly App project template

    图 5.5–空 Blazor WebAssembly 应用项目模板

  4. 选择空 Blazor WebAssembly App 项目模板,点击下一步按钮。

  5. 项目名称文本框中输入WeatherForecast,点击创建按钮,如下图所示:

Figure 5.6 – Configure your new project dialog

图 5.6–配置新项目对话框

小费

在前面的例子中,我们将WeatherForecast项目放入E:/Blazor文件夹中。然而,这个项目的位置并不重要。

我们现在已经创建了一个 Blazor WebAssembly 项目。

添加一个 JavaScript 函数

我们现在需要添加一个类来包含我们当前的经纬度。我们将通过以下步骤来做到这一点:

  1. 右键单击wwwroot文件夹,从菜单中选择添加,新文件夹选项。

  2. 命名新文件夹scripts

  3. 右键单击scripts文件夹,从菜单中选择添加,新项目选项。

  4. 搜索框中输入javascript

  5. 选择 JavaScript 文件

  6. Name the file bweInterop.js.

    小费

    在本书中,我们将为我们的 JavaScript 代码使用bweInterop命名空间,以构建我们的代码并最小化命名冲突的风险。

  7. 点击添加按钮。

  8. Enter the following JavaScript:

    var bweInterop = {};
    bweInterop.getPosition = async function () {
        function getPositionAsync() {
            return new Promise((success, error) => {
                navigator.geolocation.getCurrentPosition(success, error);
            });
        }
        if (navigator.geolocation) {
            var position = await getPositionAsync();  
            var coords = {
                latitude: position.coords.latitude,
                longitude: position.coords.longitude
            };
            return coords;
        } else {
            throw Error("Geolocation is not supported by
                         this browser.");
        };
    }
    

    前面的 JavaScript 代码使用地理定位应用编程接口返回设备的纬度和经度。如果不允许或不支持,则会引发错误。

  9. 打开wwwroot\index.html文件。

  10. Add the following reference toward the bottom of the body element:

```cs
<script src="scripts/bweInterop.js"></script>
```

你应该在提到`_framework/Blazor.webassembly.js`之前加上它。

我们已经创建了一个 JavaScript 函数,它使用地理定位 API 来返回我们当前的纬度和经度。接下来,我们需要从我们的 web 应用中调用它。

使用地理定位应用编程接口

我们需要从我们的网络应用中调用我们的bweInterop.getPosition功能。我们将通过以下步骤来做到这一点:

  1. 右键单击WeatherForecast项目,从菜单中选择添加,新文件夹选项。

  2. 命名新文件夹Models

  3. 右键单击Models文件夹,从菜单中选择添加,类别选项。

  4. 命名新类Position

  5. Add the following highlighted properties to the Position class:

    public class Position
    {
        public double Latitude { get; set; }
        public double Longitude { get; set; }
    }
    

    这是我们将用来存储坐标的类。

  6. 打开Pages\Index.razor文件。

  7. Add the following markup:

    @using WeatherForecast.Models
    @inject IJSRuntime js
    @if (pos == null)
    {
        <p><em>@message</em></p>
    }
    else
    {
        <h2>Latitude: @pos.Latitude, Longitude: 
           @pos.Longitude </h2>
    }
    @code {
        string message = "Loading...";
        Position pos;
    }
    

    如果pos属性为null,则前面的标记会显示一条消息。否则,显示pos属性的经纬度。

  8. Add the following OnInitializedAsync method to the @code block:

    protected override async Task OnInitializedAsync()
    {
        try
        {
            await GetPosition();
        }
        catch (Exception)
        {
            message = "Geolocation is not supported.";
        };
    }
    

    前面的代码试图在页面初始化时获取我们的坐标。

  9. Add the following GetPosition method to the @code block:

    private async Task GetPosition()
    {
        pos = await js.InvokeAsync<Position>(
            "bweInterop.getPosition");
    }
    

    前面的代码使用 JS 互操作来调用我们编写的 JavaScript 函数,该函数使用地理定位 API 来返回我们的坐标。有关 JS 互操作的更多信息,请参考第 4 章 【使用 JavaScript 互操作性构建本地存储服务】

  10. From the Debug menu, select the Start Without Debugging (Ctrl + F5) option to run the project.

下面的截图是一个对话框的例子,该对话框将询问您是否允许访问您的位置:

![Figure 5.7 – Geolocation permission dialog ](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/blazor-webasm-exam/img/Figure_5.7_B16786.jpg)

图 5.7–地理定位权限对话框
  1. Click the Allow button to allow the app to have access to your location.
以下截图为更新后的**首页**页面:

Figure 5.8 – Home page displaying coordinates

图 5.8–显示坐标的主页

您可以使用允许的位置访问对话框禁用应用访问您的位置的功能,如下图所示:

Figure 5.9 – Location access allowed dialog

图 5.9–允许位置访问对话框

允许的位置访问对话框通过浏览器工具栏上突出显示的按钮进行访问。您可能想切换权限,看看这对应用有何影响。

小费

要更改网址访问您所在位置的权限,请从浏览器菜单中选择设置。然后,从隐私和安全区域选择站点设置。最后,搜索您正在使用的网址,选择它,并将位置字段的值更改为以下值之一:询问(默认)允许阻止

我们已经使用地理定位应用编程接口在主页页面上显示了我们的纬度和经度。接下来,我们需要将这些坐标提供给 OpenWeather One Call API。

添加预测类别

我们需要添加一个Forecast类来获取开放天气一呼应用编程接口的结果。我们将通过以下步骤来做到这一点:

  1. 返回 Visual Studio

  2. 右键单击Models文件夹,从菜单中选择添加,类别选项。

  3. 命名新类OpenWeather

  4. Add the following classes:

    public class OpenWeather
    {
        public Daily[] Daily { get; set; }
    }
    public class Daily
    {
        public long Dt { get; set; }
        public Temp Temp { get; set; }
        public Weather[] Weather { get; set; }
    }
    public class Temp
    {
        public double Min { get; set; }
        public double Max { get; set; }
    }
    public class Weather
    {
        public string Description { get; set; }
        public string Icon { get; set; }
    }
    

    前面的类将与开放天气一次调用应用编程接口一起使用。

添加每日预测组件

我们需要一个组件来显示每天的预报。我们将通过以下步骤来做到这一点:

  1. 右键单击Shared文件夹,从菜单中选择添加,剃刀组件选项。

  2. 命名新组件DailyForecast

  3. Replace the existing markup with the following markup:

    <div class="card text-center">
        <div class="card-header">
            @Date
        </div>
        <div class="card-body">
            <img src="@IconUrl" />
            <h4 class="card-title">@Description</h4>
            <b>@((int)HighTemp) F&deg;</b> / 
            @((int)LowTemp) F&deg;
        </div>
    </div>
    @code {
    }
    

    该组件使用来自引导Card组件显示每日预报。有关Card组件的更多信息,请参见https://getbootstrap.com/docs/5.0/components/card

  4. Add the following code to the @code block:

    [Parameter] public long Seconds { get; set; }
    [Parameter] public double HighTemp { get; set; }
    [Parameter] public double LowTemp { get; set; }
    [Parameter] public string Description { get; set; }
    [Parameter] public string Icon { get; set; }
    private string Date;
    private string IconUrl;
    protected override void OnInitialized()
    {
        Date = DateTimeOffset
            .FromUnixTimeSeconds(Seconds)
            .LocalDateTime
            .ToLongDateString();
        IconUrl = String.Format(
            "https://openweathermap.org/img/wn/{0}@2x.png",
            Icon);
    }
    

    上述代码定义了用于显示每日天气预报的参数。OnInitialized方法用于格式化DateIconUrl字段。

我们增加了一个Razor组件,使用 Bootstrap 的Code组件显示每天的天气预报。

使用 OpenWeather 一次调用 API

我们需要使用 OpenWeather 一键调用 API 获取天气预报。我们将通过以下步骤来做到这一点:

  1. 打开Pages\Index.razor文件。

  2. 增加以下using语句:

    @using System.Text
    
  3. 增加以下@inject指令:

    @inject HttpClient Http
    
  4. 将以下属性添加到@code块:

    OpenWeather forecast;
    
  5. Add the GetForecast method to the @code block, as follows:

    private async Task GetForecast()
    {
        string APIKey = "{Your_API_Key}";
        StringBuilder url = new StringBuilder();
        url.Append("https://api.openweathermap.org");
        url.Append("/data/2.5/onecall?");
        url.Append("lat=");
        url.Append(pos.Latitude);
        url.Append("&lon=");
        url.Append(pos.Longitude);
        url.Append("&exclude=");
        url.Append("current,minutely,hourly,alerts");
        url.Append("&units=imperial");
        url.Append("&appid=");
        url.Append(APIKey);
        forecast = await Http
            .GetFromJsonAsync<OpenWeather>
            (url.ToString());
    }
    

    前面的方法使用 OpenWeather One Call API,坐标通过GetPosition方法获得。

  6. Update the OnInitializedAsync method to call the GetForecast method and update the error message, like this:

    try
    {
        await GetPosition();
        await GetForecast();
    }
    catch (Exception)
    {
        message = "Error encountered";
    };
    

    前面的代码使用GetForecast方法填充forecast对象。

    重要说明

    您需要将APIKey字符串的值设置为从 OpenWeather 获得的 API 密钥。

我们已经填充了forecast对象。接下来,我们需要展示它。

显示预测

我们需要将每日预报集合添加到主页页面。我们将通过以下步骤来做到这一点:

  1. 返回Pages\Index.razor文件。

  2. Replace the @if statement with the following markup:

    @if (forecast == null)
    {
        <p><em>@message</em></p>
    }
    else
    {
        <div class="card-group">
            @foreach (var item in forecast.Daily.Take(5))
            {
                <DailyForecast 
                   Seconds="@item.Dt"
                   LowTemp="@item.Temp.Min"
                   HighTemp="@item.Temp.Max"
                   Description="@item.Weather[0].Description"
                   Icon="@item.Weather[0].Icon" />
            }
        </div>
    }
    

    前面的标记在预测对象中循环五次。它使用DailyForecast组件显示每日预报。

  3. 构建菜单中,选择构建解决方案选项。

  4. 返回浏览器。

  5. 使用 Ctrl + R 刷新浏览器。

  6. 关闭浏览器。

我们已经完成了WeatherForecast申请。现在,我们需要将其转换为 PWA。为此,我们需要添加一个徽标、一个清单文件和一个服务人员。

添加标识

我们需要添加一个图像作为应用的标志。我们将通过以下步骤来做到这一点:

  1. 右键单击wwwroot文件夹,从菜单中选择添加,新文件夹选项。
  2. 命名新文件夹images
  3. Sun-512.png图像从 GitHub 存储库复制到images文件夹。

清单文件中必须至少包含一个映像,才能安装 PWA。现在,我们可以添加一个清单文件。

添加清单文件

要将 web 应用转换成 PWA,我们需要添加一个清单文件。我们将通过以下步骤来做到这一点:

  1. 右键单击wwwroot文件夹,从菜单中选择添加,新项目选项。

  2. 搜索框中输入json

  3. 选择 JSON 文件

  4. 命名文件manifest.json

  5. 点击添加按钮。

  6. 输入以下 JSON 代码:

    {
      "lang": "en",
      "name": "5-Day Weather Forecast",
      "short_name": "Weather",
      "display": "standalone",
      "start_url": "./",
      "background_color": "#ffa500",
      "theme_color": "transparent",
      "description": "This is a simple 5-day weather 
         forecast application.",
      "orientation": "any",
      "icons": [
        {
          "src": "img/Sun-512.png",
          "type": "img/png",
          "sizes": "512x512"
        }
      ]
    }
    
  7. 打开wwwroot\index.html文件。

  8. head元素的底部添加以下标记:

    <link href="manifest.json" rel="manifest" />
    
  9. Add the following markup below the preceding markup:

    <link rel="apple-touch-icon" 
          sizes="512x512" 
          href="Sun-512.png" />
    

    小费

    对于 iOS Safari 用户,您必须包含前面的链接标签,以指示它使用指示的图标,否则它将通过截图页面内容来生成图标。

我们在我们的网络应用中添加了一个清单文件,以控制它在安装时的外观和行为。接下来,我们需要添加一个服务人员。

添加简单的服务人员

要完成将 web 应用转换成 PWA,我们需要添加一个服务人员。我们将通过以下步骤来做到这一点:

  1. 右键单击wwwroot文件夹,从菜单中选择添加,新项目选项。

  2. 搜索框中输入html

  3. 选择 HTML 页面

  4. 命名文件offline.html

  5. 点击添加按钮。

  6. body元素添加以下标记:

    <h1>You are offline.</h1>
    
  7. 右键单击wwwroot文件夹,从菜单中选择添加,新项目选项。

  8. 搜索框中输入java

  9. 选择 JavaScript 文件

  10. 命名文件service-worker.js

  11. 点击添加按钮。

  12. Add the following constants:

```cs
const OFFLINE_VERSION = 1;
const CACHE_PREFIX = 'offline';
const CACHE_NAME = `${CACHE_PREFIX}${OFFLINE_VERSION}`;
const OFFLINE_URL = 'offline.html';
```

前面的代码设置了当前缓存的名称和我们将用来指示我们脱机的文件名。
  1. Add the following event listeners:
```cs
self.addEventListener('install',
    event => event.waitUntil(onInstall(event)));
self.addEventListener('activate',
    event => event.waitUntil(onActivate(event)));
self.addEventListener('fetch',
    event => event.respondWith(onFetch(event)));
```

前面的代码指定了用于以下每个步骤的函数:安装、激活和提取。
  1. Add the following onInstall function:
```cs
async function onInstall(event) {
    console.info('Service worker: Install');
    const cache = await caches.open(CACHE_NAME);
    await cache.add(new Request(OFFLINE_URL));
}
```

前面的函数打开指定的缓存。如果缓存尚不存在,它会创建缓存,然后将其打开。缓存打开后,它会将指示的请求/响应对添加到缓存中。
  1. Add the following onActivate function:
```cs
async function onActivate(event) {
    console.info('Service worker: Activate');
    const cacheKeys = await caches.keys();
    await Promise.all(cacheKeys
        .filter(key => key.startsWith(CACHE_PREFIX)
            && key !== CACHE_NAME)
        .map(key => caches.delete(key)));
}
```

前面的代码获取所有缓存的名称。所有与指定缓存名称不匹配的缓存都将被删除。

小费

清除过时的缓存是你的责任。每个浏览器对 web 应用可以使用的存储量都有限制。如果您违反了该限制,浏览器可能会删除您的所有缓存。
  1. Add the following onFetch function:
```cs
async function onFetch(event) {
    if (event.request.method === 'GET') {
        try {
            return await fetch(event.request);
        } catch (error) {
            const cache = await 
               caches.open(CACHE_NAME);
            return await cache.match(OFFLINE_URL);
        };
    };
}
```

在前面的代码中,如果提取失败,将打开缓存,并提供以前缓存的脱机页。
  1. 打开wwwroot\index.html文件。
  2. body元素的底部添加以下标记:
```cs
<script>
   navigator.serviceWorker.register('service-worker.js');
</script>
```

我们增加了一个离线页面服务人员,当 PWA 离线时将显示offline.html页面。

测试服务人员

我们需要来测试服务人员是否允许我们离线工作。我们将通过以下步骤来做到这一点:

  1. 调试菜单中,选择不调试启动 ( Ctrl + F5 )选项运行项目。

  2. 点击 F12 打开开发者工具界面。

  3. 选择应用选项卡。

  4. Select the Manifest option from the menu on the left to view the App Manifest.

    Figure 5.10 – App Manifest details

    图 5.10–应用清单详细信息

  5. Select the Service Workers option from the menu on the left to view the service worker that is installed for the current client, as illustrated in the following screenshot:

    Figure 5.11 – Service Workers dialog

    图 5.11–服务人员对话框

    小费

    点击查看所有注册链接,查看安装在您设备上的所有服务人员。

  6. 从左侧菜单中选择缓存存储选项,查看缓存。

  7. Click on the offline1 cache to view its contents, as illustrated in the following screenshot:

    Figure 5.12 – Cache Storage option

    图 5.12–缓存存储选项

  8. 从左侧菜单中选择服务人员选项。

  9. 选中服务人员对话框中的离线复选框。

  10. Refresh the browser, and you should see the following screen:

![Figure 5.13 – Offline page ](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/blazor-webasm-exam/img/Figure_5.13_B16786.jpg)

图 5.13–离线页面

显示的页面来自浏览器的缓存。
  1. 取消选中服务人员对话框中的离线复选框。
  2. 刷新浏览器。

我们已经测试了服务人员使我们的网络应用能够离线工作。现在,我们可以安装 PWA 了。

安装功率放大器

我们需要通过安装来测试 PWA。我们将通过以下步骤来做到这一点:

  1. Select the Install 5-Day Weather Forecast menu option from the browser's menu:

    Figure 5.14 – Install 5-Day Weather Forecast option

    图 5.14–安装 5 天天气预报选项

    小费

    在铬基浏览器上,安装按钮在网址栏上。但是,对于其他类型的浏览器,您需要从菜单按钮或共享按钮安装 PWA。

  2. Click the Install button on the dialog:

    Figure 5.15 – Install PWA dialog

    图 5.15–安装 PWA 对话框

    安装后,PWA 会出现,但没有地址栏。它出现在我们的任务栏上,我们可以从开始菜单运行它。以下截图显示了安装后的 PWA:

    Figure 5.16 – Installed PWA

    图 5.16–安装的功率放大器

  3. 关闭浏览器。

  4. Click the Windows key and open the 5-Day Weather Forecast app:

    Figure 5.17 – The PWA on the Start menu

图 5.17–开始菜单上的 PWA

应用打开,它的图标出现在任务栏上。如果我们愿意,我们可以把它固定在任务栏上。

我们已经成功安装并运行了 PWA。卸载 PWA 和安装 PWA 一样简单。

卸载 PWA

我们需要卸载 PWA。我们将通过以下步骤来做到这一点:

  1. Select the Customize and control 5-Day Weather Forecast option from the PWA's menu:

    Figure 5.18 – Customize and control 5-Day Weather Forecast option

    图 5.18–定制和控制 5 天天气预报选项

  2. Select the Uninstall 5-Day Weather Forecast… option:

    Figure 5.19 – Customize and control PWA dialog

    图 5.19–定制和控制 PWA 对话框

  3. 点击移除按钮:

Figure 5.20 – Remove PWA dialog

图 5.20–删除 PWA 对话框

我们已经卸载了 PWA。

总结

现在,您应该能够通过添加清单文件和服务人员,将 Blazor WebAssembly 应用转换为 PWA。

在本章中,我们介绍了 PWAs。我们解释了如何通过添加清单文件和服务人员将 web 应用转换为 PWA。我们解释了如何使用清单文件和服务人员。我们详细解释了不同类型的服务工作人员,并解释了如何使用缓存存储应用编程接口来缓存请求/响应对。最后,我们演示了如何使用地理定位应用编程接口和开放天气一键调用应用编程接口。

之后,我们使用空 Blazor App 项目模板创建了一个新项目。我们添加了一个 JavaScript 函数,它使用地理定位应用编程接口来获取我们的坐标。我们添加了一些模型来捕捉坐标,并使用 JS 互操作来调用 JavaScript 函数。我们使用 OpenWeather One Call API 来获取当地的 5 天天气预报,并创建了几个 Razor 组件来显示它。

在本章的最后一部分,我们通过添加一个图像、一个清单文件和一个离线页面服务工作器,将 Blazor WebAssembly 应用转换为 PWA。最后,我们安装、运行并卸载了 PWA。我们可以运用我们的新技能,将现有的网络应用转换为 pwa,将网络应用的优势与原生应用的外观和感觉结合起来。

在下一章中,我们将使用依赖注入 ( DI )来构建购物车应用。

问题

以下问题供您考虑:

  1. 服务工作者是异步的还是同步的?
  2. localStorage能否在服务人员内部使用,进行数据存储?
  3. 服务人员可以操纵 DOM 吗?
  4. PWAs 安全吗?
  5. PWAs 是特定于平台的吗?
  6. PWA 和原生应用有什么区别?

进一步阅读

以下资源提供了有关本章主题的更多信息:

六、使用应用状态构建购物车

有时,我们需要我们的应用来维护不同页面之间的状态。我们可以通过使用依赖注入 ( DI )来实现这一点。DI 用于访问在中心位置配置的服务。

在本章中,我们将创建一个购物车。当您在购物车中添加和删除商品时,应用将维护购物车中商品的列表。当用户导航到另一个页面,然后带着购物车返回该页面时,购物车的内容将被保留。此外,购物车的总数将显示在所有页面上。

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

  • 应用状态
  • 依赖注入
  • 创建购物车项目

技术要求

要完成此项目,您需要在电脑上安装 Visual Studio 2019。关于如何安装 Visual Studio 2019 免费社区版的说明,请参考 第 1 章 、Blazor WebAssembly 简介

本章的源代码可在以下 GitHub 存储库中获得:https://GitHub . com/PacktPublishing/Blazor-web assembly by Example/tree/main/chapter 06

行动中的代码视频可在此获得:https://bit.ly/3fxwYob

应用状态

在 Blazor WebAssembly 应用中,浏览器的内存用于保存应用的状态。这意味着当用户在页面之间导航时,状态会丢失,除非我们保留它。我们将使用应用状态模式来保存应用的状态。

AppState 模式中,服务被添加到阿迪容器中,以协调相关组件之间的状态。该服务包含所有需要维护的状态。因为服务是由 DI 容器管理的,所以它可以比单个组件更长寿,并且随着用户界面的变化而保持应用的状态。

服务可以是简单的类,也可以是复杂的类。一个服务可以用来管理整个应用中多个组件的状态。 AppState 模式的一个好处是,它导致了表示和业务逻辑之间更大的分离。

重要说明

当用户重新加载页面时,保存在浏览器内存中的应用状态会丢失。

对于本章中的项目,我们将使用阿迪服务实例来保持应用的状态。

理解 DI

DI 是一种技术,其中一个对象访问已经在中央位置配置的服务。中心位置是 DI 容器。使用 DI 时,每个消费类不需要创建自己的依赖注入类的实例。它由框架提供,称为服务。在 Blazor WebAssembly 应用中,服务是在program.cs文件的Program.Main方法中定义的。

我们已经在本书中通过以下服务使用了 DI:

  • http client(http 客户端)
  • IJSRuntime
  • 导航管理器

去离子容器

当一个 Blazor WebAssembly 应用启动时,它会配置阿迪容器。DI 容器负责构建服务实例,并一直存在到用户关闭运行 web 应用的浏览器中的选项卡。在以下示例中,CartService实现注册为IcartService:

 builder.Services.AddSingleton<ICartService, CartService>();

将服务添加到阿迪容器后,我们使用@inject指令将服务注入到依赖它的任何类中。@inject指令采用两个参数:类型和属性:

  • 类型:这是服务的类型。
  • 属性:这是接收服务的属性的名称。

以下示例显示了如何使用@inject指令:

@inject ICounterService counterService

依赖关系是在组件实例创建之后,但在执行OnInitializedOnInitializedAsync生命周期事件之前注入的。这意味着您不能在组件的构造函数中使用注入类,但是您可以在OnInitializedOnInitializedAsync方法中使用它。

使用寿命

使用 DI 注入的服务的寿命可以是以下任何值:

  • 一个
  • 审视
  • 短暂的

一个

如果服务生存期被定义为Singleton,这意味着将创建该类的单个实例,并且该实例将在整个应用中共享。使用该服务的任何组件都将收到同一服务的实例。

在 Blazor WebAssembly 应用中,对于在浏览器的当前选项卡中运行的当前应用的生存期来说,这是正确的。这是我们将在本章的项目中用来管理应用状态的服务生命周期。

审视

如果服务的服务生存期被定义为Scoped,这意味着将为每个范围创建一个新的类实例。由于 Blazor WebAssembly 应用没有 DI 作用域的概念,这些服务被视为Singleton服务。

在我们的项目模板中,我们使用Scoped服务来创建我们用于数据访问的HttpClient实例。这是因为微软的项目模板使用其服务的作用域服务生存期与服务器端 Blazor 对称。

短暂的

如果服务的服务生命周期被定义为Transient,这意味着每次请求服务实例时都会创建一个新的类实例。当使用临时服务时,DI 容器只是作为一个工厂,创建类的唯一实例。一旦实例被创建并注入依赖组件,容器就不再对它感兴趣了。

我们可以使用 DI 将同一个服务实例注入到多个组件中。它由 AppState 模式使用,允许应用维护组件之间的状态。

现在,让我们快速了解一下我们将在本章中构建的项目。

项目概述

在本章中,我们将构建一个包含购物车的 Blazor WebAssembly 应用。我们将能够在购物车中添加和移除不同的产品。购物车的总数将显示在应用的每个页面上。

以下是完整应用的屏幕截图:

Figure 6.1 – ShoppingCart app

图 6.1-购物卡应用

这个项目的构建时间大约为 60 分钟。

创建购物车项目

将使用空 Blazor WebAssembly 应用项目模板创建ShoppingCart项目。首先,我们将添加逻辑来添加和移除购物车中的产品。然后,我们将演示当我们在页面之间导航时,购物车的状态会丢失。为了维护购物车的状态,我们将在 DI 容器中注册一个使用 AppState 模式的服务。最后,我们将演示通过将新服务注入相关组件,购物车的状态不会丢失。

开始项目

我们需要创建一个新的 Blazor WebAssembly 应用。我们按如下方式进行:

  1. 打开 Visual Studio 2019

  2. 点击新建项目按钮。

  3. In the Search for templates (Alt + S) textbox, enter Blazor and then hit the Enter key.

    以下截图显示了我们在 第二章 中创建的空 Blazor WebAssembly App 项目模板,构建您的第一个 Blazor WebAssembly 应用:

    Figure 6.2 – Empty Blazor WebAssembly App project template

    图 6.2–空 Blazor WebAssembly 应用项目模板

  4. 选择空 Blazor WebAssembly App 项目模板,然后点击下一步按钮。

  5. Enter ShoppingCart in the Project name textbox and then click the Create button:

    Figure 6.3 – Configure your new project dialog

    图 6.3–配置新项目对话框

    小费

    在前面的例子中,我们将ShoppingCart项目放入E:/Blazor文件夹中。然而,项目的位置并不重要。

  6. 打开Pages\Index.razor页面。

  7. 添加以下标记:

    <div class="jumbotron">
        <h1 class="display-4">Welcome to Blazing Tasks!</h1>
        <p class="lead">
            Your one stop shop for all your tasks.
        </p>
    </div>
    

我们现在已经创建了 Blazor WebAssembly 项目。

添加产品类别

我们需要添加待售的产品。我们按如下方式进行:

  1. 右键单击ShoppingCart项目,从菜单中选择添加,新文件夹选项。

  2. 命名新文件夹Models

  3. 右键单击Models文件夹,从菜单中选择添加,类别选项。

  4. 命名新类Product

  5. 点击添加按钮。

  6. 将以下属性添加到Product类中:

    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public int Price { get; set; }
    public string Image { get; set; }
    
  7. 右键单击wwwroot文件夹,从菜单中选择添加,新文件夹选项。

  8. 命名新文件夹sample-data

  9. 右键单击sample-data文件夹,从菜单中选择添加,新项目选项。

  10. 搜索框中输入json

  11. 选择 JSON 文件

  12. 命名文件products.json

  13. 点击添加按钮。

  14. Update the file to the following:

products.json

```cs
[
  {
    "productId": 1,
    "productName": "Charger",
    "price": 15,
    "image": "charger.jpg"
  },
  {
    "productId": 2,
    "productName": "Ear Buds",
    "price": 22,
    "image": "earbuds.jpg"
  },
  {
    "productId": 3,
    "productName": "Key Chain",
    "price": 1,
    "image": "keychain.jpg"
  },
  {
    "productId": 4,
    "productName": "Travel Mug",
    "price": 8,
    "image": "travelmug.jpg"
  },
  {
    "productId": 5,
    "productName": "T-Shirt",
    "price": 20,
    "image": "tshirt.jpg"
  }
]
```

重要说明

可以从 GitHub 库中复制`products.json`文件。
  1. 右键单击wwwroot文件夹,从菜单中选择添加,新文件夹选项。
  2. 命名新文件夹images
  3. 将以下图像从 GitHub 存储库中复制到images文件夹:Charger.jpgEarbuds.jpgKeyChain.jpgTravelMug.jpgTshirt.jpg.

我们已经在网络应用中添加了一系列产品。接下来,我们需要添加一个商店。

添加商店页面

要添加商店,我们需要在我们的网络应用中添加一个Store组件。我们按如下方式进行:

  1. 打开Shared\NavMenu.razor页面。

  2. Add the following markup before the closing ul tag:

    <li class="nav-item px-3">
        <NavLink class="nav-link" href="store">
            <span class="oi oi-home" aria-hidden="true">
            </span> 
            Store
        </NavLink>
    </li> 
    

    上述标记为存储页面添加了一个菜单选项。

  3. 右键单击Pages文件夹,从菜单中选择添加,剃刀组件选项。

  4. 命名新组件Store

  5. 点击添加按钮。

  6. Replace the markup with the following:

    @page "/store"
    @using ShoppingCart.Models
    @inject HttpClient Http
    @if (products == null)
    {
        <p><em>Loading...</em></p>
    }
    else
    {
        <div class="row">
        </div>
    }
    @code {
        public IList<Product> products;
        public IList<Product> cart = new List<Product>();
        private int total;
    }
    

    前面的代码增加了一些指令和一些属性。

  7. Add the following markup in the div element:

    <div class="col-xl-4 col-lg-6">
        <h2>Products</h2>
        <table class="table">
            @foreach (Product item in products)
            {
                <tr>
                    <td>
                        <img src="img/@item.Image" />
                    </td>
                    <td class="align-middle">
                        @item.ProductName
                    </td>
                    <td class="align-middle">
                        $@item.Price
                    </td>
                    <td class="align-middle">
                        <button class="btn btn-primary" 
                        @onclick="@(() =>
                          AddProduct(item))">
                            Add to Cart
                        </button>
                    </td>
                </tr>
            }
        </table>
    </div> 
    

    前面的标记添加了一个显示所有待售产品的表格。

  8. Add the following markup below the preceding div element:

    <div class="col-xl-4 col-lg-6">
        @if (cart.Any())
        {
            <h2>Your Cart</h2>
            <ul class="list-group">
                @foreach (Product item in cart)
                {
                    <li class="list-group-item p-2">
                        <button class="btn btn-sm" 
                                @onclick="@(()
                                 =>DeleteProduct(item))">
                            <span class="oi oi-delete">
                            </span>
                        </button>
                        @item.ProductName - $@item.Price
                    </li>
                }
            </ul>
            <div class="p-2">
                <h3>Total: $@total</h3>
            </div>
        }
    </div>
    

    前面的标记显示了我们列表中的所有项目。

  9. Add the following code to the @code block:

    protected override async Task OnInitializedAsync()
    {
        products = await Http.GetFromJsonAsync<Product[]>
                ("sample-data/products.json");
    }
    

    前面的代码使用 HttpClientproducts.json文件中读取products

  10. Add the AddProduct method to the @code block:

```cs
private void AddProduct(Product product)
{
    cart.Add(product);
    total += product.Price;
}
```

前面的代码将指定的产品添加到购物车中,并按产品价格递增总数。
  1. Add the DeleteProduct method to the @code block:
```cs
private void DeleteProduct(Product product)
{
    cart.Remove(product);
    total -= product.Price;
}
```

前面的代码从购物车中删除指定的产品,并按产品价格递减总数。

我们在网络应用中添加了商店页面。现在我们需要测试它。

证明应用状态丢失

我们需要测试商店页面。我们按如下方式进行:

  1. 调试菜单中,选择不调试启动(Ctrl+F5)option 运行项目。
  2. 选择导航菜单上的存储选项。
  3. 向购物车中添加一些物品。
  4. 选择导航菜单上的主页选项。
  5. 选择导航菜单上的商店选项,返回商店页面。
  6. 确认购物车现在是空的。

当我们在 web 应用的页面之间导航时,状态会丢失。我们可以使用 AppState 模式来维护状态。

创建 ICartService 接口

我们需要创建一个ICartService界面。我们按如下方式进行:

  1. 返回 Visual Studio

  2. 右键单击ShoppingCart项目,从菜单中选择添加,新文件夹选项。

  3. 命名新文件夹Services

  4. 右键单击Services文件夹,从菜单中选择添加,新项目选项。

  5. 搜索框中输入interface

  6. 选择界面

  7. 命名文件ICartService

  8. 点击添加按钮。

  9. 输入以下代码:

    IList<Product> Cart{ get; }
    int Total { get; set; }
    event Action OnChange;
    void AddProduct(Product product);
    void DeleteProduct(Product product); 
    
  10. 增加以下using语句:

```cs
using ShoppingCart.Models;
```

我们已经创建了ICartService界面。现在我们需要创建一个从它继承的类。

创建 CartService 类

我们需要创建CartService类。我们按如下方式进行:

  1. 右键单击Services文件夹,从菜单中选择添加,类别选项。

  2. 命名类CartService

  3. 点击添加按钮。

  4. Update the class to the following:

    public class CartService : ICartService
    {
        public IList<Product> Cart { get; private set; }
        public int Total { get; set; }
        public event Action OnChange;
    }
    

    CartService类继承自ICartService接口。

  5. 增加以下using语句:

    using ShoppingCart.Models;
    
  6. 添加以下构造函数:

    public CartService() { Cart = new List<Product>(); }
    
  7. Add the NotifyStateChanged method to the class:

    private void NotifyStateChanged() => OnChange?.Invoke();
    

    在前面的代码中,调用NotifyStateChanged方法时会调用OnChange事件。

  8. Add the AddProduct method to the class:

    public void AddProduct(Product product)
    {
        Cart.Add(product);
        Total += product.Price;
        NotifyStateChanged();
    }
    

    前面的代码将指示的产品添加到产品列表中,并增加总数。它还调用NotifyStateChanged方法。

  9. Add the DeleteProduct method to the class:

    public void DeleteProduct(Product product)
    {
        Cart.Remove(product);
        Total -= product.Price;
        NotifyStateChanged();
    }
    

    上面的代码从产品列表中删除指定的产品,并减少总数。它还调用NotifyStateChanged方法。

我们已经完成了CartService课。现在我们需要在 DI 容器中注册CartService

在 DI 容器中注册 CartService

我们需要在去离子容器中注册,然后才能将其注入我们的商店页面。我们按如下方式进行:

  1. 打开Program.cs文件。

  2. 在注册HttpClient的代码后添加以下代码:

    builder.Services.AddScoped<ICartService, CartService>();
    
  3. 增加以下using语句:

    using ShoppingCart.Services;
    

我们已经注册CartService。现在我们需要更新商店页面来使用它。

注射卡丁车服务

我们需要更新商店页面。我们按如下方式进行:

  1. 打开Pages\Store.razor页面。

  2. 增加以下@using指令:

    @using ShoppingCart.Services
    
  3. 增加以下@inject指令:

    @inject ICartService cartService
    
  4. Update the Add to Cart button to the following:

    <button class="btn btn-primary"
            @onclick="@(() =>
              cartService.AddProduct(item))">
        Add to Cart
    </button>
    

    前面的标记使用cartService将产品添加到购物车中。

  5. Update the cart div element to the following:

    @if (cartService.Cart.Any())
    {
        <h2>Your Cart</h2>
        <ul class="list-group">
            @foreach (Product item in cartService.Cart)
            {
                <li class="list-group-item p-2">
                    <button class="btn btn-sm"
                            @onclick="@(() =>cartService.DeleteProduct(item))">
                        <span class="oi oi-delete"></span>
                    </button>
                    @item.ProductName - $@item.Price
                </li>
            }
        </ul>
        <div class="p-2">
            <h3>Total: $@cartService.Total</h3>
        </div>
    }
    

    前面的标记使用CartService遍历购物车中的产品,并使用从购物车中删除产品。

  6. @code块中删除cart属性、AddProduct方法和DeleteProduct方法。

  7. 构建菜单中,选择构建解决方案选项。

  8. 返回浏览器。

  9. 使用 Ctrl + R 刷新浏览器。

  10. 向购物车中添加一些物品。

  11. 选择导航菜单上的主页选项。

  12. 选择导航菜单上的商店选项,返回商店页面。

  13. 确认购物车不是空的。

我们已经确认CartService正在工作。现在我们需要将购物车总数添加到所有页面中。

将购物车总数添加到所有页面

要查看所有页面上的购物车总数,我们需要将购物车总数添加到所有页面上使用的组件中。由于所有页面都使用了MainLayout组件,我们将向其中添加购物车总数。我们按如下方式进行:

  1. 返回 Visual Studio

  2. 打开Shared\MainLayout.razor页面。

  3. 增加以下@using指令:

    @using ShoppingCart.Services
    
  4. 增加以下@inject指令:

    @inject ICartService cartService
    
  5. 将以下标记添加到top-row div :

    <h3>Cart Total: $@cartService.Total</h3>
    
  6. 构建菜单中,选择构建解决方案选项。

  7. 返回浏览器。

  8. 使用 Ctrl + R 刷新浏览器。

  9. 向购物车中添加一些物品。

  10. 确认页面顶部的购物车合计字段没有更新。

当我们向购物车中添加新商品时,页面顶部的购物车总数不会更新。我们需要处理这个。

使用 OnChange 方法

我们需要通知组件什么时候需要更新。我们按如下方式进行:

  1. 返回 Visual Studio

  2. 打开Shared\MainLayout.razor页面。

  3. 增加以下@implements指令:

    @implements IDisposable
    
  4. Add the following @code block:

    @code{
        protected override void OnInitialized()
        {
            cartService.OnChange += StateHasChanged;
        }
        public void Dispose()
        {
            cartService.OnChange -= StateHasChanged;
        }
    }
    

    在前面的代码中,组件的StateHasChanged方法订阅了OnInitialized方法中的cartService.OnChange方法,而在Dispose方法中取消了订阅。

  5. 构建菜单中,选择构建解决方案选项。

  6. 返回浏览器。

  7. 使用 Ctrl + R 刷新浏览器。

  8. 向购物车中添加一些物品。

  9. 确认页面顶部的购物车总计字段更新。

我们已经更新了组件,以便在调用CartServiceOnChange方法时调用StateHasChanged方法。

小费

处理组件时,不要忘记取消订阅事件。

您必须取消订阅该事件,以防止每次引发cartService.OnChange事件时调用StateHasChanged方法。否则,您的应用将会遇到资源泄漏。

总结

现在,您应该能够使用 DI 将应用状态模式应用到 Blazor WebAssembly 应用中。

在本章中,我们介绍了应用状态和 DI。之后,我们使用空 Blazor WebAssembly App 项目模板创建了一个新项目。我们向项目中添加了一个购物车,并演示了当我们在页面之间导航时,应用状态会丢失。为了维护应用的状态,我们在 DI 容器中注册了CartService服务。最后,我们演示了通过使用 AppState 模式,我们可以维护购物车的状态。

我们可以用 DI 应用我们的新技能来维护任何 Blazor WebAssembly 应用的应用状态。

在下一章中,我们将使用事件构建看板板。

问题

以下问题供您考虑:

  1. 当页面重新加载时,本地存储可以用来维护购物车的状态吗?
  2. 为什么不需要在Store组件中调用StateHasChanged方法?

进一步阅读

以下资源提供了有关本章所涵盖主题的更多信息:

七、使用事件构建看板

作为开发人员,我们努力使我们的应用尽可能动态。为此,我们使用事件。事件是由对象发送的消息,用于指示某个操作已经发生。Razor 组件可以处理许多不同类型的事件。

在本章中,我们将学习如何在 Blazor WebAssembly 应用中处理不同类型的事件。我们还将学习如何使用任意参数属性 分割来简化我们如何为组件分配属性。

我们在本章中创建的项目将是一个使用拖放事件的看板。看板直观地描绘了流程各个阶段的工作。我们的看板将包括三个拖放区。最后,我们将使用任意参数和属性规划来创建一个对象,以向我们的看板板添加新任务。

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

  • 事件处理
  • 任意参数
  • 属性拆分
  • 创建看板项目

技术要求

要完成此项目,您需要在电脑上安装 Visual Studio 2019。关于如何安装 Visual Studio 2019 免费社区版的说明,请参考 第 1 章 、Blazor WebAssembly 简介。您还需要我们在 第 2 章 中创建的空 Blazor WebAssembly App 项目模板,构建您的第一个 Blazor WebAssembly 应用

本章的源代码可在以下 GitHub 存储库中获得:https://GitHub . com/PacktPublishing/Blazor-web assembly by Example/tree/main/chapter 07

行动视频中的代码可在此获得:https://bit.ly/3bHfPHt

事件处理

Razor 组件通过使用名为@on{EVENT}的 HTML 元素属性来处理事件,其中EVENT是事件的名称。

下面的代码在点击按钮时调用OnClickHandler方法:

<button class="btn btn-success" @onclick="OnClickHandler">
    Click Me
</button>
@code {
    private void OnClickHandler()
    {
        // ...
    }
}

由于事件处理程序会自动触发用户界面渲染,因此我们在处理它们时不需要调用StateHasChanged。事件处理程序可用于调用同步和异步方法。此外,它们可以引用与事件相关联的任何参数。

当复选框更改时,以下代码异步调用OnChangeHandler方法:

<input type="checkbox" @onchange="OnChangedHandler" />
@code {
    private async Task OnChangedHandler(ChangeEventArgs e)
    {
        newvalue = e.Value.ToString();
        // await ...
    }
}

在前面的代码中,ChangeEventArgs类用于提供关于变更事件的信息。事件参数是可选的,只有在方法使用它们的情况下才应该包含它们。

所有由 ASP.NET Core 框架支持的EventArgs类都由 BlazorWebAssembly 框架支持。这是支持的EventArgs类列表:

  • ClipboardEventArgs
  • DragEventArgs
  • ErrorEventArgs
  • EventArgs
  • FocusEventArgs
  • ChangeEventArgs
  • KeyboardEventArgs
  • MouseEventArgs
  • PointerEventArgs
  • WheelEventArgs
  • ProgressEventArgs
  • TouchEventArgs

λ表达式

当我们需要用方法包含参数时,我们可以使用λ表达式。λ表达式是用来创建匿名函数的。他们使用=>运算符将参数从表达式主体中分离出来。

这是一个使用λ表达式的@onclick事件的例子:

<button class="btn btn-info" 
        @onclick="@(e => Console.WriteLine("Blazor
          Rocks!"))">
    Who Rocks?
</button>

在前面的代码中,@onclick事件为Console.WriteLine方法提供了一个字符串。

小费

如果在 lambda 表达式中直接使用循环变量,则所有 lambda 表达式都将使用相同的变量。因此,在 lambda 表达式中使用循环变量之前,应该在局部变量中捕获它的值。

防止违约行为

偶尔,我们需要阻止与事件相关的默认动作。我们可以通过使用@on{EVENT}:preventDefault指令来做到这一点,其中EVENT是事件的名称。

例如,当拖动一个元素时,默认行为是不允许用户将它放到另一个元素中。在看板项目中,我们需要将项目放入不同的拖放区。因此,我们需要防止这种默认行为。

以下代码防止ondragover默认行为发生,以便允许我们将元素放入div元素:

<div class="dropzone" 
     dropzone="true" 
     ondragover="event.preventDefault();"
</div>

Blazor WebAssembly 框架使我们通过使用@on{EVENT}属性来访问事件变得很容易。当使用组件时,我们通常需要提供多个属性。使用属性拆分,我们可以避免在 HTML 标记中直接分配属性。

属性拆分

当一个子组件有多个参数时,在 HTML 中分配每个值可能会很繁琐。为了避免这样做,我们可以使用属性拆分。

通过属性拆分,属性被捕获到字典中,然后作为一个单元传递给组件。每个字典条目添加一个属性。字典必须用字符串键实现IEnumerable<KeyValuePair<string, object>>IReadOnlyDictionary<string, object>。我们使用@attributes指令查阅字典。

这是一个名为BweButton的组件的代码,它有很多参数:

剃刀

<button class="@Class" disabled="@Disabled" title="@Title" @onclick="@ClickEvent">
    @ChildContent
</button>
@code {
    [Parameter] public string Class { get; set; }
    [Parameter] public bool Disabled { get; set; }
    [Parameter] public string Title { get; set; }
    [Parameter]
    public EventCallback ClickEvent { get; set; }
    [Parameter]
    public RenderFragment ChildContent { get; set; }
}

这是示例标记,用于在不使用属性拆分的情况下渲染BweButton组件:

<BweButton Class="btn btn-danger" 
           Disabled="false" 
           Title="This is a button"         
           ClickEvent="OnClickHandler">
    Submit
</BweButton>

这是由前面的标记呈现的按钮:

Figure 7.1 – Rendered BweButton

图 7.1–渲染的浏览器按钮

小费

有些 HTML 属性,如disabledtranslate,不需要任何值。对于这些类型的属性,如果该值设置为false,该属性将不会包含在由 Blazor WebAssembly 框架生成的 HTML 输出中

通过使用属性拆分,我们可以将前面的标记简化为以下内容:

<BweButton @attributes="InputAttributes" 
           ClickEvent="OnClickHandler">
    Submit
</BweButton >

这是前面标记使用的对InputAttributes的定义:

public Dictionary<string, object> InputAttributes { get; set; } 
    = new Dictionary<string, object>()
    {
        { "Class", "btn btn-danger" },
        { "Disabled", false},
        { "Title", "This is a button" }
    };

前面的代码定义了传递给BweButtonInputAttributes

当与任意参数结合时,实现了属性规划的真正威力。

任意参数

在前面的例子中,我们使用了明确定义的参数来分配按钮的属性。为属性赋值的一种更有效的方法是使用任意参数。任意参数是组件没有明确定义的参数。Parameter属性有一个CaptureUnmatchedValues属性,用于捕获任意参数。

这是BweButton的新版本,使用任意参数:

<button @attributes="InputAttributes" >
    @ChildContent
</button>
@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object> InputAttributes {
      get; set; }
    [Parameter]
    public RenderFragment ChildContent { get; set; }
}

前面的代码包括一个名为InputAttributes的参数,其CaptureUnmatchedValues属性设置为true

小费

一个组件只能有一个参数CaptureUnmatchedValues设置为true

这是用于渲染新版本BweButton的更新标记:

<BweButton @attributes="InputAttributes" 
           @onclick="ButtonClicked" 
           class="btn btn-info">
    Submit
</BweButton>

这是前面标记使用的InputAttributes的定义:

public Dictionary<string, object> InputAttributes { get; set; } =
    new Dictionary<string, object>()
    {
        { "class", "btn btn-danger" },
        { "title", "This is another button" },
        { "name", "btnSubmit" },
        { "type", "button" },
          { "myAttribute", "123"}
    };

虽然在新版本的BweButton中没有明确定义字典中的任何属性,但是BweButton仍然被渲染。

这是由前面的标记呈现的按钮:

Figure 7.2 – Rendered BweButton using arbitrary parameters

图 7.2–使用任意参数渲染的按钮

按钮现在是青色的原因是由于@attributes指令在按钮标记中的位置。当属性被拆分到元素上时,它们是从左到右处理的。因此,如果分配了重复的属性,顺序中后面出现的属性将是使用的属性。

任意参数用于允许组件呈现以前未定义的属性。这对于支持多种定制的组件非常有用,例如包含input元素的组件。

现在让我们快速了解一下我们将在本章中构建的项目。

项目概述

我们将在本章中构建的 Blazor WebAssembly 应用是看板板。看板将有三个拖放区:高优先级中优先级低优先级。我们将能够在拖放区之间拖放任务,并添加其他任务。

这是完整应用的屏幕截图:

图 7.3–看板板应用

这个项目的构建时间大约为 60 分钟。

创建看板项目

KanbanBoard项目将通过使用空 Blazor WebAssembly App 项目模板创建。首先,我们将添加TaskItem类。然后,我们将添加一个Dropzone组件。我们将在主页中添加三个Dropzone组件来创建看板。最后,我们将向看板添加添加新任务的能力。

开始项目

我们需要创建一个新的 Blazor WebAssembly 应用。我们按如下方式进行:

  1. 打开 Visual Studio 2019

  2. 点击新建项目按钮。

  3. In the Search for templates (Alt + S) textbox, enter Blazor and hit the Enter key.

    以下截图显示了我们在 第二章 中创建的空 Blazor WebAssembly App 项目模板,构建您的第一个 Blazor WebAssembly 应用:

    Figure 7.4 – Empty Blazor WebAssembly App project template

    图 7.4–空 Blazor WebAssembly 应用项目模板

  4. 选择空 Blazor WebAssembly App 项目模板,点击下一步按钮。

  5. 项目名称文本框中输入KanbanBoard,然后点击创建按钮:

Figure 7.5 – Configure your new project dialog

图 7.5–配置新项目对话框

小费

在前面的例子中,我们将KanbanBoard项目放入E:/Blazor文件夹中。然而,这个项目的位置并不重要。

我们现在已经创建了 Blazor WebAssembly 项目。

添加类别

我们需要添加一个TaskPriority枚举和一个TaskItem类。我们按如下方式进行:

  1. 右键单击KanbanBoard项目,从菜单中选择添加,新文件夹选项。

  2. 命名新文件夹Models

  3. 右键单击Models文件夹,从菜单中选择添加,类别选项。

  4. 命名新类TaskPriority

  5. 点击添加按钮。

  6. 将类替换为以下枚举:

    public enum TaskPriority
    {
        High,
        Medium,
        Low
    }
    
  7. 右键单击Models文件夹,从菜单中选择添加,类别选项。

  8. 命名新类TaskItem

  9. 点击添加按钮。

  10. 将以下属性添加到TaskItem类中:

```cs
public string TaskName { get; set; }
public TaskPriority Priority { get; set; }
```

我们添加了TaskPriority枚举和TaskItem类来表示看板板上的任务。接下来,我们需要创建拖放区。

创建拖放区组件

我们需要添加一个Dropzone组件。我们按照以下步骤进行:

  1. 右键单击Shared文件夹,从菜单中选择添加,剃刀组件选项。

  2. 命名新组件Dropzone

  3. 点击添加按钮。

  4. 移除h3元素。

  5. 增加以下@using指令:

    @using KanbanBoard.Models
    
  6. Add the following markup:

    <div class="priority">
        <h2>@Priority.ToString() Priority</h2>
        <div class="dropzone"  
             ondragover="event.preventDefault();"
             @ondrop="OnDropHandler">
            @foreach (var item in TaskItems
                .Where(q => q.Priority == Priority))
            {
            }
        </div>
    </div>
    

    前面的标记通过优先级来标记dropzone,并通过阻止ondragover事件的默认值来允许将元素放入元素中。当一个元素被放入dropzone时,调用OnDropHandler方法。最后,它循环遍历所有指定的PriorityTaskItems类。

  7. Add the following markup within the @foreach loop:

    <div class="draggable" 
         draggable="true"
         @ondragstart="@(() => OnDragStartHandler(item))">
        @item.TaskName
        <span class="badge badge-secondary">
          @item.Priority</span>
    </div>
    

    前面的标记通过将draggable元素设置为true使div元素可拖动。拖动元素时调用OnDragStartHandler方法。

  8. 将以下参数添加到@code块:

    [Parameter]
    public List<TaskItem> TaskItems { get; set; }
    [Parameter]
    public TaskPriority Priority { get; set; }
    [Parameter]
    public EventCallback<TaskPriority> OnDrop { get; set; }
    [Parameter]
    public EventCallback<TaskItem> OnStartDrag { get; set; } 
    
  9. Add the following OnDropHandler method:

    private void OnDropHandler()
    {
        OnDrop.InvokeAsync(Priority);
    }
    

    前面的代码调用OnDrop方法。

  10. Add the following OnDragStartHandler method:

```cs
private void OnDragStartHandler(TaskItem task)
{
    OnStartDrag.InvokeAsync(task);
}
```

前面的代码调用了`OnStartDrag`方法。

我们添加了一个Dropzone组件。现在我们需要给组件添加一些样式。

添加样式表

我们将使用 CSS 隔离向Dropzone组件添加一个样式表。我们按如下方式进行:

  1. 右键单击Shared文件夹,从菜单中选择添加,新项目选项。
  2. 搜索框中输入css
  3. 选择样式表
  4. 命名样式表Dropzone.razor.css
  5. 点击添加按钮。
  6. 输入以下样式:

Dropzone.razor.css 文件

.draggable {
    margin-bottom: 10px;
    padding: 10px 25px;
    border: 1px solid #424d5c;
    cursor: grab;
    background: #ff6a00;
    color: #ffffff;
    border-radius: 5px;
    width: 16rem;
}
    .draggable:active {
        cursor: grabbing;
    }
.dropzone {
    padding: .75rem;
    border: 2px solid black;
    min-height: 20rem;
}
.priority {
    min-width: 20rem;
    padding-right: 2rem;
}

我们已经完成了组件的造型。现在我们可以把看板放在一起了。

创建看板板

我们需要添加三个拖放区来创建我们的看板板,三种类型的任务各有一个拖放区。我们按如下方式进行:

  1. 打开Pages\Index.razor页面。

  2. 增加以下@using指令:

    @using KanbanBoard.Models
    
  3. 添加以下标记:

    <div class="row p-2">
        <Dropzone Priority="TaskPriority.High" 
                  TaskItems="TaskItems" 
                  OnDrop="OnDrop" 
                  OnStartDrag="OnStartDrag" />
        <Dropzone Priority="TaskPriority.Medium" 
                  TaskItems="TaskItems" 
                  OnDrop="OnDrop" 
                  OnStartDrag="OnStartDrag" />
        <Dropzone Priority="TaskPriority.Low" 
                  TaskItems="TaskItems" 
                  OnDrop="OnDrop" 
                  OnStartDrag="OnStartDrag" />
    </div>
    
  4. Add the following @code block:

    @code {
        public TaskItem CurrentItem;
        List<TaskItem> TaskItems = new List<TaskItem>();
    
        protected override void OnInitialized()
        {
            TaskItems.Add(new TaskItem
            {
                TaskName = "Call Mom",
                Priority = TaskPriority.High
            });
           TaskItems.Add(new TaskItem
           {
               TaskName = "Buy milk",
               Priority = TaskPriority.Medium
            });
            TaskItems.Add(new TaskItem
            {
                TaskName = "Exercise",
                Priority = TaskPriority.Low
            });    
        }
    }
    

    前面的代码用三个任务初始化了TaskItems对象。

  5. Add the OnStartDrag method to the @code block:

    private void OnStartDrag(TaskItem item)
    {
        CurrentItem = item;
    }
    

    前面的代码将CurrentItem的值设置为当前正在拖动的项目。当项目被删除时,我们将使用该值。

  6. Add the OnDrop method to the @code block:

    private void OnDrop(TaskPriority priority)
    {
        CurrentItem.Priority = priority;
    }
    

    前面的代码将CurrentItemPriority设置为与其所属的拖放区相关联的优先级。

  7. 调试菜单中,选择不调试启动 ( Ctrl + F5 )选项运行项目。

  8. 拖放任务以更改其优先级。

我们用三个项目创建了一个非常简单的看板。让我们添加添加更多项目的能力。

创建新任务组件

我们需要添加一个NewTask组件。我们按如下方式进行:

  1. 返回 Visual Studio

  2. 右键单击Shared文件夹,从菜单中选择添加,剃刀组件选项。

  3. 命名新组件NewTask

  4. 点击添加按钮。

  5. 移除h3元素。

  6. Add the following markup:

    <div class="row p-3" style="max-width:950px">
        <div class="input-group mb-3">
            <label class="input-group-text"
              for="inputTask">
                Task
            </label>
            <input type="text"
                   id="inputTask"
                   class="form-control"
                   @bind-value="@taskName"
                   @attributes="InputParameters" />
            <button type="button"
                    class="btn btn-outline-secondary"
                    @onclick="OnClickHandler">
                Add Task
            </button>
        </div>
    </div>
    

    前面的标记包括一个标签、一个文本框和一个按钮。

    这是NewTask组件的截图:

    Figure 7.6 – NewTask component

    图 7.6–新任务组件

  7. 将以下代码添加到@code块:

    private string taskName;
    [Parameter]
    public EventCallback<string> OnSubmit { get; set; }
    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object>
        InputParameters{ get; set; }
    
  8. Add the OnClickHandler method to the @code block:

    private async Task OnClickHandler()
    {
        if (!string.IsNullOrWhiteSpace(taskName))
        {
            await OnSubmit.InvokeAsync(taskName);
            taskName = null;
        }
    }
    

    前面的代码调用OnSubmit方法并将taskName对象设置为null

我们现在已经创建了NewTask组件。接下来,我们需要开始使用它。

使用新任务组件

我们需要将组件添加到主页页面。我们按如下方式进行:

  1. 打开Pages\Index.razor页面。

  2. @using指令下添加以下标记:

    <NewTask OnSubmit="AddTask" 
             @attributes="InputAttributes" />
    
  3. 将以下代码添加到@code块:

    public Dictionary<string, object> InputAttributes = new Dictionary<string, object>()
    {
        { "maxlength", "25" },
        { "placeholder", "enter new task" },
        { "title", "This textbox is used to enter your
          tasks." }
    };
    
  4. Add the AddTask method to the @code block:

    private void AddTask(string taskName)
    {
        var taskItem = new TaskItem()
        {
            TaskName = taskName,
            Priority = TaskPriority.High
        };
        TaskItems.Add(taskItem);
    }
    

    前面的代码将新项目的优先级设置为High,并将其添加到TaskItems对象中。

  5. 构建菜单中,选择构建解决方案选项。

  6. 返回浏览器。

  7. 使用 Ctrl + R 刷新浏览器。

  8. 添加一些新任务。

  9. 拖放任务以更改其优先级。

我们已经增加了在看板板上添加新任务的能力。

总结

现在,您应该能够在 Blazor WebAssembly 应用中处理事件了。此外,您应该对使用属性拆分和任意参数感到满意。

在这一章中,我们介绍了事件处理、属性规划和任意参数。之后,我们使用空 Blazor WebAssembly App 项目模板创建了一个新项目。我们向应用添加了一个Dropzone组件,并使用它来创建看板。最后,我们增加了向看板添加任务的能力,同时演示了属性拆分和任意参数。

现在,您已经知道如何在 Blazor WebAssembly 应用中处理不同类型的事件,您可以创建更具响应性的应用。而且,因为您可以使用字典将显式声明的属性和隐式属性传递给组件,所以您可以更快地创建组件,因为您不需要显式定义每个参数。

在下一章中,我们将使用 SQL Server 构建一个使用 ASP.NET 网络应用编程接口的任务管理器。

问题

以下问题供您考虑:

  1. 如何更新看板板以允许用户删除任务?
  2. 为什么要在字典中包含一个未在组件上显式或隐式定义的属性拆分?
  3. DragEventArgs类的基类是什么?

进一步阅读

以下资源提供了有关本章所涵盖主题的更多信息:

八、使用 ASP.NET Web API 构建任务管理器

大多数网站不是孤立的孤岛。他们需要一台服务器。除了其他服务之外,他们在数据访问和安全性方面都依赖于服务器。

在本章中,我们将学习如何创建一个托管的 Blazor WebAssembly 应用。我们将学习如何使用HttpClient服务调用 web APIs,我们还将学习如何使用 JSON 助手方法进行请求,以便读取、添加、编辑和删除数据。

我们在本章中创建的项目将是一个任务管理器。我们将使用多项目架构将 Blazor WebAssembly 应用与ASP.NET Web API端点分开。托管的 Blazor WebAssembly 应用将使用 JSON 助手方法来读取、添加、编辑和删除存储在 SQL Server 上的任务。一个 ASP.NET Core 项目将提供 ASP.NET 网络应用编程接口端点。

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

  • 了解托管应用
  • 使用HttpClient服务
  • 使用 JSON 助手方法
  • 创建任务管理器项目

技术要求

要完成此项目,您需要在电脑上安装 Visual Studio 2019。关于如何安装 Visual Studio 2019 免费社区版的说明,请参考 第 1 章 、Blazor WebAssembly 简介。您还需要访问一个版本的 SQL Server。关于如何安装 SQL Server 2019 免费版的说明,请参考 第一章 、Blazor WebAssembly 简介

本章的源代码可在以下 GitHub 存储库中获得:https://GitHub . com/PacktPublishing/Blazor-web assembly by Example/tree/main/chapter 08

行动中的代码视频可在此获得:https://bit.ly/340Im6M

了解托管应用

当我们使用微软的 Blazor WebAssembly App 项目模板创建一个新的 Blazor WebAssembly 项目时,我们可以选择通过选中ASP.NET Core 托管的复选框来创建一个托管的 Blazor WebAssembly 应用。

以下截图突出显示了ASP.NET Core 托管的复选框:

Figure 8.1 – Blazor WebAssembly App project template

图 8.1–Blazor WebAssembly 应用项目模板

Blazor WebAssembly App 项目模板创建的托管 Blazor WebAssembly 应用包括以下三个项目:

  • 客户项目
  • 服务器项目
  • 共享项目

客户项目

客户端项目是客户端 Blazor WebAssembly 项目。它几乎与我们在本书第二章**【构建你的第一个 Blazor WebAssembly 应用】中创建的独立 Blazor WebAssembly 应用相同。唯一的区别是数据的访问方式。在客户端项目中,使用网络应用编程接口端点而不是静态文件从服务器项目访问示例数据。

*## 服务器项目

服务器项目是 ASP.NET Core 项目。该项目负责为应用提供服务。除了托管客户端应用之外,服务器项目还提供了网络应用编程接口端点。

小费

在这种情况下,服务器项目必须设置为解决方案中的启动项目。

共享项目

共享项目也是 ASP.NET Core 项目。它包含在其他两个项目之间共享的应用逻辑。过去,我们必须在客户机和服务器上编写验证代码。我们必须为客户端编写 JavaScript 验证代码,为服务器编写 C#验证代码。毫不奇怪,有时两个验证模型不匹配。共享项目解决了这个问题,因为所有的验证代码都保存在一个位置和一种语言中。

通过使用多项目解决方案,我们可以创建一个更健壮的应用。共享项目定义类,客户端项目使用HttpClient服务向服务器项目请求数据。

使用 HttpClient 服务

HTTP 不仅仅是为 web 页面服务——它还可以用来服务数据。这些是我们将在本章中使用的 HTTP 方法:

  • GET:此方法用于请求一个或多个资源。
  • POST:此方法用于新建资源。
  • PUT:此方法用于更新指定的资源。
  • DELETE:此方法用于删除指定的资源。

HttpClient服务是一个预配置的服务,用于从 Blazor WebAssembly 应用发出 HTTP 请求。在Program.cs文件中配置。下面的代码用于配置它:

builder.Services.AddScoped(sp => new HttpClient {
  BaseAddress = new
    Uri(builder.HostEnvironment.BaseAddress) 
});

使用依赖注入 ( DI )将HttpClient服务添加到页面。要在组件中使用HttpClient服务,您必须通过使用@inject指令或Inject属性来注入它。关于 DI 的更多信息,请参见 第六章 ,使用应用状态构建购物车。

下面的代码显示了将HttpClient服务注入组件的两种不同方式:

@inject HttpClient Http
[Inject] public HttpClient Http { get; set; }

在我们将HttpClient服务注入到组件中之后,我们可以使用 JSON 助手方法向网络应用编程接口发送请求。

使用 JSON 助手方法

有三种 JSON 助手方法。有一个用于读取数据,一个用于添加数据,一个用于更新数据。由于没有删除数据的方法,我们将使用HttpClient.DeleteAsync方法删除数据:

Figure 8.2 – Relationship between the HTTP methods and the JSON helper methods

图 8.2–HTTP 方法和 JSON 助手方法之间的关系

上表指出了 JSON 助手方法和 HTTP 方法之间的关系。

小费

您也可以使用HttpClient服务和 JSON 助手方法来调用外部网络应用编程接口端点。举例来说,参见 第五章 ,将天气应用构建为渐进式网络应用(PWA)

GetFromJsonAsync

GetFromJsonAsync方法是用来读取数据。它做到了以下几点:

  • 向指定的 URI 发送HTTP GET请求。
  • 反序列化 JSON 响应体以创建指示的对象。

以下代码返回一组TaskItem对象:

string requestUri = "TaskItems";
tasks = await 
    Http.GetFromJsonAsync<IList<TaskItem>>(requestUri); 

在后续的代码中,返回的对象类型是IList<TaskItem>。我们也可以使用GetFromJsonAsync方法来获取单个对象。以下代码返回单个TaskItem对象,其中Id是该对象的唯一标识符:

string requestUri = $"TaskItems/{Id}";
tasks = await 
    Http.GetFromJsonAsync<TaskItem>(requestUri);

在前面的代码中,返回的对象类型是TaskItem.

postsjsonasync

PostAsJsonAsync方法用于添加数据。它执行以下操作:

  • 向指示 URI 的发送HTTP POST请求。该请求包括用于创建新数据的 JSON 编码内容。
  • 返回一个包含状态代码和数据的HttpResponseMessage实例。

以下代码创建了一个新的TaskItem对象:

string requestUri = "TaskItems";
var response = await 
    Http.PostAsJsonAsync(requestUri, newTaskItem);
if (response.IsSuccessStatusCode)
{
    var task = await 
        response.Content.ReadFromJsonAsync<TaskItem>();
}; 

在前面的代码中,如果 HTTP 响应成功,则task将从响应中反序列化。

婊子们同步

PutAsJsonAsync方法用于更新数据。它做到了以下几点:

  • 向指定的 URI 发送HTTP PUT请求。该请求包括用于更新数据的 JSON 编码内容。
  • 返回一个包含状态代码和数据的HttpResponseMessage实例。

以下代码更新现有的TaskItem对象:

string requestUri = $"TaskItems/{task.TaskItemId}";
var response = await 
   Http.PutAsJsonAsync<TaskItem>(requestUri, updatedTaskItem);
if (response.IsSuccessStatusCode)
{
    var task = await 
        response.Content.ReadFromJsonAsync<TaskItem>();
};

在前面的代码中,如果 HTTP 响应成功,将从响应中反序列化task

HttpClient。DeleteAsync(删除异步)

HttpClient.DeleteAsync方法是用来删除数据。它执行以下操作:

  • 向指定的 URI 发送HTTP DELETE请求。
  • 返回一个包含状态代码和数据的HttpResponseMessage实例。

以下代码删除现有的TaskItem对象:

string requestUri = $"TaskItems/{taskItem.TaskItemId}";
var response = await Http.DeleteAsync(requestUri);
if (!response.IsSuccessStatusCode)
{
    // handle error
};

JSON 助手方法使使用网络应用编程接口变得容易。我们使用它们来读取、创建和更新数据。我们用HttpClient.DeleteAsync删除数据。

现在,让我们快速了解一下我们将在本章中构建的项目。

项目概述

在本章中,我们将构建一个 Blazor WebAssembly 应用来管理任务。我们将能够查看、添加、编辑和删除任务。这些任务将存储在一个 SQL Server 数据库中。

这是完整应用的屏幕截图:

Figure 8.3 – TaskManager project

图 8.3–任务管理器项目

该项目的构建时间约为 75 分钟。

创建任务管理器项目

TaskManager项目将通过使用微软的 Blazor WebAssembly 应用项目模板创建一个托管的 Blazor WebAssembly 应用来创建。首先,我们将检查由项目模板创建的演示项目。然后,我们将添加一个TaskItem类和一个TaskItemsController类。我们将使用实体框架迁移在 SQL Server 中创建一个数据库。最后,我们将演示如何使用HttpClient服务读取数据、更新数据、删除数据和添加数据。

开始项目

我们需要创建一个新的 Blazor WebAssembly 应用。我们按如下方式进行:

  1. 打开 Visual Studio 2019

  2. 点击新建项目按钮。

  3. In the Search for templates (Alt+S) textbox, enter Blazor and then hit the Enter key.

    下面的截图显示了我们将要使用的 Blazor WebAssembly App 项目模板:

    Figure 8.4 – Blazor WebAssembly App project template

    图 8.4–Blazor WebAssembly 应用项目模板

  4. 选择 Blazor WebAssembly App 项目模板,点击下一步按钮。

  5. Enter TaskManager in the Project name textbox and then click the Next button.

    这是用于配置新项目的对话框截图:

    Figure 8.5 – Configure your new project dialog

    图 8.5–配置您的新项目对话框

    小费

    在前面的例子中,我们将TaskManager项目放入E:/Blazor文件夹中。然而,这个项目的位置并不重要。

  6. 选择.NET 5.0 作为目标框架

  7. 选中ASP.NET Core 托管复选框。

  8. 点击创建按钮。

  9. 右键单击TaskManager.Server项目,从菜单中选择设置为启动项目选项。

您已经创建了任务管理器项目。下面的屏幕截图显示了组成任务管理器项目的三个项目:

Figure 8.6 – Solution Explorer

图 8.6–解决方案资源管理器

现在我们都准备好检查托管的 Blazor WebAssembly 应用了。

检查托管的 Blazor WebAssembly 应用

托管的 Blazor WebAssembly 应用包括一个演示项目。让我们运行它来了解它的功能。我们按如下方式进行:

  1. 调试菜单中,选择不调试启动 ( Ctrl + F5 )选项运行项目。
  2. 查看主页页面。
  3. 查看计数器页面。
  4. 查看获取数据页面。
  5. 关闭浏览器。

在开始我们的任务管理器项目之前,我们需要移除演示项目。

清空溶液

要清空解决方案,我们需要删除一些组件,更新几个组件,并删除一个控制器和一个类。我们按如下方式进行:

  1. 返回 Visual Studio

  2. 删除TaskManager.Client.Pages文件夹中除Index以外的所有组件。

  3. 删除TaskManager.Client.Shared\SurveyPrompt.razor文件。

  4. 打开TaskManager.Client.Shared\MainLayout.razor文件。

  5. 通过移除以下标记,从布局的顶行移除About链接:

    <a href="http://Blazor.net" target="_blank"
       class="ml-md-auto">
            About
    </a>
    
  6. 打开TaskManager.Client.Shared\NavMenu.razor文件。

  7. 删除CounterFetch data页面的li元素。

  8. TaskManager.Server项目中,删除Controllers\WeatherForecastConroller.cs文件。

  9. TaskManager.Shared项目中,删除WeatherForecast.cs文件。

  10. 构建菜单中,选择构建解决方案选项。

我们通过删除演示项目准备了解决方案。现在,我们可以开始添加我们的TaskManager特定内容了。首先,我们将添加一个类来包含任务。

添加任务项类

我们需要增加TaskItem类。我们按如下方式进行:

  1. 右键单击TaskManager.Shared项目,从菜单中选择添加,类别选项。

  2. 命名新类TaskItem

  3. 点击添加按钮。

  4. 通过添加public修饰符

    public class TaskItem
    

    使类公开

  5. 将以下属性添加到TaskItem类中:

    public int TaskItemId { get; set; }
    public string TaskName { get; set; }
    public bool IsComplete { get; set; }
    
  6. 构建菜单中,选择构建解决方案选项。

我们增加了TaskItem类。接下来,我们需要为TaskItem类添加一个 API 控制器

添加任务项应用编程接口控制器

我们需要增加一个TaskItemsController类。我们按如下方式进行:

  1. 右键单击TaskManager.Server.Contollers文件夹,从菜单中选择添加控制器选项。

  2. Select the API Controller with actions, using Entity Framework option:

    Figure 8.7 – Add New Scaffolded Item dialog

    图 8.7–添加新脚手架项目对话框

  3. 点击添加按钮。

  4. 型号等级设置为TaskItem (TaskManager.Shared)

  5. Click the Add data context button to open the Add Data Context dialog:

    Figure 8.8 – Add Data Context dialog

    图 8.8–添加数据上下文对话框

  6. 点击添加按钮到接受默认值。

  7. Click the Add button on the Add API Controller with actions, using Entity Framework dialog:

    Figure 8.9 – Add API Controller with actions, using Entity Framework dialog

    图 8.9–使用实体框架对话框添加带动作的应用编程接口控制器

  8. 将路线更新为:

    [Route("[controller]")]
    

我们创建了TaskItemsController类。现在我们需要设置 SQL Server。

设置 SQL 服务器

我们需要在 SQL Server 上创建一个新的数据库,并添加一个包含任务的表。我们按如下方式进行:

  1. 打开TaskManager.Server\appsettings.json文件。

  2. Update the connection string to point to your instance of SQL Server and change the name of the database to TaskManager:

    "ConnectionStrings": {
      "TaskManagerServerContext": "Server=TOI-WORK\\SQLEXPRESS2019; Database=TaskManager; Trusted_Connection=True; MultipleActiveResultSets=true"
    }
    

    前面的代码假设我们的服务器名为TOI-WORK\\SQLEXPRESS2019,数据库名为TaskManager

    重要说明

    虽然此示例使用的是 SQL Server Express 2019,但使用什么版本的 SQL Server 并不重要。

  3. 工具菜单中,选择获取包装管理器,包装管理器控制台选项。

  4. 包管理器控制台中,将默认项目更改为TaskManager.Server

  5. Execute the following commands in Package Manager Console:

    Add-Migration Init
    Update-Database
    

    上述命令使用实体框架迁移来更新 SQL Server。

  6. 视图菜单中,选择 SQL Server 对象浏览器

  7. If you do not see the SQL Server instance that you are using for this project, click the Add SQL Server button to connect it:

    Figure 8.10 – SQL Server Object Explorer

    图 8.10–SQL Server 对象资源管理器

  8. Navigate to TaskManager, Tables, dbo.TaskItem:

    Figure 8.11 – TaskManager database

    图 8.11–任务管理器数据库

  9. 右键单击dbo.TaskItem和选择查看数据选项。

  10. 别上标签。

  11. Enter a couple of tasks by completing the TaskName field and setting the IsComplete field to False:

![Figure 8.12 – Sample data ](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/blazor-webasm-exam/img/Figure_8.12_B16786.jpg)

图 8.12–样本数据
  1. 调试菜单中,选择不调试启动 ( Ctrl + F5 )选项运行项目。
  2. Add /taskitems to the address bar and then click Enter.
下面的截图显示了`TaskItemsController`返回的 JSON:

![Figure 8.13 – JSON returned by the TaskItem API controller ](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/blazor-webasm-exam/img/Figure_8.13_B16786.jpg)

图 8.13–任务项应用编程接口控制器返回的 JSON
  1. 关闭浏览器。

我们已经证明了TaskItemsController是有效的。现在我们可以开始我们的客户项目了。

显示任务

我们需要获取任务列表并显示给用户。我们按如下方式进行:

  1. 返回 Visual Studio

  2. 右键单击TaskManager.Client.Pages文件夹,从菜单中选择添加,类别选项。

  3. 命名新类Index.razor.cs

  4. 点击添加按钮。

  5. partial修改器添加到类中:

    public partial class Index
    
  6. Add the following code to the Index class:

    [Inject] public HttpClient Http { get; set; }
    private IList<TaskItem> tasks;
    private string error;
    protected override async Task OnInitializedAsync()
    {
        try
        {
            string requestUri = "TaskItems";
            tasks = await
                Http.GetFromJsonAsync<IList<TaskItem>>
               (requestUri);
        }
        catch (Exception)
        {
            error = "Error Encountered";
        };
    }
    

    前面的代码使用GetFromJsonAsync方法返回TaskItem对象的集合。

  7. 增加以下using语句:

    using Microsoft.AspNetCore.Components;
    using System.Net.Http;
    using System.Net.Http.Json;
    using TaskManager.Shared;
    
  8. 打开TaskManager.Client.Pages\Index.razor页面。

  9. Update the markup to the following:

    @page "/"
    @if (tasks == null)
    {
        <p><em>Loading...</em></p>
    }
    else
    {
        @foreach (var taskItem in tasks)
        {
    
        }
    }
    

    如果tasksnull,则前面的标记显示加载信息。否则,它会在tasks中的TaskItem对象集合中循环。

  10. Add the following markup to the @foreach loop:

```cs
<div class="d-flex col col-lg-3 border-bottom"
        @key="taskItem">
    <div class="p-2 flex-fill">
        <input type="checkbox"
                checked="@taskItem.IsComplete" />
        <span>@taskItem.TaskName</span>
    </div>
    <div class="p-1">
        <button class="btn btn-outline-danger btn-sm" 
                title="Delete task">
            <span class="oi oi-trash"></span>
        </button>
    </div>
</div>
```

前面的标记为每个`TaskItem`类显示一个复选框、`TaskName`字段和一个删除按钮。
  1. From the Debug menu, select the Start Without Debugging (Ctrl+F5) option to run the project.
以下是**首页**页面截图:

Figure 8.14 – List of tasks

图 8.14–任务列表

我们已经向主页页面添加了任务列表,但是当我们单击复选框或删除按钮时,什么也没有发生。接下来,我们需要允许用户将任务标记为完成。

完成任务

我们将允许用户通过点击任务名称旁边的复选框将任务标记为完成。我们按如下方式进行:

  1. 返回 Visual Studio

  2. 右键单击Pages文件夹,从菜单中选择添加,新项目选项。

  3. 搜索框中输入css

  4. 选择样式表

  5. 命名文件Index.razor.css

  6. 点击添加按钮。

  7. 输入以下样式:

    .completed-task {
        text-decoration: line-through;
    } 
    
  8. 打开Index.razor文件。

  9. Update the span element used to display the task's name to the following:

    <span class="@((taskItem.IsComplete? "completed-task" : ""))">
        @taskItem.TaskName
    </span>
    

    当任务完成时,前面的标记将把span元素的类设置为completed-task

  10. 在复选框中添加以下标记:

```cs
@onchange="@(()=>CheckboxChecked(taskItem))"
```
  1. 打开TaskManager.Client.Pages\Index.razor.cs文件。
  2. Add the following CheckboxChecked method:
```cs
private async Task CheckboxChecked(TaskItem task)
{
    task.IsComplete = !task.IsComplete;
    string requestUri =
      $"TaskItems/{task.TaskItemId}";
    var response = await
        Http.PutAsJsonAsync<TaskItem>(requestUri,
          task);
    if (!response.IsSuccessStatusCode)
    {
        error = response.ReasonPhrase;
    };
}
```

前面的代码使用`PutAsJsonAsync`方法更新指示的`TaskItem`类。
  1. 构建菜单中,选择构建解决方案选项。
  2. 返回浏览器。
  3. 使用 Ctrl + R 刷新浏览器。
  4. Mark one of the tasks as complete by clicking the checkbox next to it.
下面的截图显示了一个已经完成的任务:

![Figure 8.15 – Completed task ](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/blazor-webasm-exam/img/Figure_8.15_B16786.jpg)

图 8.15–已完成的任务
  1. 返回 Visual Studio
  2. 选择和 dbo。我们之前锁定的任务项[数据] 选项卡。
  3. 点击刷新(Shift+Alt+R按钮,确认IsComplete字段已更新。

当用户选中任务旁边的复选框时,用户界面会更新,并且 SQL Server 数据库也会更新。接下来,我们需要添加删除任务的能力。

删除任务

我们需要允许用户删除任务。我们按如下方式进行:

  1. 打开Index.razor文件。

  2. 通过添加突出显示的代码将button元素更新为以下内容:

    <button class="btn btn-outline-danger btn-sm" 
            title="Delete task"
            @onclick="@(()=>DeleteTask(taskItem))">
        <span class="oi oi-trash"></span>
    </button>
    
  3. 打开TaskManager.Client.Pages\Index.razor.cs文件。

  4. Add the following DeleteTask method:

    private async Task DeleteTask(TaskItem taskItem)
    {
        tasks.Remove(taskItem);
        string requestUri = 
            $"TaskItems/{taskItem.TaskItemId}";
        var response = await Http.DeleteAsync(requestUri);
        if (!response.IsSuccessStatusCode)
        {
            error = response.ReasonPhrase;
        };
    }
    

    前面的代码使用Http.DeleteAsync方法删除指示的TaskItem类。

  5. 构建菜单中,选择构建解决方案选项。

  6. 返回浏览器。

  7. 使用 Ctrl + R 刷新浏览器。

  8. 点击删除按钮,删除其中一个任务。

  9. 返回 Visual Studio

  10. 选择 dbo。任务项目[数据] 选项卡。

  11. 点击刷新(Shift+Alt+R按钮,确认其中一个任务已经删除。

我们增加了删除任务的能力。现在我们需要添加添加新任务的能力。

添加新任务

我们需要增加添加新任务的能力。我们按如下方式进行:

  1. 打开Index.razor文件。

  2. @foreach循环前添加以下标记:

    <div class="d-flex col col-lg-3 mb-4">
        <input placeholder="Enter Task" @bind="newTask" />
        <button class="btn btn-success" 
                @onclick="AddTask">Submit</button>
    </div>
    
  3. 打开TaskManager.Client.Pages\Index.razor.cs文件。

  4. 添加以下变量:

    private string newTask;
    
  5. Add the following AddTask method:

    private async Task AddTask()
    {
        if (!string.IsNullOrWhiteSpace(newTask))
        {
            TaskItem newTaskItem = new TaskItem
            {
                TaskName = newTask,
                IsComplete = false
            };
            tasks.Add(newTaskItem);            
            string requestUri = "TaskItems";
            var response = await
               Http.PostAsJsonAsync(requestUri,
                 newTaskItem);
            if (response.IsSuccessStatusCode)
            {
                newTask = string.Empty;
                var task = 
                  await response.Content.ReadFromJsonAsync
                    <TaskItem>();
            }
            else
            {
                error = response.ReasonPhrase;
            };
        };
    }
    

    前面的代码使用PostAsJsonAsync方法创建了一个新的TaskItem类。返回的TaskItem类被反序列化为task变量。

    小费

    如果需要新的TaskItem类的Id属性,可以从task变量中获取。

  6. 构建菜单中,选择构建解决方案选项。

  7. 返回浏览器。

  8. 使用 Ctrl + R 刷新浏览器。

  9. 添加一些新任务。

  10. 返回 Visual Studio

  11. 选择 dbo。任务项目[数据] 选项卡。

  12. 单击刷新(Shift+Alt+R按钮,验证任务是否已添加到 SQL Server 数据库。

我们现在增加了添加新任务的能力。

总结

现在,您应该能够创建一个托管的 Blazor WebAssembly 应用,该应用使用 ASP.NET Web API 来更新 SQL Server 数据库中的数据。

在本章中,我们介绍了托管的 Blazor WebAssembly 应用、HttpClient服务以及用于读取、创建和更新数据的 JSON 助手方法。我们还演示了如何使用HttpClient.DeleteAsync方法删除数据。

之后,我们使用微软的 Blazor WebAssembly 应用项目模板创建了一个托管的 Blazor WebAssembly 应用。我们检查了演示项目,然后将其从多项目解决方案中删除。我们增加了一个TaskItem类和一个TaskItem API 控制器。接下来,我们通过更新数据库的连接字符串并使用实体框架迁移来配置 SQL Server。最后,我们使用HttpClient服务来读取任务列表、更新任务、删除任务和添加新任务。

我们可以应用我们的新技能来创建一个托管的 Blazor WebAssembly 应用,它是多项目解决方案的一部分,并使用 ASP.NET Web API 来读取、创建、更新和删除数据。

在下一章中,我们将使用EditForm组件构建一个费用跟踪器。

问题

以下问题供您考虑:

  1. 与独立的 Blazor WebAssembly 项目相比,使用托管的 Blazor WebAssembly 项目有什么好处?
  2. 代码使用@code块好还是代码使用分部类好?

进一步阅读

以下资源提供了有关本章所涵盖主题的更多信息:

九、使用编辑表单组件构建费用跟踪器

大多数应用都需要一些数据输入。Blazor WebAssembly 框架包括许多用于输入和验证数据的内置组件。

在本章中,我们将学习如何使用EditForm组件、各种内置输入组件和内置输入验证组件。

我们将在本章中创建的项目将是一个旅行费用追踪器。我们将使用多项目架构将 Blazor WebAssembly 应用与ASP.NET Web API端点分开。用于添加和编辑费用的页面将使用EditForm组件以及许多内置的输入组件。它还将使用内置的验证组件。我们将学习如何使用内置组件向任何 Blazor WebAssembly 应用添加数据输入、验证和提交。

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

  • EditForm组件
  • 使用内置的输入组件
  • 使用验证组件
  • 创建ExpenseTracker项目

技术要求

要完成此项目,您需要在电脑上安装 Visual Studio 2019。关于如何安装 Visual Studio 2019 免费社区版的说明,请参考 第 1 章 、Blazor WebAssembly 简介。您还需要访问一个版本的 SQL Server。关于如何安装 SQL Server 2019 免费版的说明,请参考 第一章 、Blazor WebAssembly 简介

本章的源代码可在以下 GitHub 存储库中获得:https://GitHub . com/PacktPublishing/Blazor-web assembly by Example/tree/main/chapter 09

行动中的代码视频可在此获得:https://bit.ly/2T5UfpR

编辑表单组件概述

在本书前面的章节中,我们使用了标准的 HTML form元素来收集用户输入。然而,Blazor WebAssembly 框架提供了标准 HTML form元素的增强版本,称为EditForm组件。

EditForm组件不仅管理表单,还协调验证和提交事件。以下代码显示了一个空的EditForm元素:

<EditForm Model="expense" OnValidSubmit="HandleValidSubmit">   
</EditForm>

在前面的代码中,Model属性指定了表单的顶层模型对象。OnValidSubmit属性指定当提交表单时没有任何验证错误时将调用的回调。

有三种不同的回调与表单提交相关联:

  • OnValidSubmit
  • OnInvalidSubmit
  • OnSubmit

我们可以一起使用OnValidSubmitOnInvalidSubmit回调,也可以单独使用。或者,我们可以自行使用OnSubmit回调。如果我们使用OnSubmit回调,我们负责执行表单验证。否则,表单验证由EditForm组件执行。

小费

如果我们设置一个OnSubmit回调,任何使用OnValidSubmitOnInvalidSubmit设置的回调都会被忽略。

有相当多的内置输入组件,我们可以结合EditForm组件使用。

使用内置的输入组件

下面的表列出了内置的输入组件以及它们呈现的 HTML:

Figure 9.1 – Built-in input components

图 9.1–内置输入组件

所有内置输入组件都能够在放置在EditForm元素中时接收和验证用户输入。它们继承自抽象的InputBase类。我们可以通过创建同样继承自InputBase类的组件来添加额外的输入组件。

输入数据在表单提交和数据更改时都会被验证。

使用验证组件

输入验证是每个应用的一个重要方面,因为它防止用户输入无效数据。Blazor WebAssembly 框架使用数据注释进行输入验证。有 30 多个内置数据注释属性。这是我们将在此项目中使用的列表:

  • Required:该属性指定需要一个值。它是最常用的属性。
  • Display:该属性指定在错误消息中显示的字符串。
  • MaxLength:该属性指定允许的最大字符串长度。
  • Range:该属性指定数值的数值范围约束。

下面的代码演示了一些数据注释的使用:

[Required]
public DateTime? Date { get; set; }
[Required]
[Range(0, 500, ErrorMessage = "The {0} field must be < {2}.")]
public decimal? Amount { get; set; }

有两个内置的验证组件:

  • ValidationSummary
  • ValidationMessage

ValidationSummary组件汇总验证消息,而ValidationMessage组件显示单个组件的验证消息。EditForm组件可以包括两种类型的验证组件。然而,为了使用任一类型的验证组件,我们必须将DataAnnotationsValidator添加到EditForm组件中。

下面的截图显示了一个ValidationSummary组件和单个ValidationMesssage组件的结果:

Figure 9.2 – Validation components

图 9.2–验证组件

现在让我们快速了解一下我们将在本章中构建的项目。

项目概述

在这章,我们将构建一个项目来跟踪差旅费。我们将能够查看、添加和编辑费用。费用将存储在一个 SQL Server 数据库中。

这是已完成申请的添加/编辑费用页面截图。

Figure 9.3 – Add/Edit Expense page

图 9.3–添加/编辑费用页面

本项目建设时间约 90 分钟。

创建费用追踪项目

ExpenseTracker项目将使用微软的 Blazor WebAssembly 应用项目模板创建一个托管的 Blazor WebAssembly 应用。首先,我们将删除演示项目。然后,我们将添加项目所需的类和应用编程接口控制器。我们将在首页增加一个表格,显示当前的费用清单。最后,我们将结合许多内置的输入组件使用EditForm组件来添加和编辑费用。

开始项目

我们需要创建一个新的 Blazor WebAssembly 应用。我们按如下方式进行:

  1. 打开 Visual Studio 2019

  2. 点击新建项目按钮。

  3. In the Search for templates (Alt + S) textbox, enter Blazor and hit the Enter key.

    下面的截图显示了我们将要使用的 Blazor WebAssembly App 项目模板。

    Figure 9.4 – Blazor WebAssembly App project template

    图 9.4–Blazor WebAssembly 应用项目模板

  4. 选择 Blazor WebAssembly App 项目模板,然后点击下一步按钮。

  5. Enter ExpenseTracker in the Project name textbox and click the Next button:

    Figure 9.5 – Configure your new project dialog

    图 9.5–配置您的新项目拨号 og

    小费

    在前面的例子中,我们将ExpenseTracker项目放入E:/Blazor文件夹中。然而,这个项目的位置并不重要。

  6. 选择.NET 5.0 作为目标框架

  7. Check the ASP.NET Core Hosted checkbox.

    当您选中ASP.NET Core 托管复选框时,项目模板将创建一个多项目解决方案。有关托管应用的更多信息,请参考第 8 章 【使用 ASP.NET 网络应用编程接口构建任务管理器】

  8. 点击创建按钮。

  9. 右键单击ExpenseTracker.Server项目,从菜单中选择设置为启动项目选项。

您已经创建了ExpenseTracker Blazor WebAssembly 项目。使用托管应用的项目模板创建的项目包括一个演示项目。在我们开始之前,我们需要删除演示项目。

删除演示项目

要删除演示项目,我们需要删除一些组件,更新几个组件,并删除一个控制器和一个类。我们按如下方式进行:

  1. 删除ExpenseTracker.Client.Pages文件夹中除Index以外的所有组件。

  2. 删除ExpenseTracker.Client.Shared\SurveyPrompt.razor文件。

  3. 打开ExpenseTracker.Client.Shared\MainLayout.razor文件。

  4. 通过移除以下标记,从布局的顶行移除About链接:

    <a href="http://Blazor.net" target="_blank"
       class="ml-md-auto">
            About
    </a>
    
  5. 打开ExpenseTracker.Client.Shared\NavMenu.razor文件。

  6. 删除CounterFetch data页面的 li元素。

  7. ExpenseTracker.Server项目中,删除Controllers\WeatherForecastConroller.cs文件。

  8. ExpenseTracker.Shared项目中,删除WeatherForecast.cs文件。

  9. 构建菜单中,选择构建解决方案选项。

我们通过移除演示项目准备了解决方案。现在,我们可以开始添加我们的ExpenseTracker具体内容了。首先,我们需要添加几个类。

添加类别

我们需要增加一个ExpenseType类和一个Expense类。我们按如下方式进行:

  1. 右键单击ExpenseTracker.Shared文件夹,从菜单中选择添加,类别选项。

  2. 命名新类ExpenseType

  3. 点击添加按钮。

  4. 通过添加public修饰符

    public class ExpenseType
    

    使类公开

  5. 将以下属性添加到ExpenseType类中:

    public int Id { get; set; }
    public string Type { get; set; }
    
  6. 右键单击ExpenseTracker.Shared文件夹,从菜单中选择添加,类别选项。

  7. 命名新类Expense

  8. 点击添加按钮。

  9. 通过添加public修饰符

    public class Expense
    

    使类公开

  10. 增加以下using语句:

```cs
using System.ComponentModel.DataAnnotations;
```
  1. Add the following properties to the Expense class:
```cs
public int Id { get; set; }
[Required]
public DateTime? Date { get; set; }
[Required]
[MaxLength(100)]
public string Vendor { get; set; }
public string Description { get; set; }

[Required]
[Display(Name = "Expense Type")]
public int? ExpenseTypeId { get; set; }
[Required]
[Range(0, 500, ErrorMessage = "The {0} field must be <= {2}.")]
public decimal? Amount { get; set; }
public bool Paid { get; set; }
```

在前面的代码中,我们使用了数据注释来添加一些简单的数据验证。`Date`、`Vendor`、`ExpenseTypeId`、`Amount`均为必填项。`Vendor`最大长度为 100 个字符。`ExpenseTypeId`的显示名称为`Expense Type`。费用的`Amount`上限为`500`。
  1. 构建菜单中,选择构建解决方案选项。

我们增加了这个ExpenseType类和Expense类。现在我们需要配置网络应用编程接口端点。

添加应用编程接口控制器

我们需要为每个新类添加一个应用编程接口控制器。我们按如下方式进行:

  1. 右键单击ExpenseTracker.Server.Contollers文件夹,从菜单中选择添加控制器选项。

  2. 使用实体框架选项,选择带动作的应用编程接口控制器。

  3. 点击添加按钮。

  4. 模型类设置为ExpenseType (ExpenseTracker.Shared)

  5. Click the Add data context button to open the Add Data Context dialog:

    Figure 9.6 – Add Data Context dialog

    图 9.6–添加数据上下文对话框

  6. 点击添加按钮接受默认值。

  7. Click the Add button:

    Figure 9.7 – Add API Controller with actions, using Entity Framework dialog

    图 9.7–使用实体框架对话框添加带动作的应用编程接口控制器

  8. 将路线更新为如下:

    [Route("[controller]")]
    
  9. 右键单击ExpenseTracker.Server.Contollers文件夹,从菜单中选择添加控制器选项。

  10. 使用实体框架选项,选择带动作的应用编程接口控制器。

  11. 点击添加按钮。

  12. 模型类设置为Expense (ExpenseTracker.Shared)

  13. 点击添加按钮。

  14. 将路线更新如下:

```cs
[Route("[controller]")]
```

我们添加了两个新的控制器来提供我们的应用将使用的应用编程接口端点。接下来,我们需要创建 SQL Server 数据库。

创建 SQL Server 数据库

我们需要创建 SQL Server 数据库,并向其中添加两个表。我们按如下方式进行:

  1. 打开ExpenseTracker.Server\appsettings.json文件。

  2. Update the connection string to point to your instance of SQL Server and change the name of the database to ExpenseTracker:

    "ConnectionStrings": {
      "ExpenseTrackerServerContext": "Server=TOI-WORK\\SQLEXPRESS2019; Database=ExpenseTracker; Trusted_Connection=True; MultipleActiveResultSets=true"
    }
    

    前面的代码假设我们的服务器名为TOI-WORK\\SQLEXPRESS2019,数据库名为ExpenseTracker

    重要说明

    虽然示例使用的是 SQL Server Express 2019,但是您使用的是什么版本的 SQL Server 并不重要。

  3. 打开ExpenseTracker.Server.Data\ExpenseTrackerServerContext.cs文件。

  4. Add the following OnModelCreating method:

    protected override void OnModelCreating
        (ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ExpenseType>().HasData(
        new ExpenseType { Type = "Airfare", Id = 1 },
        new ExpenseType { Type = "Lodging", Id = 2 },
        new ExpenseType { Type = "Meal", Id = 3 },
        new ExpenseType { Type = "Other", Id = 4 }
        );
    }
    

    前面的代码将播种ExpenseType表。

  5. 工具菜单中,选择获取包装管理器,包装管理器控制台选项。

  6. 包管理器控制台中,将默认项目更改为ExpensesManager.Server

  7. Execute the following commands in the Package Manager Console:

    Add-Migration Init
    Update-Database
    

    上述命令使用实体框架迁移来更新 SQL Server。

  8. 调试菜单中,选择不调试启动 ( Ctrl + F5 )选项运行项目。

  9. Add /expensetypes to the address bar and click Enter.

    下面的截图显示了ExpenseTypesController返回的 JSON。

    Figure 9.8 – JSON returned by the ExpenseTypes API controller

    图 9.8–ExpenseTypes API 控制器返回的 JSON

  10. 关闭浏览器。

我们在 SQL Server 上创建了一个新的数据库,添加了两个表,并用种子数据填充了其中一个表。设置完 SQL Server 后,我们测试ExpenseTypesController工作正常。最后,我们准备创建一个组件来显示费用。

查看费用

我们需要添加一个表格来显示费用列表。我们按如下方式进行:

  1. 返回 Visual Studio

  2. 打开ExpenseTracker.Client.Pages\Index.razor页面。

  3. Update the markup to the following:

    @page "/"
    @using ExpenseTracker.Shared
    @inject HttpClient Http
    <h2>Expenses</h2>
    @if (expenses == null)
    {
        <p><em>Loading...</em></p>
    }
    else if (expenses.Count == 0)
    {
        <div>None Found</div>
    }
    else
    {
    }
    @code{
        IList<Expense> expenses;
    }
    

    前面的代码将expenses定义为IList<Expense>,并检查它是null还是空的。

  4. Add the following OnInitializedAsync method to the @code block.

    protected override async Task OnInitializedAsync()
    {
        expenses = await Http.GetFromJsonAsync
                <IList<Expense>>("Expenses");
    }
    

    前面的代码填充了expenses对象。

  5. else声明中增加以下table:

    <table class="table">
    </table>
    
  6. table中添加以下thead元素:

    <thead>
        <tr>
            <th></th>
            <th>#</th>
            <th>Date</th>
            <th>Vendor</th>
            <th class="text-right">Amount</th>
        </tr>
    </thead>
    
  7. Add the following tbody element to table:

    <tbody>
        @foreach (var item in expenses)
        {
        <tr class="@(item.Paid ? "" : "table-danger")">
            <td>
                <a href="/expense/@item.Id">Edit</a>
            </td>
            <td>@item.Id</td>
            <td>@item.Date.Value.ToShortDateString()</td>
            <td>@item.Vendor</td>
            <td class="text-right">@item.Amount</td>
        </tr>
        }
    </tbody>
    

    前面的代码循环遍历expenses对象中的每个Expense对象,并将它们显示为表格中的行。如果费用尚未支付,则使用table-danger类以红色突出显示该行。

  8. From the Debug menu, select the Start Without Debugging (Ctrl + F5) option to run the project.

    以下截图显示空的首页页面:

Figure 9.9 – Empty Home page

图 9.9–空主页

我们增加了在表格中显示费用的功能。接下来,我们需要添加添加费用的能力。

添加费用编辑组件

我们需要添加一个组件,使我们能够添加和编辑费用。我们按如下方式进行:

  1. 返回 Visual Studio

  2. 打开ExpenseTracker.Client.Shared\NavMenu.razor页面。

  3. ul元素添加以下标记:

    <li class="nav-item px-3">
        <NavLink class="nav-link" href="expense">
            <span class="oi oi-home" 
                  aria-hidden="true"></span> 
            Add Expense
        </NavLink>
    </li>
    
  4. 右键单击ExpenseTracker.Client.Pages文件夹,从菜单中选择添加,剃刀组件选项。

  5. 命名新组件ExpenseEdit

  6. 点击添加按钮。

  7. Update the markup to the following:

    @page "/expense"
    @page "/expense/{id:int}"
    @using ExpenseTracker.Shared
    @inject HttpClient Http
    @inject NavigationManager Nav
    <h3>Add/Edit Expense</h3>
    @if (!ready)
    {
        <p><em>Loading...</em></p>
    }
    else
    {
        <EditForm Model="expense" 
                  OnValidSubmit="HandleValidSubmit">
    
        </EditForm>
        <div>@error</div>
    }
    

    如果部件准备好了,前面的代码显示EditForm

  8. 增加以下@code块:

    @code {
        [Parameter] public int id { get; set; }
        private bool ready;
        private string error;
        private Expense;
        private IList<ExpenseType> types;
    }
    
  9. Add the following OnInitializedAsync method to the @code block:

    protected override async Task OnInitializedAsync()
    {
        types = await Http.GetFromJsonAsync
            <IList<ExpenseType>>("ExpenseTypes");
        if (id == 0)
        {
            expense = new Expense();
        }
        else
        {
            expense = await Http.GetFromJsonAsync
                <Expense>($"Expenses/{id}");
        }
        ready = true;
    }
    

    前面的代码初始化了types对象和expense对象。一旦它们都被初始化,ready的值被设置为true

  10. Add the following HandleValidSubmit method to the @code block:

```cs
private async Task HandleValidSubmit()
{
    HttpResponseMessage response;
    if (expense.Id == 0)
    {
        response = await Http.PostAsJsonAsync
            ("Expenses", expense);
    }
    else
    {
        string requestUri = $"Expenses/{expense.Id}";
        response = await Http.PutAsJsonAsync
            (requestUri, expense);
    };
    if (response.IsSuccessStatusCode)
    {
        Nav.NavigateTo("/");
    }
    else
    {
        error = response.ReasonPhrase;
    };
}
```

之前的代码使用`PostAsJsonAsync`方法添加新费用,并使用`PutAsJsonAsync`方法更新现有费用。如果相关方法成功,用户返回到**主页**页面。否则,将显示一条错误消息。

我们已经完成了这个组件的代码,但是EditForm仍然是空的。我们需要给EditForm添加一些标记。

添加输入组件

我们需要向EditForm元素添加输入组件。我们按如下方式进行:

  1. 将以下标记添加到EditForm以输入Date :

    <div>
        <label>
            Date
            <InputDate @bind-Value="expense.Date" 
                       class="form-control" />
        </label>
    </div>
    
  2. 将以下标记添加到EditForm以输入Vendor :

    <div>
        <label class="d-block">
            Vendor
            <InputText @bind-Value="expense.Vendor" 
                       class="form-control" />
        </label>
    </div>
    
  3. 将以下标记添加到EditForm以输入Description :

    <div>
        <label class="d-block">
            Description
            <InputTextArea @bind-Value=
                             "expense.Description"
                           class="form-control" />
        </label>
    </div>
    
  4. 将以下标记添加到EditForm以输入ExpenseTypeId :

    <div>
        <label class="d-block">
            Type
            <InputSelect @bind-Value=
                           "expense.ExpenseTypeId"
                         class="form-control">
                <option value=""></option>
                @foreach (var item in types)
                {
                    <option value="@item.Id">
                        @item.Type
                    </option>
                }
            </InputSelect>
        </label>
    </div>
    
  5. 将以下标记添加到EditForm以输入Amount :

    <div>
        <label>
            Amount
            <InputNumber @bind-Value="expense.Amount" 
                         class="form-control" />
        </label>
    </div>
    
  6. 将以下标记添加到EditForm以输入Paid :

    <div>
        <label>
            Paid
            <InputCheckbox @bind-Value="expense.Paid" 
                           class="form-control" />
        </label>
    </div>
    
  7. Submit按钮添加以下标记:

    <div class="pt-2 pb-2">
        <button type="submit" 
                class="btn btn-primary mr-auto">
            Save
        </button>
    </div>
    
  8. 添加以下标记以添加验证摘要:

    <DataAnnotationsValidator />
    <ValidationSummary />
    
  9. 构建菜单中,选择构建解决方案选项。

  10. 返回浏览器。

  11. 使用 Ctrl + R 刷新浏览器。

  12. 从菜单中选择添加费用选项。

  13. Click the Save button.

以下截图显示了验证错误:

![Figure 9.10 – Data validation for the ExpenseEdit component ](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/blazor-webasm-exam/img/Figure_9.10_B16786.jpg)

图 9.10–费用编辑组件的数据验证
  1. 添加费用。
  2. 点击保存按钮。

我们已经完成了费用追踪项目。

总结

现在,您应该能够结合内置输入组件使用EditForm组件来输入数据。您还应该对内置的验证组件感到满意。

在本章中,我们介绍了内置的EditForm组件、各种输入组件和验证组件。之后,我们使用 Blazor WebAssembly App 项目模板创建了一个多项目解决方案。我们添加了几个类和应用编程接口控制器。接下来,我们通过更新数据库的连接字符串并使用实体框架迁移来配置 SQL Server。我们更新了主页页面以显示费用列表。最后,我们添加了一个新页面,其中包括一个EditForm组件和许多内置的输入组件,以便输入、验证和提交费用。

我们可以应用我们的新技能向任何 Blazor WebAssembly 应用添加数据输入、验证和提交。

下一步是开始构建自己的网络应用。要了解最新信息并了解更多关于 Blazor WebAssembly 的信息,请访问https://Blazor.net,并阅读位于https://devblogs.microsoft.com/aspnetASP.NET 博客

我们希望你喜欢这本书,并祝你一切顺利!

问题

以下问题供您考虑:

  1. 使用内置输入组件的优势是什么?
  2. 您希望在框架已经提供的内置输入组件列表中添加哪些额外的输入组件?

进一步阅读

以下资源提供了有关本章主题的更多信息:

posted @ 2025-10-22 10:25  绝不原创的飞龙  阅读(12)  评论(0)    收藏  举报