Intégrer le composant Map .Net MAUI en MVVM


Pourquoi utiliser un composant de carte dans son Application Mobile

Aujourd’hui les applications mobiles intègrent très souvent des cartographie avec de nombreux usages pour l’utilisateur :

  • Uber pour suivre l’arrivée du chauffeur / livreur
  • Strava pour voir des parcours
  • SeLoger pour voir des biens immobiliers
  • etc

On imagine mal ces application sans ce type de fonctionnalité.

Exemple d'application mobile avec carte

Ayant eu un long passé de développeur dans le milieu du transport, les composants de cartographie étaient essentiels pour les clients finaux comme pour les exploitants pour les aider à la prise de décision.

Pour la dernière App que j’ai développé pour FeelU, la carte nous sert à contrôler à distance les déplacements d’un utilisateur dans Google Street View.

Ressources

Code source du projet disponible sur Gitlab.

Tuto Vidéo Utiliser le composant carte de .Net MAUI en MVVM :

Tuto Vidéo Utiliser le composant carte de .Net MAUI en MVVM

La documentation officielle Microsoft .Net MAUI Map

Création du Projet Dotnet MAUI

Le projet est réalisé sous Visual Studio 2022 (17.10.5) mais également être réalisé sous VS Code.

Le framework utilisé est la version .Net 8.0.

On commence par créer un nouveau projet avec le template de projet MAUI :

Template de création de projet MAUI

Configuration Android avec Google Maps

Pour afficher la carte, nous devons passer par un fournisseur cartographique, nous allons utiliser Google Map. Pour accéder aux tuiles cartographique de Google nous devons posséder une clé d’API.

Vous pouvez en demander une gratuitement avec suffisamment de crédit pour tester votre application sur le site console.cloud.google.com

Créer une clé API Google Maps

Votre clé est directement accessible :

Clé Google Maps

Pensez à restreindre l’accès de votre clé API à votre utilisation, ici nous restreignions l’accès à Maps SDK for Android.

Restriction d'API Google Maps

Vous devez maintenant intégrer la clé dans votre Application, il faut pour cela modifier le fichier AndroidManifest.xml Ajouter les meta-data ainsi que les autorisations d’application, remplacer “PASTE-YOUR-API-KEY-HERE” par votre clé d’API.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
	<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true">
		<meta-data android:name="com.google.android.geo.API_KEY" android:value="PASTE-YOUR-API-KEY-HERE" />
		<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
	</application>
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
	<uses-permission android:name="android.permission.INTERNET" />
	<!-- Required to access the user's location -->
	<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
	<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

Utilisation Basique du composant Maps

Pour pouvoir utiliser le composant Map dans vos Pages ou View vous devez d’abord l’installer via NuGet :

dotnet add package Microsoft.Maui.Controls.Maps --version 8.0.70

ou

Installation du package Nuget Maui Controls Maps

Il suffit ensuite d’ajouter le composant dans votre page de la façon suivante :

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="MauiMapsAppBlog.MainPage"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:maps="http://schemas.microsoft.com/dotnet/2021/maui/maps">

    <StackLayout>
        <maps:Map />
    </StackLayout>

</ContentPage>

Avant de lancer l’application en debug pour la première fois, ajouter le référencement du Plugin Maps dans MauiProgram.cs via UseMauiMaps() :

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

        return builder.Build();
    }
}

Vous avez maintenant à cette étape une application qui affiche une carte Google Maps :

Premier lancement d'une Application MAUI avec composant Maps

Conversion du Composant en MVVM

Maintenant que vous avez un composant carte qui fonctionne, il faut pouvoir afficher des informations dessus et positionner la carte au bon endroit.

De base le composant ne supporte pas MVVM ce qui est bien dommage pour un composant MAUI, nous allons rendre les fonctionnalité de positionnement de carte et de sélection de Pin accessible à un ViewModel.

Pour rendre le composant compatible, nous allons l’étendre en créant un nouveau contrôle qui va hériter du composant Maps de base.

namespace MvvmMap;

using Microsoft.Maui.Maps;
using Map = Microsoft.Maui.Controls.Maps.Map;

public class MvvmMap : Map
{
    public static readonly BindableProperty MapSpanProperty = BindableProperty.Create(nameof(MapSpan), typeof(MapSpan), typeof(MvvmMap), null, BindingMode.TwoWay, propertyChanged: (b, _, n) =>
    {
        if (b is MvvmMap map && n is MapSpan mapSpan)
        {
            MoveMap(map, mapSpan);
        }
    });

    public static readonly BindableProperty SelectedItemProperty = BindableProperty.Create(nameof(SelectedItem), typeof(object), typeof(MvvmMap), null, BindingMode.TwoWay);

    public MapSpan MapSpan
    {
        get => (MapSpan)this.GetValue(MapSpanProperty);
        set => this.SetValue(MapSpanProperty, value);
    }

    public object? SelectedItem
    {
        get => (object?)this.GetValue(SelectedItemProperty);
        set => this.SetValue(SelectedItemProperty, value);
    }

    private static void MoveMap(MvvmMap map, MapSpan mapSpan)
    {
        var timer = Application.Current!.Dispatcher.CreateTimer();
        timer.Interval = TimeSpan.FromMilliseconds(500);
        timer.Tick += (s, e) =>
        {
            if (s is IDispatcherTimer timer)
            {
                timer.Stop();

                MainThread.BeginInvokeOnMainThread(() => map.MoveToRegion(mapSpan));
            }
        };

        timer.Start();
    }
}

Merci à Edward Miller pour avoir présenté la solution sur dev.to

Nous pouvons maintenant créer un ViewModel qui va positionner la carte sur un point particulier :

internal class MainPageViewModel : BasePageViewModel
{
    private static readonly Location CenterOfParis = new() { Latitude = 48.8637, Longitude = 2.3134, };
    private static readonly Distance StartRadius = Distance.FromKilometers(20);

    private readonly IDataParis2024Service dataParis2024Service;

    private MapSpan mapSpan = MapSpan.FromCenterAndRadius(CenterOfParis, StartRadius);

    public MapSpan MapSpan
    {
        get => mapSpan;
        set { mapSpan = value; RaisePropertyChanged(nameof(MapSpan)); }
    }
}

Il ne reste plus qu’à remplacer le composant de base dans le xaml de notre page et instancier le viewmodel pour le binder sur la page :

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="MauiMapsApp.MainPage"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:maps="http://schemas.microsoft.com/dotnet/2021/maui/maps"
    xmlns:uiCtrl="clr-namespace:MauiMapsApp.UIControls">

    <StackLayout>
        <uiCtrl:MvvmMap
            x:Name="map"
            MapSpan="{Binding MapSpan}">
        </uiCtrl:MvvmMap>
    </StackLayout>

</ContentPage>
  public partial class MainPage : ContentPage
  {
      public MainPage()
      {
          InitializeComponent();
          this.BindingContext = new MainPageViewModel();
      }
  }

Récupération des sites sur DataParis2024

Il est temps de récupérer des données à afficher, nous allons utiliser les Api officielles des Jeux Olympiques de Paris 2024.

Cette méthode en particulier : /api/explore/v2.1/catalog/datasets/paris-2024-sites-de-competition/records?limit=20

API Paris 2024

Créons un model de données correspondant aux Sites de Compétions renvoyé par l’API.

Les attributs au dessus des propriétés JsonPropertyName permettent d’effectuer le mapping lors de la désérialisation.

public class OlympicSession
{
    [JsonPropertyName("code_site")]
    public string CodeSite { get; set; } = string.Empty;
    [JsonPropertyName("nom_site")]
    public string NomSite { get; set; } = string.Empty;
    [JsonPropertyName("category_id")]
    public string CategoryId { get; set; } = string.Empty;
    [JsonPropertyName("sports")]
    public string Sports { get; set; } = string.Empty;
    [JsonPropertyName("start_date")]
    public DateTime StartDate { get; set; }
    [JsonPropertyName("end_date")]
    public DateTime EndDate { get; set; }
    [JsonPropertyName("point_geo")]
    public PointGeo PointGeo { get; set; }
}
public class PointGeo
{
    [JsonPropertyName("lon")]
    public double Lon { get; set; }
    [JsonPropertyName("lat")]
    public double Lat { get; set; }
}

Nous créons ensuite un service de récupération de données qui va appeler le point de terminaison de l’Api en GET.

internal class DataParis2024Service : IDataParis2024Service
{
    const string apiUrl = "https://data.paris2024.org/api/explore/v2.1/catalog/datasets/paris-2024-sites-de-competition/records?limit=100";
    class ApiResponse
    {
        [JsonPropertyName("total_count")]
        public int TotalCount { get; set; }

        [JsonPropertyName("results")]
        public IEnumerable<OlympicSession> Results { get; set; }
    }

    public async Task<IEnumerable<OlympicSession>> GetOlympicSessions()
    {
        using (HttpClient client = new HttpClient())
        {
            HttpResponseMessage response = await client.GetAsync(apiUrl);

            if (response.IsSuccessStatusCode)
            {
                string jsonResponse = await response.Content.ReadAsStringAsync();

                var apiResponse = JsonSerializer.Deserialize<ApiResponse>(jsonResponse);
                return apiResponse?.Results ?? Enumerable.Empty<OlympicSession>();
            }
            else
            {
                throw new Exception("Erreur lors de l'appel à l'API : " + response.ReasonPhrase);
            }
        }
    }
}

Il faut ensuite enregistré ce service dans l’injecteur de dépendance de l’application (MauiProgramm.cs) pour une utilisation aisé dans l’application. (même si dans un exemple aussi simple cela n’est pas obligatoire) :

        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
...
            builder.Services.AddTransient<IDataParis2024Service, DataParis2024Service>();
            builder.Services.AddSingleton<MainPageViewModel>();
            builder.Services.AddSingleton<MainPage>();

Affichage de Point sur le composant Carte avec MAUI

Nous arrivons à la dernière étape.

Nous ajoutons une méthode dans notre ViewModel pour initialiser la récupération de données :

internal class MainPageViewModel : BasePageViewModel
{
...
    private readonly IDataParis2024Service dataParis2024Service;

    private IEnumerable<OlympicSession> places = Enumerable.Empty<OlympicSession>();

    public IEnumerable<OlympicSession> Places
    {
        get => this.places;
        set { this.places = value; RaisePropertyChanged(nameof(Places)); }
    }
...
    public MainPageViewModel(IDataParis2024Service dataParis2024Service)
    {
        this.dataParis2024Service = dataParis2024Service ?? throw new ArgumentNullException(nameof(dataParis2024Service));
    }

    public void InitData()
    {
        Task.Run(async () =>
        {
            try
            {
                this.Places = await this.dataParis2024Service.GetOlympicSessions();
            }
            catch (Exception ex)
            {
                // Add logger 
                this.Places = Enumerable.Empty<OlympicSession>();
            }
        });
    }
}

Nous modifions ensuite le code de la page qui utilise le composant pour initialiser les données :

 public MainPage(IDataParis2024Service dataParis2024Service)
 {
     InitializeComponent();
     var viewModel = new MainPageViewModel(dataParis2024Service);
     this.BindingContext = viewModel;
     viewModel.InitData();
 }

Et enfin nous allons Binder la propriété Places ajouter dans le ViewModel ainsi que définir un template d’affichage des Pins sur la carte :

 <uiCtrl:MvvmMap
     x:Name="map"
     ItemsSource="{Binding Places}"
     MapSpan="{Binding MapSpan}">
     <uiCtrl:MvvmMap.ItemTemplate>
         <DataTemplate x:DataType="models:OlympicSession">
             <maps:Pin
                 Address="{Binding Sports}"
                 Label="{Binding NomSite}"
                 Location="{Binding PointGeo, Converter={StaticResource pgToLocation}}" />
         </DataTemplate>
     </uiCtrl:MvvmMap.ItemTemplate>
 </uiCtrl:MvvmMap>

Nous avons besoin d’un Converter pour transformer les PointGeo retourné par l’Api en Location compréhensible par le composant Pin Maps de Microsoft :

internal class PointGeoToLocationConverter : IValueConverter
{
    public object? Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture)
    {
        if (value is null)
        {
            return null;
        }

        if (value is not PointGeo latLong)
        {
            throw new ArgumentException($"value is not a {nameof(PointGeo)}");
        }

        return new Location(latLong.Lat,latLong.Lon);
    }

    public object ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) => throw new NotImplementedException();
}

Il faut ensuite enregistré ce converter dans le XAML de la page :

<ContentPage
    x:Class="MauiMapsApp.MainPage"
...
    xmlns:conv="clr-namespace:MauiMapsApp.Converters"
  ...>

    <ContentPage.Resources>
        <conv:PointGeoToLocationConverter x:Key="pgToLocation" />
    </ContentPage.Resources>

Votre application est maintenant prête à afficher les sites Olympiques :

API Paris 2024

Et après ?

Les cas d’usages en B2B ou B2C sont infini avec les composants de cartographie, on peut par exemple afficher des corridors sur la carte pour délimiter des zones, utiliser se propres fonds de carte ou encore ajouter des représentations 3D.

Si cet article vous a plu, un commentaire et un partage serait fort apprécié.

Vous souhaitez un coup de main sur vos projets .Net Maui, nous pouvons en discuter sur linkedin.

Merci de m’avoir lu