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;

    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.

    bookstore-add-create-dialog

    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 standard PageModel. BookStorePageModel indirectly inherits the PageModel and adds some common properties & methods that can be shared in your page model classes.
    • [BindProperty] attribute on the Book property binds post request data to this property.
    • This class simply injects the IBookAppService in the constructor and calls the CreateAsync method in the OnPostAsync handler.
    • It creates a new CreateUpdateBookDto object in the OnGet 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 the CreateUpdateBookDto properties.

    CreateModal.cshtml

    Open the CreateModal.cshtml file and paste the code below:

    1. @page
    2. @using Acme.BookStore.Localization
    3. @using Acme.BookStore.Web.Pages.Books
    4. @using Microsoft.Extensions.Localization
    5. @using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
    6. @model CreateModalModel
    7. @inject IStringLocalizer<BookStoreResource> L
    8. @{
    9. Layout = null;
    10. }
    11. <abp-dynamic-form abp-model="Book" asp-page="/Books/CreateModal">
    12. <abp-modal>
    13. <abp-modal-header title="@L["NewBook"].Value"></abp-modal-header>
    14. <abp-modal-body>
    15. <abp-form-content />
    16. </abp-modal-body>
    17. <abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
    18. </abp-modal>
    19. </abp-dynamic-form>
    • This modal uses abp-dynamic-form to automatically create the form from the CreateBookViewModel model class.
    • abp-model attribute indicates the model object where it’s the Book 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 the abp-dynamic-form tag, just like in this page).

    Add the “New book” Button

    1. <abp-card-header>
    2. <abp-row>
    3. <abp-column size-md="_6">
    4. <abp-card-title>@L["Books"]</abp-card-title>
    5. </abp-column>
    6. <abp-column size-md="_6" class="text-right">
    7. <abp-button id="NewBookButton"
    8. text="@L["NewBook"].Value"
    9. icon="plus"
    10. button-type="Primary"/>
    11. </abp-column>
    12. </abp-row>
    13. </abp-card-header>

    The final content of the Index.cshtml is shown below:

    1. @page
    2. @using Acme.BookStore.Localization
    3. @using Acme.BookStore.Web.Pages.Books
    4. @using Microsoft.Extensions.Localization
    5. @model IndexModel
    6. @inject IStringLocalizer<BookStoreResource> L
    7. @section scripts
    8. {
    9. <abp-script src="/Pages/Books/Index.js"/>
    10. }
    11. <abp-card>
    12. <abp-card-header>
    13. <abp-row>
    14. <abp-column size-md="_6">
    15. <abp-card-title>@L["Books"]</abp-card-title>
    16. </abp-column>
    17. <abp-column size-md="_6" class="text-right">
    18. <abp-button id="NewBookButton"
    19. text="@L["NewBook"].Value"
    20. icon="plus"
    21. button-type="Primary"/>
    22. </abp-column>
    23. </abp-row>
    24. </abp-card-header>
    25. <abp-card-body>
    26. <abp-table striped-rows="true" id="BooksTable"></abp-table>
    27. </abp-card-body>
    28. </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:

    1. $(function () {
    2. var l = abp.localization.getResource('BookStore');
    3. var dataTable = $('#BooksTable').DataTable(
    4. abp.libs.datatables.normalizeConfiguration({
    5. serverSide: true,
    6. order: [[1, "asc"]],
    7. searching: false,
    8. scrollX: true,
    9. ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList),
    10. columnDefs: [
    11. {
    12. title: l('Name'),
    13. data: "name"
    14. },
    15. {
    16. title: l('Type'),
    17. data: "type",
    18. return l('Enum:BookType:' + data);
    19. }
    20. },
    21. {
    22. title: l('PublishDate'),
    23. data: "publishDate",
    24. render: function (data) {
    25. return luxon
    26. .DateTime
    27. .fromISO(data, {
    28. locale: abp.localization.currentCulture.name
    29. }).toLocaleString();
    30. }
    31. },
    32. {
    33. title: l('Price'),
    34. data: "price"
    35. },
    36. {
    37. title: l('CreationTime'), data: "creationTime",
    38. render: function (data) {
    39. return luxon
    40. .DateTime
    41. .fromISO(data, {
    42. locale: abp.localization.currentCulture.name
    43. }).toLocaleString(luxon.DateTime.DATETIME_SHORT);
    44. }
    45. }
    46. ]
    47. })
    48. );
    49. var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
    50. createModal.onResult(function () {
    51. dataTable.ajax.reload();
    52. });
    53. $('#NewBookButton').click(function (e) {
    54. e.preventDefault();
    55. createModal.open();
    56. });
    57. });

    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:

    bookstore-add-edit-dialog

    Open the EditModal.cshtml.cs file (EditModalModel class) and replace with the following code:

    1. using System;
    2. using System.Threading.Tasks;
    3. using Acme.BookStore.Books;
    4. using Microsoft.AspNetCore.Mvc;
    5. namespace Acme.BookStore.Web.Pages.Books
    6. {
    7. public class EditModalModel : BookStorePageModel
    8. {
    9. [HiddenInput]
    10. [BindProperty(SupportsGet = true)]
    11. public Guid Id { get; set; }
    12. [BindProperty]
    13. public CreateUpdateBookDto Book { get; set; }
    14. private readonly IBookAppService _bookAppService;
    15. public EditModalModel(IBookAppService bookAppService)
    16. {
    17. _bookAppService = bookAppService;
    18. }
    19. public async Task OnGetAsync()
    20. {
    21. var bookDto = await _bookAppService.GetAsync(Id);
    22. Book = ObjectMapper.Map<BookDto, CreateUpdateBookDto>(bookDto);
    23. }
    24. public async Task<IActionResult> OnPostAsync()
    25. {
    26. await _bookAppService.UpdateAsync(Id, Book);
    27. return NoContent();
    28. }
    29. }
    30. }
    • [HiddenInput] and [BindProperty] are standard ASP.NET Core MVC attributes. SupportsGet is used to be able to get Id value from query string parameter of the request.
    • In the OnGetAsync method, we get BookDto from the BookAppService and this is being mapped to the DTO object CreateUpdateBookDto.
    • The OnPostAsync uses BookAppService.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:

    1. using AutoMapper;
    2. namespace Acme.BookStore.Web
    3. {
    4. public class BookStoreWebAutoMapperProfile : Profile
    5. {
    6. public BookStoreWebAutoMapperProfile()
    7. {
    8. CreateMap<BookDto, CreateUpdateBookDto>();
    9. }
    10. }
    • 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 the Id 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:

    1. $(function () {
    2. var l = abp.localization.getResource('BookStore');
    3. var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
    4. var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal');
    5. var dataTable = $('#BooksTable').DataTable(
    6. abp.libs.datatables.normalizeConfiguration({
    7. serverSide: true,
    8. paging: true,
    9. order: [[1, "asc"]],
    10. searching: false,
    11. scrollX: true,
    12. ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList),
    13. columnDefs: [
    14. {
    15. title: l('Actions'),
    16. rowAction: {
    17. items:
    18. [
    19. {
    20. text: l('Edit'),
    21. action: function (data) {
    22. editModal.open({ id: data.record.id });
    23. }
    24. }
    25. ]
    26. }
    27. },
    28. {
    29. title: l('Name'),
    30. data: "name"
    31. },
    32. {
    33. title: l('Type'),
    34. data: "type",
    35. render: function (data) {
    36. return l('Enum:BookType:' + data);
    37. }
    38. },
    39. {
    40. title: l('PublishDate'),
    41. data: "publishDate",
    42. render: function (data) {
    43. return luxon
    44. .DateTime
    45. .fromISO(data, {
    46. locale: abp.localization.currentCulture.name
    47. }).toLocaleString();
    48. }
    49. },
    50. {
    51. title: l('Price'),
    52. data: "price"
    53. },
    54. {
    55. title: l('CreationTime'), data: "creationTime",
    56. render: function (data) {
    57. return luxon
    58. .DateTime
    59. .fromISO(data, {
    60. locale: abp.localization.currentCulture.name
    61. }).toLocaleString(luxon.DateTime.DATETIME_SHORT);
    62. }
    63. }
    64. ]
    65. })
    66. );
    67. createModal.onResult(function () {
    68. dataTable.ajax.reload();
    69. });
    70. editModal.onResult(function () {
    71. dataTable.ajax.reload();
    72. });
    73. $('#NewBookButton').click(function (e) {
    74. e.preventDefault();
    75. createModal.open();
    76. });
    77. });
    • Added a new ModalManager named editModal 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:

    1. {
    2. text: l('Delete'),
    3. confirmMessage: function (data) {
    4. return l('BookDeletionConfirmationMessage', data.record.name);
    5. },
    6. action: function (data) {
    7. acme.bookStore.books.book
    8. .delete(data.record.id)
    9. .then(function() {
    10. abp.notify.info(l('SuccessfullyDeleted'));
    11. dataTable.ajax.reload();
    12. });
    13. }
    14. }
    • confirmMessage option is used to ask a confirmation question before executing the action.
    • 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):

    1. "BookDeletionConfirmationMessage": "Are you sure to delete the book '{0}'?",

    The final Index.js content is shown below:

    See the next part of this tutorial.