Do something when async task finishes

robs23robs23 ✭✭Member ✭✭

Hi,

In my App initialization I have asynchronous call:

string output = "";
Task.Run(async () => { output = await ds.ReloadUsers(); });

After it finishes, output is equal to JSON serialized data which is as follows:

    [
        {
            "UserId": 4,
            "Name": "Krzysztof",
            "Surname": "Jeżyna",
            "Password": "jdepass123",
            "isMechanic": true,
            "TenantId": 1,
            "TenantName": "JDE_PL",
            "CreatedOn": "2018-04-18T07:00:00",
            "CreatedBy": 1,
            "CreatedByName": "Robert Roszak",
            "LastLoggedOn": null
        },
        {
            "UserId": 6,
            "Name": "Tomasz",
            "Surname": "Gorkowski",
            "Password": "jdepass123",
            "isMechanic": true,
            "TenantId": 1,
            "TenantName": "JDE_PL",
            "CreatedOn": "2018-04-20T11:06:47.413",
            "CreatedBy": 1,
            "CreatedByName": "Robert Roszak",
            "LastLoggedOn": null
        }
    ]

Now I must deserialize output to List, like:

List<User> Users = new List<User>();
Users = JsonConvert.DeserializeObject<List<User>>(output);

However, I don't know where to put it so it's triggered only when asynchronously called Task.Run finishes. I thought that maybe I could put it within the call, but it doesn't work:

Task.Run(async () => { output = await ds.ReloadUsers();
                Users = JsonConvert.DeserializeObject<List<User>>(output);
            });

So maybe there's some event I could use to deserialize output only when Task.Run finishes? How do I go about it?

Best Answer

Answers

  • Gigex42Gigex42 ✭✭✭✭ USMember ✭✭✭✭

    Where exactly do you call your async Task?

    Normally you would await it and handle the result then.
    If you call it on your constructor you'll have to think about another way.

    Maybe load your data on the OnAppearing() Method which you can override and make it async.

  • JohnHardmanJohnHardman mod GBUniversity mod
    edited April 2018

    @robs23 -

    Option #1: use ContinueWith:

    Task.Run(async () => 
    { 
        output = await ds.ReloadUsers(); 
    })
    .ContinueWith(t => 
    {
        // put code here 
    });
    

    Option #2: inside the async block.

    Task.Run(async () => 
    { 
        output = await ds.ReloadUsers(); 
        // put code here 
    });
    

    When you said "I thought that maybe I could put it within the call, but it doesn't work", what does "it doesn't work" mean - what happens and what do you expect to happen? It's possible that you might need to explicitly make work happen on a particular thread, but cannot be sure without more detail.

  • robs23robs23 ✭✭ Member ✭✭

    @Gigex42 Thanks for answer. At the moment it is:

          List<User> Users;
        DataService ds;
    
        public App()
            {
                ds = new DataService();
                Users = new List<User>();
                string output = "";
                Task.Run(async () => { output = await ds.ReloadUsers(); });
    
                StackLayout layout = new StackLayout { VerticalOptions = LayoutOptions.Center };
                Picker pick = new Picker { HorizontalOptions = LayoutOptions.Center, Title = "Wybierz użytkownika" };
                layout.Children.Add(pick);
                var content = new ContentPage
                {
                    Title = "Zgłoszenia serwisowe",
                    Content = layout
                };
    
                MainPage = new NavigationPage(content);
            }
    
            protected override void OnStart()
            {
                // Handle when your app starts
            }
    
            protected override void OnSleep()
            {
                // Handle when your app sleeps
            }
    
            protected override void OnResume()
            {
                // Handle when your app resumes
            }
        }
    

    I understand it should be more-less as below, right? I'm using OnStart() as I don't see OnAppearing():

    List<User> Users;
            DataService ds;
    
            public App()
                {
                    ds = new DataService();
                    Users = new List<User>();
                    string output = "";
                    StackLayout layout = new StackLayout { VerticalOptions = LayoutOptions.Center };
                    Picker pick = new Picker { HorizontalOptions = LayoutOptions.Center, Title = "Wybierz użytkownika" };
                    layout.Children.Add(pick);
    
                    var content = new ContentPage
                    {
                        Title = "Zgłoszenia serwisowe",
                        Content = layout
                    };
    
                    MainPage = new NavigationPage(content);
                }
    
                protected override async void OnStart()
                {
                    // Handle when your app starts
    
                    Task.Run(async () => { output = await ds.ReloadUsers(); });
                    Users = JsonConvert.DeserializeObject<List<User>>(output);
                }
    
                protected override void OnSleep()
                {
                    // Handle when your app sleeps
                }
    
                protected override void OnResume()
                {
                    // Handle when your app resumes
                }
            }
    

    As I want Users to be source of picker, I should change onStart() to:

    protected override async void OnStart()
                    {
                        // Handle when your app starts
    
                        Task.Run(async () => { output = await ds.ReloadUsers(); });
                        Users = JsonConvert.DeserializeObject<List<User>>(output);
                        foreach(User user in Users){
                            pick.Items.Add(User.Name + " " + User.Surname);
                        }
                    }
    

    Right?

  • JohnHardmanJohnHardman mod GBUniversity mod
    edited April 2018

    @robs23 - As your app grows, you will want to restructure your existing code.

    For each type of page in your app, create a class that inherits from ContentPage. Move the code that builds the UI for each page into the constructor or OnAppearing method of your page-specific class. So, in your code above, this means moving the StackLayout, Picker, Title and Content code.

    Depending on what ReloadUsers does, you may need to use Device.BeginInvokeOnMainThread (Google this for many, many examples).

    Avoid doing async UI work in OnStart - doing async UI work there creates a race condition. I've not seen a problem as a result on Android or iOS, but it can cause a problem on UWP (I raised a bug report for this a while back).

  • Gigex42Gigex42 ✭✭✭✭ USMember ✭✭✭✭

    Only load your data where you need them. So create your mainpage in visual studio -> New Item -> Content Page and set this as your MainPage.
    MainPage = new NavigationPage(YOURCREATEDPAGE());

    Now on your new page you have the onappearing method. Here you can load your data:
    output = await ds.ReloadUsers();

  • robs23robs23 ✭✭ Member ✭✭

    Ok, so I've restructured my app. Now my app constructor is simple and just calls for LoginPage:

    public App()
            {
                MainPage = new NavigationPage(new LoginPage());
            }
    

    And the login page is as follows:

    public class LoginPage: ContentPage
        {
            Button btnLogin;
            Picker pick;
            Label lbl;
            Entry entry;
            DataService ds;
            UsersKeeper keeper;
    
            public LoginPage() : base()
            {
                keeper = new UsersKeeper();
                ds = new DataService();
                btnLogin = new Button() { Text = "Zaloguj" };
                pick = new Picker { HorizontalOptions = LayoutOptions.Center, Title = "Wybierz użytkownika" };
                lbl = new Label { HorizontalTextAlignment = TextAlignment.Center, Text = "Wybierz użytkownika i podaj hasło" };
                entry = new Entry { HorizontalTextAlignment = TextAlignment.Center, Placeholder = "Hasło", IsPassword = true };
    
                btnLogin.Clicked += async (sender, e) =>
                {
                    await Application.Current.MainPage.Navigation.PushAsync(new ScanPage("xyz"));
                };
    
                StackLayout layout = new StackLayout { VerticalOptions = LayoutOptions.Center };
                layout.Children.Add(lbl);
                layout.Children.Add(pick);
                layout.Children.Add(entry);
                layout.Children.Add(btnLogin);
                Title = "Logowanie";
                Content = layout;
    
            }
    
            protected override async void OnAppearing()
            {
                base.OnAppearing();
                string output;
                output = await ds.ReloadUsers();
                keeper.Users = JsonConvert.DeserializeObject<List<User>>(output);
                foreach(User user in keeper.Users)
                {
                    pick.Items.Add(user.Name);
                }
            }
    
        }
    

    I was able to rebuild the app without any error. I've deployed it to my phone and ran it, the UI looks like before but the app crashes after 1-2 seconds. Could you have a look at my OnAppearing() event? Is there something obviously wrong?

  • robs23robs23 ✭✭ Member ✭✭

    Just verified this part works correclty:

    protected override async void OnAppearing()
            {
                base.OnAppearing();
                string output;
                output = await ds.ReloadUsers()
            }
    

    How can I verify if deserailization worked well? I put it in try - catch statement but it crashes somewhere after output = await ds.ReloadUsers(); without any message:

     protected override async void OnAppearing()
            {
                base.OnAppearing();
                try
                {
                    output = await ds.ReloadUsers();
                    keeper.Users = JsonConvert.DeserializeObject<List<User>>(output);
                    await DisplayAlert("Ilość użytkowników", keeper.Users.Count.ToString(), "OK");
                    if (keeper.Users.Any())
                    {
                        foreach (User user in keeper.Users)
                        {
                            pick.Items.Add(user.Name);
                        }
                    }
                }catch
                {
                    throw;
                }
    
            }
    

    Can I place some MessageBox type event in OnAppearing() to display e.g. keeper.Users.Count.ToString() ?

  • ChaseFlorellChaseFlorell mod CAInsider, University mod
    edited April 2018

    put your dialog in the catch, and don't rethrow

    try
    {
       await doBreakingWork();
        await DisplayAlert("Ilość użytkowników", keeper.Users.Count.ToString(), "OK");
    } catch
    {
        await DisplayAlert("Ładowanie nie powiodło się", "OK");
    }
    
  • JohnHardmanJohnHardman mod GBUniversity mod

    @robs23 - It would help to know what exception is being thrown and why. Change your "catch" to "catch (Exception ex)" and output (either via DisplayAlert or System.Diagnostics.Debug.WriteLine) ex.ToString().

  • Gigex42Gigex42 ✭✭✭✭ USMember ✭✭✭✭

    Seems you got some error when Deserializing.
    Like @JohnHardman said change your catch and tell us the error.

    try
    {
    
    } 
    catch (Exception ex)
    {
    string error = ex.message;
    }
    
  • robs23robs23 ✭✭ Member ✭✭

    Hi Guys,

    Thanks for your support! I caught the error and here are some details:

    • Message: Exception has been thrown by the target of an invocation
    • InnerMessage:{System.MissingMethodException}
    • InnerMessage.Message:Constructor on type 'System.Net.Http.HttpClient' not found.
    • StackTrace: at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAt…

    Hopefully they tell you more than they are telling me..

  • JohnHardmanJohnHardman mod GBUniversity mod
    edited April 2018

    @Robs23 - Check that you have a reference to System.Net.Http (and probably System.Net.Http.Extensions and System.Net.Http.Primitives too). If you do have those references, try disabling linking in the project properties (if linking is identified as the problem, you can re-enable it but then explicitly disable it for System.Net.Http)

  • robs23robs23 ✭✭ Member ✭✭

    @JohnHardman - Thanks. I'm not sure if you meant main forms project's references or droid project's references, so I'm attaching screenshot of both. From the references you mentioned, I can see forms project is missing System.Net.Http (but it has both System.Net.Http.Extensions and System.Net.Http.Primitives). Should I add System.Net.Http then?
    As to linking - I understand I should set Linking in Linker properties of my droid project to "none", right? Currently they're set to "SDK and User Assemblies"

  • JohnHardmanJohnHardman mod GBUniversity mod

    @robs23 said:
    @JohnHardman - Thanks. I'm not sure if you meant main forms project's references or droid project's references, so I'm attaching screenshot of both. From the references you mentioned, I can see forms project is missing System.Net.Http (but it has both System.Net.Http.Extensions and System.Net.Http.Primitives). Should I add System.Net.Http then?

    There's no harm in doing so.

    As to linking - I understand I should set Linking in Linker properties of my droid project to "none", right? Currently they're set to "SDK and User Assemblies"

    Yes, set it to none to help identify the problem. This is the most likely cause.

  • robs23robs23 ✭✭ Member ✭✭

    @JohnHardman said:
    There's no harm in doing so.

    Having checked in NuGet and System.Net.Http is installed for all 3 projects (forms, droid, iOS).

    I set Linking to none, rebuilt the solutions (no errors) and ran - same error as before with same details.

  • robs23robs23 ✭✭ Member ✭✭

    @JohnHardman said:
    There's no harm in doing so.

    Having checked in NuGet and System.Net.Http is installed for all 3 projects (forms, droid, iOS).

    I set Linking to none, rebuilt the solutions (no errors) and ran - same error as before with same details.

  • JohnHardmanJohnHardman mod GBUniversity mod

    @robs23 - I'm surprised by that. The exception details

    InnerMessage:{System.MissingMethodException}
    InnerMessage.Message:Constructor on type 'System.Net.Http.HttpClient' not found.
    StackTrace: at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAt…
    

    all point to reflection being used to find the HttpClient constructor, but failing to find it. Linker settings or missing references would be the usual cause. I'll have a think whilst walking the dog...

  • robs23robs23 ✭✭ Member ✭✭

    I deleted everything from solution's packages folder and projects' bin and obj folders. Checked if I was using shared runtime but I wasn't. I rebuilt the app, no errors. Archived android project and deployed to the phone. Installed it and ran it without much faith in success and bang everything works as expected! :smiley:
    Now I don't know what made it work - setting Linking to None or deleting garbage from bin & obj.. What I noticed, though, it's 4 times bigger weight of the APK. I guess it's must be because of Linking = None as this is what really changed from previous built. I think it should get back to normal size when I set it back to Linking = SDK and user assemblies, right?
    Anyway, thank you for walking me through here, I definitely appreciate this! And for future I must remember to clear bin and obj before each building, it's been 2nd time old bins got me into trouble with Xamarin..

    Robert

Sign In or Register to comment.