项目

本文档有多个版本。请选择最适合您的选项。

UI
Database

Web 应用开发教程 - 第 9 部分:作者:用户界面

简介

本部分介绍如何为前几部分引入的 Author 实体创建 CRUD 页面。

作者列表页

Acme.BookStore.Web 项目的 Pages/Authors 文件夹下创建一个新的 Razor 页面 Index.cshtml,并按如下所示更改内容。

Index.cshtml

@page
@using Acme.BookStore.Localization
@using Acme.BookStore.Permissions
@using Acme.BookStore.Web.Pages.Authors
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<BookStoreResource> L
@inject IAuthorizationService AuthorizationService
@model IndexModel

@section scripts
{
    <abp-script src="/Pages/Authors/Index.js"/>
}

<abp-card>
    <abp-card-header>
        <abp-row>
            <abp-column size-md="_6">
                <abp-card-title>@L["Authors"]</abp-card-title>
            </abp-column>
            <abp-column size-md="_6" class="text-end">
                @if (await AuthorizationService
                    .IsGrantedAsync(BookStorePermissions.Authors.Create))
                {
                    <abp-button id="NewAuthorButton"
                                text="@L["NewAuthor"].Value"
                                icon="plus"
                                button-type="Primary"/>
                }
            </abp-column>
        </abp-row>
    </abp-card-header>
    <abp-card-body>
        <abp-table striped-rows="true" id="AuthorsTable"></abp-table>
    </abp-card-body>
</abp-card>

这是一个与我们之前创建的书籍页面类似的简单页面。它导入了一个将在下面介绍的 JavaScript 文件。

Index.cshtml.cs

using Microsoft.AspNetCore.Mvc.RazorPages;

namespace Acme.BookStore.Web.Pages.Authors;

public class IndexModel : PageModel
{
    public void OnGet()
    {

    }
}

Index.js

$(function () {
    var l = abp.localization.getResource('BookStore');
    var createModal = new abp.ModalManager(abp.appPath + 'Authors/CreateModal');
    var editModal = new abp.ModalManager(abp.appPath + 'Authors/EditModal');

    var dataTable = $('#AuthorsTable').DataTable(
        abp.libs.datatables.normalizeConfiguration({
            serverSide: true,
            paging: true,
            order: [[1, "asc"]],
            searching: false,
            scrollX: true,
            ajax: abp.libs.datatables.createAjax(acme.bookStore.authors.author.getList),
            columnDefs: [
                {
                    title: l('Actions'),
                    rowAction: {
                        items:
                            [
                                {
                                    text: l('Edit'),
                                    visible: 
                                        abp.auth.isGranted('BookStore.Authors.Edit'),
                                    action: function (data) {
                                        editModal.open({ id: data.record.id });
                                    }
                                },
                                {
                                    text: l('Delete'),
                                    visible: 
                                        abp.auth.isGranted('BookStore.Authors.Delete'),
                                    confirmMessage: function (data) {
                                        return l(
                                            'AuthorDeletionConfirmationMessage',
                                            data.record.name
                                        );
                                    },
                                    action: function (data) {
                                        acme.bookStore.authors.author
                                            .delete(data.record.id)
                                            .then(function() {
                                                abp.notify.info(
                                                    l('SuccessfullyDeleted')
                                                );
                                                dataTable.ajax.reload();
                                            });
                                    }
                                }
                            ]
                    }
                },
                {
                    title: l('Name'),
                    data: "name"
                },
                {
                    title: l('BirthDate'),
                    data: "birthDate",
                    render: function (data) {
                        return luxon
                            .DateTime
                            .fromISO(data, {
                                locale: abp.localization.currentCulture.name
                            }).toLocaleString();
                    }
                }
            ]
        })
    );

    createModal.onResult(function () {
        dataTable.ajax.reload();
    });

    editModal.onResult(function () {
        dataTable.ajax.reload();
    });

    $('#NewAuthorButton').click(function (e) {
        e.preventDefault();
        createModal.open();
    });
});

简单来说,这个 JavaScript 页面:

  • 创建了一个包含 ActionsNameBirthDate 列的数据表。
    • Actions 列用于添加 编辑删除 操作。
    • BirthDate 列提供了一个 render 函数,使用 luxon 库格式化 DateTime 值。
  • 使用 abp.ModalManager 来打开 创建编辑 模态窗体。

这段代码与之前创建的书籍页面非常相似,因此不再赘述。

本地化配置

此页面使用了一些我们需要声明的本地化键。打开 Acme.BookStore.Domain.Shared 项目 Localization/BookStore 文件夹下的 en.json 文件,并添加以下条目:

"Menu:Authors": "Authors",
"Authors": "Authors",
"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?",
"BirthDate": "Birth date",
"NewAuthor": "New author"

注意,我们添加了更多键。它们将在后续章节中使用。

添加到主菜单

打开 Acme.BookStore.Web 项目 Menus 文件夹下的 BookStoreMenuContributor.cs 文件,在 *书店* 菜单项下添加一个新的 *Authors* 菜单项。以下代码(在 ConfigureMainMenuAsync 方法中)展示了最终的部分代码:

context.Menu.AddItem(
    new ApplicationMenuItem(
        "BooksStore",
        l["Menu:BookStore"],
        icon: "fa fa-book"
    ).AddItem(
        new ApplicationMenuItem(
            "BooksStore.Books",
            l["Menu:Books"],
            url: "/Books"
        ).RequirePermissions(BookStorePermissions.Books.Default)
    ).AddItem( // 在 "书店" 菜单下添加新的 "AUTHORS" 菜单项
        new ApplicationMenuItem(
            "BooksStore.Authors",
            l["Menu:Authors"],
            url: "/Authors"
        ).RequirePermissions(BookStorePermissions.Authors.Default)
    )
);

运行应用程序

运行并登录到应用程序。由于你还没有权限,所以看不到菜单项。 进入 Identity/Roles 页面,点击 *Actions* 按钮,并为 admin 角色选择 *Permissions* 操作:

bookstore-author-permissions

如你所见,admin 角色目前还没有 *Author Management* 权限。点击复选框并保存模态窗体以授予必要的权限。刷新页面后,你将在主菜单的 *书店* 下看到 *Authors* 菜单项:

bookstore-authors-page

除了 *New author**Actions/Edit*(因为我们尚未实现它们)之外,该页面已完全可用。

提示:在定义新权限后,如果运行 .DbMigrator 控制台应用程序,它会自动将这些新权限授予 admin 角色,你无需手动授予权限。

创建模态窗体

Acme.BookStore.Web 项目的 Pages/Authors 文件夹下创建一个新的 Razor 页面 CreateModal.cshtml,并按如下所示更改内容。

CreateModal.cshtml

@page
@using Acme.BookStore.Localization
@using Acme.BookStore.Web.Pages.Authors
@using Microsoft.Extensions.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model CreateModalModel
@inject IStringLocalizer<BookStoreResource> L
@{
    Layout = null;
}
<form asp-page="/Authors/CreateModal">
    <abp-modal>
        <abp-modal-header title="@L["NewAuthor"].Value"></abp-modal-header>
        <abp-modal-body>
            <abp-input asp-for="Author.Name" />
            <abp-input asp-for="Author.BirthDate" />
            <abp-input asp-for="Author.ShortBio" />
        </abp-modal-body>
        <abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
    </abp-modal>
</form>

之前我们在书籍页面上使用了 ABP 的动态表单。我们本可以在这里使用相同的方法,但我们想展示如何手动完成。实际上,也不是完全手动,因为我们使用了 abp-input 标签助手来简化表单元素的创建。

你当然可以使用标准的 Bootstrap HTML 结构,但这需要编写大量代码。abp-input 会根据数据类型自动添加验证、本地化和其他标准元素。

CreateModal.cshtml.cs

using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Acme.BookStore.Authors;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;

namespace Acme.BookStore.Web.Pages.Authors;

public class CreateModalModel : BookStorePageModel
{
    [BindProperty]
    public CreateAuthorViewModel Author { get; set; }

    private readonly IAuthorAppService _authorAppService;

    public CreateModalModel(IAuthorAppService authorAppService)
    {
        _authorAppService = authorAppService;
    }

    public void OnGet()
    {
        Author = new CreateAuthorViewModel();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        var dto = ObjectMapper.Map<CreateAuthorViewModel, CreateAuthorDto>(Author);
        await _authorAppService.CreateAsync(dto);
        return NoContent();
    }

    public class CreateAuthorViewModel
    {
        [Required]
        [StringLength(AuthorConsts.MaxNameLength)]
        public string Name { get; set; } = string.Empty;

        [Required]
        [DataType(DataType.Date)]
        public DateTime BirthDate { get; set; }

        [TextArea]
        public string? ShortBio { get; set; }
    }
}

此页面模型类简单地注入并使用 IAuthorAppService 来创建新作者。与书籍创建模型类的主要区别在于,此类声明了一个新的类 CreateAuthorViewModel 作为视图模型,而不是重用 CreateAuthorDto

做出这个决定的主要原因是想向你展示如何在页面中使用不同的模型类。但还有一个好处:我们为类成员添加了两个在 CreateAuthorDto 中不存在的属性:

  • BirthDate 添加了 [DataType(DataType.Date)] 属性,这将在 UI 上为此属性显示一个日期选择器。
  • ShortBio 添加了 [TextArea] 属性,这将显示一个多行文本区域而不是标准文本框。

通过这种方式,你可以根据 UI 需求专门设计视图模型类,而无需修改 DTO。因此,我们使用 ObjectMapperCreateAuthorViewModel 映射到 CreateAuthorDto。为了能够做到这一点,你需要在 BookStoreWebMappers 类中定义一个新的映射配置:

[Mapper]
public partial class CreateAuthorViewModelToCreateAuthorDtoMapper : MapperBase<Pages.Authors.CreateModalModel.CreateAuthorViewModel, CreateAuthorDto>
{
    public override partial CreateAuthorDto Map(Pages.Authors.CreateModalModel.CreateAuthorViewModel source);
    public override partial void Map(Pages.Authors.CreateModalModel.CreateAuthorViewModel source, CreateAuthorDto destination);
}

当你再次运行应用程序时,“New author”按钮将按预期工作并打开一个新的模态窗体:

bookstore-new-author-modal

编辑模态窗体

Acme.BookStore.Web 项目的 Pages/Authors 文件夹下创建一个新的 Razor 页面 EditModal.cshtml,并按如下所示更改内容。

EditModal.cshtml

@page
@using Acme.BookStore.Localization
@using Acme.BookStore.Web.Pages.Authors
@using Microsoft.Extensions.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model EditModalModel
@inject IStringLocalizer<BookStoreResource> L
@{
    Layout = null;
}
<form asp-page="/Authors/EditModal">
    <abp-modal>
        <abp-modal-header title="@L["Update"].Value"></abp-modal-header>
        <abp-modal-body>
            <abp-input asp-for="Author.Id" />
            <abp-input asp-for="Author.Name" />
            <abp-input asp-for="Author.BirthDate" />
            <abp-input asp-for="Author.ShortBio" />
        </abp-modal-body>
        <abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
    </abp-modal>
</form>

EditModal.cshtml.cs

using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Acme.BookStore.Authors;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;

namespace Acme.BookStore.Web.Pages.Authors;

public class EditModalModel : BookStorePageModel
{
    [BindProperty]
    public EditAuthorViewModel Author { get; set; }

    private readonly IAuthorAppService _authorAppService;

    public EditModalModel(IAuthorAppService authorAppService)
    {
        _authorAppService = authorAppService;
    }

    public async Task OnGetAsync(Guid id)
    {
        var authorDto = await _authorAppService.GetAsync(id);
        Author = ObjectMapper.Map<AuthorDto, EditAuthorViewModel>(authorDto);
    }

    public async Task<IActionResult> OnPostAsync()
    {
        await _authorAppService.UpdateAsync(
            Author.Id,
            ObjectMapper.Map<EditAuthorViewModel, UpdateAuthorDto>(Author)
        );

        return NoContent();
    }

    public class EditAuthorViewModel
    {
        [HiddenInput]
        public Guid Id { get; set; }

        [Required]
        [StringLength(AuthorConsts.MaxNameLength)]
        public string Name { get; set; } = string.Empty;

        [Required]
        [DataType(DataType.Date)]
        public DateTime BirthDate { get; set; }

        [TextArea]
        public string? ShortBio { get; set; }
    }
}

此类与 CreateModal.cshtml.cs 类似,但有一些主要区别:

  • 使用 IAuthorAppService.GetAsync(...) 方法从应用层获取要编辑的作者。
  • EditAuthorViewModel 有一个额外的 Id 属性,该属性标有 [HiddenInput] 属性,用于为此属性创建隐藏输入。

此类需要添加两个对象映射声明,因此打开 BookStoreWebMappers 类并添加以下映射:

[Mapper]
public partial class AuthorDtoToEditAuthorViewModelMapper : MapperBase<AuthorDto, EditAuthorViewModel>
{
    public override partial EditAuthorViewModel Map(AuthorDto source);

    public override partial void Map(AuthorDto source, EditAuthorViewModel destination);
}

[Mapper]
public partial class EditAuthorViewModelToUpdateAuthorDtoMapper : MapperBase<Pages.Authors.EditModalModel.EditAuthorViewModel, UpdateAuthorDto>
{
    public override partial UpdateAuthorDto Map(Pages.Authors.EditModalModel.EditAuthorViewModel source);
    public override partial void Map(Pages.Authors.EditModalModel.EditAuthorViewModel source, UpdateAuthorDto destination);
}

就是这样!你可以运行应用程序并尝试编辑作者。


在本文档中