Convert Create/Edit Modals to Page

In this document we will explain how to convert BookStore's Books create & edit modals to regular blazor pages.

Before

bookstore-crud-before

After

bookstore-crud-after

Books.razor Page

Books.razor page is the main page of the books management. Create & Update operations are done in this page. So we'll remove create & update operations from this page and move a separate blazor component for each operation. Each component will be a page.

  • Remove both Create & Update modals.

    remove-all-modals

  • Replace NewBook button with a link to CreateBook page.

    <Button Color="Color.Primary" Type="ButtonType.Link" To="books/new">
        @L["NewBook"]
    </Button>
    
  • Inject NavigationManager to Books.razor page.

    @inject NavigationManager NavigationManager
    
  • Replace Edit button with a link to UpdateBook page.

    <Button Color="Color.Primary" Type="ButtonType.Link" OnClick="() => NavigateToEdit(book.Id)">
        @L["Edit"]
    </Button>
    
    private void NavigateToEdit(Guid id)
    {
        NavigationManager.NavigateTo($"books/{id}/edit");
    }
    
  • Remove all methods in the Books.razor page except constructor. And add GoToEditPage as below:

    protected void GoToEditPage(BookDto book)
    {
        NavigationManager.NavigateTo($"books/{book.Id}");
    }
    

    bookstore-remove-methods

  • Change Edit button to a link in the table.

    <EntityAction TItem="BookDto"
                Text="@L["Edit"]"
                Visible=HasUpdatePermission
                Clicked="() => GoToEditPage(context)" />
    

CreateBooks Page

Create new CreateBook.razor and CreateBook.razor.cs files in your project.

  • CreateBook.razor
@page "/books/new"
@attribute [Authorize(BookStorePermissions.Books.Create)]
@inherits BookStoreComponentBase

@using Acme.BookStore.Books;
@using Acme.BookStore.Localization;
@using Acme.BookStore.Permissions;
@using Microsoft.Extensions.Localization;
@using Volo.Abp.AspNetCore.Components.Web;

@inject IStringLocalizer<BookStoreResource> L
@inject AbpBlazorMessageLocalizerHelper<BookStoreResource> LH
@inject IBookAppService AppService
@inject NavigationManager NavigationManager

<Card>
    <CardHeader>
        <HeadContent>
            <ModalTitle>@L["NewBook"]</ModalTitle>
        </HeadContent>
    </CardHeader>
    <CardBody>
        <Validations @ref="@CreateValidationsRef" Model="@NewEntity" ValidateOnLoad="false">
            <Validation MessageLocalizer="@LH.Localize">
                <Field>
                    <FieldLabel>@L["Author"]</FieldLabel>
                    <Select TValue="Guid" @bind-SelectedValue="@NewEntity.AuthorId">
                        @foreach (var author in authorList)
                        {
                            <SelectItem TValue="Guid" Value="@author.Id">
                                @author.Name
                            </SelectItem>
                        }
                    </Select>
                </Field>
                <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>
    </CardBody>
    <CardFooter>
        <Button Color="Color.Secondary" Type="ButtonType.Link" To="books">
            @L["Cancel"]
        </Button>
        <Button Color="Color.Primary"
                Type="@ButtonType.Submit"
                PreventDefaultOnSubmit="true"
                Clicked="CreateEntityAsync">
            @L["Save"]
        </Button>
    </CardFooter>
</Card>
  • CreateBook.razor.cs
using Acme.BookStore.Books;
using Blazorise;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp;

namespace Acme.BookStore.Blazor.Pages;

public partial class CreateBook
{
    protected Validations CreateValidationsRef;
    protected CreateUpdateBookDto NewEntity = new();
    IReadOnlyList<AuthorLookupDto> authorList = Array.Empty<AuthorLookupDto>();

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        authorList = (await AppService.GetAuthorLookupAsync()).Items;

        if (!authorList.Any())
        {
            throw new UserFriendlyException(message: L["AnAuthorIsRequiredForCreatingBook"]);
        }

        NewEntity.AuthorId = authorList.First().Id;

        if (CreateValidationsRef != null)
        {
            await CreateValidationsRef.ClearAll();
        }
    }

    protected virtual async Task CreateEntityAsync()
    {
        try
        {
            var validate = true;
            if (CreateValidationsRef != null)
            {
                validate = await CreateValidationsRef.ValidateAll();
            }
            if (validate)
            {
                await AppService.CreateAsync(NewEntity);
                NavigationManager.NavigateTo("books");
            }
        }
        catch (Exception ex)
        {
            await HandleErrorAsync(ex);
        }
    }
}

EditBooks Page

Create new EditBook.razor and EditBook.razor.cs files in your project.

  • EditBook.razor
@page "/books/{Id}"
@attribute [Authorize(BookStorePermissions.Books.Edit)]
@inherits BookStoreComponentBase
@using Acme.BookStore.Books;
@using Acme.BookStore.Localization;
@using Acme.BookStore.Permissions;
@using Microsoft.Extensions.Localization;
@using Volo.Abp.AspNetCore.Components.Web;

@inject IStringLocalizer<BookStoreResource> L
@inject AbpBlazorMessageLocalizerHelper<BookStoreResource> LH
@inject IBookAppService AppService
@inject NavigationManager NavigationManager

<Card>
    <CardHeader>
        <HeadContent>
            <ModalTitle>@EditingEntity.Name</ModalTitle>
        </HeadContent>
    </CardHeader>
    <CardBody>
        <Validations @ref="@EditValidationsRef" Model="@EditingEntity" ValidateOnLoad="false">
            <Validation MessageLocalizer="@LH.Localize">
                <Field>
                    <FieldLabel>@L["Author"]</FieldLabel>
                    <Select TValue="Guid" @bind-SelectedValue="@EditingEntity.AuthorId">
                        @foreach (var author in authorList)
                        {
                            <SelectItem TValue="Guid" Value="@author.Id">
                                @author.Name
                            </SelectItem>
                        }
                    </Select>
                </Field>
                <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>
    </CardBody>
    <CardFooter>
        <Button Color="Color.Secondary" Type="ButtonType.Link" To="books">
            @L["Cancel"]
        </Button>
        <Button Color="Color.Primary"
                Type="@ButtonType.Submit"
                PreventDefaultOnSubmit="true"
                Clicked="UpdateEntityAsync">
            @L["Save"]
        </Button>
    </CardFooter>
</Card>
  • EditBook.razor.cs
using Acme.BookStore.Books;
using Blazorise;
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp;

namespace Acme.BookStore.Blazor.Pages;

public partial class EditBook
{
    protected CreateUpdateBookDto EditingEntity = new();
    protected Validations EditValidationsRef;
    IReadOnlyList<AuthorLookupDto> authorList = Array.Empty<AuthorLookupDto>();

    [Parameter]
    public string Id { get; set; }

    public Guid EditingEntityId { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();

        // Blazor can't parse Guid as route constraint currently.
        // See https://github.com/dotnet/aspnetcore/issues/19008
        EditingEntityId = Guid.Parse(Id);

        authorList = (await AppService.GetAuthorLookupAsync()).Items;

        if (!authorList.Any())
        {
            throw new UserFriendlyException(message: L["AnAuthorIsRequiredForCreatingBook"]);
        }

        var entityDto = await AppService.GetAsync(EditingEntityId);

        EditingEntity = ObjectMapper.Map<BookDto,CreateUpdateBookDto>(entityDto);

        if (EditValidationsRef != null)
        {
            await EditValidationsRef.ClearAll();
        }
    }

    protected virtual async Task UpdateEntityAsync()
    {
        try
        {
            var validate = true;
            if (EditValidationsRef != null)
            {
                validate = await EditValidationsRef.ValidateAll();
            }
            if (validate)
            {
                await AppService.UpdateAsync(EditingEntityId, EditingEntity);

                NavigationManager.NavigateTo("books");
            }
        }
        catch (Exception ex)
        {
            await HandleErrorAsync(ex);
        }
    }
}

You can check the following commit for details:
https://github.com/abpframework/abp-samples/commit/aae61ad6d66ebf6191dd4dcfb4e23d30bd680a4e

Alper Ebicoglu 10 weeks ago

thanks for sharing how to convert the modal content to plain razor pages.

Onur Pıçakcı 10 weeks ago

Great article!

Berkan Sasmaz 10 weeks ago

Great article!

Enis Necipoğlu 10 weeks ago

Looking for MVC? Here mvc version of this arcitle: https://community.abp.io/posts/converting-createedit-modal-to-page-4ps5v60m

Val Kelmendi 9 weeks ago

looking for angular

Masum ULU 7 weeks ago