[MAUI 项目实战] 手势控制音乐播放器:圆形进度条
Posted 林晓lx
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[MAUI 项目实战] 手势控制音乐播放器:圆形进度条相关的知识,希望对你有一定的参考价值。
文章目录
我们将绘制一个圆形的音乐播放控件,它包含一个圆形的进度条、专辑页面和播放按钮。
关于图形绘制
使用MAUI的绘制功能,需要Microsoft.Maui.Graphics库。
Microsoft.Maui.Graphics 是一个实验性的跨平台图形库,它可以在 .NET MAUI 中使用。它提供了一组基本的图形元素,如矩形、圆形、线条、路径、文本和图像。它还提供了一组基本的图形操作,如填充、描边、裁剪、变换和渐变。
Microsoft.Maui.Graphics在不同的目标平台上使用一致的API访问本机图形功能,而底层实现使用了不同的图形渲染引擎。其中通用性较好的是SkiaSharp图形库,支持几乎所有的操作系统,在不同平台上的表现也近乎一致。
创建自定义控件
在项目中添加SkiaSharp绘制功能的引用Microsoft.Maui.Graphics.Skia
以及SkiaSharp.Views.Maui.Controls
。
<ItemGroup>
<PackageReference Include="Microsoft.Maui.Graphics.Skia" Version="7.0.59" />
<PackageReference Include="SkiaSharp.Views.Maui.Controls" Version="2.88.3" />
</ItemGroup>
创建CircleSlider.xaml文件,添加如下内容:
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:forms="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls"
x:Class="MatoMusic.Controls.CircleSlider">
<ContentView.Content>
<forms:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
</ContentView.Content>
</ContentView>
SKCanvasView是SkiaSharp.Views.Maui.Controls封装的View控件。
打开CircleSlider.xaml.cs文件
控件将包含以下可绑定属性:
- Maximum:最大值
- Minimum:最小值
- Value:当前值
- TintColor:进度条颜色
- ContainerColor:进度条背景颜色
- BorderWidth:进度条宽度
定义两个SKPaint画笔属性,OutlinePaint用于绘制进度条背景,ArcPaint用于绘制进度条本身。他们的描边宽度StrokeWidth则是圆形进度条的宽度。
两个画笔的初始值样式为SKPaintStyle.Stroke,描边宽度为BorderWidth的值。
private SKPaint _outlinePaint;
public SKPaint OutlinePaint
get
if (_outlinePaint == null)
SKPaint outlinePaint = new SKPaint
Style = SKPaintStyle.Stroke,
StrokeWidth = BorderWidth,
;
_outlinePaint = outlinePaint;
return
_outlinePaint;
set _outlinePaint = value;
private SKPaint _arcPaint;
public SKPaint ArcPaint
get
if (_arcPaint == null)
SKPaint arcPaint = new SKPaint
Style = SKPaintStyle.Stroke,
StrokeWidth = BorderWidth,
;
_arcPaint = arcPaint;
return _arcPaint;
set _arcPaint = value;
SetStrokeWidth用于设置描边宽度,并产生一个动效,
在BorderWidth发生变更的时候,会出现一个动效。宽度会缓慢地变化至新的值。刷新率为10ms一次,每次变化的值为1。
private float _borderWidth;
public float BorderWidth
get return _borderWidth;
set
var old_borderWidth = _borderWidth;
var span = value - old_borderWidth;
SetStrokeWidth(span, old_borderWidth);
_borderWidth = value;
this.ArcPaint.StrokeWidth = _borderWidth;
this.OutlinePaint.StrokeWidth = _borderWidth;
private async void SetStrokeWidth(float span, float old_borderWidth)
if (span > 0)
for (int i = 0; i <= span; i++)
await Task.Delay(10);
this.ArcPaint.StrokeWidth = old_borderWidth + i;
this.OutlinePaint.StrokeWidth = old_borderWidth + i;
RefreshMainRectPadding();
else
for (int i = 0; i >= span; i--)
await Task.Delay(10);
this.ArcPaint.StrokeWidth = old_borderWidth + i;
this.OutlinePaint.StrokeWidth = old_borderWidth + i;
RefreshMainRectPadding();
于此同时,因为描边宽度变化了,需要对Padding进行补偿。调用RefreshMainRectPadding方法计算一个新的Padding值,BoderWidth缩小时,Padding也随之增大。
private void RefreshMainRectPadding()
this._mainRectPadding = this.BorderWidth / 2;
在视觉上,进度条宽度从内向外扩张变细。
若设为原宽度减去计算值,从视觉上是从外向内收缩变细。
private void RefreshMainRectPadding()
this._mainRectPadding = 15 - this.BorderWidth / 2;
接下来写订阅了CanvaseView的PaintSurface事件的方法OnCanvasViewPaintSurface。在这个方法中,我们将编写圆形进度条的绘制逻辑。
PaintSurface事件在绘制图形时触发。程序运行时会实时触发这个方法,它的参数SKPaintSurfaceEventArgs事件附带的对象具有两个属性:
- Info类型SKImageInfo
- Surface类型SKSurface
SKImageInfo对象包含如宽度和高度等有关绘图区域的信息,对象SKSurface为绘制本身,我们需要利用SKImageInfo宽度和高度等信息,结合业务数据,在SKSurface绘制出我们想要的图形。
清空上一次绘制的图形,调用SKSurface.Canvas获取Canvas对象,调用Canvas.Clear方法清空上一次绘制的图形。
canvas.Clear();
rect是一个SKRect对象,进度条本身是圆形,我们需要一个正方形的区域来控制圆形区域。
sweepAngle是当前进度对应的角度,首先计算出总进度值,通过计算当前进度对应总进度的比值,换算成角度,将这一角度赋值给sweepAngle。
startAngle是进度条的起始角度,我们将其设置为-90度,即从正上方开始绘制。
SKRect rect = new SKRect(_mainRectPadding, _mainRectPadding, info.Width - _mainRectPadding, info.Height - _mainRectPadding);
float startAngle = -90;
float sweepAngle = (float)((Value / SumValue) * 360);
调用Canvas.DrawOval,使用OutlinePaint画笔绘制进度条背景,它是一个圆形
canvas.DrawOval(rect, OutlinePaint);
创建绘制路径path,调用AddArc方法,将rect对象和起始角度和终止角度传入,即可绘制出弧形。
using (SKPath path = new SKPath())
path.AddArc(rect, startAngle, sweepAngle);
canvas.DrawPath(path, ArcPaint);
绘制部分的完整代码如下:
private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
var SumValue = Maximum - Minimum;
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
SKRect rect = new SKRect(_mainRectPadding, _mainRectPadding, info.Width - _mainRectPadding, info.Height - _mainRectPadding);
float startAngle = -90;
float sweepAngle = (float)((Value / SumValue) * 360);
canvas.DrawOval(rect, OutlinePaint);
using (SKPath path = new SKPath())
path.AddArc(rect, startAngle, sweepAngle);
canvas.DrawPath(path, ArcPaint);
使用控件
在MainPage.xaml中添加一个CircleSlider控件,
设置的Maximum,是当前曲目的时长,Value是当前曲目的进度
<controls:CircleSlider
HeightRequest="250"
WidthRequest="250"
x:Name="MainCircleSlider"
Maximum="Binding Duration"
Minimum="0.0"
TintColor="#FFFFFF"
ContainerColor="#4CFFFFFF"
IsEnabled="Binding Canplay"
ValueChanged="OnValueChanged"
Value="Binding CurrentTime,Mode=TwoWay ">
</controls:CircleSlider>
创建专辑封面
使用MAUI的VisualElement中的Clip属性,创建Clip裁剪,可以传入一个Geometry对象,这里我们使用RoundRectangleGeometry,将它的CornerRadius属性设置为图片宽度的一半,即可实现圆形图片。
<Image HeightRequest="250"
WidthRequest="250"
Margin="7.5"
Source="Binding CurrentMusic.AlbumArt"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"
Aspect="AspectFill">
<Image.Clip>
<RoundRectangleGeometry CornerRadius="125" Rect="0,0,250,250" />
</Image.Clip>
</Image>
设置一个半透明背景的播放状态指示器,当IsPlaying为False时将显示一个播放按钮
<Grid IsVisible="Binding IsPlaying, Converter=StaticResource True2FalseConverter">
<BoxView HeightRequest="250"
WidthRequest="250"
Margin="7.5"
Color="#60000000"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"
CornerRadius="250" ></BoxView>
<Label x:Name="PauseLabel"
HorizontalOptions="CenterAndExpand"
FontSize="58"
TextColor="Binding Canplay,Converter=StaticResource Bool2StringConverter,ConverterParameter=White|#434343"
FontFamily="FontAwesome"
Margin="0"></Label>
</Grid>
创建PanContainer对象,用于实现拖动效果,设置AutoAdsorption属性为True,即可实现拖动后自动吸附效果。
关于PanContainer请查看上期的文章:平移手势交互
用一个Grid将专辑封面,CircleSlider,以及播放状态指示器包裹起来。完整代码如下
<controls1:PanContainer BackgroundColor="Transparent"
x:Name="DefaultPanContainer"
OnTapped="DefaultPanContainer_OnOnTapped"
AutoAdsorption="True"
OnfinishedChoise="DefaultPanContainer_OnOnfinishedChoise">
<Grid PropertyChanged="BindableObject_OnPropertyChanged"
VerticalOptions="Start"
HorizontalOptions="Start">
<Image HeightRequest="250"
WidthRequest="250"
Margin="7.5"
Source="Binding CurrentMusic.AlbumArt"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"
Aspect="AspectFill">
<Image.Clip>
<RoundRectangleGeometry CornerRadius="125" Rect="0,0,250,250" />
</Image.Clip>
</Image>
<controls:CircleSlider>...</controls:CircleSlider>
<Grid IsVisible="Binding IsPlaying, Converter=StaticResource True2FalseConverter">
<BoxView HeightRequest="250"
WidthRequest="250"
Margin="7.5"
Color="#60000000"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"
CornerRadius="250" ></BoxView>
<Label x:Name="PauseLabel"
HorizontalOptions="CenterAndExpand"
FontSize="58"
TextColor="Binding Canplay,Converter=StaticResource Bool2StringConverter,ConverterParameter=White|#434343"
FontFamily="FontAwesome"
Margin="0"></Label>
</Grid>
</Grid>
</controls1:PanContainer>
以上就是这个项目的全部内容,感谢阅读
项目地址
将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,对歌曲信息进行增、删、查、改。
下面来看看如何搭建
搭建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项目保持一致
点击“Create My Project”生成abp项目文件,等待下载完成
下载,解压好后,打开src目录可以发现4个项目目录,我们仅需要Core和EntityFrameworkCore项目,将这两个目录移至项目根目录,并且添加至解决方案。
配置应用入口点
在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
至此,在主机管道中已经配置了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
编辑
运行Add-Migration命令,将生成迁移脚本
运行MauiBoilerplate.EntityFrameworkCore,将生成mato.db等三个文件,
编辑
编写基类(可选)
我们在使用相关的父类时,某某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基类。
在这里我们编写了两个基类
编辑
本地化配置
新建一个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页面
编写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);
此页面将显示一个列表,并在列表条目下可以弹出一个菜单
编写条目编辑页面
新建一个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校验数据功能
这个部分使用Abp的ValidationConfiguration功能校验表单数据,以展示Abp功能的使用
首先在MusicItemPageViewModel 构造函数中添加对IValidationConfiguration对象的注入
编辑
添加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) );
运行,新建条目。当我们如下填写的时候,将会弹出提示框
iOS平台也测试通过
至此我们完成了所有的工作。
结束语
Abp是一个很好用的.Net开发框架,Abp库帮助我们抽象了整个项目以及更多的设计模式应用,虽然有一个Asp在其中,但其功能不仅仅可以构建AspNet Core应用,
经过我们的探索用Abp构建了跨平台应用,同样它还可以用于Xamarin,Wpf甚至是WinForms这些基于桌面的应用。
欢迎参与讨论和转发。
项目地址
jevonsflash/maui-abp-sample (github.com)
以上是关于[MAUI 项目实战] 手势控制音乐播放器:圆形进度条的主要内容,如果未能解决你的问题,请参考以下文章