BindableObject + Behaviors to enhance MVVM in .NET MAUI Maps
Tags: maui, dotnetmaui, dotnet
Published: 2022-12-13 12:34:00

This publication is part of the [C# Advent Calendar 2022](https://www.csadvent.christmas/), an initiative led by [Matthew D. Groves](https://twitter.com/mgroves) and [Calvin A. Allen](https://twitter.com/_calvinallen). Check it out for more interesting C# articles posted by community members.

With the release of .NET 7 and the corresponding [update](https://devblogs.microsoft.com/dotnet/dotnet-maui-dotnet-7/) in .NET MAUI, one of the newest additions is the [Map Control](https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/map?view=net-maui-7.0) that you can use in order to include a control that supports annotation and display of native map controls across Android and iOS mobile applications.


![.NET MAUI app with Map on iOS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jix9t4223vy9om5k8piv.png)


While this control includes several powerful features, not all of them can be used directly with Data Binding. The following actions can't be the target of data bindings in a .NET MAUI Map

- **Displaying a specific location on a Map and/or Moving the map** (for example, setting the initial position of the map to an specific location, otherwise the map is centered on Maui, Hawaii by default). For both operations, a **MapSpan** object is involved.

- **Pin interaction (click)**. You can't even add a TapGestureRecognizer, sadly.

- **Drawing polygons, polylines, and circles**. Perhaps you want to highlight specific areas on a map with these shapes. 

[MVVM](https://learn.microsoft.com/en-us/dotnet/architecture/maui/mvvm) is a great pattern that is highly used in .NET development. We want to decouple every layer so our code is reusable, maintainable, etc. [Behaviors](https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/behaviors?view=net-maui-7.0) is a class that help us to add functionality to UI controls in a separate class that is not a subclass of them, but attached to the control as if it was part of the control itself. The idea is to directly interact with the API of the control in such a way that it can be concisely attached to the control and packaged for reuse across more than one application.

And then we have [BindableObjects](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindableobject?view=net-maui-7.0), which allows us to propagate changes that are made to data in one object to another, by enabling validation, type coercion, and an event system. We can combine the potential advantages of BindableObjects and Behaviors into one class, a BindableBehavior, that can be reused in MVVM to extend capabilities of controls such as a .NET MAUI Map. **Enough theory, show me the code!**

**Step 1. Set up** Clone [this GitHub repo](https://github.com/icebeam7/MapDemoNetMaui) (`master` branch)

This is a .NET MAUI app that:

- Targets .NET 7
- Has already been configured to display a map in a `ContentPage`. Check [the official documentation](https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/map?view=net-maui-7.0) for specifics on how to do it, such as: adding the `Microsoft.Maui.Controls.Maps` Nuget package, adding `.UseMauiMaps()` method in MauiProgram.cs and adding the `Map` control with the `Microsoft.Maui.Controls.Maps` namespace on a `ContentPage`.
- It also includes a basic MVVM setup with a Model (`Place `class), ViewModel (`BaseViewModel` and `MapViewModel`, which gets the current location, which is added to the `Places` collection), and a View (`MapView`, which displays the map with the features mentioned in the VM, the `ItemsSource `that displays a pin uses Data Binding)

If you are testing the app on Android, there is an additional thing to do: Add a **Google Maps API key** in the `AndroidManifest.xml`.

This is how the app looks in Android when is executed for the first time. 


![Maps in .NET MAUI 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gjy3xgk7ameb2pvf5fek.png)



It asks for permission, then it shows the current location after pressing the button:


![Location permission](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wplbn73zb8bqf51srcr9.png)


BUT you must manually navigate through the map to find the pin:


![Map with a pin](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q36gzvcz5xqa2dw6ryiy.png)


**Step 2. BindableBehavior class** Create a folder (`Behaviors`) and a class `BindableBehavior` that extends from `Behavior<T>`. The class includes a generic `AssociatedObject` (a UI control) and overrides the two basic methods from the Behavior class: `OnAttachedTo `and `OnDetachingFrom`, which are typically used to add and remove the behavior from a control. Moreover (and this is the key point of everything in this implementation), the BindingContext of the associated control is also referenced, so we can notify (and get notified) about changes in properties from the class and control. This is the code:


```
namespace MapDemo.Behaviors
{
	public class BindableBehavior<T> : Behavior<T> where T : BindableObject
	{
		public T AssociatedObject { get; private set; }

		protected override void OnAttachedTo(T bindable)
		{
			base.OnAttachedTo(bindable);

			AssociatedObject = bindable;

			if (bindable.BindingContext != null)
				BindingContext = bindable.BindingContext;

			bindable.BindingContextChanged += Bindable_BindingContextChanged;
		}

		private void Bindable_BindingContextChanged(object sender, EventArgs e)
		{
			OnBindingContextChanged();
		}

		protected override void OnDetachingFrom(T bindable)
		{
			base.OnDetachingFrom(bindable);

			bindable.BindingContextChanged -= Bindable_BindingContextChanged;
		}

		protected override void OnBindingContextChanged()
		{
			base.OnBindingContextChanged();

			BindingContext = AssociatedObject.BindingContext;
		}
	}
}
```


**Step 3. MapBehavior class** Now it is time to consume the above class. Create a new class (`MapBehavior`). Here:

- Add namespaces: `Microsoft.Maui.Controls.Maps`, `Microsoft.Maui.Maps`, and `MapDemo.Models`. Moreover, use an alias for a `Map` object to avoid ambiguity (there's another [Map](https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/appmodel/maps) class already included in the global usings). We call it `MauiMap`.
- This class extends from the BindableBehavior class that we just created. The generic T member is a MauiMap.
- Create a MauiMap local object.

And here we also have the most important part which includes 3 elements: A BindableProperty, a public property, and a method:

- `IsReadyProperty` is a public static BindableProperty member that gets notified when there is a change in the value of `IsReady` public property. When it happens, the `OnIsReadyChanged` method is invoked.
- `IsReady` is a public boolean property that is bound to `IsReadyProperty` for notifications when its value changes.
- `OnIsReadyChanged` method handles the value change. We have access to the previous and new value, and the `ChangePosition` method is invoked.

The IsReady property will be the target for data binding in the View after the Behavior is attached, and its value will be affected/read from the ViewModel. More on that later :-).

Then we also have another `BindableProperty` element: `PlacesProperty`, which is bound to `Places`, an `IEnumerable` of `Place`. When there is a change in this collection value, the OnPlacesChanged method is invoked, which in turn executes `ChangePosition` (and DrawLocation, if it contains only one element).

You might wonder **_why_** `Places` is an `IEnumerable` rather than just one `Place` object. The answer is that in an upcoming post I'll use the `Places` collection to draw a route between the first point and another one (selected by the user). 

The `ChangePosition` method uses `MoveToRegion` from the map reference to display the map in an specific location, while `DrawLocation` highlights the location by drawing a `Circle` on the map (it is drawn at the moment it is added to the Elements collection of the map).

Both `OnAttachedTo` and `OnDetachingFrom` overriden methods set and remove the map reference, respectively. The implementations from the base class are also invoked (if you remember, we set the BindingContext there).

The code goes as follows:

```
using Microsoft.Maui.Controls.Maps;
using Microsoft.Maui.Maps;

using MauiMap = Microsoft.Maui.Controls.Maps.Map;
using MapDemo.Models;

namespace MapDemo.Behaviors
{
	public class MapBehavior : BindableBehavior<MauiMap>
	{
		private MauiMap map;

		public static readonly BindableProperty IsReadyProperty = 
			BindableProperty.CreateAttached(nameof(IsReady),
				typeof(bool),
				typeof(MapBehavior),
				default(bool),
				BindingMode.Default,
				null,
				OnIsReadyChanged);

		public bool IsReady
		{
			get => (bool)GetValue(IsReadyProperty);
			set => SetValue(IsReadyProperty, value);
		}

		private static void OnIsReadyChanged(BindableObject view, object oldValue, object newValue)
		{
			var mapBehavior = view as MapBehavior;

			if (mapBehavior != null)
			{
				if (newValue is bool)
					mapBehavior.ChangePosition();
			}
		}

		public static readonly BindableProperty PlacesProperty =
			BindableProperty.CreateAttached(nameof(Places),
				typeof(IEnumerable<Place>),
				typeof(MapBehavior),
				default(IEnumerable<Place>),
				BindingMode.Default,
				null,
				OnPlacesChanged);


		public IEnumerable<Place> Places
		{
			get => (IEnumerable<Place>)GetValue(PlacesProperty);
			set => SetValue(PlacesProperty, value);
		}

		private static void OnPlacesChanged(BindableObject view, object oldValue, object newValue)
		{
			var mapBehavior = view as MapBehavior;

			if (mapBehavior != null)
			{
				mapBehavior.ChangePosition();

				if (mapBehavior.Places.Count() == 1)
					mapBehavior.DrawLocation();
			}
		}

		private void DrawLocation()
		{
			map.MapElements.Clear();

			if (Places == null || !Places.Any())
				return;

			var place = Places.First();
			var distance = Distance.FromMeters(50);

			Circle circle = new Circle()
			{
				Center = place.Location,
				Radius = distance,
				StrokeColor = Color.FromArgb("#88FF0000"),
				StrokeWidth = 8,
				FillColor = Color.FromArgb("#88FFC0CB")
			};

			map.MapElements.Add(circle);
		}

		private void ChangePosition()
		{
			if (!IsReady || Places == null || !Places.Any())
				return;

			var place = Places.First();
			var distance = Distance.FromKilometers(1);

			map.MoveToRegion(MapSpan.FromCenterAndRadius(place.Location, distance));
		}

		protected override void OnAttachedTo(MauiMap bindable)
		{
			base.OnAttachedTo(bindable);
			map = bindable;
		}

		protected override void OnDetachingFrom(MauiMap bindable)
		{
			base.OnDetachingFrom(bindable);
			map = null;
		}
	}
}
```

** Step 4. ViewModel** Now let's proceed to modify MapViewModel:

- Two new observable properties are added: `isReady` (a boolean) and `bindablePlaces` (an observable collection of Place). They are a bridge between the View and the BindableBehavior. 
- In the `GetCurrentLocationAsync` method, set the location obtained from the sensor into a new object (`place`) that is added to the already existing `Places` observable collection object. Then, insert it into an IEnumerable object that is used to create a new instance of the `BindablePlaces` object. Moreover, `IsReady` is set to true.


This is the code:

```
...
using CommunityToolkit.Mvvm.ComponentModel;

namespace MapDemo.ViewModels
{
    public partial class MapViewModel : BaseViewModel
    {
        ...

        [ObservableProperty]
        bool isReady;

        [ObservableProperty]
        ObservableCollection<Place> bindablePlaces;

        ...

        [RelayCommand]
        private async Task GetCurrentLocationAsync()
        {
            try
            {
                ...

                var place = new Place()
                {
                    Location = location, 
                    Address = address,
                    Description = "Current Location"
                };

                Places.Add(place);

                var placeList = new List<Place>() { place };
				BindablePlaces = new ObservableCollection<Place>(placeList);
                IsReady = true;
            }
            catch (Exception ex)
            {
                // Unable to get location
            }
        }
    }
}
```

**Step 5. View** Finally, let's attach the behavior to the map. Go to `MapView` and add a reference to the `Behaviors` namespace; then, inside the Map definition access the Behaviors section where you'll include:

- a MapBehavior instance
- The IsReady property from the behavior is bound to the `IsReady` from the viewmodel.
- The `Places` property from the behavior is bound to the `BindablePlaces` from the viewmodel.

Here we are connecting everything we prepared earlier! Check the code:

```
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...
             xmlns:behaviors="clr-namespace:MapDemo.Behaviors"
             ...>
    <Grid ...>
        <maps:Map ...>
            <maps:Map.Behaviors>
                <behaviors:MapBehavior 
                    IsReady="{Binding IsReady}"
                    Places="{Binding BindablePlaces}"/>
            </maps:Map.Behaviors>
        </maps:Map>
    </Grid>
</ContentPage>
```

**Let's run the application!** Once we click on the button, the map will immediately be centered around the current location where we will see the pin and red circle (before, we manually had to scroll through the map to the pin location). 


![Map with pin and red circle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h8v8n00wg7t20ps1752u.png)


In case you want the app to directly access the current location, simply get rid of the button and invoke the `GetCurrentLocationCommand` command in the `OnAppearing` method from the `MapView` `ContentPage` class.

The final code is [here](https://github.com/icebeam7/MapDemoNetMaui/tree/bindable-behavior), which is the `bindable-behavior` branch from the MapDemoNetMaui project.