In this chapter, we will delve deeper into code sharing. We will build a Xamarin.Forms
application that integrates native GPS location services and Google Maps APIs. We will cover more content on IoC containers, the Xamarin.Forms.Maps
library, and techniques for c-sharp async
and background tasks.
Expected knowledge:
In this chapter, you will learn the following:
Xamarin.Forms
Xamarin.Forms.Maps
CLLocationManager
LibraryLocationManager
Newtonsoft.Json
and Microsoft HTTP client librariesModernHttpClient
and client message handlersIObservable
framework more reactive extensionsOnNavigatedTo
and OnShow
All mobile phone platforms have access to core location services. These services are background tasks that run in the background and update the latitude and longitude values at certain intervals indefinitely until the service is stopped. 99% of smart phones come with a built-in GPS tracker, allowing you to integrate these latitude and longitude values with your application.
Let's jump straight into project setup and create a new Xamarin.Forms
application. We are going to start by setting up an IoC container with Autofac, exactly the same as the previous project, import Autofac into all three projects (PCL, Android, and iOS). We can reuse a lot of the PCL code from the IoC container implementation in the previous project.
Copy in the IoC
, Pages
, and ViewModels
folders, and let's start building our MainPage
:
<?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="Locator.Pages.MainPage" BackgroundColor="White" Title="Welcome"> <ContentPage.Content> <Grid x:Name="Grid" RowSpacing="10" Padding="10, 10, 10, 10" VerticalOptions="Center"> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Image x:Name="Image" Source="map.png" HeightRequest="120" WidthRequest="120" Grid.Row="0" Grid.Column="0"/> <Label x:Name="DesciptionLabel" Text="{Binding DescriptionMessage}" HorizontalOptions="Center" Font="Arial, 20" Grid.Row="1" Grid.Column="0"> <Label.TextColor> <OnPlatform x:TypeArguments="Color" Android="Black" WinPhone="Black" iOS="Black"> </OnPlatform> </Label.TextColor> </Label> <Button x:Name="LocationButton" Text="{Binding LocationTitle}" Command="{Binding LocationCommand}" BackgroundColor="Silver" Grid.Row="2" Grid.Column="0"> <Button.TextColor> <OnPlatform x:TypeArguments="Color" Android="Navy" WinPhone="Blue" iOS="Black"> </OnPlatform> </Button.TextColor> </Button> <Button x:Name="ExitButton" Text="{Binding ExitTitle}" Command="{Binding ExitCommand}" BackgroundColor="Silver" Grid.Row="3" Grid.Column="0"> <Button.TextColor> <OnPlatform x:TypeArguments="Color" Android="Navy" WinPhone="Blue" iOS="Black"> </OnPlatform> </Button.TextColor> </Button> </Grid> </ContentPage.Content> </ContentPage>
This is very much the same as the previous MainPage
, but this time we are adding two Buttons
, a Label
, and an Image
.
Before we start building any view models we are going to build our navigation system. Xamarin.Forms
comes complete with navigation control for all platforms, so you won't have to worry about it. But as we always like to do things the hard way, we are going to show you a technique to separate our cross-platform structure a little more, in order to keep things more modular. Using one PCL project to contain both view models and views is great, but what if we could separate our views from view models into two PCL projects?
One small issue we have with the current PCL is that it relies completely on Xamarin.Forms
. Only our XAML sheets and user interfaces rely on Xamarin.Forms
; our view models do not. Then let's move the view models from the Xamarin.Forms
PCL into an even lower-level PCL project that only relies on c-sharp libraries.
This is a good technique to keep the PCL projects completely separated. Developing a modular system is advantageous when it comes to code sharing. For example, we are building a new app that requires a login screen, a list view screen, and other similar screens most apps include. As we already have the view models that handle all the web services, JSON processing, and property bindings, do we really need to change much? Now that we have a low-level project that simply has the view models, let's just extract the ones we need, design our user interfaces for the view models, and bind them together. Not only can we reuse these view models for other apps, but if we wanted to develop an entirely separated application (for example, a WPF application), we can just compare the required screens, take the related view models, create new user interfaces, and bind them together. Keeping everything completely separated allows for complete plug-and-play capability, which will dramatically decrease the development time required to build similar applications.
Let's approach this pattern by creating a new PCL project and copying in the view models; call it Locator.Portable:
We also want to copy over the IoC
folder as well.
Our first step is to create a folder called enum
, add the PageNames.cs
file, and copy in the following:
public enum PageNames { MainPage, MapPage }
Now let's add a new folder called UI
and create a new file called INavigationService.cs
:
public interface INavigationService { Task Navigate(PageNames pageName); }
Then we create a new folder in the Xamarin.Forms
PCL (Locator) project called UI
, and create a new file called NavigationService.cs
. The NavigationService
class will inherit the INavigationService
interface:
public class NavigationService : INavigationService { #region INavigationService implementation public async Task Navigate (PageNames pageName) { } #endregion }
Simple, right? Navigate will be used whenever we want the stack to navigate to a page. In making an abstracted interface, as we have done for navigation, this allows us to control navigation way down in the lower-level PCL. Now, fill in the rest:
public async Task Navigate (PageNames pageName, IDictionary<string, object> navigationParameters) { var page = GetPage (pageName); if (page != null) { var navigablePage = page as INavigableXamarinFormsPage; if (navigablePage != null) { await IoC.Resolve<NavigationPage> ().PushAsync (page); navigablePage.OnNavigatedTo (navigationParameters); } } } private Page GetPage(PageNames page) { switch(page) { case PageNames.MainPage: return IoC.Resolve<MainPage> (); case PageNames.MapPage: return IoC.Resolve<MapPage> (); default: return null; } }
Firstly, look more closely at the private function GetPage
; this will be called every time the Navigate
function is called to retrieve the correct ContentPage
object (which is registered in the IoC
container) based upon the PageName
enum
passed to it, and if we have found the correct page, push it onto the navigation stack.
Finally, let's build our new XamFormsModule
for registering the pages and navigation service:
public void Register(ContainerBuilder builer) { builer.RegisterType<MainPage> ().SingleInstance(); builer.RegisterType<MapPage> ().SingleInstance(); builer.Register (x => new NavigationPage(x.Resolve<MainPage>())).AsSelf().SingleInstance(); builer.RegisterType<NavigationService> ().As<INavigationService>().SingleInstance(); }
We are registering one navigation page throughout the entire life of the application, and we set the starting page to the one main page item we registered before.
Now open up the App.cs
file and update it accordingly:
public App () { MainPage = IoC.Resolve<NavigationPage> (); }
Making sense now?
IoC is a very powerful pattern for cross-platform applications.
Now let's get back to our MainPageViewModel
and update and modify the previous chapter's MainPageViewModel
with the properties required for the data-bindings on MainPage.xaml
shown previously. Firstly, let's implement the private
properties:
public class MainPageViewModel : ViewModelBase { #region Private Properties private readonly IMethods _methods; private string _descriptionMessage = "Find your location"; private string _locationTitle = "Find Location"; private string _exitTitle = "Exit"; private ICommand _locationCommand; private ICommand _exitCommand; #endregion }
Now for the Public
properties:
#region Public Properties public string DescriptionMessage { get { return _descriptionMessage; } set { if (value.Equals(_descriptionMessage)) { return; } _descriptionMessage = value; OnPropertyChanged("DescriptionMessage"); } } public string LocationTitle { get { return _locationTitle; } set { if (value.Equals(_locationTitle)) { return; } _locationTitle = value; OnPropertyChanged("LocationTitle"); } } public string ExitTitle { get { return _exitTitle; } set { if (value.Equals(_exitTitle)) { return; } _exitTitle = value; OnPropertyChanged("ExitTitle"); } } public ICommand LocationCommand { get { return _locationCommand; } set { if (value.Equals(_locationCommand)) { return; } _locationCommand = value; OnPropertyChanged("LocationCommand"); } } public ICommand ExitCommand { get { return _exitCommand; } set { if (value.Equals(_exitCommand)) { return; } _exitCommand = value; OnPropertyChanged("ExitCommand"); } } #endregion
Are we starting to see the same pattern here?
Now add the constructor, which is going to use the navigation service interface that we abstracted earlier through the IoC
container:
#region Constructors public MainPageViewModel (INavigationService navigation) : base (navigation) { } #endregion
Now it's time to show you another trick using the IoC container. In our constructor, we need to be able to create a new Command
object from the Xamarin.Forms
library. We are lucky here, because since commands from Xamarin.Forms
inherit the ICommand
interface from System.Windows.Input
, we are able to register this object in the IoC container. Open up XamFormsModule.cs
and update the Register
function to include the following:
builer.RegisterType<Xamarin.Forms.Command> ().As<ICommand>().InstancePerDependency();
Now let's create a new command through the constructor of MainPageViewModel
; update the constructor like this:
#region Constructors public MainPageViewModel (INavigationService navigation, Func<Action, ICommand> commandFactory) : base (navigation) { _locationCommand = commandFactory (() => Navigation.Navigate(PageNames.MapPage)); } #endregion
In the constructor, we are pulling a Func
out of the IoC
container, which takes an Action and returns an ICommand
object, because we have registered this interface to a Xamarin.FormsCommand
object, we will be left with a new Command
with the action passed in the constructor as follows:
locationCommand = commandFactory (() => Navigation.Navigate(PageNames.MapPage));
This is exactly the same as doing this if we were using the Xamarin.Forms
library:
locationCommand = new Command (() => Navigation.Navigate(PageNames.MapPage));
Now we have a new Command
set with and Action
to push a new MapPage
onto the stack when the button is pressed:
public class PortableModule : IModule { public void Register(ContainerBuilder builer) { builer.RegisterType<MainPageViewModel> ().SingleInstance(); } }
Now to register our new view model with the IoC
container. Create a new folder called Modules
for the portable IoC
module. Create a new file called PortableModule.cs
and paste in the preceding code into it.
Our next step is to implement the MapPage
; this page will show a panel that will display Google Maps. Underneath this panel, we will also display the location information (latitude, longitude, address, and so on) retrieved from our native platform core location services. To access these native services, we need to import Xamarin.Forms.Maps
:
Now that we have imported the Xamarin.Forms.Maps
library, we can access the native Google Maps services. We can now create the Map
user interface element via MapPage.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:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps" x:Class="Locator.Pages.MapPage" BackgroundColor="White" Title="Map"> <ContentPage.Content> <Grid x:Name="Grid" RowSpacing="10" Padding="10, 10, 10, 10"> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="80"/> <RowDefinition Height="60"/> <RowDefinition Height="60"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <maps:Map x:Name="MapView" IsShowingUser="true" Grid.Row="0" Grid.Column="0"/> <Label x:Name="AddressLabel" Text="{Binding Address}" TextColor="Black" Grid.Row="1" Grid.Column="0"/> <Button x:Name="GeolocationButton" Text="{Binding GeolocationButtonTitle}" Command="{Binding GeolocationCommand}" Grid.Row="2" Grid.Column="0"/> <Button x:Name="NearestAddressButton" Text="Find Nearest Address" Command="{Binding NearestAddressCommand}" Grid.Row="3" Grid.Column="0"/> </Grid> </ContentPage.Content> </ContentPage>
See at the top how we imported the Xamarin.Forms.Maps
library?
We have created four rows in the Grid
, one for the Map
(this will cover most of the screen), one for a label that will display the address, and two buttons for starting/stopping location updates and finding the closest location out of a list of addresses.
So where does the address come from?
We now need to implement the core location service; this is a background service that will send position information based upon your location. The information returned is very detailed; we can depict exact longitude and latitude values, as well as addresses.
To begin our core location implementation, we are going to create an abstracted geolocation interface called IGeolocator, but first we are going to add another library for processing our location updates.
If you haven't heard of the RX framework before, you are about to enter a never-ending rabbit hole of asynchrony. RX gives developers the ability to use LINQ-style query operations for processing objects in observable sequences. It allows for full control over event-based operations between different elements of an application.
In our project, we are going to use a Subject for handling location events received on the native side. In cross-platform development, because we work in both PCL and native-level projects, it involves passing data and events up and down the project structure.
We could use the event
framework, which is standard in c-sharp, but instead we are going to use a Subject
to push events into an observable sequence, while we subscribe to the subject at a lower level to receive and handle these events.
Let's start by importing the Reactive Extensions interface in our native and PCL projects:
Now let's create our IGeolocator
class:
public interface IGeolocator { Subject<IPosition> Positions { get; set; } void Start(); void Stop(); }
Notice the interface IPosition
? We must also create a new interface, which is going to store all the location information:
public interface IPosition { double Latitude {get; set;} double Longitude {get; set;} }
The interface is designed to return these variables to be used for the Xamarin.Forms
geolocator, so we can pull down address information. This information is returned by CLLocationManager
with every position update.
Why do we need to create an interface for the position information?
As this information comes from different native services, we want to create our own object to contain the information we need in the lower-level projects.
CLLocationManager
is used for the delivery of location and heading events; we must use this object in our Geolocator implementation, so let's begin:
public class GeolocatorIOS : IGeolocator { public Subject<IPosition> Positions { get; set; } }
From our interface, we must include the Subject
. Now let's instantiate CLLocationManager
. First, we must import the CoreLocation
library:
using CoreLocation;
Now we instantiate CLLocationManager
in the constructor when this is created through the IoC container. According to iOS standards, since changes to iOS 9 and iOS 8, we must implement a few separate calls to allow the location manager to begin sending location events:
public GeolocatorIOS() { Positions = new Subject<IPosition> (); locationManager = new CLLocationManager(); locationManager.PausesLocationUpdatesAutomatically = false; // iOS 8 has additional permissions requirements if (UIDevice.CurrentDevice.CheckSystemVersion (8, 0)) { locationManager.RequestWhenInUseAuthorization (); } if (UIDevice.CurrentDevice.CheckSystemVersion (9, 0)) { locationManager.AllowsBackgroundLocationUpdates = true; } }
This is nothing major; in iOS 8 we must request the authorization before using the location manager. For iOS 9, we can also set some conditional settings. For our example, we have used this:
AllowsBackgroundLocationUpdates = true
This allows the location manager to keep sending events, even when the app is in the background. We can also do this:
if (UIDevice.CurrentDevice.CheckSystemVersion (8, 0)) { locationManager.RequestWhenInUseAuthorization (); }
This will only allow events from CLLocationManager
when the app is in the foreground. There are multiple settings that can be changed, between controlling location events in the foreground and background when using location services. We want to know whether our app is going to keep updates running in the background/foreground. Most of the time, we want location updates when the app is in the foreground to reduce battery consumption, but there are scenarios where updates should continue in the background.
Now for the rest of the class; let's begin handling the location events:
private void handleLocationsUpdated (object sender, CLLocationsUpdatedEventArgs e) { var location = e.Locations.LastOrDefault (); if (location != null) { Console.WriteLine ("Location updated, position: " + location.Coordinate.Latitude + "-" + location.Coordinate.Longitude); // fire our custom Location Updated event Positions.OnNext(new Position() { Latitude = location.Coordinate.Latitude, Longitude = location.Coordinate.Longitude, }); } }
The previous function is called every time we receive a location update from CLLocationManager
. From the event argument CLLocationsUpdatedEventArgs
, we pull out a list of locations; as sometimes the CLLocationManager
receives multiple updates at one time, we always want to take the very last location. Then once we create a new Position
, assign the latitude and longitude values, and by calling the OnNext
function, we push a new event into the observable sequence.
Our next step is to add some small additions to the info.plist
file.
Let's add the following keys:
<key>NSLocationAlwaysUsageDescription</key> <string>Can we use your location</string> key>NSLocationWhenInUseUsageDescription</key> <string>We are using your location</string>
The NSLocationAlwaysUsageDescription
and NSLocationWhenInUseUsageDescription
keys will be displayed to the user in the alert that requests location data access. We must also add the background modes for the location in which we can set the iOS project properties:
Now we must implement the Start
and Stop
functions:
public void Start() { if (CLLocationManager.LocationServicesEnabled) { locationManager.DesiredAccuracy = 1; locationManager.LocationsUpdated += handleLocationsUpdated; locationManager.StartUpdatingLocation(); } } public void Stop() { locationManager.LocationsUpdated -= handleLocationsUpdated; locationManager.StopUpdatingLocation(); }
The Start
function will check whether location services have been enabled, assign the LocationsUpdated
event, and start the location updates:
public void Register(ContainerBuilder builer) { builer.RegisterType<GeolocatorIOS>().As<IGeolocator>().SingleInstance(); }
The Stop
function will do nothing more than stop the location updates and remove the event handler. That's all for the iOS geolocator. Next, we must register this interface through the IoC container.
18.189.186.167