六、使用应用状态构建购物车
有时,我们需要我们的应用来维护不同页面之间的状态。我们可以通过使用依赖注入 ( 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
依赖关系是在组件实例创建之后,但在执行OnInitialized
或OnInitializedAsync
生命周期事件之前注入的。这意味着您不能在组件的构造函数中使用注入类,但是您可以在OnInitialized
或OnInitializedAsync
方法中使用它。
使用寿命
使用 DI 注入的服务的寿命可以是以下任何值:
- 一个
- 审视
- 短暂的
一个
如果服务生存期被定义为Singleton
,这意味着将创建该类的单个实例,并且该实例将在整个应用中共享。使用该服务的任何组件都将收到同一服务的实例。
在 Blazor WebAssembly 应用中,对于在浏览器的当前选项卡中运行的当前应用的生存期来说,这是正确的。这是我们将在本章的项目中用来管理应用状态的服务生命周期。
审视
如果服务的服务生存期被定义为Scoped
,这意味着将为每个范围创建一个新的类实例。由于 Blazor WebAssembly 应用没有 DI 作用域的概念,这些服务被视为Singleton
服务。
在我们的项目模板中,我们使用Scoped
服务来创建我们用于数据访问的HttpClient
实例。这是因为微软的项目模板使用其服务的作用域服务生存期与服务器端 Blazor 对称。
短暂的
如果服务的服务生命周期被定义为Transient
,这意味着每次请求服务实例时都会创建一个新的类实例。当使用临时服务时,DI 容器只是作为一个工厂,创建类的唯一实例。一旦实例被创建并注入依赖组件,容器就不再对它感兴趣了。
我们可以使用 DI 将同一个服务实例注入到多个组件中。它由 AppState 模式使用,允许应用维护组件之间的状态。
现在,让我们快速了解一下我们将在本章中构建的项目。
项目概述
在本章中,我们将构建一个包含购物车的 Blazor WebAssembly 应用。我们将能够在购物车中添加和移除不同的产品。购物车的总数将显示在应用的每个页面上。
以下是完整应用的屏幕截图:
图 6.1-购物卡应用
这个项目的构建时间大约为 60 分钟。
创建购物车项目
将使用空 Blazor WebAssembly 应用项目模板创建ShoppingCart
项目。首先,我们将添加逻辑来添加和移除购物车中的产品。然后,我们将演示当我们在页面之间导航时,购物车的状态会丢失。为了维护购物车的状态,我们将在 DI 容器中注册一个使用 AppState 模式的服务。最后,我们将演示通过将新服务注入相关组件,购物车的状态不会丢失。
开始项目
我们需要创建一个新的 Blazor WebAssembly 应用。我们按如下方式进行:
- 打开 Visual Studio 2019 。
- 点击新建项目按钮。
-
In the Search for templates (Alt + S) textbox, enter
Blazor
and then hit the Enter key.以下截图显示了我们在 第二章 中创建的空 Blazor WebAssembly App 项目模板,构建您的第一个 Blazor WebAssembly 应用:
图 6.2–空 Blazor WebAssembly 应用项目模板
-
选择空 Blazor WebAssembly App 项目模板,然后点击下一步按钮。
-
Enter
ShoppingCart
in the Project name textbox and then click the Create button:图 6.3–配置新项目对话框
小费
在前面的例子中,我们将
ShoppingCart
项目放入E:/Blazor
文件夹中。然而,项目的位置并不重要。 -
打开
Pages\Index.razor
页面。 -
添加以下标记:
cs <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 项目。
添加产品类别
我们需要添加待售的产品。我们按如下方式进行:
- 右键单击
ShoppingCart
项目,从菜单中选择添加,新文件夹选项。 - 命名新文件夹
Models
。 - 右键单击
Models
文件夹,从菜单中选择添加,类别选项。 - 命名新类
Product
。 - 点击添加按钮。
-
将以下属性添加到
Product
类中:cs public int ProductId { get; set; } public string ProductName { get; set; } public int Price { get; set; } public string Image { get; set; }
-
右键单击
wwwroot
文件夹,从菜单中选择添加,新文件夹选项。 - 命名新文件夹
sample-data
。 - 右键单击
sample-data
文件夹,从菜单中选择添加,新项目选项。 - 在搜索框中输入
json
。 - 选择 JSON 文件。
- 命名文件
products.json
。 - 点击添加按钮。
-
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
文件。 -
右键单击
wwwroot
文件夹,从菜单中选择添加,新文件夹选项。 - 命名新文件夹
images
。 - 将以下图像从 GitHub 存储库中复制到
images
文件夹:Charger.jpg
、Earbuds.jpg
、KeyChain.jpg
、TravelMug.jpg
和Tshirt.jpg.
我们已经在网络应用中添加了一系列产品。接下来,我们需要添加一个商店。
添加商店页面
要添加商店,我们需要在我们的网络应用中添加一个Store
组件。我们按如下方式进行:
- 打开
Shared\NavMenu.razor
页面。 -
Add the following markup before the closing
ul
tag:cs <li class="nav-item px-3"> <NavLink class="nav-link" href="store"> <span class="oi oi-home" aria-hidden="true"> </span> Store </NavLink> </li>
上述标记为存储页面添加了一个菜单选项。
-
右键单击
Pages
文件夹,从菜单中选择添加,剃刀组件选项。 - 命名新组件
Store
。 - 点击添加按钮。
-
Replace the markup with the following:
cs @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; }
前面的代码增加了一些指令和一些属性。
-
Add the following markup in the
div
element:cs <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>
前面的标记添加了一个显示所有待售产品的表格。
-
Add the following markup below the preceding
div
element:cs <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>
前面的标记显示了我们列表中的所有项目。
-
Add the following code to the
@code
block:cs protected override async Task OnInitializedAsync() { products = await Http.GetFromJsonAsync<Product[]> ("sample-data/products.json"); }
前面的代码使用 HttpClient 从
products.json
文件中读取products
。 -
Add the
AddProduct
method to the@code
block:cs private void AddProduct(Product product) { cart.Add(product); total += product.Price; }
前面的代码将指定的产品添加到购物车中,并按产品价格递增总数。
-
Add the
DeleteProduct
method to the@code
block:cs private void DeleteProduct(Product product) { cart.Remove(product); total -= product.Price; }
前面的代码从购物车中删除指定的产品,并按产品价格递减总数。
我们在网络应用中添加了商店页面。现在我们需要测试它。
证明应用状态丢失
我们需要测试商店页面。我们按如下方式进行:
- 从调试菜单中,选择不调试启动(Ctrl+F5)option 运行项目。
- 选择导航菜单上的存储选项。
- 向购物车中添加一些物品。
- 选择导航菜单上的主页选项。
- 选择导航菜单上的商店选项,返回商店页面。
- 确认购物车现在是空的。
当我们在 web 应用的页面之间导航时,状态会丢失。我们可以使用 AppState 模式来维护状态。
创建 ICartService 接口
我们需要创建一个ICartService
界面。我们按如下方式进行:
- 返回 Visual Studio 。
- 右键单击
ShoppingCart
项目,从菜单中选择添加,新文件夹选项。 - 命名新文件夹
Services
。 - 右键单击
Services
文件夹,从菜单中选择添加,新项目选项。 - 在搜索框中输入
interface
。 - 选择界面。
- 命名文件
ICartService
。 - 点击添加按钮。
-
输入以下代码:
cs IList<Product> Cart{ get; } int Total { get; set; } event Action OnChange; void AddProduct(Product product); void DeleteProduct(Product product);
-
增加以下
using
语句:cs using ShoppingCart.Models;
我们已经创建了ICartService
界面。现在我们需要创建一个从它继承的类。
创建 CartService 类
我们需要创建CartService
类。我们按如下方式进行:
- 右键单击
Services
文件夹,从菜单中选择添加,类别选项。 - 命名类
CartService
。 - 点击添加按钮。
-
Update the class to the following:
cs public class CartService : ICartService { public IList<Product> Cart { get; private set; } public int Total { get; set; } public event Action OnChange; }
CartService
类继承自ICartService
接口。 -
增加以下
using
语句:cs using ShoppingCart.Models;
-
添加以下构造函数:
cs public CartService() { Cart = new List<Product>(); }
-
Add the
NotifyStateChanged
method to the class:cs private void NotifyStateChanged() => OnChange?.Invoke();
在前面的代码中,调用
NotifyStateChanged
方法时会调用OnChange
事件。 -
Add the
AddProduct
method to the class:cs public void AddProduct(Product product) { Cart.Add(product); Total += product.Price; NotifyStateChanged(); }
前面的代码将指示的产品添加到产品列表中,并增加总数。它还调用
NotifyStateChanged
方法。 -
Add the
DeleteProduct
method to the class:cs public void DeleteProduct(Product product) { Cart.Remove(product); Total -= product.Price; NotifyStateChanged(); }
上面的代码从产品列表中删除指定的产品,并减少总数。它还调用
NotifyStateChanged
方法。
我们已经完成了CartService
课。现在我们需要在 DI 容器中注册CartService
。
在 DI 容器中注册 CartService
我们需要在去离子容器中注册,然后才能将其注入我们的商店页面。我们按如下方式进行:
- 打开
Program.cs
文件。 -
在注册
HttpClient
的代码后添加以下代码:cs builder.Services.AddScoped<ICartService, CartService>();
-
增加以下
using
语句:cs using ShoppingCart.Services;
我们已经注册CartService
。现在我们需要更新商店页面来使用它。
注射卡丁车服务
我们需要更新商店页面。我们按如下方式进行:
- 打开
Pages\Store.razor
页面。 -
增加以下
@using
指令:cs @using ShoppingCart.Services
-
增加以下
@inject
指令:cs @inject ICartService cartService
-
Update the Add to Cart button to the following:
cs <button class="btn btn-primary" @onclick="@(() => cartService.AddProduct(item))"> Add to Cart </button>
前面的标记使用
cartService
将产品添加到购物车中。 -
Update the
cart
div
element to the following:cs @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
遍历购物车中的产品,并使用从购物车中删除产品。 -
从
@code
块中删除cart
属性、AddProduct
方法和DeleteProduct
方法。 - 从构建菜单中,选择构建解决方案选项。
- 返回浏览器。
- 使用 Ctrl + R 刷新浏览器。
- 向购物车中添加一些物品。
- 选择导航菜单上的主页选项。
- 选择导航菜单上的商店选项,返回商店页面。
- 确认购物车不是空的。
我们已经确认CartService
正在工作。现在我们需要将购物车总数添加到所有页面中。
将购物车总数添加到所有页面
要查看所有页面上的购物车总数,我们需要将购物车总数添加到所有页面上使用的组件中。由于所有页面都使用了MainLayout
组件,我们将向其中添加购物车总数。我们按如下方式进行:
- 返回 Visual Studio 。
- 打开
Shared\MainLayout.razor
页面。 -
增加以下
@using
指令:cs @using ShoppingCart.Services
-
增加以下
@inject
指令:cs @inject ICartService cartService
-
将以下标记添加到
top-row
div
:cs <h3>Cart Total: $@cartService.Total</h3>
-
从构建菜单中,选择构建解决方案选项。
- 返回浏览器。
- 使用 Ctrl + R 刷新浏览器。
- 向购物车中添加一些物品。
- 确认页面顶部的购物车合计字段没有更新。
当我们向购物车中添加新商品时,页面顶部的购物车总数不会更新。我们需要处理这个。
使用 OnChange 方法
我们需要通知组件什么时候需要更新。我们按如下方式进行:
- 返回 Visual Studio 。
- 打开
Shared\MainLayout.razor
页面。 -
增加以下
@implements
指令:cs @implements IDisposable
-
Add the following
@code
block:cs @code{ protected override void OnInitialized() { cartService.OnChange += StateHasChanged; } public void Dispose() { cartService.OnChange -= StateHasChanged; } }
在前面的代码中,组件的
StateHasChanged
方法订阅了OnInitialized
方法中的cartService.OnChange
方法,而在Dispose
方法中取消了订阅。 -
从构建菜单中,选择构建解决方案选项。
- 返回浏览器。
- 使用 Ctrl + R 刷新浏览器。
- 向购物车中添加一些物品。
- 确认页面顶部的购物车总计字段更新。
我们已经更新了组件,以便在调用CartService
的OnChange
方法时调用StateHasChanged
方法。
小费
处理组件时,不要忘记取消订阅事件。
您必须取消订阅该事件,以防止每次引发cartService.OnChange
事件时调用StateHasChanged
方法。否则,您的应用将会遇到资源泄漏。
总结
现在,您应该能够使用 DI 将应用状态模式应用到 Blazor WebAssembly 应用中。
在本章中,我们介绍了应用状态和 DI。之后,我们使用空 Blazor WebAssembly App 项目模板创建了一个新项目。我们向项目中添加了一个购物车,并演示了当我们在页面之间导航时,应用状态会丢失。为了维护应用的状态,我们在 DI 容器中注册了CartService
服务。最后,我们演示了通过使用 AppState 模式,我们可以维护购物车的状态。
我们可以用 DI 应用我们的新技能来维护任何 Blazor WebAssembly 应用的应用状态。
在下一章中,我们将使用事件构建看板板。
问题
以下问题供您考虑:
- 当页面重新加载时,本地存储可以用来维护购物车的状态吗?
- 为什么不需要在
Store
组件中调用StateHasChanged
方法?
进一步阅读
以下资源提供了有关本章所涵盖主题的更多信息:
- 关于 DI 的更多信息,请参考https://docs . Microsoft . com/en-us/aspnet/core/基本面/依赖注入?view=aspnetcore-5.0 。
- 有关事件的更多信息,请参考https://docs . Microsoft . com/en-us/dotnet/cs harp/programming-guide/events。
版权属于:月萌API www.moonapi.com,转载请注明出处