项目

本文档有多个版本。请选择最适合您的选项。

UI
Database

Web 应用开发教程 - 第 9 部分:作者:用户界面

简介

本部分介绍如何为前几部分引入的 Author 实体创建 CRUD 页面。

作者管理页面

Authors Razor 组件

Acme.BookStore.MauiBlazor 项目中创建一个新的 Razor 组件页面 /Pages/Authors.razor,内容如下:

@page "/authors"
@using Acme.BookStore.Authors
@using Acme.BookStore.Localization
@using Volo.Abp.AspNetCore.Components.Web
@inherits BookStoreComponentBase
@inject IAuthorAppService AuthorAppService
@inject AbpBlazorMessageLocalizerHelper<BookStoreResource> LH
<Card>
    <CardHeader>
        <Row Class="justify-content-between">
            <Column ColumnSize="ColumnSize.IsAuto">
                <h2>@L["Authors"]</h2>
            </Column>
            <Column ColumnSize="ColumnSize.IsAuto">
                    @if (CanCreateAuthor)
                    {
                        <Button Color="Color.Primary"
                                Clicked="OpenCreateAuthorModal">
                            @L["NewAuthor"]
                        </Button>
                    }
            </Column>
        </Row>
    </CardHeader>
    <CardBody>
        <DataGrid TItem="AuthorDto"
                  Data="AuthorList"
                  ReadData="OnDataGridReadAsync"
                  TotalItems="TotalCount"
                  ShowPager="true"
                  PageSize="PageSize">
            <DataGridColumns>
                <DataGridColumn Width="150px"
                                TItem="AuthorDto"
                                Field="@nameof(AuthorDto.Id)"
                                Sortable="false"
                                Caption="@L["Actions"]">
                    <DisplayTemplate>
                        <Dropdown>
                            <DropdownToggle Color="Color.Primary">
                                @L["Actions"]
                            </DropdownToggle>
                            <DropdownMenu>
                                @if (CanEditAuthor)
                                {
                                    <DropdownItem Clicked="() => OpenEditAuthorModal(context)">
                                        @L["Edit"]
                                    </DropdownItem>
                                }
                                @if (CanDeleteAuthor)
                                {
                                    <DropdownItem Clicked="() => DeleteAuthorAsync(context)">
                                        @L["Delete"]
                                    </DropdownItem>
                                }
                            </DropdownMenu>
                        </Dropdown>
                    </DisplayTemplate>
                </DataGridColumn>
                <DataGridColumn TItem="AuthorDto"
                                Field="@nameof(AuthorDto.Name)"
                                Caption="@L["Name"]"></DataGridColumn>
                <DataGridColumn TItem="AuthorDto"
                                Field="@nameof(AuthorDto.BirthDate)"
                                Caption="@L["BirthDate"]">
                    <DisplayTemplate>
                        @context.BirthDate.ToShortDateString()
                    </DisplayTemplate>
                </DataGridColumn>
            </DataGridColumns>
        </DataGrid>
    </CardBody>
</Card>

<Modal @ref="CreateAuthorModal">
    <ModalBackdrop />
    <ModalContent IsCentered="true">
        <Form>
            <ModalHeader>
                <ModalTitle>@L["NewAuthor"]</ModalTitle>
                <CloseButton Clicked="CloseCreateAuthorModal" />
            </ModalHeader>
            <ModalBody>
                <Validations @ref="@CreateValidationsRef" Model="@NewAuthor" ValidateOnLoad="false">
                    <Validation MessageLocalizer="@LH.Localize">
                        <Field>
                            <FieldLabel>@L["Name"]</FieldLabel>
                            <TextEdit @bind-Text="@NewAuthor.Name">
                                <Feedback>
                                    <ValidationError/>
                                </Feedback>
                            </TextEdit>
                        </Field>
                    </Validation>
                    <Field>
                        <FieldLabel>@L["BirthDate"]</FieldLabel>
                        <DateEdit TValue="DateTime" @bind-Date="@NewAuthor.BirthDate"/>
                    </Field>
                    <Validation MessageLocalizer="@LH.Localize">
                        <Field>
                            <FieldLabel>@L["ShortBio"]</FieldLabel>
                            <MemoEdit Rows="5" @bind-Text="@NewAuthor.ShortBio">
                                <Feedback>
                                    <ValidationError/>
                                </Feedback>
                            </MemoEdit>
                        </Field>
                    </Validation>
                </Validations>
            </ModalBody>
            <ModalFooter>
                <Button Color="Color.Secondary"
                        Clicked="CloseCreateAuthorModal">
                    @L["Cancel"]
                </Button>
                <Button Color="Color.Primary"
                        Type="@ButtonType.Submit"
                        PreventDefaultOnSubmit="true"
                        Clicked="CreateAuthorAsync">
                    @L["Save"]
                </Button>
            </ModalFooter>
        </Form>
    </ModalContent>
</Modal>

<Modal @ref="EditAuthorModal">
    <ModalBackdrop />
    <ModalContent IsCentered="true">
        <Form>
            <ModalHeader>
                        <ModalTitle>@EditingAuthor.Name</ModalTitle>
                        <CloseButton Clicked="CloseEditAuthorModal" />
                    </ModalHeader>
            <ModalBody>
                <Validations @ref="@EditValidationsRef" Model="@EditingAuthor" ValidateOnLoad="false">
                    <Validation MessageLocalizer="@LH.Localize">
                        <Field>
                            <FieldLabel>@L["Name"]</FieldLabel>
                            <TextEdit @bind-Text="@EditingAuthor.Name">
                                <Feedback>
                                    <ValidationError/>
                                </Feedback>
                            </TextEdit>
                        </Field>
                    </Validation>
                    <Field>
                        <FieldLabel>@L["BirthDate"]</FieldLabel>
                        <DateEdit TValue="DateTime" @bind-Date="@EditingAuthor.BirthDate"/>
                    </Field>
                    <Validation>
                        <Field>
                            <FieldLabel>@L["ShortBio"]</FieldLabel>
                            <MemoEdit Rows="5" @bind-Text="@EditingAuthor.ShortBio">
                                <Feedback>
                                    <ValidationError/>
                                </Feedback>
                            </MemoEdit>
                        </Field>
                    </Validation>
                </Validations>
            </ModalBody>
            <ModalFooter>
                <Button Color="Color.Secondary"
                        Clicked="CloseEditAuthorModal">
                    @L["Cancel"]
                </Button>
                <Button Color="Color.Primary"
                        Type="@ButtonType.Submit"
                        PreventDefaultOnSubmit="true"
                        Clicked="UpdateAuthorAsync">
                    @L["Save"]
                </Button>
            </ModalFooter>
        </Form>
    </ModalContent>
</Modal>
  • 此代码与 Books.razor 类似,只是它不从 AbpCrudPageBase 继承,而是使用自己的实现。
  • 注入 IAuthorAppService 以便从 UI 使用服务器端 HTTP API。通过动态 C# HTTP API 客户端代理系统的帮助,我们可以直接注入应用服务接口并像常规方法调用一样使用,该系统为我们执行 REST API 调用。请参阅下面的 Authors 类以查看用法。

Pages 文件夹下创建一个新的代码隐藏文件 Authors.razor.cs,内容如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Acme.BookStore.Authors;
using Acme.BookStore.Permissions;
using Blazorise;
using Blazorise.DataGrid;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;

namespace Acme.BookStore.MauiBlazor.Pages;

public partial class Authors
{
    private IReadOnlyList<AuthorDto> AuthorList { get; set; }

    private int PageSize { get; } = LimitedResultRequestDto.DefaultMaxResultCount;
    private int CurrentPage { get; set; }
    private string CurrentSorting { get; set; }
    private int TotalCount { get; set; }

    private bool CanCreateAuthor { get; set; }
    private bool CanEditAuthor { get; set; }
    private bool CanDeleteAuthor { get; set; }

    private CreateAuthorDto NewAuthor { get; set; }

    private Guid EditingAuthorId { get; set; }
    private UpdateAuthorDto EditingAuthor { get; set; }

    private Modal CreateAuthorModal { get; set; }
    private Modal EditAuthorModal { get; set; }

    private Validations CreateValidationsRef;
    
    private Validations EditValidationsRef;
    
    public Authors()
    {
        NewAuthor = new CreateAuthorDto();
        EditingAuthor = new UpdateAuthorDto();
    }

    protected override async Task OnInitializedAsync()
    {
        await SetPermissionsAsync();
        await GetAuthorsAsync();
    }

    private async Task SetPermissionsAsync()
    {
        CanCreateAuthor = await AuthorizationService
            .IsGrantedAsync(BookStorePermissions.Authors.Create);

        CanEditAuthor = await AuthorizationService
            .IsGrantedAsync(BookStorePermissions.Authors.Edit);

        CanDeleteAuthor = await AuthorizationService
            .IsGrantedAsync(BookStorePermissions.Authors.Delete);
    }

    private async Task GetAuthorsAsync()
    {
        var result = await AuthorAppService.GetListAsync(
            new GetAuthorListDto
            {
                MaxResultCount = PageSize,
                SkipCount = CurrentPage * PageSize,
                Sorting = CurrentSorting
            }
        );

        AuthorList = result.Items;
        TotalCount = (int)result.TotalCount;
    }

    private async Task OnDataGridReadAsync(DataGridReadDataEventArgs<AuthorDto> e)
    {
        CurrentSorting = e.Columns
            .Where(c => c.SortDirection != SortDirection.Default)
            .Select(c => c.Field + (c.SortDirection == SortDirection.Descending ? " DESC" : ""))
            .JoinAsString(",");
        CurrentPage = e.Page - 1;

        await GetAuthorsAsync();

        await InvokeAsync(StateHasChanged);
    }

    private void OpenCreateAuthorModal()
    {
        CreateValidationsRef.ClearAll();
        
        NewAuthor = new CreateAuthorDto();
        CreateAuthorModal.Show();
    }

    private void CloseCreateAuthorModal()
    {
        CreateAuthorModal.Hide();
    }

    private void OpenEditAuthorModal(AuthorDto author)
    {
        EditValidationsRef.ClearAll();
        
        EditingAuthorId = author.Id;
        EditingAuthor = ObjectMapper.Map<AuthorDto, UpdateAuthorDto>(author);
        EditAuthorModal.Show();
    }

    private async Task DeleteAuthorAsync(AuthorDto author)
    {
        try
        {
            var confirmMessage = L["AuthorDeletionConfirmationMessage", author.Name];
            if (!await Message.Confirm(confirmMessage))
            {
                return;
            }

            await AuthorAppService.DeleteAsync(author.Id);
            await GetAuthorsAsync();
        }
        catch(Exception ex)
        {
            await HandleErrorAsync(ex);
        }
    }

    private void CloseEditAuthorModal()
    {
        EditAuthorModal.Hide();
    }

    private async Task CreateAuthorAsync()
    {
        try
        {
            if (await CreateValidationsRef.ValidateAll())
            {
                await AuthorAppService.CreateAsync(NewAuthor);
                await GetAuthorsAsync();
                CreateAuthorModal.Hide();
            }
        }
        catch(Exception ex)
        {
            await HandleErrorAsync(ex);
        }
    }

    private async Task UpdateAuthorAsync()
    {
        try
        {
            if (await EditValidationsRef.ValidateAll())
            {
                await AuthorAppService.UpdateAsync(EditingAuthorId, EditingAuthor);
                await GetAuthorsAsync();
                EditAuthorModal.Hide();
            }
        }
        catch(Exception ex)
        {
            await HandleErrorAsync(ex);
        }
    }
}

此类主要定义了 Authors.razor 页面使用的属性和方法。

对象映射

Authors 类在 OpenEditAuthorModal 方法中使用了 IObjectMapper。因此,我们需要定义此映射。

打开 Acme.BookStore.MauiBlazor 项目中的 BookStoreBlazorMappers.cs 文件,并在类中添加以下映射:

using Riok.Mapperly.Abstractions;
using Volo.Abp.Mapperly;
using Acme.BookStore.Authors;

//...

[Mapper]
public partial class AuthorDtoToUpdateAuthorDtoMapper : MapperBase<AuthorDto, UpdateAuthorDto>
{
    public override partial UpdateAuthorDto Map(AuthorDto source);

    public override partial void Map(AuthorDto source, UpdateAuthorDto destination);
}

添加到主菜单

打开 Acme.BookStore.MauiBlazor 项目中的 BookStoreMenuContributor.cs 文件,并将以下代码添加到 ConfigureMainMenuAsync 方法的末尾:

context.Menu.AddItem(new ApplicationMenuItem(
        "BooksStore.Authors",
        l["Menu:Authors"],
        url: "/authors"
    ).RequirePermissions(BookStorePermissions.Books.Default));

本地化配置

我们应该完成上面使用的本地化配置。打开 Acme.BookStore.Domain.Shared 项目 Localization/BookStore 文件夹下的 en.json 文件,并添加以下条目:

"Menu:Authors": "Authors",
"Authors": "Authors",
"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?",
"BirthDate": "Birth date",
"NewAuthor": "New author"

运行应用程序

运行并登录到应用程序。如果你在 书店 菜单下没有看到 Authors 菜单项,这意味着你还没有权限。 进入 identity/roles 页面,点击 *Actions* 按钮,并为 admin 角色选择 *Permissions* 操作:

bookstore-author-permissions

如你所见,admin 角色目前还没有 *Author Management* 权限。点击复选框并保存模态窗体以授予必要的权限。刷新页面后,你将在主菜单的 *书店* 下看到 *Authors* 菜单项:

bookstore-authors-page

就是这样!这是一个功能完整的 CRUD 页面,你可以创建、编辑和删除作者。

提示:在定义新权限后,如果运行 .DbMigrator 控制台应用程序,它会自动将这些新权限授予 admin 角色,你无需手动授予权限。


在本文档中