Web应用程序开发教程 - 第九章: 作者: 用户页面
关于本教程
在本系列教程中, 你将构建一个名为 Acme.BookStore
的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:
- Entity Framework Core 做为ORM提供程序.
- MVC / Razor Pages 做为UI框架.
本教程分为以下部分:
- Part 1: 创建服务端
- Part 2: 图书列表页面
- Part 3: 创建,更新和删除图书
- Part 4: 集成测试
- Part 5: 授权
- Part 6: 作者: 领域层
- Part 7: 作者: 数据库集成
- Part 8: 作者: 应用服务层
- Part 9: 作者: 用户页面 (本章)
- Part 10: 图书到作者的关系
下载源码
本教程根据你的UI 和 数据库偏好有多个版本,我们准备了几种可供下载的源码组合:
如果你在Windows中遇到 “文件名太长” or “解压错误”, 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 在Windows 10中启用长路径.
如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
git config --system core.longpaths true
简介
这章阐述如何为前一章介绍的 作者
实体创建CRUD页面.
作者列表页面
在 Acme.BookStore.Web
项目的 Pages/Authors
文件夹下创建一个新的razor页面, Index.cshtml
, 修改文件内容如下.
Index.cshtml
@page
@using Acme.BookStore.Localization
@using Acme.BookStore.Permissions
@using Acme.BookStore.Web.Pages.Authors
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<BookStoreResource> L
@inject IAuthorizationService AuthorizationService
@model IndexModel
@section scripts
{
<abp-script src="/Pages/Authors/Index.js"/>
}
<abp-card>
<abp-card-header>
<abp-row>
<abp-column size-md="_6">
<abp-card-title>@L["Authors"]</abp-card-title>
</abp-column>
<abp-column size-md="_6" class="text-right">
@if (await AuthorizationService
.IsGrantedAsync(BookStorePermissions.Authors.Create))
{
<abp-button id="NewAuthorButton"
text="@L["NewAuthor"].Value"
icon="plus"
button-type="Primary"/>
}
</abp-column>
</abp-row>
</abp-card-header>
<abp-card-body>
<abp-table striped-rows="true" id="AuthorsTable"></abp-table>
</abp-card-body>
</abp-card>
这是一个简单的页面, 和我们以前创建的图书页面一样. 它导入了一个JavaScript文件, 我们后面会进行介绍这个文件.
IndexModel.cshtml.cs
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Acme.BookStore.Web.Pages.Authors
{
public class IndexModel : PageModel
{
public void OnGet()
{
}
}
}
Index.js
$(function () {
var l = abp.localization.getResource('BookStore');
var createModal = new abp.ModalManager(abp.appPath + 'Authors/CreateModal');
var editModal = new abp.ModalManager(abp.appPath + 'Authors/EditModal');
var dataTable = $('#AuthorsTable').DataTable(
abp.libs.datatables.normalizeConfiguration({
serverSide: true,
paging: true,
order: [[1, "asc"]],
searching: false,
scrollX: true,
ajax: abp.libs.datatables.createAjax(acme.bookStore.authors.author.getList),
columnDefs: [
{
title: l('Actions'),
rowAction: {
items:
[
{
text: l('Edit'),
visible:
abp.auth.isGranted('BookStore.Authors.Edit'),
action: function (data) {
editModal.open({ id: data.record.id });
}
},
{
text: l('Delete'),
visible:
abp.auth.isGranted('BookStore.Authors.Delete'),
confirmMessage: function (data) {
return l(
'AuthorDeletionConfirmationMessage',
data.record.name
);
},
action: function (data) {
acme.bookStore.authors.author
.delete(data.record.id)
.then(function() {
abp.notify.info(
l('SuccessfullyDeleted')
);
dataTable.ajax.reload();
});
}
}
]
}
},
{
title: l('Name'),
data: "name"
},
{
title: l('BirthDate'),
data: "birthDate",
render: function (data) {
return luxon
.DateTime
.fromISO(data, {
locale: abp.localization.currentCulture.name
}).toLocaleString();
}
}
]
})
);
createModal.onResult(function () {
dataTable.ajax.reload();
});
editModal.onResult(function () {
dataTable.ajax.reload();
});
$('#NewAuthorButton').click(function (e) {
e.preventDefault();
createModal.open();
});
});
简单来说, 这个JavaScript页面:
- 创建了一个具有
操作
,姓名
和生日
列的数据表格.Actions
列用来添加 编辑 和 删除 操作.生日
提供了一个render
函数, 使用 luxon 库格式化DateTime
值.
- 使用
abp.ModalManager
打开 新建 和 编辑 模态表单.
这块代码与以前创建的图书页面非常相似, 所以我们不再赘述.
本地化
这个页面使用了一些需要声明的本地化键. 打开 Acme.BookStore.Domain.Shared
项目中 Localization/BookStore
文件夹下的 en.json
文件, 加入以下条目:
"Menu:Authors": "Authors",
"Authors": "Authors",
"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?",
"BirthDate": "Birth date",
"NewAuthor": "New author"
简体中文翻译请打开
zh-Hans.json
文件 ,并将”Texts”对象中对应的值替换为中文.
注意我们加入了额外的键. 它们会在下面的小节中被使用.
加入主菜单
打开 Acme.BookStore.Web
项目的 Menus
文件夹中的 BookStoreMenuContributor.cs
, 在 ConfigureMainMenuAsync
方法的结尾加入以下代码:
if (await context.IsGrantedAsync(BookStorePermissions.Authors.Default))
{
bookStoreMenu.AddItem(new ApplicationMenuItem(
"BooksStore.Authors",
l["Menu:Authors"],
url: "/Authors"
));
}
运行应用程序
运行并登录应用程序. 因为你还没有权限, 所以不能看见菜单项. 转到 Identity/Roles
页面, 点击 操作 按钮并选择管理员角色的权限操作:
如你所见, 管理员角色还没有作者管理权限. 单击复选框并保存, 赋予权限. 刷新页面后, 你会在主菜单中的图书商店下看到作者菜单项:
页面是完全可以工作的, 除了 新建作者 和 操作/编辑, 因为它们还没有实现 .
提示: 如果你在定义一个新权限后运行
.DbMigrator
控制台程序, 它会自动将这些权限赋予管理员角色, 你不需要手工赋予权限.
新建模态窗口
在 Acme.BookStore.Web
项目的 Pages/Authors
文件夹下创建一个 razor 页面 CreateModal.cshtml
, 修改它的内容如下:
CreateModal.cshtml
@page
@using Acme.BookStore.Localization
@using Acme.BookStore.Web.Pages.Authors
@using Microsoft.Extensions.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model CreateModalModel
@inject IStringLocalizer<BookStoreResource> L
@{
Layout = null;
}
<form asp-page="/Authors/CreateModal">
<abp-modal>
<abp-modal-header title="@L["NewAuthor"].Value"></abp-modal-header>
<abp-modal-body>
<abp-input asp-for="Author.Name" />
<abp-input asp-for="Author.BirthDate" />
<abp-input asp-for="Author.ShortBio" />
</abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
</abp-modal>
</form>
之前我们已经使用ABP框架的 动态表单开发了图书页面. 这里可以使用相同的方法, 但我们希望展示如何手工完成它. 实际上, 没有那么手工化, 因为在这个例子中我们使用了 abp-input
标签简化了表单元素的创建.
你当然可以使用标准Bootstrap HTML结构, 但是这需要写很多代码. abp-input
自动添加验证, 本地化和根据数据类型生成标准元素.
CreateModal.cshtml.cs
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Acme.BookStore.Authors;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
namespace Acme.BookStore.Web.Pages.Authors
{
public class CreateModalModel : BookStorePageModel
{
[BindProperty]
public CreateAuthorViewModel Author { get; set; }
private readonly IAuthorAppService _authorAppService;
public CreateModalModel(IAuthorAppService authorAppService)
{
_authorAppService = authorAppService;
}
public void OnGet()
{
Author = new CreateAuthorViewModel();
}
public async Task<IActionResult> OnPostAsync()
{
var dto = ObjectMapper.Map<CreateAuthorViewModel, CreateAuthorDto>(Author);
await _authorAppService.CreateAsync(dto);
return NoContent();
}
public class CreateAuthorViewModel
{
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }
[TextArea]
public string ShortBio { get; set; }
}
}
}
这个页面模型类注入和使用 IAuthorAppService
创建新作者. 它和图书创建模型类之间主要的区别是这个模型类为视图模型声明了一个新类 CreateAuthorViewModel
, 而不是重用 CreateAuthorDto
.
这么做的主要原因是展示在页面中如何使用不同的模型. 但还有一个好处: 我们为类成员添加了两个不存在于 CreateAuthorDto
中的特性:
- 为
BirthDate
添加[DataType(DataType.Date)]
特性, 这会在UI为这个属性显示一个日期选择控件. - 为
ShortBio
添加[TextArea]
特性, 这会显示一个多行文本框, 而不是标准文本框.
通过这种方式, 可以根据UI需求定制视图模型类, 而无需修改DTO. 这么做的一个结果是: 使用 ObjectMapper
将 CreateAuthorViewModel
映射到 CreateAuthorDto
. 为了完成映射, 需要在 BookStoreWebAutoMapperProfile
构造函数中加入新的映射代码:
using Acme.BookStore.Authors; // ADDED NAMESPACE IMPORT
using Acme.BookStore.Books;
using AutoMapper;
namespace Acme.BookStore.Web
{
public class BookStoreWebAutoMapperProfile : Profile
{
public BookStoreWebAutoMapperProfile()
{
CreateMap<BookDto, CreateUpdateBookDto>();
// ADD a NEW MAPPING
CreateMap<Pages.Authors.CreateModalModel.CreateAuthorViewModel,
CreateAuthorDto>();
}
}
}
当你重新运行应用程序后, 点击”新建作者” 按钮会打开一个新的模态窗口.
编辑模态窗口
在 Acme.BookStore.Web
项目的 Pages/Authors
文件夹下创建一个 razor 页面 EditModal.cshtml
, 修改它的内容如下:
EditModal.cshtml
@page
@using Acme.BookStore.Localization
@using Acme.BookStore.Web.Pages.Authors
@using Microsoft.Extensions.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model EditModalModel
@inject IStringLocalizer<BookStoreResource> L
@{
Layout = null;
}
<form asp-page="/Authors/EditModal">
<abp-modal>
<abp-modal-header title="@L["Update"].Value"></abp-modal-header>
<abp-modal-body>
<abp-input asp-for="Author.Id" />
<abp-input asp-for="Author.Name" />
<abp-input asp-for="Author.BirthDate" />
<abp-input asp-for="Author.ShortBio" />
</abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
</abp-modal>
</form>
EditModal.cshtml.cs
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Acme.BookStore.Authors;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
namespace Acme.BookStore.Web.Pages.Authors
{
public class EditModalModel : BookStorePageModel
{
[BindProperty]
public EditAuthorViewModel Author { get; set; }
private readonly IAuthorAppService _authorAppService;
public EditModalModel(IAuthorAppService authorAppService)
{
_authorAppService = authorAppService;
}
public async Task OnGetAsync(Guid id)
{
var authorDto = await _authorAppService.GetAsync(id);
Author = ObjectMapper.Map<AuthorDto, EditAuthorViewModel>(authorDto);
}
public async Task<IActionResult> OnPostAsync()
{
await _authorAppService.UpdateAsync(
Author.Id,
ObjectMapper.Map<EditAuthorViewModel, UpdateAuthorDto>(Author)
);
return NoContent();
}
public class EditAuthorViewModel
{
[HiddenInput]
public Guid Id { get; set; }
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }
[TextArea]
public string ShortBio { get; set; }
}
}
}
这个类与 CreateModal.cshtml.cs
类似, 主要不同是:
- 使用
IAuthorAppService.GetAsync(...)
方法从应用层获取正在编辑的作者. EditAuthorViewModel
拥有一个额外的Id
属性, 它被[HiddenInput]
特性标记, 会为这个属性在页面上创建一个隐藏输入框.
这个类要求在 BookStoreWebAutoMapperProfile
类中添加两个对象映射声明:
using Acme.BookStore.Authors;
using Acme.BookStore.Books;
using AutoMapper;
namespace Acme.BookStore.Web
{
public class BookStoreWebAutoMapperProfile : Profile
{
public BookStoreWebAutoMapperProfile()
{
CreateMap<BookDto, CreateUpdateBookDto>();
CreateMap<Pages.Authors.CreateModalModel.CreateAuthorViewModel,
CreateAuthorDto>();
// ADD THESE NEW MAPPINGS
CreateMap<AuthorDto, Pages.Authors.EditModalModel.EditAuthorViewModel>();
CreateMap<Pages.Authors.EditModalModel.EditAuthorViewModel,
UpdateAuthorDto>();
}
}
}
这就是全部了! 你可以运行应用程序并尝试编辑一个作者.
下一章
查看本教程的下一章.