41 KiB
Web应用程序开发教程 - 第十章: 图书到作者的关系
//[doc-params]
{
"UI": ["MVC","Blazor","BlazorServer","NG"],
"DB": ["EF","Mongo"]
}
关于本教程
在本系列教程中, 你将构建一个名为 Acme.BookStore
的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:
- {{DB_Text}} 做为ORM提供程序.
- {{UI_Value}} 做为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
简介
我们已经为图书管理应用程序创建了 图书
和 作者
功能. 然而, 这些实体间还没有关联.
在本章, 我们会在 作者
和 图书
实体间建立 1 对 N 的关系.
在图书实体中加入关系
打开 Acme.BookStore.Domain
项目中的 Books/Book.cs
, 在 Book
实体中加入下列属性:
public Guid AuthorId { get; set; }
{{if DB=="EF"}}
在本章中, 我们选择不在
Book
类中加入Author
实体的 导航属性 (例如public Author Author { get; set; }
). 这是为了遵循 DDD 最佳实践 (规则: 仅通过id引用其它聚合对象). 但是, 你自己可以添加这样的导航属性, 并为EF Core配置它. 这样, 你在获取图书和它们的作者时就不需要写join查询了(如同下面我们做的一样), 这会使代码更简洁一些.
{{end}}
数据库 & 数据迁移
为 Book
实体新增一个不为空的 AuthorId
属性. 但是, 数据库中已存在的图书怎么办? 它们没有 AuthorId
s, 当我们尝试运行应用程序时会出问题.
这是一个 典型的迁移问题, 解决方案依赖于你的具体情况;
- 如果你还没有发布应用程序到生产环境, 你可以直接删除数据库中的图书数据, 甚至你可以删除开发环境中的整个数据库.
- 你可以在数据迁移或生成种子阶段使用代码更新已有数据.
- 你可以手工处理这些数据.
我们倾向于 删除数据库 {{if DB=="EF"}}(你可以在 Package Manager 控制台中运行 Drop-Database
){{end}}, 因为这只是个示例项目, 数据丢失并不要紧. 因为这个主题不是关于ABP框架的, 我们不会深入所有的场景.
{{if DB=="EF"}}
更新 EF Core 映射
定位到 Acme.BookStore.EntityFrameworkCore
项目的 EntityFrameworkCore
文件夹下的 BookStoreDbContext
类的 OnModelCreating
方法, 修改 builder.Entity<Book>
部分如下:
builder.Entity<Book>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema);
b.ConfigureByConvention(); //auto configure for the base class props
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
// ADD THE MAPPING FOR THE RELATION
b.HasOne<Author>().WithMany().HasForeignKey(x => x.AuthorId).IsRequired();
});
新增 EF Core 迁移
启动解决方案被配置为使用 Entity Framework Core Code First Migrations. 因为我们修改了数据库映射配置, 我们需要新建一个迁移并应用于数据库.
在 Acme.BookStore.EntityFrameworkCore
项目的文件目录打开命令行终端, 输入命令:
dotnet ef migrations add Added_AuthorId_To_Book
这会创建一个新的迁移类, 在它的 Up
方法中使用下列方法:
migrationBuilder.AddColumn<Guid>(
name: "AuthorId",
table: "AppBooks",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.CreateIndex(
name: "IX_AppBooks_AuthorId",
table: "AppBooks",
column: "AuthorId");
migrationBuilder.AddForeignKey(
name: "FK_AppBooks_AppAuthors_AuthorId",
table: "AppBooks",
column: "AuthorId",
principalTable: "AppAuthors",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
AppBooks
表增加一个AuthorId
字段 .- 根据
AuthorId
字段新建一个索引. - 声明到
AppAuthors
表的外键.
如果你使用 Visual Studio, 可能希望在 Package Manager Console (PMC) 使用
Add-Migration Added_AuthorId_To_Book -c BookStoreDbContext
和Update-Database -Context BookStoreDbContext
命令. 如果这样, 保证 {{if UI=="MVC"}}Acme.BookStore.Web
{{else if UI=="BlazorServer"}}Acme.BookStore.Blazor
{{else if UI=="Blazor" || UI=="NG"}}Acme.BookStore.HttpApi.Host
{{end}} 是启动项目并且在PMC中Acme.BookStore.EntityFrameworkCore
是 默认项目 .
{{end}}
修改数据种子
因为 AuthorId
是 Book
实体的不可为空属性, 当前的数据种子代码不能工作. 打开 Acme.BookStore.Domain
项目中的 BookStoreDataSeederContributor
, 修改成以下代码:
using System;
using System.Threading.Tasks;
using Acme.BookStore.Authors;
using Acme.BookStore.Books;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore
{
public class BookStoreDataSeederContributor
: IDataSeedContributor, ITransientDependency
{
private readonly IRepository<Book, Guid> _bookRepository;
private readonly IAuthorRepository _authorRepository;
private readonly AuthorManager _authorManager;
public BookStoreDataSeederContributor(
IRepository<Book, Guid> bookRepository,
IAuthorRepository authorRepository,
AuthorManager authorManager)
{
_bookRepository = bookRepository;
_authorRepository = authorRepository;
_authorManager = authorManager;
}
public async Task SeedAsync(DataSeedContext context)
{
if (await _bookRepository.GetCountAsync() > 0)
{
return;
}
var orwell = await _authorRepository.InsertAsync(
await _authorManager.CreateAsync(
"George Orwell",
new DateTime(1903, 06, 25),
"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)."
)
);
var douglas = await _authorRepository.InsertAsync(
await _authorManager.CreateAsync(
"Douglas Adams",
new DateTime(1952, 03, 11),
"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'."
)
);
await _bookRepository.InsertAsync(
new Book
{
AuthorId = orwell.Id, // SET THE AUTHOR
Name = "1984",
Type = BookType.Dystopia,
PublishDate = new DateTime(1949, 6, 8),
Price = 19.84f
},
autoSave: true
);
await _bookRepository.InsertAsync(
new Book
{
AuthorId = douglas.Id, // SET THE AUTHOR
Name = "The Hitchhiker's Guide to the Galaxy",
Type = BookType.ScienceFiction,
PublishDate = new DateTime(1995, 9, 27),
Price = 42.0f
},
autoSave: true
);
}
}
}
唯一的区别是设置 Book
实体的 AuthorId
属性.
执行
DbMigrator
前删除已有图书或数据库. 参阅上面的 数据库 & 数据迁移 小节获取详细信息.
{{if DB=="EF"}}
你现在可以运行 .DbMigrator
控制台应用程序, 迁移 数据库 schema 并生成 种子 初始数据.
{{else if DB=="Mongo"}}
你现在可以运行 .DbMigrator
控制台应用程序, 迁移 数据库 schema 并生成 种子 初始数据.
{{end}}
应用层
我们将修改 BookAppService
, 支持作者关系.
数据传输对象
让我们从DTOs开始.
BookDto
打开 Acme.BookStore.Application.Contracts
项目的 Books
文件夹下的 BookDto
类, 添加如下属性:
public Guid AuthorId { get; set; }
public string AuthorName { get; set; }
最终的 BookDto
类应该如下:
using System;
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Books
{
public class BookDto : AuditedEntityDto<Guid>
{
public Guid AuthorId { get; set; }
public string AuthorName { get; set; }
public string Name { get; set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
}
}
CreateUpdateBookDto
打开 Acme.BookStore.Application.Contracts
项目的 Books
文件夹下的 CreateUpdateBookDto
类, 添加 AuthorId
属性:
public Guid AuthorId { get; set; }
AuthorLookupDto
在 Acme.BookStore.Application.Contracts
项目的 Books
文件夹下新建一个类 AuthorLookupDto
:
using System;
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Books
{
public class AuthorLookupDto : EntityDto<Guid>
{
public string Name { get; set; }
}
}
它会被一个将要添加到 IBookAppService
的新方法使用.
IBookAppService
打开 Acme.BookStore.Application.Contracts
项目的 Books
文件夹下的 IBookAppService
接口, 添加一个名为 GetAuthorLookupAsync
的新方法:
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Acme.BookStore.Books
{
public interface IBookAppService :
ICrudAppService< //Defines CRUD methods
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto> //Used to create/update a book
{
// ADD the NEW METHOD
Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync();
}
}
这个新方法将被UI用来获取作者列表, 填充一个下拉框. 使用这个下拉框选择图书作者.
BookAppService
打开 Acme.BookStore.Application
项目的 Books
文件夹下的 BookAppService
类, 更新为以下代码:
{{if DB=="EF"}}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading.Tasks;
using Acme.BookStore.Authors;
using Acme.BookStore.Permissions;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Books
{
[Authorize(BookStorePermissions.Books.Default)]
public class BookAppService :
CrudAppService<
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{
private readonly IAuthorRepository _authorRepository;
public BookAppService(
IRepository<Book, Guid> repository,
IAuthorRepository authorRepository)
: base(repository)
{
_authorRepository = authorRepository;
GetPolicyName = BookStorePermissions.Books.Default;
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Delete;
}
public override async Task<BookDto> GetAsync(Guid id)
{
//Get the IQueryable<Book> from the repository
var queryable = await Repository.GetQueryableAsync();
//Prepare a query to join books and authors
var query = from book in queryable
join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id
where book.Id == id
select new { book, author };
//Execute the query and get the book with author
var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query);
if (queryResult == null)
{
throw new EntityNotFoundException(typeof(Book), id);
}
var bookDto = ObjectMapper.Map<Book, BookDto>(queryResult.book);
bookDto.AuthorName = queryResult.author.Name;
return bookDto;
}
public override async Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
//Get the IQueryable<Book> from the repository
var queryable = await Repository.GetQueryableAsync();
//Prepare a query to join books and authors
var query = from book in queryable
join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id
select new {book, author};
//Paging
query = query
.OrderBy(NormalizeSorting(input.Sorting))
.Skip(input.SkipCount)
.Take(input.MaxResultCount);
//Execute the query and get a list
var queryResult = await AsyncExecuter.ToListAsync(query);
//Convert the query result to a list of BookDto objects
var bookDtos = queryResult.Select(x =>
{
var bookDto = ObjectMapper.Map<Book, BookDto>(x.book);
bookDto.AuthorName = x.author.Name;
return bookDto;
}).ToList();
//Get the total count with another query
var totalCount = await Repository.GetCountAsync();
return new PagedResultDto<BookDto>(
totalCount,
bookDtos
);
}
public async Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync()
{
var authors = await _authorRepository.GetListAsync();
return new ListResultDto<AuthorLookupDto>(
ObjectMapper.Map<List<Author>, List<AuthorLookupDto>>(authors)
);
}
private static string NormalizeSorting(string sorting)
{
if (sorting.IsNullOrEmpty())
{
return $"book.{nameof(Book.Name)}";
}
if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase))
{
return sorting.Replace(
"authorName",
"author.Name",
StringComparison.OrdinalIgnoreCase
);
}
return $"book.{sorting}";
}
}
}
我们做了以下修改:
- 给所有新建/覆写的方法增加
[Authorize(BookStorePermissions.Books.Default)]
进行授权(当授权特性应用于类时, 它对这个类的所有方法有效). - 注入
IAuthorRepository
, 从作者中查询. - 覆写基类
CrudAppService
的GetAsync
方法. 这个方法根据给定的id
返回单一BookDto
对象.- 使用一个简单的LINQ表达式关联图书和作者, 根据给定的图书id查询, 查询结果同时包含图书和作者.
- 使用
AsyncExecuter.FirstOrDefaultAsync(...)
执行查询并得到一个结果. 这是一种无需依赖database provider API, 使用异步LINQ扩展的方法. 参阅 repository文档以理解我们为什么使用它. - 如果请求的图书在数据库中不存在, 抛出一个
EntityNotFoundException
, 这会导致一个HTTP 404
(not found) 状态码. - 最后, 使用
ObjectMapper
创建一个BookDto
对象, 然后手工给AuthorName
赋值.
- 覆写
CrudAppService
基类的GetListAsync
方法, 返回图书列表. 逻辑与前一个方法类似, 所以很容易理解. - 新建一个方法:
GetAuthorLookupAsync
. 这个方法只是简单地获取所有作者. UI使用这个方法填充一个下拉框, 当编辑图书时用来选择作者.
{{else if DB=="Mongo"}}
using System;
using System.Collections.Generic;
using System.Linq.Dynamic.Core;
using System.Linq;
using System.Threading.Tasks;
using Acme.BookStore.Authors;
using Acme.BookStore.Permissions;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Books
{
[Authorize(BookStorePermissions.Books.Default)]
public class BookAppService :
CrudAppService<
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{
private readonly IAuthorRepository _authorRepository;
public BookAppService(
IRepository<Book, Guid> repository,
IAuthorRepository authorRepository)
: base(repository)
{
_authorRepository = authorRepository;
GetPolicyName = BookStorePermissions.Books.Default;
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Create;
}
public async override Task<BookDto> GetAsync(Guid id)
{
var book = await Repository.GetAsync(id);
var bookDto = ObjectMapper.Map<Book, BookDto>(book);
var author = await _authorRepository.GetAsync(book.AuthorId);
bookDto.AuthorName = author.Name;
return bookDto;
}
public async override Task<PagedResultDto<BookDto>>
GetListAsync(PagedAndSortedResultRequestDto input)
{
//Set a default sorting, if not provided
if (input.Sorting.IsNullOrWhiteSpace())
{
input.Sorting = nameof(Book.Name);
}
//Get the IQueryable<Book> from the repository
var queryable = await Repository.GetQueryableAsync();
//Get the books
var books = await AsyncExecuter.ToListAsync(
queryable
.OrderBy(input.Sorting)
.Skip(input.SkipCount)
.Take(input.MaxResultCount)
);
//Convert to DTOs
var bookDtos = ObjectMapper.Map<List<Book>, List<BookDto>>(books);
//Get a lookup dictionary for the related authors
var authorDictionary = await GetAuthorDictionaryAsync(books);
//Set AuthorName for the DTOs
bookDtos.ForEach(bookDto => bookDto.AuthorName =
authorDictionary[bookDto.AuthorId].Name);
//Get the total count with another query (required for the paging)
var totalCount = await Repository.GetCountAsync();
return new PagedResultDto<BookDto>(
totalCount,
bookDtos
);
}
public async Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync()
{
var authors = await _authorRepository.GetListAsync();
return new ListResultDto<AuthorLookupDto>(
ObjectMapper.Map<List<Author>, List<AuthorLookupDto>>(authors)
);
}
private async Task<Dictionary<Guid, Author>>
GetAuthorDictionaryAsync(List<Book> books)
{
var authorIds = books
.Select(b => b.AuthorId)
.Distinct()
.ToArray();
var queryable = await _authorRepository.GetQueryableAsync();
var authors = await AsyncExecuter.ToListAsync(
queryable.Where(a => authorIds.Contains(a.Id))
);
return authors.ToDictionary(x => x.Id, x => x);
}
}
}
我们做了以下修改:
- 给所有新建/覆写的方法增加
[Authorize(BookStorePermissions.Books.Default)]
进行授权(当授权特性应用于类时, 它对这个类的所有方法有效). - 注入
IAuthorRepository
, 从作者中查询. - 覆写基类
CrudAppService
的GetAsync
方法. 这个方法根据给定的id
返回单一BookDto
对象. - 覆写
CrudAppService
基类的GetListAsync
方法, 返回图书列表. 这里将从数据库中查询作者和在应用层设置作者名进行了分离. 你也可以创建一个自定义repository方法, 执行一个join查询, 或者利用MongoDB AP在一个查询中获取图书的作者, 这种做法性能会更好. - 新建一个方法:
GetAuthorLookupAsync
. 这个方法只是简单地获取所有作者. UI使用这个方法填充一个下拉框, 当编辑图书时用来选择作者.
{{end}}
对象到对象映射映射
引入 AuthorLookupDto
类, 在 GetAuthorLookupAsync
方法中使用对象映射. 所以, 我们需要在 Acme.BookStore.Application
项目的 BookStoreApplicationAutoMapperProfile.cs
文件中加入一个新的映射定义.
CreateMap<Author, AuthorLookupDto>();
单元测试
因为修改了 AuthorAppService
, 一些单元测试失败了. 打开 Acme.BookStore.Application.Tests
项目的 Books
目录中的 BookAppService_Tests
, 修改成以下代码:
using System;
using System.Linq;
using System.Threading.Tasks;
using Acme.BookStore.Authors;
using Shouldly;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Validation;
using Xunit;
namespace Acme.BookStore.Books
{ {{if DB=="Mongo"}}
[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}}
public class BookAppService_Tests : BookStoreApplicationTestBase
{
private readonly IBookAppService _bookAppService;
private readonly IAuthorAppService _authorAppService;
public BookAppService_Tests()
{
_bookAppService = GetRequiredService<IBookAppService>();
_authorAppService = GetRequiredService<IAuthorAppService>();
}
[Fact]
public async Task Should_Get_List_Of_Books()
{
//Act
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "1984" &&
b.AuthorName == "George Orwell");
}
[Fact]
public async Task Should_Create_A_Valid_Book()
{
var authors = await _authorAppService.GetListAsync(new GetAuthorListDto());
var firstAuthor = authors.Items.First();
//Act
var result = await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
AuthorId = firstAuthor.Id,
Name = "New test book 42",
Price = 10,
PublishDate = System.DateTime.Now,
Type = BookType.ScienceFiction
}
);
//Assert
result.Id.ShouldNotBe(Guid.Empty);
result.Name.ShouldBe("New test book 42");
}
[Fact]
public async Task Should_Not_Create_A_Book_Without_Name()
{
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () =>
{
await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
});
exception.ValidationErrors
.ShouldContain(err => err.MemberNames.Any(m => m == "Name"));
}
}
}
- 修改
Should_Get_List_Of_Books
中的断言条件, 从b => b.Name == "1984"
修改为b => b.Name == "1984" && b.AuthorName == "George Orwell"
, 检查用户名是否被填充. - 修改
Should_Create_A_Valid_Book
方法, 当新建图书时, 设置AuthorId
, 因为它现在是不可为空的了.
用户页面
{{if UI=="MVC"}}
图书列表
图书列表页面的修改很小. 打开 Acme.BookStore.Web
项目上的 Pages/Books/Index.js
, 在 name
and type
列之间加入如下列定义:
...
{
title: l('Name'),
data: "name"
},
// ADDED the NEW AUTHOR NAME COLUMN
{
title: l('Author'),
data: "authorName"
},
{
title: l('Type'),
data: "type",
render: function (data) {
return l('Enum:BookType:' + data);
}
},
...
运行应用程序, 你会在表格中看到 Author 列:
新建模态窗口
打开 Acme.BookStore.Web
项目中的 Pages/Books/CreateModal.cshtml.cs
, 修改文件内容为:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Acme.BookStore.Books;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
namespace Acme.BookStore.Web.Pages.Books
{
public class CreateModalModel : BookStorePageModel
{
[BindProperty]
public CreateBookViewModel Book { get; set; }
public List<SelectListItem> Authors { get; set; }
private readonly IBookAppService _bookAppService;
public CreateModalModel(
IBookAppService bookAppService)
{
_bookAppService = bookAppService;
}
public async Task OnGetAsync()
{
Book = new CreateBookViewModel();
var authorLookup = await _bookAppService.GetAuthorLookupAsync();
Authors = authorLookup.Items
.Select(x => new SelectListItem(x.Name, x.Id.ToString()))
.ToList();
}
public async Task<IActionResult> OnPostAsync()
{
await _bookAppService.CreateAsync(
ObjectMapper.Map<CreateBookViewModel, CreateUpdateBookDto>(Book)
);
return NoContent();
}
public class CreateBookViewModel
{
[SelectItems(nameof(Authors))]
[DisplayName("Author")]
public Guid AuthorId { get; set; }
[Required]
[StringLength(128)]
public string Name { get; set; }
[Required]
public BookType Type { get; set; } = BookType.Undefined;
[Required]
[DataType(DataType.Date)]
public DateTime PublishDate { get; set; } = DateTime.Now;
[Required]
public float Price { get; set; }
}
}
}
- 将
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
, 修改文件内容为:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Acme.BookStore.Books;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
namespace Acme.BookStore.Web.Pages.Books
{
public class EditModalModel : BookStorePageModel
{
[BindProperty]
public EditBookViewModel Book { get; set; }
public List<SelectListItem> Authors { get; set; }
private readonly IBookAppService _bookAppService;
public EditModalModel(IBookAppService bookAppService)
{
_bookAppService = bookAppService;
}
public async Task OnGetAsync(Guid id)
{
var bookDto = await _bookAppService.GetAsync(id);
Book = ObjectMapper.Map<BookDto, EditBookViewModel>(bookDto);
var authorLookup = await _bookAppService.GetAuthorLookupAsync();
Authors = authorLookup.Items
.Select(x => new SelectListItem(x.Name, x.Id.ToString()))
.ToList();
}
public async Task<IActionResult> OnPostAsync()
{
await _bookAppService.UpdateAsync(
Book.Id,
ObjectMapper.Map<EditBookViewModel, CreateUpdateBookDto>(Book)
);
return NoContent();
}
public class EditBookViewModel
{
[HiddenInput]
public Guid Id { get; set; }
[SelectItems(nameof(Authors))]
[DisplayName("Author")]
public Guid AuthorId { get; set; }
[Required]
[StringLength(128)]
public string Name { get; set; }
[Required]
public BookType Type { get; set; } = BookType.Undefined;
[Required]
[DataType(DataType.Date)]
public DateTime PublishDate { get; set; } = DateTime.Now;
[Required]
public float Price { get; set; }
}
}
}
- 将
Book
属性的类型从CreateUpdateBookDto
修改为这个文件中新定义的EditBookViewModel
类, 和我们前面所做的创建模型的修改一样. - 移动新类
EditBookViewModel
的Id
属性. - 新增
Authors
属性, 在OnGetAsync
方法中使用前面定义的IBookAppService.GetAuthorLookupAsync
方法填充它. - 修改
OnPostAsync
方法, 映射EditBookViewModel
对象到CreateUpdateBookDto
对象, 因为IBookAppService.UpdateAsync
需要一个这种类型的参数.
这些修改需要对 EditModal.cshtml
进行一些小修改. 移除 <abp-input asp-for="Id" />
标签, 因为我们不再需要它了 (因为它被移动到 EditBookViewModel
中了). EditModal.cshtml
的最终内容应为:
@page
@using Acme.BookStore.Localization
@using Acme.BookStore.Web.Pages.Books
@using Microsoft.Extensions.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model EditModalModel
@inject IStringLocalizer<BookStoreResource> L
@{
Layout = null;
}
<abp-dynamic-form abp-model="Book" asp-page="/Books/EditModal">
<abp-modal>
<abp-modal-header title="@L["Update"].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>
对象到对象映射配置
以下修改需要定义一些对象到对象映射. 打开 Acme.BookStore.Web
项目中的 BookStoreWebAutoMapperProfile.cs
, 在构造函数中添加下列映射定义:
CreateMap<Pages.Books.CreateModalModel.CreateBookViewModel, CreateUpdateBookDto>();
CreateMap<BookDto, Pages.Books.EditModalModel.EditBookViewModel>();
CreateMap<Pages.Books.EditModalModel.EditBookViewModel, CreateUpdateBookDto>();
你可以运行应用程序, 尝试新建或更新一本书. 你将在新建/更新表单上看到一个下拉框, 使用它指定图书的作者:
{{else if UI=="NG"}}
生成服务代理
因为修改了 HTTP APIs, 你需要更新 Angular 客户端 服务代理. 运行 generate-proxy
命令前, 应用程序必须启动运行.
在 angular
文件夹中运行下面的命令 (你可能需要停止angular应用程序):
abp generate-proxy -t ng
这个命令将会更新 /src/app/proxy/
文件夹中的服务代理文件.
图书列表
图书列表页面的修改很小. 打开 /src/app/book/book.component.html
, 在 Name
和 Type
列之间加入以下列定义:
<ngx-datatable-column
[name]="'::Author' | abpLocalization"
prop="authorName"
[sortable]="false"
></ngx-datatable-column>
运行应用程序, 你会在表格中看到 作者 列:
新建/编辑 表单
下一步是添加作者选择下拉框到新建/编辑表单. 最终的页面如下图:
添加作者下拉框, 作为表单中的第一个元素.
打开 /src/app/book/book.component.ts
, 修改文件内容为:
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { BookService, BookDto, bookTypeOptions, AuthorLookupDto } from '@proxy/books';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-book',
templateUrl: './book.component.html',
styleUrls: ['./book.component.scss'],
providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class BookComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
form: FormGroup;
selectedBook = {} as BookDto;
authors$: Observable<AuthorLookupDto[]>;
bookTypes = bookTypeOptions;
isModalOpen = false;
constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder,
private confirmation: ConfirmationService
) {
this.authors$ = bookService.getAuthorLookup().pipe(map((r) => r.items));
}
ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getList(query);
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}
createBook() {
this.selectedBook = {} as BookDto;
this.buildForm();
this.isModalOpen = true;
}
editBook(id: string) {
this.bookService.get(id).subscribe((book) => {
this.selectedBook = book;
this.buildForm();
this.isModalOpen = true;
});
}
buildForm() {
this.form = this.fb.group({
authorId: [this.selectedBook.authorId || null, Validators.required],
name: [this.selectedBook.name || null, Validators.required],
type: [this.selectedBook.type || null, Validators.required],
publishDate: [
this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null,
Validators.required,
],
price: [this.selectedBook.price || null, Validators.required],
});
}
save() {
if (this.form.invalid) {
return;
}
const request = this.selectedBook.id
? this.bookService.update(this.selectedBook.id, this.form.value)
: this.bookService.create(this.form.value);
request.subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.list.get();
});
}
delete(id: string) {
this.confirmation.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure').subscribe((status) => {
if (status === Confirmation.Status.confirm) {
this.bookService.delete(id).subscribe(() => this.list.get());
}
});
}
}
- 为
AuthorLookupDto
,Observable
和map
添加引用. - 在
selectedBook
后添加authors$: Observable<AuthorLookupDto[]>;
成员. - 在构造函数中添加
this.authors$ = bookService.getAuthorLookup().pipe(map((r) => r.items));
. - 在
buildForm()
函数中添加authorId: [this.selectedBook.authorId || null, Validators.required]
.
打开 /src/app/book/book.component.html
, 在图书名称表单组前加入下列表单组:
<div class="form-group">
<label for="author-id">Author</label><span> * </span>
<select class="form-control" id="author-id" formControlName="authorId">
<option [ngValue]="null">Select author</option>
<option [ngValue]="author.id" *ngFor="let author of authors$ | async">
{%{{{ author.name }}}%}
</option>
</select>
</div>
这就是全部了. 运行应用程序, 尝试新建或编辑一个作者.
{{end}}
{{if UI == "Blazor" || UI == "BlazorServer"}}
The Book List
在图书列表上显示 作者姓名 非常容易. 打开 Acme.BookStore.Blazor
项目中的 the /Pages/Books.razor
文件, 在 名称
(图书名) 列后加入下列 DataGridColumn
定义:
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.AuthorName)"
Caption="@L["Author"]"></DataGridColumn>
运行应用程序, 你将在表格上看到 作者 列:
新建图书模态窗口
在 Books.razor
文件的 @code
小节加入以下成员变量:
IReadOnlyList<AuthorLookupDto> authorList = Array.Empty<AuthorLookupDto>();
覆写 OnInitializedAsync
方法, 加入以下代码:
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
authorList = (await AppService.GetAuthorLookupAsync()).Items;
}
- 调用
base.OnInitializedAsync()
是有必要的, 因为AbpCrudPageBase
有一些初始化代码要执行.
最终的 @code
代码如下:
@code
{
//ADDED A NEW FIELD
IReadOnlyList<AuthorLookupDto> authorList = Array.Empty<AuthorLookupDto>();
public Books() // Constructor
{
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Delete;
}
//GET AUTHORS ON INITIALIZATION
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
authorList = (await AppService.GetAuthorLookupAsync()).Items;
}
}
最后, 在 新建 模态窗口的 ModalBody
加入以下 Field
定义, 在 Name
field之前, 做为第一项:
<Field>
<FieldLabel>@L["Author"]</FieldLabel>
<Select TValue="Guid" @bind-SelectedValue="@NewEntity.AuthorId">
<SelectItem TValue="Guid" Value="Guid.Empty">@L["PickAnAuthor"]</SelectItem>
@foreach (var author in authorList)
{
<SelectItem TValue="Guid" Value="@author.Id">
@author.Name
</SelectItem>
}
</Select>
</Field>
这需要在 en.json
文件中加入一个新的本地化键:
"PickAnAuthor": "Pick an author"
你可以运行应用程序, 当新建图书时, 可以看见 作者选择:
编辑图书模态窗口
在 编辑 模态窗口的 ModalBody
加入以下 Field
定义, 在 Name
field之前, 做为第一项:
<Field>
<FieldLabel>@L["Author"]</FieldLabel>
<Select TValue="Guid" @bind-SelectedValue="@EditingEntity.AuthorId">
@foreach (var author in authorList)
{
<SelectItem TValue="Guid" Value="@author.Id">
@author.Name
</SelectItem>
}
</Select>
</Field>
这就是全部了. 我们重用了定义在 新建 模态窗口中的 authorList
.
{{end}}