DEV Community

Edward Miller
Edward Miller

Posted on • Edited on

Use .NET MAUI Map control with MVVM

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.

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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 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.

    <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>
Enter fullscreen mode Exit fullscreen mode

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

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

See a working example here: https://github.com/symbiogenesis/MauiMvvmMap

Top comments (0)