I am writing a Xamarin.Forms application, but since GPS is not part of Xamarin.Forms, I am using dependency injection to get a native Android and iOS implementation of it.
Essentially, what I want to do is have this:
public async Task<Tuple<bool, string, GPSData>> GetGPSData() { gpsData = null; bool success = false; string error = string.Empty; if (!manager.IsProviderEnabled(LocationManager.GpsProvider)) { //request permission or location services enabling //Handle any error messages, store in variable string } else { manager.RequestSingleUpdate(LocationManager.GpsProvider, this, null); success = true; //Assuming this worked } return new Tuple<bool, string, GPSData>(success, error, gpsData.Value); }
Be a method in my IGeolocator implementation (which also extends ILocationListener on Android). It should return a Task that is a Tuple of bool, string and GPSData, where bool is true/false on whether it was a success grabbing the data, the string is the error message if the bool was false, and if it was true GPSData is not null, which is simply a wrapper around the data that is found in Androids Location object.
Before I do the error checking and such I just want to return a GPS location, hence the hard-coded return value.
The problem is that RequestSingleUpdate runs asynchronously, so when it is invoked long after the method returns. I want to handle multi threading on the Xamarin.Forms side outside of this method call. Really, ideally I'd like to simply await manager.RequestSingleUpdate.
I thought about throwing in a hacky
while(gpsData == null);
after it requests so that it blocks that thread until OnLocationChanged is invoked and sets gpsData, however it seems when I do this, it is never invoked. OnLocationChanged only ever gets invoked if I let my GetGPSData() function return.
Is there any way of my getting my gpsData set (AKA OnLocationChanged invoked) BEFORE this method returns?
Thank you for your time.
For future people, I solved it using TaskCompletionSource.
TaskCompletionSource<Tuple<bool, string, GPSData>> tcs; public Task<Tuple<bool, string, GPSData>> GetGPSData() { tcs = new TaskCompletionSource<Tuple<bool, string, GPSData>>(); gpsData = null; if (!manager.IsProviderEnabled(provider)) { tcs.TrySetException(new Exception("some error")); // This will throw on the await-ing caller of this method. } else { manager.RequestSingleUpdate(provider, this, null); } return tcs.Task; } public void OnLocationChanged(Location location) { gpsData = new GPSData(location.Latitude, location.Longitude, "", ""); tcs.TrySetResult(new Tuple<bool, string, GPSData>(false, "someString", gpsData)); }
Essentially, GetGPSData returns the Task. The result of the task is set by OnLocationChanged.
Now in my ViewModel that calls this in Xamarin.Forms I get call the method to get the task, which I can invoke
task.ContinueWith((task) => { var result = task.Result; //work });
To do the work I want on the result of the task without having to couple the Forms ViewModel and Android implementation by passing around callbacks (which you then need to go through WeakReferences to protect against memory leaks and such)
Answers
For future people, I solved it using TaskCompletionSource.
Essentially, GetGPSData returns the Task. The result of the task is set by OnLocationChanged.
Now in my ViewModel that calls this in Xamarin.Forms I get call the method to get the task, which I can invoke
To do the work I want on the result of the task without having to couple the Forms ViewModel and Android implementation by passing around callbacks (which you then need to go through WeakReferences to protect against memory leaks and such)