Xamarin.forms: [Shell] Uri Navigation Spec when using GoToAsync

Created on 3 Apr 2019  Â·  20Comments  Â·  Source: xamarin/Xamarin.Forms

Shell.Navigation.GotoAsync

Parent #2415

Route structure

//route (required) / page (optional) ? QueryParams (optional)

  • Route defines the path to a ShellContent element.These are the views that exist as part of the Shell visual hierarchy.
  • Pages don't exist in the visual hierarchy but can be pushed onto the stack from wherever you are. For example an Item Details page won't be defined in the Shell XAML file but you will want to push it onto the stack from different places.
  • QueryParams are any parameters you want passed into your destination page

relative route

  • route

    • this will search relative to current position and will then go up the hierarchy until a route is found

    • page will be pushed to stack

  • /route

    • this will search from the root down the hierarchy until a page is found

    • page will be pushed to stack

  • //route
    -this will search relative to current position and will then go up the hierarchy until a route is found

    • page will replace the stack

  • ///route (think of this like //root/route/)

    • this will search from the root down the hierarchy until a page is found

    • page will replace the stack

Route Decomposition

Given the following URI
//store/category/edit?CategoryId=12

The routing system will first try to locate what part of the URI represents the ShellContent. So if the structure of the shell page is defined as

--search
--category
about

The routing system will first try to locate a full path that matches //store/category/edit. Once that's not found it will remove the last segment and try to locate a route at //store/category.

This will cause the store page to be visible with the category tab selected. Now it has to locate the edit page you want to use. The routing system will perform the following route searches and once a route is found then whatever page is found there will be set as the content page.

//store/category/edit
//store/edit
//edit

Once the page is found that is what the visible content will be set to. This will not create a page stack, the stack will only have one item on it.

Currently the pages will be registered in code
```c#
RegisterRoute("//store/category/edit", typeof(EditCategory));
RegisterRoute("//people/search/edit", typeof(EditSearch));

## Add better debug information for routes

### Error messages

#### Incorrect Routes
If a user tries
`//store/category/ediit`

Then it should throw an exception much like you'd see on MVC
"Edit not found at any of the following locations <list of locations searched>"

#### Duplicates Routes
If a duplicate route is registered it should fail with an exception right when the duplicate route is added. If the XAML defined causes a duplicate route then an exception will be thrown on startup with a helpful message

### Route debugging
Add a static property to routes (That's enabled on the templates) which will dump the current set of routes to the debug output. This will dump the routes out when the app first spins up and then when any changes are made to the routes structure it will dump out all available routes

# API

## Shell

### Properties

| API | Description |
| ------------- | ------------- |
| Navigation | Adds a static property onto shell for accessing Navigation. |

## Shell.Navigation

### Properties

| API | Description |
| ------------- | ------------- |
| GoToAsync |  Provide a uri that you want to navigate to |

## Routing

### Properties

| API | Description |
| ------------- | ------------- |
| DebuggingEnabled | Setting this to true will dump information about the route structure to the output window  |

# Scenarios
Given the following setup
```XAML
<Shell>
    <FlyoutItem Route="xamtube">
        <Tab Route="home" />
        <Tab Route="library" />
        <Tab Route="store" />
    </FlyoutItem>
</Shell>
RegisterRoute("LoginPage", typeof(LoginPage));
RegisterRoute("StoreItem", typeof(StoreItem));
RegisterRoute("//xamtube/library/edit", typeof(EditLibraryContentPage));
RegisterRoute("//xamtube/library/store", typeof(EditStoreContentPage));
RegisterRoute("edit", typeof(EditSettings));

Set root of shell to default content page for xamtube
```C#
GotoAsync("///xamtube")

*Set xam tube as root of shell and activate library tab*
```C#
GotoAsync("///xamtube/library") 

User will only see Login Page and no Shell Navigation
```C#
GotoAsync("//LoginPage")

*Store tab will be selected on the xamtube page. The content will be set to the StoreItem Page.*
```C#
GotoAsync("///xamtube/store/StoreItem?ItemId=123") 

This creates a navigation stack with the home tab and then the store tab. If the user hits the back button it will go to the home page
```C#
GotoAsync("///xamtube/home")
GotoAsync("xamtube/store")

*This will set `xamtube/library` as root of shell with `EditLibraryContentPage` as the content*

```C#
GotoAsync("///xamtube/library/edit") 

Existing INavigation mappings

```C#
public interface INavigation
{
// Contains stack of Pages on shell contents
IReadOnlyList NavigationStack { get; }

//can be used to insert a page somewhere on stack. This is the same as pushing a PageContent
void InsertPageBefore(Page page, Page before);

// locate content in stack that matches this page and remove it from the stack
void RemovePage(Page page);

// Go back in stack
Task<Page> PopAsync();

// Go back in stack
Task<Page> PopAsync(bool animated);

// pop everything except base
Task PopToRootAsync();

// pop everything except base
Task PopToRootAsync(bool animated);

// push a PageContent onto Shell stack
Task PushAsync(Page page);

// push a PageContent onto Shell stack
Task PushAsync(Page page, bool animated);

// push a PageContent onto Shell stack without navigation structure (at some point we will let people make this prettier)
Task PushModalAsync(Page page);

// push a PageContent onto Shell stack without navigation structure (at some point we will let people make this prettier)
Task PushModalAsync(Page page, bool animated);

// Contains stack of any modal pages pushed
IReadOnlyList<Page> ModalStack { get; }

// Close current Modal page
Task<Page> PopModalAsync();

// Close current Modal page
Task<Page> PopModalAsync(bool animated);

}
```

Additional things to flesh out and other specs in the works

  • Embedded a stack into the uri?
  • Navigating by the x:Name of a Shell
  • Overloading GoToAsync with a NavigationBehavior structure to allow for more complex parameters and used to indicate things like transitions

Difficulty : medium

shell enhancement âž•

Most helpful comment

@andreinitescu

Navigation should support complex parameters, not just query string. See Prism's NavigationParameters

The plan is to overload GoToAsync with some additional parameters that will let you specify complex parameters, transitions, modal information, etc..

I'd like something like 'NavigateAsync' instead of 'GoToAsync'

We went back and forth with this. We definitely want everything to be close to each other. The two things we are thinking are to prefix everything with Navigate or just move everything off of the Shell class into a Navigation property
```c#
// long form
Shell.Current.Navigation.GotToAsync

// short form static proprety
Shell.Navigation.GoToAsync
```

I hope XF will preserve navigation backstack, just how native does

Whenever you call GotoAsync with a relative uri (like prism) it will go on a stack. Once you do an absolute navigation it'll wipe the stack

All 20 comments

I'm not sure from the above whether you support navigation to a stack of pages with a single absolute url?
eg. GotoAsync("//xamtube/home//xamtube/library/edit") where home and library edit are different pages in a stack? This is needed eg. where you want to bookmark the UI state in order to return to it when the app resumes. When the user opens this route and goes back, they expect to see //xamtube/home

I've had experience of Shell navigation recently (using 4.0-pre6/7), and my comments are:

  1. When you have a grip on routes, GoToAsync works well. But it's always going to be error prone because of being based on magic strings. The chances are users will make mistakes and get their routes wrong on first attempt (by not fully defining them, or getting them incorrect when calling GoToAsync). It can be a head scratcher figuring out what the problem is (you only know there's a problem because navigation doesn't occur).
  2. There's a NotImplementedException in GoToAsync. I managed to trigger it several times in my first couple of attempts to get routes working correctly.
  3. I wonder if there's a need for the Routing class to expose a Routes property dictionary, that has the registered routes? I had a need for that, and so ended up storing my routes in a dictionary, that I then iterated over when calling RegisterRoutes. It was that or embed even more magic strings in my code.

How do we attach a BindingContext to the Page, when navigating with Shell?

With regular Page navigation we already have the Page in our hand and can do stuff to the instance before we navigate to it.

I guess one would have to just try navigating to an URL, then when the Task returns look in the navigation stack for which pages are in there, then add the BindingContext. Not super neat.

Could it be an idea to have GotoAsync return _the_ Page it navigates to?

Also what happens when you have multiple routes with the same name, or is that not possible?
I would assume if it is possible either last added wins or first added wins, but it is unclear from the spec.

@buzzware
It seems that

GotoAsync("//xamtube/home") 
GotoAsync("xamtube/store") 

is intended to be an example of setting up a stack in one step.

Is that meant to be

await GotoAsync("//xamtube/home"); 
await GotoAsync("xamtube/store"); 

and does that imply that the user will see the home page _and then_ the store page as the stack is setup?

I would also like to add my support the comment by @Cheesebaron that getting hold of the resulting Page is important.

@buzzware

I'm not sure from the above whether you support navigation to a stack of pages with a single absolute url?
eg. GotoAsync("//xamtube/home//xamtube/library/edit") where home and library edit are different pages in a stack? This is needed eg. where you want to bookmark the UI state in order to return to it when the app resumes. When the user opens this route and goes back, they expect to see //xamtube/home

We're still working through the idea of how to let users express a stack with a uri and if we want to. The syntax you have there was something we had discussed internally but we weren't sure if it would be confusing or not to stack the Urls like that. The other thing to note is that we are also working on the best way for a user to resume an app and the best way to make it so the developer has to do as little work as possible for this. For example not requiring the user to "bookmark" the current stack into a URI they have to remember. We would prefer to just "resume" the app and then provide hooks to the developer where they can perform different actions when an app is resuming.

A few ideas we've been tossing around

  • the syntax you've expressed "//route/page//route/page"
  • The plan is to add another parameter to GotoAsync that takes a NavigationBehavior or Intent so there a user can express things like transitions, modaltype, navigationstack, etc...
  • Add actions to the uri (much like prism) "//route/page?shell.stack="

So a couple things here:

  1. Navigation with URI's should support true URI's

What's currently supported but shouldn't be:

GoToAsync("/ViewA/ViewB/ViewC?foo=1&bar=2");

In this scenario both foo and bar are passed into ViewA, ViewB, and ViewC

How this should work:

GoToAsync("/ViewA?foo=1/ViewB?bar=2/ViewC?foo=3");

In this scenario we're actually starting to support real URI's in which query parameters are passed specifically to individual segments so ViewA only sees a query parameter of foo with a value of 1, while ViewC also sees a query parameter of foo but with a value of 3. Here you would be matching Prism from about 2015.

Now if we want to learn from the lessons that Prism has from years of Xamarin Forms developers working with URI based navigation, it's important that we really support real URI's. In the current examples we can think of QueryString Parameters as a Dictionary, but that's actually not accurate. What you should see is more like this:

GoToAsync("/ViewA?color=red&color=white&color=blue");

You'll see here that we really have a collection of something with overloaded querystring keys. This is because actual QueryStrings are not Dictionaries, they are better represented by List<KeyValuePair>.

2.

How do we attach a BindingContext to the Page, when navigating with Shell?

@Cheesebaron I would suggest that the topic of how does a 3rd party such as Prism or MvvmCross actually integrate with this be tracked in issue #5166

  1. Navigation should support complex parameters, not just query string. See Prism's NavigationParameters
  2. I'd like something like 'NavigateAsync' instead of 'GoToAsync'
  3. I hope XF will preserve navigation backstack, just how native does

@davidbritch

When you have a grip on routes, GoToAsync works well. But it's always going to be error prone because of being based on magic strings. The chances are users will make mistakes and get their routes wrong on first attempt (by not fully defining them, or getting them incorrect when calling GoToAsync). It can be a head scratcher figuring out what the problem is (you only know there's a problem because navigation doesn't occur).

Yea this reminds me when I was starting out with MVC... Why are you not activating my controller?!?!?!? The plan here is to add better error messaging and we are going to add a debug hook that will print out all available routes to your debug console. This way users can see what they all are and if an incorrect one is asked for it'll throw an error. I've added an additional API and notes to the spec

There's a NotImplementedException in GoToAsync. I managed to trigger it several times in my first couple of attempts to get routes working correctly.

Yea :-/ getting these under control is my current priority while this spec is brewing.

I wonder if there's a need for the Routing class to expose a Routes property dictionary, that has the registered routes? I had a need for that, and so ended up storing my routes in a dictionary, that I then iterated over when calling RegisterRoutes. It was that or embed even more magic strings in my code.

Would the route debugging I spoke of earlier help with this a little bit? You can attach x:Names to the different shell parts as well so maybe adding some routing behavior around this would be helpful. Something that could take in the name of a ShellI and then navigate to it. Hmmm

@andreinitescu

Navigation should support complex parameters, not just query string. See Prism's NavigationParameters

The plan is to overload GoToAsync with some additional parameters that will let you specify complex parameters, transitions, modal information, etc..

I'd like something like 'NavigateAsync' instead of 'GoToAsync'

We went back and forth with this. We definitely want everything to be close to each other. The two things we are thinking are to prefix everything with Navigate or just move everything off of the Shell class into a Navigation property
```c#
// long form
Shell.Current.Navigation.GotToAsync

// short form static proprety
Shell.Navigation.GoToAsync
```

I hope XF will preserve navigation backstack, just how native does

Whenever you call GotoAsync with a relative uri (like prism) it will go on a stack. Once you do an absolute navigation it'll wipe the stack

@Cheesebaron

Could it be an idea to have GotoAsync return the Page it navigates to?

Returning something useful could be useful. Maybe a result structure.
As @dansiegel said we are still figuring out the best extensibility points for this https://github.com/xamarin/Xamarin.Forms/issues/5166

Also what happens when you have multiple routes with the same name, or is that not possible?
I would assume if it is possible either last added wins or first added wins, but it is unclear from the spec.

It'll throw an exception. I added some notes to the spec.

@dansiegel If I'm following your comments

GoToAsync("/ViewA?foo=1/ViewB?bar=2/ViewC?foo=3");

Is what you want to be able to do? @davidortinau had brought this up a bit. Our concern was that this isn't a fully valid URI. Those parts could be encoded or possibly we can extend the querystring syntax
```C#
GoToAsync("//ViewA/ViewB/ViewC?ViewA.foo=1&ViewB.bar=2&foo=3");

The other thing is that the URI we are proposing here isn't building a stack it's just taking you to a place with only one item in the stack.  We're still settling in on all the best ways for users to construct a navigation stack. There will be APIS available so users can compose the stack at any point they want, for example
`Navigation.Stack.Insert(0, "<SomeURI>")`


> GoToAsync("//ViewA?color=red&color=white&color=blue");

So this will map to a parameter like so

```C#
string color[]

@GalaxiaGuy

and does that imply that the user will see the home page and then the store page as the stack is setup?

Yes that example was more to demonstrate URI syntax and what would happen. We're going to provide some apis that allow users to setup and modify the stack as needed

@PureWeen I'll grant you that it isn't widely familiar but it is valid. I never really got deep into many modern web frameworks like Angular but as I recall some of them actually do something very similar to this. Also I can guarantee that .NET sees the syntax as valid since this is what Prism has been doing for years.

@dansiegel

My mistake on that one. I ran my test of that Uri structure incorrect and it is indeed correct.

@PureWeen I'll add to what @dansiegel said. The reason the System.Uri class works with the ViewA?foo=1/ViewB?bar=2/ViewC?foo=3 syntax is because anything after the first ? is considered a parameter. However, this doesn't matter to the developer, all they care about is that the API is logical. Passing parameters to specific pages is logical. Appending all parameters for all pages at the end of the URI and prefixing them with the page name is not logical.

The navigation service will handle parsing the URI properly and just work, and the dev doesn't have to worry about if something is technically a valid URI format.

I'll also add that requiring pages to be registered against a fully known route like //xamtube/library/store is a pain. Ideally, you would just register the page with your route "key", such as RegisterForNavigation("LoginPage", typeof(LoginPage)); and just build your stack using those names regardless of the route they belong to.

Honestly, Prism has been doing URI navigation for years and have pretty much got it figured out. You should take a look at what we are doing there instead of reinventing the wheel.

I also understand that you may have some technical limitations because of the way Shell was implemented that may prevent you from taking the more flexible approach provided by Prism.

@brianlagunas having to know the whole route is kind of a pain and I've been wondering ways around this. For example ways to register globally unique routes.

The difference here (as I understand it) between Prism and Shell is that Prism doesn't have an opinionated structure like shell. With Prism you just have a collection of page types and then the uri cleanly maps to calls of PushAsync/PushModal to just push different types of pages on the stack.

With shell we will have structures like this

<Shell>
    <FlyoutItem Route="Store">
        <Tab Route="Categories" ContentTemplate="{DataTemplate pages:StoreCategories}"/>
        <Tab Route="Edit"  ContentTemplate="{DataTemplate pages:StoreEdit}"/>
        <Tab Route="About"  ContentTemplate="{DataTemplate pages:StoreAbout}"/>
    </FlyoutItem>
         <FlyoutItem Route="User">
        <Tab Route="Categories" ContentTemplate="{DataTemplate pages:UserCategories}"/>
        <Tab Route="Edit"  ContentTemplate="{DataTemplate pages:UserEdit}"/>
        <Tab Route="About"  ContentTemplate="{DataTemplate pages:AboutEdit}"/>
    </FlyoutItem>
         <FlyoutItem Route="Sports">
<!-- note how these are all the same pages but the display of the page will just be based on the route -->
        <Tab Route="Football"  ContentTemplate="{DataTemplate pages:Sports}"/>
        <Tab Route="BaseBall" ContentTemplate="{DataTemplate pages:Sports}" />
        <Tab Route="BasketBall" ContentTemplate="{DataTemplate pages:Sports}" />           
      </FlyoutItem>
</Shell>

There's not a one to one mapping in shell with PageType == Route

We had discussed the idea of forcing Routes to be globally unique and then we could have each segment of a uri represent a location within the Shell but we're not sure if that's useful.

I'll also add that requiring pages to be registered against a fully known route like //xamtube/library/store is a pain. Ideally, you would just register the page with your route "key", such as RegisterForNavigation("LoginPage", typeof(LoginPage)); and just build your stack using those names regardless of the route they belong to.

We're not really requiring this. It's only if you want to register a different type of page at a different layer. Think how MVC does routing.

The LoginPage could totally just be registered like this

RegisterForNavigation("LoginPage", typeof(LoginPage));

The full route path would only be if you wanted to register different page at different hierarchies

RegisterForNavigation("//store/edit", typeof(EditStore));
RegisterForNavigation("//user/edit", typeof(EditUser));

This way if you're currently at route //store and you route to edit it will go to EditStore
if you're currently at route //user and you route to edit it will go to EditUser

and If users opt to make the routes globally unique we could totally just fall back to something like that

If it can't find a specific route called EditStore then just search for anything called EditStore and navigate to that

@PureWeen that's the difference right there! Prism requires a unique key for each page that is registered. Shell is allowing the reuse the same route name except mapping different pages to them depending on where it is defined.

@PureWeen

Is it still planned to add relative routing that replaces the stack at some point soon? 4.1? I really need that in our current app we're trying to build. Or should it work in 4.0 stable somehow? It doesn't seem implemented as of yet.

@anthcool not yet I'm afraid :-/

Was this page helpful?
0 / 5 - 0 ratings