Prism: [XF] Add ability to Remove pages from the navigation stack

Created on 27 Jan 2017  ·  55Comments  ·  Source: PrismLibrary/Prism

A common scenario in mobile app is having the ability to remove a page that exists somewhere in the navigation stack. This is not a navigation operation, but really a targeted way of removing a very specific page.

Example:
Initially navigate the app:
MasterDetailPage/NavigationPage/ProjectsListPage
Navigate to create a new project:
MasterDetailPage/NavigationPage/ProjectsListPage/CreateProjectPage
when finished with the create page navigate to a session page, and remove the create page:
MasterDetailPage/NavigationPage/ProjectsListPage/SessionsListPage

There are two options to achieve this:

  • Remove(string name) - you provide the registered name of the page to be removed. This can be very tricky to implement and error prone since there are multiple navigation stacks to look through to find the page. Especially when you have more than one instance of the same page in the navigation stack.
  • Remove(int index) - you provide the index of the page to remove. This is safer, but places the responsibility of knowing exactly which page needs to be removed on the developer. Since there are multiple navigation stacks, the Remove method may have to have an optional parameter to tell the service which stack to look in to remove the page.

New Ideas from conversation below:

  • IDestroyAfterNavigation - since the main scenario for removing a page is normally to remove the page that is prior to the last page in the stack, we can add an interface which will indicate that the page is to be removed immediately after navigation to the new navigation target has completed.
  • Support NavigateAsync("../Name") - stick with a uri based approach and add support for navigating backwards while appending a view.

Thoughts?

XF

Most helpful comment

I like the interface approach for the same reason you are mentioning, not touching the current signature.
Although it would make discoverability of this new feature not easy, so there is also something positive about adding an extra overload for the NavigateAsync method.

But adding a Remove method ( like you first suggested ), sound not ok for me :)

Now wait what others have to say...

All 55 comments

Just to be sure, is there any scenario that a direct remove will be necessary? ( leaving out the marginal uses )
Aren't we actually looking for a scenario that we want forward navigation and have the ability to remove the 'previous' page? If so, can't we indicate this with the Navigate method with a bool or something? ( not sure the navigation service knows what the previous is in the stack - but I would assume it does )

@Depechie I have not yet seen a scenario where you were deep in a navigation stack and wanted to suddenly remove a page in the very start of the stack.. It has always been the page prior to the last navigation action.

Another example is a Login. As user logs in, the app navigates to another page, and now the login page should be removed. Right now, an absolute URI is perfect for login, but if the login happens somewhere else in the app, and not at app startup, then this remove would be helpful.

What do you think the API would look like. Add a bool "removePreviousPage" argument to the NavigateAsync signature?

NavigateAsync(string uri, bool useModalNaviagtion, bool animated, bool removePreviousPage)

I am hesitant to add another signature to the navigate method. I would almost prefer an interface over a change in signature. Something like IDestroyAfterNavigation which simply implements IDestructible, but is used as a marker to remove the page when the target page has been navigated to.

I like the interface approach for the same reason you are mentioning, not touching the current signature.
Although it would make discoverability of this new feature not easy, so there is also something positive about adding an extra overload for the NavigateAsync method.

But adding a Remove method ( like you first suggested ), sound not ok for me :)

Now wait what others have to say...

The use cases I've has are always creating for a termporary page that behaves similar to a popup dialogue.

An overload for NavigateAsync would fall down when the temporary page itself is the one performing the navigation. Consider navigating to your Login page, but then pressing a 'Navigate back' button, rather than a 'Cancel' button. The handler for 'Back' would need to know about temporary pages in order to call the overload. You are going to end up having to implement an interface.

I like the interface solution. It wouldn't the first feature that is a little hard to find but a good solid read of the docs always worked for me. IRegionMemberLifetime is very similar to what you might want.

Just thinking out loud here...

NavigateAsync(string uri, bool useModalNaviagtion, bool animated, bool removePreviousPage)
I would say, is probably the wrong way to go about accomplishing what we'd like to see.

Remove(string name) & Remove(int index) I believe both present a design challenge for the NavigationService in it's current state, as it would need to maintain some concept of the navigation stack (which has been asked for periodically for a while). Also for Remove(string name) we would need to establish what the logic would be for something like MainPage/ViewA/ViewB/ViewA which instance of ViewA do you pop?

I do however believe with relative ease we could maintain Uri based navigation with the desired effect. We certainly see this all the time with web design NavigateAsync("../ViewB")

@dansiegel that's an interesting approach. I'm wondering if this would cause confusion between GoBack() or not. Would we also support something like "../../../ViewB"? I can see where that can start to get very difficult to manage. Defintiely worth something worth looking into.

@brianlagunas I would say that anytime the Navigation Service encounters a segment "..", then it would pop a page. As terrible as it would be technically "../ViewA/../ViewB" should be supported allowable....

@dansiegel the difficulty lies in knowing if to PopModalAsync or PopAsync. This would provide a little extra flexibility though. It would probably remove a lot of cases where you have to reset the navigation stack with an absolute URI and rebuild everything.

To define it's function; every "../" encountered would represent a Pop, and then if a view is provided "../ViewName" the view would be created and added to the stack?

@dansiegel also, how would one control if the pop would be modal or just async?

Edit: would we use the existing useModalNavigation parameter? If so, the pops would be one or the other and we will not be able to control them in more complex scenarios.

Perhaps we do need to add a Navigation stack that could help us for the crazy scenarios. We might add something like the following code, where INavigationStack is a singleton registered with the Container.

public interface INavigationStack
{
    List<NavigationStackPage> NavigationStack { get; }
}

public class NavigationStackPage
{
    public string Name { get; set; }
    public bool IsModal { get; set; }
    public Type Type { get; set; }
}

If the main scenario is to remove only the last page, we might implement it similar to Prism UWP?
https://github.com/PrismLibrary/Prism/blob/master/Source/Windows10/Prism.Windows/Navigation/FrameNavigationService.cs#L125

I've used it before to create custom navigation flows and wizards (so the backstack stays clean). Example on this blog post.

why couldn't these be properties of the NavigationParameters. It seems like we are going to far with adding new interfaces or adding parameters to methods. Wouldn't there be value in NavigateAsyn("ViewA?isModal=true&isAnimated&removePreviousPage=true") and then just adding the explict propeties to NavigationParameters?

@powerdude this is something we are also looking at. We actually are going to make a breaking change in #922 to support this. Though, we cannot add any properties to the NavigationParameetrs class, rather they would have to be extension methods. NavigationParameetrs will be shared across multiple platforms, and some known parameters will not be used on other platforms.

So I have something working with the "../" syntax, but there is a side effect that may not be desirable. Lets say the navigation stack looks like this:

"ViewA/ViewB/ViewC"

Now I navigate using:

NavigateAsync("../../ViewD");

There will be a quick flash of ViewA and then ViewD will be shown. This is because when the previous views are removed, ViewA is made the current page and then the navigation to ViewD occurs. I'm not sure this is acceptable or not.

Also, should the INavigationAware methods be called? I am leaning towards no, since we are not trying to navigate to or from them, but rather remove them completely. I can see calling IDestructible on them

Okay, so I have some good news and bad news.

Good news, I have a working prototype of using "../NewView" to remove pages from the navigation stack without having the flash issue I mentioned above.

Bad news, this can only be supported for NavigationPages and will not work for anything modal. This is because the XF INavigation.RemovePage method is only supported for NavigationPages.

Is this acceptable, or is supporting modal pages also required?

My general thinking is that "../NewView" shouldn't care if the ".." is a modal, wrapped in a NavigationPage, etc. If we're adding RemovePage it would be nice but I would expect something like having a stack "NavigationPage/PageA/PageB/PageC" RemovePage("PageB")

Unfortunately, the only way to achieve support for both modal and non-modal scenarios is to pop the pages off the stack and then add the new pages onto the stack. This causes a quick visual flash of the previous page, which is undesirable.

XF does not have a method that can remove pages off of a modal stack. They must be popped.

So it's either support both and have this weird flash, or only support NavigationPages and have no flash.

It would be better not to have the flash. You can work around not using modal pages in the remove pages scenarios.

could this be something that we could introduce an interface to control which is more important? I see how a number of developers would agree with @snowppy but I would say there are also a number of developers and apps that would require the ability to pop Modal's even given the page flashing (at least until we can work with Xamarin to find a better alternative).

public interface INavigationServiceOptions
{
    bool PreferNavigationPageRemoval { get; }
} 

I'm sure we could get a better name...

I have a slight variation on this that, maybe I could implement differently, but this is how I have it working right now.

I start on a components page and the user can tap a component, then drill down through multiple levels (variable) of produt group, family, etc, etc, arriving at a final selection page where they make their choice (in my case it is a process/thickness but you can imagine it as S, M, L, XL)

At each level of the heirarchy I have navigated to a "ProductPage" so my navigation stack can look like:

navpage/componentspage/productpage/detailpage

or

navpage/componentspage/productpage/productpage/productpage/detailpage

which is really great for navigating back but once they make a final selection I don't know how far down the stack I am so I navigate absolute to navpage/componentspage

Which is not ideal as it rebuilds that page... What would be great is to be able to create a "bookmark" (or something similar) and then GoBack to the bookmark, having Prism pop the pages off the stack as required.

Hope that makes sense.

This is like Android's FLAG_ACTIVITY_NEW_TASK and FLAG_ACTIVITY_TASK_ON_HOME.
The scenario I have is like
the current stack is ...../ViewA/ViewB/ViewN

When form ViewN can Navigation to ViewO and ViewO can navigate back to ViewN, like a circle,
ViewN->ViewO->ViewN

ViewN and ViewO also have a back button ,need to navigate back to ViewB(before ViewN is on top of stack)

If I use Navigate from ViewN and ViewO, the stack will became too deep. thus this is no way for the back button to navigate back to ViewB.

So I just want a method to replace top page on the stack, like

..../ViewA/ViewB/ViewN --> ..../ViewA/ViewB/ViewO

Are your pages inside a NavigationPage? So is it like this:

NavigationPage/ViewA/ViewB/VIewN

No, not in NavigationPage, I have to custom the page header, the default navigation Page doesn't fit for our app. all page is a regular content page.

Just so you know, there is no way to remove a Page in a modal scenario. You can only pop and then push a new page, which results in a "flash" of the previous page. Would this be acceptable?

Thanks, I did try use Goback and then Navigate ,doesn't seems work as expected, that's why I asked for a replace method, is it OK to call Navigate immediately after Goback? Maybe I need to implement a FlipPage (with 2 child pages) myself :-)?

you can't call navigate from the same page you called GoBack, because you just popped that page off the stack so it is no longer part of the navigation stack.

Thank you Brian, then I have to find alternative solution.

I haven't tested this, but you can try calling NavigateAsync on the previous page on the IOnNavigatedTo method. Just pass a parameter in the GoBackAsync call so that you know that you want to navigate in response to a GoBack. I'm not sure if that will work either because technically then GoBackAsync task hasn't completed yet, but it's worth a try.

Thank you, I will give it try tomorrow.

@brianlagunas Just want to let you know, this trick works as expected, thanks a lot.

Here is another scenario for you to think about when you design your approach.

I have two modes to view particular content in my app, standard and advanced. I have two pages, one is StandardPage, another is AdvancedPage. I have a button that allows to switch between this two modes, basically from StandardPage I can navigate to AdvancedPage and vice versa. When such particular navigation happens, I would like it to be removed from the stack. If I navigate from any of these pages somewhere else, I would like it to be left in the stack.

@maxal1917 it sounds like the "../" option would be ideal for you?
My understanding is that you would be able to do, from standard "../Advanced" and from advanced "../Standard"

@djcparker looks like ../ option covers that. It's very similar to what @guidebee described, it's just in my case I'm within Navigation page, my stack is MasterDetail/Navigation/Main/Standard <-> MasterDetail/Navigatoin/Main/Advanced.

With the caveat that instead of "Main" there can be something else, there can be more than one way to arrive to Standard or Advanced page, and I want to switch back and forth between them, and when Back button is pressed, go back to where I came from.

Given your initial example it sounds like it will:

MasterDetailPage/NavigationPage/ProjectsListPage
Navigate to create a new project:
MasterDetailPage/NavigationPage/ProjectsListPage/CreateProjectPage
when finished with the create page navigate to a session page, and remove the create page:
MasterDetailPage/NavigationPage/ProjectsListPage/SessionsListPage

at the end of CreateProjectPage you'd do "../SessionsListPage" wouldn't you?

The second example

As user logs in, the app navigates to another page, and now the login page should be removed. Right now, an absolute URI is perfect for login, but if the login happens somewhere else in the app, and not at app startup, then this remove would be helpful.

would still work. I guess you were thinking something like, going from open to secure content?

MasterDetailPage/NavigationPage/ViewA
Navigate to something secure
MasterDetailPage/NavigationPage/ViewA/LoginPage
On success "../SecurePage"
MasterDetailPage/NavigationPage/ViewA/SecurePage

Pretty sure it works for @maxal1917 examples:

it's just in my case I'm within Navigation page, my stack is MasterDetail/Navigation/Main/Standard <-> MasterDetail/Navigatoin/Main/Advanced.
With the caveat that instead of "Main" there can be something else, there can be more than one way to arrive to Standard or Advanced page, and I want to switch back and forth between them, and when Back button is pressed, go back to where I came from.

MasterDetail/Navigation/Main/Standard
go to advanced "../Advanced"
MasterDetail/Navigation/Main/Advanced

or he says different ways to arrive so I guess:

MasterDetail/Navigation/OtherView/ViewC/Standard
go to advanced "../Advanced"
MasterDetail/Navigation/OtherView/ViewC/Advanced

Somewhere I saw a good walkthrough of when you would change the navigation flow but I can't think where. I'm sure it was to do with Android and expected behaviour... or maybe UWP/Mobile...

I can't think of a reason to want to remove a page from some arbitrary point in the navigation stack.

My only problem is as described above where I don't know how many I need to go back.. I used the suggestion you gave of executing GoBack in the OnNavigatedTo and it works but you get a flash of the pages as they "reappear" - animation:false made it better, with the default animation it looked cool but everyone just laughed :)

My problem is different to the scenario's described above, in all of those, you want to remove the previous page and replace it.

My opinion, if you have a more complex flow then you are better having different absolute flows and maintaining state in a singleton object in the container (or something like that, which is what I do because the logon flow is multi-page).

I like the IDestroyAfterNavigation approach. Fits my use-cases

I like the IDestoryAfterNavigation approach as well, but even if it was a modal page we would still have the flash or no?

Yes, it will still flash. There is no way to remove a modal page from the stack without popping it.

After navigating to a page (especially when using ViewModel navigation) how do I access that page the prism way (i.e. not via MainPage.Navigation.ModalStack) - or is that the right way?
Then, how do I get the ViewModel? Getting it via the BindingContext looks too ugly to me.

Thanks.

@weitzhandler you never access a Page from a ViewModel. Prism will never let you do that. If you are in the Page's code-behind and need access to the ViewModel, you get it from the BindingContext. This is the standard approach.

Agree,
So is there a way to remove a VM from the navigation stack (and in turn removal of its page), or get the VM of type T that is currently in stack / in front?

Besides calling GoBackAsync, no. That's why this issue exists :)

As of now, your best bet is to use code-behind to control removing specific pages in the stack.

@brianlagunas

no. That's why this issue exists :)

Consider adding an extension Remove<TViewModel> if implemented.

The chances of me adding a remove method using that syntax is very low, as that creates a coupling between ViewModels. In Prism, even ViewModels shouldn't know about other ViewModels.

I do like the idea of the IDestoryAfterNavigation,, just one question. Will this interface be XF only? Or will it also be available in the WPF version. (or is there something like this already in the WPF version I can make use of). Just wondering as it's something that's been discussed a few times now about a project I'm working on.

@wowfood you can already remove views from a region in WPF in a number of ways.

I have read this discussion and was thinking about another solution. Would it be a solution to include a marker on the stack to go back to. I was thinking about an anchor like this:

If this stack looks like this /ViewA/ViewB⚓/ViewC/ViewD

than NavigateAsync("⚓/ViewE") would result in /ViewA/ViewB⚓/ViewE

and NavigateAsync("⚓") would result in /ViewA/ViewB⚓

I'm still thinking about this. Until I figure this out, I do have an easy solution for PopToRoot scenarios.

Use this extension method:

    public static class NavigationServiceExtensions
    {
        public static System.Threading.Tasks.Task PopToRoot(this INavigationService navigationService)
        {
            Prism.Common.IPageAware page = (Prism.Common.IPageAware)navigationService;
            return page.Page.Navigation.PopToRootAsync();
        }
    }

FYI... both PopToRoot and RemovePage are part of the Prism.Forms.Extensions (pre3) on NuGet. 🍻

Just a thought, you added some syntax for tabpages like this:

NavigateAsync("TabbedPage?selectedTab=MiddleTab/ViewA/ViewB")

Would it be worth doing something similar for this - allow people to add an anchor/replace point

I think one example given was this:

MasterDetailPage/NavigationPage/ProjectsListPage
Navigate to create a new project:
MasterDetailPage/NavigationPage/ProjectsListPage/CreateProjectPage
when finished with the create page navigate to a session page, and remove the create page:
MasterDetailPage/NavigationPage/ProjectsListPage/SessionsListPage

NavigateAsync("MasterDetailPage/NavigationPage/ProjectsListPage?navigationPoint=replaceme/CreateProjectPage")
then, I don't know what syntax but something like:

NavigateAsync("SessionListPage", navigationPoint: "replaceme");

I guess you could extend or have an interface that ProjectsListPage could implement that named the navigationpoint...

Has anyone tried the extension method in the Prism.Forms extension to see if it will work?

public static void RemoveView(this INavigationService navigationService, string name)
{
    var formsNav = ((Prism.Common.IPageAware)navigationService).Page;
    var pageType = PageNavigationRegistry.GetPageType(name);
    page = formsNav.NavigationStack.LastOrDefault(p => p.GetType() == pageType);
    if (page != null)
    {
         formsNav.RemovePage(page);     
    }    
 }

Will this be sufficient to solve this issue?

Good point! Not yet.
For me, I want to remove several pages at once... I guess I could do something similar like gobackto("xx"). Will give it a try

This has been implement with PR #1207. Please try this out with the latest 163-CI build on MyGet. I need feedback as soon as you can give it to me.

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings