将Abp移植进.NET MAUI项目

前言

写在.NET MAUI官宣正式发布之际,热烈庆祝MAUI正式发布!

去年12月份做了MAUI混合开发框架的调研,想起来文章里给自己挖了个坑,要教大家如何把Abp移植进Maui项目。

熟悉Abp的同学都知道,Abp 是一套强大的应用程序设计时框架(俗称脚手架),新版本的Abp vNext为微服务和网络优化的更多,然而本地开发经典Abp已经够用,而且官方没有停止维护,因此使用这个框架.

MAUI则是跨平台的应用程序抽象层,强大的运行时框架 + 强大的设计时框架 , 我说这是宇宙最强大跨平台开发框架,不为过吧?

计划:

  • 整个程序我们还是利用Mvvm设计模式,但是将利用Abp的Ioc容器,而不使用mvvmlight或者xamarinToolkit这些库,自行编写一个ViewModelBase

  • 使用Abp.EntityFrameworkCore库中的EF相关功能,使用sqlite作为数据持久化方案。

目标:编写一个歌单App,对歌曲信息进行增、删、查、改。

将Abp移植进.NET MAUI项目将Abp移植进.NET MAUI项目

下面来看看如何搭建

搭建MAUI项目

请注意:本文发布时,MAUI处于RC3版本,仍没有正式发布,需要安装Visual Studio 2022 17.3 (Preview)

首先按照官方教程搭建一个MAUI项目, 命名为MauiBoilerplateBuild your first .NET MAUI app - .NET MAUI | Microsoft Docs

再前往Abp官网生成一个项目 
Startup Templates - Create a Demo | AspNet Boilerplate

  • 选择最新版本 v7.x 和.Net 6版本

  • 取消勾选“Include login, register, user, role and tenant management pages”

  • 项目名称中填入MauiBoilerplate与Maui项目保持一致

将Abp移植进.NET MAUI项目将Abp移植进.NET MAUI项目

点击“Create My Project”生成abp项目文件,等待下载完成

下载,解压好后,打开src目录可以发现4个项目目录,我们仅需要Core和EntityFrameworkCore项目,将这两个目录移至项目根目录,并且添加至解决方案。

将Abp移植进.NET MAUI项目将Abp移植进.NET MAUI项目

配置应用入口点

在MauiBoilerplate.Core项目中

改写默认配置文件

{
  "ConnectionStrings": {
    "Default": "Data Source=file:{0};"
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

在MauiBoilerplate.Core.csproj中的ItemGroup节点下添加

	  <EmbeddedResource Include="appsettings.json">
	    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
	  </EmbeddedResource>

在MauiBoilerplate.Core项目中新建MauiBoilerplateBuilderExtensions.cs 作为程序入口

添加一个静态方法InitConfig,用于读取项目的配置文件appsettings.json,若第一次运行或者该文件不存在则读取默认的配置文件

        private static void InitConfig(string logCfgName, string documentsPath)
        {

            var assembly = IntrospectionExtensions.GetTypeInfo(typeof(MauiBoilerplateBuilderExtensions)).Assembly;

            Stream stream = assembly.GetManifestResourceStream($"MauiBoilerplate.Core.{logCfgName}");
            string text = "";
            using (var reader = new System.IO.StreamReader(stream))
            {
                text = reader.ReadToEnd();
            }
            if (DirFileHelper.IsExistFile(documentsPath))
            {
                var currentFileContent = DirFileHelper.ReadFile(documentsPath);
                var isSameContent = currentFileContent.ToMd5() == text.ToMd5();
                if (isSameContent)
                {
                    return;
                }
                DirFileHelper.CreateFile(documentsPath, text);

            }
            else
            {
                DirFileHelper.CreateFile(documentsPath, text);

            }
        }

添加一个静态方法InitDataBase用于初始化sqlite数据库文件"mato.db"

    private static void InitDataBase(string dbName, string documentsPath)
    {
            var assembly = IntrospectionExtensions.GetTypeInfo(typeof(MauiBoilerplateBuilderExtensions)).Assembly;
            Stream stream = assembly.GetManifestResourceStream($"MauiBoilerplate.Core.{dbName}");
            StreamHelper.WriteStream(stream, documentsPath);

            var path = Path.GetDirectoryName(documentsPath);
            DirFileHelper.CreateDir(path);
    }

添加一个 静态方法UseMauiBoilerplate用于初始化配置文件,初始化db文件和向管道服务中注册AbpBootstrapper实例。

        public static MauiAppBuilder UseMauiBoilerplate<TStartupModule>(this MauiAppBuilder builder) where TStartupModule : AbpModule
        {
            var logCfgName = "log4net.config";
            var appCfgName = "appsettings.json";
            var dbName = "mato.db";

            string documentsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), MauiBoilerplateConsts.LocalizationSourceName, logCfgName);
            string documentsPath2 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), MauiBoilerplateConsts.LocalizationSourceName, appCfgName);
            string dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), MauiBoilerplateConsts.LocalizationSourceName, dbName);

            InitConfig(logCfgName, documentsPath);
            InitConfig(appCfgName, documentsPath2);
            InitDataBase(dbName, dbPath);
            var _bootstrapper = AbpBootstrapper.Create<TStartupModule>(options =>
            {
                options.IocManager = new IocManager();
            });
            _bootstrapper.IocManager.IocContainer.AddFacility<LoggingFacility>(f => f.UseAbpLog4Net().WithConfig(documentsPath));

            builder.Services.AddSingleton(_bootstrapper);
            WindsorRegistrationHelper.CreateServiceProvider(_bootstrapper.IocManager.IocContainer, builder.Services);

            return builder;
        }

在MauiBoilerplate项目中

新建MauiBoilerplateModule.cs ,并编写代码如下,这是App起始模块

[DependsOn(typeof(MauiBoilerplateEntityFrameworkCoreModule))]
    public class MauiBoilerplateModule : AbpModule
    {
        public override void Initialize()
        {
            IocManager.RegisterAssemblyByConvention(typeof(MauiBoilerplateModule).GetAssembly());
        }

    }

打开MauiProgram.cs文件,将UseMauiBoilerplate添加到MauiAppBuilder

这里提一下, MAUI 应用跟其他.Net6应用一样采用泛型主机启动应用,在项目中有一个静态MauiProgram类,这是应用的入口点。这提供了从单个位置配置应用、服务和第三方库的功能。

更多泛型主机的信息,请参阅微软文档.NET 通用主机 | Microsoft Docs

将Abp移植进.NET MAUI项目将Abp移植进.NET MAUI项目

 至此,在主机管道中已经配置了MauiBoilerplate服务

配置Abp

App.xaml是应用的声明起始点,将从这里初始化Abp

打开App.xaml.cs,添加如下代码:

public partial class App : Application
    {
        private readonly AbpBootstrapper _abpBootstrapper;

        public App(AbpBootstrapper abpBootstrapper)
        {
            _abpBootstrapper = abpBootstrapper;
            InitializeComponent();
            _abpBootstrapper.Initialize();
            this.MainPage = abpBootstrapper.IocManager.Resolve(typeof(MainPage)) as MainPage;
        }
    }

注意,我们还没有创建初始页面MainPage,你可以先创建这个文件,将在第三章讲UI层时介绍

至此,就完成了MAUI项目的搭建与Abp脚手架的集成,现在你可以在这个项目中使用Abp的IocManager,ConfigurationManager,工作单元特性,模组化特性,等等任何的Abp提供的功能了。

但是距离目标:制作一个具有数据访问层的App,还需要两段路要走:配置数据库,以及编写界面。

因为我们要做一个数据持久化型的小应用,所以在完成Abp功能的集成后,我们需要做数据库相关的配置工作

配置数据库

在MauiBoilerplate.Core项目中,添加两个实体类:

我们简单的写一个歌曲(song)的实体类

其中包含了歌曲标题(MusicTitle),艺术家(Artist),专辑(Album),时长(Duration)以及发售日期(ReleaseDate)

    public class Song : FullAuditedEntity<long>
    {
        [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public override long Id { get; set; }

        public string MusicTitle { get; set; }

        public string Artist { get; set; }

        public string Album { get; set; }

        public TimeSpan Duration { get; set; }

        public DateTime ReleaseDate { get; set; }

    }

在MauiBoilerplate.EntityFrameworkCore项目中:将这个类添加至MauiBoilerplateDbContext中

public class MauiBoilerplateDbContext : AbpDbContext
{
     //Add DbSet properties for your entities...
     public DbSet<Song> Song { get; set; }
}

新建WithDbContextHelper.cs

创建一个静态类WithDbContext,利用Abp的工作单元模式对dbcontext执行操作

    public class WithDbContextHelper
    {
        public static void WithDbContext<TDbContext>(IIocResolver iocResolver, Action<TDbContext> contextAction)
    where TDbContext : DbContext
        {
            using (var uowManager = iocResolver.ResolveAsDisposable<IUnitOfWorkManager>())
            {
                using (var uow = uowManager.Object.Begin(TransactionScopeOption.Suppress))
                {
                    var context = uowManager.Object.Current.GetDbContext<TDbContext>();

                    contextAction(context);

                    uow.Complete();
                }
            }
        }

    }

[可选]种子数据相关类编写

编写种子数据帮助类SeedHelper.cs,与数据库初始化类InitialDbBuilder,这里将在程序启动时向数据库插入一些种子数据

    public static class SeedHelper
    {
        public static void SeedHostDb(IIocResolver iocResolver)
        {
            Helper.WithDbContextHelper.WithDbContext<MauiBoilerplateDbContext>(iocResolver, SeedHostDb);
        }

        public static void SeedHostDb(MauiBoilerplateDbContext context)
        {
            context.SuppressAutoSetTenantId = true;

            // Host seed
            new InitialDbBuilder(context).Create();
        }

    }

编写MauiBoilerplateEntityFrameworkCoreModule.cs

    [DependsOn(
        typeof(MauiBoilerplateCoreModule), 
        typeof(AbpEntityFrameworkCoreModule))]
    public class MauiBoilerplateEntityFrameworkCoreModule : AbpModule
    {
        public bool SkipDbContextRegistration { get; set; }

        public bool SkipDbSeed { get; set; }

        public override void PreInitialize()
        {
            if (!SkipDbContextRegistration)
            {
                Configuration.Modules.AbpEfCore().AddDbContext<MauiBoilerplateDbContext>(options =>
                {
                    if (options.ExistingConnection != null)
                    {
                        DbContextOptionsConfigurer.Configure(options.DbContextOptions, options.ExistingConnection);
                    }
                    else
                    {
                        DbContextOptionsConfigurer.Configure(options.DbContextOptions, options.ConnectionString);
                    }
                });
            }
        }
        public override void Initialize()
        {
 
            IocManager.RegisterAssemblyByConvention(typeof(MauiBoilerplateEntityFrameworkCoreModule).GetAssembly());
            
        }

        public override void PostInitialize()
        {
            Helper.WithDbContextHelper.WithDbContext<MauiBoilerplateDbContext>(IocManager, RunMigrate);
            if (!SkipDbSeed)
            {
                SeedHelper.SeedHostDb(IocManager);
            }
        }

        public static void RunMigrate(MauiBoilerplateDbContext dbContext)
        {
            dbContext.Database.Migrate();
        }


    }

将MauiBoilerplate.EntityFrameworkCore设置为启动项目,选择框架为.net6.0

打开程序包管理器控制台,选择默认项目MauiBoilerplate.EntityFrameworkCore

将Abp移植进.NET MAUI项目将Abp移植进.NET MAUI项目

 运行Add-Migration命令,将生成迁移脚本

运行MauiBoilerplate.EntityFrameworkCore,将生成mato.db等三个文件,

将Abp移植进.NET MAUI项目

编写基类(可选)

我们在使用相关的父类时,某某ContentPage,或者某某UserControl时,需要像使用AbpServiceBase一样使用一些常用的功能,比如字符串的本地化,配置,AutoMapper对象等,就像AbpServiceBase的注释里描述的那样:

    /// <summary>
    /// This class can be used as a base class for services.
    /// It has some useful objects property-injected and has some basic methods
    /// most of services may need to.
    /// </summary>

此时,需要编写一个基类(奈何.net本身没有Mixin模式,C#语言也不支持多继承),这些基类仅是注入了一些常用的Manager,方便代码编写者使用,因此基类的创建不是必须的。

比如可以增加一个ContentPageBase类作为ContentPage实例控件的基类

新建ContentPageBase.cs文件,创建类ContentPageBase继承于ContentPage

    public class ContentPageBase : ContentPage
    {
        public IObjectMapper ObjectMapper { get; set; }


        /// <summary>
        /// Reference to the setting manager.
        /// </summary>
        public ISettingManager SettingManager { get; set; }


        /// <summary>
        /// Reference to the localization manager.
        /// </summary>
        public ILocalizationManager LocalizationManager { get; set; }

        /// <summary>
        /// Gets/sets name of the localization source that is used in this application service.
        /// It must be set in order to use <see cref="L(string)"/> and <see cref="L(string,CultureInfo)"/> methods.
        /// </summary>
        protected string LocalizationSourceName { get; set; }

        /// <summary>
        /// Gets localization source.
        /// It's valid if <see cref="LocalizationSourceName"/> is set.
        /// </summary>
        protected ILocalizationSource LocalizationSource
        {
            get
            {
                if (LocalizationSourceName == null)
                {
                    throw new AbpException("Must set LocalizationSourceName before, in order to get LocalizationSource");
                }

                if (_localizationSource == null || _localizationSource.Name != LocalizationSourceName)
                {
                    _localizationSource = LocalizationManager.GetSource(LocalizationSourceName);
                }

                return _localizationSource;
            }
        }
        private ILocalizationSource _localizationSource;


        /// <summary>
        /// Constructor.
        /// </summary>
        protected ContentPageBase()
        {
            LocalizationSourceName = MauiBoilerplateConsts.LocalizationSourceName;
            ObjectMapper = NullObjectMapper.Instance;
            LocalizationManager = NullLocalizationManager.Instance;
        }

        /// <summary>
        /// Gets localized string for given key name and current language.
        /// </summary>
        /// <param name="name">Key name</param>
        /// <returns>Localized string</returns>
        protected virtual string L(string name)
        {
            return LocalizationSource.GetString(name);
        }

        /// <summary>
        /// Gets localized string for given key name and current language with formatting strings.
        /// </summary>
        /// <param name="name">Key name</param>
        /// <param name="args">Format arguments</param>
        /// <returns>Localized string</returns>
        protected virtual string L(string name, params object[] args)
        {
            return LocalizationSource.GetString(name, args);
        }

        /// <summary>
        /// Gets localized string for given key name and specified culture information.
        /// </summary>
        /// <param name="name">Key name</param>
        /// <param name="culture">culture information</param>
        /// <returns>Localized string</returns>
        protected virtual string L(string name, CultureInfo culture)
        {
            return LocalizationSource.GetString(name, culture);
        }

        /// <summary>
        /// Gets localized string for given key name and current language with formatting strings.
        /// </summary>
        /// <param name="name">Key name</param>
        /// <param name="culture">culture information</param>
        /// <param name="args">Format arguments</param>
        /// <returns>Localized string</returns>
        protected virtual string L(string name, CultureInfo culture, params object[] args)
        {
            return LocalizationSource.GetString(name, culture, args);
        }
    }

同理,若我们使用了其他控件类时,可以增加一个Base类作为实例控件的基类的

比如Popup控件,就编写一个PopupBase基类。

在这里我们编写了两个基类

将Abp移植进.NET MAUI项目

本地化配置

新建一个TranslateExtension.cs作为Xaml标签的本地化处理类

    [ContentProperty("Text")]
    public class TranslateExtension : DomainService, IMarkupExtension
    {
        public TranslateExtension()
        {
            LocalizationSourceName = MauiBoilerplateConsts.LocalizationSourceName;

        }
        public string Text { get; set; }

        public object ProvideValue(IServiceProvider serviceProvider)
        {
            if (Text == null)
                return "";
            var translation = L(Text);          
            return translation;
        }



    }

在MauiBoilerplateLocalization.cs配置好SourceFiles 

    public static void Configure(ILocalizationConfiguration localizationConfiguration)
        {
            localizationConfiguration.Sources.Add(
                new DictionaryBasedLocalizationSource(MauiBoilerplateConsts.LocalizationSourceName,
                    new XmlEmbeddedFileLocalizationDictionaryProvider(
                        typeof(LocalizationConfigurer).GetAssembly(),
                        "MauiBoilerplate.Core.Localization.SourceFiles"
                    )
                )
            );
        }

编写ViewModelBase

为实现Mvvm设计模式,页面需要绑定一个继承于ViewModelBase的类型

在ViewModelBase中,需要实现INotifyPropertyChanged以处理绑定成员变化时候的通知消息;

ViewModelBase集成于AbpServiceBase以方便ViewModel代码编写者使用常用的功能,比如字符串的本地化,配置,AutoMapper对象等。

    public abstract class ViewModelBase : AbpServiceBase, ISingletonDependency, INotifyPropertyChanged
    {
        public ViewModelBase()
        {
            LocalizationSourceName = MauiBoilerplateConsts.LocalizationSourceName;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected PropertyChangedEventHandler PropertyChangedHandler { get; }

        public void VerifyPropertyName(string propertyName)
        {
            Type type = GetType();
            if (!string.IsNullOrEmpty(propertyName) && type.GetTypeInfo().GetDeclaredProperty(propertyName) == null)
                throw new ArgumentException("找不到属性", propertyName);
        }

        public virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChangedEventHandler propertyChanged = PropertyChanged;
            if (propertyChanged == null)
                return;
            propertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        public virtual void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)
        {
            if (PropertyChanged == null)
                return;
            string propertyName = GetPropertyName(propertyExpression);
            if (string.IsNullOrEmpty(propertyName))
                return;
            RaisePropertyChanged(propertyName);
        }

        protected static string GetPropertyName<T>(Expression<Func<T>> propertyExpression)
        {
            if (propertyExpression == null)
                throw new ArgumentNullException(nameof(propertyExpression));
            MemberExpression body = propertyExpression.Body as MemberExpression;
            if (body == null)
                throw new ArgumentException("参数不合法", nameof(propertyExpression));
            PropertyInfo member = body.Member as PropertyInfo;
            if (member == null)
                throw new ArgumentException("找不到属性", nameof(propertyExpression));
            return member.Name;
        }

    }

至此,我们完成了数据库的配置,内容页基类与 ViewModel基类的编写,接下来可以制作我们的页面了。

很开心,终于到了创建页面的时候了!

我们需要两个页面

  • MainPage 主页面

  • MusicItemPage 条目编辑页面

编写主页面

 新建一个MainPageViewModel.cs,作为MainPage的ViewModel层

    public class MainPageViewModel : ViewModelBase
    {
        private readonly IRepository<Song, long> songRepository;

        public MainPageViewModel(IRepository<Song, long> songRepository)
        {
            this.RefreshCommand=new Command(Refresh, (o) => true);
            this.DeleteCommand=new Command(Delete, (o) => true);
            this.songRepository=songRepository;

        }
        private void Delete(object obj)
        {
            songRepository.Delete(obj as Song);
        }
        private async void Refresh(object obj)
        {
            this.IsRefreshing=true;
            var getSongs = this.songRepository.GetAllListAsync();
            await getSongs.ContinueWith(r => IsRefreshing=false);
            var songs = await getSongs;
            this.Songs=new ObservableCollection<Song>(songs);
        }

        private ObservableCollection<Song> songs;

        public ObservableCollection<Song> Songs
        {
            get { return songs; }
            set
            {
                songs = value;
                RaisePropertyChanged();
            }
        }

        private Song currentSong;

        public Song CurrentSong
        {
            get { return currentSong; }
            set
            {
                currentSong = value;
                RaisePropertyChanged();
            }
        }

        private bool _isRefreshing;

        public bool IsRefreshing
        {
            get { return _isRefreshing; }
            set
            {
                _isRefreshing = value;
                RaisePropertyChanged();

            }
        }
        public Command RefreshCommand { get; set; }
        public Command DeleteCommand { get; private set; }
    }

新建一个MainPage页面

将Abp移植进.NET MAUI项目将Abp移植进.NET MAUI项目

编写Xaml为:

注意这个页面将继承MauiBoilerplate.ContentPageBase

<?xml version="1.0" encoding="utf-8" ?>
<mato:ContentPageBase xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:mato="clr-namespace:MauiBoilerplate;assembly=MauiBoilerplate.Core"
             x:Class="MauiBoilerplate.MainPage">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="155"></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>

        <Label Text="My Music" FontSize="65"></Label>
        <ListView 
                Grid.Row="1"
                ItemsSource="{Binding Songs,Mode=TwoWay}"
                x:Name="MainListView"
                RowHeight="74" 
                IsPullToRefreshEnabled="True"
                IsRefreshing="{Binding IsRefreshing}"
                RefreshCommand="{Binding RefreshCommand}"
                SelectedItem="{Binding CurrentSong,Mode=TwoWay}">
            <ListView.Header>
                <Grid HeightRequest="96">
                    <Grid.RowDefinitions>
                        <RowDefinition></RowDefinition>
                        <RowDefinition></RowDefinition>
                    </Grid.RowDefinitions>


                    <Button Clicked="AddButton_Clicked"
                            CornerRadius="100"
                            Text=""
                            HeightRequest="44"
                            WidthRequest="200"
                            FontFamily="FontAwesome"
                                ></Button>


                    <StackLayout VerticalOptions="End"
                                 Margin="0,0,0,8"
                                 Grid.Row="1"
                                 HorizontalOptions="Center"
                                 Orientation="Horizontal">
                        <Label HorizontalTextAlignment="Center"
                            FontSize="Small" 
                            Text="{Binding Songs.Count}"></Label>
                        <Label  HorizontalTextAlignment="Center"
                            FontSize="Small" 
                            Text="首歌"></Label>

                    </StackLayout>
                </Grid>
            </ListView.Header>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Grid x:Name="ModeControlLayout" 
                              VerticalOptions="CenterAndExpand">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="Auto" />
                            </Grid.ColumnDefinitions>


                            <StackLayout Grid.Column="0" 
                                             HorizontalOptions="Center" 
                                             VerticalOptions="CenterAndExpand">
                                <Label 
                                    Text="{Binding MusicTitle}"                                    
                                    HorizontalOptions="FillAndExpand" 
                                    HorizontalTextAlignment="Center" 
                                    FontSize="Body" 
                                    />
                                <Label
                                    Text="{Binding Artist}" 
                                    HorizontalOptions="FillAndExpand" 
                                    HorizontalTextAlignment="Center" 
                                    FontSize="Body" 
                                    />
                            </StackLayout>
                            <Button 
                                x:Name="MoreButton"
                                HeightRequest="44" 
                                WidthRequest="44" 
                                Margin="10"
                                Text=""
                                Clicked="SongMoreButton_OnClicked"
                                FontFamily="FontAwesome"
                                Grid.Column="1" 
                                CornerRadius="100"
                                HorizontalOptions="Center" />

                        </Grid>

                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</mato:ContentPageBase>

编写CodeBehind为:

注意将它继承ITransientDependency接口

这个页面之前提到过,已经通过IocManager.Resolve(typeof(MainPage))解析出实例并赋值给App.MainPage了。

public partial class MainPage : ContentPageBase, ITransientDependency
{
    private readonly MainPageViewModel mainPageViewModel;
    private readonly MusicItemPageViewModel musicItemPageViewModel;
    private readonly MusicItemPage musicItemPage;

    public MainPage(MainPageViewModel mainPageViewModel, MusicItemPageViewModel musicItemPageViewModel, MusicItemPage musicItemPage)
    {
        InitializeComponent();
        this.mainPageViewModel=mainPageViewModel;
        this.musicItemPageViewModel=musicItemPageViewModel;
        this.musicItemPage=musicItemPage;
        BindingContext=this.mainPageViewModel;
       
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();
        mainPageViewModel.RefreshCommand.Execute(null);

    }

    private async void SongMoreButton_OnClicked(object sender, EventArgs e)
    {
        var currentsong = (sender as BindableObject).BindingContext as Song;
        string action = await DisplayActionSheet(currentsong.MusicTitle, "取消", null, "修改", "删除");
        if (action=="修改")
        {
            musicItemPageViewModel.CurrentSong  = currentsong;
            await Navigation.PushModalAsync(musicItemPage);
        }
        else if (action=="删除")
        {
            mainPageViewModel.DeleteCommand.Execute(currentsong);
            mainPageViewModel.RefreshCommand.Execute(null);
        }
    }

    private async void AddButton_Clicked(object sender, EventArgs e)
    {
        musicItemPageViewModel.CurrentSong  = new Song();
        await Navigation.PushModalAsync(musicItemPage);
    }
}

此页面将显示一个列表,并在列表条目下可以弹出一个菜单

将Abp移植进.NET MAUI项目

 编写条目编辑页面

 新建一个MusicItemPageViewModel.cs,作为MusicItemPage的ViewModel层

 public class MusicItemPageViewModel : ViewModelBase
 {
        private readonly IIocResolver iocResolver;
        private readonly IRepository<Song, long> songRepository;

        public event EventHandler OnFinished;

        public MusicItemPageViewModel(
            IIocResolver iocResolver,
            IRepository<Song, long> songRepository)
        {
            this.CommitCommand=new Command(Commit, (o) => CurrentSong!=null);
            this.iocResolver=iocResolver;
            this.songRepository=songRepository;
            this.PropertyChanged+=MusicItemPageViewModel_PropertyChanged;
        }

        private void MusicItemPageViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName==nameof(CurrentSong))
            {
                CommitCommand.ChangeCanExecute();
            }
        }

        private void Commit(object obj)
        {
            songRepository.InsertOrUpdate(currentSong);       
        }

        private Song currentSong;

        public Song CurrentSong
        {
            get { return currentSong; }
            set
            {
                currentSong = value;
                RaisePropertyChanged();
            }
        }
  }

新建一个MusicItemPage 页面

编写Xaml为:

注意这个页面将继承MauiBoilerplate.ContentPageBase

<?xml version="1.0" encoding="utf-8" ?>
<mato:ContentPageBase xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:mato="clr-namespace:MauiBoilerplate;assembly=MauiBoilerplate.Core"
             x:Class="MauiBoilerplate.MusicItemPage">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition Height="155"></RowDefinition>
        </Grid.RowDefinitions>
        <TableView Intent="Form">
            <TableRoot>
                <TableSection Title="基础">
                    <EntryCell Label="标题"   Text="{Binding CurrentSong.MusicTitle, Mode=TwoWay}"/>
                    <EntryCell  Label="艺术家"  Text="{Binding CurrentSong.Artist, Mode=TwoWay}"/>
                    <EntryCell  Label="专辑"  Text="{Binding CurrentSong.Album, Mode=TwoWay}"/>

                </TableSection>
                <TableSection Title="其他">
                    <EntryCell  Label="时长"  Text="{Binding CurrentSong.Duration}"/>
                    <EntryCell  Label="发布日期"  Text="{Binding CurrentSong.ReleaseDate}"/>
                </TableSection>

            </TableRoot>
        </TableView>
        <Button x:Name="CommitButton"
                Grid.Row="1"
                CornerRadius="100"
                HeightRequest="44"
                WidthRequest="200"
                Text=""
                Command="{Binding CommitCommand}"
                FontFamily="FontAwesome"             
                HorizontalOptions="Center" />
    </Grid>
</mato:ContentPageBase>

编写CodeBehind为:

注意将它继承ITransientDependency接口

public partial class MusicItemPage : ContentPageBase, ITransientDependency
{
    private readonly MusicItemPageViewModel musicItemPageViewModel;

    public MusicItemPage(MusicItemPageViewModel musicItemPageViewModel)
    {
        InitializeComponent();
        this.musicItemPageViewModel=musicItemPageViewModel;
        this.musicItemPageViewModel.OnValidateErrors+=MusicItemPageViewModel_OnValidateErrors;
        this.musicItemPageViewModel.OnFinished+=MusicItemPageViewModel_OnFinished;
        BindingContext=this.musicItemPageViewModel;
        Unloaded+=MusicItemPage_Unloaded;
    }

    private async void MusicItemPageViewModel_OnFinished(object sender, EventArgs e)
    {
       await this.Navigation.PopModalAsync();
    }

    private void MusicItemPage_Unloaded(object sender, EventArgs e)
    {
        musicItemPageViewModel.CurrentSong = null;
    }

    private async void MusicItemPageViewModel_OnValidateErrors(object sender, List<System.ComponentModel.DataAnnotations.ValidationResult> e)
    {
        var content = string.Join(',', e);
        await DisplayAlert("请注意", content, "好的");
    }
}

这个页面提供歌曲条目新增和编辑的交互功能

将Abp移植进.NET MAUI项目

[可选]使用Abp校验数据功能

这个部分使用Abp的ValidationConfiguration功能校验表单数据,以展示Abp功能的使用

首先在MusicItemPageViewModel 构造函数中添加对IValidationConfiguration对象的注入

将Abp移植进.NET MAUI项目将Abp移植进.NET MAUI项目编辑

添加OnValidateErrors事件,并且在Page中订阅这个事件。此事件将在校验未通过时触发

MusicItemPageViewModel.cs中:

public event EventHandler<List<ValidationResult>> OnValidateErrors;

MusicItemPage.xaml.cs中:

        this.musicItemPageViewModel.OnValidateErrors+=MusicItemPageViewModel_OnValidateErrors;

 

    private async void MusicItemPageViewModel_OnValidateErrors(object sender, List<System.ComponentModel.DataAnnotations.ValidationResult> e)
    {
        var content = string.Join(',', e);
        await DisplayAlert("请注意", content, "好的");
    }

编写校验逻辑代码

MusicItemPageViewModel.cs中:

        protected List<ValidationResult> GetValidationErrors(Song validatingObject)
        {
            List<ValidationResult> validationErrors = new List<ValidationResult>();

            foreach (var validatorType in _configuration.Validators)
            {
                using (var validator = iocResolver.ResolveAsDisposable<IMethodParameterValidator>(validatorType))
                {
                    var validationResults = validator.Object.Validate(validatingObject);
                    validationErrors.AddRange(validationResults);
                }

            }
            return validationErrors;
        }

Commit提交方法,改造如下:

当GetValidationErrors返回的校验错误列表中有内容时,将OnValidateErrors事件Invoke

        private void Commit(object obj)
        {
            var validateErrors = GetValidationErrors(this.CurrentSong);
            if (validateErrors.Count==0)
            {
                songRepository.InsertOrUpdate(currentSong);
                this.OnFinished?.Invoke(this, EventArgs.Empty);

            }
            else
            {
                OnValidateErrors?.Invoke(this, validateErrors);
            }
        }

接下来在实体中定义校验规则,校验器将按照这些规则返回校验结果

    public class Song : FullAuditedEntity<long>, IValidatableObject
    {
        [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public override long Id { get; set; }

        [Required]
        [StringLength(6, ErrorMessage = "歌曲名称要在6个字以内")]
        public string MusicTitle { get; set; }

        [Required]
        [StringLength(10, ErrorMessage = "歌曲名称要在10个字以内")]
        public string Artist { get; set; }

        [Required]
        [StringLength(10, ErrorMessage = "歌曲名称要在10个字以内")]
        public string Album { get; set; }

        public TimeSpan Duration { get; set; }
        public DateTime ReleaseDate { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            if (ReleaseDate != default && ReleaseDate>DateTime.Now)
            {
                yield return new ValidationResult("ReleaseDate不能大于当天",
                                  new[] { nameof(ReleaseDate) });
            }

        }
    }

运行,新建条目。当我们如下填写的时候,将会弹出提示框

将Abp移植进.NET MAUI项目将Abp移植进.NET MAUI项目

iOS平台也测试通过 

将Abp移植进.NET MAUI项目

至此我们完成了所有的工作。

结束语

Abp是一个很好用的.Net开发框架,Abp库帮助我们抽象了整个项目以及更多的设计模式应用,虽然有一个Asp在其中,但其功能不仅仅可以构建AspNet Core应用,

经过我们的探索用Abp构建了跨平台应用,同样它还可以用于Xamarin,Wpf甚至是WinForms这些基于桌面的应用。

欢迎参与讨论和转发。

项目地址

jevonsflash/maui-abp-sample (github.com)