-1

In a Xamarin.Forms project I want to launch a different page depending on the existence of a certain document in a Firestore database. As a result I tried to use await in the App.xaml.cs file's constructor.

Here's my code:

public partial class App : Application
{
        public App()
        {
            InitializeComponent();

            new Action(async () => await firebaseExists())();

            if (firebaseDocExists == false) MainPage = new MyPage();
            else MainPage = new NavigationPage(new MainPage());
        }

        bool firebaseDocExists = false;

        private async Task DocExists()
        {
            var firebaseDoc = await CrossCloudFirestore.Current
                                                       .Instance
                                                       .Collection("myCollection")
                                                       .Document("docId")
                                                       .GetAsync();

           firebaseDocExists = firebaseDoc.Exists;
    }

}

I expected that this line: new Action(async () => await firebaseExists())(); would update the firebaseDocExists variable's value, but in reality it stays stuck as false and the value of the initialization never changes.

Can the approach I have adopted work if modified or should I find another way to use await in the constructor?

Costas
  • 459
  • 3
  • 15
  • using new Action(async () => await firebaseExists())(); is a synchronous call, meaning it won't be awaited. You'd probably want something like Task.Run(async () => await firebaseExists()); – damiensprinkle Apr 04 '23 at 21:57
  • Just used `Task.Run`. No luck. Still `MyPage` is the one that launches which means that `firebaseDocExists` remains `false`, although the `firebaseDoc` exists. – Costas Apr 04 '23 at 22:10
  • Anything inside Task.Run runs AFTER the calling method (the constructor) finishes. You have to put ALL remaining code of constructor inside the Task.Run. BUT you have UI calls (page navigation), so instead of Task.Run use `MainThread.BeginInvokeOnMainThread`. Note that questions involving `await` in a `constructor` have been asked multiple times already at StackOverflow. Google `xamarin Problem with using await in a constructor` would find those Q&As. – ToolmakerSteve Apr 05 '23 at 00:56
  • What do you mean by "ALL remaining" code? Every line from `firebaseExists()` till the end of the constructor or everything in the body of the constructor? – Costas Apr 05 '23 at 07:12
  • @ToolmakerSteve Just for the record. I used `MainThread.BeginInvokeOnMainThread` as you suggested, but it didn't make any difference. – Costas Apr 05 '23 at 19:12
  • I meant all code till the end of the constructor. Similar to moving that code to `Loader` as in your answer, but wrapping it with `MainThread.BeginInvokeOnMainThread .. async .. await`, as described in a comment I've added to your answer. – ToolmakerSteve Apr 15 '23 at 20:00

3 Answers3

2

You can try to put the code into the OnStart method instead of the constructor. Such as:

public partial class App : Application
{
        public App()
        {
            InitializeComponent();
        }

        protected override void OnStart()
        {
            new Action(async () => await firebaseExists())();
            if(MainPage == null)
            {
               if (firebaseDocExists == false) MainPage = new MyPage();
               else MainPage = new NavigationPage(new MainPage());
            }
        }

        bool firebaseDocExists = false;

        private async Task DocExists()
        {
            var firebaseDoc = await CrossCloudFirestore.Current
                                                       .Instance
                                                       .Collection("myCollection")
                                                       .Document("docId")
                                                       .GetAsync();

           firebaseDocExists = firebaseDoc.Exists;
    }

}

Update 1:

I can't understand why you used new Action(async () => await firebaseExists())();, but you can try to change the code such as:

public partial class App : Application
{
        public App()
        {
            InitializeComponent();
        }

        protected override async void OnStart()
        {
            bool firebaseDocExists = await DocExists();
            if(MainPage == null)
            {
               if (firebaseDocExists == false) MainPage = new MyPage();
               else MainPage = new NavigationPage(new MainPage());
            }
        }

        private async Task<bool> DocExists()
        {
            var firebaseDoc = await CrossCloudFirestore.Current
                                                       .Instance
                                                       .Collection("myCollection")
                                                       .Document("docId")
                                                       .GetAsync();

           return firebaseDoc.Exists;
    }

}

Update 2:

Set the default page as NavigationPage(new MainPage()); in the constructor and change it in the OnStart method.

public partial class App : Application
{
        public App()
        {
            InitializeComponent();
            MainPage = new NavigationPage(new MainPage());
        }

        protected override async void OnStart()
        {
            bool firebaseDocExists = await DocExists();
            if (firebaseDocExists == false) MainPage = new MyPage();
        }

        private async Task<bool> DocExists()
        {
            var firebaseDoc = await CrossCloudFirestore.Current
                                                       .Instance
                                                       .Collection("myCollection")
                                                       .Document("docId")
                                                       .GetAsync();

           return firebaseDoc.Exists;
    }

}

Update 3:

I found that MainThread.BeginInvokeOnMainThread can set the MainPage as as NavigationPage(new MainPage()) in the OnStart method. So you can also used the following code:

public partial class App : Application
{
        public App()
        {
            InitializeComponent();
        }

        protected override async void OnStart()
        {
            bool firebaseDocExists = await DocExists();
            if(MainPage == null)
            {  
               MainThread.BeginInvokeOnMainThread(()=>
               {
                  if (firebaseDocExists == false) MainPage = new MyPage();
                  else MainPage = new NavigationPage(new MainPage());
               });
            }
        }

        private async Task<bool> DocExists()
        {
            var firebaseDoc = await CrossCloudFirestore.Current
                                                       .Instance
                                                       .Collection("myCollection")
                                                       .Document("docId")
                                                       .GetAsync();

           return firebaseDoc.Exists;
    }

}

Update 4:

        protected override async void OnStart()
        {
            bool firebaseDocExists = await DocExists();
            if(MainPage == null)
            {
               if (firebaseDocExists == false) MainPage = new MyPage();
               else 
                  {
                     MainThread.BeginInvokeOnMainThread(()=>
                         {
                           MainPage = new NavigationPage(new MainPage());
                         });
                 }
            }
        }

Update 5:

public partial class App : Application
{
        public App()
        {
            InitializeComponent();

            DocExists().Wait();

            if (firebaseDocExists == false) MainPage = new MyPage();
            else MainPage = new NavigationPage(new MainPage());
        }

        bool firebaseDocExists = false;

        private async Task DocExists()
        {
            var firebaseDoc = await CrossCloudFirestore.Current
                                                       .Instance
                                                       .Collection("myCollection")
                                                       .Document("docId")
                                                       .GetAsync();

           firebaseDocExists = firebaseDoc.Exists;
    }

}
Liyun Zhang - MSFT
  • 8,271
  • 1
  • 2
  • 14
  • Still the same behavior with your code. The page opens first and this line `firebaseDocExists = firebaseDoc.Exists;` gets executed afterwards. – Costas Apr 05 '23 at 13:41
  • You can check the updated part in my answer. @Costas – Liyun Zhang - MSFT Apr 06 '23 at 02:02
  • I have already tried this approach. While it solves the problem with `firebaseDocExists` and gives it the correct value before the if - else statement gets executed, the `MainPage` does not open. The app stays at a blank screen. – Costas Apr 06 '23 at 06:50
  • Did you try to debug to check if the code gets into `else MainPage = new NavigationPage(new MainPage());` or not? @Costas – Liyun Zhang - MSFT Apr 06 '23 at 06:56
  • Yes I have and the answer is that it does. It didn't reach the else statement with the previous version of the code, but now `firebaseDocExists` has the correct value and it does reach the else statement, but the `MainPage` never opens. Maybe this is the issue: https://stackoverflow.com/questions/50603118/setting-mainpage-navigationpage-during-onstart-in-xamarin-forms-displays-a-bla ? – Costas Apr 06 '23 at 07:09
  • Yes, I have tested it and found the problem is same as the issue you mentioned. So you can try to set the default page as the `NavigationPage(new MainPage());` in the constructor. You can refer to the updated part 2 in my answer.@Costas – Liyun Zhang - MSFT Apr 06 '23 at 07:22
  • Did you reslove the probelm? And I found the `MainThread.BeginInvokeOnMainThread` can reslove the issue you mentioned. You can check the updated part 3 in my answer. @Costas – Liyun Zhang - MSFT Apr 06 '23 at 07:41
  • Well, I just tried the 3rd version of your code. Same thing. The code gets to the else statement, but I get again a blank page. The second version would probably work, but it always tries to launch `MainPage` in the constructor and that in my case means that the app crashes with an exception thrown, because in MainPage I try to access things from the database that only logged in users are allowed to have access. – Costas Apr 06 '23 at 07:55
  • I have tested again. The 3rd code can work but the blank page will keep a few seconds and then goto the mainpage. And when I only put the `MainPage = new NavigationPage(new MainPage())` in the `MainThread.BeginInvokeOnMainThread` will reduce the blank page time. You can have a try with the 4th part code. @Costas – Liyun Zhang - MSFT Apr 06 '23 at 08:11
  • And you can also try to use the `Task.Wait()` or the `bool firebaseDocExists =DocExists().Result` to make the code run sync instead of async in the constructor. You can check the updated 5th part.@Costas – Liyun Zhang - MSFT Apr 06 '23 at 08:26
  • Well, the `MainThread` approach didn't work for me. I waited for minutes. No `MainPage` ever got launched. I tried to use `Result`, but I got: `Error CS1061 'Task' does not contain a definition for 'Result' and no accessible extension method 'Result' accepting a first argument of type 'Task' could be found (are you missing a using directive or an assembly reference?)` I also tried to use `DocExists.Wait()`. I again get a blank page. With `Wait()` the code never reaches this line `firebaseDocExists = firebaseDoc.Exists;` in the `Task`. Only the first `Task` statement gets executed. – Costas Apr 06 '23 at 08:46
  • When you use the `bool firebaseDocExists =DocExists().Result`, you need to use my code to change the `DocExists()` method as `private async Task DocExists() { var firebaseDoc = await CrossCloudFirestore.Current ... return firebaseDoc.Exists; }`. @Costas – Liyun Zhang - MSFT Apr 06 '23 at 08:59
  • OK. I'm using `Result` right now. I have put a break point in every line of the `App.xaml.cs` possible. Here is what happens: When the code reaches `bool firebaseDocExists =DocExists().Result` it enters in the Task body and after executing the first statement: `var firebaseDoc = ...;` the yellow arrow of VS that points to the next code line that will get executed disappears without ever reaching the second and the third statements of the Task. The exact same things happens when I use `DocExists().Wait()`. – Costas Apr 06 '23 at 09:11
  • You can refer to this answer about [run async in the constructor](https://stackoverflow.com/a/12520574/17455524) which used a static method. @Costas – Liyun Zhang - MSFT Apr 06 '23 at 09:17
  • What would be `TData` and `Data` in my case? – Costas Apr 06 '23 at 09:30
  • Sorry for my careless, the app is special partial class and can't use this kind of solution. @Costas – Liyun Zhang - MSFT Apr 06 '23 at 09:56
  • OK. Did all the three other approaches (`MainThread` in the `OnStart` method and `Result` / `Wait` in the constructor) worked for you? I wonder why they don't work for me. Anyway, maybe I should adopt a different approach. Maybe I should create a new page which will have as only purpose to handle which page will open afterwards, instead of doing that work in the App.xaml.cs file. I don't think that would be ideal, but so far everything seems as a dead end. – Costas Apr 06 '23 at 10:24
  • Finally, I found a solution! Thank you very much for your help, thank you for all of your answers! – Costas Apr 06 '23 at 23:04
0

Can the approach I have adopted work if modified or should I find another way to use await in the constructor?

The code you posted is a convoluted way of saying async void, which is why it's not working. There's no way to force this to work.

Consider this: when the OS asks your app to display something, it needs to open it now. It does not want to wait for your app to talk to some database somewhere before it decides what to display.

What you actually need to do is to add a third state: a "loading" state.

Once you do that, the answer is much simpler: load the loading state, and start the asynchronous work. When the asynchronous work completes, then change to one of the other states (or an error state if the DB query fails).

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • I actually [thought of adding a new Page in the project](https://stackoverflow.com/questions/75931043/problem-with-using-await-in-a-constructor/75953236#comment133956789_75937447) that will handle which pages will open when the app launches. Is that a good way to do it or are there any other more specific ways for implementing that "loading state"? – Costas Apr 06 '23 at 20:03
  • 1
    @Costas: That's what I would do. – Stephen Cleary Apr 06 '23 at 20:04
  • Then I guess that's what I will do too! Thanks for your time answering my question! – Costas Apr 06 '23 at 20:12
0

After many tries here is what finally worked. The code is implemented in the App.Xaml.cs file. In the end, I didn't use another Page for handling the situation.

public partial class App : Application
{
        public App()
        {
            InitializeComponent();

            MainPage = new Page(); // If this line is omitted the app shows a blank page
                                   // no matter what I tried in the Loader method.

            MainThread.BeginInvokeOnMainThread(async () => await Loader());
        }

        
        private async Task Loader()
        {
            var firebaseDocExists = false;

            var firebaseDoc = await CrossCloudFirestore.Current
                                                       .Instance
                                                       .Collection("myCollection")
                                                       .Document("docId")
                                                       .GetAsync();

            firebaseDocExists = firebaseDoc.Exists;

            if (firebaseDocExists == false) MainPage = new MyPage();
            else MainPage = new NavigationPage(new MainPage());
        }
}
Costas
  • 459
  • 3
  • 15
  • To future readers: (1) `new Page();` is a hack; Normally one doesn't construct `Page` directly. It would be better to change this to a custom page (thus a subclass of `Page`) that says "Loading...", or similar. However, this won't display unless the next change is also made: (2) `Loader();` should be replaced with `MainThread.BeginInvokeOnMainThread(async () => await Loader());` This allows `App constructor` to return, before `Loader` does its work. Without this, app can't show anything until Loader finishes. Maybe should instead do this in `async OnStart` as Liyun shows. – ToolmakerSteve Apr 15 '23 at 19:54
  • I tried all approaches Liyun suggested. No matter what I tried `OnStart` always returned a blank page for me. I have the impression that `MainPage` must get initialized in the constructor. If not, initializing it anywhere else will not work. Of course it's not necessary to use `new Page()` for the initialization. Anything except `null` will work fine. – Costas Apr 16 '23 at 20:12
  • My more important point is that unless app developer does (2) above, whatever MainPage is set to cannot display anything; your answer locks a blank UI until the work is done. This happens because you call Loader() during constructor, instead of allowing constructor to return before work is done. Worst case, this might cause OS to kill app as failing to start properly within the permitted time. Best case, the user is not being given any feedback. This answer should not be used as is. Sorry if that sounds harsh, for what appears to be working for you. – ToolmakerSteve Apr 17 '23 at 00:37
  • 1
    Edited the answer according to your suggestion. – Costas Apr 17 '23 at 11:26