How to improve Windows Phone 8 Map control perform

2019-07-21 03:28发布

问题:

In my Windows Phone 8 app, I have map control which has collection of planes (pushpins).

Map is defined in xaml like this:

<maps:Map x:Name="Map">
    <maptk:MapExtensions.Children>

        <maptk:MapItemsControl Name="Planes" ItemTemplate="{StaticResource PlaneTemplate}"/>

    </maptk:MapExtensions.Children>
</maps:Map>

Planes MapItemsControl is bind to collection (ObservableCollection) of objects in view model like this:

private ObservableCollection<IPlane> _planes;
public ObservableCollection<IPlane> Planes
{
    get { return _planes; }
    set
    {
        _planes = value;
        NotifyOfPropertyChange(() => Planes);
    }
}

public async void GetPlanes()
{
    IReadOnlyCollection<IPlane> planes = await realtimePlanes.GetAllPlanesAsync();

    foreach (IPlane newPlane in planes)
    {
        this.Vehicles.Add(newPlane);
    }
}

When I call the GetPlaes(), to get the planes from web api and update the collection, the whole app freeses for couple of seconds. Typically there is something like 150 - 300 items in Planes collectoin.

How should I improve this to make the user experience better, to remove the lag when I update the Planes collection?

Can I somehow force this collection update to be made in background thread(?) or is the problem map control performance? It can not draw all items fast enough in one go?

UPDATE

Here is some more code, how my actual app works.

Just build quick and dirty test class/service to generate test planes:

public class TestRealtimePlaneService : IRealtimePlaneService
{
    private Random random;

    public async Task<IEnumerable<IRealtimePlane>> GetAllPlanesAsync()
    {
        this.random = new Random();

        int count = 350;
        double x0 = 24.9375;
        double y0 = 60.170833;
        int radius = 8000;

        List<TestRealtimePlane> planes = new List<TestRealtimePlane>();

        for (int i = 0; i < count; i++)
        {
            planes.Add(new TestRealtimePlane() { Location = getLocation(x0, y0, radius), Bearing = 1 });
        }

        await Task.Delay(5000); // Just to simulate webservice call

        return planes;
    }

    // Taken from http://gis.stackexchange.com/questions/25877/how-to-generate-random-locations-nearby-my-location
    public GeoCoordinate getLocation(double x0, double y0, int radius)
    {
        double radiusInDegrees = radius / 111000f;

        double u = this.random.NextDouble();
        double v = this.random.NextDouble();
        double w = radiusInDegrees * Math.Sqrt(u);
        double t = 2 * Math.PI * v;
        double x = w * Math.Cos(t);
        double y = w * Math.Sin(t);

        double new_x = x / Math.Cos(y0);

        double foundLongitude = new_x + x0;
        double foundLatitude = y + y0;

        return new GeoCoordinate(foundLatitude, foundLongitude);
    }
}

And here is the actual map component

<maps:Map x:Name="Map">
    <maptk:MapExtensions.Children>

        <maptk:MapItemsControl Name="Planes">
            <maptk:MapItemsControl.ItemTemplate>
                <DataTemplate>
                    <maptk:Pushpin GeoCoordinate="{Binding Location}" PositionOrigin="0.5,0.5">
                        <maptk:Pushpin.Template>
                            <ControlTemplate TargetType="maptk:Pushpin">
                                <Grid Width="45" Height="45" Background="Transparent">
                                    <Polygon Fill="Yellow" Points="22,0 34,13, 12,13" Width="45" Height="45" RenderTransformOrigin="0.5,0.5">
                                        <Polygon.RenderTransform>
                                            <RotateTransform CenterX="0.5" CenterY="0.5" Angle="{Binding Bearing}"/>
                                        </Polygon.RenderTransform>
                                    </Polygon>
                                    <Ellipse Fill="Yellow" Stroke="Black" HorizontalAlignment="Center" VerticalAlignment="Center" Width="15" Height="15" StrokeThickness="2" />
                                </Grid>
                            </ControlTemplate>
                        </maptk:Pushpin.Template>
                    </maptk:Pushpin>
                </DataTemplate>
            </maptk:MapItemsControl.ItemTemplate>
        </maptk:MapItemsControl>

    </maptk:MapExtensions.Children>
</maps:Map>

It seems that the pushpin template also affects quite lot for the performance. If I remove my own control template from pushpin, this works gain just little bit faster.

回答1:

When you use WPToolkit's MapExtension to display pushpins you are looking for trouble. You'd expect that Binding ItemsSource to ObservableCollection<T> gives you easy way to update your map along with nice & smooth user experience.

Hell no! And I quote (myself):

WPToolkit's MapExtensions sucks donkey's arse. It's the kind of a component that makes you question why you started programming in the first place.

You can bind ItemsSource to collection but you can't do that in XAML. No, you have to dig up the ItemsSource yourself and set it explicitly in codebehind.

var control = MapExtensions.GetChildren(MyMap).OfType<MapItemsControl>().FirstOrDefault();

Not that bad, but wait, there's more! If collection has items, you can't just replace it with new one or you'll end up with an exception. No, instead you have to clear the items and then add new items one by one. Yes, that's right. One-by-one!

var planes = (await PlaneRepository.GetAllPlanesAsync()).ToList();

if (Planes.Any())
    Planes.Clear();

foreach (var plane in planes)
    Planes.Add(plane);

When you update the collection then the UI is blocked. There is no Dispatcher, BackgroundWorker nor Task<T> that gets you around this.

If someone has any doubts, then by all means, please point out where I went wrong. I set up a public GitHub repo (master branch) to fork from.

 

In order to make map non-blocking you have to do some compromises and ditch the MapExtensions altogether. When you create new MapOverlay, having one Pushpin as content, and add those manually to your map, UI remains responsive.

But there's a catch, too. If you add lot's of MapOverlays at once then you still have that brief freeze. If you delay addition of each item on purpose (say, 75ms) then you have nice effect where pushpins appear on your map one by one and UI remains responsive.

Task.Factory.StartNew(() =>
    {
        if (message.Delay != null)
        {
            foreach (var plane in message.Planes)
            {
                AddPins(new[] {plane});
                Thread.Sleep(message.Delay.Value);
            }
        }
    });

 

private void AddPins(IEnumerable<IPlane> planes)
{
    DispatcherHelper.CheckBeginInvokeOnUI(() =>
        {
            foreach (var plane in planes)
            {
                var pushpin = new Pushpin
                    {
                        Style = Resources["PlaneStyle"] as Style
                    };

                var pushpinOverlay = new MapOverlay
                    {
                        GeoCoordinate = plane.Location,
                        Content = pushpin
                    };
                _pushpinLayer.Add(pushpinOverlay);
            }
        });
}

 

Responsive map example, using MapOverlays, is located in the same GitHub repo but in no-map-extensions branch.

That's how you improve Windows Phone 8 Map control performance :).



回答2:

Its because you are awaiting the main thread. You should use a BackgroundWorker to go off and get the planes, or just do it asynchronously. and then update the UI. Getting back to the main thread is VERY easy.

Deployment.Current.Dispatcher.BeginInvoke(() =>
{
    //Stuff to do in UI thread
});