將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.pswp.cn/news/286831.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/286831.shtml
英文地址,請注明出處:http://en.pswp.cn/news/286831.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Sql數據庫批量清理日志

說明&#xff1a;SQL數據庫日志經常過大&#xff0c;引起很多問題&#xff0c;網上很多清理日志的方法&#xff0c;但都比較麻煩&#xff0c;并且不能一次性清理所有數據庫。如果有幾十個數據庫要清理&#xff0c;一個個手工來&#xff0c;想必非常麻煩。 以下代碼清除 除了指定…

【BZOJ-2299】向量 裴蜀定理 + 最大公約數

2299: [HAOI2011]向量 Time Limit: 10 Sec Memory Limit: 256 MBSubmit: 1118 Solved: 488[Submit][Status][Discuss]Description 給你一對數a,b&#xff0c;你可以任意使用(a,b), (a,-b), (-a,b), (-a,-b), (b,a), (b,-a), (-b,a), (-b,-a)這些向量&#xff0c;問你能不能拼…

采用ArcGIS 10.6制作漂亮的點陣世界地圖,完美!!!

如下圖所示,怎樣制作完美漂亮的點陣世界地圖呢?今天我就教大家吧! 其實,制作過程相當簡單,主要的思路是通過世界地圖范圍去創建漁網(標注點),再選擇范圍內的標注點,符號化即可,怎么樣,很簡單吧,下面我們一步一步來實現吧。 1. 加載世界地圖 打開ArcGIS軟件,加載軟…

Android ping命令 -- Runtime

代碼&#xff1a; 1 public String Run(String _strsIp) throws Exception2 {3 String strRst "";4 try5 {6 String str "ping -c 1 "_strsIp;7 Runtime runtime Runtime.getRuntime();8 …

懶辦法1篇文10分鐘快速入門MySQL增刪查改

作者簡介 作者名&#xff1a;1_bit 簡介&#xff1a;CSDN博客專家&#xff0c;2020年博客之星TOP5&#xff0c;InfoQ簽約作者&#xff0c;藍橋簽約作者。15-16年曾在網上直播&#xff0c;帶領一批程序小白走上程序員之路。歡迎各位小白加我咨詢我相關信息&#xff0c;迷茫的你…

Android(kotlin)之對一組圖片數據更新最后的修改時間進行分類顯示

1 需求 我們需要實現攜帶時間頭的一系列照片如下顯示,現在我們拿到了圖片集合,肯定需要對圖片根據實現進行分組顯示 date picture picture picture picture picture picture picture picture picturedate picture picture picture picture picture 2 代碼實現 fun getImag…

SqlServer 數據庫 分離復制備份然后附加回數據庫 sql代碼

工作中&#xff0c;我們經常需要把數據庫進行分離&#xff0c;復制數據庫到另一個地方&#xff0c;再將原數據庫文件附加回數據庫 。 當然通過鼠標操作的步驟比較麻煩&#xff1a; 1、 右鍵數據庫 --------任務------分離 2、打開數據庫文件所在目錄&#xff0c;復制數據庫 …

01:操作系統(centos,redhat):性能監控和網絡命令

性能監控和優化命令 top命令功能&#xff1a;顯示當前系統正在執行的進程的相關信息&#xff0c;包括進程ID、內存占用率、CPU占用率等常用參數&#xff1a;-d 屏幕刷新間隔時間屏幕信息解釋字段說明&#xff1a;第一行&#xff1a;top 當前系統時間up 系統運行時間…

大話領域驅動設計——領域層

概述在DDD中&#xff0c;業務邏輯主要分布在領域層和應用層兩層&#xff0c;他們包含不同的業務邏輯。這一篇&#xff0c;我們先對領域層做詳細的講解分析。領域層實現了領域或系統的&#xff0c;與用戶界面上的用戶交互&#xff08;用例&#xff09;無關的核心業務邏輯。總覽領…

【北斗】北斗衛星導航系統(BDS)介紹

一、概述 北斗衛星導航系統(以下簡稱北斗系統)是中國著眼于國家安全和經濟社會發展需要,自主建設運行的全球衛星導航系統,是為全球用戶提供全天候、全天時、高精度的定位、導航和授時服務的國家重要時空基礎設施。 北斗系統提供服務以來,已在交通運輸、農林漁業、水文監…

Android之解決VideoView控件的進度條位置擺放問題和打開播放黑屏問題

1 問題 VideoView控件如何配合MediaController使用,就會顯示進度條,但是位置擺放在最底部,不是我們想要的結果 2、基本使用 private var mMediaController:MediaController? = nullfun playVedio() {mMediaController = MediaController(this as Activity)trashVideoView…

Snmp linux

http://blog.csdn.net/youngqj/article/details/7311849 http://blog.csdn.net/howema/article/details/4182408 http://my.oschina.net/yisenn/blog/14626轉載于:https://www.cnblogs.com/diyunpeng/p/5720952.html

正則驗證金額大于等于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;帶領一批程序小白走上程序員之路。歡迎各位小白加我咨詢我相關信息&#…

【專升本計算機】計算機文化基礎練習題(選擇題300道附答案)

專升本計算機文化基礎練習題 1、 世界上第一臺電子計算機誕生于 ______A_ 。 A . 20 世紀 40 年代 B . 19 世紀 C . 20 世紀 80 年代 D . 1950 年 2、 世界上第一臺電子計算機是 1946 年在美國研制成功的,該機的英文縮寫名是 ___A__ 。 A . ENIAC B . EDVAC C…

《微軟云計算Microsoft Azure部署與管理指南》即將上市!!!

大家好&#xff0c;本人新作《微軟云計算Microsoft Azure部署與管理指南》即將與廣大讀者見面&#xff0c;由電子工業出版社出版。希望大家能關注此書&#xff0c;并推薦給身邊的好友和技術人員。 眾所周知&#xff0c;Microsoft Azure是專業的國際化公有云平臺, 是微軟研發的公…

如何用SQL來檢測文件是否存在

工作中&#xff0c;我們經常需要檢查上傳的文件是否存在&#xff0c;用戶上傳的頭像是否存在&#xff0c;等等。 有沒有辦法直接用SQL來查詢呢&#xff1f; 請直接看代碼&#xff1a; /************************************************************ * Create By Jacky * T…

如何解決分布式日志exceptionless的寫入瓶頸

我們都知道在分布式日志當中&#xff0c;exceptionless客戶端是把日志寫到Elasticsearch數據庫&#xff0c;就像我們把數據寫入到關系數據庫一樣&#xff1b;既然是寫入&#xff0c;那么在短時間大數據量的情況下&#xff0c;寫入就會涉及到效率的問題&#xff1b;首先我們看下…

iVX 基礎

1.1 iVX 線上集成環境進入 點擊 連接 或通過瀏覽器輸入網址 https://editor.ivx.cn/ 進入線上集成開發環境。 進入 在線集成開發環境 后&#xff0c;可點擊右上角 登錄/注冊 進行帳號登錄或者注冊。登錄賬戶 后在進行項目開發時會自動保存項目開發進度。 [外鏈圖片轉存失敗…

關于有序二維矩陣查找和字符串替換的兩道算法題

最近看一本書上寫到的兩個面試題 于是實現了一下 感覺思路很好,大牛略過 : 1、對于一個二維矩陣,從左到右 從上到下 都是遞增的,如何判斷一個值是否在矩陣內部?&#xff08;C實現 實現復雜度 O(n)&#xff09; bool FindInTwoDimensionalMatrix(int*pMatrix,int iRows,int i…