Use .NET MAUI Map control with MVVM
Published: 2023-01-22 04:08:24

To use the current version of the .NET MAUI map control with MVVM, you will need to wrap it with some extra properties, if you want basic functionality like being able to set a selected pin or move the map via the ViewModel.

```csharp
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();
    }
}
```

Then you will want to implement it in XAML, backed with a ViewModel that contains an ObservableCollection of whatever model object you are using (in this case "Records").

To get the event arguments back from the EventToCommandBehavior, you need to specify the TypeArguments, as shown. 

**Note:** this is also implementing the [CustomPin](https://vladislavantonyuk.github.io/articles/Customize-map-pins-in-.NET-MAUI/) control from Vladislav Antonyuk to allow for custom icons. If you use this, then you need to name the map and bind the map to the pin via the name, as shown.

```xml
    <ContentPage.Resources>
        <converters:StringToLocationConverter x:Key="stringToLocation" />
        <converters:RecordToIconConverter x:Key="recordToIcon" />
    </ContentPage.Resources>

    <cc:MvvmMap x:Name="mvvmMap1" ItemsSource="{Binding Records}" MapType="{Binding MapType, Mode=OneTime}" MapSpan="{Binding MapSpan}" IsShowingUser="True">
        <cc:MvvmMap.ItemTemplate>
            <DataTemplate x:DataType="models:Record">
                <cc:CustomPin Location="{Binding Location, Converter={StaticResource stringToLocation}}"
                              ImageSource="{Binding Converter={StaticResource recordToIcon}}"
                              Label="{Binding Name}"
                              Map="{Reference mvvmMap1}" />
            </DataTemplate>
        </cc:MvvmMap.ItemTemplate>
        <cc:MvvmMap.Behaviors>
            <toolkit:EventToCommandBehavior EventName="Loaded" Command="{Binding LoadedCommand}" />
            <toolkit:EventToCommandBehavior x:TypeArguments="MapClickedEventArgs" EventName="MapClicked" Command="{Binding MapClickedCommand}" />
        </cc:MvvmMap.Behaviors>
    </cc:MvvmMap>
```

The converter allows storing locations as strings in your database or wherever, and could look like this:

```csharp
namespace YourProject.Converters;

using System.Globalization;

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

        if (value is not string latLong)
        {
            throw new ArgumentException("value is not a string");
        }

        return GetLocation(latLong);
    }

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

    public static Location GetLocation(string latLong)
    {
        var (latitude, longitude) = GetLatitudeAndLongitude(latLong);
        return new Location(latitude, longitude);
    }

    public static (double Latitude, double Longitude) GetLatitudeAndLongitude(string latLong)
    {
        if (string.IsNullOrEmpty(latLong))
        {
            throw new ArgumentNullException(nameof(latLong));
        }

        var segments = latLong.Split(',');
        var latitude = double.Parse(segments[0], CultureInfo.InvariantCulture);
        var longitude = double.Parse(segments[1].TrimStart(), CultureInfo.InvariantCulture);
        return (latitude, longitude);
    }
}
```

See a working example here: [https://github.com/symbiogenesis/MauiMvvmMap](https://github.com/symbiogenesis/MauiMvvmMap)