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

測試 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 是一個全棧應用程序框架,可幫助您通過最佳實踐來實現這些概念。它提供了必要的基礎設施,使您的日常開發更容易。
此時您可能不瞭解所有細節。其餘篇幅會深入研究這些概念並展示它們的細節和不同的用例。
以上的示例相對簡單,它不包含任何重要的業務邏輯,因爲我引入了許多概念目的是想讓大家對這些基礎概念有個初步的理解而不是業務複雜性。 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章