在本节中,我们将开发新增产品所需的功能。我们的大致思路如下:
定义新的应用服务方法来获取类别和创建产品。
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`中实现`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>();
```
## 用户界面
在[*ProductManagement.Web*](http://ProductManagement.Web)项目的*Pages/Products*文件夹下创建一个`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](http://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*](http://xn--ProductManagement-6793a.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](http://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 尝试创建新产品
![](https://img.kancloud.cn/b2/2f/b22fa36b220d23e6467e087d1a4ccc87_1136x668.png)
ABP基于 C# 类模型自动创建表单字段。本地化和验证也可以通过读取属性和使用约定来自动工作。我们将在\[*第 12 章*\] *使用 MVC/Razor 页面* 中更详细地介绍验证和本地化主题。
我们现在可以在 UI 上创建产品了。
- 前言
- 第一部分
- 第1章 现代软件开发和 ABP 框架
- 企业级 Web 开发的挑战
- ABP框架的能力清单
- 第2章 ABP框架入门
- 安装 ABP CLI
- 创建新解决方案
- 运行解决方案
- 探索预构建模块
- 第3章 逐步开发开发ABP应用
- 创建解决方案
- 定义领域对象
- EFCore和数据库映射
- 定义应用服务
- 测试产品
- 产品列表
- 创建产品
- 编辑产品
- 删除产品
- 第4章 探索 EventHub解决方案
- 应用介绍
- 架构探索
- 方案运行
- 第二部分
- 第5章 探索ABP基础架构
- 了解模块化
- 使用依赖注入系统
- 配置应用程序
- 实现选项模式
- 日志系统
- 第6章 数据访问基础架构
- 定义实体
- 定义仓储库
- EF Core集成
- 了解 UoW
- 第7章 探索横切关注点
- 认证授权
- 用户验证
- 异常处理
- 第8章 体验 ABP 的功能和服务
- 获取当前用户
- 使用数据过滤
- 控制审计日志
- 缓存数据
- 本地化用户界面
- 第三部分
- 第9章 理解领域驱动设计
- 介绍 DDD
- 构建基于 DDD 的 解决方案
- 处理多个应用程序
- 了解执行流程
- DDD的通用原则
- 第10章 领域层 Domain
- 领域事件案例分析
- 聚合和实体的设计原则和实践
- 实现领域服务
- 落地存储库
- 构建规约(Specification)
- 领域事件
- 第11章 应用层 Application
- 落地应用服务
- 设计 DTO
- 理解各层的职责
- 第四部分
- 第12章 MVC/Razor 页面
- 主题系统
- 绑定和压缩
- 导航菜单
- Bootstrap标签助手
- 创建表单并验证
- 使用模态窗口
- 使用JS API
- 调用HTTP API
- 第13章 Blazor WebAssembly UI
- 什么是Blazor
- ABP Blazor UI
- 验证用户身份
- 理解主题系统
- 使用菜单
- 使用基本服务
- 使用UI服务
- 消费HTTP API
- 使用全局脚本和样式
- 第14章 HTTP API 和实时服务
- 构建HTTP API
- 使用HTTP API
- 使用SignalR
- 第五部分
- 第15章 落地模块化
- 理解模块化
- 构建支付模块
- 安装模块
- 第16章 实现多租户
- 理解多租户
- 多租户基础设施
- 使用功能系统
- 何时使用多租户
- 第17章 构建自动化测试
- 了解ABP测试基础设施
- 构建单元测试
- 构建集成测试