Search code examples
c#google-mapsxamarinmauipolyline

.Net Maui Google Maps Polyline drawning route feature


I have this situation: I'm building a .net Maui smartphone sports app that grabs a list of latitude and longitude (new Location class) of a running activity and draws a line (polyline) in the map to display the route. I can grab the list of exercises from the database and I can draw a polyline in the map, the problem is that I can't do both together because I don't know how to databind the Map functionalitys in my ViewModel class.

Here is my xaml code for the ExercisePage.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="DoradSmartphone.Views.ExercisePage"
             xmlns:model="clr-namespace:DoradSmartphone.Models"
             xmlns:viewmodel="clr-namespace:DoradSmartphone.ViewModels"
             xmlns:maps="clr-namespace:Microsoft.Maui.Controls.Maps;assembly=Microsoft.Maui.Controls.Maps"
             xmlns:sensors="clr-namespace:Microsoft.Maui.Devices.Sensors;assembly=Microsoft.Maui.Essentials"
             x:DataType ="viewmodel:ExerciseViewModel"
             Title="{Binding Title}">




    <Grid Padding="5" Margin="5" RowSpacing="5" ColumnSpacing="3">
        <Grid.RowDefinitions>
            <RowDefinition Height="2*"/>
            <RowDefinition Height="150"/>
        </Grid.RowDefinitions>

        <maps:Map Grid.Row="0" x:Name="routeMap" VerticalOptions="CenterAndExpand" Grid.ColumnSpan="3" HeightRequest="400" IsZoomEnabled="False" IsEnabled="False">
            <x:Arguments>
                <MapSpan>
                    <x:Arguments>
                        <sensors:Location>
                            <x:Arguments>
                                <x:Double>38.744418137669875</x:Double>
                                <x:Double>-9.128544160596851</x:Double>
                            </x:Arguments>
                        </sensors:Location>
                        <x:Double>0.7</x:Double>
                        <x:Double>0.7</x:Double>
                    </x:Arguments>
                </MapSpan>
            </x:Arguments>
        </maps:Map>

        <CarouselView ItemsSource="{Binding Exercises}" Grid.Row="1" PeekAreaInsets="100">
            <CarouselView.ItemTemplate>
                <DataTemplate x:DataType="model:Exercise">
                    <Frame HeightRequest="90" Margin="5">
                        <Frame.GestureRecognizers>
                            <TapGestureRecognizer Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:ExerciseViewModel}}, Path=ExerciseDetailsCommand}
                                            " CommandParameter="{Binding .}"></TapGestureRecognizer>
                        </Frame.GestureRecognizers>
                        <HorizontalStackLayout Padding="10" Spacing="5" >
                            <Label Text="{Binding Id}"></Label>
                            <Label Text="{Binding Date}"></Label>
                        </HorizontalStackLayout>
                    </Frame>
                </DataTemplate>
            </CarouselView.ItemTemplate>
        </CarouselView>
    </Grid>
</ContentPage>

As you can see I have my map name declared as routeMap and the first location just to start in somewhere. I also has my model and viewmodel declared for DataBinding of the exercise list in the CarouselView. The tap feature works fine and take me to a new view called ExerciseDetailsPage.

This is the code behind ExercisePage.xaml.cs

using DoradSmartphone.Models;
using DoradSmartphone.ViewModels;
using Microsoft.Maui.Controls.Maps;
using Microsoft.Maui.Maps;

namespace DoradSmartphone.Views;

public partial class ExercisePage : ContentPage
{
    public ExercisePage(ExerciseViewModel exerciseViewModel)
    {
        InitializeComponent();
        BindingContext = exerciseViewModel;
    }

    
    private void OnTapGestureRouteUpdate(object sender, EventArgs e)
    {
        var route = new Polyline
        {
            StrokeColor = Colors.Red,
            StrokeWidth = 12,
            Geopath =
            {
                new Location(38.70061856336034 , -8.957381918676203 ),
                new Location(38.70671683905933 , -8.945225024701308 ),
                new Location(38.701985630081595, -8.944503277546072 ),
                new Location(38.701872978433386, -8.940750192338834 ),
                new Location(38.71054663609023 , -8.939162348597312 ),
                new Location(38.717755109243214, -8.942193686649311 ),
                new Location(38.7435419727561  , -8.928480490699792 ),
                new Location(38.78327379379296 , -8.880556478454272 ),
                new Location(38.925473761602376, -8.881999972299806 ),
                new Location(38.93692729913667 , -8.869585920414709 ),
                new Location(38.93493556584553 , -8.86536198145887  )
            }
        };
        routeMap.MoveToRegion(
            MapSpan.FromCenterAndRadius(
                new Location(38.93479161472441, -8.865352563545757), Distance.FromMiles(1)));
        // Add the polyline to the map
        routeMap.MapElements.Add(route);
    }
}

If I change the actual tap functionality to this tap event, I can drawn any line and other stuffs with in the Map because I can read the map name defined in the xaml code. But in this codebehind class I can't reach my ViewModel, Services or Model class.

This is my ExerciseViewModel.cs class:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DoradSmartphone;
using DoradSmartphone.Models;
using DoradSmartphone.Services;
using DoradSmartphone.Views;
using Microsoft.Maui.Controls.Maps;
using Microsoft.Maui.Maps;
using System.Collections.ObjectModel;

namespace DoradSmartphone.ViewModels
{
    public partial class ExerciseViewModel : BaseViewModel
    {
        private readonly ExerciseService exerciseService;

        public ObservableCollection<Exercise> Exercises { get; private set; } = new();
        public ExerciseViewModel(ExerciseService exerciseService)
        {
            Title = "Training Routes";
            this.exerciseService = exerciseService;
            _ = GetExerciseList();                                   
        }

        [ObservableProperty]
        bool isRefreshing;
        
        async Task GetExerciseList()
        {
            if (IsLoading) return;
            try
            {
                IsLoading = true;
                if (Exercises.Any()) Exercises.Clear();

                var exercices = exerciseService.GetExercises();
                foreach (var exercise in exercices) Exercises.Add(exercise);
            } catch(Exception ex) { 
                Console.WriteLine(ex.ToString());
                await Shell.Current.DisplayAlert("Error", "Failed to retrieve the exercice list", "Ok");
            }
            finally { 
                IsLoading = false; 
                isRefreshing= false;
             }
        }
        [RelayCommand]
        async Task ExerciseDetails(Exercise exercise)
        {
            if(exercise == null) return;

            var routes = GetLocations(exercise.Id);

            DrawRoutes(routes);
        }

        public List<Location> GetLocations(int exerciseId)
        {
            if (exerciseId == 1)
            {
                return new List<Location>
                        {
                            new Location(35.6823324582143, 139.7620853729577),
                            new Location(35.679263477092704, 139.75773939496295),
                            new Location(35.68748054650018, 139.761486207315),
                            new Location(35.690745005825136, 139.7560362984393),
                            new Location(35.68966608916097, 139.75147199952355),
                            new Location(35.68427128680411, 139.7442168083328)
                        };
            }
            else if (exerciseId == 2)
            {
                return new List<Location>
                        {
                            new Location(35.6823324582143, 139.7620853729577),
                            new Location(35.679263477092704, 139.75773939496295),
                            new Location(35.68748054650018, 139.761486207315),
                            new Location(35.690745005825136, 139.7560362984393),
                            new Location(35.68966608916097, 139.75147199952355),
                            new Location(35.68427128680411, 139.7442168083328)
                        };
            }
            else
            {
                return new List<Location>
                        {
                            new Location(35.6823324582143, 139.7620853729577),
                            new Location(35.679263477092704, 139.75773939496295),
                            new Location(35.68748054650018, 139.761486207315),
                            new Location(35.690745005825136, 139.7560362984393),
                            new Location(35.68966608916097, 139.75147199952355),
                            new Location(35.68427128680411, 139.7442168083328)
                        };
            }
        }

        private void DrawRoutes(List<Location> routes)
        {
            var polylines = new Polyline
            {
                StrokeColor = Colors.Red,
                StrokeWidth = 12,
            };

            foreach(var route in routes)
            {
                polylines.Geopath.Add(route);
            }                        
            
            routeMap.MoveToRegion(
                MapSpan.FromCenterAndRadius(
                    routes.FirstOrDefault(), Distance.FromMiles(1)));
            // Add the polyline to the map
            routeMap.MapElements.Add(polylines);
        }
    }
}

This class inherits the BaseViewModel that inherits ObservableObject and has some common properties for all others classes. In the ExerciseViewModel I have my RelayCommand related to the tap feature that grabs the exercise object and add the route, but I cant access the routeMap object. I've tried also to declare a Map class in my viewmodel class, but I get the error all the time that I can't create a instance of a static class.

This is my MauiProgram.cs just in case there's something wrong:

using DoradSmartphone.Data;
using DoradSmartphone.Services;
using DoradSmartphone.ViewModels;
using DoradSmartphone.Views;

namespace DoradSmartphone;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()       
            .UseMauiMaps()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

        builder.Services.AddSingleton<DatabaseConn>();
        builder.Services.AddScoped<IRepository, DatabaseConn>();

        builder.Services.AddSingleton<MainPage>();
        builder.Services.AddSingleton<UserPage>();
        builder.Services.AddSingleton<LoginPage>();        
        builder.Services.AddSingleton<LoadingPage>();
        builder.Services.AddSingleton<ExercisePage>();
        builder.Services.AddSingleton<DashboardPage>();
        builder.Services.AddSingleton<ExerciseDetailsPage>();

        builder.Services.AddSingleton<UserService>();
        builder.Services.AddSingleton<LoginService>();        
        builder.Services.AddSingleton<ExerciseService>();
        builder.Services.AddSingleton<DashboardService>();

        builder.Services.AddSingleton<UserViewModel>();
        builder.Services.AddSingleton<LoginViewModel>();        
        builder.Services.AddSingleton<LoadingViewModel>();
        builder.Services.AddSingleton<ExerciseViewModel>();
        builder.Services.AddSingleton<DashboardViewModel>();
        builder.Services.AddTransient<ExerciseDetailsViewModel>();

        return builder.Build();
    }
}

Thank you in advance!


Solution

  • unfortunately, MapElements is not a bindable property. However, you can work around that in a couple of ways

    for example, create a public method in your VM that returns the route data

    public Polyline GetRouteData()
    {
        var polylines = new Polyline
            {
                StrokeColor = Colors.Red,
                StrokeWidth = 12,
            };
    
            foreach(var route in routes)
            {
                polylines.Geopath.Add(route);
            } 
    
      return polylines; 
    }
    

    then in your code behind, first create a class reference to the VM

    ExerciseViewModel ViewModel;
    
    public ExercisePage(ExerciseViewModel exerciseViewModel)
    {
        InitializeComponent();
        BindingContext = ViewModel = exerciseViewModel;
    }
    

    then your code behind can get the data from the VM that it needs to update the map

    routeMap.MapElements.Add(ViewModel.GetRouteData());