将Abp移植进.NET MAUI项目(三):构建UI层
很开心,终于到了创建页面的时候了!
我们需要两个页面
- 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 Boilerplate,虽然有一个Asp在其中,但其功能不仅仅可以构建AspNet Core应用,
经过我们的探索用Abp构建了跨平台应用,同样它还可以用于Xamarin,Wpf甚至是WinForms这些基于桌面的应用。
欢迎参与讨论和转发。
项目地址
jevonsflash/maui-abp-sample (github.com)
本文来自博客园,作者:林晓lx,转载请注明原文链接:https://www.cnblogs.com/jevonsflash/p/16310399.html