.NET MAUI嵌套CollectionView刷新时触发空引用异常问题
问题现象
页面初始加载时所有功能运行正常,数据可正确完整展示,但执行页面更新操作(切换至其他页面后返回、下拉刷新)时会抛出null reference exception空引用异常,暂未明确问题调试方法与修复方案。
页面功能说明
当前页面用于展示Display列表数据,每个Display实体下分别关联一组视频列表与一组图片列表。
XAML布局代码(DisplaysPage.xaml)
<StackLayout> <Button Text="+ Create new display" Command="{Binding UploadDisplayCommand}" CornerRadius="0" BackgroundColor="ForestGreen" FontAttributes="Bold" /> <Label Text="No displays found in database." TextColor="White" FontSize="20" HorizontalTextAlignment="Center" IsVisible="{Binding IsEmpty}" Padding="20" /> <RefreshView IsRefreshing="{Binding IsRefreshing}" RefreshColor="Cyan" Command="{Binding LoadDisplaysCommand}" Margin="0, 5, 0, 1"> <CollectionView ItemsSource="{Binding Displays}"> <CollectionView.ItemTemplate> <DataTemplate x:DataType="model:Display"> <Grid Padding="5"> <Frame CornerRadius="5" Padding="10"> <StackLayout Spacing="10"> <StackLayout Orientation="Horizontal"> <Label Text="{Binding Name}" FontAttributes="Bold" FontSize="23" TextColor="White" /> <StackLayout Padding="0, -10, 0, -10" HorizontalOptions="EndAndExpand"> <Switch IsToggled="{Binding IsOn}" Toggled="Switch_Toggled" OnColor="LightGreen" ThumbColor="White" /> </StackLayout> </StackLayout> <StackLayout BindableLayout.ItemsSource="{Binding Videos}"> <BindableLayout.ItemTemplate> <DataTemplate x:DataType="model:Video"> <Grid Padding="0, 5"> <Frame CornerRadius="5" Padding="0" BackgroundColor="AliceBlue"> <Image Source="{Binding ThumbnailPath}" Aspect="AspectFill" /> </Frame> </Grid> </DataTemplate> </BindableLayout.ItemTemplate> </StackLayout> <StackLayout BindableLayout.ItemsSource="{Binding Images}"> <BindableLayout.ItemTemplate> <DataTemplate x:DataType="model:Video"> <Grid Padding="0, 5"> <Frame CornerRadius="5" Padding="0" BackgroundColor="AliceBlue"> <Image Source="{Binding FilePath}" Aspect="AspectFill" /> </Frame> </Grid> </DataTemplate> </BindableLayout.ItemTemplate> </StackLayout> <Label Text="{Binding Description}" FontSize="16" TextColor="White" /> <StackLayout Orientation="Horizontal"> <Label Text="Number of videos associated with the display: " FontSize="16" TextColor="White" /> <Label Text="{Binding Videos.Count}" FontSize="16" TextColor="White" /> </StackLayout> <StackLayout Orientation="Horizontal"> <Label Text="Number of images associated with the display: " FontSize="16" TextColor="White" /> <Label Text="{Binding Images.Count}" FontSize="16" TextColor="White" /> </StackLayout> </StackLayout> </Frame> </Grid> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> </RefreshView> </StackLayout>
视图模型代码(DisplaysViewModel.cs)
[INotifyPropertyChanged] public partial class DisplaysViewModel { private readonly Service<Display> displayService; [ObservableProperty] private ObservableCollection<Display> displays = new(); [ObservableProperty] private bool isEmpty; [ObservableProperty] private bool isRefreshing; public DisplaysViewModel(Service<Display> displayService) { this.displayService = displayService; } [ICommand] internal async Task LoadDisplaysAsync() { #if ANDROID || IOS || tvOS || Tizen UserDialogs.Instance.ShowLoading("Loading displays from the database..."); #endif if (Displays.Count is not 0) { Displays.Clear(); } try { await foreach (Display display in displayService.GetAllAsync().OrderBy(x => x.Id)) { Displays.Add(display); } } catch (Exception ex) { throw new Exception(ex.Message); } finally { if (Displays.Count is 0) { IsEmpty = true; } else { IsEmpty = false; } IsRefreshing = false; #if ANDROID || IOS || tvOS UserDialogs.Instance.HideLoading(); #endif } } [ICommand] private async Task UploadDisplayAsync() { await Shell.Current.DisplayAlert("Create a new display", "Under construction!", "OK"); } }
排查进展
经断点调试,已初步定位问题来源:空引用异常出现在代码隐藏文件DisplaysPage.xaml.cs的Switch_Toggled事件中,调用GetByIdAsync方法时触发,对应后置代码如下:
public partial class DisplaysPage : ContentPage { private readonly Service<Display> displayService; private readonly Service<Log> logService; public DisplaysPage(DisplaysViewModel displaysViewModel, Service<Display> displayService, Service<Log> logService) { InitializeComponent(); this.displayService = displayService; this.logService = logService; BindingContext = displaysViewModel; } protected override async void OnAppearing() { await ((DisplaysViewModel)BindingContext).LoadDisplaysAsync(); } private async void Switch_Toggled(object sender, ToggledEventArgs e) { Switch displaySwitch = sender as Switch; Display selectedDisplay = displaySwitch.BindingContext as Display; try { Display displayToUpdate = await displayService.GetByIdAsync(selectedDisplay.Id); displayToUpdate.IsOn = displaySwitch.IsToggled; string isOnString = displaySwitch.IsToggled ? "on" : "off"; await displayService.UpdateAsync(displayToUpdate); await logService.CreateAsync(new Log() { Description = $"{JwtParser.ParseClaimsFromJwt(await SecureStorage.GetAsync("JWT")).Where(x => x.Type == ClaimTypes.Name).FirstOrDefault()?.Value} has {isOnString} the display.", UserId = JwtParser.ParseClaimsFromJwt(await SecureStorage.GetAsync("JWT")).Where(x => x.Type == ClaimTypes.NameIdentifier).FirstOrDefault()?.Value }); } catch (Exception) { throw new Exception("null exception"); } } }
异常产生原因
- 核心触发逻辑:.NET MAUI/Forms中
Switch控件的Toggled事件,会在列表项被回收、控件BindingContext重置时自动触发。当下拉刷新、页面返回执行LoadDisplaysAsync方法清空Displays集合时,CollectionView会销毁原有列表项,此时列表内的Switch触发Toggled事件,控件的BindingContext已经被置为null,代码中直接将displaySwitch.BindingContext转成Display类型拿到的是null,后续访问selectedDisplay.Id就会抛出空引用异常。 - 次要潜在问题:XAML中图片列表的DataTemplate错误声明了数据类型为
model:Video,开启编译绑定后会触发类型不匹配错误。 - 代码缺陷:现有try-catch逻辑直接抛出新的自定义异常,吞掉了原始错误堆栈,不利于排查具体问题。
修复方案
- 在
Switch_Toggled事件入口增加严格的空值校验,直接拦截列表回收、BindingContext失效场景下的无效事件触发,修改后的事件代码如下:
private async void Switch_Toggled(object sender, ToggledEventArgs e) { // 拦截控件为空、绑定数据为空的无效场景 if (sender is not Switch displaySwitch || displaySwitch.BindingContext is not Display selectedDisplay) { return; } try { Display displayToUpdate = await displayService.GetByIdAsync(selectedDisplay.Id); // 拦截数据库查询不到对应数据的场景 if (displayToUpdate == null) { return; } displayToUpdate.IsOn = displaySwitch.IsToggled; string isOnString = displaySwitch.IsToggled ? "on" : "off"; await displayService.UpdateAsync(displayToUpdate); // 提前读取解析JWT,避免重复IO和解析操作 string jwt = await SecureStorage.GetAsync("JWT"); var claims = JwtParser.ParseClaimsFromJwt(jwt); await logService.CreateAsync(new Log() { Description = $"{claims.Where(x => x.Type == ClaimTypes.Name).FirstOrDefault()?.Value} has {isOnString} the display.", UserId = claims.Where(x => x.Type == ClaimTypes.NameIdentifier).FirstOrDefault()?.Value }); } catch (Exception ex) { // 输出原始错误日志便于排查,不要直接抛出自定义异常吞掉堆栈 Console.WriteLine($"Switch toggle operation failed: {ex}"); } }
- 修正XAML中图片列表的DataTemplate类型声明,将
x:DataType="model:Video"改为和图片实体匹配的x:DataType="model:Image",避免编译绑定报错。 - 可选优化:执行集合清空操作前,临时移除Switch的Toggled事件绑定,列表数据加载完成后再重新绑定,从触发源头减少无效事件调用,注意入口空值校验是必须保留的兜底逻辑。
内容的提问来源于stack exchange,提问作者DM-98
相关产品推荐
相关产品推荐

