项目

移动应用开发教程 - MAUI

关于本教程

您必须拥有 ABP Team 或更高等级的许可证 才能创建移动应用程序。

本教程假设您已经完成了 Web 应用开发教程,并构建了一个名为 Acme.BookStore 的基于 ABP 的应用程序,且选择了 MAUI 作为移动端选项。因此,如果您尚未完成 Web 应用开发教程,您需要先完成它,或者从下方下载源代码并跟随本教程操作。

在本教程中,我们将仅关注 Acme.BookStore 应用程序的 UI 部分,并为 MAUI 移动应用实现 CRUD 操作。本教程遵循 MVVM (模型-视图-视图模型) 模式,该模式将 UI 与应用程序的业务逻辑分离。

下载源代码

您可以使用以下链接下载本文中所述应用程序的源代码:

如果您在 Windows 上遇到“文件名太长”或“解压缩”错误,请参阅 此指南

创建作者页面 - 列表与删除作者

Acme.BookStore.Maui 项目的 Pages 文件夹下创建一个内容页面 AuthorsPage.xaml,并按如下内容修改:

AuthorsPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Acme.BookStore.Maui.Pages.AuthorsPage"
             xmlns:ext="clr-namespace:Acme.BookStore.Maui.Extensions"
             xmlns:viewModels="clr-namespace:Acme.BookStore.Maui.ViewModels"
             xmlns:author="clr-namespace:Acme.BookStore.Authors;assembly=Acme.BookStore.Application.Contracts"
             xmlns:u="http://schemas.enisn-projects.io/dotnet/maui/uraniumui"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             x:Name="page"
             x:DataType="viewModels:AuthorPageViewModel"
             Title="{ext:Translate Authors}">
    <ContentPage.Behaviors>
        <toolkit:EventToCommandBehavior EventName="Appearing" Command="{Binding RefreshCommand}" />
    </ContentPage.Behaviors>
    <ContentPage.ToolbarItems>
        <ToolbarItem Text="{ext:Translate NewAuthor,StringFormat='+ {0}'}"
            Command="{Binding OpenCreateModalCommand}"
            IconImageSource="{OnIdiom Desktop={FontImageSource FontFamily=MaterialRegular, Glyph={x:Static u:MaterialRegular.Add}}}"/>
    </ContentPage.ToolbarItems>

    <Grid RowDefinitions="Auto,*" Padding="16,16,16,0">

        <!-- 搜索 -->
        <Frame BorderColor="Transparent" Padding="0" HasShadow="False">
            <SearchBar Text="{Binding Input.Filter}" SearchCommand="{Binding RefreshCommand}" Placeholder="{ext:Translate Search}"/>
        </Frame>

        <RefreshView Grid.Row="1"
            IsRefreshing="{Binding IsBusy}"
            Command="{Binding RefreshCommand}">

            <CollectionView
                ItemsSource="{Binding Items}"
                SelectionMode="None">
                <CollectionView.Header>
                    <BoxView HeightRequest="16" Color="Transparent" />
                </CollectionView.Header>
                <CollectionView.ItemTemplate>
                    <DataTemplate x:DataType="author:AuthorDto">
                        <Grid ColumnDefinitions="Auto,*,Auto" Padding="4,0" Margin="0,8" ColumnSpacing="10">
                            <VerticalStackLayout Grid.Column="1" VerticalOptions="Center">
                                <Label Text="{Binding Name}" FontAttributes="Bold" />
                                <Label Text="{Binding ShortBio}" StyleClass="muted" />
                            </VerticalStackLayout>

                            <ImageButton Grid.Column="2" VerticalOptions="Center" Margin="0,16"
                                Command="{Binding BindingContext.ShowActionsCommand, Source={x:Reference page}}"
                                CommandParameter="{Binding .}" HeightRequest="24">
                                <ImageButton.Source>
                                    <FontImageSource FontFamily="MaterialRegular"
                                        Glyph="{x:Static u:MaterialRegular.More_vert}"
                                        Color="{AppThemeBinding Light={StaticResource ForegroundDark}, Dark={StaticResource ForegroundLight}}" />
                                </ImageButton.Source>
                            </ImageButton>
                        </Grid>
                    </DataTemplate>
                </CollectionView.ItemTemplate>

                <CollectionView.Footer>
                    <VerticalStackLayout>
                        <ActivityIndicator HorizontalOptions="Center"
                            IsRunning="{Binding IsLoadingMore}"
                            Margin="20"/>

                        <ContentView Margin="0,0,0,8" IsVisible="{OnIdiom Default=False, Desktop=True}" HorizontalOptions="Center">
                            <Button IsVisible="{Binding CanLoadMore}"  
                                StyleClass="TextButton" Text="{ext:Translate LoadMore}"
                                Command="{Binding LoadMoreCommand}"/>
                        </ContentView>
                    </VerticalStackLayout>
                </CollectionView.Footer>
            </CollectionView>
        </RefreshView>
    </Grid>
</ContentPage>

这是一个简单的页面,用于列出作者,允许打开创建模态框来新建作者、编辑现有作者以及删除作者。

AuthorsPage.xaml.cs

让我们创建 AuthorsPage.xaml.cs 代码隐藏类,并复制粘贴以下内容:

using Acme.BookStore.Maui.ViewModels;
using Volo.Abp.DependencyInjection;

namespace Acme.BookStore.Maui.Pages;

public partial class AuthorsPage : ContentPage, ITransientDependency
{
 public AuthorsPage(AuthorPageViewModel vm)
 {
  InitializeComponent();
  BindingContext = vm;
 }
}

这里,我们将页面注册为 Transient 生命周期,以便稍后用于导航目的,并将绑定源(BindingContext)指定为 AuthorPageViewModel,以充分利用 MVVM 模式。我们尚未创建 AuthorPageViewModel 类,让我们现在创建它。

AuthorPageViewModel.cs

在项目的 ViewModels 文件夹下创建一个视图模型类 AuthorPageViewModel,并按如下内容修改:

using Acme.BookStore.Authors;
using Acme.BookStore.Maui.Messages;
using Acme.BookStore.Maui.Pages;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using System.Collections.ObjectModel;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client;
using Volo.Abp.Threading;

namespace Acme.BookStore.Maui.ViewModels
{
    public partial class AuthorPageViewModel : BookStoreViewModelBase,
        IRecipient<AuthorCreateMessage>, // 创建
        IRecipient<AuthorEditMessage>, // 编辑
        ITransientDependency
    {
        [ObservableProperty]
        private bool isBusy;

        [ObservableProperty]
        bool isLoadingMore;

        [ObservableProperty]
        bool canLoadMore;

        public GetAuthorListDto Input { get; } = new();

        public ObservableCollection<AuthorDto> Items { get; } = new();

        protected IAuthorAppService AuthorAppService { get; }

        protected SemaphoreSlim SemaphoreSlim { get; } = new SemaphoreSlim(1, 1);

        public AuthorPageViewModel(IAuthorAppService authorAppService)
        {
            AuthorAppService = authorAppService;

            WeakReferenceMessenger.Default.Register<AuthorCreateMessage>(this);
            WeakReferenceMessenger.Default.Register<AuthorEditMessage>(this);
        }

        [RelayCommand]
        async Task OpenCreateModal()
        {
            await Shell.Current.GoToAsync(nameof(AuthorCreatePage));
        }

        [RelayCommand]
        async Task OpenEditModal(Guid authorId)
        {
            await Shell.Current.GoToAsync($"{nameof(AuthorEditPage)}?Id={authorId}");
        }

        [RelayCommand]
        async Task Refresh()
        {
            await GetAuthorsAsync();
        }

        [RelayCommand]
        async Task ShowActions(AuthorDto author)
        {
            var result = await App.Current!.MainPage!.DisplayActionSheet(
                L["Actions"],
                L["Cancel"],
                null,
                L["Edit"], L["Delete"]);

            if (result == L["Edit"])
            {
                await OpenEditModal(author.Id);
            }

            if (result == L["Delete"])
            {
                await Delete(author);
            }
        }

        [RelayCommand]
        async Task Delete(AuthorDto author)
        {
            var confirmed = await Shell.Current.CurrentPage.DisplayAlert(
                L["Delete"],
                string.Format(L["AuthorDeletionConfirmationMessage"].Value, author.Name),
                L["Delete"],
                L["Cancel"]);

            if (!confirmed)
            {
                return;
            }

            try
            {
                await AuthorAppService.DeleteAsync(author.Id);
            }
            catch (AbpRemoteCallException remoteException)
            {
                HandleException(remoteException);
            }

            await GetAuthorsAsync();
        }

        private async Task GetAuthorsAsync()
        {
            IsBusy = true;

            try
            {
                Input.SkipCount = 0;

                var result = await AuthorAppService.GetListAsync(Input);

                Items.Clear();
                foreach (var user in result.Items)
                {
                    Items.Add(user);
                }

                CanLoadMore = result.Items.Count >= Input.MaxResultCount;

            }
            catch (AbpRemoteCallException remoteException)
            {
                HandleException(remoteException);
            }
            finally
            {
                IsBusy = false;
            }
        }

        [RelayCommand]
        async Task LoadMore()
        {
            if (!CanLoadMore)
            {
                return;
            }

            try
            {
                using (await SemaphoreSlim.LockAsync())
                {
                    IsLoadingMore = true;

                    Input.SkipCount += Input.MaxResultCount;

                    var result = await AuthorAppService.GetListAsync(Input);

                    CanLoadMore = result.Items.Count >= Input.MaxResultCount;

                    foreach (var tenant in result.Items)
                    {
                        Items.Add(tenant);
                    }
                }
            }
            catch (Exception ex)
            {
                HandleException(ex);
            }
            finally
            {
                IsLoadingMore = false;
            }
        }

        public void Receive(AuthorCreateMessage message)
        {
            MainThread.BeginInvokeOnMainThread(async () =>
            {
                await GetAuthorsAsync();
            });
        }

        public void Receive(AuthorEditMessage message)
        {
            MainThread.BeginInvokeOnMainThread(async () =>
            {
                await GetAuthorsAsync();
            });
        }
    }
}

AuthorPageViewModel 类是 Authors 页面背后所有逻辑的所在。在这里,我们执行以下步骤:

  • 我们从数据库获取所有作者,并将这些记录设置到 Items 属性中,该属性是 ObservableCollection<AuthorDto> 类型,因此每当作者列表更改时,CollectionView 将被刷新。
  • 我们定义了 OpenCreateModalOpenEditModal 方法,用于导航到创建模态框和编辑模态框页面(我们将在后续章节中创建它们)。
  • 我们定义了 ShowActions 方法,允许编辑或删除特定作者。
  • 我们创建了 Delete 方法,用于删除特定作者并重新渲染网格。
  • 最后,我们实现了 IRecipient<AuthorCreateMessage>IRecipient<AuthorEditMessage> 接口,以便在创建新作者或编辑现有作者后刷新网格。(我们将在后续章节中创建 AuthorCreateMessageAuthorEditMessage 类)

注册作者页面路由

打开 Acme.BookStore.Maui 项目下的 AppShell.xaml.cs 文件,并注册创建模态框和编辑模态框页面的路由:

using Acme.BookStore.Maui.Pages;
using Acme.BookStore.Maui.ViewModels;
using Volo.Abp.DependencyInjection;

namespace Acme.BookStore.Maui;

public partial class AppShell : Shell, ITransientDependency
{
    public AppShell(ShellViewModel vm)
    {
        BindingContext = vm;

        InitializeComponent();

        // 其他路由...

        // 作者
        Routing.RegisterRoute(nameof(AuthorCreatePage), typeof(AuthorCreatePage));
        Routing.RegisterRoute(nameof(AuthorEditPage), typeof(AuthorEditPage));
    }
}

由于我们需要在点击操作按钮时导航到创建模态框和编辑模态框页面,我们需要使用它们的路由注册这些页面。我们可以在 AppShell.xaml.cs 文件中完成此操作,该文件负责提供应用程序的导航功能。

创建新作者

Acme.BookStore.Maui 项目的 Pages 文件夹下创建一个新的内容页面 AuthorCreatePage.xaml,并按如下内容修改:

AuthorCreatePage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:ext="clr-namespace:Acme.BookStore.Maui.Extensions"
             x:Class="Acme.BookStore.Maui.Pages.AuthorCreatePage"
             xmlns:viewModels="clr-namespace:Acme.BookStore.Maui.ViewModels"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             x:DataType="viewModels:AuthorCreateViewModel"
             Title="{ext:Translate NewAuthor}">
    
    <ContentPage.ToolbarItems>
        <ToolbarItem Text="{ext:Translate Cancel}" Command="{Binding CancelCommand}"/>
    </ContentPage.ToolbarItems>

    <ScrollView>
        <VerticalStackLayout Padding="20" Spacing="20">

            <Border StyleClass="AbpInputContainer">
                <VerticalStackLayout>
                    <Label Text="{ext:Translate Name}" />
                    <Entry Text="{Binding Author.Name}" />
                </VerticalStackLayout>
            </Border>

            <Border StyleClass="AbpInputContainer">
                <VerticalStackLayout>
                    <Label Text="{ext:Translate ShortBio}" />
                    <Entry Text="{Binding Author.ShortBio}" />
                </VerticalStackLayout>
            </Border>

            <Border StyleClass="AbpInputContainer">
                <VerticalStackLayout>
                    <Label Text="{ext:Translate BirthDate}" />
                    <DatePicker Date="{Binding Author.BirthDate}"/>
                </VerticalStackLayout>
            </Border>

            <Grid>
                <Button Text="{ext:Translate Save}" Command="{Binding CreateCommand}" />
                <ActivityIndicator IsRunning="{Binding IsBusy}" IsVisible="{Binding IsBusy}" />
            </Grid>
        </VerticalStackLayout>
    </ScrollView>
</ContentPage>

在此页面中,我们定义了创建作者所需的表单元素,例如 姓名简介出生日期。每当用户点击 保存 按钮时,CreateCommand 将被触发,如果操作成功,将创建一个新作者。

让我们定义 AuthorCreateViewModel 作为此页面的 BindingContext,然后定义 CreateCommand 的逻辑。

AuthorCreatePage.xaml.cs

创建 AuthorCreatePage.xaml.cs 代码隐藏类,并复制粘贴以下内容:

using Acme.BookStore.Maui.ViewModels;
using Volo.Abp.DependencyInjection;

namespace Acme.BookStore.Maui.Pages;

public partial class AuthorCreatePage : ContentPage, ITransientDependency
{
 public AuthorCreatePage(AuthorCreateViewModel vm)
 {
  InitializeComponent();
  BindingContext = vm;
 }
}

AuthorCreateViewModel.cs

在项目的 ViewModels 文件夹下创建一个视图模型类 AuthorCreateViewModel,并按如下内容修改:

using Acme.BookStore.Authors;
using Acme.BookStore.Maui.Messages;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Volo.Abp.DependencyInjection;

namespace Acme.BookStore.Maui.ViewModels
{
    public partial class AuthorCreateViewModel : BookStoreViewModelBase, ITransientDependency
    {
        [ObservableProperty]
        private bool isBusy;

        public CreateAuthorDto Author { get; set; } = new();

        protected IAuthorAppService AuthorAppService { get; }

        public AuthorCreateViewModel(IAuthorAppService authorAppService)
        {
            AuthorAppService = authorAppService;

            Author.BirthDate = DateTime.Now;
        }

        [RelayCommand]
        async Task Cancel()
        {
            await Shell.Current.GoToAsync("..");
        }

        [RelayCommand]
        async Task Create()
        {
            try
            {
                IsBusy = true;

                await AuthorAppService.CreateAsync(Author);
                await Shell.Current.GoToAsync("..");
                WeakReferenceMessenger.Default.Send(new AuthorCreateMessage(Author));
            }
            catch (Exception ex)
            {
                HandleException(ex);
            }
            finally
            {
                IsBusy = false;
            }
        }
    }
}

在这里,我们执行以下步骤:

  • 此类简单地注入并使用 IAuthorAppService 来创建新作者。
  • 我们为 AuthorCreatePage 中的操作创建了两个方法,即 CancelCreate 方法。
  • Cancel 方法简单地返回到上一个页面 AuthorPage
  • Create 方法在 AuthorCreatePage 上点击 保存 按钮时创建新作者。

AuthorCreateMessage.cs

在项目的 Messages 文件夹下创建一个类 AuthorCreateMessage,并按如下内容修改:

using Acme.BookStore.Authors;
using CommunityToolkit.Mvvm.Messaging.Messages;

namespace Acme.BookStore.Maui.Messages
{
    public class AuthorCreateMessage : ValueChangedMessage<CreateAuthorDto>
    {
        public AuthorCreateMessage(CreateAuthorDto value) : base(value)
        {
        }
    }
}

此类用于表示我们将在作者创建后触发返回结果的消息。然后,我们订阅此消息并在 AuthorsPage 上更新网格。

更新作者

Acme.BookStore.Maui 项目的 Pages 文件夹下创建一个新的内容页面 AuthorEditPage.xaml,并按如下内容修改:

AuthorEditPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:ext="clr-namespace:Acme.BookStore.Maui.Extensions"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Acme.BookStore.Maui.Pages.AuthorEditPage"
             xmlns:viewModels="clr-namespace:Acme.BookStore.Maui.ViewModels"
             xmlns:identity="clr-namespace:Acme.BookStore.Authors;assembly=Acme.BookStore.Application.Contracts"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             x:DataType="viewModels:AuthorEditViewModel"
             Title="{ext:Translate Edit}">
    
    <ContentPage.ToolbarItems>
        <ToolbarItem Text="{ext:Translate Cancel}" Command="{Binding CancelCommand}"/>
    </ContentPage.ToolbarItems>

    <ScrollView>
        <VerticalStackLayout Padding="20" Spacing="20">
            <Border StyleClass="AbpInputContainer">
                <VerticalStackLayout>
                    <Label Text="{ext:Translate Name}" />
                    <Entry Text="{Binding Author.Name}" />
                </VerticalStackLayout>
            </Border>

            <Border StyleClass="AbpInputContainer">
                <VerticalStackLayout>
                    <Label Text="{ext:Translate ShortBio}" />
                    <Entry Text="{Binding Author.ShortBio}" />
                </VerticalStackLayout>
            </Border>

            <Border StyleClass="AbpInputContainer">
                <VerticalStackLayout>
                    <Label Text="{ext:Translate BirthDate}" />
                    <DatePicker Date="{Binding Author.BirthDate}"/>
                </VerticalStackLayout>
            </Border>

            <Grid>
                <Button Text="{ext:Translate Save}" Command="{Binding UpdateCommand}" />
                <ActivityIndicator IsRunning="{Binding IsSaving}" IsVisible="{Binding IsSaving}" />
            </Grid>
        </VerticalStackLayout>
    </ScrollView>
</ContentPage>

在此页面中,我们定义了编辑作者所需的表单元素,例如 姓名简介出生日期。每当用户点击 保存 按钮时,UpdateCommand 将被触发,如果操作成功,将更新现有作者。

让我们定义 AuthorEditViewModel 作为此页面的 BindingContext,然后定义 UpdateCommand 的逻辑。

AuthorEditPage.xaml.cs

创建 AuthorEditPage.xaml.cs 代码隐藏类,并复制粘贴以下内容:

using Acme.BookStore.Maui.ViewModels;
using Volo.Abp.DependencyInjection;

namespace Acme.BookStore.Maui.Pages;

public partial class AuthorEditPage : ContentPage, ITransientDependency
{
 public AuthorEditPage(AuthorEditViewModel vm)
 {
  InitializeComponent();
  BindingContext = vm;
 }
}

AuthorEditViewModel.cs

在项目的 ViewModels 文件夹下创建一个视图模型类 AuthorEditViewModel,并按如下内容修改:

using Acme.BookStore.Authors;
using Acme.BookStore.Maui.Messages;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Volo.Abp.DependencyInjection;

namespace Acme.BookStore.Maui.ViewModels
{
    [QueryProperty("Id", "Id")]
    public partial class AuthorEditViewModel : BookStoreViewModelBase, ITransientDependency
    {
        [ObservableProperty]
        public string? id;

        [ObservableProperty]
        private bool isBusy;

        [ObservableProperty]
        private bool isSaving;

        [ObservableProperty]
        private UpdateAuthorDto? author;

        protected IAuthorAppService AuthorAppService { get; }

        public AuthorEditViewModel(IAuthorAppService authorAppService)
        {
            AuthorAppService = authorAppService;
        }

        async partial void OnIdChanged(string? value)
        {
            IsBusy = true;
            await GetAuthor();
            IsBusy = false;
        }

        [RelayCommand]
        async Task GetAuthor()
        {
            try
            {
                var authorId = Guid.Parse(Id!);
                var authorDto = await AuthorAppService.GetAsync(authorId);

                Author = new UpdateAuthorDto
                {
                    BirthDate = authorDto.BirthDate,
                    Name = authorDto.Name,
                    ShortBio = authorDto.ShortBio
                };
            }
            catch (Exception ex)
            {
                HandleException(ex);
            }
        }

        [RelayCommand]
        async Task Cancel()
        {
            await Shell.Current.GoToAsync("..");
        }

        [RelayCommand]
        async Task Update()
        {
            try
            {
                IsSaving = true;

                await AuthorAppService.UpdateAsync(Guid.Parse(Id!), Author!);
                await Shell.Current.GoToAsync("..");
                WeakReferenceMessenger.Default.Send(new AuthorEditMessage(Author!));
            }
            catch (Exception ex)
            {
                HandleException(ex);
            }
            finally
            {
                IsSaving = false;
            }
        }
    }
}

在这里,我们执行以下步骤:

  • 此类简单地注入并使用 IAuthorAppService 来更新现有作者。
  • 我们为 AuthorEditPage 中的操作创建了三个方法,即 GetAuthorCancelUpdate 方法。
  • GetAuthor 方法用于从 Id 查询参数获取作者,并将其设置到 Author 属性。
  • Cancel 方法简单地返回到上一个页面 AuthorPage
  • Update 方法在 AuthorEditPage 上点击 保存 按钮时更新现有作者。

AuthorEditMessage.cs

在项目的 Messages 文件夹下创建一个类 AuthorEditMessage,并按如下内容修改:

using Acme.BookStore.Authors;
using CommunityToolkit.Mvvm.Messaging.Messages;

namespace Acme.BookStore.Maui.Messages
{
    public class AuthorEditMessage : ValueChangedMessage<UpdateAuthorDto>
    {
        public AuthorEditMessage(UpdateAuthorDto value) : base(value)
        {
        }
    }
}

此类用于表示我们将在作者更新后触发返回结果的消息。然后,我们订阅此消息并在 AuthorsPage 上更新网格。

将作者菜单项添加到主菜单

打开 AppShell.xaml 文件,并在 设置 菜单项下添加以下代码:

AppShell.xaml

    <FlyoutItem Title="{ext:Translate Authors}" IsVisible="{Binding HasAuthorsPermission}"
                Icon="{FontImageSource FontFamily=MaterialOutlined, Glyph={x:Static u:MaterialOutlined.Person_add}, Color={AppThemeBinding Light={StaticResource ForegroundDark}, Dark={StaticResource ForegroundLight}}}">
        <Tab>
            <ShellContent Route="authors"
                          ContentTemplate="{DataTemplate pages:AuthorsPage}"/>
        </Tab>
    </FlyoutItem>

此代码块在 设置 菜单项下添加了一个新的 作者 菜单项。我们需要仅在授予所需权限时显示此菜单项。因此,让我们更新 ShellViewModel.cs 类并检查是否授予了权限。

ShellViewModel.cs

public partial class ShellViewModel : BookStoreViewModelBase, ITransientDependency
{
    // 在下面添加这两行
    [ObservableProperty]
    bool hasAuthorsPermission = true;

    //...

    [RelayCommand]
    private async Task UpdatePermissions()
    {
        HasUsersPermission = await AuthorizationService.IsGrantedAsync(IdentityPermissions.Users.Default);
        HasTenantsPermission = await AuthorizationService.IsGrantedAsync(SaasHostPermissions.Tenants.Default);

        // 添加下面这行
        HasAuthorsPermission = await AuthorizationService.IsGrantedAsync(BookStorePermissions.Authors.Default);
    }

    //...
}

创建图书页面 - 列表与删除图书

Acme.BookStore.Maui 项目的 Pages 文件夹下创建一个新的内容页面 BooksPage.xaml,并按如下内容修改:

BooksPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Acme.BookStore.Maui.Pages.BooksPage"
             xmlns:ext="clr-namespace:Acme.BookStore.Maui.Extensions"
             xmlns:viewModels="clr-namespace:Acme.BookStore.Maui.ViewModels"
             xmlns:book="clr-namespace:Acme.BookStore.Books;assembly=Acme.BookStore.Application.Contracts"
             xmlns:u="http://schemas.enisn-projects.io/dotnet/maui/uraniumui"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             x:Name="page"
             x:DataType="viewModels:BookPageViewModel"
             Title="{ext:Translate Books}">

    <ContentPage.Behaviors>
        <toolkit:EventToCommandBehavior EventName="Appearing" Command="{Binding RefreshCommand}" />
    </ContentPage.Behaviors>

    <ContentPage.ToolbarItems>
        <ToolbarItem Text="{ext:Translate NewBook,StringFormat='+ {0}'}"
            Command="{Binding OpenCreateModalCommand}"
            IconImageSource="{OnIdiom Desktop={FontImageSource FontFamily=MaterialRegular, Glyph={x:Static u:MaterialRegular.Add}}}"/>
    </ContentPage.ToolbarItems>

    <Grid RowDefinitions="Auto,*" 
            StyleClass="Max720"
            Padding="16,16,16,0">

        <!-- 列表 -->
        <RefreshView Grid.Row="1"
            IsRefreshing="{Binding IsBusy}"
            Command="{Binding RefreshCommand}">

            <CollectionView
                ItemsSource="{Binding Items}"
                SelectionMode="None"
                RemainingItemsThreshold="2"
                RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}">
                <CollectionView.Header>
                    <!-- 顶部内边距 -->
                    <BoxView HeightRequest="16" Color="Transparent" />
                </CollectionView.Header>
                <CollectionView.ItemTemplate>
                    <DataTemplate x:DataType="book:BookDto">
                        <Grid ColumnDefinitions="*,Auto" Padding="4,0" Margin="0,8" HeightRequest="36" ColumnSpacing="10">
                            <VerticalStackLayout Grid.Column="0" VerticalOptions="Center">
                                <Label Text="{Binding Name}" FontAttributes="Bold" />
                                <Label Text="{Binding Type}" StyleClass="muted" />
                            </VerticalStackLayout>

                            <ImageButton Grid.Column="1" VerticalOptions="Center" HeightRequest="24" WidthRequest="24" BackgroundColor="Transparent"
                                Command="{Binding BindingContext.ShowActionsCommand, Source={x:Reference page}}"
                                CommandParameter="{Binding .}">
                                <ImageButton.Source>
                                    <FontImageSource FontFamily="MaterialRegular"
                                        Glyph="{x:Static u:MaterialRegular.More_vert}"
                                        Color="{AppThemeBinding Light={StaticResource ForegroundDark}, Dark={StaticResource ForegroundLight}}" />
                                </ImageButton.Source>
                            </ImageButton>
                        </Grid>
                    </DataTemplate>
                </CollectionView.ItemTemplate>

                <CollectionView.EmptyView>
                    <Image Source="empty.png" 
                           MaximumWidthRequest="400"
                           HorizontalOptions="Center"
                           Opacity=".5"/>
                </CollectionView.EmptyView>

                <CollectionView.Footer>
                    <VerticalStackLayout>
                        <ActivityIndicator HorizontalOptions="Center"
                             IsRunning="{Binding IsLoadingMore}" IsVisible="{Binding IsLoadingMore}"
                             Margin="20"/>

                        <ContentView Margin="0,0,0,8" IsVisible="{OnIdiom Default=False, Desktop=True}" HorizontalOptions="Center">
                            <Button IsVisible="{Binding CanLoadMore}" StyleClass="TextButton" Text="{ext:Translate LoadMore}"
                                Command="{Binding LoadMoreCommand}"  />
                        </ContentView>
                    </VerticalStackLayout>
                </CollectionView.Footer>
            </CollectionView>
        </RefreshView>
    </Grid>
</ContentPage>

这是一个简单的页面,用于列出图书,允许打开创建模态框来新建图书、编辑现有图书以及删除图书。

BooksPage.xaml.cs

创建 BooksPage.xaml.cs 代码隐藏类,并复制粘贴以下内容:

using Acme.BookStore.Maui.ViewModels;
using Volo.Abp.DependencyInjection;

namespace Acme.BookStore.Maui.Pages;

public partial class BooksPage : ContentPage, ITransientDependency
{
 public BooksPage(BookPageViewModel vm)
 {
  InitializeComponent();
  BindingContext = vm;
 }
}

BookPageViewModel.cs

在项目的 ViewModels 文件夹下创建一个视图模型类 BookPageViewModel,并按如下内容修改:

using Acme.BookStore.Books;
using Acme.BookStore.Maui.Messages;
using Acme.BookStore.Maui.Pages;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using System.Collections.ObjectModel;
using Volo.Abp.Application.Dtos;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client;
using Volo.Abp.Threading;

namespace Acme.BookStore.Maui.ViewModels
{
    public partial class BookPageViewModel : BookStoreViewModelBase,
        IRecipient<BookCreateMessage>, // 创建
        IRecipient<BookEditMessage>, // 编辑
        ITransientDependency
    {
        [ObservableProperty]
        bool isBusy;

        [ObservableProperty]
        bool isLoadingMore;

        [ObservableProperty]
        bool canLoadMore = true;

        public ObservableCollection<BookDto> Items { get; } = new();

        public PagedAndSortedResultRequestDto Input { get; } = new();

        protected IBookAppService BookAppService { get; }

        protected SemaphoreSlim SemaphoreSlim { get; } = new SemaphoreSlim(1, 1);

        public BookPageViewModel(IBookAppService bookAppService)
        {
            BookAppService = bookAppService;

            WeakReferenceMessenger.Default.Register<BookCreateMessage>(this);
            WeakReferenceMessenger.Default.Register<BookEditMessage>(this);
        }

        [RelayCommand]
        async Task OpenCreateModal()
        {
            await Shell.Current.GoToAsync(nameof(BookCreatePage));
        }

        [RelayCommand]
        async Task OpenEditModal(Guid id)
        {
            await Shell.Current.GoToAsync($"{nameof(BookEditPage)}?Id={id}");
        }

        [RelayCommand]
        async Task Refresh()
        {
            try
            {
                IsBusy = true;

                using (await SemaphoreSlim.LockAsync())
                {
                    Input.SkipCount = 0;

                    var books = await BookAppService.GetListAsync(Input);
                    Items.Clear();

                    foreach (var book in books.Items)
                    {
                        Items.Add(book);
                    }

                    CanLoadMore = books.Items.Count >= Input.MaxResultCount;
                }
            }
            catch (AbpRemoteCallException remoteException)
            {
                HandleException(remoteException);
            }
            finally
            {
                IsBusy = false;
            }
        }

        [RelayCommand]
        async Task LoadMore()
        {
            if (!CanLoadMore)
            {
                return;
            }

            try
            {
                using (await SemaphoreSlim.LockAsync())
                {
                    IsLoadingMore = true;

                    Input.SkipCount += Input.MaxResultCount;

                    var books = await BookAppService.GetListAsync(Input);

                    CanLoadMore = books.Items.Count >= Input.MaxResultCount;

                    foreach (var book in books.Items)
                    {
                        Items.Add(book);
                    }
                }
            }
            catch (Exception ex)
            {
                HandleException(ex);
            }
            finally
            {
                IsLoadingMore = false;
            }
        }

        [RelayCommand]
        async Task ShowActions(BookDto entity)
        {
            var result = await App.Current!.MainPage!.DisplayActionSheet(
                L["Actions"],
                L["Cancel"],
                null,
                L["Edit"], L["Delete"]);

            if (result == L["Edit"])
            {
                await OpenEditModal(entity.Id);
            }

            if (result == L["Delete"])
            {
                await Delete(entity);
            }
        }

        [RelayCommand]
        async Task Delete(BookDto entity)
        {
            if (Application.Current is { MainPage: { } })
            {
                var confirmed = await Shell.Current.CurrentPage.DisplayAlert(
                    L["Delete"],
                    string.Format(L["BookDeletionConfirmationMessage"], entity.Name),
                    L["Delete"],
                    L["Cancel"]);

                if (!confirmed)
                {
                    return;
                }

                try
                {
                    await BookAppService.DeleteAsync(entity.Id);
                }
                catch (AbpRemoteCallException remoteException)
                {
                    HandleException(remoteException);
                }

                await Refresh();
            }
        }

        public async void Receive(BookCreateMessage message)
        {
            await Refresh();
        }

        public async void Receive(BookEditMessage message)
        {
            await Refresh();
        }
    }
}

BookPageViewModel 类是 Books 页面背后所有逻辑的所在。在这里,我们执行以下步骤:

  • 我们从数据库获取所有图书,并将这些记录设置到 Items 属性中,该属性是 ObservableCollection<BookDto> 类型,因此每当图书列表更改时,CollectionView 将被刷新。
  • 我们定义了 OpenCreateModalOpenEditModal 方法,用于导航到创建模态框和编辑模态框页面(我们将在后续章节中创建它们)。
  • 我们定义了 ShowActions 方法,允许编辑或删除特定图书。
  • 我们创建了 Delete 方法,用于删除特定图书并重新渲染网格。
  • 最后,我们实现了 IRecipient<BookCreateMessage>IRecipient<BookEditMessage> 接口,以便在创建新图书或编辑现有图书后刷新网格。(我们将在后续章节中创建 BookCreateMessageBookEditMessage 类)

注册图书页面路由

打开 Acme.BookStore.Maui 项目下的 AppShell.xaml.cs 文件,并注册创建模态框和编辑模态框页面的路由:

using Acme.BookStore.Maui.Pages;
using Acme.BookStore.Maui.ViewModels;
using Volo.Abp.DependencyInjection;

namespace Acme.BookStore.Maui;

public partial class AppShell : Shell, ITransientDependency
{
    public AppShell(ShellViewModel vm)
    {
        BindingContext = vm;

        InitializeComponent();

        // 其他路由...

        // 作者
        Routing.RegisterRoute(nameof(AuthorCreatePage), typeof(AuthorCreatePage));
        Routing.RegisterRoute(nameof(AuthorEditPage), typeof(AuthorEditPage));

        // 图书 - 注册图书页面路由
        Routing.RegisterRoute(nameof(BookCreatePage), typeof(BookCreatePage));
        Routing.RegisterRoute(nameof(BookEditPage), typeof(BookEditPage));
    }
}

由于我们需要在点击操作按钮时导航到创建模态框和编辑模态框页面,我们需要使用它们的路由注册这些页面。我们可以在 AppShell.xaml.cs 文件中完成此操作,该文件负责提供应用程序的导航功能。

创建新图书

Acme.BookStore.Maui 项目的 Pages 文件夹下创建一个新的内容页面 BookCreatePage.xaml,并按如下内容修改:

BookCreatePage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:ext="clr-namespace:Acme.BookStore.Maui.Extensions"
             x:Class="Acme.BookStore.Maui.Pages.BookCreatePage"
             xmlns:viewModels="clr-namespace:Acme.BookStore.Maui.ViewModels"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             x:DataType="viewModels:BookCreateViewModel"
             Title="{ext:Translate NewBook}">

    <ContentPage.Behaviors>
        <toolkit:EventToCommandBehavior EventName="Appearing" Command="{Binding GetAuthorsCommand}"/>
    </ContentPage.Behaviors>

    <ContentPage.Resources>
        <ResourceDictionary>
            <toolkit:IsEqualConverter x:Key="IsEqualConverter" />
        </ResourceDictionary>
    </ContentPage.Resources>

    <ContentPage.ToolbarItems>
        <ToolbarItem Text="{ext:Translate Cancel}" Command="{Binding CancelCommand}"/>
    </ContentPage.ToolbarItems>

    <ScrollView>
        <VerticalStackLayout Padding="20" Spacing="20">

            <Border StyleClass="AbpInputContainer">
                <VerticalStackLayout>
                    <Label Text="{ext:Translate Name}" />
                    <Entry Text="{Binding Book.Name}" />
                </VerticalStackLayout>
            </Border>

            <Border StyleClass="AbpInputContainer">
                <VerticalStackLayout>
                    <Label Text="{ext:Translate Type}" />
                    <Picker x:Name="bookType" ItemsSource="{Binding Types}" SelectedItem="{Binding Book.Type}"/>
                </VerticalStackLayout>
            </Border>

            <Border StyleClass="AbpInputContainer">
                <VerticalStackLayout>
                    <Label Text="{ext:Translate AuthorName}" />
                    <Picker ItemsSource="{Binding Authors}" SelectedItem="{Binding SelectedAuthor}" ItemDisplayBinding="{Binding Name}"/>
                </VerticalStackLayout>
            </Border>

            <Border StyleClass="AbpInputContainer">
                <VerticalStackLayout>
                    <Label Text="{ext:Translate PublishDate}" />
                    <DatePicker Date="{Binding Book.PublishDate}"/>
                </VerticalStackLayout>
            </Border>

            <Grid>
                <Button Text="{ext:Translate Save}" Command="{Binding CreateCommand}" />
                <ActivityIndicator IsRunning="{Binding IsBusy}" IsVisible="{Binding IsBusy}" />
            </Grid>
        </VerticalStackLayout>
    </ScrollView>
</ContentPage>

在此页面中,我们定义了创建图书所需的表单元素,例如 名称类型作者Id出版日期。每当用户点击 保存 按钮时,CreateCommand 将被触发,如果操作成功,将创建一个新图书。

让我们定义 BookCreateViewModel 作为此页面的 BindingContext,然后定义 CreateCommand 的逻辑。

BookCreatePage.xaml.cs

using Acme.BookStore.Maui.ViewModels;
using Volo.Abp.DependencyInjection;

namespace Acme.BookStore.Maui.Pages;

public partial class BookCreatePage : ContentPage, ITransientDependency
{
 public BookCreatePage(BookCreateViewModel vm)
 {
  InitializeComponent();
  BindingContext = vm;
 }
}

BookCreateViewModel.cs

在项目的 ViewModels 文件夹下创建一个视图模型类 BookCreateViewModel,并按如下内容修改:

using Acme.BookStore.Books;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using System.Collections.ObjectModel;
using Volo.Abp.DependencyInjection;
using Acme.BookStore.Maui.Messages;

namespace Acme.BookStore.Maui.ViewModels
{
    public partial class BookCreateViewModel : BookStoreViewModelBase, ITransientDependency
    {
        [ObservableProperty]
        private bool isBusy;

        [ObservableProperty]
        private ObservableCollection<AuthorLookupDto> authors = default!;

        [ObservableProperty]
        private AuthorLookupDto selectedAuthor = default!;

        public CreateUpdateBookDto Book { get; set; } = new();

        public BookType[] Types { get; } = new[]
        {
            BookType.Undefined,
            BookType.Adventure,
            BookType.Biography,
            BookType.Poetry,
            BookType.Fantastic,
            BookType.ScienceFiction,
            BookType.Science,
            BookType.Dystopia,
            BookType.Horror
        };

        protected IBookAppService BookAppService { get; }

        public BookCreateViewModel(IBookAppService bookAppService)
        {
            BookAppService = bookAppService;
        }

        [RelayCommand]
        async Task GetAuthors()
        {
            try
            {
                Authors = new((await BookAppService.GetAuthorLookupAsync()).Items);
            }
            catch (Exception ex)
            {
                HandleException(ex);
            }
        }

        [RelayCommand]
        async Task Cancel()
        {
            await Shell.Current.GoToAsync("..");
        }

        [RelayCommand]
        async Task Create()
        {
            try
            {
                IsBusy = true;
                Book.AuthorId = SelectedAuthor!.Id;

                await BookAppService.CreateAsync(Book);
                await Shell.Current.GoToAsync("..");

                WeakReferenceMessenger.Default.Send(new BookCreateMessage(Book));
            }
            catch (Exception ex)
            {
                HandleException(ex);
            }
            finally
            {
                IsBusy = false;
            }
        }
    }
}

在这里,我们执行以下步骤:

  • 此类简单地注入并使用 IBookAppService 来创建新图书。
  • 我们为 BookCreatePage 中的操作创建了两个方法,即 CancelCreate 方法。
  • Cancel 方法简单地返回到上一个页面 BooksPage
  • Create 方法在 BookCreatePage 上点击 保存 按钮时创建新图书。

BookCreateMessage.cs

在项目的 Messages 文件夹下创建一个类 BookCreateMessage,并按如下内容修改:

using Acme.BookStore.Books;
using CommunityToolkit.Mvvm.Messaging.Messages;

namespace Acme.BookStore.Maui.Messages
{
    public class BookCreateMessage : ValueChangedMessage<CreateUpdateBookDto>
    {
        public BookCreateMessage(CreateUpdateBookDto value) : base(value)
        {
        }
    }
}

此类用于表示我们将在图书创建后触发返回结果的消息。然后,我们订阅此消息并在 BooksPage 上更新网格。

更新图书

Acme.BookStore.Maui 项目的 Pages 文件夹下创建一个新的内容页面 BookEditPage.xaml,并按如下内容修改:

BookEditPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:ext="clr-namespace:Acme.BookStore.Maui.Extensions"
             x:Class="Acme.BookStore.Maui.Pages.BookEditPage"
             xmlns:viewModels="clr-namespace:Acme.BookStore.Maui.ViewModels"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             x:DataType="viewModels:BookEditViewModel"
             Title="{ext:Translate Edit}">
    
    <ContentPage.Resources>
        <ResourceDictionary>
            <toolkit:IsEqualConverter x:Key="IsEqualConverter" />
        </ResourceDictionary>
    </ContentPage.Resources>

    <ContentPage.ToolbarItems>
        <ToolbarItem Text="{ext:Translate Cancel}" Command="{Binding CancelCommand}"/>
    </ContentPage.ToolbarItems>

    <ScrollView>
        <VerticalStackLayout Padding="20" Spacing="20">

            <Border StyleClass="AbpInputContainer">
                <VerticalStackLayout>
                    <Label Text="{ext:Translate Name}" />
                    <Entry Text="{Binding Book.Name}" />
                </VerticalStackLayout>
            </Border>

            <Border StyleClass="AbpInputContainer">
                <VerticalStackLayout>
                    <Label Text="{ext:Translate Type}" />
                    <Picker x:Name="bookType" ItemsSource="{Binding Types}" SelectedItem="{Binding Book.Type}" />
                </VerticalStackLayout>
            </Border>

            <Border StyleClass="AbpInputContainer">
                <VerticalStackLayout>
                    <Label Text="{ext:Translate AuthorName}" />
                    <Picker ItemsSource="{Binding Authors}" SelectedItem="{Binding SelectedAuthor}" ItemDisplayBinding="{Binding Name}"/>
                </VerticalStackLayout>
            </Border>

            <Border StyleClass="AbpInputContainer">
                <VerticalStackLayout>
                    <Label Text="{ext:Translate PublishDate}" />
                    <DatePicker Date="{Binding Book.PublishDate}"/>
                </VerticalStackLayout>
            </Border>

            <Grid>
                <Button Text="{ext:Translate Save}" Command="{Binding UpdateCommand}" />
                <ActivityIndicator IsRunning="{Binding IsBusy}" IsVisible="{Binding IsBusy}" />
            </Grid>
        </VerticalStackLayout>
    </ScrollView>
</ContentPage>

在此页面中,我们定义了编辑图书所需的表单元素,例如 名称类型作者Id出版日期。每当用户点击 保存 按钮时,UpdateCommand 将被触发,如果操作成功,将更新现有图书。

让我们定义 BookEditViewModel 作为此页面的 BindingContext,然后定义 UpdateCommand 的逻辑。

BookEditPage.xaml.cs

using Acme.BookStore.Maui.ViewModels;
using Volo.Abp.DependencyInjection;

namespace Acme.BookStore.Maui.Pages;

public partial class BookEditPage : ContentPage, ITransientDependency
{
 public BookEditPage(BookEditViewModel vm)
 {
  InitializeComponent();
  BindingContext = vm;
 }
}

BookEditViewModel.cs

在项目的 ViewModels 文件夹下创建一个视图模型类 BookEditViewModel,并按如下内容修改:

using Acme.BookStore.Books;
using Acme.BookStore.Maui.Messages;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using System.Collections.ObjectModel;
using Volo.Abp.DependencyInjection;

namespace Acme.BookStore.Maui.ViewModels
{
    [QueryProperty("Id", "Id")]
    public partial class BookEditViewModel : BookStoreViewModelBase, ITransientDependency
    {
        [ObservableProperty]
        public string? id;

        [ObservableProperty]
        private bool isBusy;

        [ObservableProperty]
        private bool isSaving;

        [ObservableProperty]
        private ObservableCollection<AuthorLookupDto> authors = new();

        [ObservableProperty]
        private AuthorLookupDto? selectedAuthor;

        [ObservableProperty]
        public CreateUpdateBookDto book;

        public BookType[] Types { get; } = new[]
        {
            BookType.Undefined,
            BookType.Adventure,
            BookType.Biography,
            BookType.Poetry,
            BookType.Fantastic,
            BookType.ScienceFiction,
            BookType.Science,
            BookType.Dystopia,
            BookType.Horror
        };

        protected IBookAppService BookAppService { get; }

        public BookEditViewModel(IBookAppService bookAppService)
        {
            BookAppService = bookAppService;
        }

        async partial void OnIdChanged(string? value)
        {
            IsBusy = true;
            await GetBook();
            await GetAuthors();
            IsBusy = false;
        }

        [RelayCommand]
        async Task GetBook()
        {
            try
            {
                var bookId = Guid.Parse(Id!);
                var bookDto = await BookAppService.GetAsync(bookId);

                Book = new CreateUpdateBookDto
                {
                    AuthorId = bookDto.AuthorId,
                    Name = bookDto.Name,
                    Price = bookDto.Price,
                    PublishDate = bookDto.PublishDate,
                    Type = bookDto.Type
                };
            }
            catch (Exception ex)
            {
                HandleException(ex);
            }
        }

        [RelayCommand]
        async Task GetAuthors()
        {
            try
            {
                Authors = new((await BookAppService.GetAuthorLookupAsync()).Items);
                SelectedAuthor = Authors.FirstOrDefault(x => x.Id == Book?.AuthorId);
            }
            catch (Exception ex)
            {
                HandleException(ex);
            }
        }

        [RelayCommand]
        async Task Cancel()
        {
            await Shell.Current.GoToAsync("..");
        }

        [RelayCommand]
        async Task Update()
        {
            try
            {
                IsSaving = true;
                Book!.AuthorId = SelectedAuthor!.Id;

                await BookAppService.UpdateAsync(Guid.Parse(Id!), Book);
                await Shell.Current.GoToAsync("..");
                WeakReferenceMessenger.Default.Send(new BookEditMessage(Book));
            }
            catch (Exception ex)
            {
                HandleException(ex);
            }
            finally
            {
                IsSaving = false;
            }
        }
    }
}

在这里,我们执行以下步骤:

  • 此类简单地注入并使用 IBookAppService 来更新现有图书。
  • 我们为 BookEditPage 中的操作创建了四个方法,即 GetBookGetAuthorsCancelUpdate 方法。
  • GetBook 方法用于从 Id 查询参数获取图书,并将其设置到 Book 属性。
  • GetAuthors 方法用于获取作者查找列表,以便在选取器中列出作者。
  • Cancel 方法简单地返回到上一个页面 BooksPage
  • Update 方法在 BookEditPage 上点击 保存 按钮时更新现有图书。

BookEditMessage.cs

在项目的 Messages 文件夹下创建一个类 BookEditMessage,并按如下内容修改:

using Acme.BookStore.Books;
using CommunityToolkit.Mvvm.Messaging.Messages;

namespace Acme.BookStore.Maui.Messages
{
    public class BookEditMessage : ValueChangedMessage<CreateUpdateBookDto>
    {
        public BookEditMessage(CreateUpdateBookDto value) : base(value)
        {
        }
    }
}

此类用于表示我们将在图书更新后触发返回结果的消息。然后,我们订阅此消息并在 BooksPage 上更新网格。

将图书菜单项添加到主菜单

打开 AppShell.xaml 文件,并在 作者 菜单项前添加以下代码:

AppShell.xaml

    <FlyoutItem Title="{ext:Translate Books}" IsVisible="{Binding HasBooksPermission}"
                Icon="{FontImageSource FontFamily=MaterialOutlined, Glyph={x:Static u:MaterialOutlined.Book}, Color={AppThemeBinding Light={StaticResource ForegroundDark}, Dark={StaticResource ForegroundLight}}}">
        <Tab>
            <ShellContent Route="books"
                          ContentTemplate="{DataTemplate pages:BooksPage}"/>
        </Tab>
    </FlyoutItem>

此代码块在 作者 菜单项前添加了一个新的 图书 菜单项。我们需要仅在授予所需权限时显示此菜单项。因此,让我们更新 ShellViewModel.cs 类并检查是否授予了权限。

ShellViewModel.cs

using Acme.BookStore.Permissions;

public partial class ShellViewModel : BookStoreViewModelBase, ITransientDependency
{
    // 在下面添加这两行
    [ObservableProperty]
    bool hasBooksPermission = true;

    //...

    [RelayCommand]
    private async Task UpdatePermissions()
    {
        HasUsersPermission = await AuthorizationService.IsGrantedAsync(IdentityPermissions.Users.Default);
        HasTenantsPermission = await AuthorizationService.IsGrantedAsync(SaasHostPermissions.Tenants.Default);
        HasAuthorsPermission = await AuthorizationService.IsGrantedAsync(BookStorePermissions.Authors.Default);

        // 添加下面这行
        HasBooksPermission = await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Default);
    }

    //...
}

运行应用程序

就这样!您可以运行应用程序并登录。然后您可以浏览页面,列出、创建、更新和/或删除作者和图书。

在本文档中