将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)

posted @ 2022-05-25 18:46  林晓lx  阅读(772)  评论(3编辑  收藏  举报