unoplatform / uno

Build Mobile, Desktop and WebAssembly apps with C# and XAML. Today. Open source and professionally supported.
https://platform.uno
Apache License 2.0
8.76k stars 706 forks source link

[Android] Performance-Issue on collection-rendering #1403

Open neoleptic opened 5 years ago

neoleptic commented 5 years ago

We choose Uno (over plain Xamarin because of the Xaml-Support) for porting our Apps to Android. So far, Uno seems quiet promising. Although, while testing the MvvmLight and Control capabilities in a relatively simple Application, I discovered some Performance-Issues while Self-Knowing Collection-Controls are rendered. It first occured when using a ListView with an ItemsSource bound to the ViewModel, but I tried different approaches, a one-column GridView, a Combobox as well as a ContentControl, and except the ContentControl (which has no Selection-Capabilities) any Navigation to the page containing these Controlls took at least 3 Seconds (this was tested with a Nokia 3 [MediaTek MT6737] with Android 8.1), and aproximately another second for every Collection-Control added to the page. There are no noteworthy differences, when adding Items directly in Xaml, instead of binding to the ViewModel.

This is the related View

<Page
    x:Class="UnoMvvm.Shared.View.SecondView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:UnoMvvm.Shared.View"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:c="using:Microsoft.Xaml.Interactions.Core"
    xmlns:i="using:Microsoft.Xaml.Interactivity"
    xmlns:dm="using:UnoMvvm.Shared.Datamodel"
    mc:Ignorable="d"
    NavigationCacheMode="Enabled"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup>
            <VisualState>
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="450"/>
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Target="spHeader.Orientation"
                            Value="Horizontal"/>
                </VisualState.Setters>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition />
            <RowDefinition />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="200"/>
        </Grid.RowDefinitions>
        <StackPanel x:Name="spHeader" Orientation="Vertical" >
            <TextBlock x:Name="tbTitle" Text="{Binding Title}" FontSize="30" 
                               Margin="20 20 0 0" TextWrapping="Wrap"/>
            <TextBlock Margin="20 20 0 0" VerticalAlignment="Bottom">
                <Run Text="Number of View Entries: "/>
                <Run Text="{Binding EntryCounter}"/>
            </TextBlock>
        </StackPanel>
        <Button Content="Go Back" Command="{Binding GoBackCommand}" Grid.Row="1"
                      HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <ComboBox Grid.Row="2" HorizontalAlignment="Center" Width="300" 
                              VerticalAlignment="Center" ItemsSource="{Binding ItemCollection}">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}"/>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
        <!--<ItemsControl ItemsSource="{Binding ItemCollection}" Grid.Row="3" 
                                       HorizontalAlignment="Center" VerticalAlignment="Center">
            <ItemsControl.ItemTemplate>
                <DataTemplate x:DataType="dm:Item">
                    <Grid>
                        <i:Interaction.Behaviors>
                            <c:EventTriggerBehavior EventName="Clicked">
                                <...>
                            </c:EventTriggerBehavior>
                        </i:Interaction.Behaviors>
                        <TextBlock Text="{Binding Name}"/>
                    </Grid>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>-->
        <!--<GridView ItemsSource="{Binding ItemCollection}" Grid.Row="3" 
                                HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
            <GridView.ItemsPanel>
                <ItemsPanelTemplate>
                    <ItemsStackPanel Orientation="Vertical"/>
                </ItemsPanelTemplate>
            </GridView.ItemsPanel>
            <GridView.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}"/>
                </DataTemplate>
            </GridView.ItemTemplate>
        </GridView>-->
        <!--<ListView ItemsSource="{Binding ItemCollection}" Grid.Row="3" 
                               HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}"/>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>-->
        <!--<ListView Grid.Row="3" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
            <ListView.Items>
                <ListViewItem>
                    <TextBlock Text="Item 1"/>
                </ListViewItem>
                <ListViewItem>
                    <TextBlock Text="Item 2"/>
                </ListViewItem>
                <ListViewItem>
                    <TextBlock Text="Item 3"/>
                </ListViewItem>
            </ListView.Items>
        </ListView>-->
    </Grid>
</Page>

And its ViewModel

using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Windows.Input;
using UnoMvvm.Shared.Datamodel;

namespace UnoMvvm.Shared.ViewModel
{
    public class SecondViewModel : BaseViewModel
    {
        private string title;
        private int entryCounter = 0;
        //private int selectedItemIndex = -1;
        public string Title
        {
            get => title;
            set => Set(ref title, value);
        }
        public int EntryCounter
        {
            get => entryCounter;
            set => Set(ref entryCounter, value);
        }
        //public int SelectedItemIndex
        //{
        //    get => selectedItemIndex;
        //    set => Set(ref selectedItemIndex, value);
        //}

        public ObservableCollection<Item> ItemCollection { get; set; } = new ObservableCollection<Item>();
        public ObservableCollection<Item> ItemCollection2 { get; set; } = new ObservableCollection<Item>();

        public override void OnActivate(object parameter)
        {
            EntryCounter++;
            if (EntryCounter % 10 == 0)
                ItemCollection.Add(new Item($"Item {ItemCollection.Count + 1}"));
        }
        public SecondViewModel()
        {
            Title = "Second View Test Title";
            Enumerable.Range(1, 10).ToList().ForEach(i => ItemCollection.Add(new Item($"Item {i}")));
            Enumerable.Range(1, 10).ToList().ForEach(i => ItemCollection2.Add(new Item($"Item2 {i}")));
        }

        public ICommand GoBackCommand => new RelayCommand(() => navigationService.GoBack());
    }
}

Binding works via a simple ViewModelLocator

using GalaSoft.MvvmLight.Ioc;
using GalaSoft.MvvmLight.Messaging;
using Microsoft.Practices.ServiceLocation;
using System;
using System.Collections.Generic;
using System.Text;
using UnoMvvm.Shared.Services;
using UnoMvvm.Shared.View;

namespace UnoMvvm.Shared.ViewModel
{
    public class ViewModelLocator
    {
        public ViewModelLocator()
        {
            SimpleIoc.Default.Register(() => new NavigationService());
            SimpleIoc.Default.Register<IMessenger>(() => new Messenger());

            Register<MainViewModel, MainView>();
            Register<SecondViewModel, SecondView>();
        }

        public void Register<VM, V>() where VM:class
        {
            SimpleIoc.Default.Register<VM>();
            SimpleIoc.Default.GetInstance<NavigationService>().Configure(typeof(VM).Name, typeof(V));
        }

        public NavigationService NavigationService => SimpleIoc.Default.GetInstance<NavigationService>();

        public MainViewModel MainView => SimpleIoc.Default.GetInstance<MainViewModel>();
        public SecondViewModel SecondView => SimpleIoc.Default.GetInstance<SecondViewModel>();
    }
}

Attatched to the DataContext in CodeBehind

using GalaSoft.MvvmLight.Ioc;
using GalaSoft.MvvmLight.Messaging;
using Microsoft.Practices.ServiceLocation;
using System;
using System.Collections.Generic;
using System.Text;
using UnoMvvm.Shared.Services;
using UnoMvvm.Shared.View;

namespace UnoMvvm.Shared.ViewModel
{
    public class ViewModelLocator
    {
        public ViewModelLocator()
        {
            SimpleIoc.Default.Register(() => new NavigationService());
            SimpleIoc.Default.Register<IMessenger>(() => new Messenger());

            Register<MainViewModel, MainView>();
            Register<SecondViewModel, SecondView>();
        }

        public void Register<VM, V>() where VM:class
        {
            SimpleIoc.Default.Register<VM>();
            SimpleIoc.Default.GetInstance<NavigationService>().Configure(typeof(VM).Name, typeof(V));
        }

        public NavigationService NavigationService => SimpleIoc.Default.GetInstance<NavigationService>();

        public MainViewModel MainView => SimpleIoc.Default.GetInstance<MainViewModel>();
        public SecondViewModel SecondView => SimpleIoc.Default.GetInstance<SecondViewModel>();
    }
}

And registered in App.xaml.cs - OnLaunched

if (e.PrelaunchActivated == false)
{
    if (rootFrame.Content == null)
    {
        // When the navigation stack isn't restored navigate to the first page,
        // configuring the new page by passing required information as a navigation
        // parameter

        SimpleIoc.Default.Reset();
        SimpleIoc.Default.Register<ViewModelLocator>(() => new ViewModelLocator());

        rootFrame.Navigate(typeof(MainView), e.Arguments);
    }
    // Ensure the current window is active
    Windows.UI.Xaml.Window.Current.Activate();
}

We understand the device we are using is not the newest or best, our App is B2B so the users usualy use faster devices. Still, it would be great, if this specific lack in performance could be solved any how, or at least to know, we need to incorporate this issue, when recomending devices to our customers.

davidjohnoliver commented 5 years ago

In this case there are a few things to be aware of, and a few tricks that will improve performance. View creation is an expensive operation on all platforms, but particularly Android.

DanielCauser commented 3 years ago

Came across this issue in one of the projects I'm working on. Switching the ScrollView style as suggested solved the problem.

I applied this style:

<xamarin:Style TargetType="ScrollViewer"
              x:Key="ListViewBaseScrollViewerStyle">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ScrollViewer">
                <xamarin:ListViewBaseScrollContentPresenter x:Name="ScrollContentPresenter"
                                        Content="{TemplateBinding Content}"
                                        ContentTemplate="{TemplateBinding ContentTemplate}"
                                        ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}" />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</xamarin:Style>
pitush commented 9 months ago

@DanielCauser how do i implement your code? Sorry im new on uno plataform.

jeromelaban commented 9 months ago

@pitush Can you open a discussion with the behavior you're getting? Also make sure to read our performance tips documentation first.