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

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

    本教程分为以下部分:

    下载源码

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

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

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

    CreateModal.cshtml.cs

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

    • 该类派生于 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 中像本示例这样添加了其他内容才需要).

    添加 “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>
    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 按钮:

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

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

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

    更新书籍

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

    bookstore-add-edit-dialog

    打开 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. }
    • 我们添加了 CreateMap<BookDto, CreateUpdateBookDto>(); 作为映射定义.

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

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

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

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

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

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

    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看起来如下:

    打开 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}'?",

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

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

    下一章