Forum Cross Platform with Xamarin
We are excited to announce that the Xamarin Forums are moving to the new Microsoft Q&A experience. Q&A is the home for technical questions and answers at across all products at Microsoft now including Xamarin!

We encourage you to head over to Microsoft Q&A for .NET for posting new questions and get involved today.

I don't get it, Andorid is just ....

I dont't get it, I tested now hundertes of combinations. Everytime I use the Button Click with a Command or ICommand binding with Android, it is messing up, it is 1000 times slower, means a Code which runs lessthan a second, runs for minutes (10 Minutes and more). IOS is working as expected. I have the newest Xamarin.Forms installed, even going back to an older Version, it is not working

Now, if I start the same Code without the button click, putting it in public TrackViewModel() , everything is perfectly fine

I even can't after the ICommand or Commeand Access reading the Data in the OberservableCollection in the Code behind from the viewmodel

like

((TrackViewModel)this.BindingContext).GPXTrack

it just says Count 0 and the Code is very slow to exceute. But without the click it gives me the result back

What is wrong with the Andorid binding a Button Click in Xamarin.Forms?

Answers

  • jezhjezh Member, Xamarin Team Xamurai

    Everytime I use the Button Click with a Command or ICommand binding with Android, it is messing up, it is 1000 times slower, means a Code which runs lessthan a second, runs for minutes (10 Minutes and more).

    Hi @antero, how can we reproduce this question on our side? We didn't have this problem before .

    Could you please post some code snippets so that we can try to reproduce this question on our side?

  • anteroantero Member ✭✭

    Hi jezh:

    The Code I set up:

    Testpage.xaml:

        <?xml version="1.0" encoding="utf-8" ?>
        <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
                     xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                     xmlns:d="http://xamarin.com/schemas/2014/forms/design"
                     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                     xmlns:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps"
                     xmlns:local="clr-namespace:CycleSplit.ViewModels"
                     mc:Ignorable="d"
                     x:Class="CycleSplit.Views.Testpage" Title="Testpage">
            <ContentPage.BindingContext>
                <local:TestpageViewModel />
            </ContentPage.BindingContext>
    
            <ContentPage.Content>
    
                <StackLayout>
    
                    <Button Text="Add Polyline" Command="{Binding LoadingGPX}"/>
    
                    <local:CustomMapTest x:Name="map" MapType="{Binding MapType}" HeightRequest="300"  CustomPosition="{Binding MyMapSpan}" CustomPolyline="{Binding MyPolyline}" ItemsSource="{Binding Locations}">
                        <maps:Map.ItemTemplate>
                            <DataTemplate>
                                <maps:Pin
                                                Type="SavedPin"
                                                Address="{Binding Address}"
                                                Label="{Binding Description}"
                                                Position="{Binding Position}" />
                            </DataTemplate>
                        </maps:Map.ItemTemplate>
                    </local:CustomMapTest>
                </StackLayout>
            </ContentPage.Content>
        </ContentPage>
    

    Testpage.xaml.cs:

        using System;
        using System.Collections.Generic;
        using System.Linq;
        using System.Text;
        using System.Threading.Tasks;
    
        using Xamarin.Forms;
        using Xamarin.Forms.Xaml;
    
        namespace CycleSplit.Views
        {
            [XamlCompilation(XamlCompilationOptions.Compile)]
            public partial class Testpage : ContentPage
            {
                public Testpage()
                {
                    InitializeComponent();
                }
            }
        }
    

    TestpageViewModel.cs

    using CycleSplit.Models;
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.IO;
    using System.Text;
    using Xamarin.Essentials;
    using System.Linq;
    using System.Xml.Linq;
    using Xamarin.Forms.Maps;
    using System.Globalization;
    using Xamarin.Forms;
    
    namespace CycleSplit.ViewModels
    {
        public class TestpageViewModel : INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler PropertyChanged;
    
            public Command LoadingGPX { get; private set; }
            public Polyline MyPolyline { get; set; }
            public MapSpan MyMapSpan { get; set; }
            public Position MyPosition { get; }
            public MapType MapType { get; }
    
            private ObservableCollection<GPXTrack> _gpxTrack;
            public ObservableCollection<GPXTrack> GPXTrack
            {
                get { return _gpxTrack; }
                set { _gpxTrack = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("GPXTrack")); }
            }
    
            #region temp varible for testing
    
            public string _gpxFileName = "testgpx1.gpx";
    
            public XDocument gpxDoc;
    
            public string _gpxString;
            private string GpxString
            {
                get { return _gpxString; }
                set
                {
                    this._gpxString = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("GpxString"));
                }
            }
    
            #endregion
    
            public TestpageViewModel()
            {
                LoadingGPX = new Command(async() => LoadGPXTrack());
    
                MapType = MapType.Street;
                MyPolyline = new Polyline();
                MyPosition = new Position();
                _gpxTrack = new ObservableCollection<GPXTrack>();
    
            }
    
            public async System.Threading.Tasks.Task<XDocument> GetGpxDoc(string sFile)
            {
                using (var stream = await FileSystem.OpenAppPackageFileAsync(sFile))
                {
                    using (var reader = new StreamReader(stream))
                    {
                        var fileContents = await reader.ReadToEndAsync();
                        XDocument gpxDoc = XDocument.Parse(fileContents);
                        return gpxDoc;
                    }
                }
            }
    
            /// <summary> 
            /// Load the namespace for a standard GPX document 
            /// </summary> 
            /// <returns></returns> 
            private XNamespace GetGpxNameSpace()
            {
                XNamespace gpx = XNamespace.Get("http://www.topografix.com/GPX/1/1");
                return gpx;
            }
    
            /// <summary>
            /// test Load from File
            /// </summary>
            /// 
    
            public async System.Threading.Tasks.Task<XDocument> GetGpxDocLoad()
            {
    
                var customFileType =
                new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
                {
                                {DevicePlatform.iOS, new[] {"com.xyz.gpx"} },
                                {DevicePlatform.Android, new[] {"application/gpx"}},
                                {DevicePlatform.UWP, new[] {".gpx"}}
                });
    
                var pickResult = await FilePicker.PickAsync(new PickOptions
                {
                    PickerTitle = "Pick GPX File"
                });
    
                string gpxFile = "";
    
                if (pickResult != null)
                {
                    var stream = await pickResult.OpenReadAsync();
                    gpxDoc = XDocument.Load(stream);
                }
                string localPath = Path.Combine(FileSystem.AppDataDirectory, "testgpx99.txt");
                File.WriteAllText(localPath, gpxDoc.ToString());
                return gpxDoc;
            }
    
            public async System.Threading.Tasks.Task<XDocument> LoadGpxDocLoad(string sFile)
            {
                string localPath = Path.Combine(FileSystem.AppDataDirectory, "testgpx99.txt");
                var gpxprase = File.ReadAllText(localPath);
                XDocument gpxDoc = XDocument.Parse(gpxprase);
                return gpxDoc;
            }
    
            public async void LoadGPXTrack()
            {
                _gpxTrack.Clear();
                MyPolyline.Geopath.Clear();
    
                await GetGpxDocLoad();
                string newFile = "testgpx99.gpx"; // This is only set for easy testing
                XDocument gpxDoc = await LoadGpxDocLoad(newFile);
                XNamespace gpx = GetGpxNameSpace();
                if (gpxDoc != null)
                {
    
                    var tracks = from track in gpxDoc.Descendants(gpx + "trk")
                                 select new
                                 {
                                     Name = track.Element(gpx + "name") != null ?
                                    track.Element(gpx + "name").Value : null,
                                     Segs = (
                                        from trackpoint in track.Descendants(gpx + "trkpt")
                                        select new
                                        {
                                            Latitude = trackpoint.Attribute("lat").Value,
                                            Longitude = trackpoint.Attribute("lon").Value,
                                            Elevation = trackpoint.Element(gpx + "ele") != null ?
                                            trackpoint.Element(gpx + "ele").Value : null,
                                            Time = trackpoint.Element(gpx + "time") != null ?
                                            trackpoint.Element(gpx + "time").Value : null
                                        }
                                      )
                                 };
    
                    StringBuilder sb = new StringBuilder();
    
                    int count = 0;
                    double prevLatitude = 0;
                    double prevLongitude = 0;
                    double distanceTotal = 0;
                    double distance = 0;
                    var polyline = new Polyline();
    
                    foreach (var trk in tracks)
                    {
                        foreach (var trkSeg in trk.Segs)
                        {
    
                            string NumberDecimalSeparator = System.Threading.Thread.CurrentThread.CurrentUICulture.NumberFormat.NumberDecimalSeparator;
    
                            double invariantCultureLatitude = 0;
                            double invariantCultureLongitude = 0;
                            double invariantCultureElevation = 0;
    
                            if (trkSeg.Latitude.Contains(".") && NumberDecimalSeparator == ",")
                            {
                                double.TryParse(trkSeg.Latitude, NumberStyles.Any, CultureInfo.InvariantCulture, out invariantCultureLatitude);
                                double.TryParse(trkSeg.Longitude, NumberStyles.Any, CultureInfo.InvariantCulture, out invariantCultureLongitude);
                                double.TryParse(trkSeg.Elevation, NumberStyles.Any, CultureInfo.InvariantCulture, out invariantCultureElevation);
                            }
                            else
                            {
                                double.TryParse(trkSeg.Latitude, NumberStyles.Any, CultureInfo.CurrentCulture, out invariantCultureLatitude);
                                double.TryParse(trkSeg.Longitude, NumberStyles.Any, CultureInfo.CurrentCulture, out invariantCultureLongitude);
                                double.TryParse(trkSeg.Elevation, NumberStyles.Any, CultureInfo.CurrentCulture, out invariantCultureElevation);
                            }
    
                            if (count > 0)
                            {
                                Location sourceCoordinates = new Location(prevLatitude, prevLongitude);
                                Location destinationCoordinates = new Location(invariantCultureLatitude, invariantCultureLongitude);
                                distance = Location.CalculateDistance(sourceCoordinates, destinationCoordinates, DistanceUnits.Kilometers);
                            }
    
                            distanceTotal += distance;
                            prevLatitude = invariantCultureLatitude;
                            prevLongitude = invariantCultureLongitude;
    
                            MyPolyline.StrokeColor = Xamarin.Forms.Color.Red;
                            MyPolyline.StrokeWidth = 3f;
                            MyPolyline.Geopath.Add(new Position(invariantCultureLatitude, invariantCultureLongitude));
    
                            _gpxTrack.Add(new GPXTrack()
                            {
                                Latitude = invariantCultureLatitude,
                                Longitude = invariantCultureLongitude,
                                Elevation = invariantCultureElevation,
                                Distance = distanceTotal
                            });
    
                            count += 1;
                        }
                    }
    
                    MyMapSpan = MapSpan.FromCenterAndRadius(new Position(prevLatitude, prevLongitude), Xamarin.Forms.Maps.Distance.FromKilometers(40));
    
                }
                else
                {
                    int error = 0;
                }
            }
    
        }
    
    public class CustomMapTest : Xamarin.Forms.Maps.Map
    {
        public static readonly BindableProperty CustomPolylineProperty = BindableProperty.Create("CustomPolyline", typeof(Polyline), typeof(CustomMap),
                                                                                                propertyChanged: (bindableObject, oldValue, newValue) =>
                                                                                                {
                                                                                                    Xamarin.Forms.Maps.Map map = bindableObject as Xamarin.Forms.Maps.Map;
                                                                                                    map.MapElements.Clear();
                                                                                                    map.MapElements.Add((Polyline)newValue);
                                                                                                });
    
        public Polyline CustomPolyline
        {
            get => (Polyline)GetValue(CustomPolylineProperty);
            set => SetValue(CustomPolylineProperty, value);
        }
    
    
    
        public static readonly BindableProperty CustomPositionProperty = BindableProperty.Create("CustomPosition", typeof(MapSpan), typeof(CustomMap),
                                                                                        propertyChanged: (bindableObject, oldValue, newValue) =>
                                                                                        {
                                                                                            Xamarin.Forms.Maps.Map map = bindableObject as Xamarin.Forms.Maps.Map;
                                                                                            map.MoveToRegion((MapSpan)newValue);
                                                                                        });
        public MapSpan CustomPosition
        {
            get => (MapSpan)GetValue(CustomPositionProperty);
            set => SetValue(CustomPositionProperty, value);
        }
    
        public CustomMapTest() : base()
        {
    
        }
        public CustomMapTest(MapSpan region) : base(region)
        {
    
        }
    }
    

    }

    Tracks.cs:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Xamarin.Forms;
    using Xamarin.Forms.Maps;
    
    namespace CycleSplit.Models
    {
        public class Track : INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler PropertyChanged;
        }
    
        public class TrackLocation : INotifyPropertyChanged
        {
            Position _position;
            public string Address { get; }
            public string Description { get; }
    
            public Position Position
            {
                get => _position;
                set
                {
                    if (!_position.Equals(value))
                    {
                        _position = value;
                        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Position)));
                    }
                }
            }
    
            public TrackLocation(string address, string description, Position position)
            {
                Address = address;
                Description = description;
                Position = position;
            }
    
            #region INotifyPropertyChanged
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            #endregion
        }
    
        public class GPXTrack : INotifyPropertyChanged
        {
            private double _latitude;
            private double _longitude;
            private double _distance;
            private double _elevation;
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            public double Latitude
            {
                get { return _latitude; }
                //set { trackName = value; }
                set { this._latitude = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Latitude")); }
            }
    
            public double Longitude
            {
                get { return _longitude; }
                //set { trackID = value; }
                set { this._longitude = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Longitude")); }
            }
            public double Distance
            {
                get { return _distance; }
                //set { trackID = value; }
                set { this._distance = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Distance")); }
            }
            public double Elevation
            {
                get { return _elevation; }
                //set { trackID = value; }
                set { this._elevation = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Elevation")); }
            }
        }
    }
    

    I attached the .gpx file, I use to test

    Thanks,

    Markus

  • anteroantero Member ✭✭

    I run the same above Code with IOS, it finished less than a second, Android 10 Minutes
    Since the gpx file was not attached, I used the gpx file from this link with the above code:
    https://oetztaler-radmarathon.com/dam/jcr:b916012a-b238-403d-9a59-1b87b6db8132/gpx_rennradtour-oetztaler-radmarathon.gpx

  • anteroantero Member ✭✭

    For now, I build a work around, it is not pretty, but it speed things up to normal on using Android and it is working

    1. I leave the CustomMap
    2. I added a label and assign the filename of the ggx file I picked to the label, when loop thru that in the viewmodel
    3. When that happed, the label rises a PropertyChanged, where I can pickup the assigned filename and ran the loop again to read the gpx file directly in the Codepage (xxx.xaml.cs) and add the polyline to the map.

    Since I need the data from the gpx file also in the Viewmodel, I need to run the loop (read gpx file) twice. But I can apply the polylines in 2 seconds on the map and also added all the data in the ObservableCollection in the Viewmodel

    Since binding PINs to the map is supported, I add all my PINs with normal binding over viewmodel

    Hopefully Polyline, will be in future also supported normaly as binding object, Maybe this will than resolve Android slowness.

  • JohnHardmanJohnHardman GBUniversity admin
    edited October 11

    @antero

    When I hear about performance differences between Android and iOS on this scale, my general advice is to look at how often the code is resulting in UI updates that result in layout cycles. Unnecessary layout cycles seems to have a much bigger impact on Android than iOS. Once a UI has been rendered, the fewer programmatic updates to the UI the better. If you can find ways to minimise the number of programmatic updates that result in layout changes, the performance will be better (see https://forums.xamarin.com/discussion/comment/423106/#Comment_423106 for a simple example).

    What follows are just a few bits that jumped out whilst reading your post. A proper code review (or profiling) may well find other things too.

    I suggest adding unit tests for your ViewModels, that include tests for the number of PropertyChanged events fired. This may be more detail than some would consider should be tested by a unit test (such tests are fragile), but it’s an effective way of checking that your ViewModels are as efficient as you would want them to be. If the PropertyChanged count for each property is greater than you would expect, there are performance improvements to be made. Whilst you could record without adding unit tests the number of times your propertyChanged handlers update map, it’s harder without adding more overhead (such as dummy ValueConverters) to record the number of times bindings update the UI, hence using unit tests.

    As part of adding unit tests, code such as System.Threading.Thread.CurrentThread.CurrentUICulture.NumberFormat.NumberDecimalSeparator will want to be re-factored, so that the tests are not dependent on environmental considerations. Whilst re-factoring that, any code inside the two nested for loops that can be moved outside those loops should be moved. Even without adding unit tests, moving code outside those loops will improve performance. Whether System.Threading.Thread.CurrentThread.CurrentUICulture.NumberFormat.NumberDecimalSeparator is significantly slower on Android than iOS, I don’t know (you might want to measure it), but calling it only once will be more performant than calling it repeatedly inside nested loops.

    Are there places that binding is being used where it doesn’t need to be? Or are there places where BindingMode.OneTime can be used? Such changes can help performance.

    I haven’t checked in your code, but it’s always worth checking that ObservableCollection is being used correctly. Do collections want to be replaced, or do the contents of collections want to be modified. Are the correct events being handles for the latter?

    It's quite old now, but you might want to search for Jason Smith's presentation about Xamarin.Forms performance. It's a good starting point for improving performance issues.

  • anteroantero Member ✭✭

    @JohnHardman said:
    @antero

    When I hear about performance differences between Android and iOS on this scale, my general advice is to look at how often the code is resulting in UI updates that result in layout cycles. Unnecessary layout cycles seems to have a much bigger impact on Android than iOS. Once a UI has been rendered, the fewer programmatic updates to the UI the better. If you can find ways to minimise the number of programmatic updates that result in layout changes, the performance will be better (see https://forums.xamarin.com/discussion/comment/423106/#Comment_423106 for a simple example).

    What follows are just a few bits that jumped out whilst reading your post. A proper code review (or profiling) may well find other things too.

    I suggest adding unit tests for your ViewModels, that include tests for the number of PropertyChanged events fired. This may be more detail than some would consider should be tested by a unit test (such tests are fragile), but it’s an effective way of checking that your ViewModels are as efficient as you would want them to be. If the PropertyChanged count for each property is greater than you would expect, there are performance improvements to be made. Whilst you could record without adding unit tests the number of times your propertyChanged handlers update map, it’s harder without adding more overhead (such as dummy ValueConverters) to record the number of times bindings update the UI, hence using unit tests.

    As part of adding unit tests, code such as System.Threading.Thread.CurrentThread.CurrentUICulture.NumberFormat.NumberDecimalSeparator will want to be re-factored, so that the tests are not dependent on environmental considerations. Whilst re-factoring that, any code inside the two nested for loops that can be moved outside those loops should be moved. Even without adding unit tests, moving code outside those loops will improve performance. Whether System.Threading.Thread.CurrentThread.CurrentUICulture.NumberFormat.NumberDecimalSeparator is significantly slower on Android than iOS, I don’t know (you might want to measure it), but calling it only once will be more performant than calling it repeatedly inside nested loops.

    Are there places that binding is being used where it doesn’t need to be? Or are there places where BindingMode.OneTime can be used? Such changes can help performance.

    I haven’t checked in your code, but it’s always worth checking that ObservableCollection is being used correctly. Do collections want to be replaced, or do the contents of collections want to be modified. Are the correct events being handles for the latter?

    It's quite old now, but you might want to search for Jason Smith's presentation about Xamarin.Forms performance. It's a good starting point for improving performance issues.

    Hi JohnHardman,

    I did couple of tests, also without System.Threading.Thread.CurrentThread.CurrentUICulture.NumberFormat.NumberDecimalSeparator and this one did not make the difference. When I cycle thru the loop, without adding the polyline, the speed is good. So it has something todo with adding the polyline and binding the command triggering it over the viewmodel. Adding all the data in this loop just into the ObservableCollection, the Speed is good, but than adding it from there, it than again couple of minutes

    I looked over the Performance guide, but it did not help me.

    Thanks,
    Markus

  • JohnHardmanJohnHardman GBUniversity admin
    edited October 11

    @antero said:
    I did couple of tests, also without System.Threading.Thread.CurrentThread.CurrentUICulture.NumberFormat.NumberDecimalSeparator and this one did not make the difference. When I cycle thru the loop, without adding the polyline, the speed is good. So it has something todo with adding the polyline and binding the command triggering it over the viewmodel. Adding all the data in this loop just into the ObservableCollection, the Speed is good, but than adding it from there, it than again couple of minutes

    Yes, that's why most of what I posted above relates to the PropertyChanged (and implicitly CollectionChanged) events and use of binding. You need to minimise the number of times those events result in an already rendered UI being updated. There are options about how to go about that, but what I proposed above is how I would do it. Jason's presentation covers much of this (although BindingMode.OneTime didn't exist when he created that presentation). For anybody looking for it, his (old) presentation is at

Sign In or Register to comment.