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!
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());