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

.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 10:40:30