如何使用ABP开发二

简介

本文是“如何上手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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值