How to fix performance issue (reduce frozen UI) on ObservableCollection Add() method called

David Melkumyan 20 Reputation points
2024-08-13T15:15:06.5566667+00:00

Description:

Hello, I'm working currently on a .NET MAUI project and i'm facing a performance issue in my Android app, i'm using MVVM pattern via MC-Toolkit, in my page RefreshView is refreshing Bindable items using CollectionView.

Issue:

When i pull to refresh the page, I'm having frozen UI for ~1 second, displaying 10 items, and as more my Items are, worse it goes, from debug I see performance drop on adding each item, same as i put under foreach loop

RelayCommands VM:

[RelayCommand]

async Task Refresh()

{

IsBusy = true;

Coffee.Clear();

await LoadMore();

IsBusy = false;

}

[RelayCommand]

async Task LoadMore()

{

await Task.Delay(1000);

var image = "coffeebag.png";

Coffee.Add(new Coffee { Roaster = "Yes Plz", Name = "Sip of Sunshine", Image = image, HEHE = 12.98754, ID2 = 12653, Id = 1313223, Propp = "sfdawfwajlfn"});

Coffee.Add(new Coffee { Roaster = "Yes Plz", Name = "Potent Potable", Image = "dotnet_bot.svg", HEHE = 12.98754, ID2 = 12653, Id = 1313223, Propp = "sfdawfwajlfn" });

Coffee.Add(new Coffee { Roaster = "Yes Plz", Name = "Potent Potable", Image = image, HEHE = 12.98754, ID2 = 12653, Id = 1313223, Propp = "sfdawfwajlfn" });

Coffee.Add(new Coffee { Roaster = "Yes Plz", Name = "Potent Potable", Image = image, HEHE = 12.98754, ID2 = 12653, Id = 1313223, Propp = "sfdawfwajlfn" });

Coffee.Add(new Coffee { Roaster = "Yes Plz", Name = "Potent Potable", Image = "dotnet_bot.svg", HEHE = 12.98754, ID2 = 12653, Id = 1313223, Propp = "sfdawfwajlfn" });

Coffee.Add(new Coffee { Roaster = "Blue Bottle", Name = "Kenya Kiambu Handege", Image = "dotnet_bot.svg", HEHE = 12.98754, ID2 = 12653, Id = 1313223, Propp = "sfdawfwajlfn" });

Coffee.Add(new Coffee { Roaster = "Blue Bottle", Name = "Kenya Kiambu Handege", Image = "dotnet_bot.svg", HEHE = 12.98754, ID2 = 12653, Id = 1313223, Propp = "sfdawfwajlfn" });

Coffee.Add(new Coffee { Roaster = "Blue Bottle", Name = "Kenya Kiambu Handege", Image = "dotnet_bot.svg", HEHE = 12.98754, ID2 = 12653, Id = 1313223, Propp = "sfdawfwajlfn" });

Coffee.Add(new Coffee { Roaster = "Blue Bottle", Name = "Kenya Kiambu Handege", Image = "dotnet_bot.svg", HEHE = 12.98754, ID2 = 12653, Id = 1313223, Propp = "sfdawfwajlfn" });

Coffee.Add(new Coffee { Roaster = "Blue Bottle", Name = "Kenya Kiambu Handege", Image = "dotnet_bot.svg", HEHE = 12.98754, ID2 = 12653, Id = 1313223, Propp = "sfdawfwajlfn" });

CoffeeGroups.Clear();

CoffeeGroups.Add(new Grouping<string, Coffee>("Blue Bottle", Coffee.Where(c => c.Roaster == "Blue Bottle")));

CoffeeGroups.Add(new Grouping<string, Coffee>("Yes Plz", Coffee.Where(c => c.Roaster == "Yes Plz")));

}

Model:

public partial class Coffee

{

[PrimaryKey, AutoIncrement]

public int Id { get; set; }

public string Roaster { get; set; }

public string Name { get; set; }

public string Image { get; set; }

public int ID2 { get; set; }

public double HEHE { get; set; }

public string Propp { get; set; }

[RelayCommand]

void Test()

{

Console.Write("Test");

}

}

View:

<?xml version="1.0" encoding="utf-8" ?>

<ContentPage

x:Class="MyCoffeeApp.Views.CoffeeEquipmentCVPage"

xmlns="http://schemas.microsoft.com/dotnet/2021/maui"

xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"

xmlns:android="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific;assembly=Microsoft.Maui.Controls"

xmlns:cells="clr-namespace:MyCoffeeApp.Cells"

xmlns:fontAwesome="clr-namespace:FontAwesome"

xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls"

xmlns:model="clr-namespace:MyCoffeeApp.Shared.Models;assembly=MyCoffeeApp.Shared"

xmlns:mvvm="clr-namespace:MvvmHelpers;assembly=MvvmHelpers"

xmlns:viewmodels="clr-namespace:MyCoffeeApp.ViewModels"

x:Name="CoffeePage"

x:DataType="viewmodels:CoffeeEquipmentViewModel"

BackgroundColor="{AppThemeBinding Dark={StaticResource WindowBackgroundColorDark},

Light={StaticResource WindowBackgroundColor}}">

<RefreshView

Command="{Binding RefreshCommand}"

IsRefreshing="{Binding IsBusy, Mode=OneWay}"

Style="{StaticResource BaseRefreshView}">

<CollectionView

BackgroundColor="Transparent"

IsGrouped="True"

ItemSizingStrategy="MeasureAllItems"

ItemsLayout="VerticalList"

ItemsSource="{Binding CoffeeGroups}"

RemainingItemsThreshold="1"

RemainingItemsThresholdReachedCommand="{Binding DelayLoadMoreCommand}"

SelectedItem="{Binding SelectedCoffee, Mode=TwoWay}"

SelectionMode="None">

<CollectionView.EmptyView>

<StackLayout Padding="12">

<Label HorizontalOptions="Center" Text="No coffee" />

</StackLayout>

</CollectionView.EmptyView>

<CollectionView.GroupHeaderTemplate>

<DataTemplate x:DataType="{x:Null}">

<StackLayout Padding="12">

<Label Style="{StaticResource LabelMedium}" Text="{Binding Key}" />

</StackLayout>

</DataTemplate>

</CollectionView.GroupHeaderTemplate>

<CollectionView.ItemTemplate>

<DataTemplate x:DataType="model:Coffee">

<SwipeView android:SwipeView.SwipeTransitionMode="Reveal">

<SwipeView.RightItems>

<SwipeItems SwipeBehaviorOnInvoked="Close">

<SwipeItem

BackgroundColor="Pink"

Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodels:CoffeeEquipmentViewModel}}, Path=FavoriteCommand}"

CommandParameter="{Binding .}"

IconImageSource="dotnet_bot.png"

Text="Favorite" />

<SwipeItem Text="Delete" />

</SwipeItems>

</SwipeView.RightItems>

<SwipeView.LeftItems>

<SwipeItems SwipeBehaviorOnInvoked="Close">

<SwipeItemView>

<Frame

Padding="0"

BackgroundColor="Orange"

CornerRadius="50"

HeightRequest="100"

VerticalOptions="Center"

WidthRequest="100">

<Label

HorizontalOptions="Center"

Text="Circle"

VerticalOptions="Center" />

</Frame>

</SwipeItemView>

<SwipeItemView>

<Frame

Padding="0"

BackgroundColor="Orange"

CornerRadius="50"

HeightRequest="100"

VerticalOptions="Center"

WidthRequest="100">

<Label

HorizontalOptions="Center"

Text="Circle"

VerticalOptions="Center" />

</Frame>

</SwipeItemView>

</SwipeItems>

</SwipeView.LeftItems>

<cells:CoffeeCard>

<FlyoutBase.ContextFlyout>

<MenuFlyout>

<MenuFlyoutItem

Command="{Binding Source={x:Reference CoffeePage}, Path=BindingContext.FavoriteCommand}"

CommandParameter="{Binding .}"

Text="Favorite">

<MenuFlyoutItem.IconImageSource>

<FontImageSource

FontFamily="FAS"

Glyph="{x:Static fontAwesome:FontAwesomeIcons.MugHot}"

Color="{AppThemeBinding Dark=White,

Light={StaticResource SystemGray5Dark}}" />

</MenuFlyoutItem.IconImageSource>

</MenuFlyoutItem>

<MenuFlyoutSeparator />

<MenuFlyoutItem Text="Delete" />

<MenuFlyoutSeparator />

<MenuFlyoutSubItem Text="Options">

<MenuFlyoutItem Text="Option 1" />

<MenuFlyoutItem Text="Option 2" />

<MenuFlyoutItem Text="Option 3" />

<MenuFlyoutItem Text="Option 4" />

</MenuFlyoutSubItem>

</MenuFlyout>

</FlyoutBase.ContextFlyout>

</cells:CoffeeCard>

</SwipeView>

</DataTemplate>

</CollectionView.ItemTemplate>

</RefreshView>

</ContentPage>

P.S Code is modified from Coffe App of James Montemagno.

.NET
.NET
Microsoft Technologies based on the .NET software framework.
3,799 questions
.NET MAUI
.NET MAUI
A Microsoft open-source framework for building native device applications spanning mobile, tablet, and desktop.
3,412 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,844 questions
{count} votes

Accepted answer
  1. Leon Lu (Shanghai Wicresoft Co,.Ltd.) 74,491 Reputation points Microsoft Vendor
    2024-08-22T08:31:18.1966667+00:00

    Hello,

    This issue can be fixed by using Display grouped data in a CollectionView - .NET MAUI | Microsoft Learn to replace of the following code in the NearBusStopsView.xaml. You used to many nested StackLayout indableLayout.ItemsSource. and Nested ContentView. When you load to many data at once, you will get performance issue. You will see [xxx.ap] Explicit concurrent copying GC freed 1730(73KB) AllocSpace objects, 0(0B) LOS objects, 49% free, 5533KB/10MB, paused 264us,61us total 15.069ms in the output window, when you debug your application.

      <ScrollView VerticalScrollBarVisibility="Never">
                    <VerticalStackLayout Margin="0,15,0,0"
                                         Spacing="16"
                                         BackgroundColor="Transparent"
                                         BindableLayout.ItemsSource="{Binding PublicTransportStops}">
                        <BindableLayout.ItemTemplate>
                            <DataTemplate x:DataType="models:PublicTransportStop">
                                <cts:PublicTransportStopItem />
                            </DataTemplate>
                        </BindableLayout.ItemTemplate>
                    </VerticalStackLayout>
                </ScrollView>
    

    Before changing the code, you can comment-line above code, this performance issue will be fixed.

    Firstly, I change PublicTransportStop.cs code like above document. By the way, we cannot extend multiply classes in C#, So I use INotifyPropertyChanged to instead of the INotifyPropertyChanged.

    public partial class PublicTransportStop : List<PublicTransport>, INotifyPropertyChanged
    {
        public PublicTransportStop(List<PublicTransport> publicTransports) : base(publicTransports)
        {
            StopDescription = string.Empty;
        }
     
        public string StopNumber { get; set; }
     
        public string StopDescription { get; set; }
     
        private bool isStared;
     
        public bool IsStared
        {
            get { return isStared; }
            set { isStared = value; OnPropertyChanged(); }
        }
     
     
        public int Distance { get; set; }
     
        public event PropertyChangedEventHandler PropertyChanged;
     
        public void OnPropertyChanged([CallerMemberName] string name = "") =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
    

    Then you need to change popup data way in the GetNearestBusStopsAsync of GttTransportService.cs

    public Task<List<PublicTransportStop>> GetNearestBusStopsAsync(int distance)
     {
         return Task.FromResult(new List<PublicTransportStop>()
         {
              new PublicTransportStop(new List<PublicTransport>()
                 {
                     new PublicTransport()
                     {
                         ArrivalTimeRemaining = 4,
                         Description = "FALCHERA",
                         IsRealTime = true,
                         Number = 4,
                         TransportType = TransportTypes.Tram
                     },
                     new PublicTransport()
                     {
                         ArrivalTimeRemaining = 12,
                         Description = "FALCHERA",
                         IsRealTime = false,
                         Number = 46,
                         TransportType = TransportTypes.Bus
                     }
                 })
             {
                 StopDescription = "CORRADINO",
                 StopNumber = "233",
                
             },
             ...
    

    Then use following Collectionview in the NearBusStopsView.xaml

            <RefreshView Grid.Column="0"
                         Grid.Row="2"
                         IsRefreshing="{Binding IsBusy, Mode=OneWay}"
                         Command="{Binding DownloadTextCommand}"
                         ZIndex="0">
    
                <CollectionView 
                    Margin="0,15,0,0"                           
                    IsGrouped="true"
                    ItemsSource="{Binding PublicTransportStops}">
                    <CollectionView.GroupHeaderTemplate>
                        <DataTemplate x:DataType="models:PublicTransportStop">
    
                            <Grid RowDefinitions="50,1,*"
                                ColumnDefinitions="*">
                                <Border StrokeShape="RoundRectangle 12 12 0 0"
                                         StrokeThickness="0"
                                         BackgroundColor="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource SecondaryDarkText}}"/>
                                <Grid RowDefinitions="*"
                                           ColumnDefinitions="*,50,30"
                                           Padding="15,0,15,0">
                                    <HorizontalStackLayout Grid.Column="0" 
                                    VerticalOptions="Center">
                                        <Label FontFamily="InterMedium"
                                                Text="{Binding StopNumber, Mode=OneWay, StringFormat='Fermata {0}'}"
                                                LineHeight="1.3"
                                                TextColor="{StaticResource White}"
                                                Margin="0,0,12,0"
                                                FontSize="15"/>
                                        <Label FontFamily="InterRegular"
                                                 Text="{Binding StopDescription, Mode=OneWay}"
                                                 TextColor="{StaticResource White}"
                                                 LineHeight="1.3"
                                                 FontSize="15"/>
                                    </HorizontalStackLayout>
                                    <Label Grid.Column="1" 
                                            LineHeight="1.3"
                                            FontSize="16"
                                            Text="{Binding Distance, Mode=OneWay, StringFormat='{0}m'}"
                                            TextColor="{StaticResource White}"
                                            HorizontalOptions="Center"
                                            FontFamily="InterSemiBold"
                                            VerticalOptions="Center"/>
                                    <ImageButton Grid.Column="2" 
                                               HeightRequest="30"
                                               CornerRadius="50"
                                               Command="{Binding StarUnStarCommand, Source={x:RelativeSource AncestorType={x:Type viewModels:NearBusStopsViewModel} }}"
                                               CommandParameter="{Binding .}"
                                               Source="rate_star_icon"
                                               Clicked="ImageButton_Clicked"
                                               Grid.Row="0">
                                        <ImageButton.Triggers>
                                            <DataTrigger TargetType="ImageButton" Binding="{Binding IsStared}" Value="True">
                                                <Setter Property="Source" Value="rate_star_filled_icon"></Setter>
                                            </DataTrigger>
                                            <DataTrigger TargetType="ImageButton" Binding="{Binding IsStared}" Value="False">
                                                <Setter Property="Source" Value="rate_star_icon"></Setter>
                                            </DataTrigger>
                                        </ImageButton.Triggers>
                                    </ImageButton>
                                </Grid>
                                <BoxView BackgroundColor="{AppThemeBinding Light={StaticResource Gray100}, Dark={StaticResource White}}" 
                                           Grid.Column="0"
                                           Grid.Row="1"/>
                            </Grid>
                        </DataTemplate>
                    </CollectionView.GroupHeaderTemplate>
    
    
                    <CollectionView.ItemTemplate>
                        <DataTemplate x:DataType="models:PublicTransport">
    
                            <Grid RowDefinitions="42,32,1" 
                                      ColumnDefinitions="30,*,.6*">
                                <Border StrokeThickness="0"
                                              HeightRequest="30"
                                              Grid.Column="0"
                                              WidthRequest="30"
                                              VerticalOptions="Center"
                                              StrokeShape="RoundRectangle 6"
                                              BackgroundColor="{StaticResource Primary}">
                                    <Image Source="filter_bus_icon"
                                               Aspect="Center">
                                        <Image.Behaviors>
                                            <toolkit:IconTintColorBehavior TintColor="White"/>
                                        </Image.Behaviors>
                                        <Image.Triggers>
                                            <DataTrigger TargetType="Image" Binding="{Binding TransportType, Converter={toolkit:EnumToIntConverter}}" Value="0">
                                                <Setter Property="Source" Value="filter_bus_icon"/>
                                            </DataTrigger>
                                            <DataTrigger TargetType="Image" Binding="{Binding TransportType, Converter={toolkit:EnumToIntConverter}}" Value="1">
                                                <Setter Property="Source" Value="filter_tram_icon"/>
                                            </DataTrigger>
                                        </Image.Triggers>
                                    </Image>
                                </Border>
                                <Label Text="{Binding Number, Mode=OneWay, StringFormat='№ {0}'}"
                                          FontSize="18"
                                          Margin="8,0,0,0"
                                          FontFamily="InterSemiBold"
                                          VerticalOptions="Center"
                                          Grid.Column="1"/>
                                <cts:StatusIndicator Grid.Column="2" />
                                <Image Source="arrow_down_left_icon"
                                              Aspect="Center"
                                              Grid.Row="1"/>
                                <Label Text="{Binding Description, Mode=OneWay, StringFormat='Direzione: {0}'}"
                                          FontSize="14"
                                          Grid.Row="1"
                                          VerticalOptions="Center"
                                          FontFamily="InterRegular"
                                          Grid.Column="1"/>
                                <HorizontalStackLayout Grid.Row="1"
                                         Spacing="8"
                                         HorizontalOptions="EndAndExpand"
                                         Grid.Column="2">
                                    <Image Source="clock_icon"
                                               VerticalOptions="Center"
                                               Aspect="Center"/>
                                    <Label Text="{Binding ArrivalTimeRemaining, Mode=OneWay, StringFormat='{0} min'}"
                                                  FontSize="17"
                                                  HorizontalTextAlignment="End"
                                                  VerticalOptions="Center"
                                                  FontFamily="InterMedium"/>
                                </HorizontalStackLayout>
                                <BoxView BackgroundColor="{AppThemeBinding Light={StaticResource Gray100}, Dark={StaticResource White}}" 
                                             Grid.Column="0" Grid.ColumnSpan="3"
                                             Grid.Row="2"/>
                            </Grid>
    
                        </DataTemplate>
                    </CollectionView.ItemTemplate>
                </CollectionView>
            </RefreshView>
    
    

    In the end, do not forget to add following click event in the NearBusStopsView.xaml.cs.

        private async void ImageButton_Clicked(object sender, EventArgs e)
        {
            var btn = (ImageButton)sender;
            if (btn != null)
            {
                await btn.ScaleTo(0.8, 200, Easing.Default);
                await btn.ScaleTo(1, 200, Easing.SpringOut);
            }
        }
    
    

    Best Regards,

    Leon Lu


    If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    1 person found this answer helpful.

1 additional answer

Sort by: Most helpful
  1. Deleted

    This answer has been deleted due to a violation of our Code of Conduct. The answer was manually reported or identified through automated detection before action was taken. Please refer to our Code of Conduct for more information.


    Comments have been turned off. Learn more

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.