练习 - 使用 @page 指令更改 Blazor 应用中的导航
Blazor 具有导航状态帮助程序,可帮助 C# 代码管理应用的 URI。 还有一个恰好用于替换 <a>
元素的 NavLink 组件。 NavLink 的一项功能是向应用菜单的 HTML 链接添加活动类。
你的团队初步创建了 Blazing Pizza 应用,并构建了 Blazor 组件来表示披萨和订单。 现在需要在该应用中添加结帐和其他与订单相关的页面。
在此练习中,你将添加新的结帐页,为应用添加顶部导航,然后使用 Blazor NavLink 组件改进代码。
克隆团队的现有应用
备注
本模块使用 .NET 命令行接口 (CLI) 和 Visual Studio Code 进行本地开发。 完成本模块后,你可以使用 Visual Studio (Windows) 或 Visual Studio for Mac (macOS) 来应用概念。 使用适用于 Windows、Linux 和 macOS 的 Visual Studio Code 继续开发。
此模块使用 .NET 6.0 SDK。 通过在首选终端中运行以下命令,确保你已安装 .NET 6.0:
dotnet --list-sdks
将显示类似于下面的输出:
3.1.100 [C:\program files\dotnet\sdk]
5.0.100 [C:\program files\dotnet\sdk]
6.0.100 [C:\program files\dotnet\sdk]
确保列出了以 6
开头的版本。 如果未列出任何版本或未找到命令,请安装最新的 .NET 6.0 SDK。
如果之前尚未创建 Blazor 应用,请按照 Blazor 设置说明安装正确版本的 .NET,并检查计算机是否设置正确。 停止“创建应用”步骤。
打开 Visual Studio Code。
选择“视图”,以便从 Visual Studio Code 中打开集成终端。 然后在主菜单上选择“终端”。
在终端中,转到要创建项目的位置。
从 GitHub 克隆应用。
git clone https://github.com/MicrosoftDocs/mslearn-blazor-navigation.git BlazingPizza
选择“文件”,然后选择“打开文件夹”。
在“打开”对话框中,转到 BlazingPizza 文件夹,然后选择“选择文件夹”。
Visual Studio Code 可能会提示你关于未解析的依赖项。 选择“还原”。
运行应用以检查一切是否正常。
在 Visual Studio Code 中,选择 F5。 在“运行”菜单上,选择“开始调试”。
对一些披萨进行搭配并将其添加到订单中。 在页面底部选择“订购”。 你会看到默认的“404 未找到”消息,因为团队尚未创建结帐页。
选择 Shift + F5 以停止应用。
添加结帐页
在 Visual Studio Code 中的文件资源管理器中,选择“App.razor”。
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Found Context="routeData"> <RouteView RouteData="@routeData" /> </Found> <NotFound> <LayoutView> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router>
客户尝试转到不存在的页面时,会看到
<NotFound>
代码块。在文件资源管理器中,展开“页面”,右键单击文件夹,然后选择“新建文件”。
将新文件命名为 Checkout.razor。 在此文件中,编写以下代码:
@page "/checkout" @inject OrderState OrderState @inject HttpClient HttpClient @inject NavigationManager NavigationManager <div class="top-bar"> <a class="logo" href=""> <img src="img/logo.svg" /> </a> <a href="" class="nav-tab"> <img src="img/pizza-slice.svg" /> <div>Get Pizza</div> </a> </div> <div class="main"> <div class="checkout-cols"> <div class="checkout-order-details"> <h4>Review order</h4> @foreach (var pizza in Order.Pizzas) { <p> <strong> @(pizza.Size)" @pizza.Special.Name (£@pizza.GetFormattedTotalPrice()) </strong> </p> } <p> <strong> Total price: £@Order.GetFormattedTotalPrice() </strong> </p> </div> </div> <button class="checkout-button btn btn-warning"> Place order </button> </div> @code { Order Order => OrderState.Order; }
此页面基于当前应用生成,并使用
OrderState
中保存的应用状态。 第一个div
是应用的新标头导航。 让我们将此添加到索引页。在文件资源管理器中,展开“页面”,然后选择“index.razor”。
在
<div class="main">
类的上方,添加top-bar
html。<div class="top-bar"> <a class="logo" href=""> <img src="img/logo.svg" /> </a> <a href="" class="nav-tab" > <img src="img/pizza-slice.svg" /> <div>Get Pizza</div> </a> </div>
位于此页上时,通过突出显示链接来告知客户是很好的做法。 团队已经创建了一个
active
css 类,因此将active
添加到已包含nav-tab
样式的class
属性中。<div class="top-bar"> <a class="logo" href=""> <img src="img/logo.svg" /> </a> <a href="" class="nav-tab active" > <img src="img/pizza-slice.svg" /> <div>Get Pizza</div> </a> </div>
在 Visual Studio Code 中,选择 F5。 在“运行”菜单上,选择“开始调试”。
该应用顶部现在有了美观的菜单栏,其中包括公司的徽标。 添加一些披萨,将订单推进到结帐页。 你将看到其中列出了披萨,并且活动指示器从菜单中消失。
选择 Shift + F5 以停止应用。
允许客户下单
此时的结帐页不允许客户下单。 应用的逻辑需要存储要发送到厨房的订单。 发送订单后,让我们将客户重定向回主页。
在文件资源管理器中,展开“页面”,然后选择“Checkout.razor”。
修改按钮元素以调用
PlaceOrder
方法。 添加@onclick
和disabled
属性,如下所示:<button class="checkout-button btn btn-warning" @onclick="PlaceOrder" disabled=@isSubmitting> Place order </button>
我们不希望客户下重复的订单,因此你将禁用“下单”按钮,直到订单处理完毕。
在
@code
块中,将此代码添加到代码Order Order => OrderState.Order;
下。bool isSubmitting; async Task PlaceOrder() { isSubmitting = true; var response = await HttpClient.PostAsJsonAsync(NavigationManager.BaseUri + "orders", OrderState.Order); var newOrderId= await response.Content.ReadFromJsonAsync<int>(); OrderState.ResetOrder(); NavigationManager.NavigateTo("/"); }
上述代码将禁用“下单”按钮,发布将添加到 pizza.db 的 JSON,完成订单,并使用
NavigationManager
将客户重定向到主页。你需要添加处理订单的代码。 你将为此任务添加 OrderController 类。 如果查看 PizzaStoreContext.cs,你将看到只有
PizzaSpecials
具有实体框架数据库支持。 让我们首先解决此问题。
为订单和披萨添加实体框架支持
在文件资源管理器中,选择“PizzaStoreContext.cs”。
将
PizzaStoreContext
类替换为此代码:public class PizzaStoreContext : DbContext { public PizzaStoreContext( DbContextOptions options) : base(options) { } public DbSet<Order> Orders { get; set; } public DbSet<Pizza> Pizzas { get; set; } public DbSet<PizzaSpecial> Specials { get; set; } public DbSet<Topping> Toppings { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Configuring a many-to-many special -> topping relationship that is friendly for serialization modelBuilder.Entity<PizzaTopping>().HasKey(pst => new { pst.PizzaId, pst.ToppingId }); modelBuilder.Entity<PizzaTopping>().HasOne<Pizza>().WithMany(ps => ps.Toppings); modelBuilder.Entity<PizzaTopping>().HasOne(pst => pst.Topping).WithMany(); } }
此代码将为应用的 order 和 pizza 类添加实体框架支持。
在 Visual Studio Code 的菜单上,选择“文件”>“新建文本文件”。
选择 C# 语言并输入此代码:
using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace BlazingPizza; [Route("orders")] [ApiController] public class OrdersController : Controller { private readonly PizzaStoreContext _db; public OrdersController(PizzaStoreContext db) { _db = db; } [HttpGet] public async Task<ActionResult<List<OrderWithStatus>>> GetOrders() { var orders = await _db.Orders .Include(o => o.Pizzas).ThenInclude(p => p.Special) .Include(o => o.Pizzas).ThenInclude(p => p.Toppings).ThenInclude(t => t.Topping) .OrderByDescending(o => o.CreatedTime) .ToListAsync(); return orders.Select(o => OrderWithStatus.FromOrder(o)).ToList(); } [HttpPost] public async Task<ActionResult<int>> PlaceOrder(Order order) { order.CreatedTime = DateTime.Now; // Enforce existence of Pizza.SpecialId and Topping.ToppingId // in the database - prevent the submitter from making up // new specials and toppings foreach (var pizza in order.Pizzas) { pizza.SpecialId = pizza.Special.Id; pizza.Special = null; } _db.Orders.Attach(order); await _db.SaveChangesAsync(); return order.OrderId; } }
上述代码允许应用获取所有当前订单并下订单。
[Route("orders")]
Blazor 属性允许此类处理对 /orders 和 /orders/{orderId} 的传入 HTTP 请求。使用 Ctrl+S 保存所做的更改。
对于文件名,请使用“OrderController.cs”。 请确保将文件保存在与 OrderState.cs 相同的目录中。
在文件资源管理器中,选择“OrderState.cs”。
在类的底部
RemoveConfiguredPizza
方法下方,修改ResetOrder()
以重置订单:public void ResetOrder() { Order = new Order(); }
测试结帐功能
在 Visual Studio Code 中,选择 F5。 在“运行”菜单上,选择“开始调试”。
应用应编译,但如果创建订单并尝试结帐,则会看到运行时错误。 发生该错误是因为创建 pizza.db SQLLite 数据库时尚不支持订单和披萨。 我们需要删除文件,以便可以正确创建新数据库。
选择 Shift + F5 以停止应用。
在文件资源管理器中,删除 pizza.db 文件。
按 F5 。 在“运行”菜单上,选择“开始调试”。
作为测试,请添加披萨,去结帐,然后下订单。 将重定向到主页,订单现在显示为空。
选择 Shift + F5 以停止应用。
应用在不断改进。 我们有了披萨配置和结帐。 我们希望在客户下达披萨订单后让其能够查看订单状态。
添加订单页
在文件资源管理器中,展开“页面”,右键单击文件夹,然后选择“新建文件”。
将新文件命名为 MyOrders.razor。 在此文件中,编写以下代码:
@page "/myorders" @inject HttpClient HttpClient @inject NavigationManager NavigationManager <div class="top-bar"> <a class="logo" href=""> <img src="img/logo.svg" /> </a> <a href="" class="nav-tab"> <img src="img/pizza-slice.svg" /> <div>Get Pizza</div> </a> <a href="myorders" class="nav-tab active"> <img src="img/bike.svg" /> <div>My Orders</div> </a> </div> <div class="main"> @if (ordersWithStatus == null) { <text>Loading...</text> } else if (!ordersWithStatus.Any()) { <h2>No orders placed</h2> <a class="btn btn-success" href="">Order some pizza</a> } else { <div class="list-group orders-list"> @foreach (var item in ordersWithStatus) { <div class="list-group-item"> <div class="col"> <h5>@item.Order.CreatedTime.ToLongDateString()</h5> Items: <strong>@item.Order.Pizzas.Count()</strong>; Total price: <strong>£@item.Order.GetFormattedTotalPrice()</strong> </div> <div class="col"> Status: <strong>@item.StatusText</strong> </div> @if (@item.StatusText != "Delivered") { <div class="col flex-grow-0"> <a href="myorders/" class="btn btn-success"> Track > </a> </div> } </div> } </div> } </div> @code { List<OrderWithStatus> ordersWithStatus = new List<OrderWithStatus>(); protected override async Task OnParametersSetAsync() { ordersWithStatus = await HttpClient.GetFromJsonAsync<List<OrderWithStatus>>( $"{NavigationManager.BaseUri}orders"); } }
在我们现在拥有的所有页面尚都需要更改导航,以包含指向新的“我的订单”页的链接。 打开“Checkout.razor”和“Index.razor”,将导航替换为以下代码:
<div class="top-bar"> <a class="logo" href=""> <img src="img/logo.svg" /> </a> <a href="" class="nav-tab active" > <img src="img/pizza-slice.svg" /> <div>Get Pizza</div> </a> <a href="myorders" class="nav-tab" > <img src="img/bike.svg" /> <div>My orders</div> </a> </div>
通过使用
<a>
元素,可以添加active
css 类来手动管理活动页。 让我们更新所有导航,改为使用 NavLink 组件。在所有三个带导航的页面(Index.razor、Checkout.razor 和 MyOrders.razor)上,使用相同的 Blazor 代码进行导航:
<div class="top-bar"> <a class="logo" href=""> <img src="img/logo.svg" /> </a> <NavLink href="" class="nav-tab" Match="NavLinkMatch.All"> <img src="img/pizza-slice.svg" /> <div>Get Pizza</div> </NavLink> <NavLink href="myorders" class="nav-tab"> <img src="img/bike.svg" /> <div>My Orders</div> </NavLink> </div>
active
css 类现在由 NavLink 组件自动添加到页面。 你不在有导航的每个页面上一一执行操作。最后一步是更改
NavigationManager
,以便在下订单后重定向到 myorders 页。 在文件资源管理器中,展开“页面”,然后选择“Checkout.razor”。通过将
/myorders
传递给NavigationManager.NavigateTo()
来更改PlaceOrder
方法以重定向到正确的页面:async Task PlaceOrder() { isSubmitting = true; var response = await HttpClient.PostAsJsonAsync($"{NavigationManager.BaseUri}orders", OrderState.Order); var newOrderId = await response.Content.ReadFromJsonAsync<int>(); OrderState.ResetOrder(); NavigationManager.NavigateTo("/myorders"); }
在 Visual Studio Code 中,选择 F5。 在“运行”菜单上,选择“开始调试”。
你应该能够订购一些披萨,然后查看数据库中现有的订单。
选择 Shift + F5 以停止应用。