You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
abp/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md

16 KiB

ASP.NET Core MVC 介绍 - 第一章

关于本教程

本教程中,你会创建一个用于管理书籍和书籍作者的程序.会用到 Entity Framework Core (EF Core)作为ORM(启动模板中预配置的ORM).

这是本教程所有章节中的第一章,下面是所有的章节:

你可以从这里下载本程序的源码.

创建项目

打开启动模板页并下载一个新的项目叫做Acme.BookStore.根据模板文档创建数据库并运行这个程序.

解决方案的结构

下面的图片展示了从启动模板创建的项目是如何分层的.

bookstore-visual-studio-solution

创建Book实体

领域层 定义实体(Acme.BookStore.Domain 中).这个项目最主要的实体就是Book:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Volo.Abp.Domain.Entities.Auditing;

namespace Acme.BookStore
{
    [Table("Books")]
    public class Book : AuditedAggregateRoot<Guid>
    {
        [Required]
        [StringLength(128)]
        public string Name { get; set; }

        public BookType Type { get; set; }

        public DateTime PublishDate { get; set; }

        public float Price { get; set; }
    }
}
  • ABP有两个基本的实体基类: AggregateRootEntity.Aggregate Root领域驱动设计(DDD) 中的概念.查看实体的更多信息和最佳实践.
  • Book实体继承了AuditedAggregateRoot,AuditedAggregateRoot类在AggregateRoot类的基础上添加了(CreationTime, CreatorId, LastModificationTime... 等.)审计属性.
  • Book实体的主键类型是Guid类型.
  • 使用 数据注解 为EF Core添加映射.或者你也可以使用 EF Core 自带的fluent mapping API.

BookType枚举

下面是所有要用到的BookType枚举:

namespace Acme.BookStore
{
    public enum BookType : byte
    {
        Undefined,
        Advanture,
        Biography,
        Dystopia,
        Fantastic,
        Horror,
        Science,
        ScienceFiction,
        Poetry
    }
}

将Book实体添加到DbContext中

EF Core需要你将实体和DbContext建立关联.最简单的做法是在Acme.BookStore.EntityFrameworkCore项目的BookStoreDbContext类中添加DbSet属性.如:

public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
{
    public DbSet<Book> Book { get; set; }
    ...
}

添加新的Migration并更新数据库

这个启动模板使用了EF Core Code First Migrations来创建并维护数据库结构.打开 Package Manager Console (PMC) (工具/Nuget包管理器菜单),选择 Acme.BookStore.EntityFrameworkCore作为默认的项目然后执行下面的命令:

bookstore-pmc-add-book-migration

这样就会在Migrations文件夹中创建一个新的migration类.然后执行Update-Database命令更新数据库结构.

PM> Update-Database

添加示例数据

Update-Database命令会在数据库中创建Books表.打开这个表添加几行数据,然后就可以把这些数据展示到页面上:

bookstore-books-table

创建应用服务

下一步是创建应用服务来管理(创建,列出,更新,删除...)书籍.

BookDto

Acme.BookStore.Application项目中添加一个名为BookDto的DTO类:

using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.AutoMapper;

namespace Acme.BookStore
{
    [AutoMapFrom(typeof(Book))]
    public class BookDto : AuditedEntityDto<Guid>
    {
        public string Name { get; set; }

        public BookType Type { get; set; }

        public DateTime PublishDate { get; set; }

        public float Price { get; set; }
    }
}
  • DTO类被用来在 基础设施层应用层 传递数据.查看DTO文档查看更多信息.
  • 为了在页面上展示书籍信息,BookDto被用来将书籍数据传递到基础设施层.
  • BookDto继承自 AuditedEntityDto<Guid>.跟上面定义的Book类一样具有一些审计属性.
  • [AutoMapFrom(typeof(Book))]用来创建从Book类到BookDto的映射.使用这种方法.你可以将Book对象自动转换成BookDto对象(而不是手动复制所有的属性).

CreateUpdateBookDto

Acme.BookStore.Application项目中创建一个名为CreateUpdateBookDto的DTO类:

using System;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.AutoMapper;

namespace Acme.BookStore
{
    [AutoMapTo(typeof(Book))]
    public class CreateUpdateBookDto
    {
        [Required]
        [StringLength(128)]
        public string Name { get; set; }

        [Required]
        public BookType Type { get; set; } = BookType.Undefined;

        [Required]
        public DateTime PublishDate { get; set; }

        [Required]
        public float Price { get; set; }
    }
}
  • 这个DTO类在创建和更新书籍的时候被使用,用来从页面获取图书信息.
  • 类中的属性定义了数据注解(如[Required])用来定义有效性验证.ABP会自动校验DTO的数据有效性.

IBookAppService

为应用服务定义一个名为 IBookAppService 的接口:

using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;

namespace Acme.BookStore
{
    public interface IBookAppService : 
        IAsyncCrudAppService< //定义了CRUD方法
            BookDto, //用来展示书籍
            Guid, //Book实体的主键
            PagedAndSortedResultRequestDto, //获取书籍的时候用于分页和排序
            CreateUpdateBookDto, //用于创建书籍
            CreateUpdateBookDto> //用户更新书籍
    {

    }
}
  • 为应用服务定义接口不是必须的,不过,我们推荐这么做.
  • IAsyncCrudAppService中定义了基础的 CRUD方法:GetAsync, GetListAsync, CreateAsync, UpdateAsyncDeleteAsync.不需要扩展它.取而代之,你可以继承空的IApplicationService接口定义你自己的方法.
  • IAsyncCrudAppService有一些变体,你可以为每一个方法使用单个或者多个的DTO.(译者注:意思是类似EntityDto和UpdateEntityDto可以用同一个,也可以分别单独指定 )

BookAppService

创建 BookAppService 并实现 IBookAppService接口:

using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;

namespace Acme.BookStore
{
    public class BookAppService : 
        AsyncCrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto,
                            CreateUpdateBookDto, CreateUpdateBookDto>,
        IBookAppService
    {
        public BookAppService(IRepository<Book, Guid> repository) 
            : base(repository)
        {

        }
    }
}
  • BookAppService继承了AsyncCrudAppService<...>.AsyncCrudAppService<...>实现了上面定义的CRUD方法.
  • BookAppService注入了IRepository<Book, Guid>,IRepository<Book, Guid>是默认为Book创建的仓储.ABP会自动为每一个聚合根(或实体)创建仓储.参考仓储.
  • BookAppService使用了 IObjectMapperBook转换成BookDto,将CreateUpdateBookDto转换成Book.启动模板中使用了AutoMapper作为映射工具.你可以像上面那样使用AutoMapFromAutoMapTo定义映射.查看AutoMapper继承获取更多信息.

自动生成API Controllers

你通常需要创建 Controllers 将应用服务暴露为 HTTP API.这样浏览器或第三方客户端可以通过AJAX的方式访问它们.

ABP可以 自动地 (../../AspNetCore/Auto-API-Controllers.md)将应用服务转换成MVC API Controllers.

Swagger UI

启动模板使用了Swashbuckle.AspNetCore库配置了swagger UI.运行程序并在浏览器中输入http://localhost:53929/swagger/.

你会看到一些内置的接口和Book的接口,它们都是REST风格的:

bookstore-swagger

动态JavaScript代理

通过AJAX的方式调用HTTP API接口是很常见的,你可以使用$.ajax或这其他的工具来调用接口.当然,ABP中提供了更好的方式.

ABP 自动 为所有的API接口创建了JavaScript 代理.因此,你可以像调用 JavaScript function一样调用任何接口.

在浏览器的开发者控制台中测试接口

你可以使用你最爱的浏览器的 开发者控制台 中轻松测试JavaScript代理.运行程序,并打开浏览器的 开发者工具(快捷键:F12),切换到 Console,输入下面的代码并回车:

acme.bookStore.book.getList({}).done(function (result) { console.log(result); });
  • acme.bookStoreBookAppService的命名空间,转换成了驼峰命名.
  • bookBookAppService转换后的名字(去除了AppService后缀并转成了驼峰命名).
  • getList是定义在AsyncCrudAppService基类中的GetListAsync方法转换后的名字(去除了Async后缀并转成了驼峰命名).
  • {}参数用来传递一个空的对象给GetListAsync方法.GetListAsync期望的参数是PagedAndSortedResultRequestDto类型,PagedAndSortedResultRequestDto类型中定义了分页和排序.
  • getList方法返回了一个promise.因此,你可以传递一个回调函数到done(或者then)方法中来获取服务返回的结果.

运行这段代码会产生下面的输出:

bookstore-test-js-proxy-getlist

你可以看到服务器返回的 book list.你还可以切换到开发者工具的 network 查看客户端和服务器的连接:

bookstore-test-js-proxy-getlist-network

我们使用create方法 创建一本新书:

acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05-24', price: 21.5 }).done(function (result) { console.log('successfully created the book with id: ' + result.id); });

你会看到控制台会显示类似这样的输出:

successfully created the book with id: f3f03580-c1aa-d6a9-072d-39e75c69f5c7

检查数据库表books中的数据,你会发现多了一行新数据,你也可以尝试get, updatedelete方法.

创建书籍页面

现在我们来创建一些可见的可用的东西,我们使用Razor Pages UI代替经典的MVC.微软也推荐使用Razor Pages UI

Acme.BookStore.Web项目的Pages文件夹下创建一个新的文件夹叫Books并添加一个名叫Index.cshtml的Razor Page.

bookstore-add-index-page

打开Index.cshtml并把内容修改成下面这样:

@page
@using Acme.BookStore.Pages.Books
@inherits Acme.BookStore.Pages.BookStorePageBase
@model IndexModel

<h2>Books</h2>
  • 改变Razor View Page Model默认的继承,使页面的 inherits 来自BookStorePageBase类(代替PageModel).BookStorePageBase类来自启动模板并提供了一些公开的属性/方法,这些属性/方法可以被所有的页面使用.

将Books页面添加到主菜单

打开Menus文件夹中的 BookStoreMenuContributor 类,在ConfigureMainMenuAsync方法的底部添加如下代码:

context.Menu.AddItem(
    new ApplicationMenuItem("BooksStore", l["Menu:BookStore"])
        .AddItem(new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books"))
);

本地化菜单

本地化的文本在Acme.BookStore.Domain项目的Localization/BookStore文件夹中.

bookstore-localization-files

打开en.json文件,为Menu:BookStoreMenu:Books添加本地化文本:

{
  "culture": "en",
  "texts": {
    //...
    "Menu:BookStore": "Book Store",
    "Menu:Books": "Books"
  }
}
  • ABP的本地化功能建立在ASP.NET Core's standard localization之上并增加了一些扩展.查看本地化文档.
  • 本地化中key的名字是随便定义的,你可以随意命名.我们喜欢为菜单添加Menu命名空间,以区别于其他的文本.如果文本没有在本地化文件中定义,就会 返回 本地的化的key(ASP.NET Core的标准做法).

运行程序就会看到菜单已经添加到了顶部:

bookstore-menu-items

点击菜单就会调转到新增书籍的页面.

书籍列表

我们会在页面上使用JQuery插件Datatables.net来展示列表.Datatables可以完全通过AJAX工作,所以它很快而且有良好的用户体验.启动模板中已经配置好了Datatables,因此你可以在你的页面中直接使用,不需要引用样式和脚本文件.

修改Index.cshtml

Pages/Books/Index.cshtml改成下面的样子:

@page
@using Acme.BookStore.Pages.Books
@inherits Acme.BookStore.Pages.BookStorePageBase
@model IndexModel
@section scripts
{
    <abp-script src="/Pages/Books/index.js" />
}
<abp-card>
    <abp-card-header>
        <h2>@L["Books"]</h2>
    </abp-card-header>
    <abp-card-body>
        <abp-table striped-rows="true" id="BooksTable">
            <thead>
                <tr>
                    <th>@L["Name"]</th>
                    <th>@L["Type"]</th>
                    <th>@L["PublishDate"]</th>
                    <th>@L["Price"]</th>
                    <th>@L["CreationTime"]</th>
                </tr>
            </thead>
        </abp-table>
    </abp-card-body>
</abp-card>
  • abp-script tag helper可以将添加外部的 scripts添加到页面中.它比标准的script标签多了很多额外的功能.它可以处理 最小化版本.查看bundling & minification 文档获取更多信息.
  • abp-cardabp-table 是为Twitter Bootstrap的card component封装的 tag helpers.ABP中有很多tag helpers,可以很方便的使用大多数bootstrap组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看tag helpers 文档.
  • 你可以像上面本地化菜单一样 本地化 列名.

添加脚本文件

Pages/Books/文件夹中创建 index.js文件

bookstore-index-js-file

index.js的内容如下:

$(function () {
    var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({
        ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
        columnDefs: [
            { data: "name" },
            { data: "type" },
            { data: "publishDate" },
            { data: "price" },
            { data: "creationTime" }
        ]
    }));
});
  • abp.libs.datatables.createAjax是帮助ABP的动态JavaScript API代理跟Datatable的格式相适应的辅助方法.
  • abp.libs.datatables.normalizeConfiguration是另一个辅助方法.不是必须的, 但是它通过为缺少的选项提供常规值来简化数据表配置.
  • acme.bookStore.book.getList是获取书籍列表的方法(上面已经介绍过了)
  • 查看 Datatable's 文档 了解更多配置项.

最终的页面如下:

bookstore-book-list

下一章

点击查看 下一章 的介绍.