概述
简介
本文是“如何上手ABP进行开发”系列文章的第二篇,原版标题为“使用ASP.Net Core,EF Core和ABP来创建分层Web应用”。
总目录:
Part I
Part II(this one)
本篇主要是新建了人员表,并在任务中增加一个人员属性,可以在任务列表显示指派的人员,也可以在新增任务时,选择相应的人员。
开发应用
创建Person实体
为了在Task实体中增加外键,引用Person信息,因此我们先定义一个Person实体
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
using Abp.Domain.Entities;
using Abp.Domain.Entities.Auditing;
namespace myAbpBasic.People
{
[Table("AppPersons")]
public class Person : AuditedEntity<Guid>
{
public const int MaxNameLength = 32;
[Required]
[StringLength(MaxNameLength)]
public string Name { get; set; }
public Person()
{
}
public Person(string name)
{
Name = name;
}
}
}
正如Part I开头所提到的,我们也可以使用int之外的其他类型作为主键,这里用Guid作为主键(仅仅是为了演示)。同时,Person类继承自AuditedEntity类,它包含了CreationTime,CreaterUserId,LastModificationTime,LastModifierUserId属性。
关联Person到Task实体
修改Task实体,增加Person相关字段
[Table("AppTasks")]
public class Task : Entity, IHasCreationTime
{
//...
[ForeignKey(nameof(AssignedPersonId))]
public Person AssignedPerson { get; set; }
public Guid? AssignedPersonId { get; set; }
public Task(string title, string description = null, Guid? assignedPersonId = null)
: this()
{
Title = title;
Description = description;
AssignedPersonId = assignedPersonId;
}
}
指派人员并不是必须的,是一个可选项。
在DbContext添加Person
using Abp.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using myAbpBasic.People;
using myAbpBasic.Tasks;
namespace myAbpBasic.EntityFrameworkCore
{
public class myAbpBasicDbContext : AbpDbContext
{
//Add DbSet properties for your entities...
public DbSet<Task> Tasks { get; set; }
public DbSet<Person> People { get; set; }
public myAbpBasicDbContext(DbContextOptions<myAbpBasicDbContext> options)
: base(options)
{
}
}
}
进行数据库迁移
在程序包管理器控制台执行Add-Migration "add_person"
文件内容如下:
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace myAbpBasic.Migrations
{
public partial class add_person : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "AssignedPersonId",
table: "AppTasks",
nullable: true);
migrationBuilder.CreateTable(
name: "AppPersons",
columns: table => new
{
Id = table.Column<Guid>(nullable: false),
CreationTime = table.Column<DateTime>(nullable: false),
CreatorUserId = table.Column<long>(nullable: true),
LastModificationTime = table.Column<DateTime>(nullable: true),
LastModifierUserId = table.Column<long>(nullable: true),
Name = table.Column<string>(maxLength: 32, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AppPersons", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_AppTasks_AssignedPersonId",
table: "AppTasks",
column: "AssignedPersonId");
migrationBuilder.AddForeignKey(
name: "FK_AppTasks_AppPersons_AssignedPersonId",
table: "AppTasks",
column: "AssignedPersonId",
principalTable: "AppPersons",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_AppTasks_AppPersons_AssignedPersonId",
table: "AppTasks");
migrationBuilder.DropTable(
name: "AppPersons");
migrationBuilder.DropIndex(
name: "IX_AppTasks_AssignedPersonId",
table: "AppTasks");
migrationBuilder.DropColumn(
name: "AssignedPersonId",
table: "AppTasks");
}
}
}
找到column: "AssignedPersonId"那一行,将ReferentialAction从Restrict修改为SetNull。这样当删除Person时,引用该Person的任务的AssignPersonId字段会设置为null。上述修改在本例中并不是关键点,只是说明一下可以在这里修改数据库的外键关联,一般我简易Update-Database之前都检查一遍自动生成的代码。
最后在Person表添加一些数据,并把一条任务指派给某个人员。
在Task列表返回人员信息
为了在前端收到人员信息,首先应该修改Dto模型,将数据库查询到的信息返回给展现层
[AutoMapFrom(typeof(Task))]
public class TaskListDto : EntityDto, IHasCreationTime
{
public string Title { get; set; }
public string Description { get; set; }
public DateTime CreationTime { get; set; }
public TaskState State { get; set; }
public Guid? AssignedPersonId { get; set; }
public string AssignedPersonName { get; set; }
}
修改AppService,增加 Include(t => t.AssignedPerson), 就能将AssignedPersonId关联的Person信息查询出来
public async Task<ListResultDto<TaskListDto>> GetAll(GetAllTasksInput input)
{
var tasks = await _taskRepository
.GetAll()
.Include(t => t.AssignedPerson)
.WhereIf(input.State.HasValue, t => t.State == input.State.Value)
.OrderByDescending(t => t.CreationTime)
.ToListAsync();
return new ListResultDto<TaskListDto>(
ObjectMapper.Map<List<TaskListDto>>(tasks)
);
}
这样,GetAll方法就能返回指派人员的信息,因为我们使用了AutoMapper,人员信息也会自动复制到Dto中。
有不明白的地方,建议先直接运行项目,断点查看下各个变量的值,也可以用日志记录。
修改单元测试以适应Person字段
修改单元测试,来验证Person信息是否已经存在于Task列表中,首先修改TestDataBuilder
using myAbpBasic.EntityFrameworkCore;
using myAbpBasic.People;
using myAbpBasic.Tasks;
namespace myAbpBasic.Tests.TestDatas
{
public class TestDataBuilder
{
private readonly myAbpBasicDbContext _context;
public TestDataBuilder(myAbpBasicDbContext context)
{
_context = context;
}
public void Build()
{
//create test data here...
var neo = new People.Person("Neo");
_context.People.Add(neo);
_context.SaveChanges();
_context.Tasks.AddRange(
new Task("Follow the white rabbit", "Follow the white rabbit in order to know the reality.", neo.Id),
new Task("Clean your room") {State = TaskState.Completed}
);
}
}
}
注意,新增了一个名为Neo的Person,并在新增第一条Task时,把PersonId设置为Neo.Id. 测试代码如下:
[Fact]
public async System.Threading.Tasks.Task Should_Get_All_Tasks()
{
//Act
var output = await _taskAppService.GetAll(new GetAllTasksInput());
//Assert
output.Items.Count.ShouldBe(2);
output.Items.Count(t => t.AssignedPersonName != null).ShouldBe(1);
}
注:Count方法位于System.Linq命名空间
在Task列表显示人员信息
修改Web项目的Task/Index.cshtml视图来展示人员信息。
@foreach (var task in Model.Tasks)
{
<li class="list-group-item">
<span class="pull-right label label-lg @Model.GetTaskLabel(task)">@L($"TaskState_{task.State}")</span>
<h4 class="list-group-item-heading">@task.Title</h4>
<div class="list-group-item-text">
@task.CreationTime.ToString("yyyy-MM-dd HH:mm:ss") | @(task.AssignedPersonName ?? L("Unassigned"))
</div>
</li>
}
运行项目,发现列表页已经存在了人员信息
新增Task的Create应用服务
我们已经可以展示任务列表了,但还没有创建任务的界面。首先我们修改一下ITaskAppService,增加一个Create方法
public interface ITaskAppService : IApplicationService
{
Task<ListResultDto<TaskListDto>> GetAll(GetAllTasksInput input);
System.Threading.Tasks.Task Create(CreateTaskInput input);
}
然后在TaskAppService中实现它
public async System.Threading.Tasks.Task Create(CreateTaskInput input)
{
var task = ObjectMapper.Map<Task>(input); //model transform
await _taskRepository.InsertAsync(task); //insert
}
Create方法自动将dto模型转换为Task实体,并写入数据库。CreateTaskInput这个Dto定义如下:
[AutoMapTo(typeof(Task))]
public class CreateTaskInput
{
[Required]
[StringLength(Task.MaxTitleLength)]
public string Title { get; set; }
[StringLength(Task.MaxDescriptionLength)]
public string Description { get; set; }
public Guid? AssignedPersonId { get; set; }
}
务必加上AutoMapTo属性,并加上所需的验证,如字符串长度。因为我们在Task Entity中定义了关于字段最大长度的常量,所以此处无需重复定义,直接使用即可。
创建Task代码的单元测试
[Fact]
public async System.Threading.Tasks.Task Should_Create_New_Task_With_Title()
{
await _taskAppService.Create(new CreateTaskInput
{
Title = "Newly created task #1"
});
UsingDbContext(context =>
{
var task1 = context.Tasks.FirstOrDefault(t => t.Title == "Newly created task #1");
task1.ShouldNotBeNull();
});
}
[Fact]
public async System.Threading.Tasks.Task Should_Create_New_Task_With_Title_And_Assigned_Person()
{
var neo = UsingDbContext(context => context.People.Single(p => p.Name == "Neo"));
await _taskAppService.Create(new CreateTaskInput
{
Title = "Newly created task #1",
AssignedPersonId = neo.Id
});
UsingDbContext(context =>
{
var task1 = context.Tasks.FirstOrDefault(t => t.Title == "Newly created task #1");
task1.ShouldNotBeNull();
task1.AssignedPersonId.ShouldBe(neo.Id);
});
}
[Fact]
public async System.Threading.Tasks.Task Should_Not_Create_New_Task_Without_Title()
{
await Assert.ThrowsAsync<AbpValidationException>(async () =>
{
await _taskAppService.Create(new CreateTaskInput
{
Title = null
});
});
}
第一项测试了创建带title的任务,第二项测试验证了创建指派了人员的任务,第三项测试了dto验证功能是否正常,因为Task Entity中Title字段被标记为Required。
新增Task页面
我们已经验证了Task服务一切正常,下面我们新建Create方法的action以及视图
using Microsoft.AspNetCore.Mvc;
using myAbpBasic.Tasks;
using myAbpBasic.Tasks.Dto;
using myAbpBasic.Web.Models;
using System.Threading.Tasks;
using myAbpBasic.Common;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Linq;
using Abp.Application.Services.Dto;
namespace myAbpBasic.Web.Controllers
{
public class TasksController : myAbpBasicControllerBase
{
private readonly ITaskAppService _taskAppService;
private readonly ILookupAppService _lookupAppService;
public TasksController(ITaskAppService taskAppService,
ILookupAppService lookupAppService)
{
_taskAppService = taskAppService;
_lookupAppService = lookupAppService;
}
//...
public async Task<ActionResult> Create()
{
var peopleSelectListItems = (await _lookupAppService.GetPeopleComboboxItems()).Items
.Select(p => p.ToSelectListItem())
.ToList();
peopleSelectListItems.Insert(0, new SelectListItem { Value = string.Empty, Text = L("Unassigned"), Selected = true });
return View(new CreateTaskViewModel(peopleSelectListItems));
}
}
}
这里注入ILoopupAppService是用于获取人员下拉框数据。这里我们可以直接注入IRepository<Person,Guid>,但这一会影响分层结构,不利于代码复用。下面是ILoopupAppService的定义以及实现:
ILoopupAppService
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Abp.Application.Services;
using Abp.Application.Services.Dto;
namespace myAbpBasic.Common
{
public interface ILookupAppService : IApplicationService
{
Task<ListResultDto<ComboboxItemDto>> GetPeopleComboboxItems();
}
}
LoopupAppService
using System;
using System.Linq;
using System.Threading.Tasks;
using Abp.Application.Services.Dto;
using Abp.Domain.Repositories;
using myAbpBasic.People;
namespace myAbpBasic.Common
{
public class LookupAppService : myAbpBasicAppServiceBase, ILookupAppService
{
private readonly IRepository<Person, Guid> _personRepository;
public LookupAppService(IRepository<Person, Guid> personRepository)
{
_personRepository = personRepository;
}
public async Task<ListResultDto<ComboboxItemDto>> GetPeopleComboboxItems()
{
var people = await _personRepository.GetAllListAsync();
return new ListResultDto<ComboboxItemDto>(
people.Select(p => new ComboboxItemDto(p.Id.ToString("D"), p.Name)).ToList()
);
}
}
}
ComboboxItemDto是ABP框架中定义的下拉框数据类,控制器将该类型Dto转换为SelectListItem并传递给视图。视图模型类如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace myAbpBasic.Web.Models
{
public class CreateTaskViewModel
{
public List<SelectListItem> People { get; set; }
public CreateTaskViewModel(List<SelectListItem> people)
{
People = people;
}
}
}
视图代码如下:
@using myAbpBasic.Web.Models
@model CreateTaskViewModel
@section scripts
{
<environment names="Development">
<script src="~/js/views/tasks/create.js"></script>
</environment>
<environment names="Staging,Production">
<script src="~/js/views/tasks/create.min.js"></script>
</environment>
}
<h2>
@L("NewTask")
</h2>
<form id="TaskCreationForm">
<div class="form-group">
<label for="Title">@L("Title")</label>
<input type="text" name="Title" class="form-control" placeholder="@L("Title")" required maxlength="@myAbpBasic.Tasks.Task.MaxTitleLength">
</div>
<div class="form-group">
<label for="Description">@L("Description")</label>
<input type="text" name="Description" class="form-control" placeholder="@L("Description")" maxlength="@myAbpBasic.Tasks.Task.MaxDescriptionLength">
</div>
<div class="form-group">
@Html.Label(L("AssignedPerson"))
@Html.DropDownList(
"AssignedPersonId",
Model.People,
new
{
@class = "form-control",
id = "AssignedPersonCombobox"
})
</div>
<button type="submit" class="btn btn-default">@L("Save")</button>
</form>
create.js代码如下:
(function($) {
$(function() {
var _$form = $('#TaskCreationForm');
_$form.find('input:first').focus();
_$form.validate();
_$form.find('button[type=submit]')
.click(function(e) {
e.preventDefault();
if (!_$form.valid()) {
return;
}
var input = _$form.serializeFormToObject();
abp.services.app.task.create(input)
.done(function() {
location.href = '/Tasks';
});
});
});
})(jQuery);
- 使用 JQuery Validation 插件,在加载页面表单时先进行验证,并在点击保存按钮时也进行验证
- 使用serializeFormToObject插件(在jquery-extensioins.js中定义,改脚本包含在_Layout.cshtml中)把表单数据转换为json对象
- 使用abp.services.task.create方法来调用TaskAppService.Create方法。这是ABP的 Application Services as Controllers 功能。允许我们从页面js代码中像调用js方法一样调用AppService的方法。
最后,我们在Task Index页,加上Create按钮
<h2>
@L("TaskList")
<span><a class="btn btn-primary btn-sm" asp-action="Create">@L("AddNew")</a></span>
<span class="pull-right">
@Html.DropDownListFor(
model => model.SelectedTaskState,
Model.GetTasksStateSelectListItems(LocalizationManager),
new
{
@class = "form-control",
id = "TaskStateCombobox"
})
</span>
</h2>
这时运行程序,已经可以添加任务,同时指派人员了。
移除Home,About页面
如果想移除某些页面,只需按以下步骤:
- 修改Home控制器的Index方法进行跳转
- 删除Views/Home文件夹
- 从StartUp/myAbpBasicNavigationProvider.cs类中移除不需要的菜单
- 从本土化翻译的json文件中删除不需要的字段
源码
https://github.com/cn421127/myAbpPractice
最后
以上就是知性皮皮虾为你收集整理的如何使用ABP开发二的全部内容,希望文章能够帮你解决如何使用ABP开发二所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复