Web Application Development Tutorial - Part 3: Creating, Updating and Deleting Books
- Entity Framework Core as the ORM provider.
- MVC / Razor Pages as the UI Framework.
This tutorial is organized as the following parts;
- Part 1: Creating the server side
- Part 3: Creating, updating and deleting books (this part)
- Part 4: Integration tests
- Part 6: Authors: Domain layer
- Part 8: Authors: Application Layer
- Part 10: Book to Author Relation
This tutorial has multiple versions based on your UI and Database preferences. We’ve prepared two combinations of the source code to be downloaded:
Video Tutorial
This part is also recorded as a video tutorial and published on YouTube.
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 in the image below:
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 with the following code:
- This class is derived from the
BookStorePageModel
instead of standardPageModel
.BookStorePageModel
indirectly inherits thePageModel
and adds some common properties & methods that can be shared in your page model classes. [BindProperty]
attribute on theBook
property binds post request data to this property.- This class simply injects the
IBookAppService
in the constructor and calls theCreateAsync
method in theOnPostAsync
handler. - It creates a new
CreateUpdateBookDto
object in theOnGet
method. 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 theCreateUpdateBookDto
properties.
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-form
to automatically create the form from theCreateBookViewModel
model class. abp-model
attribute indicates the model object where it’s theBook
property in this case.abp-form-content
tag helper is a placeholder to render the form controls (it is optional and needed only if you have added some other content in theabp-dynamic-form
tag, just like in this page).
Add the “New book” Button
<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>
The final content of the 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-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>
This adds a new button called New book to the top-right of the table:
Open the Pages/Books/Index.js
and add the following code just after the Datatable
configuration:
abp.ModalManager
is a helper class to manage modals in 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
should be like that:
$(function () {
var l = abp.localization.getResource('BookStore');
var dataTable = $('#BooksTable').DataTable(
abp.libs.datatables.normalizeConfiguration({
serverSide: 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",
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();
});
});
Now, you can run the application and add some new books using the new modal form.
Create a new razor page, named EditModal.cshtml
under the Pages/Books
folder of the Acme.BookStore.Web
project:
Open the EditModal.cshtml.cs
file (EditModalModel
class) and replace 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.SupportsGet
is used to be able to getId
value from query string parameter of the request.- In the
OnGetAsync
method, we getBookDto
from theBookAppService
and this is being mapped to the DTO objectCreateUpdateBookDto
. - The
OnPostAsync
usesBookAppService.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
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.
Replace EditModal.cshtml
content with the following content:
This page is very similar to the CreateModal.cshtml
, except:
- It includes an
abp-input
for theId
property to store of the editing book (which is a hidden input). - It uses
Books/EditModal
as 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
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",
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();
});
});
- Added a new
ModalManager
namededitModal
to open the edit modal dialog. - Added a new column at the beginning of the
columnDefs
section. This column is used for the “Actions“ dropdown button. - “Edit“ action simply calls
editModal.open()
to open the edit dialog. 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:
Open the Pages/Books/Index.js
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();
});
}
}
confirmMessage
option is used to ask a confirmation question before executing theaction
.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}'?",
The final Index.js
content is shown below:
See the next part of this tutorial.