Thomas Weller
Thomas Weller

Reputation: 11717

Bind to Xamarin.Forms.Maps.Map from ViewModel

I'm working on a Xamarin.Forms app using a page that displays a map. The XAML is:

<maps:Map x:Name="Map">
    ...
</maps:Map>

I know that the map can be accessed from the page's code-behind like this:

var position = new Position(37.79762, -122.40181);
Map.MoveToRegion(new MapSpan(position, 0.01, 0.01));
Map.Pins.Add(new Pin
{
    Label = "Xamarin",
    Position = position
});

But because this code would break the app's MVVM architecture, I'd rather like to access the Map object from my ViewModel, not directly from the View/page - either using it directly like in the above code or by databinding to its properties.

Does anybody know a way how this can be done?

Upvotes: 17

Views: 15062

Answers (6)

Pejman
Pejman

Reputation: 3964

What about creating a new Control say BindableMap which inherits from Map and performs the binding updates which the original Map lacks internally. The implementation is pretty straightforward and I have included 2 basic needs; the Pins property and the current MapSpan. Obviously, you can add your own special needs to this control. All you have to do afterward is to add a property of type ObservableCollection<Pin> to your ViewModel and bind it to the PinsSource property of your BindableMap in XAML.

Here is the BindableMap:

public class BindableMap : Map
{

    public BindableMap()
    {
        PinsSource = new ObservableCollection<Pin>();
    }
    
    public ObservableCollection<Pin> PinsSource
    {
        get { return (ObservableCollection<Pin>)GetValue(PinsSourceProperty); }
        set { SetValue(PinsSourceProperty, value); }
    }

    public static readonly BindableProperty PinsSourceProperty = BindableProperty.Create(
                                                     propertyName: "PinsSource",
                                                     returnType: typeof(ObservableCollection<Pin>),
                                                     declaringType: typeof(BindableMap),
                                                     defaultValue: null,
                                                     defaultBindingMode: BindingMode.TwoWay,
                                                     validateValue: null,
                                                     propertyChanged: PinsSourcePropertyChanged);


    public MapSpan MapSpan
    {
        get { return (MapSpan)GetValue(MapSpanProperty); }
        set { SetValue(MapSpanProperty, value); }
    }

    public static readonly BindableProperty MapSpanProperty = BindableProperty.Create(
                                                     propertyName: "MapSpan",
                                                     returnType: typeof(MapSpan),
                                                     declaringType: typeof(BindableMap),
                                                     defaultValue: null,
                                                     defaultBindingMode: BindingMode.TwoWay,
                                                     validateValue: null,
                                                     propertyChanged: MapSpanPropertyChanged);

    private static void MapSpanPropertyChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var thisInstance = bindable as BindableMap;
        var newMapSpan = newValue as MapSpan;

        thisInstance?.MoveToRegion(newMapSpan);
    }
    private static void PinsSourcePropertyChanged(BindableObject bindable, object oldvalue, object newValue)
    {
        var thisInstance = bindable as BindableMap;
        var newPinsSource = newValue as ObservableCollection<Pin>;

        if (thisInstance == null ||
            newPinsSource == null)
            return;

        UpdatePinsSource(thisInstance, newPinsSource);
        newPinsSource.CollectionChanged += thisInstance.PinsSourceOnCollectionChanged;

    }
    private void PinsSourceOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        UpdatePinsSource(this, sender as IEnumerable<Pin>);
    }

    private static void UpdatePinsSource(Map bindableMap, IEnumerable<Pin> newSource)
    {
        bindableMap.Pins.Clear();
        foreach (var pin in newSource)
            bindableMap.Pins.Add(pin);
    }
}

Notes:

  • I have omitted the using statements and namespace declaration for the sake of simplicity.
  • In order for our original Pins property to be updated as we add members to our bindable PinsSource property, I declared the PinsSource as ObservableCollection<Pin> and subscribed to its CollectionChanged event. Obviously, you can define it as an IList if you intend to only change the whole value of your bound property.

My final word regarding the 2 first answers to this question:

Although having a View control as a ViewModel property exempts us from writing business logic in code behind, but it still feels kind of hacky. In my opinion, the whole point of (well, at least a key point in) the VM part of the MVVM is that it is totally separate and decoupled from the V. Whereas the solution provided in the above-mentioned answers is actually this:

Insert a View Control into the heart of your ViewModel.

I think this way, not only you break the MVVM pattern but also you break its heart!

Upvotes: 13

maf-soft
maf-soft

Reputation: 2552

Yes, Map.Pins is not bindable, but there is ItemsSource, which is easy to use instead.

   <maps:Map ItemsSource="{Binding Locations}">
        <maps:Map.ItemTemplate>
            <DataTemplate>
                <maps:Pin Position="{Binding Position}"
                          Label="{Binding Name}"
                          Address="{Binding Subtitle}" />

So, just for the pins, MVVM can be done without any custom control. But Map.MoveToRegion() (and Map.VisibleRegion to read) is still open. There should be a way to bind them. Why not both in a single read/write property? (Answer: because of an endless loop.)

Note: if you need Map.MoveToRegion only once on start, the region can be set in the constructor.

Upvotes: 2

Aquablue
Aquablue

Reputation: 304

If you don't want to break the MVVM pattern and still be able to access your Map object from the ViewModel then you can expose the Map instance with a property from your ViewModel and bind to it from your View.

Your code should be structured like described here below.

The ViewModel:

using Xamarin.Forms.Maps;

namespace YourApp.ViewModels
{
    public class MapViewModel
    {
        public MapViewModel()
        {
            Map = new Map();
        }

        public Map Map { get; private set; }
    }
}

The View (in this example I'm using a ContentPage but you can use whatever you like):

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="YourApp.Views.MapView">
    <ContentPage.Content>
                <!--The map-->
                <ContentView Content="{Binding Map}"/>
    </ContentPage.Content>
</ContentPage>

I didn't show how, but the above code snipped can only work when the ViewModel is the BindingContext of your view.

Upvotes: 23

Damien Doumer
Damien Doumer

Reputation: 2265

I have two options which worked for me and which could help you.

  1. You could either add a static Xamarin.Forms.Maps Map property to your ViewModel and set this static property after setting the binding context, during the instantiation of your View, as show below:

    public MapsPage()
        {
            InitializeComponent();
            BindingContext = new MapViewModel();
            MapViewModel.Map = MyMap;
        }
    

This will permit you to access your Map in your ViewModel.

  1. You could pass your Map from your view to the ViewModel during binding, for example:

    <maps:Map
        x:Name="MyMap"
        IsShowingUser="true"
        MapType="Hybrid" />
    <StackLayout Orientation="Horizontal" HorizontalOptions="Center">
        <Button x:Name="HybridButton" Command="{Binding MapToHybridViewChangeCommand}" 
                CommandParameter="{x:Reference MyMap}"
                Text="Hybrid" HorizontalOptions="Center" VerticalOptions="Center" Margin="5"/>`
    

And get the Map behind from the ViewModel's Command.

Upvotes: 4

Rob
Rob

Reputation: 143

It is not ideal, but you could listen for the property changed event in the code behind and then apply the change from there. Its a bit manual, but it is doable.

((ViewModels.YourViewModel)BindingContext).PropertyChanged += yourPropertyChanged;

And then define the "yourPropertyChanged" method

private void yourPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if(e.PropertyName == "YourPropertyName")
    {
        var position = new Position(37.79762, -122.40181);
        Map.MoveToRegion(new MapSpan(position, 0.01, 0.01));
        Map.Pins.Add(new Pin
        {
            Label = "Xamarin",
            Position = position
        });
    }
}

Upvotes: 1

Prashant Cholachagudda
Prashant Cholachagudda

Reputation: 13092

I don't think Pins is a bindable property on Map, you may want to file feature request at Xamarin's Uservoice or the fourm here: http://forums.xamarin.com/discussion/31273/

Upvotes: 1

Related Questions