-1

I've read many explanations but none of them made sense to me.

I'm doing this in Xamarin.Forms:

public class SomeClass
{
    public SomeClass
    {
        var request = new GeolocationRequest(GeolocationAccuracy.High, TimeSpan.FromSeconds(10));
        var cts = new CancellationTokenSource();
        var location = Task.Run<Location>(async () => await Geolocation.GetLocationAsync(request, cts.Token));
        var userLat = location.Latitude; // doesn't work
        var userLon = location.Longitude; // doesn't work
    }
}

The reason I'm doing this is that I'm trying to load all the methods I need such as getting the user's location, show it on the map, load up some pins etc. as soon as the Xamarin.Forms.Maps map appears.

I know it's bad practice from what you guys answered so I'm working on changing that but I'm still having a hard time understanding how to do it differently in the sense of it's confusing. But I'm reading your articles and links to make sure I understand.

I tried to run Task.Run(async () => await) on many methods, and tried to save their values in variables but I can't get the returned value and that's what made me post this question, I need to change my code.

I know could get the returned value using Task.Result() but I've read that this was bad.

How to get the UI to load and wait on the ViewModel to do what it has to do, and then tell the UI to then use whatever the ViewModel is giving him when it is ready ?

Jimmy
  • 105
  • 15
  • If you need to have used `userLat` and `userLon` to correctly construct your instance then you have an *impedence mismatch*. You either need to block the thread that called the constructor or rearrange things - such as by introducing a factory method that can be async and which won't attempt to construct this object (whatever it is) until after `GetLocationAsync` has produced a result. – Damien_The_Unbeliever Jan 26 '23 at 06:46
  • To be honest, I didn't really understand what you've explained to me. Could you please explain it as if it were for a beginner ? – Jimmy Jan 26 '23 at 06:59
  • You might find this useful: [Can constructors be async?](https://stackoverflow.com/questions/8145479/can-constructors-be-async) – Theodor Zoulias Jan 26 '23 at 07:07
  • 2
    Usually you'd solve this by making your constructor `private` and create a `static async Task CreateWhateverPage()` where you call your private constructor, and do your `async` stuff alltogether – Pieterjan Jan 26 '23 at 07:17
  • I see, thank you guys I'll read the posts and get back to you. – Jimmy Jan 26 '23 at 07:28
  • @Pieterjan thank you. I'll have to look up private constructors and see if it's a fit. – Jimmy Jan 26 '23 at 07:28
  • Could you edit the question, and include the definition of the method that contains this code? – Theodor Zoulias Jan 26 '23 at 07:51
  • @TheodorZoulias the method is the class's constructor. It is in there not in a method. – Jimmy Jan 26 '23 at 08:06
  • Yea, I get it. I think that if you include the signature of the class and the constructor, the code will be more clear. You have already mentioned in the question that you are doing this in a constructor, but it's not very prominent. And this context is **very** important for understanding the question. – Theodor Zoulias Jan 26 '23 at 08:28
  • I see, I'm adding it now. – Jimmy Jan 26 '23 at 17:20

4 Answers4

1

I think it could be this:

public async Task SomeMethodAsync()
{
    var request = new GeolocationRequest(GeolocationAccuracy.High, TimeSpan.FromSeconds(10));
    var cts = new CancellationTokenSource();
    var location = await Geolocation.GetLocationAsync(request, cts.Token);
    var userLat = location.Latitude;
    var userLon = location.Longitude;
}

Not that methods which contain await calls should be marked as async. If you need to return something from this method then the return type will be Task<TResult>.

Constructors cannot be marked with async keyword. And it's better not to call any async methods from constructor, because it cannot be done the right way with await. As an alternative please consider Factory Method pattern. Check the following article of Stephen Cleary for details: Async OOP 2: Constructors.

Mike Mozhaev
  • 2,367
  • 14
  • 13
  • Sorry I forgot to mention this is in a constructor so its synchronous – Jimmy Jan 26 '23 at 06:42
  • 3
    It's not a good practice to do any business logic in constructors. And it's impossible to do async there without problems. I highly recommend making it a separate method. – Mike Mozhaev Jan 26 '23 at 06:46
  • I see, but I'm trying to have methods run as a "set up" before the map appears or before the data appears on the screen, and passing these methods in the constructor have been the only way I've made it work so far. Do you know another way to do it ? – Jimmy Jan 26 '23 at 07:01
  • 1
    Then you probably should show some "placeholder" while loading data and then update to the real content once you get the result of async method. Please check https://stackoverflow.com/questions/52476029/correct-way-to-load-data-async-in-xamarin-forms if it helps. – Mike Mozhaev Jan 26 '23 at 07:08
1

You mentioned in a comment to another answer that you can't do it async because this code is in a constructor. In that case, it is recommended to move the asynchronous code into a separate method:

public class MyClass
{
   public async Task Init()
   {
      var request = new GeolocationRequest(GeolocationAccuracy.High, TimeSpan.FromSeconds(10));
      var cts = new CancellationTokenSource();
      var location = await Geolocation.GetLocationAsync(request, cts.Token);
      var userLat = location.Latitude; 
      var userLon = location.Longitude;
  }
}

You can use it the following way:

var myObject = new MyClass();
await myObject.Init();

The other methods of this class could throw an InvalidOperationException if Init() wasn't called yet. You can set a private boolean variable wasInitialized to true at the end of the Init() method. As an alternative, you can make your constructor private an create a static method that creates your object. By this, you can assure that your object is always initialized correctly:

public class MyClass
    {
       private MyClass() { }
       public async Task<MyClass> CreateNewMyClass()
       {
          var result = new MyClass();
          var request = new GeolocationRequest(GeolocationAccuracy.High, TimeSpan.FromSeconds(10));
          var cts = new CancellationTokenSource();
          var location = await Geolocation.GetLocationAsync(request, cts.Token);
          result.userLat = location.Latitude; 
          result.userLon = location.Longitude; 
          return result;
       }
    }
SomeBody
  • 7,515
  • 2
  • 17
  • 33
  • `Task.Run(async () => await Geolocation.GetLocationAsync(request, cts.Token))` could be shortend to `Task.Run(() => Geolocation.GetLocationAsync(request, CancellationToken.None))` – Firo Jan 26 '23 at 07:47
  • I see, but isn't calling methods inside a constructor bad practice ? I was trying to do it as you say in the first half using a separate method, how can I make sure it is called on initialization ? – Jimmy Jan 26 '23 at 07:50
  • 1. In this answer, the await is not inside the constructor. 2. Note the `private` constructor. Caller calls `CreateNewMyClass`, which is an `async` method, instead of doing `new MyClass()`. – ToolmakerSteve Jan 26 '23 at 20:56
1

The normal way to retrieve results from a Task<T> is to use await. However, since this is a constructor, there are some additional wrinkles.

Think of it this way: asynchronous code make take some time to complete, and you can never be sure how long. This is in conflict with the UI requirements; when Xamarin creates your UI, it needs something to show the user right now.

So, your UI class constructor must complete synchronously. The normal way to handle this is to (immediately and synchronously) create the UI in some kind of "loading..." state and start the asynchronous operation. Then, when the operation completes, update the UI with that data.

I discuss this in more detail in my article on async data binding.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • This is a great article and I think it answers my question pretty well, I just have a question. Does any of the method or the ways to do it apply to a > 2020 Xamarin.Forms app ? Aren't there any parts you would change or update to make it more modern or use the current functionality that weren't there before ? – Jimmy Jan 26 '23 at 17:38
  • @Jimmy: Some frameworks have added their own NotifyTask, but vanilla Xamarin.Forms has not AFAIK. – Stephen Cleary Jan 26 '23 at 22:15
  • So I'm implementing the code from the article, but I'm having an issue. All my methods are Tasks and not Task, since they don't return anything but do something, such as load pins directly to a `CustomMap` created as the X.F.Maps doc says. So in `NotifyTaskCompletion` I'm trying to remove the `` but when I reach the `public TResult Result` property I don't know what to change the type to instead of `TResult`, and I don't know if I'm doing it is wrong to start and if instead I should return the lists/geolocation and pass it to the MainViewModel. What do you think I should do ? – Jimmy Jan 29 '23 at 20:33
  • Also since I'm using `Commands` in the ViewModel, how would I call them from the constructor ? Should I do it in this way : `UrlByteCount = new UrlByteCountCommand()` and the command should be `UrlByteCountCommand { new NotifyTaskCompletion() } ` ? – Jimmy Jan 29 '23 at 20:39
  • It's kind of difficult to visualize the code. There is a non-generic `NotifyTask` [here](https://github.com/StephenCleary/Mvvm/blob/d90b2a0e4792a450549f3770e7886275c3ccb0fc/future/Nito.Mvvm.Async/NotifyTask.cs). – Stephen Cleary Jan 30 '23 at 10:45
  • Thank you for it. My question was that if a method does `Map.MoveToRegion(mapSpan);`, will `GetUserLocation = NotifyTaskCompletion(MoveToRegionMethod)` would just move the map when the `Task.IsCompleted` is returned ? And if yes, how would you connect it to the xaml to have the map movement run on `Task.IsCompleted` ? – Jimmy Jan 31 '23 at 06:56
  • Again, it's difficult to visualize the code from SO comments. I.e., I don't know what `MoveToRegionMethod` does (this is the first time it's been mentioned), so I can't say if that will work or not. – Stephen Cleary Jan 31 '23 at 22:09
0

Do the asynchronous work before constructing the object and pass in the data to the constructor. I suggest a factory.

class MyFactory : IMyFactory
{
    public async Task<MyClass> GetMyClass()
    {
        var request = new GeolocationRequest(GeolocationAccuracy.High, TimeSpan.FromSeconds(10));
        var cts = new CancellationTokenSource();
        var location = Task.Run<Location>(async () => await Geolocation.GetLocationAsync(request, cts.Token));
        return new MyClass(location);   
    }
}

class MyClass
{
    public MyClass(Location location)
    {
        var userLat = location.Latitude; 
        var userLon = location.Longitude; 
        //etc....
    }
}

To create an instance, instead of

var x = new MyClass();

you'd call

var factory = new MyFactory();
var x = await factory.GetMyClass();

The nice thing about this approach is that you can mock the factory (e.g. for unit tests) in a way that does not depend on the external service.

John Wu
  • 50,556
  • 8
  • 44
  • 80