学习笔记(Maui 07 无限滚动)

学习笔记(Maui 07 无限滚动)

DailyPoetryM 项目(对应 P13 部分和 P14)

本节通过实现搜索结果 ViewModelResultPage 讲解无限滚动的实现和测试

1 无限滚动的实现

1.1 基本实现

无限滚动需要使用第三方库实现,引入库 TheSalLab.MauiInfiniteScrolling (张引改写库)。无限滚动使用该库中的数据类型 MauiInfiniteScrollCollection<>,该类型与 ObservableCollection<> 功能相同且支持无限滚动。ObservableCollection<> 作为数据集合,绑定的前端可以自动获得数据并显示,但不支持无限滚动。
在中间库项目 DailyPoetryM.Library 文件夹 ViewModels 中新建类 ViewModelResultPage,定义 数据类型 MauiInfiniteScrollCollection<> 的变量 Poetries 用来保存目前显示的搜索结果,结果改变时可以自动更新页面,结果过多时可以实现无限滚动。

public  class ViewModelResultPage : ObservableObject
{
	private Expression<Func<Poetry, bool>> _Where;
    public Expression<Func<Poetry, bool>> Where
    {
        get => _Where;
        set => SetProperty(ref _Where, value);
	}

    public MauiInfiniteScrollCollection<Poetry> Poetries { get; }

    public ViewModelResultPage(IStoragePoetry storagePoetry)
    {
        Poetries = new MauiInfiniteScrollCollection<Poetry>
        {
            OnCanLoadMore = () => true,

            OnLoadMore = async () =>
            {
                return (await storagePoetry.GetPoetriesAsync(Where, Poetries.Count, PageSize)).ToList();
            }
        };
    }
    public const int PageSize = 20;
}

变量 Poetries 在构造函数中初始化。MauiInfiniteScrollCollection<> 类型的变量初始化时需要指定两个委托,一个是 OnCanLoadMore,在需要的时候调用,用来验证能否加载更多数据,返回值为 true,有更多数据,能够继续滚动;否则,停止滚动。

OnCanLoadMore = () => true

例子里的语句意味着永远有数据可用,即可以永远继续滚动。仅用作测试。
MauiInfiniteScrollCollection<> 类型的变量初始化时指定的另一个委托是 OnLoadMore,用来进行数据加载,返回加载的新数据。

(await storagePoetry.GetPoetriesAsync(Where, Poetries.Count, PageSize)).ToList();

Where 属性是搜索的条件。Poetries.Count 是跳过已经显示的诗词数量,PageSize 是每次读取的诗词数量,即每次读取返回的诗词个数。ToList() 将枚举类型转换为列表类型,为了防止枚举类型枚举次数的限制的可能性。枚举类型没有属性能够直接给出元素个数,想获得元素个数只能通过枚举行为将元素取出再获得。枚举类型可能可重复枚举,也有可能只能枚举一次,一旦出现后者的情况,则只能获取一次元素个数。

1.2 _CanLoadMore 变量

定义变量 _CanLoadMore 表示“能否加载更多数据”的判断。能否加载更多数据的判断由 OnCanLoadMore 返回,其判断由 OnLoadMore 做出。

public  class ViewModelResultPage : ObservableObject
{
    private Expression<Func<Poetry, bool>> _Where;
    public Expression<Func<Poetry, bool>> Where
    {
        get => _Where;
        set => SetProperty(ref _Where, value);
	}

    public MauiInfiniteScrollCollection<Poetry> Poetries { get; }

	public ViewModelResultPage(IStoragePoetry storagePoetry)
    {
        Poetries = new MauiInfiniteScrollCollection<Poetry>
        {
            OnCanLoadMore = () => _CanLoadMore;

            OnLoadMore = async () =>
            {
                Status = Loading;

                var poetries = (await storagePoetry.GetPoetriesAsync(Where, Poetries.Count, PageSize)).ToList();

                if (poetries.Count < PageSize)
                {
                    _CanLoadMore = false;
                }

                if (poetries.Count == 0 && Poetries.Count == 0)
                {
                    _CanLoadMore = false;
                }

                return poetries;
            }
        };
    }

    private bool _CanLoadMore;
    public const int PageSize = 20;
}

poetries.Count 为从数据库读出的诗词数量,如果其值小于 PageSize,说明数据库没有更多的诗词数据,所有搜索到的诗词已经展示了,即不能提供更多数据了,无限滚动停止。如果已经读出的诗词数量 Poetries.Count 为 0,并且数据库中能读出的诗词数量也为 0,说明搜索没有获得任何诗词,不能提供数据了,无限滚动也停止了。

1.3 新的搜索条件对变量 _CanLoadMore 的影响

除了在构造变量 Poetries 时,进行“能否加载更多数据”的判断,在获得新的搜索条件时也应改进行“能否加载更多数据”的判断。

 private Expression<Func<Poetry, bool>> _Where;
 public Expression<Func<Poetry, bool>> Where
 {
     get => _Where;
     set
     {
       	if (value != _Where)
      	{
           _CanLoadMore = true;
       	}
        SetProperty(ref _Where, value);
      }
 }

如果条件改变,则说明有新的搜索需求出现,可以认为能偶加载更多数据。考虑到函数 SetProperty 的返回值,可以简写。

 private Expression<Func<Poetry, bool>> _Where;
 public Expression<Func<Poetry, bool>> Where
 {
     get => _Where;
     set => _CanLoadMore = SetProperty(ref _Where, value);
 }

1.4 读取数据的状态

定义属性 Status 表示读取数据读取状态。

 public  class ViewModelResultPage : ObservableObject
 {
     private Expression<Func<Poetry, bool>> _Where;
     public Expression<Func<Poetry, bool>> Where
     {
         get => _Where;
         set => _CanLoadMore = SetProperty(ref _Where, value);
     }

     public MauiInfiniteScrollCollection<Poetry> Poetries { get; }

     private string _Status;
     public string Status
     {
         get => _Status;
         set => SetProperty(ref _Status, value);
     }

     public ViewModelResultPage(IStoragePoetry storagePoetry)
     {
         Poetries = new MauiInfiniteScrollCollection<Poetry>
         {
             OnCanLoadMore = () => _CanLoadMore;

             OnLoadMore = async () =>
             {
                 Status = Loading;
                 var poetries = (await storagePoetry.GetPoetriesAsync(Where, Poetries.Count, PageSize)).ToList();
                 Status = string.Empty;

                 if (poetries.Count < PageSize)
                 {
                     _CanLoadMore = false;
                     Status = NoMoreResult;
                 }

                 if (poetries.Count == 0 && Poetries.Count == 0)
                 {
                     _CanLoadMore = false;
                     Status = NoResult;
                 }
                 return poetries;
             }
         };
     }


	private RelayCommand _CommandNavigatedTo;
    public RelayCommand CommandNavigatedTo => _CommandNavigatedTo ??= new RelayCommand(async () =>
    {
        Poetries.Clear();
        await Poetries.LoadMoreAsync();
    });

    private bool _CanLoadMore;
    public const int PageSize = 20;
    public const string Loading = "正在载入";
    public const string NoResult = "没有满足条件的结果";
    public const string NoMoreResult = "没有更多结果";

 }

1.5 数据的第一次加载

private RelayCommand _CommandNavigatedTo;
public RelayCommand CommandNavigatedTo => _CommandNavigatedTo ??= new RelayCommand(async () =>
{
    Poetries.Clear();
	await Poetries.LoadMoreAsync();
});

每当导航到搜索结构页面时需要加载数据,命令 CommandNavigatedTo 在导航到搜索结果页面时执行。

2 单元测试

2.1 Poetries 测试

测试无限滚动功能。在测试项目 DailyPoetryM.TestProject 中创建文件夹 ViewModels,添加类 ViewModelResultPageTest,添加测试函数 Poetries_Default()。

[Fact]
public async Task Poetries_Defaullt()
{
    var Where = Expression.Lambda<Func<Poetry, bool>>(
        Expression.Constant(true),
        Expression.Parameter(typeof(Poetry), "p"));

    var storagePoetry = await StoragePoetryTest.GetInitializedStoragePoetry();
    var viewModelResultPage = new ViewModelResultPage(storagePoetry);
	viewModelResultPage.Where = Where;
	viewModelResultPage.CommandNavigatedTo.Execute(null);

    Assert.Equal(ViewModelResultPage.PageSize, viewModelResultPage.Poetries.Count);

    await storagePoetry.CloseAsync();
}

Lambda 表达式 Expression.Lambda 是没有名字的函数。函数的具体内容由 Func 描述。Func< Poetry, bool> 表示一个参数为 Poetry,返回值为 bool 的函数,函数的函数体定义 Expression.Constant(true),表示常量 true,即该函数返回常量 true。表示每一条数据都满足要求,数据库中所有数据都可以返回;函数的参数定义 Expression.Parameter(typeof(Poetry), “p”),参数的名字为 p,参数的类型为 Poetry。

Expression.Lambda 语句为数据库取值提供查询条件,也可以使用类似 SQL 语句的 LINQ(language integreted querey,LINQ)语句实现。

var pList = new List<Poetry>{new Poetry { Id = 1 }, new Poetry { Id = 2 }, new Poetry { Id = 3 }};
var poetriesWithIdGt1 = from p in pList where p.Id > 1 select p;

poetriesWithIdGt1 包含 id=2 和 3 的诗歌。LINQ 语句表示根据 where 提供的搜索条件(Id > 1) 对 pList 中的每一个元素(诗歌)进行判断,并选择所有满足条件的元素(诗歌)。现代 LINQ 语句更加简洁。

var poetriesWithIdGt2 = pList.Where(p => p.Id > 1);

使用 LINQ 语句和 Expression.Lambda 可以获得相同的效果,但是 LINQ 语句在编译时就锁定了条件,不能动态改动查询条件。而 Expression.Lambda 可以方便地改变查询条件。
主动调用 RelayCommand时需要使用 Execute。
测试失败,ViewModelResultPage.PageSize 和 viewModelResultPage.Poetries.Count 不相等。

2.2 RelayCommand 的测试

测试失败的原因是 Execute 执行时,会新开一个新线程执行程序,将原来的单线程执行变成多线程执行。测试时,程序并行执行,Assert 时读取数据库操作并没有完成。解决方案是将 RelayCommand 的内容剥离出来。

private RelayCommand _CommandNavigatedTo;
public RelayCommand CommandNavigatedTo => _CommandNavigatedTo ??= new RelayCommand(async () =>
{
    await CommandNaviagedToFunction();
});

public async Task CommandNaviagedToFunction()
{
     Poetries.Clear();
     await Poetries.LoadMoreAsync();
}

在中间库项目 DailyPoetryM.Library 类 ViewModelResultPage 中,添加的函数 CommandNaviagedToFunction()负责将 RelayCommand CommandNavigatedTo 的内容剥离出来,方便测试。
在测试项目 DailyPoetryM.TestProject 中类 ViewModelResultPageTest 的函数 Poetries_Default()中添加语句。

await viewModelResultPage.CommandNaviagedToFunction();

代替 RelayCommand 命令的 Execute 调用。
测试通过。

2.3 属性 Status 的测试

属性 Status 在程序执行过程中会多次变化,需要测试属性变化的中间值。所以采用 List< string> 存储其在程序执行过程中出现的所有值历史值。并且采用属性变化事件触发其改变进行测试。

var listStatus = new List<string>();
viewModelResultPage.PropertyChanged += (sender, args) =>
{
    if (args.PropertyName == nameof(ViewModelResultPage.Status))
    {
      	listStatus.Add(viewModelResultPage.Status);
    }
}

Assert.Equal(2, listStatus.Count);
Assert.Equal(ViewModelResultPage.Loading, listStatus[0]);
Assert.Equal(string.Empty, listStatus[1]);

先测试的是属性变化次数,根据代码应该变化两次,然后分别判断两次的值。测试通过。
最终的测试函数

[Fact]
public async Task Poetries_Defaullt()
{
    var Where = Expression.Lambda<Func<Poetry, bool>>(
        Expression.Constant(true),
        Expression.Parameter(typeof(Poetry), "p"));

    var storagePoetry = await StoragePoetryTest.GetInitializedStoragePoetry();
    var viewModelResultPage = new ViewModelResultPage(storagePoetry);

    viewModelResultPage.Where = Where;

    var listStatus = new List<string>();
    viewModelResultPage.PropertyChanged += (sender, args) =>
    {
        if (args.PropertyName == nameof(ViewModelResultPage.Status))
        {
            listStatus.Add(viewModelResultPage.Status);
        }
    };

	await viewModelResultPage.CommandNaviagedToFunction();

	Assert.Equal(ViewModelResultPage.PageSize, viewModelResultPage.Poetries.Count);

    Assert.Equal(2, listStatus.Count);
    Assert.Equal(ViewModelResultPage.Loading, listStatus[0]);
    Assert.Equal(string.Empty, listStatus[1]);
    await storagePoetry.CloseAsync();
}

2.4 单元测试小结

单元测试遇到不能测的内容

  • 项目不能测试:剥离到能测试的类测试
  • 类不能测试:封装成接口用 mock 测试
  • RelayCommand:将内部代码剥离到函数测试
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sleevefisher

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值