我们在前面的 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.jscomponent 是我们要显示的内容,由服务端生成和渲染;我们要创建这个 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

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

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

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

在编辑组件里,我们让书名和作者失效,符合平常我们编辑的意图。什么都可以改,那还叫编辑?

Figure-10-5

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

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

Figure 10-7

我们在这个部分用服务端的 Blazor 实现了数据库的 CRUD 和订单的发货状态修改。后台管理首要原则是逻辑清晰,UI简单,数据完备,更新实时,而服务端 Blazor 基本具备了这些需求。总体来说 ,Blazor 开发几乎没有用到其他语言的复杂知识,自动化程度很高,很适合做后台管理开发之类的小型应用。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注