ABP應用開發(Step by Step)-下篇

張飛洪[廈門]發表於2022-04-27

測試 ProductAppService 類

啟動模板附帶測試基礎架構,包括xUnitShouldlyNSubstitute庫。它使用SQLite 記憶體資料庫來模擬資料庫,併為每個測試建立一個單獨的資料庫。它會自動初始化資料並在測試結束時銷燬測試資料。通過這種方式,測試不會相互影響,並且您的真實資料庫保持不變。
下面展示在 UI 上使用應用服務之前,如何為ProductAppService類的GetListAsync方法寫單元測試程式碼(構建自動化測試細節後續再議)。
在.Application.Tests專案中建立Products資料夾,並在其中建立一個ProductAppService_Tests類:
using Shouldly;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Xunit;
namespace ProductManagement.Products
{
    public class ProductAppService_Tests : ProductManagementApplicationTestBase
    {
        private readonly IProductAppService _productAppService;
        public ProductAppService_Tests()
        {
            _productAppService =
                GetRequiredService<IProductAppService>();
        }
        /* TODO: Test methods */
    }
}

該類繼承自ProductManagementApplicationTestBase,它預設整合 ABP 框架和其他基礎設施庫,這樣我們就可以直接使用內建的測試能力。另外,我們使用方法GetRequiredService來解決測試程式碼中的依賴關係,而不是建構函式注入(這在測試中是不可能的)。

現在,我們可以編寫第一個測試方法。在ProductAppService_Tests類中新增如下程式碼:
[Fact]
public async Task Should_Get_Product_List()
{
    //Act
    var output = await _productAppService.GetListAsync(
        new PagedAndSortedResultRequestDto()
    );
    //Assert
    output.TotalCount.ShouldBe(3);
    output.Items.ShouldContain(
        x => x.Name.Contains("Acme Monochrome Laser Printer")
    );
}

該方法呼叫該GetListAsync方法並檢查結果是否正確。如果您開啟測試資源管理器視窗(在 Visual Studio 中的檢視|測試資源管理器選單下),您可以看到我們新增的測試方法。測試資源管理器用於顯示和執行解決方案中的測試:


執行測試到檢查它是否按預期工作。如果方法正常工作,將在測試方法名稱的左側看到一個綠色圖示。

自動 API 控制器和 Swagger UI

Swagger一款服務於開發和測試HTTP API 的的流行工具。它啟動模板中已經預先裝了。
設定.Web專案為啟動專案,然後按 Ctrl+F5執行該專案,啟動後,輸入/swagger URL,如圖所示:
你會看到內建的很多 API。如果向下滾動,也會看到一個Product介面。您可以對其進行測試以獲取產品列表:
我們沒有建立ProductController介面。這個介面是如何出現的?
這裡運用的是ABP 框架的自動 API 控制器功能。它會根據命名約定和配置自動將您的應用服務公開為 HTTP API(通常,我們不會手動編寫控制器)。
自動 API 控制器功能將在[第 14 章] 構建 HTTP API 和實時服務 中詳細介紹。
有了 HTTP API 來獲取產品列表。下一步是在客戶端程式碼中使用此 API。

動態 JavaScript 代理

通常,您通過 JavaScript 呼叫 HTTP API 介面。ABP 會為所有 HTTP API 動態建立客戶端代理。然後,就可以使用這些動態 JavaScript 函式從客戶端呼叫我們的 API。
再次執行ProductManagement.Web專案,並在登入頁面上使用F12快捷鍵開啟瀏覽器的開發者控制檯,然後輸入以下 JavaScript 程式碼:
productManagement.products.product.getList({}).then(function(result) {
    console.log(result);
});

執行此程式碼後,將向伺服器發出請求,並將返回結果記錄在Console選項卡中,如圖所示:

我們可以看到返回的產品列表資料顯示在控制檯選項卡中。這意味著我們可以輕鬆地運用 JavaScript 呼叫伺服器端 API,而無需處理低階細節。
如果您想知道JavaScript 是在哪裡定義getList的,您可以定位到/Abp/ServiceProxyScript地址,檢視由 ABP 框架動態建立的 JavaScript 代理函式。

產品列表

推薦使用 Razor Pages在 ASP.NET Core MVC 框架中建立 UI。
首先,在ProductManagement.Web專案的Pages資料夾下建立一個Products資料夾。然後,右鍵單擊Products資料夾,然後選擇Add|Razor Page。選擇Razor 頁面 - 空選項,命名為Index.cshtml。下圖顯示了我們新增的頁面的位置:

編輯內容,Index.cshtml如下程式碼塊所示:

@page
@using ProductManagement.Web.Pages.Products
@model IndexModel
<h1>Products Page</h1>

在這裡,我放置一個h1元素作為頁首。接下來我們在主選單中新增一個選單來開啟這個頁面。

新增選單項

ABP 提供了一個動態、模組化的選單系統。每個模組都可以新增到主選單。
開啟ProductManagement.Web專案的**Menus資料夾中的ProductManagementMenuContributor類,並在ConfigureMainMenuAsync方法末尾新增以下程式碼:
context.Menu.AddItem(
    new ApplicationMenuItem(
        "ProductManagement",
        l["Menu:ProductManagement"],
        icon: "fas fa-shopping-cart"
            ).AddItem(
        new ApplicationMenuItem(
            "ProductManagement.Products",
            l["Menu:Products"],
            url: "/Products"
        )
    )
);

此程式碼新增了一個產品管理主選單,其中包含產品選單項。裡面的l["…"]語法是用來獲取本地化的值。

開啟ProductManagement.Domain.Shared 專案的Localization/ProductManagement資料夾中的en.json檔案,並將以下程式碼新增到該texts部分的末尾:
"Menu:ProductManagement": "Product Management",
"Menu:Products": "Products"

我們可以使用任意字串值作為本地化鍵。在本例中,我們使用Menu:作為選單的本地化鍵的字首,例如Menu:Products 。我們將在[第 8 章] 使用 ABP 的功能和服務中探討本地化主題。

現在,重新執行,使用新的產品管理選單開啟產品頁面,如圖所示:

建立產品資料表

接下來我們將建立一個資料表顯示帶有分頁和排序的產品列表。ABP 啟動模板帶有預安裝和配置的JS 庫 Datatables.net,用於顯示錶格資料。
開啟Index.cshtml頁面(在Pages/Products資料夾),並將其內容更改為以下內容:
@page
@using ProductManagement.Web.Pages.Products
@using Microsoft.Extensions.Localization
@using ProductManagement.Localization
@model IndexModel
@inject IStringLocalizer<ProductManagementResource> L
@section scripts
{
    <abp-script src="/Pages/Products/Index.cshtml.js" />
}
<abp-card>
    <abp-card-header>
        <h2>@L["Menu:Products"]</h2>
    </abp-card-header>
    <abp-card-body>
        <abp-table id="ProductsTable" striped-rows="true" />
    </abp-card-body>
</abp-card>

abp-script是一個 ABP 標籤助手,用於將指令碼檔案新增到頁面,並具有自動捆綁、壓縮和版本控制功能。abp-card是另一個標籤助手,以一種型別安全且簡單的方式渲染 Card 元件。

我們可以使用標準的 HTML 標籤。但是,ABP 標籤助手極大地簡化了 MVC/Razor 頁面中的 UI 建立。此外,它們支援智慧感知和編譯時錯誤型別檢查。我們將在[第 12 章] 使用 MVC/Razor 頁面中研究標籤助手。
在Pages/Products資料夾下建立一個新的 JavaScript 檔案,命名為Index.cshtml.js,內容如下:
$(function () {
    var l = abp.localization.getResource('ProductManagement');
    var dataTable = $('#ProductsTable').DataTable(
        abp.libs.datatables.normalizeConfiguration({
            serverSide: true,
            paging: true,
            order: [[0, "asc"]],
            searching: false,
            scrollX: true,
            ajax: abp.libs.datatables.createAjax(
                productManagement.products.product.getList),
            columnDefs: [
                /* TODO: Column definitions */
            ]
        })
    );
});

ABP 簡化了資料表配置並提供了內建整合:

  • abp.localization.getResource 返回一個本地化物件,ABP 允許您在 JS中重用伺服器端定義的本地化。
  • abp.libs.datatables.normalizeConfiguration是 ABP 框架定義的輔助函式。它通過為缺失選項提供常規預設值來簡化資料表的配置。
  • abp.libs.datatables.createAjax 使 ABP 的動態 JS 客戶端代理來適配資料表的引數格式。
  • productManagement.products.product.getList是動態JS代理方法。
columnDefs陣列用於定義資料表中的列:
{
    title: l('Name'),
    data: "name"
},
{
    title: l('CategoryName'),
    data: "categoryName",
    orderable: false
},
{
    title: l('Price'),
    data: "price"
},
{
    title: l('StockState'),
    data: "stockState",
    render: function (data) {
        return l('Enum:StockState:' + data);
    }
},
{
    title: l('CreationTime'),
    data: "creationTime",
    dataFormat: 'date'
}

通常,列有一個title欄位和一個data欄位。data欄位匹配ProductDto類中的屬性名稱,格式為駝峰式(一種命名風格,其中每個單詞的第一個字母大寫,第一個單詞除外;它是JavaScript 語言中常用的命名風格)。

render選項用於精細控制如何顯示列資料。
在此頁面上,我們使用了一些本地化鍵。我們應該先在本地化資源中定義它們。開啟ProductManagement.Domain.Shared專案的Localization/ProductManagement 檔案夾中的en.json檔案,並在該部分的末尾新增以下條目texts
"Name": "Name",
"CategoryName": "Category name",
"Price": "Price",
"StockState": "Stock state",
"Enum:StockState:0": "Pre-order",
"Enum:StockState:1": "In stock",
"Enum:StockState:2": "Not available",
"Enum:StockState:3": "Stopped",
"CreationTime": "Creation time"

看一下實際的產品資料表:

至此,我們建立了一個完整的工作頁面,列出了支援分頁和排序的產品。在接下來的部分中,我們將新增建立、編輯和刪除產品的功能。

建立產品

在本節中,我們將開發新增產品所需的功能。我們的大致思路如下:
定義新的應用服務方法來獲取類別和建立產品。
  1. 定義應用服務的獲取類別和建立產品方法。
  2. 在 UI 部分,使用 ABP 的動態表單功能,基於 C# 類自動生成產品建立表單。

定義應用介面

讓我們從給IProductAppService介面新增兩個新方法開始:
Task CreateAsync(CreateUpdateProductDto input);
Task<ListResultDto<CategoryLookupDto>> GetCategoriesAsync();
在建立產品時,我們使用GetCategoriesAsync方法獲取產品類別的下拉資料。我們定義了兩個新的 DTO。
CreateUpdateProductDto用於建立和更新產品(我們將在編輯產品時候重複使用它)。我們在ProductManagement.Application.Contracts專案Products資料夾中定義它:
using System;
using System.ComponentModel.DataAnnotations;
namespace ProductManagement.Products
{
    public class CreateUpdateProductDto
    {
        public Guid CategoryId { get; set; }
        [Required]
        [StringLength(ProductConsts.MaxNameLength)]
        public string Name { get; set; }
        public float Price { get; set; }
        public bool IsFreeCargo { get; set; }
        public DateTime ReleaseDate { get; set; }
        public ProductStockState StockState { get; set; }
    }
}

接下來,在ProductManagement.Application.Contracts專案的Categories資料夾中定義一個CategoryLookupDto類:

using System;
namespace ProductManagement.Categories
{
    public class CategoryLookupDto
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
    }
}

定了介面相關類,現在我們可以在應用層實現介面了。

實現應用服務

ProductAppService中實現CreateAsyncGetCategoriesAsync方法(ProductManagement.Application專案中),如下程式碼塊:
public async Task CreateAsync(CreateUpdateProductDto input)
{
    await _productRepository.InsertAsync(
        ObjectMapper.Map<CreateUpdateProductDto, Product>(input)
    );
}
public async Task<ListResultDto<CategoryLookupDto>> GetCategoriesAsync()
{
    var categories = await _categoryRepository.GetListAsync();
    return new ListResultDto<CategoryLookupDto>(
        ObjectMapper.Map<List<Category>, List<CategoryLookupDto>>(categories)
    );
}

這裡,_categoryRepository屬於IRepository<Category, Guid>服務型別,通過建構函式注入,方法實現很簡單,無需解釋。

我們已經在上面的兩個地方使用了物件對映,現在我們必須配置對映。開啟ProductManagementApplicationAutoMapperProfile.cs檔案(在ProductManagement.Application專案中),新增以下程式碼:
CreateMap<CreateUpdateProductDto, Product>();
CreateMap<Category, CategoryLookupDto>(); 

使用者介面

ProductManagement.Web專案的Pages/Products資料夾下建立一個CreateProductModal.cshtmlRazor 頁面。開啟CreateProductModal.cshtml.cs檔案,更改CreateProductModalModel程式碼:
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using ProductManagement.Products;
namespace ProductManagement.Web.Pages.Products
{
    Public class CreateProductModalModel:ProductManagementPageModel
    {
        [BindProperty]
        public CreateEditProductViewModel Product { get; set; }
        public SelectListItem[] Categories { get; set; }
        private readonly IProductAppService  _productAppService;
 
 
        public CreateProductModalModel(IProductAppService productAppService)
        {
            _productAppService = productAppService;
        }
        public async Task OnGetAsync()
        {
            // TODO
        }
        public async Task<IActionResult> OnPostAsync()
        {
            // TODO
        }
    }
}

這裡的ProductManagementPageModel是基類。你可以繼承它來建立PageModel類。[BindProperty]是一個標準的 ASP.NET Core 屬性,在HTTP Post 請求時,會將資料繫結到Product屬性。Categories將用於顯示下拉選單中的類別。我們通過注入IProductAppService介面以使用之前定義的方法。

目前使用到的CreateEditProductViewModel還沒定義,我們將其定義在與CreateProductModal.cshtml相同的資料夾下:
using ProductManagement.Products;
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
namespace ProductManagement.Web.Pages.Products
{
    public class CreateEditProductViewModel
    {
        [SelectItems("Categories")]
        [DisplayName("Category")]
        public Guid CategoryId { get; set; }
        [Required]
        [StringLength(ProductConsts.MaxNameLength)]
        public string Name { get; set; }
        public float Price { get; set; }
        public bool IsFreeCargo { get; set; }
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public ProductStockState StockState { get; set; }
    }
}
SelectItems告訴我們CategoryId屬性將從Categories列表中選擇。我們將在編輯模式對話方塊中重用此類。這就是我為什麼命名它為CreateEditProductViewModel

DTO 與 ViewModel

定義檢視模型CreateEditProductViewModel似乎沒有必要,因為它與 CreateUpdateProductDtoDTO非常相似。當然你也可以在檢視裡複用DTO。但是,考慮到這些類具有不同的用途,並且隨著時間的推移會向不同的方向發展,所更推薦的辦法是將每個關注點分開。例如,[SelectItems("Categories")]屬性指向 Razor Page 模型,它在應用層沒有任何意義。
現在,我們可以在CreateProductModalModel類中實現OnGetAsync方法:
public async Task OnGetAsync()
{
    Product = new CreateEditProductViewModel
    {
        ReleaseDate = Clock.Now,
        StockState = ProductStockState.PreOrder
    };
    
    var categoryLookup = await _productAppService.GetCategoriesAsync();
    Categories = categoryLookup.Items.Select(x => new SelectListItem(x.Name, x.Id.ToString())).ToArray();
}

我們使用預設值建立Product類,然後使用產品應用服務填充Categories列表。Clock是 ABP 框架提供的服務,用於獲取當前時間(在不處理時區和本地/UTC 時間的情況下),這裡我們不再使用DateTime.Now。具體內容這將在[第 8 章] 使用 ABP 的功能和服務中進行解釋。

我們接著實現OnPostAsync程式碼塊:
public async Task<IActionResult> OnPostAsync()
{
    await _productAppService.CreateAsync(
        ObjectMapper.Map<CreateEditProductViewModel,CreateUpdateProductDto> (Product)
    );
    return NoContent();
}

由於我們要對映CreateEditProductViewModelCreateProductDto,所以需要定義對映配置。我們在ProductManagement.Web專案中開啟ProductManagementWebAutoMapperProfile類,並更改以下程式碼塊內容:

public class ProductManagementWebAutoMapperProfile : Profile
{
    public ProductManagementWebAutoMapperProfile()
    {
        CreateMap<CreateEditProductViewModel, CreateUpdateProductDto>();
    }
}

我們已經完成了產品建立 UI 的 C# 端,接下來可以開始構建 UI 和 JavaScript 程式碼。開啟CreateProductModal.cshtml檔案,並將內容更改如下:

@page
@using Microsoft.AspNetCore.Mvc.Localization
@using ProductManagement.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model ProductManagement.Web.Pages.Products.CreateProductModalModel
@inject IHtmlLocalizer<ProductManagementResource> L
@{
    Layout = null;
}
<abp-dynamic-form abp-model="Product" asp-page="/Products/CreateProductModal">
    <abp-modal>
        <abp-modal-header title="@L["NewProduct"].Value"></abp-modal-header>
        <abp-modal-body>
            <abp-form-content />
        </abp-modal-body>
        <abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
    </abp-modal>
</abp-dynamic-form>

在這裡,abp-dynamic-form會根據 C# 模型類自動建立表單元素。abp-form-content是呈現表單元素的地方。abp-modal用於建立模態對話方塊。

您也可以使用標準的 Bootstrap HTML 元素和 ASP.NET Core 的繫結來建立表單元素。但是,ABP 的 Bootstrap 和動態表單標籤助手大大簡化了 UI 程式碼。我們將在[第 12 章] 使用 MVC/Razor 頁面中介紹 ABP 標籤助手。
我們已經完成建立產品的模態視窗程式碼。現在,我們將在產品頁面新增一個新產品按鈕以開啟該視窗。開啟Pages/Products資料夾中的Index.cshtml檔案,然後將abp-card-header部分更改如下:
<abp-card-header>
    <abp-row>
        <abp-column size-md="_6">
            <abp-card-title>@L["Menu:Products"]</abp-card-title>
        </abp-column>
        <abp-column size-md="_6" class="text-end">
            <abp-button id="NewProductButton"
                        text="@L["NewProduct"].Value"
                        icon="plus"
                        button-type="Primary"/>
        </abp-column>
    </abp-row>
</abp-card-header>

我新增了 2 列,其中每列都有一個size-md="_6"屬性(即 12 列 Bootstrap 網格的一半)。左側設定卡片標題,右側放置了一個按鈕。

之後,我新增以下程式碼到Index.cshtml.js檔案末尾(在})之前):
var createModal = new abp.ModalManager(abp.appPath + 'Products/CreateProductModal');
createModal.onResult(function () {
    dataTable.ajax.reload();
});
$('#NewProductButton').click(function (e) {
    e.preventDefault();
    createModal.open();
});
 
  • abp.ModalManager用於在客戶端管理模式對話方塊。在內部,它使用 Twitter Bootstrap 的標準模態元件,封裝了很多細節,並提供了一個簡單的 API。當模型觸發儲存時會返回一個回撥函式createModal.onResult()
  • createModal.open()用於開啟模態對話方塊。
最後,我們需要在en.json檔案中定義一些本地化文字(.Domain.Shared專案的Localization/ProductManagement 檔案夾下):
"NewProduct": "New Product",
"Category": "Category",
"IsFreeCargo": "Free Cargo",
"ReleaseDate": "Release Date"

再次執行 Web 嘗試建立新產品

ABP基於 C# 類模型自動建立表單欄位。本地化和驗證也可以通過讀取屬性和使用約定來自動工作。我們將在[第 12 章] 使用 MVC/Razor 頁面 中更詳細地介紹驗證和本地化主題。

我們現在可以在 UI 上建立產品了。

編輯產品

編輯產品類似於新增新產品,現在讓我們看看如何編輯產品:

定義應用介面

讓我們從為IProductAppService介面定義兩個新方法:
Task<ProductDto> GetAsync(Guid id);
Task UpdateAsync(Guid id, CreateUpdateProductDto input);

第一種方法用於通過ID獲取產品。我們在UpdateAsync方法中重用之前定義的CreateUpdateProductDto

實現應用介面

實現這些新方法非常簡單。將以下方法新增到ProductAppService類中:
public async Task<ProductDto> GetAsync(Guid id)
{
    return ObjectMapper.Map<Product, ProductDto>(
        await _productRepository.GetAsync(id)
    );
}
public async Task UpdateAsync(Guid id, CreateUpdateProductDto input)
{
    var product = await _productRepository.GetAsync(id);
    ObjectMapper.Map(input, product);
}

GetAsync方法用於從資料庫中獲取產品,並將其對映到ProductDto物件後進行返回。UpdateAsync方法獲取到一個產品後,將給定的DTO輸入對映到產品。通過這種方式,我們用新值覆蓋產品。

對於這個例子,我們不需要呼叫_productRepository.UpdateAsync,因為 EF Core有一個變更跟蹤系統。ABP 的工作單元如果沒有丟擲異常,則在請求結束時會自動儲存更改。我們將在[第 6 章] *使用資料訪問基礎架構”*中介紹工作單元系統。
應用層已完成。接下來,我們將建立一個產品編輯 UI。

使用者介面

建立一個EditProductModal.cshtmlRazor 頁面(ProductManagement.Web專案的 Pages/Products資料夾下)。開啟EditProductModal.cshtml.cs,程式碼更改如下:
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using ProductManagement.Products;
namespace ProductManagement.Web.Pages.Products
{
    public class EditProductModalModel : ProductManagementPageModel
    {
        [HiddenInput]
        [BindProperty(SupportsGet = true)]
        public Guid Id { get; set; }
        [BindProperty]
        public CreateEditProductViewModel Product { get; set; }
        public SelectListItem[] Categories { get; set; }
        private readonly IProductAppService _productAppService;
 
 
        public EditProductModalModel(IProductAppService productAppService)
        {
            _productAppService = productAppService;
        }
        public async Task OnGetAsync()
        {
            // TODO
        }
        public async Task<IActionResult> OnPostAsync()
        {
            // TODO
        }
    }
}

表單中Id欄位將被隱藏。

它還應該支援 HTTP GET 請求,因為 GET 請求會開啟此模型,並且我們需要產品 ID 來編輯表單。
ProductCategories屬性類似於建立產品。
我們還將IProductAppService介面注入到建構函式。
我們實現OnGetAsync方法,如下程式碼塊所示:
public async Task OnGetAsync()
{
    var productDto = await _productAppService.GetAsync(Id);
    Product = ObjectMapper.Map<ProductDto, CreateEditProductViewModel>(productDto);
    
    var categoryLookup = await _productAppService.GetCategoriesAsync();
    Categories = categoryLookup.Items
        .Select(x => new SelectListItem(x.Name, x.Id.ToString()))
        .ToArray();
}

首先,我們要先獲取一個產品 ( ProductDto),再將其轉換為CreateEditProductViewModel,使用它在 UI 上來建立編輯表單。然後,我們在表單上選擇產品類別。

因為這裡對映了ProductDtoCreateEditProductViewModel,所以我們需要在ProductManagementWebAutoMapperProfile類中定義配置對映(ProductManagement.Web專案中),這和我們之前操作是一樣的:
CreateMap<ProductDto, CreateEditProductViewModel>();

我們再看下OnPostAsync()方法:

public async Task<IActionResult> OnPostAsync()
{
    await _productAppService.UpdateAsync(Id,
        ObjectMapper.Map<CreateEditProductViewModel, CreateUpdateProductDto>(Product)
    );
    return NoContent();
}

OnPostAsync方法很簡單,把CreateEditProductViewModel轉換為CreateUpdateProductDto

接著,我們切換到EditProductModal.cshtml,內容更改如下:
@page
@using Microsoft.AspNetCore.Mvc.Localization
@using ProductManagement.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model ProductManagement.Web.Pages.Products.EditProductModalModel
@inject IHtmlLocalizer<ProductManagementResource> L
@{
    Layout = null;
}
<abp-dynamic-form abp-model="Product" asp-page="/Products/EditProductModal">
    <abp-modal>
        <abp-modal-header title="@Model.Product.Name"></abp-modal-header>
        <abp-modal-body>
            <abp-input asp-for="Id" />
            <abp-form-content/>
        </abp-modal-body>
        <abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
    </abp-modal>
</abp-dynamic-form>

頁面與CreateProductModal.cshtml非常相似。我剛剛將Id欄位作為隱藏欄位新增到表單,用來儲存Id編輯的產品的屬性。

最後,我們可以新增一個編輯按鈕以從產品列表中開啟編輯模態視窗。開啟Index.cshtml.js檔案,並在dataTable程式碼的頭部新增一個ModalManager物件:
var editModal = new abp.ModalManager(abp.appPath + 'Products/EditProductModal');

然後,在dataTable內部的columnDefs陣列中定義一個列(第一項):

{
    title: l('Actions'),
    rowAction: {
        items:
            [
                {
                    text: l('Edit'),
                    action: function (data) {
                        editModal.open({ id: data.record.id });
                    }
                }
            ]
    }
},

此程式碼向資料表新增了一個新的Actions列,並新增了一個Edit操作按鈕,單擊即可開啟編輯視窗。rowAction是 ABP Framework 提供的一個特殊選項。它用於在表中的一行新增一個或多個操作按鈕。

最後,在dataTable初始化程式碼後新增如下:
editModal.onResult(function () {
    dataTable.ajax.reload();
});

在儲存產品編輯對話方塊後重新整理資料表,確保我們可以看到表上的最新資料。最終的 UI 類似於下圖:

我們現在可以檢視、建立和編輯產品了。最後一部分將實現刪除產品。

刪除產品

刪除產品與建立或編輯操作相比,非常簡單,因為刪除我們不需要構建表單。
首先,在IProductAppService介面中新增一個新方法:
Task DeleteAsync(Guid id);

然後,在ProductAppService類中實現它:

public async Task DeleteAsync(Guid id)
{
    await _productRepository.DeleteAsync(id);
}

現在向產品列表新增一個新刪除按鈕。開啟Index.cshtml.js,並在Edit操作之後新增以下定義(在rowAction.items陣列中):

{
    text: l('Delete'),
    confirmMessage: function (data) {
        return l('ProductDeletionConfirmationMessage',data.record.name);
    },
    action: function (data) {
        productManagement.products.product
            .delete(data.record.id)
            .then(function() {
                abp.notify.info(l('SuccessfullyDeleted'));
                dataTable.ajax.reload();
            });
    }
}

confirmMessage用於在刪除之前獲得使用者確認。productManagement.products.product.delete函式由 ABP 框架動態建立。通過這種方式,可以直接在 JS 程式碼中呼叫伺服器端方法。我們只需傳遞當前記錄的 ID。then函式傳遞一個回撥函式,用於刪除之後的操作。最後,我們使用abp.notify.info通知使用者,最後重新整理資料表。

我們使用了一些本地化文字,因此我們需要在本地化en.json檔案中新增以下程式碼:
"ProductDeletionConfirmationMessage": "Are you sure to delete this book: {0}",
"SuccessfullyDeleted": "Successfully deleted!"
再次訪問 web 檢視結果:

因為現在有兩個操作按鈕,所以編輯按鈕會自動變成一個下拉選項。當您單擊刪除操作時,您會收到一條確認訊息:

如果你點選在按鈕上,您將在頁面上看到一條通知,並且資料表將被重新整理。
實施產品刪除非常簡單。ABP 的內建功能幫助我們實現了常見的模式,例如客戶端到伺服器的通訊、確認對話方塊和 UI 通知。
請注意,Product實體派生於FullAuditedAggregateRoot,所以它使用了軟刪除。刪除產品後檢查資料庫,您會看到它並沒有真正刪除,但是IsDeleted欄位已經設定為true(邏輯刪除不是物理刪除)。下次查詢商品時,已刪除的商品會自動過濾掉,不包含在查詢結果中。這是由 ABP 框架的資料過濾系統完成的。

概括

至此上下篇章全部完成了,在本篇中,我們建立了一個完整的 CRUD 頁面。我們介紹瞭解決方案中的所有層,並瞭解了ABP 的程式開發的基本方法。
同時,也向您介紹了許多不同的概念,例如實體、儲存庫、資料庫對映和遷移、自動化測試、API 控制器、動態 JavaScript 代理、物件對映、軟刪除等。ABP 是一個全棧應用程式框架,可幫助您通過最佳實踐來實現這些概念。它提供了必要的基礎設施,使您的日常開發更容易。
此時您可能不瞭解所有細節。其餘篇幅會深入研究這些概念並展示它們的細節和不同的用例。
以上的示例相對簡單,它不包含任何重要的業務邏輯,因為我引入了許多概念目的是想讓大家對這些基礎概念有個初步的理解而不是業務複雜性。 

相關文章