项目

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

UI
Database

Web 应用开发教程 - 第 3 部分:创建、更新和删除书籍

创建新书

在本节中,你将学习如何创建一个新的模态对话框表单来创建新书。该模态对话框将如下图所示:

bookstore-create-dialog

创建模态表单

Acme.BookStore.Web 项目的 Pages/Books 文件夹下创建一个名为 CreateModal.cshtml 的新 Razor 页面。

bookstore-add-create-dialog

CreateModal.cshtml.cs

打开 CreateModal.cshtml.cs 文件(CreateModalModel 类),并用以下代码替换其内容:

using System.Threading.Tasks;
using Acme.BookStore.Books;
using Microsoft.AspNetCore.Mvc;

namespace Acme.BookStore.Web.Pages.Books
{
    public class CreateModalModel : BookStorePageModel
    {
        [BindProperty]
        public CreateUpdateBookDto Book { get; set; }

        private readonly IBookAppService _bookAppService;

        public CreateModalModel(IBookAppService bookAppService)
        {
            _bookAppService = bookAppService;
        }

        public void OnGet()
        {
            Book = new CreateUpdateBookDto();
        }

        public async Task<IActionResult> OnPostAsync()
        {
            await _bookAppService.CreateAsync(Book);
            return NoContent();
        }
    }
}
  • 该类派生自 BookStorePageModel 而不是标准的 PageModelBookStorePageModel 间接继承 PageModel,并添加了一些可以在你的页面模型类中共享的通用属性和方法。
  • Book 属性上的 [BindProperty] 特性将 POST 请求数据绑定到该属性。
  • 此类在构造函数中简单地注入了 IBookAppService,并在 OnPostAsync 处理程序中调用 CreateAsync 方法。
  • 它在 OnGet 方法中创建一个新的 CreateUpdateBookDto 对象。ASP.NET Core 在没有创建此类实例的情况下也可以工作。但是,它不会为你创建实例,如果你的类在构造函数中有一些默认值赋值或代码执行,它们将不会生效。因此,我们为 CreateUpdateBookDto 的某些属性设置了默认值。

CreateModal.cshtml

打开 CreateModal.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 CreateModalModel
@inject IStringLocalizer<BookStoreResource> L
@{
    Layout = null;
}
<abp-dynamic-form abp-model="Book" asp-page="/Books/CreateModal">
    <abp-modal>
        <abp-modal-header title="@L["NewBook"].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>
  • 此模态框使用 abp-dynamic-form 标记帮助程序 根据 CreateUpdateBookDto 模型类自动创建表单。
  • abp-model 属性指示模型对象,本例中是 Book 属性。
  • abp-form-content 标记帮助程序是一个占位符,用于渲染表单控件(它是可选的,仅当你需要在 abp-dynamic-form 标记内添加其他内容时才需要,就像本页面一样)。

提示: 如本例所示,Layout 应设置为 null,因为我们不希望模态框通过 AJAX 加载时包含所有布局。

添加“新建图书”按钮

打开 Pages/Books/Index.cshtml,并按如下所示设置 abp-card-header 标记的内容:

<abp-card-header>
    <abp-row>
        <abp-column size-md="_6">
            <abp-card-title>@L["Books"]</abp-card-title>
        </abp-column>
        <abp-column size-md="_6" class="text-end">
            <abp-button id="NewBookButton"
                        text="@L["NewBook"].Value"
                        icon="plus"
                        button-type="Primary"/>
        </abp-column>
    </abp-row>
</abp-card-header>

Index.cshtml 的最终内容如下所示:

@page
@using Acme.BookStore.Localization
@using Acme.BookStore.Web.Pages.Books
@using Microsoft.Extensions.Localization
@model IndexModel
@inject IStringLocalizer<BookStoreResource> L
@section scripts
{
    <abp-script src="/Pages/Books/Index.js"/>
}

<abp-card>
    <abp-card-header>
        <abp-row>
            <abp-column size-md="_6">
                <abp-card-title>@L["Books"]</abp-card-title>
            </abp-column>
            <abp-column size-md="_6" class="text-end">
                <abp-button id="NewBookButton"
                            text="@L["NewBook"].Value"
                            icon="plus"
                            button-type="Primary"/>
            </abp-column>
        </abp-row>
    </abp-card-header>
    <abp-card-body>
        <abp-table striped-rows="true" id="BooksTable"></abp-table>
    </abp-card-body>
</abp-card>

这将向表格的右上角添加一个名为新建图书的新按钮:

bookstore-new-book-button

打开 Pages/Books/Index.js 文件,并在 Datatable 配置之后添加以下代码:

var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');

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

$('#NewBookButton').click(function (e) {
    e.preventDefault();
    createModal.open();
});
  • abp.ModalManager 是一个辅助类,用于在客户端管理模态框。它在内部使用 Twitter Bootstrap 的标准模态框,但通过提供简单的 API 抽象了许多细节。
  • createModal.onResult(...) 用于在创建新书后刷新数据表。
  • createModal.open(); 用于打开创建新书的模态框。

Index.js 文件的最终内容应如下所示:

$(function () {
    var l = abp.localization.getResource('BookStore');

    var dataTable = $('#BooksTable').DataTable(
        abp.libs.datatables.normalizeConfiguration({
            serverSide: true,
            paging: true,
            order: [[1, "asc"]],
            searching: false,
            scrollX: true,
            ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList),
            columnDefs: [
                {
                    title: l('Name'),
                    data: "name"
                },
                {
                    title: l('Type'),
                    data: "type",
                    render: function (data) {
                        return l('Enum:BookType.' + data);
                    }
                },
                {
                    title: l('PublishDate'),
                    data: "publishDate",
                    dataFormat: "datetime"
                },
                {
                    title: l('Price'),
                    data: "price"
                },
                {
                    title: l('CreationTime'), data: "creationTime",
                    dataFormat: "datetime"
                }
            ]
        })
    );

    var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');

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

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

现在,你可以运行应用程序并使用新的模态表单添加一些新书。

更新书籍

Acme.BookStore.Web 项目的 Pages/Books 文件夹下创建一个名为 EditModal.cshtml 的新 Razor 页面:

bookstore-add-edit-dialog

EditModal.cshtml.cs

打开 EditModal.cshtml.cs 文件(EditModalModel 类),并用以下代码替换其内容:

using System;
using System.Threading.Tasks;
using Acme.BookStore.Books;
using Microsoft.AspNetCore.Mvc;

namespace Acme.BookStore.Web.Pages.Books;

public class EditModalModel : BookStorePageModel
{
    [HiddenInput]
    [BindProperty(SupportsGet = true)]
    public Guid Id { get; set; }

    [BindProperty]
    public CreateUpdateBookDto Book { get; set; }

    private readonly IBookAppService _bookAppService;

    public EditModalModel(IBookAppService bookAppService)
    {
        _bookAppService = bookAppService;
    }

    public async Task OnGetAsync()
    {
        var bookDto = await _bookAppService.GetAsync(Id);
        Book = ObjectMapper.Map<BookDto, CreateUpdateBookDto>(bookDto);
    }

    public async Task<IActionResult> OnPostAsync()
    {
        await _bookAppService.UpdateAsync(Id, Book);
        return NoContent();
    }
}
  • [HiddenInput][BindProperty] 是标准的 ASP.NET Core MVC 特性。SupportsGet 用于能够从请求的查询字符串参数中获取 Id 值。
  • OnGetAsync 方法中,我们从 BookAppService 获取 BookDto,并将其映射到 DTO 对象 CreateUpdateBookDto
  • OnPostAsync 使用 BookAppService.UpdateAsync(...) 来更新实体。

从 BookDto 映射到 CreateUpdateBookDto

为了能够将 BookDto 映射到 CreateUpdateBookDto,需要配置一个新的映射。为此,请打开 Acme.BookStore.Web 项目中的 BookStoreWebMappers.cs 文件,并按如下所示进行更改:

[Mapper]
public partial class BookDtoToCreateUpdateBookDtoMapper : MapperBase<BookDto, CreateUpdateBookDto>
{
    public override partial CreateUpdateBookDto Map(BookDto source);

    public override partial void Map(BookDto source, CreateUpdateBookDto destination);
}

注意: 作为最佳实践,我们在 Web 层中进行映射定义,因为只有该层需要它。

EditModal.cshtml

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-input asp-for="Id" />
            <abp-form-content />
        </abp-modal-body>
        <abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
    </abp-modal>
</abp-dynamic-form>

此页面与 CreateModal.cshtml 非常相似,不同之处在于:

  • 它包含一个用于 Id 属性的 abp-input,用于存储要编辑书籍的 Id(这是一个隐藏输入)。
  • 它使用 Books/EditModal 作为 POST URL。

向表格添加“操作”下拉菜单

我们将向表格添加一个名为操作的下拉按钮。

打开 Pages/Books/Index.js 文件,并按如下所示替换内容:

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

    var dataTable = $('#BooksTable').DataTable(
        abp.libs.datatables.normalizeConfiguration({
            serverSide: true,
            paging: true,
            order: [[1, "asc"]],
            searching: false,
            scrollX: true,
            ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList),
            columnDefs: [
                {
                    title: l('Actions'),
                    rowAction: {
                        items:
                            [
                                {
                                    text: l('Edit'),
                                    action: function (data) {
                                        editModal.open({ id: data.record.id });
                                    }
                                }
                            ]
                    }
                },
                {
                    title: l('Name'),
                    data: "name"
                },
                {
                    title: l('Type'),
                    data: "type",
                    render: function (data) {
                        return l('Enum:BookType.' + data);
                    }
                },
                {
                    title: l('PublishDate'),
                    data: "publishDate",
                    dataFormat: "datetime"
                },
                {
                    title: l('Price'),
                    data: "price"
                },
                {
                    title: l('CreationTime'), data: "creationTime",
                    dataFormat: "datetime"
                }
            ]
        })
    );

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

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

    $('#NewBookButton').click(function (e) {
        e.preventDefault();
        createModal.open();
    });
});
  • 添加了一个名为 editModal 的新 ModalManager 来打开编辑模态对话框。
  • columnDefs 部分的开头添加了一个新列。此列用于“操作”下拉按钮。
  • 编辑”操作只是调用 editModal.open() 来打开编辑对话框。
  • 当你关闭编辑模态框时,editModal.onResult(...) 回调会刷新数据表。

你可以运行应用程序并通过选择某本书的编辑操作来编辑任何书籍。

最终用户界面如下所示:

bookstore-books-table-actions

注意: 在下图中你看不到“操作”按钮。相反,你看到一个“编辑”按钮。当下拉菜单中只有一个项目时,ABP 足够智能,会显示一个简单的按钮而不是操作下拉按钮。在下一节之后,它将变成一个下拉按钮。

删除书籍

打开 Pages/Books/Index.js 文件,并向 rowActionitems 添加一个新项目:

{
    text: l('Delete'),
    confirmMessage: function (data) {
        return l('BookDeletionConfirmationMessage', data.record.name);
    },
    action: function (data) {
        acme.bookStore.books.book
            .delete(data.record.id)
            .then(function() {
                abp.notify.info(l('SuccessfullyDeleted'));
                dataTable.ajax.reload();
            });
    }
}
  • confirmMessage 选项用于在执行 action 之前询问确认问题。
  • acme.bookStore.books.book.delete(...) 方法向服务器发出 AJAX 请求以删除书籍。
  • abp.notify.info() 在删除操作后显示通知。

由于我们使用了两个新的本地化文本(BookDeletionConfirmationMessageSuccessfullyDeleted),你需要将它们添加到本地化文件中(Acme.BookStore.Domain.Shared 项目的 Localization/BookStore 文件夹下的 en.json):

"BookDeletionConfirmationMessage": "确定要删除书籍 '{0}' 吗?",
"SuccessfullyDeleted": "删除成功!"

最终的 Index.js 内容如下所示:

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

    var dataTable = $('#BooksTable').DataTable(
        abp.libs.datatables.normalizeConfiguration({
            serverSide: true,
            paging: true,
            order: [[1, "asc"]],
            searching: false,
            scrollX: true,
            ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList),
            columnDefs: [
                {
                    title: l('Actions'),
                    rowAction: {
                        items:
                            [
                                {
                                    text: l('Edit'),
                                    action: function (data) {
                                        editModal.open({ id: data.record.id });
                                    }
                                },
                                {
                                    text: l('Delete'),
                                    confirmMessage: function (data) {
                                        return l(
                                            'BookDeletionConfirmationMessage',
                                            data.record.name
                                        );
                                    },
                                    action: function (data) {
                                        acme.bookStore.books.book
                                            .delete(data.record.id)
                                            .then(function() {
                                                abp.notify.info(
                                                    l('SuccessfullyDeleted')
                                                );
                                                dataTable.ajax.reload();
                                            });
                                    }
                                }
                            ]
                    }
                },
                {
                    title: l('Name'),
                    data: "name"
                },
                {
                    title: l('Type'),
                    data: "type",
                    render: function (data) {
                        return l('Enum:BookType.' + data);
                    }
                },
                {
                    title: l('PublishDate'),
                    data: "publishDate",
                    dataFormat: "datetime"
                },
                {
                    title: l('Price'),
                    data: "price"
                },
                {
                    title: l('CreationTime'), data: "creationTime",
                    dataFormat: "datetime"
                }
            ]
        })
    );

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

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

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

你可以运行应用程序并尝试删除一本书。


在本文档中