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

.NET 9 MAUI中CollectionView增量加载命令仅触发一次问题

.NET 9 MAUI CollectionView增量加载后续无法触发加载更多的问题

问题详情

在.NET 9 MAUI应用中使用CollectionView实现大容量列表的增量加载,初始数据加载正常,第一次滚动到底部能触发加载更多,但后续滚动无法再次触发LoadMoreCategoriesCommand。

视图XAML代码

<?xml version="1.0" encoding="utf-8" ?>
<localPages:ContentPageBase xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyNamespace.Pages.MyPage"
             xmlns:localPages="clr-namespace:MyNamespace.Pages"
             xmlns:localViewModels="clr-namespace:MyNamespace.ViewModels"
             xmlns:localModels="clr-namespace:MyNamespace.Providers.Models"
             x:DataType="localViewModels:MyViewModel">
    <Grid Padding="10,10">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="300" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" Grid.ColumnSpan="2" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" Grid.ColumnSpan="2" />
        </Grid.RowDefinitions>

        <Grid Grid.Row="0" ColumnDefinitions="*,Auto"
              HorizontalOptions="FillAndExpand">
            <Label x:Name="TitleLabel"
                   Grid.Column="0"
                   FontAttributes="Bold"
                   Text="Live TV"
                   HorizontalOptions="Start"
                   VerticalOptions="Center"/>
            <Label Grid.Column="1"
                   HorizontalOptions="End"
                   VerticalOptions="Center"
                   Text="{Binding TotalCategoriesCount, StringFormat='({0:N0} Categories)'}" />
        </Grid>

        <!-- Categories Collection -->
        <RefreshView Grid.Row="1"
                     IsRefreshing="{Binding IsRefreshing}"
                     Command="{Binding RefreshCommand}">
            <CollectionView x:Name="CategoriesCollection"
                            Grid.Row="1"
                            SelectionMode="Single"
                            ItemsSource="{Binding Categories}"
                            ItemSizingStrategy="MeasureFirstItem"
                            RemainingItemsThreshold="10"
                            RemainingItemsThresholdReachedCommand="{Binding LoadMoreCategoriesCommand}"
                            ItemsUpdatingScrollMode="KeepScrollOffset"
                            SelectionChanged="CategoriesCollection_SelectionChanged"
                            EmptyView="No categories">
                <CollectionView.ItemsLayout>
                    <GridItemsLayout Orientation="Vertical" />
                </CollectionView.ItemsLayout>
                <CollectionView.ItemTemplate>
                    <DataTemplate x:DataType="localModels:ICategoryInfo">
                        <Border Stroke="DarkGray">
                            <Border.StrokeShape>
                                <RoundRectangle CornerRadius="5,5,5,5" />
                            </Border.StrokeShape>
                            <Grid HeightRequest="40">
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="*" />
                                    <ColumnDefinition Width="Auto" />
                                </Grid.ColumnDefinitions>
                                <Label Text="{Binding Name}"
                                       Padding="5,0,5,0"
                                       FontAttributes="Bold"
                                       VerticalTextAlignment="Center"/>
                                <Label Text="{Binding ID}" Grid.Column="1"
                                       Padding="5,0,5,0"
                                       FontAttributes="Bold"
                                       VerticalTextAlignment="Center"/>
                            </Grid>
                        </Border>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
        </RefreshView>
    </Grid>
</localPages:ContentPageBase>

ViewModel代码

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using MyNamespace.Models;

namespace MyNamespace.ViewModels
{
    internal class MyViewModel : INotifyPropertyChanged
    {
        private const int _CategoriesToFetchPerRequest = 100;

        private bool _isRefreshing;
        private int _totalCategoiesCount;

        public MyViewModel()
        {
            // Initialize data loading
            RefreshCommand.Execute(null);
        }

        public ObservableCollection<ICategoryInfo> Categories { get; } = new();

        public int TotalCategoriesCount
        {
            get => _totalCategoiesCount;
            private set
            {
                if (_totalCategoiesCount != value)
                {
                    _totalCategoiesCount = value;
                    OnPropertyChanged();
                }
            }
        }

        public bool IsRefreshing
        {
            get => _isRefreshing;
            private set
            {
                if (_isRefreshing != value)
                {
                    _isRefreshing = value;
                    OnPropertyChanged();
                }
            }
        }

        public ICommand RefreshCommand => new Command(async () => await RefreshAsync());

        public ICommand LoadMoreCategoriesCommand => new Command(async () => await LoadMoreCategoriesAsync());

        public event PropertyChangedEventHandler? PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private async Task RefreshAsync()
        {
            if (IsRefreshing)
                return;
            IsRefreshing = true;
            try
            {
                // Fetch the categories here
                var categories = ...;
                // Determine total count
                var totalCategories = ...;

                Categories.Clear();
                foreach (var category in categories)
                     Categories.Add(new CategoryInfo() { ID = Categories.Count, Name = category.Name });
                    
                TotalCategoriesCount = totalCategores;
            }
            finally
            {
                IsRefreshing = false;
            }
        }

        private async Task LoadMoreCategoriesAsync()
        {
            if (IsRefreshing)
                return;
            IsRefreshing = true;
            try
            {
                // Fetch the next set of categories here
                var categories = ...;

                foreach (var category in categories)
                     Categories.Add(new CategoryInfo() { ID = Categories.Count, Name = category.Name });
            }
            finally
            {
                IsRefreshing = false;
            }
        }
    }
}

当前表现

  • 初始分类数据加载成功
  • 第一次滚动到列表底部,触发LoadMoreCategoriesCommand并加载下一页数据,符合预期
  • 继续滚动更新后的列表底部,LoadMoreCategoriesCommand不再触发,无法加载后续数据

问题原因分析

  1. 刷新与加载更多共用状态字段导致冲突
    代码中IsRefreshing同时用于控制下拉刷新和加载更多的状态,虽然逻辑上做了互斥判断,但MAUI的CollectionView嵌套在RefreshView内部时,IsRefreshing的状态变更可能干扰CollectionView的滚动阈值检测逻辑,导致后续触发失效。

  2. 缺少加载完成的判断与命令可用性控制
    没有判断已加载数据量是否达到总数据量,也未给LoadMoreCategoriesCommand添加可执行状态控制。当没有更多数据可加载时,CollectionView可能停止触发阈值命令;同时没有阻止重复请求的可靠逻辑,可能导致异步时序问题。

  3. 分页逻辑缺失
    现有代码没有明确的分页偏移量计算(如根据已加载数量Categories.Count作为skip参数),可能导致第二次加载时未正确获取到下一页数据,CollectionView认为没有新数据而不再触发命令。


修复方案

1. 分离刷新与加载更多的状态字段

在ViewModel中新增独立的加载更多状态字段,避免与刷新状态冲突:

private bool _isLoadingMore; // 新增字段,单独控制加载更多状态

2. 完善加载更多逻辑,添加分页与终止判断

修改LoadMoreCategoriesAsync方法,加入分页偏移计算、加载完成判断:

private async Task LoadMoreCategoriesAsync()
{
    // 正在加载或已加载全部数据,直接返回
    if (_isLoadingMore || Categories.Count >= TotalCategoriesCount)
        return;

    _isLoadingMore = true;
    try
    {
        // 计算分页偏移量:已加载数量作为跳过的条目数
        int skipCount = Categories.Count;
        // 按分页参数获取下一批数据
        var categories = await GetCategoriesAsync(skipCount, _CategoriesToFetchPerRequest);

        foreach (var category in categories)
            Categories.Add(new CategoryInfo() { ID = category.ID, Name = category.Name });
    }
    finally
    {
        _isLoadingMore = false;
        // 更新命令可执行状态
        ((Command)LoadMoreCategoriesCommand).ChangeCanExecute();
    }
}

3. 给加载更多命令添加可执行逻辑

修改LoadMoreCategoriesCommand的定义,加入CanExecute判断:

public ICommand LoadMoreCategoriesCommand => new Command(
    async () => await LoadMoreCategoriesAsync(),
    () => !_isLoadingMore && Categories.Count < TotalCategoriesCount);

4. 修正RefreshAsync中的拼写错误

将TotalCategoriesCount = totalCategores;修正为:

TotalCategoriesCount = totalCategories;

5. 确保分页数据获取逻辑正确

在GetCategoriesAsync(即代码中var categories = ...的部分)中,要严格按照skip和take参数获取对应页码的数据,确保每次加载的是未加载过的新数据。


内容的提问来源于stack exchange,提问作者B Singh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 15:24:50