測試 ProductAppService 類
ProductAppService
類的GetListAsync
方法寫單元測試程式碼(構建自動化測試細節後續再議)。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
URL,如圖所示:我們沒有建立ProductController介面。這個介面是如何出現的?
動態 JavaScript 代理
productManagement.products.product.getList({}).then(function(result) {
console.log(result);
});
執行此程式碼後,將向伺服器發出請求,並將返回結果記錄在Console選項卡中,如圖所示:
getList
的,您可以定位到/Abp/ServiceProxyScript
地址,檢視由 ABP 框架動態建立的 JavaScript 代理函式。產品列表
Index.cshtml
。下圖顯示了我們新增的頁面的位置:編輯內容,Index.cshtml
如下程式碼塊所示:
@page
@using ProductManagement.Web.Pages.Products
@model IndexModel
<h1>Products Page</h1>
在這裡,我放置一個h1
元素作為頁首。接下來我們在主選單中新增一個選單來開啟這個頁面。
新增選單項
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["…"]
語法是用來獲取本地化的值。
en.json
檔案,並將以下程式碼新增到該texts
部分的末尾:"Menu:ProductManagement": "Product Management", "Menu:Products": "Products"
我們可以使用任意字串值作為本地化鍵。在本例中,我們使用Menu:
作為選單的本地化鍵的字首,例如Menu:Products
。我們將在[第 8 章] 使用 ABP 的功能和服務中探討本地化主題。
建立產品資料表
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 頁面中研究標籤助手。
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
選項用於精細控制如何顯示列資料。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"
看一下實際的產品資料表:
建立產品
定義新的應用服務方法來獲取類別和建立產品。
-
定義應用服務的獲取類別和建立產品方法。
-
在 UI 部分,使用 ABP 的動態表單功能,基於 C# 類自動生成產品建立表單。
定義應用介面
IProductAppService
介面新增兩個新方法開始: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
中實現CreateAsync
和GetCategoriesAsync
方法(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>();
使用者介面
CreateProductModal.cshtml
Razor 頁面。開啟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
似乎沒有必要,因為它與 CreateUpdateProductDto
DTO非常相似。當然你也可以在檢視裡複用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(); }
由於我們要對映CreateEditProductViewModel
到CreateProductDto
,所以需要定義對映配置。我們在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
用於建立模態對話方塊。
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 頁面 中更詳細地介紹驗證和本地化主題。
編輯產品
定義應用介面
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 章] *使用資料訪問基礎架構”*中介紹工作單元系統。使用者介面
EditProductModal.cshtml
Razor 頁面(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
欄位將被隱藏。
Product
和Categories
屬性類似於建立產品。我們還將
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 上來建立編輯表單。然後,我們在表單上選擇產品類別。
ProductDto
到CreateEditProductViewModel
,所以我們需要在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
檔案中新增以下程式碼:因為現在有兩個操作按鈕,所以編輯按鈕會自動變成一個下拉選項。當您單擊刪除操作時,您會收到一條確認訊息:
Product
實體派生於FullAuditedAggregateRoot
,所以它使用了軟刪除。刪除產品後檢查資料庫,您會看到它並沒有真正刪除,但是IsDeleted
欄位已經設定為true
(邏輯刪除不是物理刪除)。下次查詢商品時,已刪除的商品會自動過濾掉,不包含在查詢結果中。這是由 ABP 框架的資料過濾系統完成的。