How-To: Define Routes
Routes provide an easy and dynamic way of navigating through your app, either via code-behind or XAML. They are particularly useful when your application has a high degree of complexity in terms of navigation, especially when dealing with nested pages using TabBars and NavigationViews. In such cases, there are multiple levels of navigation rather than just one level, as in an application that simply navigates between, for instance, a Welcome page, Login page, Main page, and Second page.
This topic walks through the process of defining routes.
Step-by-step
Important
This guide assumes you used the template wizard or dotnet new unoapp
to create your solution. If not, it is recommended that you follow the Creating an application with Uno.Extensions article to create an application from the template.
Understanding routes
In a new app with Navigation, you'll find some pre-set views: Shell
, MainPage
, and SecondPage
. Shell
acts as the main frame where views are set and navigation begins. When the app launches, it opens MainPage
, which then leads to SecondPage
.
So if we take a look at the RegisterRoutes
method in the App.xaml.cs
file, we can see how these pages are organized in terms of routes:
private static void RegisterRoutes(IViewRegistry views, IRouteRegistry routes)
{
views.Register(
new ViewMap(ViewModel: typeof(ShellViewModel)),
new ViewMap<MainPage, MainViewModel>(),
new DataViewMap<SecondPage, SecondViewModel, Entity>()
);
routes.Register(
new RouteMap("", View: views.FindByViewModel<ShellViewModel>(),
Nested:
[
new ("Main", View: views.FindByViewModel<MainViewModel>()),
new ("Second", View: views.FindByViewModel<SecondViewModel>()),
]
)
);
}
Firstly, we see that the views are registered using a ViewMap
object. This object is also responsible for associating each View with its corresponding ViewModel. Next, we observe that the routes are being registered. Here, we can see that "Main" and "Second" are nested pages within "Shell", and both are at the same level. So if we take a look at the flow of the app, ShellViewModel
opens MainPage
, which in turn contains a button that navigates to SecondPage
.
ViewMap
When registering routes, we can take advantage of the ViewMap
object and its variations DataViewMap
and ResultDataViewMap
, to correlate Views with ViewModels, specify the type of parameters ViewModels may take, and specify the type of return coming from a navigation.
Let's explore each of these arguments and how to effectively implement them:
View - The type of the View being registered. Example:
new ViewMap<MainPage>()
ViewModel - The type of the ViewModel being associated with the View. Example:
new ViewMap<MainPage, MainViewModel>()
This correlation between View and ViewModel allows navigation through ViewModels. For example, when navigating to
MainPage
the following call could be made:_navigator.NavigateViewModelAsync<MainViewModel>(this);
Data - In addition to associating Views with ViewModels, the Data type can also be associated with ViewModels. This Data type will be injected into the ViewModel constructor when an instance is created. To achieve this, we use the
DataViewMap
object instead ofViewMap
.For example, let's say we have a
Product
class that holds product information, and we useProductDetailPage
to display the product details. We can register it as follows:new DataViewMap<ProductDetailPage, ProductDetailViewModel, Product>()
This allows navigation through data, for example when navigating to
ProductDetailPage
the following call could be made:// Where `myProduct` is of type `Product` _navigator.NavigateDataAsync(this, myProduct);
Note
In order to achieve this, ensure that your ViewModel constructor accepts the data type as a parameter.
ResultData - Defines an association between the view and the type of data being requested. For example, if you're navigating to a page that has a list of products and that page should return a product that was selected by the user you can achieve it by associating the
Product
type to the View and ViewModel usingResultDataViewMap
:new ResultDataViewMap<ProductsPage, ProductsViewModel, Product>()
Then, when navigating to the
ProductsPage
and requesting a product from this navigation you can do:public async Task GoToProductsPage() { var product = await _navigator.GetDataAsync<Product>(this); }
FromQuery - Used to convert a query parameter into entities when using deep linking.
For example, let's say you have a route called "Product" which is associated with the
ProductDetailPage
. When navigating through a deep linking, you can reach that page by specifying the route name and the product you want to show by providing its ID. The way of converting the ID provided by the query string to aProduct
can be achieved with theFromQuery
parameter, as follows:new DataViewMap<ProductDetailPage, ProductDetailViewModel, Product>( FromQuery: async (sp, query) => { var productService = sp.GetRequiredService<IProductService>(); var id = query[nameof(Product.Id)]; return await productService.GetById(id, default); } )
ToQuery - Used to convert entities into a query parameter.
Following the same logic of the previous example, you can use the
ToQuery
parameter to provide the logic to convert theProduct
into a query string:new DataViewMap<ProductDetailPage, ProductDetailViewModel, Product>( ToQuery: product => new Dictionary<string, string> { { nameof(Product.Id), $"{product.Id}" } } )
Note
FromQuery
and ToQuery
are only available when using DataViewMap
.
RouteMap
RouteMaps are especially useful for managing complex navigation systems, including those with nested views. When defining routes, we utilize the RouteMap
object. This object's constructor takes six arguments. Let's explore each of these arguments and how to effectively implement them:
Path
The first parameter is Path, serving as the identifier for the route. When navigating within your app using routes, this is the reference you'll use to reach the corresponding view.
Important
When creating route names, please use only alphanumeric characters (letters and numbers). Avoid using special characters such as punctuation marks, symbols, or spaces. Using special characters may lead to incorrect or invalid navigation.
When defining route names, it's crucial to choose names carefully to avoid potential conflicts and errors. Certain names, such as "List", "Grid", or "Page" could unintentionally resolve to existing control, element, or class names within the application. For example, a route named "List" might be mistakenly resolved as a "ListView" class, leading to an incorrect path and causing errors in navigation. To avoid these issues, it's best to avoid using common names that might conflict with existing control or class names in the application. Here are some examples of names to avoid:
Names that could be mistaken for controls, elements, or components:
- Scroll
- List
- Grid
- Tree
- Web
- Navigation
- Content
- User
- Items
- Menu
Suffixes that could possibly resolve in an existing class:
- Page
- Model
- ViewModel
- Service
- Helper
- Converter
- Manager
- Handler
- Exception
- Extension
- Settings
View
The View parameter refers to the view associated with the route. The view must be registered beforehand within the IViewRegistry
, and you can establish the association using the FindByViewModel
method. Note in the following example how we add a route for a new page called LoginPage
:
protected override void RegisterRoutes(IViewRegistry views, IRouteRegistry routes)
{
views.Register(
new ViewMap(ViewModel: typeof(ShellViewModel)),
new ViewMap<LoginPage, LoginViewModel>()
);
routes.Register(
new RouteMap("", View: views.FindByViewModel<ShellViewModel>(),
Nested:
[
new ("Login", View: views.FindByViewModel<LoginViewModel>())
]
)
);
}
Note that adding a view is not mandatory. You can specify a route without a view. For example, if you want to add routes for the content of a TabBar
, you can define the routes without the view, and in your markup file, you can use the attached property uen:Region.Name
to bind the content view with the defined route.
Given the following routes:
// Omitted for brevity
new RouteMap("Main", View: views.FindByViewModel<MainViewModel>(),
Nested:
[
new ("ForYouTab"),
new ("FavoritesTab")
]
)
We can specify the content view using the uen:Region.Name
to link it to the given route name:
xmlns:uen="using:Uno.Extensions.Navigation.UI"
<Grid uen:Region.Attached="True">
<utu:TabBar uen:Region.Attached="True">
<utu:TabBarItem Content="For You"
uen:Region.Name="ForYouTab" />
<utu:TabBarItem Content="Favorites"
uen:Region.Name="FavoritesTab"/>
</utu:TabBar>
<Grid uen:Region.Attached="True"
uen:Region.Navigator="Visibility">
<ListView uen:Region.Name="ForYouTab"
ItemsSource="{Binding Items}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{x:Bind Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Grid uen:Region.Name="FavoritesTab">
<TextBlock Text="Favorites go here" />
</Grid>
</Grid>
</Grid>
In that example, we used the uen:Region.Name
attached property on the ListView
to associate it with the route named "ForYouTab" as defined in the RouteMap
. Similarly, the Grid
was linked to the "FavoritesTab" route using the same attached property.
Nested
The Nested property allows you to define hierarchical routes. It creates a parent-child relationship between routes. This helps organize navigation structure and manage complex navigation scenarios more effectively, for example when using TabBar
or NavigationView
. In the following example MainPage
has two nested routes, that represents tabs within MainPage
: "ForYouTab" and "FavoritesTab":
protected override void RegisterRoutes(IViewRegistry views, IRouteRegistry routes)
{
views.Register(
new ViewMap(ViewModel: typeof(ShellViewModel)),
new ViewMap<LoginPage, LoginViewModel>(),
new ViewMap<MainPage, MainViewModel>()
);
routes.Register(
new RouteMap("", View: views.FindByViewModel<ShellViewModel>(),
Nested:
[
new ("Login", View: views.FindByViewModel<LoginViewModel>()),
new ("Main", View: views.FindByViewModel<MainViewModel>(),
Nested:
[
new ("ForYouTab"),
new ("FavoritesTab")
]
)
]
)
);
}
IsDefault
IsDefault will make the navigator automatically shows that route when dealing with nested views. For example, imagine a scenario where you are defining routes within a NavigationView
, such as nested routes inside a MainPage
. You can set one of these routes as the default by setting IsDefault: true
. This ensures that the specified route is automatically navigated to when the page is displayed.
// Omitted for brevity
new RouteMap("Main", View: views.FindByViewModel<MainViewModel>(),
Nested:
[
new ("ForYouTab", IsDefault: true),
new ("FavoritesTab")
]
)
DependsOn
DependsOn enables you to establish a dependency between two views. This argument expects a route name and ensures that when you navigate to a view with dependencies, the dependent view will be navigated to first before opening the requested view. This is especially useful when using deep linking to navigate through pages. In the following example we add a new page called ProductsPage
and we set this page as dependent of MainPage
:
protected override void RegisterRoutes(IViewRegistry views, IRouteRegistry routes)
{
views.Register(
// Omitted for brevity
new ViewMap<ProductsPage, ProductsViewModel>()
);
routes
.Register(
new RouteMap("", View: views.FindByViewModel<ShellViewModel>(),
Nested:
[
// Omitted for brevity
new ("Main", View: views.FindByViewModel<MainViewModel>()),
new ("Products", View: views.FindByViewModel<ProductsViewModel>(), DependsOn: "Main"),
]
)
);
}
Init
Init allows you to customize the navigation request, enabling specific actions before displaying the associated view. For example, you can override the navigation request if the user is not logged in:
new ("Login", View: views.FindByViewModel<LoginViewModel>()),
new ("Main", View: views.FindByViewModel<MainViewModel>(),
Init: (request) =>
{
// Check if the user is logged in
if (!User.IsLoggedIn())
{
// Redirect to the Login page if the user is not logged in
request = request with { Route = Route.PageRoute("Login") };
}
return request;
}),