I've been using async methods for the entirety of my project, but this is the first time I've faced this situation and I don't understand it at all.
I need to feed my code some data as soon as the map loads, which is the first screen the user sees when opening the app, and in order to do this I used to use a lot of Task.Run(async () => await Command()); in the Map's constructor.
After asking SO about other issues some people pointed that this was very bad practice such as Stephen Cleary that gave me an idea on how to make this work with these two links:
You don't have to read them now since I've recreated the code and pasted it below.
My goal is to do this:
- As the map is displayed, I get the user's location
- I save it in two variables called
LatandLon. - Using the user's location, I use reverse geocoding to get the user's city.
- I also get the user's
Maps.VisibleRegion.Radius.
The problem is that the only way I've been able to make this work was with a ton of Task.Run() in the constructor of the ViewModel, while the xaml data binding was connected to the Commands in the ViewModel. But these methods were "doing" something, now I'm trying to get the result from these methods and assign them to variables on initialization for the most part ( until the user moves or moves the map with its fingers, but my issue is understanding how to run these async methods the right way ).
Trying to get the lat, lon, user city and visible region so far goes this way:
- I have a
MapServices.GeolocationServicethat contains all of the geolocation related methods, all of them run asynchronously and I don't know how to call them properly from theMapViewModelso that I don't useTask.Resultas you'll see in the code below. - I have a
MapServices.LoadPinsServicethat contains all of the user requests for different kinds of pins related methods, all of them run asynchronously as well and I don't know how to run these either now that I'm trying to not useTask.Run().
By the way, I'm still not sure if Task.Run() is a good way to run these methods/commands. I've been reading the MSDocs on async programming but I'm still having a very hard time understanding everything about it.
I use Xamarin.Forms, Xamarin.Forms.Maps and MVVM.
I have 3 files:
MapViewModel.cs, connected to Map.xaml.public class MapViewModel : BaseViewModel { public CustomMap Map { get; private set; } public List<PlaceDTO> PlacesList = new List<PlaceDTO>(); public double UserLat { get; set; } public double UserLon { get; set; } public string UserCity { get; set; } public double VisibleRegion { get; set; } public NotifyTaskCompletion DisplayCurrentUserLocation { get; set; } public MapViewModel() { Map.CustomPins = new List<CustomPin>(); // GET USER LOCATION, VISIBLE REGION AND CITY // Creating a point where to start when the map first appears // before zooming on user Position position = new Position( 48.88939331727405, 2.4849528097009705); MapSpan mapSpan = new MapSpan(position, 10, 10); Map = new CustomMap(mapSpan) { IsShowingUser = true }; // Get user location Task<Location> locationTask = Task.Run(async () => await GeolocationService .ExecuteRequestLocationCommandAsync(Map, IsBusy)); // Get the values to pass it to every method or command that need it Location userLocation = locationTask.Result; UserLat = userLocation.Latitude; UserLon = userLocation.Longitude; // Gets the user's city var userCity = Task.Run(async () => await GeolocationService .GetUserCityAsync(userLocation)); UserCity = userCity.Result; var visibleRegionTask = Task.Run(async () => await GeolocationService.GetVisibleRegion(Map)); VisibleRegion = double.Parse(visibleRegionTask.Result.Radius.ToString()); // Display user location on the map var displayLocationTask = Task.Run(async () => await GeolocationService.ExecuteDisplayCurrentLocationCommand( userLocation, Map, IsBusy)); }}
GeolocationService.cspublic static class GeolocationService { public static IPlaceRepository _placeRepository { get; set; } public static async Task<Location> ExecuteRequestLocationCommandAsync( CustomMap Map, bool isBusyFromBaseViewModel) { var request = new GeolocationRequest(GeolocationAccuracy.High, TimeSpan.FromSeconds(10)); var cts = new CancellationTokenSource(); var location = await Geolocation.GetLocationAsync(request, cts.Token); return location; } public static async Task ExecuteDisplayCurrentLocationCommand( Location location, CustomMap Map, bool isBusyFromBaseViewModel) { isBusyFromBaseViewModel = true; try { MainThread.BeginInvokeOnMainThread(async () => { if (location != null) { Position position = new Position(location.Latitude, location.Longitude); MapSpan mapSpan = MapSpan.FromCenterAndRadius(position, Distance.FromKilometers(8)); Map.MoveToRegion(mapSpan); // ADD TO CACHE } else { await Application.Current.MainPage.DisplayAlert("Can't get location", "", "Cancel"); } }); } catch (Exception ex) { Debug.WriteLine(ex); } finally { isBusyFromBaseViewModel = false; } } public static async Task<string> GetUserCityAsync( Location location) { var userCity = ""; try { var placemarks = await Geocoding.GetPlacemarksAsync( location.Latitude, location.Longitude); var placemark = placemarks?.FirstOrDefault(); if (placemark != null) { userCity = placemark.Locality; } return userCity; } catch (Exception ex) { } return userCity; } public static async Task<MapSpan> GetVisibleRegion(CustomMap map) { var visibleRegion = map.VisibleRegion; return visibleRegion; }Now, the solution given by Stephen, which makes sense but I don't know how to use it in this case because it seems to be the best way but for let's say, data waiting to update on the screen as the map is loading, for example running
GeolocationService.ExecuteDisplayPlaceLocationCommandwhich I can't since I need theTask.Resultbut without blocking the thread... ->NotifyTaskCompletionandNotifyTaskCompletion<TResult>which then bind to the xaml usingVisibility = WhicheverTask.IsCompleted / .IsNotCompleted:public sealed class NotifyTaskCompletion : INotifyPropertyChanged { public NotifyTaskCompletion(Task task) { Task = task; if (!task.IsCompleted) { var _ = WatchTaskAsync(task); } } private async Task WatchTaskAsync(Task task) { try { await task; } catch { } var propertyChanged = PropertyChanged; if (propertyChanged == null) return; propertyChanged(this, new PropertyChangedEventArgs(nameof(IsCompleted))); propertyChanged(this, new PropertyChangedEventArgs(nameof(IsNotCompleted))); propertyChanged(this, new PropertyChangedEventArgs(nameof(Status))); if (task.IsCanceled) { propertyChanged(this, new PropertyChangedEventArgs(nameof(IsCanceled))); propertyChanged(this, new PropertyChangedEventArgs(nameof(Status))); } else if (task.IsFaulted) { propertyChanged(this, new PropertyChangedEventArgs(nameof(IsFaulted))); propertyChanged(this, new PropertyChangedEventArgs(nameof(Exception))); propertyChanged(this, new PropertyChangedEventArgs(nameof(InnerException))); propertyChanged(this, new PropertyChangedEventArgs(nameof(ErrorMessage))); propertyChanged(this, new PropertyChangedEventArgs(nameof(Status))); } else { propertyChanged(this, new PropertyChangedEventArgs(nameof(IsSuccessfullyCompleted))); propertyChanged(this, new PropertyChangedEventArgs(nameof(Status))); } } public Task Task { get; private set; } public Task TaskCompleted { get; } public TaskStatus Status => Task.Status; public bool IsCompleted => Task.IsCompleted; public bool IsNotCompleted => !Task.IsCompleted; public bool IsSuccessfullyCompleted => Task.Status == TaskStatus.RanToCompletion; public bool IsCanceled => Task.IsCanceled; public bool IsFaulted => Task.IsFaulted; public AggregateException Exception => Task.Exception; public Exception InnerException => Exception?.InnerException; public string ErrorMessage => InnerException?.Message; public event PropertyChangedEventHandler PropertyChanged; } public sealed class NotifyTaskCompletion<TResult> : INotifyPropertyChanged { private readonly TResult _defaultResult; internal NotifyTaskCompletion( Task<TResult> task //TResult defaultResult ) { //_defaultResult = defaultResult; Task = task; TaskCompleted = MonitorTaskAsync(task); } private async Task MonitorTaskAsync(Task task) { try { await task; } catch { // Ignore exceptions. } finally { NotifyProperties(task); } } private void NotifyProperties(Task task) { var propertyChanged = PropertyChanged; if (propertyChanged == null) return; if (task.IsCanceled) { propertyChanged(this, new PropertyChangedEventArgs(nameof(Status))); propertyChanged(this, new PropertyChangedEventArgs(nameof(IsCanceled))); } else if (task.IsFaulted) { propertyChanged(this, new PropertyChangedEventArgs(nameof(Exception))); propertyChanged(this, new PropertyChangedEventArgs(nameof(InnerException))); propertyChanged(this, new PropertyChangedEventArgs(nameof(ErrorMessage))); propertyChanged(this, new PropertyChangedEventArgs(nameof(Status))); propertyChanged(this, new PropertyChangedEventArgs(nameof(IsFaulted))); } else { propertyChanged(this, new PropertyChangedEventArgs(nameof(Result))); propertyChanged(this, new PropertyChangedEventArgs(nameof(Status))); propertyChanged(this, new PropertyChangedEventArgs(nameof(IsSuccessfullyCompleted))); } propertyChanged(this, new PropertyChangedEventArgs(nameof(IsCompleted))); propertyChanged(this, new PropertyChangedEventArgs(nameof(IsNotCompleted))); } public Task<TResult> Task { get; } public Task TaskCompleted { get; } public TResult Result => Task.Status == TaskStatus.RanToCompletion ? Task.Result : _defaultResult; public TaskStatus Status => Task.Status; public bool IsCompleted => Task.IsCompleted; public bool IsNotCompleted => !Task.IsCompleted; public bool IsSuccessfullyCompleted => Task.Status == TaskStatus.RanToCompletion; public bool IsCanceled => Task.IsCanceled; public bool IsFaulted => Task.IsFaulted; public AggregateException Exception => Task.Exception; public Exception InnerException => Exception?.InnerException; public string ErrorMessage => InnerException?.Message; public event PropertyChangedEventHandler PropertyChanged; }
I commented all my Commands in my MapViewModel constructor since I don't even know if I should run NotifyTaskCompletion or Commands when the user clicks on something...
I'm lost, please let me know if you need more info if my issue isn't as clear as it should be. My problem at the moment is how should I call the async methods in order to get the UserLat, UserLon, UserCity and VisibleRegion. I'll figure out how to update them when the user modifies these values by moving the screen after.