Unlocking native control features the Xamarin.Forms way

While Xamarin did a great job with Forms in providing us with a set of controls that can be used creating our awesome UI, they didn’t (and couldn’t) enable all of the features that come with a Button or Label or.. whatever! So they did the basics, which is absolutely fine!

But when you are working with Xamarin.Forms a bit longer, you want to go that extra mile and enable some features that you know are available, but not unlocked in Forms.

In this post I will tell you one way of doing this, by the example of the much critiqued Xamarin.Forms.Maps component.

Situation Report

The Maps component is a pretty cool thing! If you haven’t seen it by now, go check it out!

Xamarin.Forms.Maps sample (image by Xamarin)
Xamarin.Forms.Maps sample (image by Xamarin)

It let’s you incorporate a map in a Forms abstract way and depending on the platform you’re running on, the map will show either Apple Maps, Google Maps or Bing Maps, without you having to go through too much trouble. Using it is simple: add the NuGet package, declare the namespace on Page level and add the Map to your page. It will look something like this:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:UnlockNativeFormsSample"
             xmlns:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps"
             x:Class="UnlockNativeFormsSample.MainPage">

  <StackLayout VerticalOptions="StartAndExpand" Padding="30">
    <maps:Map WidthRequest="320" HeightRequest="200"
        x:Name="MyMap" IsShowingUser="true" MapType="Hybrid" />
  </StackLayout>
</ContentPage>

The thing is, you can do a lot with maps, but all that isn’t implemented by far in the Xamarin.Forms.Maps component. No one knows why, and it doesn’t really matter as well.

Now before we go any further into this I would like to mention that there are already some good additions out there. I’ve used TK.CustomMap in a couple of projects and it seems to cover most of the missing features including data-binding. So before you go out and create your own set of components here, double-check if there aren’t any good ones out there already.

Implementing a missing feature

So our goal here is to unlock a feature that you know is available on the native (Maps) control, but isn’t implemented in a Xamarin.Forms control that you are using. And of course we want to do this in a way so that we go from our native functionality back to our shared code as soon as possible so we can share as much code as we can.

For this example I needed to detect when the user had dragged the map around and to the region changed. I knew that had to be available, yet I didn’t see any event on the default Xamarin Map. So I dug in some Android and iOS documentation and found that the Android

 

Shared

First I will create a inheritance of the Xamarin.Forms.Maps.Map. It’s there, what it does it does good. So why not use that as the base? Also I have already added a delegate in it for alter use.

using Xamarin.Forms.Maps;

namespace UnlockNativeFormsSample
{
    public class MyMap : Map
    {
        public delegate void OnRegionChangedDelegate();

        public OnRegionChangedDelegate OnRegionChanged { get; set; }
    }
}

Also, if this is there already, then they have to have created a Platform Renderer for it, so that is something we can use as well! So let’s see how we can create some custom renderers to unlock the features we want.

iOS

Let’s start with iOS. I’ve created a Custom Renderer which inherits the default renderer for the Map.
For clarity I named the references to the maps _xamarinMap, for the Xamarin.Forms abstraction and NativeMap, for the MapKit Map from Apple.

Now as you can see I can access both from this renderer. Which means I can just respond to the event of the NativeMap and at that instance pass that through to the Xamarin.Forms Map.

Also I have implemented both the delegate that I have created on my Xamarin.Forms map as well as a solution based on the MessagingCenter. Both will work fine, it’s up to you which you find more elegant.

[assembly: ExportRenderer(typeof(MyMap), typeof(MyMapRenderer))]
namespace UnlockNativeFormsSample.iOS
{
    public class MyMapRenderer : MapRenderer
    {
        private MyMap _xamarinMap;

        protected MKMapView NativeMap
        {
            get
            {
                return Control as MKMapView;
            }
        }

        protected override void OnElementChanged(ElementChangedEventArgs<View> e)
        {
            base.OnElementChanged(e);

            if (e.NewElement != null)
            {
                _xamarinMap = e.NewElement as MyMap;

                NativeMap.RegionChanged += NativeMap_RegionChanged;
            }
        }

        private void NativeMap_RegionChanged(object sender, MKMapViewChangeEventArgs e)
        {
            // This
            if (_xamarinMap.OnRegionChanged != null)
                _xamarinMap.OnRegionChanged();

            // or this
            MessagingCenter.Send<object>(this, "RegionChanged");
        }
    }
}

Android

Like you would expect for Android things go kind of the same, but also a bit different!

We also create a custom renderer in the platform project, but the GoogleMap – which is the native control there – acts a bit different.
Here we have to implement a Android interface, which gives us a sign when the GoogleMap is loaded. Only then can we make calls on it and hook up to the CameraChange event. The region from iOS is called a Region in Android world. The rest should look pretty familiar.

using Android.Gms.Maps;
using UnlockNativeFormsSample;
using UnlockNativeFormsSample.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Maps.Android;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(MyMap), typeof(MyMapRenderer))]

namespace UnlockNativeFormsSample.Droid
{
    public class MyMapRenderer : MapRenderer, IOnMapReadyCallback
    {
        private MyMap _xamarinMap;
        private GoogleMap _googleMap;

        public virtual void OnMapReady(GoogleMap googleMap)
        {
            _googleMap = googleMap;

            _googleMap.CameraChange += _googleMap_CameraChange;
        }

        private void _googleMap_CameraChange(object sender, GoogleMap.CameraChangeEventArgs e)
        {
            // This
            if (_xamarinMap.OnRegionChanged != null)
                _xamarinMap.OnRegionChanged();

            // or this
            MessagingCenter.Send<object>(this, "RegionChanged");
        }

        protected override void OnElementChanged(ElementChangedEventArgs<View> e)
        {
            base.OnElementChanged(e);

            if (e.NewElement != null)
                _xamarinMap = e.NewElement as MyMap;
        }
    }
}

 

Windows (UWP)

And the same is true for Windows, although the Windows way-of-working is of course very similar to what we have in our PCL (shared) code so you can almost connect the Map events to our own delegates one-on-one.

[assembly: ExportRenderer(typeof(MyMap), typeof(MyMapRenderer))]

namespace UnlockNativeFormsSample.UWP
{
    public class MyMapRenderer : MapRenderer
    {
        private MyMap _xamarinMap;

        protected override void OnElementChanged(ElementChangedEventArgs<Map> e)
        {
            base.OnElementChanged(e);

            if (e.NewElement != null)
            {
                _xamarinMap = e.NewElement as MyMap;

                Control.ActualCameraChanged += Control_ActualCameraChanged;
            }
        }

        private void Control_ActualCameraChanged(MapControl sender, MapActualCameraChangedEventArgs args)
        {
            // This
            if (_xamarinMap.OnRegionChanged != null)
                _xamarinMap.OnRegionChanged();

            // or this
            MessagingCenter.Send<object>(this, "RegionChanged");
        }
    }
}

Like Android, the Region is called a Camera here, but the idea remains the same.

And back to shared..

Now we have everything in place we can get back to our shared code and hook up our new event!

The page I am showing the map on looks like this in XAML:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:UnlockNativeFormsSample"
             xmlns:controls="clr-namespace:UnlockNativeFormsSample;assembly=UnlockNativeFormsSample"
             x:Class="UnlockNativeFormsSample.MainPage">

  <Grid Margin="0,20,0,0" HorizontalOptions="Fill" VerticalOptions="FillAndExpand">
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <Grid.RowDefinitions>
      <RowDefinition Height="25" />
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <Label Grid.Row="0" x:Name="MyAwesomeLabel" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" />

    <controls:MyMap Grid.Row="1" x:Name="MyAwesomeMap" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
         IsShowingUser="true" MapType="Street" />
  </Grid>
</ContentPage>

And in my code-behind I wire up my new MyMap delegate OnRegionChanged.

The other way to do it is in here as well, you just subscribe to the right message in the MessagingCenter. Now of course there are a lot of stuff that you might want to do here like provide some information from the native map as some parameters, thats all stuff you can do. Just implement some parameters on your delegate and it will get passed up. Same goes for sending messages.

using Xamarin.Forms;

namespace UnlockNativeFormsSample
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();

            // This
            MyAwesomeMap.OnRegionChanged += OnRegionChangedHandler;

            // Or this
            MessagingCenter.Subscribe<object>(this, "RegionChanged", (o) =>
            {
                // Handle region changed here
            });
        }

        private void OnRegionChangedHandler()
        {
            MyAwesomeLabel.Text = $"{MyAwesomeMap.VisibleRegion.Center.Latitude}, {MyAwesomeMap.VisibleRegion.Center.Longitude}";
        }
    }
}

Now when you run it you’ll see that everything works nicely!

Final thoughts

And it’s a wrap! Not so hard when you know what you are doing right?
So this is how to wire up an event from the native map to our Xamarin.Forms map and handle it in our shared code. With a little bit of imagination you can of course also make this work for properties and other stuff you would want to get out of your map.

Let me know what you came up with! Or if there is anything that you need some help with, let me know as well!

As usual, all the code for this can be found on GitHub!