將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) });
}
}
}
執行,新建條目。當我們如下填寫的時候,將會彈出提示框
至此我們完成了所有的工作。
結束語
Abp是一個很好用的.Net開發框架,Abp庫幫助我們抽象了整個專案以及更多的設計模式應用,其名稱Asp Boilerplate,雖然有一個Asp在其中,但其功能不僅僅可以構建AspNet Core應用,
經過我們的探索用Abp構建了跨平臺應用,同樣它還可以用於Xamarin,Wpf甚至是WinForms這些基於桌面的應用。
歡迎參與討論和轉發。
專案地址