You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在MAUI中使用ReactiveUI实现CollectionView分页的技术求助

MAUI+ReactiveUI实现CollectionView分页问题

我是一名业余开发者,在MAUI项目中用ReactiveUI实现分页时卡住了,试了各种方法都没进展。目前代码已经集成了搜索功能,现在需要给CollView(CollectionView)做分页:初始加载20条数据,用户滚动到底部时动态加载更多。当前代码如下:

ReadOnlyObservableCollection<FirebaseEvent<Kunde>> myDerivedList;

private Func<FirebaseEvent<Kunde>, bool> _searchFilter = _ => true;
public Func<FirebaseEvent<Kunde>, bool> SearchFilter
{
    get { return _searchFilter; }
    set
    {
        if (_searchFilter != value)
        {
            _searchFilter = value;
            OnPropertyChanged(nameof(SearchFilter));
        }
    }
}

public Kunden()
{
    InitializeComponent();
    BindingContext = this;

    // Subscribe SourceCache
    var dis = FirebaseService.SourceListKunden.Connect()
        .Filter(this.WhenAnyValue(x => x.SearchFilter))
        .Sort(SortExpressionComparer<FirebaseEvent<Kunde>>.Descending(x => x.Key))
        .SubscribeOn(NewThreadScheduler.Default)
        .ObserveOn(SynchronizationContext.Current)
        .Bind(out myDerivedList)
        .Subscribe();

    // Suchfunktion
    this.WhenAnyValue(x => x.searchBar.Text)
        .DistinctUntilChanged()
        .Throttle(TimeSpan.FromMilliseconds(500))
        .SubscribeOn(NewThreadScheduler.Default)
        .ObserveOn(SynchronizationContext.Current)
        .Subscribe(suchText =>
        {
            Debug.WriteLine("Eingabe Suchleiste :" + suchText);
            SearchFilter = w => string.IsNullOrWhiteSpace(suchText) || String.Join(" ", new List<string>() { w.Object.Kundennummer.ToString(), w.Object.Firma, w.Object.Vorname, w.Object.Nachname }).Contains(suchText, StringComparison.CurrentCultureIgnoreCase);
        });

    //ToDo: Pagination
    CollView.ItemsSource = myDerivedList;
}

解决方案

核心逻辑

  • 维护分页状态:当前页码、每页数量、加载状态,避免重复请求
  • 监听CollectionView滚动到底部的事件,触发下一页加载
  • 结合ReactiveUI的可观察序列,将分页逻辑整合到现有SourceCache的订阅链中
  • 搜索时重置分页,重新加载第一页数据

修改后的完整代码

ReadOnlyObservableCollection<FirebaseEvent<Kunde>> myDerivedList;

private Func<FirebaseEvent<Kunde>, bool> _searchFilter = _ => true;
public Func<FirebaseEvent<Kunde>, bool> SearchFilter
{
    get { return _searchFilter; }
    set
    {
        if (_searchFilter != value)
        {
            _searchFilter = value;
            OnPropertyChanged(nameof(SearchFilter));
        }
    }
}

// 分页相关属性
private int _currentPage = 1;
public int CurrentPage
{
    get => _currentPage;
    set
    {
        if (_currentPage != value)
        {
            _currentPage = value;
            OnPropertyChanged(nameof(CurrentPage));
        }
    }
}

private const int PageSize = 20;
private bool _isLoading;
public bool IsLoading
{
    get => _isLoading;
    set
    {
        if (_isLoading != value)
        {
            _isLoading = value;
            OnPropertyChanged(nameof(IsLoading));
        }
    }
}

// 用于跟踪总数据量(过滤后)
private int _totalFilteredCount;

public Kunden()
{
    InitializeComponent();
    BindingContext = this;

    // 监听CollectionView滚动事件,判断是否到底部
    var scrollObservable = Observable.FromEventPattern<ItemsViewScrolledEventArgs>(CollView, nameof(CollView.Scrolled))
        .Where(_ => !IsLoading)
        .Select(e => e.EventArgs)
        .Where(args =>
        {
            // 判断是否滚动到接近底部(剩余10条时触发加载)
            var totalItems = args.TotalItems;
            var lastVisibleItemIndex = args.LastVisibleItemIndex;
            return totalItems > 0 && lastVisibleItemIndex >= totalItems - 10;
        })
        .Throttle(TimeSpan.FromMilliseconds(300)) // 防抖,避免频繁触发
        .Subscribe(_ => LoadNextPage());

    // 监听搜索文本变化,重置分页
    var searchSubscription = this.WhenAnyValue(x => x.searchBar.Text)
        .DistinctUntilChanged()
        .Throttle(TimeSpan.FromMilliseconds(500))
        .SubscribeOn(NewThreadScheduler.Default)
        .ObserveOn(SynchronizationContext.Current)
        .Subscribe(suchText =>
        {
            Debug.WriteLine("Eingabe Suchleiste :" + suchText);
            SearchFilter = w => string.IsNullOrWhiteSpace(suchText) || String.Join(" ", new List<string>() { w.Object.Kundennummer.ToString(), w.Object.Firma, w.Object.Vorname, w.Object.Nachname }).Contains(suchText, StringComparison.CurrentCultureIgnoreCase);
            // 搜索时重置分页到第一页
            CurrentPage = 1;
        });

    // 整合分页、过滤、排序的SourceCache订阅
    var dataSubscription = FirebaseService.SourceListKunden.Connect()
        .Filter(this.WhenAnyValue(x => x.SearchFilter))
        .Sort(SortExpressionComparer<FirebaseEvent<Kunde>>.Descending(x => x.Key))
        .Do(list => _totalFilteredCount = list.Count) // 记录过滤后的总数据量
        .Skip(this.WhenAnyValue(x => x.CurrentPage).Select(page => (page - 1) * PageSize))
        .Take(PageSize)
        .SubscribeOn(NewThreadScheduler.Default)
        .ObserveOn(SynchronizationContext.Current)
        .Bind(out myDerivedList)
        .Subscribe();

    CollView.ItemsSource = myDerivedList;
}

private void LoadNextPage()
{
    // 判断是否还有更多数据可以加载
    var totalPages = (int)Math.Ceiling((double)_totalFilteredCount / PageSize);
    if (CurrentPage >= totalPages)
        return;

    IsLoading = true;
    try
    {
        CurrentPage++;
    }
    finally
    {
        IsLoading = false;
    }
}

关键细节说明

  • 滚动监听:通过FromEventPattern将CollectionView的Scrolled事件转为可观察序列,加入防抖和加载状态判断,避免重复触发
  • 分页逻辑:用Skip((CurrentPage-1)*PageSize)和Take(PageSize)来控制每次加载的数据量,CurrentPage变化时自动重新计算数据
  • 搜索重置:搜索关键词变化时重置CurrentPage为1,自动重新加载第一页的过滤后数据
  • 加载状态:IsLoading属性防止滚动时频繁触发加载请求

内容的提问来源于stack exchange,提问作者Thomas Ebert

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 01:15:07