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

.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 19:34:49