将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,对歌曲信息进行增、删、查、改。

707549c463a59edf215eced1640111b9.png991ca1c9c6be3dc59d0228fc6444beda.png

下面来看看如何搭建

搭建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项目保持一致

bc71f0f4f86386c239f84fbc7058a6f4.png073da811dd3662ccb82483b9f9b09d96.png

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

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

dd5b6fdda0a5779ad24c0729fcf6afc0.png9fffd527e374d6a04a8cf7a7df2e1aa2.png

配置应用入口点

在MauiBoilerplate.Core项目中

改写默认配置文件

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

44580a40288839d80e3d1905cf8ff830.png

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

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

15ff04a42d79ea7c3a759ee89599640c.png

在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);}}

54aae5235e7c6896755e019652e55a83.png

添加一个静态方法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);}

a66e56a4aae05aa8158b7999aebde753.png

添加一个 静态方法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;}

7aecd7364924c5f44c17ee89926540c2.png

在MauiBoilerplate项目中

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

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

995c488a6de27562e120b8aaf2cd0496.png

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

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

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

4bcb73f98416a791663545f2cac52302.png6ce4724bc7ec3f581dfd46deeb1cbd1d.png

 至此,在主机管道中已经配置了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;}}

26739ea2d6c7fb8f305ffc9814fe2465.png

注意,我们还没有创建初始页面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; }}

f8b0fdcd080c59946719beedfc275a0e.png

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

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

b6429595116ad0c4fd574262afb288a9.png

 新建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();}}}}

62587583a3e2e0e31f8e35699b64d126.png

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

编写种子数据帮助类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 seednew InitialDbBuilder(context).Create();}}

d2dc02c172d85c9f1850d961bb62b97e.png

编写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();}}

6181396132a3366d49a4768942070c53.png

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

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

e1684fe890f7cafde1f8c5dcc3bd6b21.pngb5c001a4e4c45790c39253d4cf866213.png编辑

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

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

6021dc52de5a84dda61d586906aa9260.png0ba9a992a5a44f8da1263a72e0467255.png编辑

编写基类(可选)

我们在使用相关的父类时,某某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);}}

c8f4892d7c21c1676af3fbd8d0acf06f.png

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

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

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

34c6858daa8f72b320eaae0f2ad935d2.pngc818286852a73add0cfd1804190105d9.png编辑

 本地化配置

新建一个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;}}

2f1ab3c4c1a76c93434439786a114c09.png

在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")));}

8789a5e8c4512982e7930063e7860953.png

编写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;}}

067af02229201be77a551e6e4f097a96.png

至此,我们完成了数据库的配置,内容页基类与 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; }}

5af1e8ffd3a4143c6b969a521625da35.png

新建一个MainPage页面

343d3c3f2c8b6725cf2cd8d5eee8901b.pngc983264aae0b1ff3517b7ad7706ea1f1.png

编写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" /><LabelText="{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>

0afa27dfb5a05c78554bc605332a1d93.png

 编写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);}
}

861d1c1cf83ff20bcb0b57f4fc8dee55.png

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

9cba059b97e969e83fe3cb368d0c0938.pngd1ef24dc50d4dec75ef44a2395cce471.png

 编写条目编辑页面

 新建一个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();}}}

7ed28b69c535657f7aa92bcf3f2a8b14.png

新建一个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>

bc69cbe14f8c7fdccf86346c7b641e7c.png

 编写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, "好的");}
}

868d9e1e10b6cf86472157507b647681.png

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

dbe9fdbcd5f129e12479554b87d34383.png3dc04e0892dc3e87c8e44f082de89c7f.png

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

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

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

b0ac3ed7a241a209c170d646c9bddda6.png1343d24d0d186dd96d4e53e41f9b3bea.png编辑

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

MusicItemPageViewModel.cs中:

public event EventHandler<List<ValidationResult>> OnValidateErrors;

e79e03e09599d2080dc8a86de5eb5bad.png

 MusicItemPage.xaml.cs中:

this.musicItemPageViewModel.OnValidateErrors+=MusicItemPageViewModel_OnValidateErrors;

5ad15f181172e67bc31a351885ebdfef.png

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

cf2335e946dd3367b0a1f165fc61b076.png

编写校验逻辑代码

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;}

2e68e703d531667e32d709d0a16e8fac.png

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);}}

80cbb815a865869d6802423a5a210964.png

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

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) });}}}

6fc8b9417f294622137f99f38e06d347.png

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

fb136792cb2c110599efae62a2aff6cb.png93a07d7aa88f2169f3b5749fcf001428.png

iOS平台也测试通过 

aa9cb46cf88b752c533bc6c12e4b5a37.png

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

结束语

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

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

欢迎参与讨论和转发。

 项目地址

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/286831.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

采用ArcGIS 10.6制作漂亮的点阵世界地图,完美!!!

如下图所示,怎样制作完美漂亮的点阵世界地图呢?今天我就教大家吧! 其实,制作过程相当简单,主要的思路是通过世界地图范围去创建渔网(标注点),再选择范围内的标注点,符号化即可,怎么样,很简单吧,下面我们一步一步来实现吧。 1. 加载世界地图 打开ArcGIS软件,加载软…

懒办法1篇文10分钟快速入门MySQL增删查改

作者简介 作者名&#xff1a;1_bit 简介&#xff1a;CSDN博客专家&#xff0c;2020年博客之星TOP5&#xff0c;InfoQ签约作者&#xff0c;蓝桥签约作者。15-16年曾在网上直播&#xff0c;带领一批程序小白走上程序员之路。欢迎各位小白加我咨询我相关信息&#xff0c;迷茫的你…

大话领域驱动设计——领域层

概述在DDD中&#xff0c;业务逻辑主要分布在领域层和应用层两层&#xff0c;他们包含不同的业务逻辑。这一篇&#xff0c;我们先对领域层做详细的讲解分析。领域层实现了领域或系统的&#xff0c;与用户界面上的用户交互&#xff08;用例&#xff09;无关的核心业务逻辑。总览领…

【北斗】北斗卫星导航系统(BDS)介绍

一、概述 北斗卫星导航系统(以下简称北斗系统)是中国着眼于国家安全和经济社会发展需要,自主建设运行的全球卫星导航系统,是为全球用户提供全天候、全天时、高精度的定位、导航和授时服务的国家重要时空基础设施。 北斗系统提供服务以来,已在交通运输、农林渔业、水文监…

正则验证金额大于等于0,并且只到小数点后2位

2019独角兽企业重金招聘Python工程师标准>>> ^(([0-9]|([1-9][0-9]{0,9}))((\.[0-9]{1,2})?))$ 转载于:https://my.oschina.net/u/934148/blog/528688

我结婚了,我要用什么做个邀请函呢?【iVX无代码YYDS 06】

作者简介 作者名&#xff1a;1_bit 简介&#xff1a;CSDN博客专家&#xff0c;2020年博客之星TOP5&#xff0c;InfoQ签约作者、CSDN新星导师&#xff0c;华为云享专家。15-16年曾在网上直播&#xff0c;带领一批程序小白走上程序员之路。欢迎各位小白加我咨询我相关信息&#…

《微软云计算Microsoft Azure部署与管理指南》即将上市!!!

大家好&#xff0c;本人新作《微软云计算Microsoft Azure部署与管理指南》即将与广大读者见面&#xff0c;由电子工业出版社出版。希望大家能关注此书&#xff0c;并推荐给身边的好友和技术人员。 众所周知&#xff0c;Microsoft Azure是专业的国际化公有云平台, 是微软研发的公…

如何解决分布式日志exceptionless的写入瓶颈

我们都知道在分布式日志当中&#xff0c;exceptionless客户端是把日志写到Elasticsearch数据库&#xff0c;就像我们把数据写入到关系数据库一样&#xff1b;既然是写入&#xff0c;那么在短时间大数据量的情况下&#xff0c;写入就会涉及到效率的问题&#xff1b;首先我们看下…

iVX 基础

1.1 iVX 线上集成环境进入 点击 连接 或通过浏览器输入网址 https://editor.ivx.cn/ 进入线上集成开发环境。 进入 在线集成开发环境 后&#xff0c;可点击右上角 登录/注册 进行帐号登录或者注册。登录账户 后在进行项目开发时会自动保存项目开发进度。 [外链图片转存失败…

Android之靠谱的把图片和视频插入手机系统相册

1 需求 把图片和视频插入手机系统相册,网上查了下基本上很乱,没几个靠谱的。 2 结果爆照 3 思路 图片插入系统相册(可以直接插入系统相册,但是我这里多做了一步就是先把图片拷贝到了一个目录再插入系统相册) 视频插入系统相册(先把视频拷贝到MIUI目录,然后再…

C# WPF 实现Tab页动态增减

概述Tab页面是一个很常用的控件&#xff0c;针对页面固定的场景&#xff0c;直接给Item进行数据绑定就行&#xff0c;如下所示&#xff1a;<dx:DXTabControl cal:Message.Attach"[Event Loaded][TabControl_Loaded($source,$eventArgs)]"><dx:DXTabItem Hea…

2014 网选 上海赛区 hdu 5047 Sawtooth

题意&#xff1a;求n个M型的折线将一个平面分成的最多的面数&#xff01; 思路&#xff1a;我们都知道n条直线将一个平面分成的最多平面数是 An An-1 n1 也就是f(n) (n*n n 2)/2 对于一个M型的折线呢&#xff1f;它有四条线&#xff0c;但是由于三个顶点的关系导致划分的平…

二、基础(IVX快速开发手册)

二、基础 通过本节你将了解 iVX 所支持应用的创建方法。 文章目录二、基础2.1 iVX 线上集成环境进入2.2 创建项目2.3 选择项目类型2.3.1 WebApp/小程序/原生应用2.3.2 微信小游戏2.3.3 微信小程序&#xff08;原生组件&#xff09;2.1 iVX 线上集成环境进入 点击 连接 或通过…

Android之TabLayout+ViewPager2+FragmentStateAdapter实现带数字变化的TAB选项

1 问题 TabLayout+ViewPager2实现带数字变化的TAB选项,然后左边滑动或者点击上面的Tab切换fragment不能刷新 2 结果爆照 3 代码实现 layer_tab_indicator.xml <?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="h…

slq2000数据库升级到sql2012

看到标题&#xff0c;估计有同行笑了&#xff0c;这年代还有用sql2000的&#xff1f;真的有&#xff0c;最近单位服务器数据迁移升级&#xff0c;将数据库迁移到新服务器后&#xff0c;发现数据全是2000的&#xff0c;无法直接导入到sql2012。没办法&#xff0c;只能先将数据库…

电脑网页打不开但qq能上解决方法

2019独角兽企业重金招聘Python工程师标准>>> 问题描述&#xff1a; 电脑网页打不开但qq能上。 问题原因&#xff1a; 是由于电脑系统的DNS解析出了问题。 解决方法&#xff1a; 首先在键盘上同时按下 winR 然后在弹窗中输入cmd &#xff0c; 再按enter键&#xf…

基于Linux命令行KVM虚拟机的安装配置与基本使用

背景由于生产环境的服务器并不会安装桌面环境&#xff0c;简单操作的图形化安装也不适合批量部署安装。因此&#xff0c;我还是更倾向于在命令下安装配置KVM虚拟机。结合了一些资料和个人使用的状况&#xff0c;我大致列出了一些基本和常用的使用方法。 安装配置一、环境介绍操…

四、WebApp 基础可视组件(IVX 快速开发教程)

四、基础可视组件 通过本节你将了解 iVX 开发中的核心—— iVX 组件的使用方法。iVX 的组件是开发应用时所必要的对象&#xff0c;通过这些对象你将快速的完成应用的开发。 在 iVX 应用开发中&#xff0c;所有交互、动画、数据都需要以组件为基础&#xff0c;通过组件之间的编…

Springboot项目搭建(三)整合thymeleaf模板

springboot整合thymeleaf模板 一、POM文件添加依赖 <!--thymeleaf--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency><!--nekohtml 解决thymea…

React-引领未来的用户界面开发框架-读书笔记(一)

这本书的主要内容都是以react v0.12为主&#xff0c;ES5语法&#xff0c;笔记中将会使用react v0.14和RS6。 第1章 react简介 1.本质上是一个状态机&#xff0c;它以精简的模型管理复杂的随着时间而变化的状态。 2.它不是model-view-controller&#xff0c;它是mvc中的v(view)&…