ASP.NET Core MVC Tutorial - Part II

About this Tutorial

This is the second part of the ASP.NET Core MVC tutorial series. See all parts:

You can access to the source code of the application from the GitHub repository.

You can also watch this video course prepared by an ABP community member, based on this tutorial.

Creating a New Book

In this section, you will learn how to create a new modal dialog form to create a new book. The result dialog will be like that:

bookstore-create-dialog

Create the Modal Form

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:

using System.Threading.Tasks;
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 async Task<IActionResult> OnPostAsync()
        {
            await _bookAppService.CreateAsync(Book);
            return NoContent();
        }
    }
}
  • This class is derived from the BookStorePageModel instead of standard PageModel. BookStorePageModel inherits the PageModel and adds some common properties/methods those can be used by your page model classes.
  • [BindProperty] attribute on the Book property binds post request data to this property.
  • This class simply injects the IBookAppService in its constructor and calls the CreateAsync method in the OnPostAsync handler.
CreateModal.cshtml

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

@page
@inherits Acme.BookStore.Web.Pages.BookStorePage
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model Acme.BookStore.Web.Pages.Books.CreateModalModel
@{
    Layout = null;
}
<abp-dynamic-form abp-model="Book" data-ajaxForm="true" 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 tag helper to automatically create the form from the CreateBookViewModel class.
    • abp-model attribute indicates the model object, the Book property in this case.
    • data-ajaxForm attribute makes the form submitting via AJAX, instead of a classic page post.
    • abp-form-content tag helper is a placeholder to render the form controls (this is optional and needed only if you added some other content in the abp-dynamic-form tag, just like in this page).

Add the "New book" Button

Open the Pages/Books/Index.cshtml and change the abp-card-header tag as shown below:

<abp-card-header>
    <abp-row>
        <abp-column size-md="_6">
            <h2>@L["Books"]</h2>
        </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>

Just added a New book button to the top right of the table:

bookstore-new-book-button

Open the pages/books/index.js and add the following code just 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.ModalManager is a helper class to open and manage modals in the client side. It internally uses Twitter Bootstrap's standard modal, but abstracts many details by providing a simple API.

Now, you can run the application and add new books using the new modal form.

Updating An Existing Book

Create a new razor page, named EditModal.cshtml under the Pages/Books folder of the Acme.BookStore.Web project:

bookstore-add-edit-dialog

EditModal.cshtml.cs

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

using System;
using System.Threading.Tasks;
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. Used SupportsGet to be able to get Id value from query string parameter of the request.
  • Mapped BookDto (received from the BookAppService.GetAsync) to CreateUpdateBookDto in the GetAsync method.
  • The OnPostAsync simply uses BookAppService.UpdateAsync to update the entity.

BookDto to CreateUpdateBookDto Mapping

In order to perform BookDto to CreateUpdateBookDto object mapping, 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>();
        }
    }
}
  • Just added CreateMap<BookDto, CreateUpdateBookDto>(); as the mapping definition.

EditModal.cshtml

Replace EditModal.cshtml content with the following content:

@page
@inherits Acme.BookStore.Web.Pages.BookStorePage
@using Acme.BookStore.Web.Pages.Books
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model EditModalModel
@{
    Layout = null;
}
<abp-dynamic-form abp-model="Book" data-ajaxForm="true" 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 the CreateModal.cshtml except;

  • It includes an abp-input for the Id property to store id of the editing book (which is a hidden input).
  • It uses Books/EditModal as the post URL and Update text as the modal header.

Add "Actions" Dropdown to the Table

We will add a dropdown button ("Actions") for each row of the table. The final UI looks like this:

bookstore-books-table-actions

Open the Pages/Books/Index.cshtml page and change the table section as shown below:

<abp-table striped-rows="true" id="BooksTable">
    <thead>
        <tr>
            <th>@L["Actions"]</th>
            <th>@L["Name"]</th>
            <th>@L["Type"]</th>
            <th>@L["PublishDate"]</th>
            <th>@L["Price"]</th>
            <th>@L["CreationTime"]</th>
        </tr>
    </thead>
</abp-table>
  • Just added a new th tag for the "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({
        processing: true,
        serverSide: true,
        paging: true,
        searching: false,
        autoWidth: false,
        scrollCollapse: true,
        order: [[1, "asc"]],
        ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
        columnDefs: [
            {
                rowAction: {
                    items:
                        [
                            {
                                text: l('Edit'),
                                action: function (data) {
                                    editModal.open({ id: data.record.id });
                                }
                            }
                        ]
                }
            },
            { data: "name" },
            { data: "type" },
            { data: "publishDate" },
            { data: "price" },
            { data: "creationTime" }
        ]
    }));

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

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

    $('#NewBookButton').click(function (e) {
        e.preventDefault();
        createModal.open();
    });
});
  • Used abp.localization.getResource('BookStore') to be able to use the same localization texts defined on the server side.
  • Added a new ModalManager named createModal to open the create modal dialog.
  • 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.
  • "New Book" action simply calls createModal.open to open the create dialog.
  • "Edit" action simply calls editModal.open to open the edit dialog. ` You can run the application and edit any book by selecting the edit action.

Deleting an Existing Book

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.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 the action.
  • Used acme.bookStore.book.delete javascript proxy function to perform an AJAX request to delete a book.
  • abp.notify.info is used to show a toastr notification just after the deletion.

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({
        processing: true,
        serverSide: true,
        paging: true,
        searching: false,
        autoWidth: false,
        scrollCollapse: true,
        order: [[1, "asc"]],
        ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
        columnDefs: [
            {
                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.book
                                    .delete(data.record.id)
                                    .then(function() {
                                        abp.notify.info(l('SuccessfullyDeleted'));
                                        dataTable.ajax.reload();
                                    });
                            }
                        }
                    ]
                }
            },
            { data: "name" },
            { data: "type" },
            { data: "publishDate" },
            { data: "price" },
            { data: "creationTime" }
        ]
    }));

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

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

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

Open the en.json in the Acme.BookStore.Domain.Shared project and add the following line:

"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?" 

Run the application and try to delete a book.

Next Part

See the next part of this tutorial.

In this document