Web Application Development Tutorial - Part 3: Creating, Updating and Deleting Books
Creating a New Book
In this section, you will learn how to create a new modal dialog form to create a new book. The modal dialog will look like the image below:

Create the Modal Form
Create a new razor page named CreateModal.cshtml under the Pages/Books folder of the Acme.BookStore.Web project.

CreateModal.cshtml.cs
Open the CreateModal.cshtml.cs file (CreateModalModel class) and replace it with the following code:
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();
        }
    }
}
- This class is derived from the BookStorePageModelinstead of the standardPageModel.BookStorePageModelindirectly inherits thePageModeland adds some common properties & methods that can be shared in your page model classes.
- [BindProperty]attribute on the- Bookproperty binds post request data to this property.
- This class simply injects the IBookAppServicein the constructor and calls theCreateAsyncmethod in theOnPostAsynchandler.
- It creates a new CreateUpdateBookDtoobject in theOnGetmethod. ASP.NET Core can work without creating a new instance like that. However, it doesn't create an instance for you and if your class has some default value assignments or code execution in the class constructor, they won't work. For this case, we set default values for some of theCreateUpdateBookDtoproperties.
CreateModal.cshtml
Open the CreateModal.cshtml file and paste the code below:
@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>
- This modal uses abp-dynamic-formtag helper to automatically create the form from theCreateUpdateBookDtomodel class.
- abp-modelattribute indicates the model object where it's the- Bookproperty in this case.
- abp-form-contenttag helper is a placeholder to render the form controls (it is optional and needed only if you have added some other content in the- abp-dynamic-formtag, just like in this page).
Tip:
Layoutshould benulljust as done in this example since we don't want to include all the layout for the modals when they are loaded via AJAX.
Add the "New book" Button
Open the Pages/Books/Index.cshtml and set the content of abp-card-header tag as below:
<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>
The final content of Index.cshtml is shown below:
@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>
This adds a new button called New book to the top-right of the table:

Open the Pages/Books/Index.js file and add the following code right after the Datatable configuration:
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
createModal.onResult(function () {
    dataTable.ajax.reload();
});
$('#NewBookButton').click(function (e) {
    e.preventDefault();
    createModal.open();
});
- abp.ModalManageris a helper class to manage modals on the client side. It internally uses Twitter Bootstrap's standard modal, but abstracts many details by providing a simple API.
- createModal.onResult(...)used to refresh the data table after creating a new book.
- createModal.open();is used to open the model to create a new book.
The final content of the Index.js file should be like this:
$(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();
    });
});
Now, you can run the application and add some new books using the new modal form.
Updating a Book
Create a new razor page, named EditModal.cshtml under the Pages/Books folder of the Acme.BookStore.Web project:

EditModal.cshtml.cs
Open the EditModal.cshtml.cs file (EditModalModel class) and replace it with the following code:
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]and- [BindProperty]are standard ASP.NET Core MVC attributes.- SupportsGetis used to be able to get the- Idvalue from the query string parameter of the request.
- In the OnGetAsyncmethod, we get theBookDtofrom theBookAppServiceand this is being mapped to the DTO objectCreateUpdateBookDto.
- The OnPostAsyncusesBookAppService.UpdateAsync(...)to update the entity.
Mapping from BookDto to CreateUpdateBookDto
To be able to map the BookDto to CreateUpdateBookDto, configure a new mapping. To do this, open the BookStoreWebAutoMapperProfile.cs file in the Acme.BookStore.Web project and change it as shown below:
using AutoMapper;
namespace Acme.BookStore.Web;
public class BookStoreWebAutoMapperProfile : Profile
{
    public BookStoreWebAutoMapperProfile()
    {
        CreateMap<BookDto, CreateUpdateBookDto>();
    }
}
- We have just added CreateMap<BookDto, CreateUpdateBookDto>();to define this mapping.
Notice that we do the mapping definition in the web layer as a best practice since it is only needed in this layer.
EditModal.cshtml
Replace EditModal.cshtml content with the following content:
@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>
This page is very similar to CreateModal.cshtml, except:
- It includes an abp-inputfor theIdproperty to store theIdof the editing book (which is a hidden input).
- It uses Books/EditModalas the post URL.
Add "Actions" Dropdown to the Table
We will add a dropdown button to the table named Actions.
Open the Pages/Books/Index.js file and replace the content as below:
$(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();
    });
});
- Added a new ModalManagernamededitModalto open the edit modal dialog.
- Added a new column at the beginning of the columnDefssection. This column is used for the "Actions" dropdown button.
- The "Edit" action simply calls editModal.open()to open the edit dialog.
- The editModal.onResult(...)callback refreshes the data table when you close the edit modal.
You can run the application and edit any book by selecting the edit action on a book.
The final UI looks as below:

Notice that you don't see the "Actions" button in the figure below. Instead, you see an "Edit" button. ABP is smart enough to show a single simple button instead of a actions dropdown button when the dropdown has only a single item. After the next section, it will turn to a drop down button.
Deleting a Book
Open the Pages/Books/Index.js file and add a new item to the 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();
            });
    }
}
- The confirmMessageoption is used to ask a confirmation question before executing theaction.
- The acme.bookStore.books.book.delete(...)method makes an AJAX request to the server to delete a book.
- abp.notify.info()shows a notification after the delete operation.
Since we've used two new localization texts (BookDeletionConfirmationMessage and SuccessfullyDeleted) you need to add these to the localization file (en.json under the Localization/BookStore folder of the Acme.BookStore.Domain.Shared project):
"BookDeletionConfirmationMessage": "Are you sure to delete the book '{0}'?",
"SuccessfullyDeleted": "Successfully deleted!"
The final Index.js content is shown below:
$(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();
    });
});
You can run the application and try to delete a book.
 
                                             
                                    