六、使用应用状态构建购物车

有时,我们需要我们的应用来维护不同页面之间的状态。我们可以通过使用依赖注入 ( 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. 添加以下标记:

    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 项目。

添加产品类别

我们需要添加待售的产品。我们按如下方式进行:

  1. 右键单击ShoppingCart项目,从菜单中选择添加,新文件夹选项。
  2. 命名新文件夹Models
  3. 右键单击Models文件夹,从菜单中选择添加,类别选项。
  4. 命名新类Product
  5. 点击添加按钮。
  6. 将以下属性添加到Product类中:

    cs 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文件。

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

  16. 命名新文件夹images
  17. 将以下图像从 GitHub 存储库中复制到images文件夹:Charger.jpgEarbuds.jpgKeyChain.jpgTravelMug.jpgTshirt.jpg.

我们已经在网络应用中添加了一系列产品。接下来,我们需要添加一个商店。

添加商店页面

要添加商店,我们需要在我们的网络应用中添加一个Store组件。我们按如下方式进行:

  1. 打开Shared\NavMenu.razor页面。
  2. 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>

    上述标记为存储页面添加了一个菜单选项。

  3. 右键单击Pages文件夹,从菜单中选择添加,剃刀组件选项。

  4. 命名新组件Store
  5. 点击添加按钮。
  6. 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; }

    前面的代码增加了一些指令和一些属性。

  7. 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>

    前面的标记添加了一个显示所有待售产品的表格。

  8. 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>

    前面的标记显示了我们列表中的所有项目。

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

    cs 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; }

    前面的代码将指定的产品添加到购物车中,并按产品价格递增总数。

  11. 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. 输入以下代码:

    cs 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:

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

    CartService类继承自ICartService接口。

  5. 增加以下using语句:

    cs using ShoppingCart.Models;

  6. 添加以下构造函数:

    cs public CartService() { Cart = new List<Product>(); }

  7. Add the NotifyStateChanged method to the class:

    cs private void NotifyStateChanged() => OnChange?.Invoke();

    在前面的代码中,调用NotifyStateChanged方法时会调用OnChange事件。

  8. Add the AddProduct method to the class:

    cs public void AddProduct(Product product) {     Cart.Add(product);     Total += product.Price;     NotifyStateChanged(); }

    前面的代码将指示的产品添加到产品列表中,并增加总数。它还调用NotifyStateChanged方法。

  9. 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

我们需要在去离子容器中注册,然后才能将其注入我们的商店页面。我们按如下方式进行:

  1. 打开Program.cs文件。
  2. 在注册HttpClient的代码后添加以下代码:

    cs builder.Services.AddScoped<ICartService, CartService>();

  3. 增加以下using语句:

    cs using ShoppingCart.Services;

我们已经注册CartService。现在我们需要更新商店页面来使用它。

注射卡丁车服务

我们需要更新商店页面。我们按如下方式进行:

  1. 打开Pages\Store.razor页面。
  2. 增加以下@using指令:

    cs @using ShoppingCart.Services

  3. 增加以下@inject指令:

    cs @inject ICartService cartService

  4. Update the Add to Cart button to the following:

    cs <button class="btn btn-primary"         @onclick="@(() =>           cartService.AddProduct(item))">     Add to Cart </button>

    前面的标记使用cartService将产品添加到购物车中。

  5. 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遍历购物车中的产品,并使用从购物车中删除产品。

  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指令:

    cs @using ShoppingCart.Services

  4. 增加以下@inject指令:

    cs @inject ICartService cartService

  5. 将以下标记添加到top-row div :

    cs <h3>Cart Total: $@cartService.Total</h3>

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

  7. 返回浏览器。
  8. 使用 Ctrl + R 刷新浏览器。
  9. 向购物车中添加一些物品。
  10. 确认页面顶部的购物车合计字段没有更新。

当我们向购物车中添加新商品时,页面顶部的购物车总数不会更新。我们需要处理这个。

使用 OnChange 方法

我们需要通知组件什么时候需要更新。我们按如下方式进行:

  1. 返回 Visual Studio
  2. 打开Shared\MainLayout.razor页面。
  3. 增加以下@implements指令:

    cs @implements IDisposable

  4. 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方法中取消了订阅。

  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方法?

进一步阅读

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