我们在前面的 EBookStore 的开发进程已经完成了面向用户的部分,接下来我们要开始开发面向管理者的部分,就是后台管理系统。后台管理部分,我们计划用 Blazor 技术来完成基本后台功能。Blazor 技术是在 ASP.NET Core
上运行的一个现在 Web 框架。Blazor 基于 HTML,CSS 和 C# 语言,帮助我们用可重用组件快速开发 Web App。这些组件既可以从客户端也可以在服务器运行。在这个部分开发,我们会用到 Blazor Server。
开始 Blazor Server
第一步是添加中间件和服务。
Listing 10-1 在 Program.cs 中添加 Blazor 服务
using EBooksStore.Models;
using EBooksStore.Models.Interfaces;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<StoreDbContext>(options => {
options.UseSqlServer(
builder.Configuration["ConnectionStrings:EBooksStoreConnection"]);
});
builder.Services.AddScoped<IStoreRepository, StoreRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
//添加 RazorPages 服务
builder.Services.AddRazorPages();
//Add session services
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options => {
options.Cookie.Name = ".EBooksStore.Session";
options.IdleTimeout = TimeSpan.FromHours(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
// 添加服务端Blazor
builder.Services.AddServerSideBlazor();
var app = builder.Build();
// enable session middleware
app.UseSession();
app.UseStaticFiles();
app.MapDefaultControllerRoute();
// RazorPages 路径映射
app.MapRazorPages();
//添加Blazor路径映射
app.MapBlazorHub();
//备用路径是 _Host
app.MapFallbackToPage("/admin/{*catchall}", "/Admin/_Host");
// Seeds.InitPopulate(app);
app.Run();
类似前面 MVC 代码文件夹结构,我们在项目文件夹下新建文件夹和子文件夹 Pages/Admin 。类似 MVC 视图结构公约,Blazor 也可以有一个 _Imports.razor,省得每个组件都要引用重复的项目。
Listing 10-2 在 Pages/Admin 文件夹新建 _Imports.razor 文件
@using Microsoft.AspNetCore.Components
@using Microsoft.EntityFrameworkCore
@using EBooksStore.Models
我们基本要用到的这几个框架里 Microsoft 引用,当然还有数据。
创建 Blazor 入口
Blazor 入口包括初始化页面,连接服务器的 JavaScript 和渲染 Blazor HTML 的 JavaScript 。根据我们在 Program.cs 设定的备用路径,我们需要在 Pages/Admin 里新建入口文件名必须是 _Host 。
Listing 10-3 在 Pages/Admin 文件夹里新建 _Host.cshtml
@page "/admin"
@namespace EBooksStore.Pages.Admin
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<!DOCTYPE html>
<html>
<head>
<title>逻辑的电子书店管理平台</title>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<base href="/" />
</head>
<body>
<component type="typeof(Routed)" render-mode="Server" />
<script src="/_framework/blazor.server.js"></script>
</body>
</html>
_Host 文件指示页面路径和命名空间,同时要载入 /_framework/blazor.server.js
。component
是我们要显示的内容,由服务端生成和渲染;我们要创建这个 component
指示的 Routed 组件。
Listing 10-4 在 Pages/Amdin 文件夹里新建 Routed.razor 文件
@using Microsoft.AspNetCore.Components.Routing
<Router AppAssembly="@typeof(Program).Assembly">
<Found>
<RouteView RouteData="@context" DefaultLayout="typeof(AdminLayout)"/>
</Found>
<NotFound>
<h1>404 - Not Found</h1>
</NotFound>
</Router>
@context 是 Blazor RouteData 的公约名字。同时 Blazor 要有它自己的布局 AdminLayout 。
Lsiting 10-5 在 Pages/Admin 文件夹里新建 AdminLayout.razor 文件
@inherits LayoutComponentBase
@using Microsoft.AspNetCore.Components.Routing
<div class="container">
<div class="row">
<div class="col-2">
<div class="d-grid gap-3 py-3">
<NavLink class="btn btn-primary" href="/admin/ebooks">电子书</NavLink>
<NavLink class="btn btn-primary" href="/admin/orders">订单</NavLink>
</div>
</div>
<div class="col-10">
@Body
</div>
</div>
</div>
这里我们用 Razor 组件内建的 NavLink 元素,点击这些链接跳转时不会发送新的请求,提高性能的同时也不会丢失应用的运行状态。
根据我们在 AdminLayout 里的设定,我们要创建 2 个组件 Ebooks 和 Orders 。
Listing 10-6 在 Pages/Admin 文件夹里新建 Ebooks.razor 文件
@page "/admin/ebooks"
@page "/admin"
<h2>电子书组件</h2>
Listing 10-7 在 Pages/Admin 文件夹里新建 Orders.razor 文件
@page "/admin/orders"
<h2>订单组件</h2>
两个文件都用 @page 指令指定 URL 路径。
现在我们运行下应用,浏览器输入 ‘http://localhost:5000/admin‘ ,这个请求会由 _Host Razor 页面处理,包含的 Blazor JavaScript 代码将打开一个到 ASP.NET Core
服务器的连接,由服务器渲染 Blazor 内容,显示在页面上。
Figure 10-1
点击菜单链接,右边栏中的文件将会改名,表明各个组件已经成功显示。注意这里显示不同页面,不需要发送新的 Http 请求。
电子书 CRUD UI
CRUD 是英文 Create Read Update 和 Delete 四个英文单词的开头字母,指的是实现可持久性存储的应用程序必须的四种功能。数据库就是一种典型可持久性存储,所以几乎所有的 Web 应用程序都要实现它们。虽然,可能会有很多现成的工具帮你把这些功能搭好也就能用,但是具体的应用如何优化和细化这些功能才是重点。基本上,我们还真的不得不从头开始自己造船,才能航行得更远更快。
我们要扩展前面的 StoreRepository ,让它不只可以 Read ,还能 Create ,Update , Delete 。
Listing 10-7 在 Models/Interfaces 文件夹的接口类 IStoreRepository 里添加相关方法
namespace EBooksStore.Models.Interfaces;
public interface IStoreRepository
{
IQueryable<EBook> EBooks { get; }
bool CreateEBook(EBook eBook);
bool SaveEBook(EBook eBook);
bool DeleteEBook(EBook eBook);
}
Listing 10-8 在 Models 文件夹的 StoreRepository 里实现接口的方法
using EBooksStore.Models.Interfaces;
namespace EBooksStore.Models;
public class StoreRepository(StoreDbContext context) : IStoreRepository
{
private readonly StoreDbContext _context = context;
public IQueryable<EBook> EBooks => _context.EBooks;
public bool CreateEBook(EBook eBook)
{
try
{
_context.EBooks.Add(eBook);
_context.SaveChanges();
return true;
}
catch
{
return false;
}
}
public bool SaveEBook(EBook eBook)
{
try
{
_context.EBooks.Update(eBook);
_context.SaveChanges();
return true;
}
catch
{
return false;
}
}
public bool DeleteEBook(EBook eBook)
{
try
{
_context.EBooks.Remove(eBook);
_context.SaveChanges();
return true;
}
catch
{
return false;
}
}
}
请注意,我们这里用来最简化的 try catch 语句,初步提高对数据库的操作的可靠性。
给 EBook 数据类添加属性限制
即使是后台管理员,也要对他的输入数据进行验证,因为他也会出错哦。
Listing 10-9 修改 Models 文件夹里的 EBooks.cs 文件,以添加限制
using System.ComponentModel.DataAnnotations;
namespace EBooksStore.Models;
public class EBook
{
public int Id { get; set; }
[Required(ErrorMessage = "请输入书名")]
public string Title { get; set; } = string.Empty;
[Required(ErrorMessage = "请输入作者")]
public string Author { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
[Required(ErrorMessage = "请输入类别")]
public string Category { get; set; } = string.Empty;
[Range(0.01, double.MaxValue, ErrorMessage = "请输入正确的价格")]
[Required(ErrorMessage = "请输入价格")]
public decimal Price { get; set; }
}
Blazor 和 MVC 有一样的验证处理方法,但是更加灵活。
创建电子书列表组件
我们接下来用 Razor Page 创建一个电子书列表组件。Razor Page 目前可以暂时理解为 MVC 里的 View + Controller 的结合体。我们要利用依赖注入的 StoreRepository 服务来取得电子书列表,并显示在 Ebooks Blazor 组件里。
Listing 10-10 修改 Pages/Admin 文件夹里的 Ebooks.razor 文件
@page "/admin/ebooks"
@page "/admin"
@inherits OwningComponentBase<IStoreRepository>
@using Microsoft.AspNetCore.Components.Routing
<table class="table table-striped">
<thead>
<tr>
<th>书名</th>
<th>作者</th>
<th>类别</th>
<th>价格</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@if (EBooks.Count() == 0)
{
<tr>
<td colspan="5" class="text-center">暂无电子书</td>
</tr>
}
else
{
@foreach (var ebook in EBooks)
{
<tr>
<td>@ebook.Title</td>
<td>@ebook.Author</td>
<td>@ebook.Category</td>
<td>@ebook.Price</td>
<td>
<NavLink href="@GetDetailsUrl(ebook.Id)" class="btn btn-sm btn-primary">详情</NavLink>
<NavLink href="@GetEditUrl(ebook.Id)" class="btn btn-sm btn-warning">编辑</NavLink>
</td>
</tr>
}
}
</tbody>
</table>
<NavLink class="btn btn-primary" href="/admin/ebooks/create">添加电子书</NavLink>
@code {
public IStoreRepository StoreRepository => Service;
public IEnumerable<EBook> EBooks { get; set; } = [];
protected async override Task OnInitializedAsync()
{
EBooks = await StoreRepository.EBooks.ToListAsync();
}
public string GetDetailsUrl(int id) => $"/admin/ebooks/details/{id}";
public string GetEditUrl(int id) => $"/admin/ebooks/edit/{id}";
}
Blazor 组件用 OwningComponentBase<> 把位于依赖注入容器里的 IStoreRepository 注册为服务属性 Service ,所以我们可以用 Service 访问 IStoreRepository 接口里的方法。当然,你也可以直接用 @inject IStoreRepository StoreRepository
注入服务,但直接注入封装性就差了。在 OnInitializedAsync() 初始化方法里我们取得 EBooks 列表填充表格。
Figure 10-2
创建 详情 组件
我们新建 Details.razor 来显示电子书详情。
Listing 10-11 在 Pages/Admin 文件夹里新建 Details.razor 文件
@page "/admin/ebooks/details/{id:int}"
@using Microsoft.AspNetCore.Components.Routing
@inherits OwningComponentBase<IStoreRepository>
<h2>电子书详情</h2>
<table class="table table-striped">
<thead>
<tr>
<th>书名</th>
<th>作者</th>
<th>类别</th>
<th>价格</th>
</tr>
</thead>
<tbody>
<tr>
<td>@EBook?.Title</td>
<td>@EBook?.Author</td>
<td>@EBook?.Category</td>
<td>@EBook?.Price</td>
</tr>
</tbody>
</table>
<NavLink class="btn btn-primary" href="/admin/ebooks">返回</NavLink>
@code {
public IStoreRepository StoreRepository => Service;
[Parameter]
public int Id { get; set; }
public EBook? EBook { get; set; }
protected override void OnParametersSet()
{
EBook = StoreRepository.EBooks.FirstOrDefault(e => e.Id == Id) ?? new();
}
}
这里重载了 OnParametersSet() 方法,初始化后取得对应 Id 的电子书。
Figure 10-3
接着我们需要完成编辑组件。
创建 编辑 组件
编辑和创建数据一般都是用同一个组件来完成。
Listing 10-12 在 Pages/Admin 里新建 Editor.razor 文件
@page "/admin/ebooks/edit/{id:int}"
@page "/admin/ebooks/create"
@inherits OwningComponentBase<IStoreRepository>
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Forms
@inject NavigationManager NavigationManager
<style>
div.validation-message { color: red; }
</style>
<h2>@(ActionText)电子书</h2>
<EditForm Model="EBook" OnValidSubmit="SaveEBook">
<DataAnnotationsValidator />
@if(Id == 0)
{
<label>书名</label>
<InputText @bind-Value="EBook.Title" class="form-control" placeholder="书名" />
<label>作者</label>
<InputText @bind-Value="EBook.Author" class="form-control" placeholder="作者" />
<label>类别</label>
<InputText @bind-Value="EBook.Category" class="form-control" placeholder="类别" />
<label>价格</label>
<InputNumber @bind-Value="EBook.Price" class="form-control" placeholder="价格" />
<label>描述</label>
<InputTextArea @bind-Value="EBook.Description" class="form-control" placeholder="描述" />
}
else
{
<input type="hidden" @bind="EBook.Id" />
<label>书名</label>
<InputText @bind-Value="EBook.Title" class="form-control" placeholder="书名" disabled />
<label>作者</label>
<InputText @bind-Value="EBook.Author" class="form-control" placeholder="作者" disabled/>
<label>类别</label>
<InputText @bind-Value="EBook.Category" class="form-control" placeholder="类别"/>
<label>价格</label>
<InputNumber @bind-Value="EBook.Price" class="form-control" placeholder="价格" />
<label>描述</label>
<InputTextArea @bind-Value="EBook.Description" class="form-control" placeholder="描述" />
}
<ValidationMessage For="@(() => EBook.Title)" />
<ValidationMessage For="@(() => EBook.Author)" />
<ValidationMessage For="@(() => EBook.Category)" />
<ValidationMessage For="@(() => EBook.Price)" />
<div class="my-2">
<button type="submit" class="btn btn-primary">@ActionText</button>
<NavLink class="btn btn-secondary" href="/admin/ebooks">取消</NavLink>
</div>
</EditForm>
@code
{
public IStoreRepository StoreRepository => Service;
[Parameter]
public int Id { get; set; }
public EBook EBook { get; set; } = new();
public string ActionText => Id == 0 ? "添加" : "编辑";
protected override void OnParametersSet()
{
if (Id != 0)
{
EBook = StoreRepository.EBooks.FirstOrDefault(e => e.Id == Id) ?? new();
}
}
public void SaveEBook()
{
bool success = false;
if (Id == 0)
{
success = StoreRepository.CreateEBook(EBook);
}
else
{
success = StoreRepository.SaveEBook(EBook);
}
if (success)
{
NavigationManager.NavigateTo("/admin/ebooks");
}
else
{
throw new InvalidOperationException("保存失败");
}
}
}
除了像前面一样要依赖注入 IStoreRepository 外,我们还要显示注入 NavigationManager ,用来在方法里面实现跳转页面。
为了逻辑清晰,我们特意用重复代码划分创建和编辑两个部分。
当准备数据修改数据库时,数据验证是必须的,我们用红色字体标识验证错误。用 EditForm 里的 OnValidSubmit 属性来指定在 SaveEBook 方法里首先执行验证。
在列表页面点击 ‘添加电子书’ 后,出现添加页面,输入正确数据并确定添加后,新电子书会出现在列表的最后一项里。
Figure 10-4
在编辑组件里,我们让书名和作者失效,符合平常我们编辑的意图。什么都可以改,那还叫编辑?
Figure 10-5
删除电子书
CRUD 最后的功能也是最需要谨慎的功能就是删除,但是这个功能实现起来又是特别简单。世界上的事情都是这样,越容易的事,越‘难’做!
Listing 10-13 在 Pages/Admin 文件夹里修改 Ebooks.razor ,添加删除操作
@page "/admin/ebooks"
@page "/admin"
@inherits OwningComponentBase<IStoreRepository>
@using Microsoft.AspNetCore.Components.Routing
@* 为了处理鼠标点击事件 *@
@using Microsoft.AspNetCore.Components.Web
<table class="table table-striped">
<thead>
<tr>
<th>书名</th>
<th>作者</th>
<th>类别</th>
<th>价格</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@if (EBooks.Count() == 0)
{
<tr>
<td colspan="5" class="text-center">暂无电子书</td>
</tr>
}
else
{
@foreach (var ebook in EBooks)
{
<tr>
<td>@ebook.Title</td>
<td>@ebook.Author</td>
<td>@ebook.Category</td>
<td>@ebook.Price</td>
<td>
<NavLink href="@GetDetailsUrl(ebook.Id)" class="btn btn-sm btn-primary">详情</NavLink>
<NavLink href="@GetEditUrl(ebook.Id)" class="btn btn-sm btn-warning">编辑</NavLink>
@* 添加删除按钮 *@
<button class="btn btn-sm btn-danger"
@onclick="@(async (MouseEventArgs e) => await DeleteEBook(ebook))">删除</button>
</td>
</tr>
}
}
</tbody>
</table>
<NavLink class="btn btn-primary" href="/admin/ebooks/create">添加电子书</NavLink>
@code {
public IStoreRepository StoreRepository => Service;
public IEnumerable<EBook> EBooks { get; set; } = [];
protected async override Task OnInitializedAsync()
{
EBooks = await StoreRepository.EBooks.ToListAsync();
}
public async Task DeleteEBook(EBook ebook)
{
if (StoreRepository.DeleteEBook(ebook))
{
EBooks = await StoreRepository.EBooks.ToListAsync();
StateHasChanged(); //确保界面更新
}
else
{
throw new InvalidOperationException("删除电子书失败");
}
}
public string GetDetailsUrl(int id) => $"/admin/ebooks/details/{id}";
public string GetEditUrl(int id) => $"/admin/ebooks/edit/{id}";
}
对电子书的 CRUD 大框架已经完成,还是老话,如果是生产版,那么还要细化。
处理订单
后台管理订单的工作比商品的管理的业务逻辑复杂很多,和实际的商业逻辑关系密切。比如,订单是否运出,是否取消,客户是否修改地址 …… ,是后台管理的最大的模块。我们这里试着完成最基本的查看订单和是否发货功能。
我们先给数据模型添加发货标记。
Listing 10-14 修改 Models 文件夹里的 Orders.cs 文件,添加 Shipped 标记
...
public IEnumerable<OrderItem>? OrderItems { get; set; }
public bool Shipped { get; set; }
...
接着我们添加迁移,更新数据库。Entity Framework Core 可以让数据迁移变得比较容易,适合像我这样对数据库不熟悉的人。因为我注释掉了 Seed 数据的语句,所以我要手动更新数据库。
Listing 10-15 在项目文件里的终端上做数据迁移
dotnet ef migrations add ShippedOrder
dotnet ef database update
显示订单
我们要修改订单组件,显示订单列表。
Listing 10-16 修改 Pages/Admin 文件夹里新建 Orders.razor 文件
@page "/admin/orders"
@inherits OwningComponentBase<IOrderRepository>
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>订单号</th>
<th>收货人</th>
<th>城市</th>
<th>国家</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@foreach (var order in orders)
{
<tr>
<td>@order.Id</td>
<td>@order.Name</td>
<td>@order.City</td>
<td>@order.Country</td>
<td>
<NavLink href="@GetDetailsUrl(order.Id)" class="btn btn-sm btn-primary">详情</NavLink>
@if(!order.Shipped)
{
<button class="btn btn-sm btn-warning ms-2"
@onclick="@(async (e) => await ShipOrder(order))">发货</button>
}
else
{
<button class="btn btn-sm btn-secondary ms-2" disabled>已发货</button>
}
</td>
</tr>
}
</tbody>
</table>
@code {
public IOrderRepository OrderRepository => Service;
public IEnumerable<Order> orders { get; set; } = [];
protected async override Task OnInitializedAsync()
{
await UpdateOrders();
}
private async Task UpdateOrders()
{
orders = await OrderRepository.Orders
.OrderBy( o => o.Shipped)
.ThenByDescending( o => o.Id)
.ToListAsync();
}
public async Task ShipOrder(Order order)
{
order.Shipped = true;
OrderRepository.SaveOrder(order);
await UpdateOrders();
}
public string GetDetailsUrl(int orderId) => $"/admin/orders/details/{orderId}";
}
我们让订单先按是否发货排序,接着在按订单号降序排序。运行后,点击左边 ‘订单’ 菜单,显示结果类似下图。
Figure 10-6
订单详情
为了获得订单详情,我们先在 IOrderRepository 服务里增加一个方法 OrderItems() 。
Listing 10-17 修改 Models/Interfaces 文件夹里的 IOrderRepository.cs ,增加2个方法
namespace EBooksStore.Models.Interfaces;
public interface IOrderRepository
{
IQueryable<Order> Orders { get; }
void SaveOrder(Order order);
void SaveOrder(Order order, List<OrderItem> orderItems);
List<OrderItem> OrderItems(int orderId);
}
Listing 10-18 修改 Models 文件夹里的 OrderRepository.cs ,实现这2个方法
using EBooksStore.Models.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace EBooksStore.Models;
public class OrderRepository(StoreDbContext context) : IOrderRepository
{
private readonly StoreDbContext _context = context;
public IQueryable<Order> Orders => _context.Orders;
public void SaveOrder(Order order)
{
if (order.Id == 0)
{
_context.Orders.Add(order);
}
_context.SaveChanges();
}
public void SaveOrder(Order order, List<OrderItem> orderItems)
{
if (order.Id == 0)
{
_context.Orders.Add(order);
}
foreach (var item in orderItems)
{
item.OrderId = order.Id;
_context.OrderItems.Add(item);
}
_context.SaveChanges();
}
public List<OrderItem> OrderItems(int orderId)
{
return [.. _context.OrderItems
.Include(oi => oi.EBook)
.Where(oi => oi.OrderId == orderId)];
}
}
接下来,我们添加一个 OrderDetails.razor 组件用来显示订单详情。订单详情除了显示送货信息,还要显示订购项列表。
Listing 10-19 在 Pages/Admin 文件夹里新建一个 OrderDetails.razor 文件
@page "/admin/orders/details/{id:int}"
@using Microsoft.AspNetCore.Components.Routing
@inherits OwningComponentBase<IOrderRepository>
<style>
.order-info label{
font-weight: bold;
}
</style>
<h2>订单详情</h2>
<div class="row order-info">
<div class="col-sm-6">
<label>收货人</label>
<p>@TheOrder?.Order.Name</p>
</div>
<div class="col-sm-6">
<label>地址</label>
<p>@TheOrder?.Order.Address</p>
</div>
<div class="col-sm-6">
<label>城市</label>
<p>@TheOrder?.Order.City</p>
</div>
<div class="col-sm-6">
<label>国家</label>
<p>@TheOrder?.Order.Country</p>
</div>
<div class="col-sm-6">
<label>已发货</label>
<p>@((TheOrder?.Order.Shipped ?? false) ? "是" : "否")</p>
</div>
<div class="col-sm-6">
<label>订单号</label>
<p>@TheOrder?.Order.Id</p>
</div>
<div class="col-sm-6">
<label>总数量</label>
<p>@TheOrder?.Quantity</p>
</div>
<div class="col-sm-6">
<label>总价</label>
<p>@TheOrder?.TotalPrice</p>
</div>
</div>
<hr />
<h3>订单项</h3>
<table class="table table-striped">
<thead>
<tr>
<th>书名</th>
<th>作者</th>
<th>价格</th>
<th>数量</th>
<th>小计</th>
</tr>
</thead>
<tbody>
@if(TheOrder?.Items == null || TheOrder.Items.Count() == 0)
{
<tr>
<td colspan="5" class="text-center">暂无订单项</td>
</tr>
}
else
{
@foreach (var item in TheOrder.Items)
{
if(item.EBook == null)
{
continue;
}
<tr>
<td>@item.EBook.Title</td>
<td>@item.EBook.Author</td>
<td>@item.EBook.Price</td>
<td>@item.Quantity</td>
<td>@(item.EBook.Price * item.Quantity)</td>
</tr>
}
}
</tbody>
</table>
<NavLink class="btn btn-primary" href="/admin/orders">返回</NavLink>
@code {
IOrderRepository OrderRepository => Service;
[Parameter]
public int Id { get; set; }
public OrderWithItems TheOrder { get; set; } = new();
protected override void OnParametersSet()
{
TheOrder = GetOrderWithItems(Id);
}
private OrderWithItems GetOrderWithItems(int id)
{
var order = OrderRepository.Orders.FirstOrDefault(o => o.Id == id);
if (order == null)
{
return new OrderWithItems();
}
var items = OrderRepository.OrderItems(order.Id);
return new OrderWithItems
{
Order = order,
Items = items
};
}
public class OrderWithItems
{
public Order Order { get; set; } = new();
public IEnumerable<OrderItem> Items { get; set; } = Array.Empty<OrderItem>();
public int Quantity => Items.Sum(i => i.Quantity);
public decimal TotalPrice =>
Items.Sum(i => (i.EBook == null ? 0 : i.EBook.Price) * i.Quantity);
}
}
和前面的操作类似,我们要用依赖注入 OrderRepository 服务。
同时,为了显示订单项,我们构建一个 DTO 类 OrderWithItems 来存取单个订单的所有信息,作为 Razor 页的数据模型。
运行应用,我们打开订单详情,可以看到类似下图所示:
Figure 10-7
我们在这个部分用服务端的 Blazor 实现了数据库的 CRUD 和订单的发货状态修改。后台管理首要原则是逻辑清晰,UI简单,数据完备,更新实时,而服务端 Blazor 基本具备了这些需求。总体来说 ,Blazor 开发几乎没有用到其他语言的复杂知识,自动化程度很高,很适合做后台管理开发之类的小型应用。
发表回复