.NET MAUI中集合/组件数据加载缓慢问题求助
.NET MAUI 数据加载后视图渲染优化方案
问题描述
在.NET MAUI中使用与Xamarin.Forms完全相同的数据加载逻辑,但视图加载耗时远高于Xamarin.Forms(后者几乎无延迟)。数据库查询本身无延迟,瓶颈出在查询完成后的视图渲染阶段,需要优化以提升加载速度。
原代码
ViewModel
public DetalleMensualViewModel() { Sales = new ObservableCollection<DetalleMensual>(); LoadItemsCommand = new Command(async () => await ExecuteLoadItemsCommand()); } async Task ExecuteLoadItemsCommand() { await CargarInformacionMensual(); await CargarVentasPorMes(); } async Task CargarInformacionMensual() { DateTime date = DateTime.Parse(ItemId); int inversionTotal = 0; int ventaTotal = 0; using (var db = new Services.AppDbContext()) { // 上月总计 decimal totalVentas = await db.AppSale .Where(w => w.CreateDate < date) .SumAsync(s => (decimal?)s.VentaTotal) ?? 0M; ventaTotal = (int)totalVentas; // 格式化千分位 totalVentas = db.AppSale .Where(w => w.CreateDate < date) .Sum(s => (decimal?)s.InversionTotal) ?? 0M; inversionTotal = (int)totalVentas; Total = ventaTotal - inversionTotal; // 当月销售额 totalVentas = await db.AppSale .Where(w => w.CreateDate.Month == date.Month && w.CreateDate.Year == date.Year) .SumAsync(s => (decimal?)s.VentaTotal) ?? 0M; Ganancia = (int)totalVentas; // 格式化千分位 // 当月投入 totalVentas = await db.AppSale .Where(w => w.CreateDate.Month == date.Month && w.CreateDate.Year == date.Year) .SumAsync(s => (decimal?)s.InversionTotal) ?? 0M; Inversion = (int)totalVentas; // 格式化千分位 TotalMes = Ganancia - Inversion; // 总计(待修正) TotalComputo = Total + Ganancia - Inversion; // 格式化千分位 } } async Task CargarVentasPorMes() { IsBusy = false; IsSpinner = true; Sales.Clear(); try { DateTime date = DateTime.Parse(ItemId); string nombreMes = date.ToString("MMMM yyyy", new System.Globalization.CultureInfo("es-ES")); Title = nombreMes; List<DetalleMensual> detalleMensual; Title = nombreMes; //int total = "$0"; using (var db = new Services.AppDbContext()) { detalleMensual = await db.AppSaleDetail .Join(db.AppProduct, saleDetail => saleDetail.ProductId, product => product.Id, (saleDetail, product) => new { saleDetail, product }) .Where(joined => joined.saleDetail.CreateDate.Month == date.Month && joined.saleDetail.CreateDate.Year == date.Year) .OrderByDescending(joined => joined.saleDetail.CreateDate) .Select(joined => new DetalleMensual { Id = joined.saleDetail.Id, SaleId = joined.saleDetail.SaleId, Nombre = $"{joined.saleDetail.Quantity} x {(joined.product.Nombre.Length > 25 ? $"{joined.product.Nombre.Substring(0, 25)}....." : joined.product.Nombre)}", Precio = joined.product.Precio, Cantidad = joined.saleDetail.Quantity, CreateDate = joined.saleDetail.CreateDate, CodigoBarra = joined.product.CodigoBarra, Descripcion = joined.product.Descripcion, Total = joined.saleDetail.Total, TipoVenta = joined.saleDetail.TipoVenta, }) //.Take(25) .ToListAsync(); } //Sales = new ObservableCollection<DetalleMensual>(detalleMensual); foreach (var item in detalleMensual) { Sales.Add(item); } } catch (Exception ex) { Console.WriteLine(ex.Message); } finally { IsSpinner = false; UpdatedAt = DateTime.Now; } }
View
<RefreshView Padding="0, 10, 0, 0" x:DataType="local:DetalleMensualViewModel" Command="{Binding LoadItemsCommand}" IsRefreshing="{Binding IsBusy, Mode=TwoWay}"> <AbsoluteLayout VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"> <ScrollView AbsoluteLayout.LayoutBounds="0,0,1,1" AbsoluteLayout.LayoutFlags="All"> <StackLayout> <StackLayout Padding="20" BackgroundColor="{StaticResource Primary}"> <StackLayout Orientation="Horizontal" HorizontalOptions="Center"> <Image Source="today.png" WidthRequest="25" HeightRequest="20" Aspect="AspectFit" /> <Label Text="{Binding Title}" FontSize="16" TextColor="White" FontAttributes="Bold" /> </StackLayout> <Label Text="Ultima actualizacion" FontSize="12" TextColor="White" FontAttributes="None" HorizontalTextAlignment="Center" /> <Label Text="{Binding UpdatedAt}" FontSize="12" TextColor="White" FontAttributes="Italic" HorizontalTextAlignment="Center"/> <Grid Margin="0,10,0,0"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="*"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Label Grid.Row = "1" Grid.Column="0" Text="Ingreso mes actual" FontSize="13" TextColor="White" /> <Label Grid.Row = "1" Grid.Column="1" Text="{Binding Ganancia, StringFormat='${0:N0}'}" FontSize="13" TextColor="White" HorizontalTextAlignment="End" /> <Label Grid.Row = "2" Grid.Column="0" Text="Inversion mes actual" FontSize="13" TextColor="White" /> <Label Grid.Row = "2" Grid.Column="1" Text="{Binding Inversion, StringFormat='-${0:N0}'}" FontSize="13" TextColor="White" HorizontalTextAlignment="End" /> </Grid> <BoxView VerticalOptions="Center" Grid.Row = "2" HeightRequest="1" Color="LightGray" /> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="*"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Label Grid.Column="0" Grid.Row = "0" Text="{Binding Title, StringFormat='Ganancia {0}'}" FontSize="13" TextColor="White" /> <Label Grid.Column="1" Grid.Row = "0" Text="{Binding TotalMes, StringFormat='${0:N0}'}" FontSize="13" TextColor="White" HorizontalTextAlignment="End" /> <Label Grid.Column="0" Grid.Row = "1" Text="Total mes anterior" FontSize="13" TextColor="White" /> <Label Grid.Column="1" Grid.Row = "1" Text="{Binding Total, StringFormat='+${0:N0}'}" FontSize="13" TextColor="White" HorizontalTextAlignment="End" /> </Grid> <BoxView VerticalOptions="Center" HeightRequest="1" Color="LightGray" /> <Grid> <Label Grid.Column="0" Grid.Row = "0" Text="Total mes anterior" FontSize="13" TextColor="White" /> <Label Grid.Column="1" Grid.Row = "0" Text="{Binding TotalComputo, StringFormat='${0:N0}'}" FontSize="13" TextColor="White" HorizontalTextAlignment="End" /> </Grid> </StackLayout> <CollectionView x:Name="ItemsListView" ItemsSource="{Binding Sales}" SelectionMode="None"> <CollectionView.ItemTemplate> <DataTemplate x:DataType="model:DetalleMensual"> <StackLayout Padding="10"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="260"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="30" /> </Grid.RowDefinitions> <Label Grid.Column="0" Grid.Row="0" Text="{Binding Nombre}" FontSize="Small" FontAttributes="Bold" /> <StackLayout Grid.Row="0" Grid.Column="1" Orientation="Horizontal" HorizontalOptions="End"> <Label Text="{Binding Total, StringFormat='${0:N0}'}" FontSize="Small" HorizontalTextAlignment="End" TextColor="Green" FontAttributes="Bold"> <Label.Triggers> <DataTrigger TargetType="Label" Binding="{Binding TipoVenta}" Value="Inversión"> <Setter Property="TextColor" Value="Red"/> <Setter Property="Text" Value="{Binding Total, StringFormat='-${0:N0}'}" /> </DataTrigger> </Label.Triggers> </Label> </StackLayout> <Label Grid.Row="1" Grid.Column="0" Text="{Binding CreateDate}" FontSize="Small" FontAttributes="None" /> <Label Grid.Row="1" Grid.Column="1" Text="{Binding TipoVenta}" HorizontalTextAlignment="End" FontSize="14" TextColor="Green"> <Label.Triggers> <DataTrigger TargetType="Label" Binding="{Binding TipoVenta}" Value="Inversión"> <Setter Property="TextColor" Value="Red"/> </DataTrigger> </Label.Triggers> </Label> <Label Grid.Row="2" Text="{Binding CodigoBarra}" FontSize="Small" FontAttributes="None" /> </Grid> <BoxView VerticalOptions="Center" HeightRequest="1" Color="LightGray" /> </StackLayout> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> </StackLayout> </ScrollView> <!-- 居中加载指示器 --> <ActivityIndicator IsVisible="{Binding IsSpinner}" IsRunning="{Binding IsSpinner}" Color="Black" AbsoluteLayout.LayoutBounds="0.5,0.5,0.1,0.1" AbsoluteLayout.LayoutFlags="All" /> </AbsoluteLayout> </RefreshView>
优化方案
1. 批量更新ObservableCollection
当前逐个调用Sales.Add(item)会触发多次CollectionChanged事件,导致UI频繁刷新。直接替换为一次性创建集合:
// 替换foreach循环 Sales = new ObservableCollection<DetalleMensual>(detalleMensual);
如果需要保留原集合引用(比如绑定已初始化),可以添加扩展方法实现批量添加:
public static void AddRange<T>(this ObservableCollection<T> collection, IEnumerable<T> items) { foreach (var item in items) { collection.Add(item); } } // 使用方式: Sales.AddRange(detalleMensual);
这样能大幅减少UI更新次数,提升渲染速度。
2. 合并数据库查询,减少IO操作
CargarInformacionMensual中多次查询同一张表,还存在同步查询(第二个Sum未用异步),合并为一次查询提升效率:
async Task CargarInformacionMensual() { DateTime date = DateTime.Parse(ItemId); using (var db = new Services.AppDbContext()) { // 一次查询获取所有需要的聚合数据 var stats = await db.AppSale .Select(s => new { IsBeforeMonth = s.CreateDate < date, IsCurrentMonth = s.CreateDate.Month == date.Month && s.CreateDate.Year == date.Year, s.VentaTotal, s.InversionTotal }) .ToListAsync(); // 计算上月总计 var beforeMonthData = stats.Where(x => x.IsBeforeMonth); decimal totalVentaBefore = beforeMonthData.Sum(x => (decimal?)x.VentaTotal) ?? 0M; decimal totalInversionBefore = beforeMonthData.Sum(x => (decimal?)x.InversionTotal) ?? 0M; Total = (int)totalVentaBefore - (int)totalInversionBefore; // 计算当月数据 var currentMonthData = stats.Where(x => x.IsCurrentMonth); decimal totalVentaCurrent = currentMonthData.Sum(x => (decimal?)x.VentaTotal) ?? 0M; decimal totalInversionCurrent = currentMonthData.Sum(x => (decimal?)x.InversionTotal) ?? 0M; Ganancia = (int)totalVentaCurrent; Inversion = (int)totalInversionCurrent; TotalMes = Ganancia - Inversion; TotalComputo = Total + TotalMes; } }
3. 优化UI布局,减少嵌套层级
- 移除冗余ScrollView:CollectionView自带滚动功能,外层ScrollView会导致双重滚动,影响性能,直接移除并调整布局结构:
<RefreshView Padding="0, 10, 0, 0" x:DataType="local:DetalleMensualViewModel" Command="{Binding LoadItemsCommand}" IsRefreshing="{Binding IsBusy, Mode=TwoWay}"> <AbsoluteLayout VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"> <StackLayout AbsoluteLayout.LayoutBounds="0,0,1,1" AbsoluteLayout.LayoutFlags="All"> <!-- 顶部统计区域 --> <StackLayout Padding="20" BackgroundColor="{StaticResource Primary}"> <!-- 原有内容保留 --> </StackLayout> <!-- CollectionView直接作为子元素,设置填充剩余空间 --> <CollectionView x:Name="ItemsListView" ItemsSource="{Binding Sales}" SelectionMode="None" VerticalOptions="FillAndExpand"> <!-- 原有ItemTemplate保留 --> </CollectionView> </StackLayout> <!-- 加载指示器 --> <ActivityIndicator IsVisible="{Binding IsSpinner}" IsRunning="{Binding IsSpinner}" Color="Black" AbsoluteLayout.LayoutBounds="0.5,0.5,0.1,0.1" AbsoluteLayout.LayoutFlags="All" /> </AbsoluteLayout> </RefreshView> - 简化ItemTemplate嵌套:将ItemTemplate中的外层StackLayout替换为Grid,减少布局层级:
<DataTemplate x:DataType="model:DetalleMensual"> <Grid Padding="10"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="1" /> </Grid.RowDefinitions> <!-- 原有Grid内容移到此处 --> <Grid Grid.Row="0"> <!-- 原有列/行及内容保留 --> </Grid> <BoxView Grid.Row="1" HeightRequest="1" Color="LightGray" /> </Grid> </DataTemplate>
4. 启用CollectionView虚拟化
明确配置CollectionView的缓存策略,复用列表项视图,减少内存占用和渲染时间:
<CollectionView x:Name="ItemsListView" ItemsSource="{Binding Sales}" SelectionMode="None" ItemsLayout="LinearItemsLayout" CachingStrategy="RecycleElement">
5. 优化属性通知逻辑
确保ViewModel实现INotifyPropertyChanged,且仅在属性值变化时触发通知:
private int _total; public int Total { get => _total; set { if (_total != value) { _total = value; OnPropertyChanged(); } } } // 其他属性如Ganancia、Inversion等同理实现
内容的提问来源于stack exchange,提问作者Felipe Peña
相关产品推荐
相关产品推荐

