1

Extremely frustrated with Shell Navigation, hope someone can help

Flow

  1. App loads and the Landing Page appears (this is the main page)
  2. The user taps the account icon and is routed to Login
  3. User enters their details and clicks the login button (i want to navigate back to Landing Page)

App Shell XAML

<ShellContent
   Title="LandingPage"
   ContentTemplate="{DataTemplate local:LandingPage}"
   Route="LandingPage" />
</Shell>

AppShell.xaml.cs

public AppShell()
{
    InitializeComponent();

    We dont register this route because its already defined in xaml
    // Routing.RegisterRoute(nameof(LandingPage), typeof(LandingPage));

    Routing.RegisterRoute(nameof(StoreItems), typeof(StoreItems));
    Routing.RegisterRoute(nameof(Register), typeof(Register));
    Routing.RegisterRoute(nameof(PasswordRecovery), typeof(PasswordRecovery));
    Routing.RegisterRoute(nameof(NewStoreRequest), typeof(NewStoreRequest));
    Routing.RegisterRoute(nameof(AccountInfo), typeof(AccountInfo));
    Routing.RegisterRoute(nameof(AllPromotions), typeof(AllPromotions));
    Routing.RegisterRoute(nameof(AllStores), typeof(AllStores));
    Routing.RegisterRoute(nameof(ChangePassword), typeof(ChangePassword));
    Routing.RegisterRoute(nameof(Checkout), typeof(Checkout));
    Routing.RegisterRoute(nameof(CreateAddress), typeof(CreateAddress));
    Routing.RegisterRoute(nameof(TrackOrder), typeof(TrackOrder));
    Routing.RegisterRoute(nameof(TrackOrderDetails), typeof(TrackOrderDetails));
    Routing.RegisterRoute(nameof(StoreBasket), typeof(StoreBasket));
    Routing.RegisterRoute(nameof(ProcessingPayment), typeof(ProcessingPayment));
    Routing.RegisterRoute(nameof(PaymentConfirmed), typeof(PaymentConfirmed));
    Routing.RegisterRoute(nameof(Login), typeof(Login));
}

MauiProgram.cs (UPDATED)

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureSyncfusionCore()
            .UseMauiCommunityToolkit()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("FontAwesome6BrandsRegular400.otf", "BrandsRegular");
                fonts.AddFont("FontAwesome6DuotoneSolid900.otf", "DuoToneSolid");
                fonts.AddFont("FontAwesome6ProLight300.otf", "ProLight");
                fonts.AddFont("FontAwesome6ProRegular.otf", "ProRegular");
                fonts.AddFont("FontAwesome6ProSolid900.otf", "ProSolid");
                fonts.AddFont("FontAwesome6ProThin100.otf", "ProThin");

                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

        // Services 
        builder.Services.AddSingleton<INippyDataService, NippyDataService>();

        // Views 
        builder.Services.AddTransient<LandingPage>();
        builder.Services.AddTransient<StoreItems>();
        builder.Services.AddSingleton<Login>();
        builder.Services.AddSingleton<Register>();
        builder.Services.AddSingleton<PasswordRecovery>();
        builder.Services.AddTransient<AccountInfo>();
        builder.Services.AddTransient<AllPromotions>();
        builder.Services.AddTransient<AllStores>();
        builder.Services.AddSingleton<ChangePassword>();
        builder.Services.AddTransient<Checkout>();
        builder.Services.AddTransient<CreateAddress>();
        builder.Services.AddSingleton<NewStoreRequest>();
        builder.Services.AddTransient<TrackOrder>();
        builder.Services.AddTransient<TrackOrderDetails>();
        builder.Services.AddTransient<StoreBasket>();
        builder.Services.AddTransient<ProcessingPayment>();
        builder.Services.AddTransient<PaymentConfirmed>();

        // View Models
        builder.Services.AddTransient<LandingPageVM>();
        builder.Services.AddTransient<StoreItemsVM>();
        builder.Services.AddSingleton<LoginVM>();
        builder.Services.AddSingleton<RegisterVM>();
        builder.Services.AddSingleton<PasswordRecoveryVM>();
        builder.Services.AddTransient<AccountInfoVM>();
        builder.Services.AddTransient<AllPromotionsVM>();
        builder.Services.AddTransient<AllStoresVM>();
        builder.Services.AddSingleton<ChangePasswordVM>();
        builder.Services.AddTransient<CheckoutVM>();
        builder.Services.AddTransient<CreateAddressVM>();
        builder.Services.AddSingleton<NewStoreRequestVM>();
        builder.Services.AddTransient<TrackOrderVM>();
        builder.Services.AddTransient<TrackOrderDetailsVM>();
        builder.Services.AddTransient<StoreBasketVM>();
        builder.Services.AddTransient<ProcessingPaymentVM>();
        builder.Services.AddTransient<PaymentConfirmedVM>();

#if DEBUG
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}

Ive tried these

await Shell.Current.GoToAsync("..", true);  (Hangs and eventually times out)
await Shell.Current.GoToAsync(nameof(LandingPage), true); (Relative routing error try ..// etc)

Crazy thing, back from a different page

LandingPage -> AllStores

await Shell.Current.GoToAsync("..", true);

Its takes me to CreateAddress???

** Updated ** (Account button click code that takes the user to login)

 [RelayCommand]
        async Task BtnAccount()
        {

            if (CurrentUser != null)
            {
                await Shell.Current.GoToAsync(nameof(AccountInfo), true);
            }
            else
            {
                bool result = await Application.Current.MainPage.DisplayAlert("Account Required", $"You must be logged in to view Account details, do you wish to login now?", "Yes", "No");
                if (result)
                {
                    await Shell.Current.GoToAsync(nameof(Login), true);
                }
            }
        }

Login Button click

 [RelayCommand]
        async Task Login()
        {

            if (IsBusy)
                return;

            try
            {
                IsBusy = true;

                if (ValidateFormData())
                {
                    User user = await _dataService.LoginUserAsync(Email, Password);
                    if (user != null)
                    {
                        user.Order.Charity = await _dataService.GetCharityAsync();

                        if (user.ContactDetails.Email != null)
                        {
                            SharedSettings.SetCurrentUser(user);
                            if (!user.ChangePassword)
                            {
                                try
                                {
                                    var p = Shell.Current.Navigation.NavigationStack;
                                    await Shell.Current.GoToAsync(nameof(LandingPage), true);
                                }
                                catch (Exception ex) 
                                {
                                    var p = ex.Message;
                                }
                             

                            }
                            else
                            {
                                await Shell.Current.GoToAsync(nameof(ChangePassword), true);
                            }
                        }
                        else
                        {
                            await Shell.Current.DisplayAlert("Error!", "Login Failed", "OK");
                        }
                    }
                    else
                    {
                        await Shell.Current.DisplayAlert("Error!", $"Login Failed, account not found", "OK");
                    }

                    
                }
                else
                {
                    await Shell.Current.DisplayAlert("Error!", ErrorMessage, "OK");
                }

            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
                await Shell.Current.DisplayAlert("Error!", $"Service is not responding, please try again later", "OK");
            }
            finally
            {
                IsBusy = false;
            }
     
           
        }

UPDATED The navigation stack in postion 0 is null and position 1 is the login page. I have change the navigation to use nameof(LandingPage) to create a new instance of the page instead of the ".." (pop) method however im still getting the hang and time out

  • The problem is probably not the return page per se, but how you got to the Login page. Could you please supply code for the complete procedure. To check the stack use `Shell.Current.Navigation.NavigationStack` in the immediate window when you have stopped on a breakpoint. Your pages should be visable in the stack. – Peter Wessberg Jul 31 '23 at 16:43
  • @PeterWessberg is simply use the following within a RelayCommand await Shell.Current.GoToAsync(nameof(Login), true); – Kevin Deery Jul 31 '23 at 21:13
  • I see that your loginview is singleton and your loginvm is transient. Also landingpagevm is transient. Shorter lifespan could be a problem when using navigation. Could you test to at least have the same lifespan on those? – Peter Wessberg Jul 31 '23 at 21:35
  • @PeterWessberg i made those changes with high hopes but same issue. The Navigation stack has a null reference to position 0. Assuming this is related to transient declaration. I update the code on the login button to call using nameof(LandingPage) but im experiencing the same issue. – Kevin Deery Jul 31 '23 at 22:33
  • Sorry to hear. The [null] you see is the root, so that is as it should ( not really but it is a known bug). If you test to go to the landing page and to other pages you will see the list with all the pages you have traversed. I have looked through your code and find nothing strange with it. Have you tried to go back using ///landingpage not using nameof. – Peter Wessberg Jul 31 '23 at 22:43
  • Does it still hang if you comment out everything in method `Login`, replace it with `await Shell.Current.GoToAsync($"//{nameof(LandingPage)"}, true);`? Testing two changes: 1) `//pagename`, 2) eliminating all the other logic in that method. – ToolmakerSteve Aug 01 '23 at 00:56
  • If you get too frustrated with shell, see [Sutt's answer here](https://stackoverflow.com/a/76789114/199364). This is a nice example of directly setting the page to go to. See also my answer there, specifically its mention of `NavigationPage` alternative. – ToolmakerSteve Aug 01 '23 at 01:47

3 Answers3

0
  1. Make all pages transient.

  2. You can navigate to your ShellContents, and keep them the only element on the stack, you cannot do this to routes however.

This is a problem in your case, 9/10 you will want login to be the only page in the stack. And then again, the shell main page, would be naturally another root.

So you need them both registered at the same level. (The top most is the entry)

Fix this, if you still have problems, ask.

Edit: To clarify a bit. You will want to jump between with "//MainPage" , "//LoginPage" to clear the stack.

H.A.H.
  • 2,104
  • 1
  • 8
  • 21
  • Ive made everything Transient, commented out the login code and left only the navigation. Ive tried ". ." and nameof(LandingPage) When i attempt to navigate, i get this "Relative routing to shell elements is currently not supported. Try prefixing your uri with ///: ///LandingPage" – Kevin Deery Aug 01 '23 at 11:57
  • @KevinDeery not that I do not trust you, but I want to see your shell xaml and cs. – H.A.H. Aug 02 '23 at 06:04
  • Not sure what happened here, i created an Answer to say the issue was not related to navigation but a rogue http client request on the landing page when a user is logged in. It was causing a deadlock situation – Kevin Deery Aug 03 '23 at 08:43
0

Turns out the Create Address page was corrupted somehow. Navigating to a different page than Login and attempting to navigate back using GoToAsync("..",true) kept taking me to Create Address.

I looked over everything on this page, including the code behind and View Model and i couldnt fault it. Once it was removed (Backup and ill create it again), i was able to Navigate from login back to the LandingPage using GoToAsync("..",true)

0

Problem solved

The issue is not with navigation rather a deadlock situation when the LandingPage is loading after a user logs in. The below http method caused the hang.

Adding .ConfigureAwait(false) to the call resolved it. Info: https://devblogs.microsoft.com/dotnet/configureawait-faq/

    public async Task<List<Address>> GetUserAddressesAsync(string email)
    {
        List<Address> data = new List<Address>();
        if (WebHelper.HasInternetConnection())
        {
            try
            {
                HttpResponseMessage response = await _httpClient.GetAsync($"{_api}/address/GetUserAddresses.php?email={email}");
                string json = await response.Content.ReadAsStringAsync();
                data = JsonConvert.DeserializeObject<List<Address>>(json);
            }
            catch (Exception ex)
            {
                Debug.WriteLine("----> " + ex.Message);
            }
        }

        return data;
    } 

Thanks everyone for the help, 3 days of pain to get moving again.