Geofencing was one of the newest features Windows Phone 8.1 provided and for the developers it indeed was cool. Previously people did it their way but it’s always better to have an api in our hand. So, before digging into that i strongly suggest you guys to read my previous 2 articles if you haven’t done it already.
1. The Complete Reference of Map Control in Windows Phone 8.1 RT
2. Tracking Location in Windows Phone 8.1
These two would get you started pretty fast and you’d probably want that. If you already read my aforementioned 2 articles, I expect you know how to use a map control in Windows Phone 8.1. We’re going to need that later.
Geofencing:
Geofencing is a mechanism where you put a virtual boundary around a specific point of interest and you want to interact with the user when you arrive or leave that specific boundary. You seriously can do pretty amazing stuff with it actually.
Initial Work:
Now, let’s take a more practical approach to this, every time I go into my room, Im supposed to check my mails. But I usually forget that. How cool it would be if my phone reminds me every time I get into my room and tells me to check my mail? 😉
So, let’s go ahead and create a new Windows Phone app and name it GeoFencingTest. Now, lets see how to do that actually.
So, first of all go over your Package.appxmanifest file and go to the Capabilities Tab and select the Location capability.
Now, when you are done with that, let’s move on and start cracking. Before going deep lets get a basic UI running. And it looks like following:
And on the XAML part it looks like the following:
<Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="4*"></RowDefinition> <RowDefinition Height="8*"></RowDefinition> </Grid.RowDefinitions> <Grid Grid.Row="0"> <TextBlock Text="GeoFencing Sample" FontSize="25" Margin="10"/> </Grid> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <StackPanel Margin="10"> <TextBlock Text="Longitude" Margin="0,0,0,15" FontSize="20"></TextBlock> <TextBlock Text="Latitude" Margin="0,0,0,15" FontSize="20"></TextBlock> <TextBlock Text="Altitude" Margin="0,0,0,15" FontSize="20"></TextBlock> </StackPanel> <StackPanel Grid.Column="1" Margin="10"> <TextBlock Name="Longitude" Margin="0,0,0,15" FontSize="20"></TextBlock> <TextBlock Name="Latitude" Margin="0,0,0,15" FontSize="20"></TextBlock> <TextBlock Name="Altitude" Margin="0,0,0,15" FontSize="20"></TextBlock> </StackPanel> </Grid> <Grid Grid.Row="2" Margin="10"> <Grid.ColumnDefinitions> <ColumnDefinition Width="4*"></ColumnDefinition> <ColumnDefinition Width="7*"></ColumnDefinition> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <CheckBox Content="Single Use"></CheckBox> <TextBlock Grid.Row="1" FontSize="20" Text="Dwell Time" Margin="0,8,0,0"/> <TextBox Grid.Column="1" Name="DwellTime" Grid.Row="1"/> <Button x:Name="CreateGeoFencingButton" Grid.Row="2" Grid.ColumnSpan="2" Content="Create GeoFence" HorizontalAlignment="Stretch" VerticalAlignment="Bottom"/> </Grid> </Grid>
Before going in deep on what is for what let’s get some basic stuff done. Let’s get our current location first.
Since you guys have seen in the last articles, it’s fairly easy to do so. All I’m doing here is get the current location using Geolocator and put the Latitude, Longitude and Altitude into the textblocks defined in the MainPage.xaml.
public sealed partial class MainPage : Page { Geolocator geolocator = new Geolocator(); CancellationTokenSource CancellationTokenSrc = new CancellationTokenSource(); public MainPage() { this.InitializeComponent(); this.NavigationCacheMode = NavigationCacheMode.Required; } protected override void OnNavigatedTo(NavigationEventArgs e) { InitializeLocation(); } async private void InitializeLocation() { try { geolocator = new Geolocator(); CancellationTokenSrc = new CancellationTokenSource(); CancellationToken token = CancellationTokenSrc.Token; var position = await geolocator.GetGeopositionAsync(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(30)).AsTask(token); Longitude.Text = position.Coordinate.Point.Position.Longitude.ToString(); Latitude.Text = position.Coordinate.Point.Position.Latitude.ToString(); Altitude.Text = position.Coordinate.Point.Position.Altitude.ToString(); } catch (Exception) { if (geolocator.LocationStatus == PositionStatus.Disabled) { ShowMessage("Location Services are turned off"); } } finally { CancellationTokenSrc = null; } } private async void ShowMessage(string message) { MessageDialog dialog = new MessageDialog(message); await dialog.ShowAsync(); } }
The only thing that is interesting here is having a CancellationToken generated from a CancellationTokenSource. Usually these are used if you really want control over your task after it’s being fired, the rest of the procedure should look familiar as it’s a basic Geolocator usage to find current geolocation.
Setting up a GeoFence:
Now comes the setting up the actual GeoFence part and even this is fairly simple. The method looks like the following:
Geofence geofence = null; int DefaultDwellTimeInSeconds = 0; private void SetupGeofence() { //Set up a unique key for the geofence string GeofenceKey = "My Home Geofence"; BasicGeoposition GeoFenceRootPosition = GetMyHomeLocation(); double Georadius = 500; // the geocircle is the circular region that defines the geofence Geocircle geocircle = new Geocircle(GeoFenceRootPosition, Georadius); bool singleUse = (bool)SingleUse.IsChecked; //Selecting a subset of the events we need to interact with the geofence MonitoredGeofenceStates GeoFenceStates = 0; GeoFenceStates |= MonitoredGeofenceStates.Entered; GeoFenceStates |= MonitoredGeofenceStates.Exited; // setting up how long you need to be in geofence for enter event to fire TimeSpan dwellTime; dwellTime = DwellTime.Text!=""? ParseTimeSpan(DwellTime.Text) : TimeSpan.FromSeconds(DefaultDwellTimeInSeconds); // setting up how long the geofence should be active TimeSpan GeoFenceDuration; GeoFenceDuration = TimeSpan.FromDays(10); // setting up the start time of the geofence DateTimeOffset GeoFenceStartTime = DateTimeOffset.Now; geofence = new Geofence(GeofenceKey, geocircle, GeoFenceStates, singleUse, dwellTime, GeoFenceStartTime, GeoFenceDuration); //Add the geofence to the GeofenceMonitor GeofenceMonitor.Current.Geofences.Add(geofence); } private TimeSpan ParseTimeSpan(string DwellTimeInSeconds) { return TimeSpan.FromSeconds(Double.Parse(DwellTimeInSeconds)); } private static BasicGeoposition GetMyHomeLocation() { BasicGeoposition GeoFenceRootPosition; GeoFenceRootPosition.Latitude = 23.742766; GeoFenceRootPosition.Longitude = 90.417566; GeoFenceRootPosition.Altitude = 0.0; return GeoFenceRootPosition; }
Now, there’s quiet a lot to catch up.
The first one is definitely the GeofenceKey. Put a GeofenceKey in position just to identify a specific Geofence. I put s generic string to identify my home geofence.
Then I called up GetMyHomeLocation() method to set the geofence center to the location of my room. This will identify the center of our desired Geofence.
And the next thing to define is a radius that will define the radius of our circular geofence. We are using Geocircle object to define our geofence here, thus we are using a center and a radius in meters to defined our geofence. You can definitely see the next thing we did is define our geocircle. Actually the Geofence takes a Geoshape object to define it’s radius and currently only Geocircle is supported.
string GeofenceKey = "My Home Geofence"; BasicGeoposition GeoFenceRootPosition = GetMyHomeLocation(); double Georadius = 500; // the geocircle is the circular region that defines the geofence Geocircle geocircle = new Geocircle(GeoFenceRootPosition, Georadius);
Now, we have put a checkbox in our XAML named SingleUse. This is supposed to define whether our geofence would be used for only one time or not. You can set up temporary one time used geofence using this.
bool singleUse = (bool)SingleUse.IsChecked;
Now, the next thing we did is we defined how our desired geofence would be treated. Which events are we considering with this geofence? Should this be fired only when we get into that geofence or should this will only be considered when we exit the geofence, or even we remove the geofence? We can actually select all or a subset of that group. So we used Logical OR operator to add up all the cases we want our geofence to react upon.
MonitoredGeofenceStates GeoFenceStates = 0; GeoFenceStates |= MonitoredGeofenceStates.Entered; GeoFenceStates |= MonitoredGeofenceStates.Exited;
Let’s move on into the time domain now. Now we need to figure how long we need to stay in our favourite geofence to trigger up it’s desired events. I put a textbox in front to define that in seconds, if that is defined, we fire out events from our geofence in that number of seconds and if not we fire out events in our default dwell time defined by 0 seconds.
// setting up how long you need to be in geofence for enter event to fire TimeSpan dwellTime; dwellTime = DwellTime.Text!=""? ParseTimeSpan(DwellTime.Text) : TimeSpan.FromSeconds(DefaultDwellTimeInSeconds);
You can even define how long a geofence should be up and running. Like I want my rooms geofence to be running for next 10 days.
// setting up how long the geofence should be active TimeSpan GeoFenceDuration; GeoFenceDuration = TimeSpan.FromDays(10);
Even with that thing defined, one question remains, even though it will run for 10 days, when does the geofence gets activated so it can start counting to that defined 10 days. Answer is right below:
// setting up the start time of the geofence DateTimeOffset GeoFenceStartTime = DateTimeOffset.Now;
Well, the only thing left to do is create our desired geofence and add it to GeofenceMonitor.
geofence = new Geofence(GeofenceKey, geocircle, GeoFenceStates, singleUse, dwellTime, GeoFenceStartTime, GeoFenceDuration); //Add the geofence to the GeofenceMonitor GeofenceMonitor.Current.Geofences.Add(geofence);
Now, that’s how you create your own geofence and add it to the GeofenceMonitor. Now you might wonder that whether you’d really have to define all that parameters to create a geofence. Not actually, the littlest you can do is define an Id and a Geocircle to start your geofence. 😉 The rest of the values would take the default assumption.
Handling GeoFence Notifications on Foreground:
We have all our background work to create a geofence but we wont be able to do a single thing unless we test our geofence. Now, we have already defined that our geofence would trigger events after the aforementioned dwellTime and will trigger events if I enter or leave it. So, I definitely have to attach a event handler at a certain place. Don’t I?
Before that, let’s create the method stub for the click event handler for CreateGeoFencingButton and start SetupGeofence() method inside it. If you don’t know how to do that, select the CreateGeoFencingButton in XAML, go to properties, select events tab and double click on the click textbox.
private void CreateGeoFencingButton_Click(object sender, RoutedEventArgs e) { SetupGeofence(); }
Then let’s add the handler in the end of SetupGeofence()
GeofenceMonitor.Current.GeofenceStateChanged += Current_GeofenceStateChanged;
Now, let’s say we want to show a message every time I enter and exit my room’s geofence. The handler might look like the following:
private async void Current_GeofenceStateChanged(GeofenceMonitor sender, object args) { var Reports = sender.ReadReports(); await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { foreach (GeofenceStateChangeReport report in Reports) { GeofenceState state = report.NewState; Geofence geofence = report.Geofence; if (state == GeofenceState.Entered) { ShowMessage("You are pretty close to your room"); } else if (state == GeofenceState.Exited) { ShowMessage("You have left pretty far away from your room"); } } }); }
There you go! That’s how you set up your geofence in an app running in the foreground.
Setting up GeoFence on the background:
Now, this is where things get a little bit tricky. There’s a number of steps involved in this.
- Create a Windows Runtime Component Project and add it to your solution.
- Change Package.appxmanifest to declare the BackgroundTask that has been created.
- Register the BackgroundTask
- Handle Geofence Notifications from Background Task
Create a Windows Runtime Component Project:
Usually, background tasks are windows runtime component so you have to add one to your solution. Please go over to your solution and add a Windows Runtime Project.
After the new project is created go over class1.cs and rename it to your liking . I renamed it to BackgroundTask and added IBackgroundTask interface to the class and implemented it. I didn’t write anything afterwards. Let’s just keep it like that for a while and move to our GeofenceTest Project. Go over references and add the newly created GeofenceTask.BackgroundGeofenceTask project to it. So our Wp8.1 project now has reference to the GeofenceTask.BackgroundGeofenceTask project.
Change Package.appxmanifest to declare the BackgroundTask:
Now let’s scoot over to our Package.appxmanifest file and go over Declarations tab. From Available Declarations combobox select BackgroundTask and select loction under the Properties list.
Now on the Entry point field please put the full qualified assembly name of your newly created background task. Don’t get confused. It’s fairly easy. Let’s have a look at BackgroundGeofenceTask class first.
namespace GeofenceTask { public sealed class BackgroundGeofenceTask : IBackgroundTask { public void Run(IBackgroundTaskInstance taskInstance) { throw new NotImplementedException(); } } }
Now put the name as (namespace.classname) format so that leaves us with GeofenceTask.BackgroundGeofenceTask
Register the BackgroundTask:
Now comes the part where you register the BackgroundTask. We need to register the background task to the app so it knows it to invoke it on time. The registering method looks like following:
private async Task RegisterBackgroundTask() { BackgroundAccessStatus backgroundAccessStatus = await BackgroundExecutionManager.RequestAccessAsync(); var geofenceTaskBuilder = new BackgroundTaskBuilder { Name = "My Home Geofence", TaskEntryPoint = "GeofenceTask.BackgroundGeofenceTask" }; var trigger = new LocationTrigger(LocationTriggerType.Geofence); geofenceTaskBuilder.SetTrigger(trigger); var geofenceTask = geofenceTaskBuilder.Register(); geofenceTask.Completed += GeofenceTask_Completed; switch (backgroundAccessStatus) { case BackgroundAccessStatus.Unspecified: case BackgroundAccessStatus.Denied: ShowMessage("This application is denied of the background task "); break; } }
Let’s see what we have in here now. The very first one is get the BackgroundAccessStatus. That helps us to know whether our application is capable of accessing BackgroundTasks. Now, let’s move up and use BackgroundTaskBuilder to create the background task we intend to create. Now, if you look at the TaskEntryPoint we provided there, you’d now realize why we created our Background Task project before and added it to the manifest. Because we are using the same entry point name here. I put a name to identify the background task. This is needed as if there is another background task with the same name you would be thrown an exception. If you want to know whether there is another BackgroundTask with the same name you can iterate through BackgroundTaskRegistration.AllTasks and then make sure whether there is one with the same name or not. You can use a method like the following to do so:
public static bool IsTaskRegistered(string TaskName) { var Registered = false; var entry = BackgroundTaskRegistration.AllTasks.FirstOrDefault(keyval => keyval.Value.Name == TaskName); if (entry.Value != null) Registered = true; return Registered; }
I haven’t used that in this solution but feel free to definitely use this. You can even unregister to make sure that your Background Task is unregistered to open up places for others
public static void Unregister(string TaskName) { var entry = BackgroundTaskRegistration.AllTasks.FirstOrDefault(keyval => keyval.Value.Name == TaskName); if (entry.Value != null) entry.Value.Unregister(true); }
The next thing that comes off is defining the LocationTrigger for the BackgroundTaskBuilder object. So, we defined a new LocationTrigger of type LocationTriggerType.Geofence and used SetTrigger method to set it up. Then we used BackgroundTaskBuilder object to register the task and we instantiated a GeofenceTask_Completed event handler for that too.
At the bottom you’d see a switch with backgroundAccessStatus and putting a MessageDialog when it’s denied or unspecified.
Handle Geofence Notifications from Background Task:
So, lets move to our Windows Runtime Component which actually is the Background Task project and implement the IBackgroundTask interface. A method named
public void Run(IBackgroundTaskInstance taskInstance)
would pop up, we’d put our regular geofence event handling in there like there in the Foreground example. Now it looks like following when we are handling it from background
public sealed class BackgroundGeofenceTask : IBackgroundTask { public void Run(IBackgroundTaskInstance taskInstance) { var Reports = GeofenceMonitor.Current.ReadReports(); var SelectedReport = Reports.FirstOrDefault(report => (report.Geofence.Id == "My Home Geofence") && (report.NewState == GeofenceState.Entered || report.NewState == GeofenceState.Exited)); if (SelectedReport==null) { return; } //We are creating a toast this time as this is running in Background var ToastContent = ToastNotificationManager.GetTemplateContent(ToastTemplateType.ToastText02); var TextNodes = ToastContent.GetElementsByTagName("text"); if (SelectedReport.NewState == GeofenceState.Entered) { TextNodes[0].AppendChild(ToastContent.CreateTextNode("You are pretty close to your room")); TextNodes[1].AppendChild(ToastContent.CreateTextNode(SelectedReport.Geofence.Id)); } else if(SelectedReport.NewState == GeofenceState.Exited) { TextNodes[0].AppendChild(ToastContent.CreateTextNode("You are pretty close to your room")); TextNodes[1].AppendChild(ToastContent.CreateTextNode(SelectedReport.Geofence.Id)); } var settings = ApplicationData.Current.LocalSettings; if (settings.Values.ContainsKey("Status")) { settings.Values["Status"] = SelectedReport.NewState; } else { settings.Values.Add(new KeyValuePair<string, object>("Status", SelectedReport.NewState.ToString())); } var Toast = new ToastNotification(ToastContent); var ToastNotifier = ToastNotificationManager.CreateToastNotifier(); ToastNotifier.Show(Toast); } }
Now, this looks a wee bit different from the first example. The first change that is noticable is that we are now selecting the reports based on our geofence id and the report’s new state. As we are in background now, we are using our geofence id to get the proper geofence from the whole list. And as now we are in background we are using a Toast Notification instead of a MessageDialog to show our notification.
Handling it from the App Side:
Now you guys might get confused what to do with the foreground code we made earlier. Do we need Current_GeofenceStateChanged event handler anymore. Now here we might have to be a bit careful. Now if we want our app to react different when it is on foreground and make some UI changes it needed then we have to use GeofenceTask_Completed event rather than Current_GeofenceStateChanged. And there’s another thing to be added. We get our GeofenceMonitor Reports using GeofenceMonitor.Current.ReadReports() and this step can only be done once for every change. So if your background reads it first then, your foreground app would not be able to read it. So, we have to save it somewhere when the background reads it so our foreground app can read it from there.
So we are using ApplicationData.Current.LocalSettings to save our state in the Background Task. If you look closely you’d find the following snippet:
var settings = ApplicationData.Current.LocalSettings; if (settings.Values.ContainsKey("Status")) { settings.Values["Status"] = SelectedReport.NewState; } else { settings.Values.Add(new KeyValuePair<string, object>("Status", SelectedReport.NewState.ToString())); }
So, in our app side GeofenceTask_Completed event handler we’d read the status and fill up a new textblock.
Thus we added a new set of textblocks in the MainPage.xaml
<TextBlock Grid.Row="2" Text="Status" FontSize="25"/> <TextBlock Grid.Row="2" Grid.Column="1" Name="Status" FontSize="25"/>
Now, our GeofenceTask_Completed event handler looks like the following:
private async void GeofenceTask_Completed(BackgroundTaskRegistration sender, BackgroundTaskCompletedEventArgs args) { if (sender != null) { // Update the UI with progress reported by the background task await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { try { // If the background task threw an exception, display the exception args.CheckResult(); var settings = ApplicationData.Current.LocalSettings; // get status if (settings.Values.ContainsKey("Status")) { Status.Text = settings.Values["Status"].ToString(); } } catch (Exception ex) { ShowMessage(ex.Message); } }); } }
Now, if you look closely you will see all we did here is get the “Status” object from our ApplicationData.Current.LocalSettings and posted it on the TextBlock named Status. 🙂
Now, all we have left to do is do a little change in UI and add an extra button to set up the Geofence background task. Now it looks like the following:
I created the method stub for the new buttons click event too and it looks like the following:
private async void CreateBackgroundServiceButton_Click(object sender, RoutedEventArgs e) { if (IsTaskRegistered("My Home Geofence")) { Unregister("My Home Geofence"); } await RegisterBackgroundTask(); }
Testing the App on the Emulator:
You can definitely test the app on the emulator. Like always click the debug button with a emulator selected as the device and when it opens go to additional tools button and open up the location tab. Select a close location to your geofence, I did select a close one to my room and the status changed itself to “Entered”. It was that easy! You can even try the foreground example in the same way!
The source code is here.
Stay frosty! Hope this would help.