Web Application Development Tutorial - Part 3: Creating, Updating and Deleting Books
About This Tutorial
In this tutorial series, you will build an ABP based web application named Acme.BookStore. This application is used to manage a list of books and their authors. It is developed using the following technologies:
- Entity Framework Core as the database provider.
- Blazor WebAssembly as the UI Framework.
This tutorial is organized as the following parts;
- Part 1: Creating the project and book list page
- Part 2: The book list page
- Part 3: Creating, updating and deleting books (this part)
- Part 4: Integration tests
- Part 5: Authorization
- Part 6: Authors: Domain layer
- Part 7: Authors: Database Integration
- Part 8: Authors: Application Layer
- Part 9: Authors: User Interface
- Part 10: Book to Author Relation
Download the Source Code
This tutorial has multiple versions based on your UI and Database preferences. We've prepared a few combinations of the source code to be downloaded:
Creating a New Book
In this section, you will learn how to create a new modal dialog form to create a new book. Since we've inherited from the AbpCrudPageBase, we only need to develop the view part.
Add "New Button" Button
Open the Books.razor and replace the <CardHeader> section with the following code:
<CardHeader>
    <Row Class="justify-content-between">
        <Column ColumnSize="ColumnSize.IsAuto">
            <h2>@L["Books"]</h2>
        </Column>
        <Column ColumnSize="ColumnSize.IsAuto">
            <Button Color="Color.Primary"
                    Clicked="OpenCreateModalAsync">@L["NewBook"]</Button>
        </Column>
    </Row>
</CardHeader>
This will change the card header by adding a "New book" button to the right side:

Now, we can add a modal that will be opened when we click to the button.
Book Creation Modal
Open the Books.razor and add the following code to the end of the page:
<Modal @ref="@CreateModal">
    <ModalBackdrop />
    <ModalContent IsCentered="true">
        <Form>
            <ModalHeader>
                <ModalTitle>@L["NewBook"]</ModalTitle>
                <CloseButton Clicked="CloseCreateModalAsync"/>
            </ModalHeader>
            <ModalBody>
                <Validations @ref="@CreateValidationsRef" Model="@NewEntity" ValidateOnLoad="false">
                    <Validation MessageLocalizer="@LH.Localize">
                        <Field>
                            <FieldLabel>@L["Name"]</FieldLabel>
                            <TextEdit @bind-Text="@NewEntity.Name">
                                <Feedback>
                                    <ValidationError/>
                                </Feedback>
                            </TextEdit>
                        </Field>
                    </Validation>
                    <Field>
                        <FieldLabel>@L["Type"]</FieldLabel>
                        <Select TValue="BookType" @bind-SelectedValue="@NewEntity.Type">
                            @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
                            {
                                <SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
                                    @L[$"Enum:BookType:{bookTypeValue}"]
                                </SelectItem>
                            }
                        </Select>
                    </Field>
                    <Field>
                        <FieldLabel>@L["PublishDate"]</FieldLabel>
                        <DateEdit TValue="DateTime" @bind-Date="NewEntity.PublishDate"/>
                    </Field>
                    <Field>
                        <FieldLabel>@L["Price"]</FieldLabel>
                        <NumericEdit TValue="float" @bind-Value="NewEntity.Price"/>
                    </Field>
                </Validations>
            </ModalBody>
            <ModalFooter>
                <Button Color="Color.Secondary"
                        Clicked="CloseCreateModalAsync">@L["Cancel"]</Button>
                <Button Color="Color.Primary"
                        Type="@ButtonType.Submit"
                        PreventDefaultOnSubmit="true"
                        Clicked="CreateEntityAsync">@L["Save"]</Button>
            </ModalFooter>
        </Form>
    </ModalContent>
</Modal>
This code requires a service; Inject the AbpBlazorMessageLocalizerHelper<T> at the top of the file, just before the @inherits... line:
@inject AbpBlazorMessageLocalizerHelper<BookStoreResource> LH
- The form implements validation and the - AbpBlazorMessageLocalizerHelperis used to simply localize the validation messages.
- CreateModalobject,- CloseCreateModalAsyncand- CreateEntityAsyncmethod are defined by the base class. See the Blazorise documentation if you want to understand the- Modaland the other components.
That's all. Run the application and try to add a new book:

Updating a Book
Editing a books is similar to the creating a new book.
Actions Dropdown
Open the Books.razor and add the following DataGridEntityActionsColumn section inside the DataGridColumns as the first item:
<DataGridEntityActionsColumn TItem="BookDto" @ref="@EntityActionsColumn">
    <DisplayTemplate>
        <EntityActions TItem="BookDto" EntityActionsColumn="@EntityActionsColumn">
            <EntityAction TItem="BookDto"
                          Text="@L["Edit"]"
                          Clicked="() => OpenEditModalAsync(context)" />
        </EntityActions>
    </DisplayTemplate>
</DataGridEntityActionsColumn>
- OpenEditModalAsyncis defined in the base class which takes the entity (book) to edit.
DataGridEntityActionsColumn component is used to show an "Actions" dropdown for each row in the DataGrid.  DataGridEntityActionsColumn shows a *single button* instead of a dropdown if there is only one available action inside it:

Edit Modal
We can now define a modal to edit the book. Add the following code to the end of the Books.razor page:
<Modal @ref="@EditModal">
    <ModalBackdrop />
    <ModalContent IsCentered="true">
        <Form>
            <ModalHeader>
                <ModalTitle>@EditingEntity.Name</ModalTitle>
                <CloseButton Clicked="CloseEditModalAsync"/>
            </ModalHeader>
            <ModalBody>
                <Validations @ref="@EditValidationsRef" Model="@NewEntity" ValidateOnLoad="false">
                    <Validation MessageLocalizer="@LH.Localize">
                        <Field>
                            <FieldLabel>@L["Name"]</FieldLabel>
                            <TextEdit @bind-Text="@EditingEntity.Name">
                                <Feedback>
                                    <ValidationError/>
                                </Feedback>
                            </TextEdit>
                        </Field>
                    </Validation>
                    <Field>
                        <FieldLabel>@L["Type"]</FieldLabel>
                        <Select TValue="BookType" @bind-SelectedValue="@EditingEntity.Type">
                            @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
                            {
                                <SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
                                    @L[$"Enum:BookType:{bookTypeValue}"]
                                </SelectItem>
                            }
                        </Select>
                    </Field>
                    <Field>
                        <FieldLabel>@L["PublishDate"]</FieldLabel>
                        <DateEdit TValue="DateTime" @bind-Date="EditingEntity.PublishDate"/>
                    </Field>
                    <Field>
                        <FieldLabel>@L["Price"]</FieldLabel>
                        <NumericEdit TValue="float" @bind-Value="EditingEntity.Price"/>
                    </Field>
                </Validations>
            </ModalBody>
            <ModalFooter>
                <Button Color="Color.Secondary"
                        Clicked="CloseEditModalAsync">@L["Cancel"]</Button>
                <Button Color="Color.Primary"
                        Type="@ButtonType.Submit"
                        PreventDefaultOnSubmit="true"
                        Clicked="UpdateEntityAsync">@L["Save"]</Button>
            </ModalFooter>
        </Form>
    </ModalContent>
</Modal>
AutoMapper Configuration
The base AbpCrudPageBase uses the object to object mapping system to convert an incoming BookDto object to a CreateUpdateBookDto object. So, we need to define the mapping.
Open the BookStoreBlazorAutoMapperProfile inside the Acme.BookStore.Blazor project and change the content as the following:
using Acme.BookStore.Books;
using AutoMapper;
namespace Acme.BookStore.Blazor
{
    public class BookStoreBlazorAutoMapperProfile : Profile
    {
        public BookStoreBlazorAutoMapperProfile()
        {
            CreateMap<BookDto, CreateUpdateBookDto>();
        }
    }
}
- We've just added the CreateMap<BookDto, CreateUpdateBookDto>();line to define the mapping.
Test the Editing Modal
You can now run the application and try to edit a book.

Deleting a Book
Open the Books.razor page and add the following EntityAction under the "Edit" action inside the EntityActions:
<EntityAction TItem="BookDto"
              Text="@L["Delete"]"
              Clicked="() => DeleteEntityAsync(context)"
              ConfirmationMessage="() => GetDeleteConfirmationMessage(context)" />
- DeleteEntityAsyncis defined in the base class that deletes the entity by performing a call to the server.
- ConfirmationMessageis a callback to show a confirmation message before executing the action.
- GetDeleteConfirmationMessageis defined in the base class. You can override this method (or pass another value to the- ConfirmationMessageparameter) to customize the localization message.
The "Actions" button becomes a dropdown since it has two actions now:

Run the application and try to delete a book.
Full CRUD UI Code
Here the complete code to create the book management CRUD page, that has been developed in the last two parts:
@page "/books"
@using Volo.Abp.Application.Dtos
@using Acme.BookStore.Books
@using Acme.BookStore.Localization
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<BookStoreResource> L
@inject AbpBlazorMessageLocalizerHelper<BookStoreResource> LH
@inherits AbpCrudPageBase<IBookAppService, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto>
<Card>
    <CardHeader>
        <Row Class="justify-content-between">
            <Column ColumnSize="ColumnSize.IsAuto">
                <h2>@L["Books"]</h2>
            </Column>
            <Column ColumnSize="ColumnSize.IsAuto">
                <Button Color="Color.Primary"
                        Clicked="OpenCreateModalAsync">@L["NewBook"]</Button>
            </Column>
        </Row>
    </CardHeader>
    <CardBody>
        <DataGrid TItem="BookDto"
                  Data="Entities"
                  ReadData="OnDataGridReadAsync"
                  CurrentPage="CurrentPage"
                  TotalItems="TotalCount"
                  ShowPager="true"
                  PageSize="PageSize">
            <DataGridColumns>
                <DataGridEntityActionsColumn TItem="BookDto" @ref="@EntityActionsColumn">
                    <DisplayTemplate>
                        <EntityActions TItem="BookDto" EntityActionsColumn="@EntityActionsColumn">
                            <EntityAction TItem="BookDto"
                                          Text="@L["Edit"]"
                                          Clicked="() => OpenEditModalAsync(context)" />
                            <EntityAction TItem="BookDto"
                                          Text="@L["Delete"]"
                                          Clicked="() => DeleteEntityAsync(context)"
                                          ConfirmationMessage="()=>GetDeleteConfirmationMessage(context)" />
                        </EntityActions>
                    </DisplayTemplate>
                </DataGridEntityActionsColumn>
                <DataGridColumn TItem="BookDto"
                                Field="@nameof(BookDto.Name)"
                                Caption="@L["Name"]"></DataGridColumn>
                <DataGridColumn TItem="BookDto"
                                Field="@nameof(BookDto.Type)"
                                Caption="@L["Type"]">
                    <DisplayTemplate>
                        @L[$"Enum:BookType:{(int) context.Type}"]
                    </DisplayTemplate>
                </DataGridColumn>
                <DataGridColumn TItem="BookDto"
                                Field="@nameof(BookDto.PublishDate)"
                                Caption="@L["PublishDate"]">
                    <DisplayTemplate>
                        @context.PublishDate.ToShortDateString()
                    </DisplayTemplate>
                </DataGridColumn>
                <DataGridColumn TItem="BookDto"
                                Field="@nameof(BookDto.Price)"
                                Caption="@L["Price"]">
                </DataGridColumn>
                <DataGridColumn TItem="BookDto"
                                Field="@nameof(BookDto.CreationTime)"
                                Caption="@L["CreationTime"]">
                    <DisplayTemplate>
                        @context.CreationTime.ToLongDateString()
                    </DisplayTemplate>
                </DataGridColumn>
            </DataGridColumns>
        </DataGrid>
    </CardBody>
</Card>
<Modal @ref="@CreateModal">
    <ModalBackdrop />
    <ModalContent IsCentered="true">
        <Form>
            <ModalHeader>
                <ModalTitle>@L["NewBook"]</ModalTitle>
                <CloseButton Clicked="CloseCreateModalAsync"/>
            </ModalHeader>
            <ModalBody>
                <Validations @ref="@CreateValidationsRef" Model="@NewEntity" ValidateOnLoad="false">
                    <Validation MessageLocalizer="@LH.Localize">
                        <Field>
                            <FieldLabel>@L["Name"]</FieldLabel>
                            <TextEdit @bind-Text="@NewEntity.Name">
                                <Feedback>
                                    <ValidationError/>
                                </Feedback>
                            </TextEdit>
                        </Field>
                    </Validation>
                    <Field>
                        <FieldLabel>@L["Type"]</FieldLabel>
                        <Select TValue="BookType" @bind-SelectedValue="@NewEntity.Type">
                            @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
                            {
                                <SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
                                    @L[$"Enum:BookType:{bookTypeValue}"]
                                </SelectItem>
                            }
                        </Select>
                    </Field>
                    <Field>
                        <FieldLabel>@L["PublishDate"]</FieldLabel>
                        <DateEdit TValue="DateTime" @bind-Date="NewEntity.PublishDate"/>
                    </Field>
                    <Field>
                        <FieldLabel>@L["Price"]</FieldLabel>
                        <NumericEdit TValue="float" @bind-Value="NewEntity.Price"/>
                    </Field>
                </Validations>
            </ModalBody>
            <ModalFooter>
                <Button Color="Color.Secondary"
                        Clicked="CloseCreateModalAsync">@L["Cancel"]</Button>
                <Button Color="Color.Primary"
                        Type="@ButtonType.Submit"
                        PreventDefaultOnSubmit="true"
                        Clicked="CreateEntityAsync">@L["Save"]</Button>
            </ModalFooter>
        </Form>
    </ModalContent>
</Modal>
<Modal @ref="@EditModal">
    <ModalBackdrop />
    <ModalContent IsCentered="true">
        <Form>
            <ModalHeader>
                <ModalTitle>@EditingEntity.Name</ModalTitle>
                <CloseButton Clicked="CloseEditModalAsync"/>
            </ModalHeader>
            <ModalBody>
                <Validations @ref="@EditValidationsRef" Model="@NewEntity" ValidateOnLoad="false">
                    <Validation MessageLocalizer="@LH.Localize">
                        <Field>
                            <FieldLabel>@L["Name"]</FieldLabel>
                            <TextEdit @bind-Text="@EditingEntity.Name">
                                <Feedback>
                                    <ValidationError/>
                                </Feedback>
                            </TextEdit>
                        </Field>
                    </Validation>
                    <Field>
                        <FieldLabel>@L["Type"]</FieldLabel>
                        <Select TValue="BookType" @bind-SelectedValue="@EditingEntity.Type">
                            @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
                            {
                                <SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
                                    @L[$"Enum:BookType:{bookTypeValue}"]
                                </SelectItem>
                            }
                        </Select>
                    </Field>
                    <Field>
                        <FieldLabel>@L["PublishDate"]</FieldLabel>
                        <DateEdit TValue="DateTime" @bind-Date="EditingEntity.PublishDate"/>
                    </Field>
                    <Field>
                        <FieldLabel>@L["Price"]</FieldLabel>
                        <NumericEdit TValue="float" @bind-Value="EditingEntity.Price"/>
                    </Field>
                </Validations>
            </ModalBody>
            <ModalFooter>
                <Button Color="Color.Secondary"
                        Clicked="CloseEditModalAsync">@L["Cancel"]</Button>
                <Button Color="Color.Primary"
                        Type="@ButtonType.Submit"
                        PreventDefaultOnSubmit="true"
                        Clicked="UpdateEntityAsync">@L["Save"]</Button>
            </ModalFooter>
        </Form>
    </ModalContent>
</Modal>
The Next Part
See the next part of this tutorial.
 
                                             
                                    