4
votes

The project I am working on contains the following structure:

When app is launched, user sees a Welcome page. At that point user has two options. They can either login or register. If logged in == true; then go to master detail page. Or in registration, if register == success then go to login page and follow the same process and end up in the master detail page.

                -> Login Page                  ||
Welcome Page >> ==================             || => MasterDetailPage
                -> Register Page -> Login page ||

I am using MVVM Light to handle my navigation stack via INavigationService as my UI and business logic is separated via MVVM. Everything works pretty good except for I need to reset the navigation stack so the user will not be able to access any page before the "MasterDetailPage" showed above. Right now users can go back to login or registration or whatever page they were before, by using the hardware back button on Android or swiping from the left edge on iOS. Plus, There is a navigation back button on top navigation bar anyway.navigation bar example

My App.cs looks something like this

public App()
{
    var nav = RegisterNavigationService();
    SimpleIoc.Default.Register<INavigationService>(() => nav);

    InitializeComponent();

    var initialPage = new NavigationPage(new WelcomePage());
    nav.Initialize(initialPage);
    MainPage = initialPage;
}

private NavigationService RegisterNavigationService()
{
    var nav = new NavigationService();
    nav.Configure(Locator.LoginForm, typeof(LoginForm));
    nav.Configure(Locator.RegisterSuccessPage, typeof(RegisterSuccessPage));
    nav.Configure(Locator.RegistrationForm, typeof(RegistrationForm));
    nav.Configure(Locator.WelcomePage, typeof(WelcomePage));
    nav.Configure(Locator.MasterMainPage, typeof(MasterMainPage));
    return nav;
 }

On my view models, I handle the navigation commands like this:

public class LoginFormViewModel : BaseViewModel
{
    private readonly INavigationService _navigationService;
    public Command NavigateToMainPage { get; }

    public LoginFormViewModel(INavigationService navigationService)
    {
        _navigationService = navigationService ?? throw new ArgumentNullException("navigationService");

        NavigateToMainPage = new Command(() => NavigateToMainApp());
    }

    private void NavigateToMainApp()
    {
        _navigationService.NavigateTo(Locator.MasterMainPage);
    }
}

Finally, my NavigationService.cs looks like this... I barely touched this part of the code... The only thing I tried is the 'ClearNavigationStack' method but that was a failure.

public class NavigationService : INavigationService, INavigationServiceExtensions
{
    private Dictionary<string, Type> _pagesByKey = new Dictionary<string, Type>();
    private NavigationPage _navigation;

    public string CurrentPageKey
    {
        get
        {
            lock (_pagesByKey)
            {
                if (_navigation.CurrentPage == null)
                {
                    return null;
                }

                var pageType = _navigation.CurrentPage.GetType();

                return _pagesByKey.ContainsValue(pageType)
                    ? _pagesByKey.First(p => p.Value == pageType).Key
                    : null;
            }
        }
    }

    public void GoBack()
    {
        _navigation.PopAsync();
    }

    public void NavigateTo(string pageKey)
    {
        NavigateTo(pageKey, null);
    }

    public void NavigateTo(string pageKey, object parameter)
    {
        lock (_pagesByKey)
        {
            if (_pagesByKey.ContainsKey(pageKey))
            {
                ConstructorInfo constructor;
                object[] parameters;
                var type = _pagesByKey[pageKey];

                if (parameter == null)
                {
                    constructor = type.GetTypeInfo()
                        .DeclaredConstructors
                        .FirstOrDefault(c => !c.GetParameters().Any());

                    parameters = new object[] { };
                }
                else
                {
                    constructor = type.GetTypeInfo()
                        .DeclaredConstructors
                        .FirstOrDefault(
                            c =>
                            {
                                var p = c.GetParameters();
                                return p.Count() == 1
                                       && p[0].ParameterType == parameter.GetType();
                            });

                    parameters = new[] { parameter };
                }

                if (constructor == null)
                {
                    throw new InvalidOperationException("No suitable constructor found for page " + pageKey);
                }

                var page = constructor.Invoke(parameters) as Page;
                _navigation.PushAsync(page);
            }
            else
            {
                throw new ArgumentException(
                    string.Format("No such page: {0}. Did you forget to call NavigationService.Configure?", pageKey), "pageKey");
            }
        }
    }

    public void Configure(string pageKey, Type pageType)
    {
        lock (_pagesByKey)
        {
            if (_pagesByKey.ContainsKey(pageKey))
            {
                _pagesByKey[pageKey] = pageType;
            }
            else
            {
                _pagesByKey.Add(pageKey, pageType);
            }
        }
    }


    public void ClearNavigationStack()
    {
        lock (_pagesByKey)
        {
            foreach (var pageKey in _pagesByKey.Keys)
            {
                _pagesByKey.Remove(pageKey);
            }
        }   
    }

    public void Initialize(NavigationPage navigation)
    {
        _navigation = navigation;
    }
}

I've taken this bit from the following git repo: https://github.com/mallibone/MvvmLightNavigation.XamarinForms

by following this tutorial: https://mallibone.com/post/xamarin.forms-navigation-with-mvvm-light

Note: It is a PCL.

Any suggestion is welcome as I've been on this for the last 2 days.

EDIT: Just now, I've managed to "hide" the nav stack by setting my MainPage to something like this

App.Current.MainPage = new MasterMainPage();

But it seems like a code smell and looks like a horrific hack. Plus I am not too sure if it "violates" the concepts I am following... And I guess this navigation stack will never be gone anyway as I will do other navigation stacks inside the master detail pages.

1

1 Answers

2
votes

From your picture I see that you have Master/Detaied page inside Navigation page. Xamarin doesn't recommend to do that. I don't know how you are going to do it in MVVM Light but in regular Forms you have couple options to achieve what you want:

  1. If you ever need to go back to your Login or register page you should use

    await Navigation.PushModalAsync(new YourMasterDetailPage());
    

Then you can popmodal to get back to them BUT in this case Hardware button will still bring you to Login. You can use part of method 2 to clear stack after you navigated to you master-detail page but be careful - you cannot remove a page from stack if it is root and currently displayed page, so you will need to clear regular navigation stack only after login page is not displayed. I wouldn't recommend that option as "Modal views are often temporary and brought on screen only long enough for the user to complete a task."

http://blog.adamkemp.com/2014/09/navigation-in-xamarinforms_2.html

  1. If you don't need to go back you can use the follow to clear Navigation stack, it will also remove Back button

    await Navigation.PushAsync(new YourMasterPage());
    var pages = Navigation.NavigationStack.ToList();
    foreach (var page in pages)
    {
        if (page.GetType() != typeof(YourMasterPage))
            Navigation.RemovePage(page);
    }