Web应用程序开发教程 - 第十章: 图书到作者的关系

关于本教程

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

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

本教程分为以下部分:

下载源码

本教程根据你的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

简介

我们已经为图书管理应用程序创建了 图书作者 功能. 然而, 这些实体间还没有关联.

在本章, 我们会在 作者图书 实体间建立 1 对 N 的关系.

在图书实体中加入关系

打开 Acme.BookStore.Domain 项目中的 Books/Book.cs, 在 Book 实体中加入下列属性:

  1. public Guid AuthorId { get; set; }

在本章中, 我们选择不在 Book 类中加入 Author 实体的 导航属性 (例如 public Author Author { get; set; }). 这是为了遵循 DDD 最佳实践 (规则: 仅通过id引用其它聚合对象). 但是, 你自己可以添加这样的导航属性, 并为EF Core配置它. 这样, 你在获取图书和它们的作者时就不需要写join查询了(如同下面我们做的一样), 这会使代码更简洁一些.

数据库 & 数据迁移

Book 实体新增一个不为空的 AuthorId 属性. 但是, 数据库中已存在的图书怎么办? 它们没有 AuthorIds, 当我们尝试运行应用程序时会出问题.

这是一个 典型的迁移问题, 解决方案依赖于你的具体情况;

  • 如果你还没有发布应用程序到生产环境, 你可以直接删除数据库中的图书数据, 甚至你可以删除开发环境中的整个数据库.
  • 你可以在数据迁移或生成种子阶段使用代码更新已有数据.
  • 你可以手工处理这些数据.

我们倾向于 删除数据库 (你可以在 Package Manager 控制台中运行 Drop-Database), 因为这只是个示例项目, 数据丢失并不要紧. 因为这个主题不是关于ABP框架的, 我们不会深入所有的场景.

更新 EF Core 映射

定位到 Acme.BookStore.EntityFrameworkCore 项目的 EntityFrameworkCore 文件夹下的 BookStoreDbContext 类的 OnModelCreating 方法, 修改 builder.Entity<Book> 部分如下:

  1. builder.Entity<Book>(b =>
  2. {
  3. b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema);
  4. b.ConfigureByConvention(); //auto configure for the base class props
  5. b.Property(x => x.Name).IsRequired().HasMaxLength(128);
  6. // ADD THE MAPPING FOR THE RELATION
  7. b.HasOne<Author>().WithMany().HasForeignKey(x => x.AuthorId).IsRequired();
  8. });

新增 EF Core 迁移

启动解决方案被配置为使用 Entity Framework Core Code First Migrations. 因为我们修改了数据库映射配置, 我们需要新建一个迁移并应用于数据库.

Acme.BookStore.EntityFrameworkCore 项目的文件目录打开命令行终端, 输入命令:

  1. dotnet ef migrations add Added_AuthorId_To_Book

这会创建一个新的迁移类, 在它的 Up 方法中使用下列方法:

  1. migrationBuilder.AddColumn<Guid>(
  2. name: "AuthorId",
  3. table: "AppBooks",
  4. nullable: false,
  5. defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
  6. migrationBuilder.CreateIndex(
  7. name: "IX_AppBooks_AuthorId",
  8. table: "AppBooks",
  9. column: "AuthorId");
  10. migrationBuilder.AddForeignKey(
  11. name: "FK_AppBooks_AppAuthors_AuthorId",
  12. table: "AppBooks",
  13. column: "AuthorId",
  14. principalTable: "AppAuthors",
  15. principalColumn: "Id",
  16. onDelete: ReferentialAction.Cascade);
  • AppBooks 表增加一个 AuthorId 字段 .
  • 根据 AuthorId 字段新建一个索引.
  • 声明到 AppAuthors 表的外键.

如果你使用 Visual Studio, 可能希望在 Package Manager Console (PMC) 使用 Add-Migration Added_AuthorId_To_Book -c BookStoreDbContextUpdate-Database -Context BookStoreDbContext 命令. 如果这样, 保证 Acme.BookStore.Web 是启动项目并且在PMC中 Acme.BookStore.EntityFrameworkCore默认项目 .

修改数据种子

因为 AuthorIdBook 实体的不可为空属性, 当前的数据种子代码不能工作. 打开 Acme.BookStore.Domain 项目中的 BookStoreDataSeederContributor, 修改成以下代码:

  1. using System;
  2. using System.Threading.Tasks;
  3. using Acme.BookStore.Authors;
  4. using Acme.BookStore.Books;
  5. using Volo.Abp.Data;
  6. using Volo.Abp.DependencyInjection;
  7. using Volo.Abp.Domain.Repositories;
  8. namespace Acme.BookStore
  9. {
  10. public class BookStoreDataSeederContributor
  11. : IDataSeedContributor, ITransientDependency
  12. {
  13. private readonly IRepository<Book, Guid> _bookRepository;
  14. private readonly IAuthorRepository _authorRepository;
  15. private readonly AuthorManager _authorManager;
  16. public BookStoreDataSeederContributor(
  17. IRepository<Book, Guid> bookRepository,
  18. IAuthorRepository authorRepository,
  19. AuthorManager authorManager)
  20. {
  21. _bookRepository = bookRepository;
  22. _authorRepository = authorRepository;
  23. _authorManager = authorManager;
  24. }
  25. public async Task SeedAsync(DataSeedContext context)
  26. {
  27. if (await _bookRepository.GetCountAsync() > 0)
  28. {
  29. return;
  30. }
  31. var orwell = await _authorRepository.InsertAsync(
  32. await _authorManager.CreateAsync(
  33. "George Orwell",
  34. new DateTime(1903, 06, 25),
  35. "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)."
  36. )
  37. );
  38. var douglas = await _authorRepository.InsertAsync(
  39. await _authorManager.CreateAsync(
  40. "Douglas Adams",
  41. new DateTime(1952, 03, 11),
  42. "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'."
  43. )
  44. );
  45. await _bookRepository.InsertAsync(
  46. new Book
  47. {
  48. AuthorId = orwell.Id, // SET THE AUTHOR
  49. Name = "1984",
  50. Type = BookType.Dystopia,
  51. PublishDate = new DateTime(1949, 6, 8),
  52. Price = 19.84f
  53. },
  54. autoSave: true
  55. );
  56. await _bookRepository.InsertAsync(
  57. new Book
  58. {
  59. AuthorId = douglas.Id, // SET THE AUTHOR
  60. Name = "The Hitchhiker's Guide to the Galaxy",
  61. Type = BookType.ScienceFiction,
  62. PublishDate = new DateTime(1995, 9, 27),
  63. Price = 42.0f
  64. },
  65. autoSave: true
  66. );
  67. }
  68. }
  69. }

唯一的区别是设置 Book 实体的 AuthorId 属性.

执行 DbMigrator 前删除已有图书或数据库. 参阅上面的 数据库 & 数据迁移 小节获取详细信息.

你现在可以运行 .DbMigrator 控制台应用程序, 迁移 数据库 schema 并生成 种子 初始数据.

应用层

我们将修改 BookAppService, 支持作者关系.

数据传输对象

让我们从DTOs开始.

BookDto

打开 Acme.BookStore.Application.Contracts 项目的 Books 文件夹下的 BookDto 类, 添加如下属性:

  1. public Guid AuthorId { get; set; }
  2. public string AuthorName { get; set; }

最终的 BookDto 类应该如下:

  1. using System;
  2. using Volo.Abp.Application.Dtos;
  3. namespace Acme.BookStore.Books
  4. {
  5. public class BookDto : AuditedEntityDto<Guid>
  6. {
  7. public Guid AuthorId { get; set; }
  8. public string AuthorName { get; set; }
  9. public string Name { get; set; }
  10. public BookType Type { get; set; }
  11. public DateTime PublishDate { get; set; }
  12. public float Price { get; set; }
  13. }
  14. }

CreateUpdateBookDto

打开 Acme.BookStore.Application.Contracts 项目的 Books 文件夹下的 CreateUpdateBookDto 类, 添加 AuthorId 属性:

  1. public Guid AuthorId { get; set; }

AuthorLookupDto

Acme.BookStore.Application.Contracts 项目的 Books 文件夹下新建一个类 AuthorLookupDto:

  1. using System;
  2. using Volo.Abp.Application.Dtos;
  3. namespace Acme.BookStore.Books
  4. {
  5. public class AuthorLookupDto : EntityDto<Guid>
  6. {
  7. public string Name { get; set; }
  8. }
  9. }

它会被一个将要添加到 IBookAppService 的新方法使用.

IBookAppService

打开 Acme.BookStore.Application.Contracts 项目的 Books 文件夹下的 IBookAppService 接口, 添加一个名为 GetAuthorLookupAsync 的新方法:

  1. using System;
  2. using System.Threading.Tasks;
  3. using Volo.Abp.Application.Dtos;
  4. using Volo.Abp.Application.Services;
  5. namespace Acme.BookStore.Books
  6. {
  7. public interface IBookAppService :
  8. ICrudAppService< //Defines CRUD methods
  9. BookDto, //Used to show books
  10. Guid, //Primary key of the book entity
  11. PagedAndSortedResultRequestDto, //Used for paging/sorting
  12. CreateUpdateBookDto> //Used to create/update a book
  13. {
  14. // ADD the NEW METHOD
  15. Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync();
  16. }
  17. }

这个新方法将被UI用来获取作者列表, 填充一个下拉框. 使用这个下拉框选择图书作者.

BookAppService

打开 Acme.BookStore.Application 项目的 Books 文件夹下的 BookAppService 类, 更新为以下代码:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Linq.Dynamic.Core;
  5. using System.Threading.Tasks;
  6. using Acme.BookStore.Authors;
  7. using Acme.BookStore.Permissions;
  8. using Microsoft.AspNetCore.Authorization;
  9. using Volo.Abp.Application.Dtos;
  10. using Volo.Abp.Application.Services;
  11. using Volo.Abp.Domain.Entities;
  12. using Volo.Abp.Domain.Repositories;
  13. namespace Acme.BookStore.Books
  14. {
  15. [Authorize(BookStorePermissions.Books.Default)]
  16. public class BookAppService :
  17. CrudAppService<
  18. Book, //The Book entity
  19. BookDto, //Used to show books
  20. Guid, //Primary key of the book entity
  21. PagedAndSortedResultRequestDto, //Used for paging/sorting
  22. CreateUpdateBookDto>, //Used to create/update a book
  23. IBookAppService //implement the IBookAppService
  24. {
  25. private readonly IAuthorRepository _authorRepository;
  26. public BookAppService(
  27. IRepository<Book, Guid> repository,
  28. IAuthorRepository authorRepository)
  29. : base(repository)
  30. {
  31. _authorRepository = authorRepository;
  32. GetPolicyName = BookStorePermissions.Books.Default;
  33. GetListPolicyName = BookStorePermissions.Books.Default;
  34. CreatePolicyName = BookStorePermissions.Books.Create;
  35. UpdatePolicyName = BookStorePermissions.Books.Edit;
  36. DeletePolicyName = BookStorePermissions.Books.Create;
  37. }
  38. public override async Task<BookDto> GetAsync(Guid id)
  39. {
  40. //Get the IQueryable<Book> from the repository
  41. var queryable = await Repository.GetQueryableAsync();
  42. //Prepare a query to join books and authors
  43. var query = from book in queryable
  44. join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id
  45. where book.Id == id
  46. select new { book, author };
  47. //Execute the query and get the book with author
  48. var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query);
  49. if (queryResult == null)
  50. {
  51. throw new EntityNotFoundException(typeof(Book), id);
  52. }
  53. var bookDto = ObjectMapper.Map<Book, BookDto>(queryResult.book);
  54. bookDto.AuthorName = queryResult.author.Name;
  55. return bookDto;
  56. }
  57. public override async Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input)
  58. {
  59. //Get the IQueryable<Book> from the repository
  60. var queryable = await Repository.GetQueryableAsync();
  61. //Prepare a query to join books and authors
  62. var query = from book in queryable
  63. join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id
  64. select new {book, author};
  65. //Paging
  66. query = query
  67. .OrderBy(NormalizeSorting(input.Sorting))
  68. .Skip(input.SkipCount)
  69. .Take(input.MaxResultCount);
  70. //Execute the query and get a list
  71. var queryResult = await AsyncExecuter.ToListAsync(query);
  72. //Convert the query result to a list of BookDto objects
  73. var bookDtos = queryResult.Select(x =>
  74. {
  75. var bookDto = ObjectMapper.Map<Book, BookDto>(x.book);
  76. bookDto.AuthorName = x.author.Name;
  77. return bookDto;
  78. }).ToList();
  79. //Get the total count with another query
  80. var totalCount = await Repository.GetCountAsync();
  81. return new PagedResultDto<BookDto>(
  82. totalCount,
  83. bookDtos
  84. );
  85. }
  86. public async Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync()
  87. {
  88. var authors = await _authorRepository.GetListAsync();
  89. return new ListResultDto<AuthorLookupDto>(
  90. ObjectMapper.Map<List<Author>, List<AuthorLookupDto>>(authors)
  91. );
  92. }
  93. private static string NormalizeSorting(string sorting)
  94. {
  95. if (sorting.IsNullOrEmpty())
  96. {
  97. return $"book.{nameof(Book.Name)}";
  98. }
  99. if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase))
  100. {
  101. return sorting.Replace(
  102. "authorName",
  103. "author.Name",
  104. StringComparison.OrdinalIgnoreCase
  105. );
  106. }
  107. return $"book.{sorting}";
  108. }
  109. }
  110. }

我们做了以下修改:

  • 给所有新建/覆写的方法增加 [Authorize(BookStorePermissions.Books.Default)] 进行授权(当授权特性应用于类时, 它对这个类的所有方法有效).
  • 注入 IAuthorRepository, 从作者中查询.
  • 覆写基类 CrudAppServiceGetAsync 方法. 这个方法根据给定的 id 返回单一 BookDto 对象.
    • 使用一个简单的LINQ表达式关联图书和作者, 根据给定的图书id查询, 查询结果同时包含图书和作者.
    • 使用 AsyncExecuter.FirstOrDefaultAsync(...) 执行查询并得到一个结果. 这是一种无需依赖database provider API, 使用异步LINQ扩展的方法. 参阅 repository文档以理解我们为什么使用它.
    • 如果请求的图书在数据库中不存在, 抛出一个 EntityNotFoundException, 这会导致一个 HTTP 404 (not found) 状态码.
    • 最后, 使用 ObjectMapper创建一个 BookDto 对象, 然后手工给 AuthorName 赋值.
  • 覆写 CrudAppService 基类的 GetListAsync 方法, 返回图书列表. 逻辑与前一个方法类似, 所以很容易理解.
  • 新建一个方法: GetAuthorLookupAsync. 这个方法只是简单地获取所有作者. UI使用这个方法填充一个下拉框, 当编辑图书时用来选择作者.

对象到对象映射映射

引入 AuthorLookupDto 类, 在 GetAuthorLookupAsync 方法中使用对象映射. 所以, 我们需要在 Acme.BookStore.Application 项目的 BookStoreApplicationAutoMapperProfile.cs 文件中加入一个新的映射定义.

  1. CreateMap<Author, AuthorLookupDto>();

单元测试

因为修改了 AuthorAppService, 一些单元测试失败了. 打开 Acme.BookStore.Application.Tests 项目的 Books 目录中的 BookAppService_Tests, 修改成以下代码:

  1. using System;
  2. using System.Linq;
  3. using System.Threading.Tasks;
  4. using Acme.BookStore.Authors;
  5. using Shouldly;
  6. using Volo.Abp.Application.Dtos;
  7. using Volo.Abp.Validation;
  8. using Xunit;
  9. namespace Acme.BookStore.Books
  10. {
  11. public class BookAppService_Tests : BookStoreApplicationTestBase
  12. {
  13. private readonly IBookAppService _bookAppService;
  14. private readonly IAuthorAppService _authorAppService;
  15. public BookAppService_Tests()
  16. {
  17. _bookAppService = GetRequiredService<IBookAppService>();
  18. _authorAppService = GetRequiredService<IAuthorAppService>();
  19. }
  20. [Fact]
  21. public async Task Should_Get_List_Of_Books()
  22. {
  23. //Act
  24. var result = await _bookAppService.GetListAsync(
  25. new PagedAndSortedResultRequestDto()
  26. );
  27. //Assert
  28. result.TotalCount.ShouldBeGreaterThan(0);
  29. result.Items.ShouldContain(b => b.Name == "1984" &&
  30. b.AuthorName == "George Orwell");
  31. }
  32. [Fact]
  33. public async Task Should_Create_A_Valid_Book()
  34. {
  35. var authors = await _authorAppService.GetListAsync(new GetAuthorListDto());
  36. var firstAuthor = authors.Items.First();
  37. //Act
  38. var result = await _bookAppService.CreateAsync(
  39. new CreateUpdateBookDto
  40. {
  41. AuthorId = firstAuthor.Id,
  42. Name = "New test book 42",
  43. Price = 10,
  44. PublishDate = System.DateTime.Now,
  45. Type = BookType.ScienceFiction
  46. }
  47. );
  48. //Assert
  49. result.Id.ShouldNotBe(Guid.Empty);
  50. result.Name.ShouldBe("New test book 42");
  51. }
  52. [Fact]
  53. public async Task Should_Not_Create_A_Book_Without_Name()
  54. {
  55. var exception = await Assert.ThrowsAsync<AbpValidationException>(async () =>
  56. {
  57. await _bookAppService.CreateAsync(
  58. new CreateUpdateBookDto
  59. {
  60. Name = "",
  61. Price = 10,
  62. PublishDate = DateTime.Now,
  63. Type = BookType.ScienceFiction
  64. }
  65. );
  66. });
  67. exception.ValidationErrors
  68. .ShouldContain(err => err.MemberNames.Any(m => m == "Name"));
  69. }
  70. }
  71. }
  • 修改 Should_Get_List_Of_Books 中的断言条件, 从 b => b.Name == "1984" 修改为 b => b.Name == "1984" && b.AuthorName == "George Orwell", 检查用户名是否被填充.
  • 修改 Should_Create_A_Valid_Book 方法, 当新建图书时, 设置 AuthorId, 因为它现在是不可为空的了.

用户页面

图书列表

图书列表页面的修改很小. 打开 Acme.BookStore.Web 项目上的 Pages/Books/Index.js, 在 name and type 列之间加入如下列定义:

  1. ...
  2. {
  3. title: l('Name'),
  4. data: "name"
  5. },
  6. // ADDED the NEW AUTHOR NAME COLUMN
  7. {
  8. title: l('Author'),
  9. data: "authorName"
  10. },
  11. {
  12. title: l('Type'),
  13. data: "type",
  14. render: function (data) {
  15. return l('Enum:BookType:' + data);
  16. }
  17. },
  18. ...

运行应用程序, 你会在表格中看到 Author 列:

bookstore-added-author-to-book-list

新建模态窗口

打开 Acme.BookStore.Web 项目中的 Pages/Books/CreateModal.cshtml.cs, 修改文件内容为:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.ComponentModel.DataAnnotations;
  5. using System.Linq;
  6. using System.Threading.Tasks;
  7. using Acme.BookStore.Books;
  8. using Microsoft.AspNetCore.Mvc;
  9. using Microsoft.AspNetCore.Mvc.Rendering;
  10. using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
  11. namespace Acme.BookStore.Web.Pages.Books
  12. {
  13. public class CreateModalModel : BookStorePageModel
  14. {
  15. [BindProperty]
  16. public CreateBookViewModel Book { get; set; }
  17. public List<SelectListItem> Authors { get; set; }
  18. private readonly IBookAppService _bookAppService;
  19. public CreateModalModel(
  20. IBookAppService bookAppService)
  21. {
  22. _bookAppService = bookAppService;
  23. }
  24. public async Task OnGetAsync()
  25. {
  26. Book = new CreateBookViewModel();
  27. var authorLookup = await _bookAppService.GetAuthorLookupAsync();
  28. Authors = authorLookup.Items
  29. .Select(x => new SelectListItem(x.Name, x.Id.ToString()))
  30. .ToList();
  31. }
  32. public async Task<IActionResult> OnPostAsync()
  33. {
  34. await _bookAppService.CreateAsync(
  35. ObjectMapper.Map<CreateBookViewModel, CreateUpdateBookDto>(Book)
  36. );
  37. return NoContent();
  38. }
  39. public class CreateBookViewModel
  40. {
  41. [SelectItems(nameof(Authors))]
  42. [DisplayName("Author")]
  43. public Guid AuthorId { get; set; }
  44. [Required]
  45. [StringLength(128)]
  46. public string Name { get; set; }
  47. [Required]
  48. public BookType Type { get; set; } = BookType.Undefined;
  49. [Required]
  50. [DataType(DataType.Date)]
  51. public DateTime PublishDate { get; set; } = DateTime.Now;
  52. [Required]
  53. public float Price { get; set; }
  54. }
  55. }
  56. }
  • Book 属性的类型从 CreateUpdateBookDto 修改为这个文件中新定义的 CreateBookViewModel 类. 这个修改的主要动机是根据UI需求自定义模型类. 我们不希望在 CreateUpdateBookDto 类中使用UI相关的 [SelectItems(nameof(Authors))][DisplayName("Author")] 特性.
  • 新增 Authors 属性, 在 OnGetAsync 方法中使用前面定义的 IBookAppService.GetAuthorLookupAsync 方法填充它.
  • 修改 OnPostAsync 方法, 映射 CreateBookViewModel 对象到 CreateUpdateBookDto 对象, 因为 IBookAppService.CreateAsync 需要一个这种类型的参数.

编辑模态窗口

打开 Acme.BookStore.Web 项目中的 Pages/Books/EditModal.cshtml.cs, 修改文件内容为:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.ComponentModel.DataAnnotations;
  5. using System.Linq;
  6. using System.Threading.Tasks;
  7. using Acme.BookStore.Books;
  8. using Microsoft.AspNetCore.Mvc;
  9. using Microsoft.AspNetCore.Mvc.Rendering;
  10. using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
  11. namespace Acme.BookStore.Web.Pages.Books
  12. {
  13. public class EditModalModel : BookStorePageModel
  14. {
  15. [BindProperty]
  16. public EditBookViewModel Book { get; set; }
  17. public List<SelectListItem> Authors { get; set; }
  18. private readonly IBookAppService _bookAppService;
  19. public EditModalModel(IBookAppService bookAppService)
  20. {
  21. _bookAppService = bookAppService;
  22. }
  23. public async Task OnGetAsync(Guid id)
  24. {
  25. var bookDto = await _bookAppService.GetAsync(id);
  26. Book = ObjectMapper.Map<BookDto, EditBookViewModel>(bookDto);
  27. var authorLookup = await _bookAppService.GetAuthorLookupAsync();
  28. Authors = authorLookup.Items
  29. .Select(x => new SelectListItem(x.Name, x.Id.ToString()))
  30. .ToList();
  31. }
  32. public async Task<IActionResult> OnPostAsync()
  33. {
  34. await _bookAppService.UpdateAsync(
  35. Book.Id,
  36. ObjectMapper.Map<EditBookViewModel, CreateUpdateBookDto>(Book)
  37. );
  38. return NoContent();
  39. }
  40. public class EditBookViewModel
  41. {
  42. [HiddenInput]
  43. public Guid Id { get; set; }
  44. [SelectItems(nameof(Authors))]
  45. [DisplayName("Author")]
  46. public Guid AuthorId { get; set; }
  47. [Required]
  48. [StringLength(128)]
  49. public string Name { get; set; }
  50. [Required]
  51. public BookType Type { get; set; } = BookType.Undefined;
  52. [Required]
  53. [DataType(DataType.Date)]
  54. public DateTime PublishDate { get; set; } = DateTime.Now;
  55. [Required]
  56. public float Price { get; set; }
  57. }
  58. }
  59. }
  • Book 属性的类型从 CreateUpdateBookDto 修改为这个文件中新定义的 EditBookViewModel 类, 和我们前面所做的创建模型的修改一样.
  • 移动新类 EditBookViewModelId 属性.
  • 新增 Authors 属性, 在 OnGetAsync 方法中使用前面定义的 IBookAppService.GetAuthorLookupAsync 方法填充它.
  • 修改 OnPostAsync 方法, 映射 EditBookViewModel 对象到 CreateUpdateBookDto 对象, 因为 IBookAppService.UpdateAsync 需要一个这种类型的参数.

这些修改需要对 EditModal.cshtml 进行一些小修改. 移除 <abp-input asp-for="Id" /> 标签, 因为我们不再需要它了 (因为它被移动到 EditBookViewModel 中了). 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-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>

对象到对象映射配置

以下修改需要定义一些对象到对象映射. 打开 Acme.BookStore.Web 项目中的 BookStoreWebAutoMapperProfile.cs, 在构造函数中添加下列映射定义:

  1. CreateMap<Pages.Books.CreateModalModel.CreateBookViewModel, CreateUpdateBookDto>();
  2. CreateMap<BookDto, Pages.Books.EditModalModel.EditBookViewModel>();
  3. CreateMap<Pages.Books.EditModalModel.EditBookViewModel, CreateUpdateBookDto>();

你可以运行应用程序, 尝试新建或更新一本书. 你将在新建/更新表单上看到一个下拉框, 使用它指定图书的作者:

bookstore-added-authors-to-modals