If you’ve done any work with location in Windows Phone you probably know that the a new location API was added at Windows Phone 8. If you want to upgrade your app to take advantage of functionality in Window Phone 8 you have a few options.
- Continue to use the original GeoCoordinateWatcher API from Windows Phone 7 in your Phone 8 and Phone 7 app
- Upgrade your app to Windows Phone 8 and use the new API in your Phone 8 app, while the Phone 7 app uses the original API.
- Upgrade your app to Windows Phone 8 and use the new API in your Phone 8 app AND your Phone 7 app!
These options assume you want to continue to support Windows Phone 7. Please do not forget that there are a lot of people with a Phone 7.
If you want to do options one or two you can stop reading now. If you want to do option three then you are in the right place. I’m not a big fan of supporting two different APIs in my apps and I don’t like using “out dated” APIs either. What we will need to do is create an API that duplicates the Windows.Devices.Geolocation API but uses the old System.Device.Location API. This will allow a file that uses location to be used with a Windows Phone 7 app AND a Phone 8 app!
I won’t get into the differences between the APIs as this upgrade link does a pretty good job. Most of the old API maps pretty easily to the new API. The bulk of the work is in the Geolocator class itself.
We’ll start with just a stub of the class.
namespace Windows.Devices.Geolocation
{
public class Geolocator
{
public event TypedEventHandler<Geolocator, PositionChangedEventArgs> PositionChanged;
public event TypedEventHandler<Geolocator, StatusChangedEventArgs> StatusChanged;
public PositionStatus LocationStatus { get; }
public Task<Geoposition> GetGeopositionAsync() { }
public double DesiredAccuracyInMeters { get; set; }
public double MovementThreshold { get; set; }
public PositionAccuracy DesiredAccuracy { get; set; }
/// <summary>
/// Not implemented
/// </summary>
public int ReportInterval { get; set; }
}
}
Notice that ReportInterval is not implemented. This is not something I wanted to implement, not nothing is stopping you from doing that!
Also notice that I did not even
Most of the properties are implemented in terms of the original API.
private static bool _disabled;
private PositionStatus _status = PositionStatus.NoData;
private GeoCoordinateWatcher _watcher;
public Geolocator()
{
_watcher = new GeoCoordinateWatcher(GeoPositionAccuracy.Default) { MovementThreshold = 10 };
}
public event TypedEventHandler<Geolocator, StatusChangedEventArgs> StatusChanged;
public PositionStatus LocationStatus
{
get
{
if (_disabled) return PositionStatus.Disabled;
return _status;
}
}
public double DesiredAccuracyInMeters
{
get { return _watcher.MovementThreshold; }
set { _watcher.MovementThreshold = value; }
}
I did not implement DesiredAccuracy or MovementThreshold in this stub because it’s a little more involved to maintain functionality. I’ll get into that more later.
Notice the static disabled property. A nice thing about the Geolocator is that it always knows if the location service for the phone itself is turned off. The GeoCoordinateWatcher only knows about it being disabled after it has already started. So to make life a little easier, this Geolocator will attempt to do the same.
The biggest different between the two APIs is the removal of the Start and Stop methods from the GeoCoordinateWatcher class. Start and Stop were replaced by simply subscribing to the PositionChanged event. Because everything happens when (un)subscribing from the event, we cannot use standard event stubs and must implement the event ourselves.
private TypedEventHandler<Geolocator, PositionChangedEventArgs> _positionChangedDelegate;
private readonly object _padLock = new object();
public event TypedEventHandler<Geolocator, PositionChangedEventArgs> PositionChanged
{
add
{
lock (_padLock)
{
if (_positionChangedDelegate == null)
{
// if this is the first subscription to the PositionChanged event
// then subscribe to the GeoCoordinateWatcher events and start
// tracking location.
_watcher.PositionChanged += OnPositionChanged;
_watcher.StatusChanged += OnStatusChanged;
_watcher.Start();
}
_positionChangedDelegate += value;
}
}
remove
{
lock (_padLock)
{
_positionChangedDelegate -= value;
if (_positionChangedDelegate == null)
{
// Last person to unsubscribe from this event
// Stop the GeoCoordinateWatcher and unsubscribe from the events.
_watcher.Stop();
_watcher.PositionChanged -= OnPositionChanged;
_watcher.StatusChanged -= OnStatusChanged;
}
}
}
}
When the first subscription to the event comes in, we’ll subscribe to the GeoCoordinateWatcher events and Start tracking location. When the last subscription is removed, we unsubscribe from the GeoCoordinateWatcher events and stop tracking location. When the position or status changes from the GeoCoordinateWatcher, we’ll fire off our own PositionChanged or StatusChanged events.
private void OnStatusChanged(object sender, GeoPositionStatusChangedEventArgs args)
{
var handler = StatusChanged;
if (handler != null)
{
_status = args.Status.ToPositionStatus();
_disabled = _status == PositionStatus.Disabled;
var changedEventArgs = new StatusChangedEventArgs(_status);
handler(this, changedEventArgs);
}
}
private void OnPositionChanged(object sender, GeoPositionChangedEventArgs<GeoCoordinate> args)
{
var handler = _positionChangedDelegate;
if (handler != null)
{
var geoposition = new Geoposition(args.Position);
handler(this, new PositionChangedEventArgs(geoposition));
}
}
We have now replaced the Start and Stop functionality from the GeoCoordinateWatcher API with the PositionChanged event. The next biggie are the GeoPositionAsync methods. I choose to only implement the default method as the first parameter from the second method (maximumAge) does not lend itself well to Windows Phone 7. The maximumAge parameter specifies “the maximum acceptable age of cached location data.” While we could cache location data, this is not the same as what is done in Windows Phone 8. In Phone 8, the system itself will cache location data. The GeoPositionAsync method performs it’s asynchronous operations via the IAsyncOperation<T> interface. This interface is not available in Windows Phone 7, but thanks to the Microsoft.Bcl library we do have the Task class which allows us to have methods that can be awaited.
public Task<Geoposition> GetGeopositionAsync()
{
var completion = new TaskCompletionSource<Geoposition>();
var locator = new Geolocator()
{
DesiredAccuracyInMeters = DesiredAccuracyInMeters,
MovementThreshold = MovementThreshold,
ReportInterval = ReportInterval
};
TypedEventHandler<Geolocator, PositionChangedEventArgs> positionChangedHandler = null;
TypedEventHandler<Geolocator, StatusChangedEventArgs> statusChangedHandler = null;
positionChangedHandler = (s, e) =>
{
locator.PositionChanged -= positionChangedHandler;
locator.StatusChanged -= statusChangedHandler;
completion.SetResult(e.Position);
};
statusChangedHandler = (sender, args) =>
{
if (args.Status == PositionStatus.Disabled)
{
// unsubscribe as we will not get any data
locator.PositionChanged -= positionChangedHandler;
locator.StatusChanged -= statusChangedHandler;
completion.SetResult(null);
}
};
locator.PositionChanged += positionChangedHandler;
locator.StatusChanged += statusChangedHandler;
return completion.Task;
}
The GeoPositionAsync method is meant to be a one time call method. This method initializes a new Geolocator object so we are able to take advantage of the new API we just wrote! The method uses the TaskCompletionSource class which gives us the power to perform this method in an async way. When GetPositionAsync is called, we need to subscribe to both the PositionChanged and the StatusChanged events. If the StatusChanged event comes back with location being Disabled, the PositionChanged event will never fire with a null location.
I mentioned earlier that DesiredAccuracy and MovementThreashold were not stubbed out because they required a little more. In Windows Phone 8, you are not allowed to change these properties (along with ReportInterval) while you are getting location. To maintain this functionality, we must check to see if anyone has subscribed to the PositionChanged event. If they have, throw an exception. In windows Phone 8, it throws the following
System.Exception was unhandled by user code
HResult=-2147467260
Message=Operation aborted (Exception from HRESULT: 0x80004004 (E_ABORT))
It throws this exception because the Geolocator is native C++. We’re in .Net so we can throw a better exception.
public double MovementThreshold
{
get { return _watcher.MovementThreshold; }
set
{
if (_positionChangedDelegate != null)
throw new NotSupportedException("Cannot change the MovementThreshold while getting location.");
_watcher.MovementThreshold = value;
}
}
public PositionAccuracy DesiredAccuracy
{
get { return (PositionAccuracy)_watcher.DesiredAccuracy; }
set
{
if (_positionChangedDelegate != null)
throw new NotSupportedException("Cannot change the DesiredAccuracy while getting location.");
double movementThreshold = MovementThreshold;
// We cannot change the accuracy in a GeoCoodinateWatcher so we need to create a new one
_watcher = new GeoCoordinateWatcher((GeoPositionAccuracy)value)
{ MovementThreshold = movementThreshold };
}
}
And we are done with the Geolocator class. The rest of the work is stubbing out the other classes. These are pretty much a one for one match from each API. To save you some time you can download the source from github.
If you are a fan of NuGet you can install the bridge:
PM> Install-Package WPLocationBridge
OH, and did I forget to mention that this API is the same used in Windows Store apps? This means that one code file can be used across ALL THREE platforms!