.NET MAUI中DatePicker选择后CollectionView网格不更新问题
问题:NHL赛程应用切换日期后CollectionView无数据显示
开发调用NHL API的赛程展示应用,默认加载当日赛事,DatePicker置于CollectionView.Header中,通过ScheduleTemplate的Grid展示数据。选择新日期后,调试确认ObservableGames已加载新数据,但Grid清空,仅显示DatePicker。
相关代码
SchedulePage.xaml
<RefreshView Command="{Binding UpdateScheduleCommand}" IsRefreshing="{Binding IsRefreshing}"> <CollectionView Margin="20" ItemsSource="{Binding ObservableGames}" ItemTemplate="{StaticResource ScheduleTemplate}" SelectionMode="Single"> <CollectionView.Header> <Grid> <DatePicker Grid.ColumnSpan="4" HorizontalOptions="Center" Date="{Binding SelectedDatePickerDate, Mode=TwoWay}" /> </Grid> </CollectionView.Header> </CollectionView> </RefreshView>
App.xaml数据模板
<DataTemplate x:Key="ScheduleTemplate"> <Grid Padding="8" RowDefinitions="Auto,Auto" ColumnDefinitions=".1*,.5*,.2*,.2*"> <Image Grid.Row="0" Grid.Column="0" Source="{Binding AwayTeamLogo, Mode=TwoWay}" HeightRequest="20" WidthRequest="20" HorizontalOptions="Center" VerticalOptions="Center" /> <Label Grid.Row="0" Grid.Column="1" Text="{Binding AwayAbbreviatedName, Mode=TwoWay}" HorizontalOptions="Start" /> <Label Grid.Row="0" Grid.Column="2" Text="{Binding AwayGoals, Mode=TwoWay}" HorizontalOptions="End" /> <Label Grid.Row="0" Grid.Column="3" Text="{Binding GameStatusString, Mode=TwoWay}" HorizontalOptions="End" /> <Image Grid.Row="1" Grid.Column="0" Source="{Binding HomeTeamLogo, Mode=TwoWay}" HeightRequest="20" WidthRequest="20" HorizontalOptions="Center" VerticalOptions="Center" /> <Label Grid.Row="1" Grid.Column="1" Text="{Binding HomeAbbreviatedName, Mode=TwoWay}" HorizontalOptions="Start" /> <Label Grid.Row="1" Grid.Column="2" Text="{Binding HomeGoals, Mode=TwoWay}" HorizontalOptions="End" /> <Grid.GestureRecognizers> <TapGestureRecognizer NumberOfTapsRequired="1" Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:ScheduleViewModel}}, Path=GameTapped}" CommandParameter="{Binding .}"> </TapGestureRecognizer> </Grid.GestureRecognizers> </Grid> </DataTemplate>
ViewModel代码
class ScheduleViewModel : INotifyPropertyChanged { public DateTime? _selectedDatePickerDate; bool isRefreshing; public ObservableCollection<Game> ObservableGames { get; private set; } public DateTime DatePickerDate { get; set; } public DateTime CurrentScheduleDate { get; set; } public Command<Game> GameTapped { get; } public Command<DateTime?> DatePickerTapped { get; } public ICommand UpdateScheduleCommand => new Command(async () => await SetScheduleDataAsync()); public bool IsRefreshing { get { return isRefreshing; } set { isRefreshing = value; OnPropertyChanged(); } } public DateTime? SelectedDatePickerDate { get => _selectedDatePickerDate; set { SetProperty(ref _selectedDatePickerDate, value); OnDatePickerSelected(value); } } public ScheduleViewModel() { ObservableGames = new ObservableCollection<Game>(); GameTapped = new Command<Game>(OnGameSelected); DatePickerTapped = new Command<DateTime?>(OnDatePickerSelected); OnPropertyChanged(nameof(ObservableGames)); DatePickerDate = DateTime.Today; SetScheduleDataAsync(); } protected bool SetProperty<T>(ref T backingStore, T value, [CallerMemberName] string propertyName = "", Action onChanged = null) { if (EqualityComparer<T>.Default.Equals(backingStore, value)) return false; backingStore = value; onChanged?.Invoke(); OnPropertyChanged(propertyName); return true; } public event PropertyChangedEventHandler PropertyChanged; void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } async void OnGameSelected(Game game) { await Shell.Current.GoToAsync($"scheduledetails?gameLink={game.link}"); } private void OnDatePickerSelected(DateTime? gameDate) { if (null != gameDate) { DatePickerDate = gameDate.Value; SetScheduleDataAsync(); } } private async Task SetScheduleDataAsync() { IsRefreshing = true; if (DatePickerDate != CurrentScheduleDate) { ObservableGames.Clear(); ScheduleData.LeagueSchedule = ScheduleData.GetSchedule(-1, DatePickerDate, null, string.Empty); try { if ((null != ScheduleData.LeagueSchedule) && (null != ScheduleData.LeagueSchedule.dates) && (ScheduleData.LeagueSchedule.dates.Count > 0) && (null != ScheduleData.LeagueSchedule.dates[0]) && (ScheduleData.LeagueSchedule.dates[0].games.Count > 0)) { ObservableGames = new ObservableCollection<Game>(ScheduleData.LeagueSchedule.dates[0].games); for (int i = 0; i < ObservableGames.Count; i++) { if ((null != ObservableGames[i]) && (null != ObservableGames[i].status) && (!string.IsNullOrEmpty(ObservableGames[i].status.detailedState))) { switch (ObservableGames[i].status.detailedState.ToLower()) { case "final": ObservableGames[i].AwayGoals = ObservableGames[i].teams.away.score; ObservableGames[i].HomeGoals = ObservableGames[i].teams.home.score; ObservableGames[i].GameStatusString = ObservableGames[i].status.detailedState; break; case "in progress": LiveGame liveGame = GameData.GetLiveGame(ObservableGames[i].link); if (null != liveGame) { ObservableGames[i].AwayGoals = liveGame.liveData.linescore.teams.away.goals; ObservableGames[i].HomeGoals = liveGame.liveData.linescore.teams.home.goals; ObservableGames[i].GameStatusString = liveGame.liveData.linescore.currentPeriodTimeRemaining + " " + liveGame.liveData.linescore.currentPeriodOrdinal; } break; case "postponed": ObservableGames[i].GameStatusString = "Postponed"; break; default: ObservableGames[i].GameStatusString = ObservableGames[i].gameDate.ToLocalTime().ToShortTimeString(); break; } } } } } catch (Exception e) { } finally { CurrentScheduleDate = DatePickerDate; IsRefreshing = false; } } } }
问题根源
核心问题在于SetScheduleDataAsync方法中,直接重新创建了ObservableGames的实例:
ObservableGames = new ObservableCollection<Game>(ScheduleData.LeagueSchedule.dates[0].games);
虽然调试时集合已更新,但ObservableGames属性是get; private set;,没有触发PropertyChanged通知。UI绑定的是最初的集合实例,替换新实例后,UI无法感知到变化,因此显示为空。
解决方案
方案1:复用原有集合实例(推荐)
不要创建新的ObservableCollection,而是清空原有集合后添加新元素,利用ObservableCollection自身的变更通知机制:
修改SetScheduleDataAsync中的对应代码:
// 替换原来的ObservableGames = new ObservableCollection<...> var newGames = ScheduleData.LeagueSchedule.dates[0].games; foreach (var game in newGames) { ObservableGames.Add(game); }
如果需要批量添加,可实现AddRange扩展方法:
public static class ObservableCollectionExtensions { public static void AddRange<T>(this ObservableCollection<T> collection, IEnumerable<T> items) { foreach (var item in items) { collection.Add(item); } } }
然后调用:
ObservableGames.AddRange(ScheduleData.LeagueSchedule.dates[0].games);
方案2:让ObservableGames属性触发PropertyChanged通知
修改ViewModel中ObservableGames的定义,使用SetProperty赋值,确保替换实例时触发通知:
private ObservableCollection<Game> _observableGames; public ObservableCollection<Game> ObservableGames { get => _observableGames; private set => SetProperty(ref _observableGames, value); }
构造函数中初始化:
public ScheduleViewModel() { _observableGames = new ObservableCollection<Game>(); // 其他代码... }
这样执行ObservableGames = new ObservableCollection<Game>(...)时,会自动触发PropertyChanged,UI会重新绑定新集合。
额外优化建议
Game类的属性(如AwayGoals、GameStatusString等)需实现INotifyPropertyChanged,否则后续属性值更新时UI不会同步。- 异常捕获中添加日志输出,方便排查潜在错误:
catch (Exception e) { Console.WriteLine($"加载赛程失败:{e.Message}"); } - 避免在构造函数中直接调用异步方法
SetScheduleDataAsync(),可改用页面加载事件触发。
内容的提问来源于stack exchange,提问作者Jason M
相关产品推荐
相关产品推荐

