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 Ebiçoğlu 67 weeks ago

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

Onur Pıçakcı 67 weeks ago

Great article!

Berkan Şaşmaz 67 weeks ago

Great article!

Enis Necipoğlu 67 weeks ago

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

Val Kelmendi 66 weeks ago

looking for angular

Masum ULU 64 weeks ago
Moriancumer 48 weeks ago

I don't know if this message comes here, but I am learning to use ABP framework trying to reproduce the BookStore Application using Blazor and Entity Framework method but when I add this line

@code { public Books() // Constructor { CreatePolicyName = BookStorePermissions.Books.Create; UpdatePolicyName = BookStorePermissions.Books.Edit; DeletePolicyName = BookStorePermissions.Books.Delete; } }

I get this compilation error "Method must have a return type" I can't go on because of this error, any tips or someone that has faced a similar problem?

Thanks in advance

Enis Necipoğlu 48 weeks ago

If it's a constructor, then its name must be the same with class name. If it's a razor page, it's better to use override OnInitializedAsync() method instead constructors