0

I've got a problem with my Xamarin App. The App uses a custom API to my website. I have not much experience in async/await methods.

The following code shows the App.xaml.cs:

public partial class App : Application
{
    public static bool IsUserLoggedIn { get; set; }
    public static UserModel currentLoggedInUser { get; set; }

    public static List<ActionTypeModel> listTypes;
    public static List<ActionSubTypeModel> listSubtypes;
    public static List<UserModel> listFriends;
    public static List<List<ActionModel>> listActiveActions;
    public static List<ActionModel> listLastAction;

    public App()
    {
        this.InitializeComponent();

        APIHelper.InitializeClient();

        StartApp().Wait();
    }

    private async Task StartApp()
    {
        //// DEBUG
        //MyAccountStorage.Logout();

        string username = await MyAccountStorage.GetUsername().ConfigureAwait(false);
        string password = await MyAccountStorage.GetPassword().ConfigureAwait(false);
        string user_id = await MyAccountStorage.GetId().ConfigureAwait(false);
        if (username != null && password != null)
        {

            currentLoggedInUser = new UserModel();
            if (user_id != null)
                currentLoggedInUser.user_id = Convert.ToInt32(user_id);
            currentLoggedInUser.username = username;
            currentLoggedInUser.password = password;

            bool isValid = false;
            isValid = await AreCredentialsCorrect(0, currentLoggedInUser.username, currentLoggedInUser.password).ConfigureAwait(false);
            if (isValid)
            {
                IsUserLoggedIn = true;

                await FillLists().ConfigureAwait(false);

                MainPage = new NavigationPage(await MyPage.BuildMyPage().ConfigureAwait(false));
            }
            else
            {
                IsUserLoggedIn = false;

                MainPage = new NavigationPage(await LoginPage.BuildLoginPage().ConfigureAwait(false));
            }

        }
        else
        {
            IsUserLoggedIn = false;

            MainPage = new NavigationPage(await LoginPage.BuildLoginPage().ConfigureAwait(false));
        }
    }

    private async Task FillLists()
    {
        listFriends = await DataControl.GetFriends(App.currentLoggedInUser.user_id, App.currentLoggedInUser.username, App.currentLoggedInUser.password).ConfigureAwait(false);
        if (listFriends == null)
            listFriends = new List<UserModel>();

        listTypes = await DataControl.GetTypes(App.currentLoggedInUser.username, App.currentLoggedInUser.password).ConfigureAwait(false);
        if (listTypes == null)
            listTypes = new List<ActionTypeModel>();

        listActiveActions = new List<List<ActionModel>>();

        for (int i = 0; i < listTypes.Count; i++)
            listActiveActions.Add(await DataControl.GetActiveActions(listTypes[i].action_type_id, currentLoggedInUser.user_id, currentLoggedInUser.username, currentLoggedInUser.password).ConfigureAwait(false));

        listSubtypes = await DataControl.GetSubtypes(App.currentLoggedInUser.username, App.currentLoggedInUser.password).ConfigureAwait(false);
        if (listSubtypes == null)
            listSubtypes = new List<ActionSubTypeModel>();

        listLastAction = await DataControl.GetLastAction(App.currentLoggedInUser.user_id, App.currentLoggedInUser.username, App.currentLoggedInUser.password).ConfigureAwait(false);
        if (listLastAction == null)
            listLastAction = new List<ActionModel>();
    }

    public static async Task<bool> AreCredentialsCorrect(int type, string user, string pass, string nick = "", string email = "")
    {
        List<UserModel> listUsers;

        if (type == 1)
            listUsers = await DataControl.CheckCredentials(1, user, pass, nick, email).ConfigureAwait(false);
        else
            listUsers = await DataControl.CheckCredentials(0, user, pass).ConfigureAwait(false);

        if (listUsers != null)
            if (listUsers.Any())
            {
                currentLoggedInUser = listUsers.First();
                currentLoggedInUser.password = pass;
                return true;
            }

        return false;
    }
}

I have the API in DataControl.cs:

public static async Task<List<UserModel>> CheckCredentials(int type, string username, string pass, string email = "", string nickname = "")
{
    string password = APIHelper.GetHashSha256(pass);

    string url = string.Empty;

    if (type == 0)
        url = APIHelper.ApiClient.BaseAddress + "/account/login.php?username=" + username + "&password=" + password;
    if (type == 1)
    {
        string nick = string.Empty;
        if (string.IsNullOrEmpty(nickname) == false)
            nick = "&nickname=" + nickname;

        url = APIHelper.ApiClient.BaseAddress + "/account/signup.php?username=" + username + "&password=" + password + "&email=" + email + nick;
    }
    if (string.IsNullOrEmpty(url))
        return null;

    using (HttpResponseMessage response = await APIHelper.ApiClient.GetAsync(url).ConfigureAwait(false))
    {
        if (response.IsSuccessStatusCode)
        {
            List<UserModel> listUsers = JsonConvert.DeserializeObject<List<UserModel>>(await response.Content.ReadAsStringAsync().ConfigureAwait(false));

            return listUsers;
        }
        else
            return null;
    }
}

That's one of the different async methods. When I leave out the ConfigureAwait(false), I run into a deadlock. When I add it to the code I run into an error.

Could you please help me.

AlKaramba
  • 9
  • 1
  • 2
    `When I leave out the "ConfigureAwait(false)" I run into a deadlock` - that strongly suggests you are [blocking on async](https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html) somewhere. Such as at `StartApp().Wait();` in your [constructor](https://stackoverflow.com/q/8145479/11683). Don't do this. – GSerg Feb 22 '20 at 19:49
  • The App constructor was the only position I used the Wait() method. Every other method is async and uses await to be called. – AlKaramba Feb 22 '20 at 20:01
  • Yes, one is enough. – GSerg Feb 22 '20 at 20:02
  • If I remove the Wait() the application is doing nothing and I see in the debug output an endless row of Thread pool startet and Thread pool finished... – AlKaramba Feb 22 '20 at 20:18
  • You can't use an async function from a constructor. You don't just need to remove `Wait` (which results in a [fire-and-forget](https://stackoverflow.com/q/46053175/11683)), you need to restructure your program so that you don't call an async method from a constructor. – GSerg Feb 22 '20 at 20:20
  • Thank you. I guess I have taken the first step in the right direction. However, now I get the error the following error `Android.Util.AndroidRuntimeException: Only the original thread that created a view hierarchy can touch its views.` I already read that it is because of the `ConfigureAwait(false)` at methods that are connected to UI objects. Could you please clarify if I am right? – AlKaramba Feb 22 '20 at 21:38
  • Yes, it is. `ConfigureAwait(false)` allows the continuation to continue on a random thread (that is, it allows it to not bother with resuming on the captured synchronization context, which for all practical purposes translates to resuming on a random thread). This is not an option in code that needs to keep affinity to a certain thread, such as code that manages UI. – GSerg Feb 23 '20 at 06:40

2 Answers2

0

As @GSerg already wrote, you have to restructure the code. The App constructor must set the MainPage to some page. That one can be empty saying something like "Loading data".

Then you can start a background Task which retrieves the data you need.

When the data has been loaded, then you can update your page with the newly loaded data. But UI updates always have to happen on the UI thread. This is the same on all platforms. So you have to switch back with Device.BeginInvokeOnMainThread(...).

public App()
{
    InitializeComponent();

    APIHelper.InitializeClient();

    MainPage = new LoadingPage();

    // start a background thread
    Task.Run(async () =>
    {
        try
        {
            await StartApp();  // load the data

            // go back to the main thread
            Device.BeginInvokeOnMainThread(() =>
            {
                // replace the MainPage with one which shows the loaded data
                MainPage = new DataPage();
            });
        }
        catch (Exception ex)
        {
            // handle the exception
        }
    });
}
Michael Rumpler
  • 305
  • 2
  • 17
-3

You can use Task.Run( //call your async method here );

So in your case:

 Task.Run(Startup);
javachipper
  • 519
  • 4
  • 15