Web应用程序开发教程 - 第三章: 创建,更新和删除图书

关于本教程

在本系列教程中, 你将构建一个名为 Acme.BookStore 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:

  • Entity Framework Core 做为ORM提供程序.
  • MVC / Razor Pages 做为UI框架.

本教程分为以下部分:

下载源码

本教程根据你的UIDatabase偏好有多个版,我们准备了两种可供下载的源码组合:

创建新书籍

通过本节, 你将会了解如何创建一个 modal form 来实现新增书籍的功能. 最终成果如下图所示:

bookstore-create-dialog

创建 modal form

Acme.BookStore.Web 项目的 Pages/Books 目录下新建一个 CreateModal.cshtml Razor页面:

bookstore-add-create-dialog

CreateModal.cshtml.cs

打开 CreateModal.cshtml.cs 代码文件,用如下代码替换 CreateModalModel 类的实现:

  1. using System.Threading.Tasks;
  2. using Acme.BookStore.Books;
  3. using Microsoft.AspNetCore.Mvc;
  4. namespace Acme.BookStore.Web.Pages.Books
  5. {
  6. public class CreateModalModel : BookStorePageModel
  7. {
  8. [BindProperty]
  9. public CreateUpdateBookDto Book { get; set; }
  10. private readonly IBookAppService _bookAppService;
  11. public CreateModalModel(IBookAppService bookAppService)
  12. {
  13. _bookAppService = bookAppService;
  14. }
  15. public void OnGet()
  16. {
  17. Book = new CreateUpdateBookDto();
  18. }
  19. public async Task<IActionResult> OnPostAsync()
  20. {
  21. await _bookAppService.CreateAsync(Book);
  22. return NoContent();
  23. }
  24. }
  25. }
  • 该类派生于 BookStorePageModel 而非默认的 PageModel. BookStorePageModel 继承了 PageModel 并且添加了一些可以被你的page model类使用的通用属性和方法.
  • Book 属性上的 [BindProperty] 特性将post请求提交上来的数据绑定到该属性上.
  • 该类通过构造函数注入了 IBookAppService 应用服务,并且在 OnPostAsync 处理程序中调用了服务的 CreateAsync 方法.
  • 它在 OnGet 方法中创建一个新的 CreateUpdateBookDto 对象。 ASP.NET Core不需要像这样创建一个新实例就可以正常工作. 但是它不会为你创建实例,并且如果你的类在类构造函数中具有一些默认值分配或代码执行,它们将无法工作. 对于这种情况,我们为某些 CreateUpdateBookDto 属性设置了默认值.

CreateModal.cshtml

打开 CreateModal.cshtml 文件并粘贴如下代码:

  1. @page
  2. @using Acme.BookStore.Localization
  3. @using Acme.BookStore.Web.Pages.Books
  4. @using Microsoft.Extensions.Localization
  5. @using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
  6. @model CreateModalModel
  7. @inject IStringLocalizer<BookStoreResource> L
  8. @{
  9. Layout = null;
  10. }
  11. <abp-dynamic-form abp-model="Book" asp-page="/Books/CreateModal">
  12. <abp-modal>
  13. <abp-modal-header title="@L["NewBook"].Value"></abp-modal-header>
  14. <abp-modal-body>
  15. <abp-form-content />
  16. </abp-modal-body>
  17. <abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
  18. </abp-modal>
  19. </abp-dynamic-form>
  • 这个 modal 使用 abp-dynamic-form tag Helper 根据 CreateBookViewModel 类自动构建了表单.
  • abp-model 指定了 Book 属性为模型对象.
  • data-ajaxForm 设置了表单通过AJAX提交,而不是经典的页面回发.
  • abp-form-content tag helper 作为表单控件渲染位置的占位符 (这是可选的,只有你在 abp-dynamic-form 中像本示例这样添加了其他内容才需要).

提示: 就像在本示例中一样,Layout 应该为 null,因为当通过AJAX加载模态时,我们不希望包括所有布局.

添加 “New book” 按钮

打开 Pages/Books/Index.cshtml 并按如下代码修改 abp-card-header :

  1. <abp-card-header>
  2. <abp-row>
  3. <abp-column size-md="_6">
  4. <abp-card-title>@L["Books"]</abp-card-title>
  5. </abp-column>
  6. <abp-column size-md="_6" class="text-right">
  7. <abp-button id="NewBookButton"
  8. text="@L["NewBook"].Value"
  9. icon="plus"
  10. button-type="Primary"/>
  11. </abp-column>
  12. </abp-row>
  13. </abp-card-header>

Index.cshtml 的内容最终如下所示:

  1. @page
  2. @using Acme.BookStore.Localization
  3. @using Acme.BookStore.Web.Pages.Books
  4. @using Microsoft.Extensions.Localization
  5. @model IndexModel
  6. @inject IStringLocalizer<BookStoreResource> L
  7. @section scripts
  8. {
  9. <abp-script src="/Pages/Books/Index.js"/>
  10. }
  11. <abp-card>
  12. <abp-card-header>
  13. <abp-row>
  14. <abp-column size-md="_6">
  15. <abp-card-title>@L["Books"]</abp-card-title>
  16. </abp-column>
  17. <abp-column size-md="_6" class="text-right">
  18. <abp-button id="NewBookButton"
  19. text="@L["NewBook"].Value"
  20. icon="plus"
  21. button-type="Primary"/>
  22. </abp-column>
  23. </abp-row>
  24. </abp-card-header>
  25. <abp-card-body>
  26. <abp-table striped-rows="true" id="BooksTable"></abp-table>
  27. </abp-card-body>
  28. </abp-card>

如下图所示,只是在表格 右上方 添加了 New book 按钮:

bookstore-new-book-button

打开 Pages/book/index.jsdatatable 配置代码后面添加如下代码:

  1. var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
  2. createModal.onResult(function () {
  3. dataTable.ajax.reload();
  4. });
  5. $('#NewBookButton').click(function (e) {
  6. e.preventDefault();
  7. createModal.open();
  8. });
  • abp.ModalManager 是一个在客户端打开和管理modal的辅助类.它基于Twitter Bootstrap的标准modal组件通过简化的API抽象隐藏了许多细节.
  • createModal.onResult(...) 用于在创建书籍后刷新数据表格.
  • createModal.open(); 用于打开模态创建新书籍.

Index.js 的内容最终如下所示:

  1. $(function () {
  2. var l = abp.localization.getResource('BookStore');
  3. var dataTable = $('#BooksTable').DataTable(
  4. abp.libs.datatables.normalizeConfiguration({
  5. serverSide: true,
  6. paging: true,
  7. order: [[1, "asc"]],
  8. searching: false,
  9. scrollX: true,
  10. ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList),
  11. columnDefs: [
  12. {
  13. title: l('Name'),
  14. data: "name"
  15. },
  16. {
  17. title: l('Type'),
  18. data: "type",
  19. render: function (data) {
  20. return l('Enum:BookType:' + data);
  21. }
  22. },
  23. {
  24. title: l('PublishDate'),
  25. data: "publishDate",
  26. render: function (data) {
  27. return luxon
  28. .DateTime
  29. .fromISO(data, {
  30. locale: abp.localization.currentCulture.name
  31. }).toLocaleString();
  32. }
  33. },
  34. {
  35. title: l('Price'),
  36. data: "price"
  37. },
  38. {
  39. title: l('CreationTime'), data: "creationTime",
  40. render: function (data) {
  41. return luxon
  42. .DateTime
  43. .fromISO(data, {
  44. locale: abp.localization.currentCulture.name
  45. }).toLocaleString(luxon.DateTime.DATETIME_SHORT);
  46. }
  47. }
  48. ]
  49. })
  50. );
  51. var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
  52. createModal.onResult(function () {
  53. dataTable.ajax.reload();
  54. });
  55. $('#NewBookButton').click(function (e) {
  56. e.preventDefault();
  57. createModal.open();
  58. });
  59. });

现在,你可以 运行程序 通过新的 modal form 来创建书籍了.

更新书籍

Acme.BookStore.Web 项目的 Pages/Books 目录下新建一个名叫 EditModal.cshtml 的Razor页面:

bookstore-add-edit-dialog

EditModal.cshtml.cs

打开 EditModal.cshtml.cs 文件(EditModalModel类) 并替换成以下代码:

  1. using System;
  2. using System.Threading.Tasks;
  3. using Acme.BookStore.Books;
  4. using Microsoft.AspNetCore.Mvc;
  5. namespace Acme.BookStore.Web.Pages.Books
  6. {
  7. public class EditModalModel : BookStorePageModel
  8. {
  9. [HiddenInput]
  10. [BindProperty(SupportsGet = true)]
  11. public Guid Id { get; set; }
  12. [BindProperty]
  13. public CreateUpdateBookDto Book { get; set; }
  14. private readonly IBookAppService _bookAppService;
  15. public EditModalModel(IBookAppService bookAppService)
  16. {
  17. _bookAppService = bookAppService;
  18. }
  19. public async Task OnGetAsync()
  20. {
  21. var bookDto = await _bookAppService.GetAsync(Id);
  22. Book = ObjectMapper.Map<BookDto, CreateUpdateBookDto>(bookDto);
  23. }
  24. public async Task<IActionResult> OnPostAsync()
  25. {
  26. await _bookAppService.UpdateAsync(Id, Book);
  27. return NoContent();
  28. }
  29. }
  30. }
  • [HiddenInput][BindProperty] 是标准的 ASP.NET Core MVC 特性.这里启用 SupportsGet 从Http请求的查询字符串中获取Id的值.
  • OnGetAsync 方法中,将 BookAppService.GetAsync 方法返回的 BookDto 映射成 CreateUpdateBookDto 并赋值给Book属性.
  • OnPostAsync 方法直接使用 BookAppService.UpdateAsync 来更新实体.

BookDto 到 CreateUpdateBookDto 对象映射

为了执行 BookDtoCreateUpdateBookDto 对象映射,请打开 Acme.BookStore.Web 项目中的 BookStoreWebAutoMapperProfile.cs 并更改它,如下所示:

  1. using AutoMapper;
  2. namespace Acme.BookStore.Web
  3. {
  4. public class BookStoreWebAutoMapperProfile : Profile
  5. {
  6. public BookStoreWebAutoMapperProfile()
  7. {
  8. CreateMap<BookDto, CreateUpdateBookDto>();
  9. }
  10. }
  11. }
  • 我们添加了 CreateMap<BookDto, CreateUpdateBookDto>(); 作为映射定义.

请注意,我们在Web层中进行映射定义是一种最佳实践,因为仅在该层中需要它.

EditModal.cshtml

EditModal.cshtml 页面内容替换成如下代码:

  1. @page
  2. @using Acme.BookStore.Localization
  3. @using Acme.BookStore.Web.Pages.Books
  4. @using Microsoft.Extensions.Localization
  5. @using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
  6. @model EditModalModel
  7. @inject IStringLocalizer<BookStoreResource> L
  8. @{
  9. Layout = null;
  10. }
  11. <abp-dynamic-form abp-model="Book" asp-page="/Books/EditModal">
  12. <abp-modal>
  13. <abp-modal-header title="@L["Update"].Value"></abp-modal-header>
  14. <abp-modal-body>
  15. <abp-input asp-for="Id" />
  16. <abp-form-content />
  17. </abp-modal-body>
  18. <abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
  19. </abp-modal>
  20. </abp-dynamic-form>

这个页面内容和 CreateModal.cshtml 非常相似,除了以下几点:

  • 它包含id属性的abp-input, 用于存储编辑书的 id (它是隐藏的Input)
  • 此页面指定的post地址是Books/EditModal.

为表格添加 “操作(Actions)” 下拉菜单

我们将为表格每行添加下拉按钮 (“Actions”):

打开 Pages/Books/Index.js 页面,并按下方所示修改表格部分的代码:

  1. $(function () {
  2. var l = abp.localization.getResource('BookStore');
  3. var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
  4. var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal');
  5. var dataTable = $('#BooksTable').DataTable(
  6. abp.libs.datatables.normalizeConfiguration({
  7. serverSide: true,
  8. paging: true,
  9. order: [[1, "asc"]],
  10. searching: false,
  11. scrollX: true,
  12. ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList),
  13. columnDefs: [
  14. {
  15. title: l('Actions'),
  16. rowAction: {
  17. items:
  18. [
  19. {
  20. text: l('Edit'),
  21. action: function (data) {
  22. editModal.open({ id: data.record.id });
  23. }
  24. }
  25. ]
  26. }
  27. },
  28. {
  29. title: l('Name'),
  30. data: "name"
  31. },
  32. {
  33. title: l('Type'),
  34. data: "type",
  35. render: function (data) {
  36. return l('Enum:BookType:' + data);
  37. }
  38. },
  39. {
  40. title: l('PublishDate'),
  41. data: "publishDate",
  42. render: function (data) {
  43. return luxon
  44. .DateTime
  45. .fromISO(data, {
  46. locale: abp.localization.currentCulture.name
  47. }).toLocaleString();
  48. }
  49. },
  50. {
  51. title: l('Price'),
  52. data: "price"
  53. },
  54. {
  55. title: l('CreationTime'), data: "creationTime",
  56. render: function (data) {
  57. return luxon
  58. .DateTime
  59. .fromISO(data, {
  60. locale: abp.localization.currentCulture.name
  61. }).toLocaleString(luxon.DateTime.DATETIME_SHORT);
  62. }
  63. }
  64. ]
  65. })
  66. );
  67. createModal.onResult(function () {
  68. dataTable.ajax.reload();
  69. });
  70. editModal.onResult(function () {
  71. dataTable.ajax.reload();
  72. });
  73. $('#NewBookButton').click(function (e) {
  74. e.preventDefault();
  75. createModal.open();
  76. });
  77. });
  • 增加了一个新的 ModalManager 名为 editModal 打开编辑模态框.
  • columnDefs 部分的开头添加了一个新列,用于”Actions“下拉按钮.
  • Edit“ 动作简单地调用 editModal.open() 打开编辑模态框.
  • editModal.onResult(...) 当你关闭编程模态框时进行回调刷新数据表格.

你可以运行应用程序,并通过选择一本书的编辑操作编辑任何一本书.

最终的UI看起来如下:

bookstore-books-table-actions

删除书籍

打开 Pages/book/index.js 文件,在 rowAction items 下新增一项:

  1. {
  2. text: l('Delete'),
  3. confirmMessage: function (data) {
  4. return l('BookDeletionConfirmationMessage', data.record.name);
  5. },
  6. action: function (data) {
  7. acme.bookStore.books.book
  8. .delete(data.record.id)
  9. .then(function() {
  10. abp.notify.info(l('SuccessfullyDeleted'));
  11. dataTable.ajax.reload();
  12. });
  13. }
  14. }
  • confirmMessage 用来在实际执行 action 之前向用户进行确认.
  • 通过javascript代理方法 acme.bookStore.books.book.delete(...) 执行一个AJAX请求来删除一个book实体.
  • abp.notify.info 用来在执行删除操作后显示一个toastr通知信息.

由于我们使用了两个新的本地化文本(BookDeletionConfirmationMessageSuccesslyDeleted),因此你需要将它们添加到本地化文件(Acme.BookStore.Domain.Shared项目的Localization/BookStore文件夹下的en.json):

  1. "BookDeletionConfirmationMessage": "Are you sure to delete the book '{0}'?",
  2. "SuccessfullyDeleted": "Successfully deleted!"

Index.js 的内容最终如下所示:

  1. $(function () {
  2. var l = abp.localization.getResource('BookStore');
  3. var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
  4. var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal');
  5. var dataTable = $('#BooksTable').DataTable(
  6. abp.libs.datatables.normalizeConfiguration({
  7. serverSide: true,
  8. paging: true,
  9. order: [[1, "asc"]],
  10. searching: false,
  11. scrollX: true,
  12. ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList),
  13. columnDefs: [
  14. {
  15. title: l('Actions'),
  16. rowAction: {
  17. items:
  18. [
  19. {
  20. text: l('Edit'),
  21. action: function (data) {
  22. editModal.open({ id: data.record.id });
  23. }
  24. },
  25. {
  26. text: l('Delete'),
  27. confirmMessage: function (data) {
  28. return l(
  29. 'BookDeletionConfirmationMessage',
  30. data.record.name
  31. );
  32. },
  33. action: function (data) {
  34. acme.bookStore.books.book
  35. .delete(data.record.id)
  36. .then(function() {
  37. abp.notify.info(
  38. l('SuccessfullyDeleted')
  39. );
  40. dataTable.ajax.reload();
  41. });
  42. }
  43. }
  44. ]
  45. }
  46. },
  47. {
  48. title: l('Name'),
  49. data: "name"
  50. },
  51. {
  52. title: l('Type'),
  53. data: "type",
  54. render: function (data) {
  55. return l('Enum:BookType:' + data);
  56. }
  57. },
  58. {
  59. title: l('PublishDate'),
  60. data: "publishDate",
  61. render: function (data) {
  62. return luxon
  63. .DateTime
  64. .fromISO(data, {
  65. locale: abp.localization.currentCulture.name
  66. }).toLocaleString();
  67. }
  68. },
  69. {
  70. title: l('Price'),
  71. data: "price"
  72. },
  73. {
  74. title: l('CreationTime'), data: "creationTime",
  75. render: function (data) {
  76. return luxon
  77. .DateTime
  78. .fromISO(data, {
  79. locale: abp.localization.currentCulture.name
  80. }).toLocaleString(luxon.DateTime.DATETIME_SHORT);
  81. }
  82. }
  83. ]
  84. })
  85. );
  86. createModal.onResult(function () {
  87. dataTable.ajax.reload();
  88. });
  89. editModal.onResult(function () {
  90. dataTable.ajax.reload();
  91. });
  92. $('#NewBookButton').click(function (e) {
  93. e.preventDefault();
  94. createModal.open();
  95. });
  96. });

你可以运行程序并尝试删除一本书.

下一章

查看本教程的下一章.