.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不再触发,无法加载后续数据
问题原因分析
刷新与加载更多共用状态字段导致冲突
代码中IsRefreshing同时用于控制下拉刷新和加载更多的状态,虽然逻辑上做了互斥判断,但MAUI的CollectionView嵌套在RefreshView内部时,IsRefreshing的状态变更可能干扰CollectionView的滚动阈值检测逻辑,导致后续触发失效。缺少加载完成的判断与命令可用性控制
没有判断已加载数据量是否达到总数据量,也未给LoadMoreCategoriesCommand添加可执行状态控制。当没有更多数据可加载时,CollectionView可能停止触发阈值命令;同时没有阻止重复请求的可靠逻辑,可能导致异步时序问题。分页逻辑缺失
现有代码没有明确的分页偏移量计算(如根据已加载数量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

