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

.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 09:39:24