Abp vnext Web应用程序开发教程 3 —— 创建、更新和删除书籍

关于本教程

本教程基于版本3.1

在本教程系列中,您将构建一个名为Acme.BookStore的基于ABPWeb应用程序。该应用程序用于管理书籍及其作者的列表。它是使用以下技术开发的:

  • 实体框架核心作为ORM提供者。
  • MVC/Razor页面作为UI框架。

本教程分为以下部分:

第1部分:创建服务器端

第2部分:图书列表页面

第3部分:创建、更新和删除书籍(此部分)

第4部分:集成测试

第5部分:授权

第6部分:作者:领领域层

第7部分:作者:数据库集成

第8部分:作者:应用程序层

第9部分:作者:用户界面

第10部分:书与作者的关系

下载源代码

MVC (Razor Pages) UI with EF Core

创建新书

在本节中,您将学习如何创建新的模态对话框形式来创建新书。模态对话框如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GZKn09IM-1601124148452)(bookstore-create-dialog-2.png)]

创建模态表单

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HH4l1JWn-1601124148453)(bookstore-add-create-dialog-v2.png)]

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标签帮助程序CreateBookViewModel模型类自动创建表单 。

  • 在这种情况下,abp-model attribute指示模型对象所在的Book属性。

  • abp-form-content标记帮助程序是呈现表单控件的占位符(仅当您在abp-dynamic-form标记中添加了其他内容(如此页面一样)时,它才是可选的,并且是必需的)。

提示:Layout应该是null,像在这个例子中做的那样,因为当通过AJAX加载时,我们不希望包括模态(modals)的所有布局。

添加“新书”按钮

打开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-right">
            <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-right">
                <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>

这将在表格的右上角添加一个名为New book的新按钮:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CjWfKqS6-1601124148454)(bookstore-new-book-button-2.png)]

打开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",
                    render: function (data) {
                        return luxon
                            .DateTime
                            .fromISO(data, {
                                locale: abp.localization.currentCulture.name
                            }).toLocaleString();
                    }
                },
                {
                    title: l('Price'),
                    data: "price"
                },
                {
                    title: l('CreationTime'), data: "creationTime",
                    render: function (data) {
                        return luxon
                            .DateTime
                            .fromISO(data, {
                                locale: abp.localization.currentCulture.name
                            }).toLocaleString(luxon.DateTime.DATETIME_SHORT);
                    }
                }
            ]
        })
    );

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

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

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

现在,您可以运行该应用程序,并使用新的模态形式添加一些新书。

更新书

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ush3qQLy-1601124148457)(bookstore-add-edit-dialog.png)]

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项目中打开BookStoreWebAutoMapperProfile.cs并按如下所示进行更改:

using AutoMapper;

namespace Acme.BookStore.Web
{
    public class BookStoreWebAutoMapperProfile : Profile
    {
        public BookStoreWebAutoMapperProfile()
        {
            CreateMap<BookDto, CreateUpdateBookDto>();
        }
    }
}
  • 我们刚刚添加了CreateMap<BookDto, CreateUpdateBookDto>();以定义此映射。

注意,由于我们仅在该层中需要映射定义,因此我们将其作为在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,除了:

  • 它包括一个abp-input用于Id存储Id编辑书的属性(这是一个隐藏的输入)。

  • 它Books/EditModal用作发布URL。

为表格添加 “操作(Actions)” 下拉菜单

我们将在名为Actions的表中添加一个下拉按钮。

打开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",
                    render: function (data) {
                        return luxon
                            .DateTime
                            .fromISO(data, {
                                locale: abp.localization.currentCulture.name
                            }).toLocaleString();
                    }
                },
                {
                    title: l('Price'),
                    data: "price"
                },
                {
                    title: l('CreationTime'), data: "creationTime",
                    render: function (data) {
                        return luxon
                            .DateTime
                            .fromISO(data, {
                                locale: abp.localization.currentCulture.name
                            }).toLocaleString(luxon.DateTime.DATETIME_SHORT);
                    }
                }
            ]
        })
    );

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

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

    $('#NewBookButton').click(function (e) {
        e.preventDefault();
        createModal.open();
    });
});
  • 添加了一个新的命名为editModalModalManager以打开编辑模态对话框。

  • 在本columnDefs节的开头添加了新列。此列用于“操作(Actions)”下拉按钮。

  • “Edit”操作只需调用editModal.open()即可打开编辑对话框。

  • 关闭编辑模态时,editModal.onResult(...)回调会刷新数据表。

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

最终的UI如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UWznCP1U-1601124148458)(bookstore-edit-button-2.png)]

删除书

打开Pages/Books/Index.js并将新项添加到rowAction items

{
    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": "Are you sure to delete the book '{0}'?",
"SuccessfullyDeleted": "Successfully deleted!"

最终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",
                    render: function (data) {
                        return luxon
                            .DateTime
                            .fromISO(data, {
                                locale: abp.localization.currentCulture.name
                            }).toLocaleString();
                    }
                },
                {
                    title: l('Price'),
                    data: "price"
                },
                {
                    title: l('CreationTime'), data: "creationTime",
                    render: function (data) {
                        return luxon
                            .DateTime
                            .fromISO(data, {
                                locale: abp.localization.currentCulture.name
                            }).toLocaleString(luxon.DateTime.DATETIME_SHORT);
                    }
                }
            ]
        })
    );

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

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

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

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

下一部分

请参阅本教程的下一部分

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值