CHAPTER 6
In the previous chapters, we went over the basics of layouts and views, which are the fundamental building blocks of the user interface of your applications. However, you only saw how to use layouts and views within a single page, while most real-world mobile apps are made of multiple pages.
The systems targeted by MAUI provide different pages that allow you to display content in several ways and to provide the best user experience possible based on the content you need to present. .NET MAUI provides unified page models you can use from your shared C# codebase that work cross-platform.
It also provides an easy-to-use navigation framework, which is the infrastructure you use to move between pages. In addition, you can leverage a simplified architecture in one place with the Shell. Pages, navigation, and the Shell are the last pieces of the user interface framework you need to know to build modern, native apps with .NET MAUI.
Note: In order to follow the examples in the first part of this chapter, either open the companion Chapter6_Navigation solution or create a new solution. If you choose the second option, every time a new page is discussed, just clean the content of the MainPage.xaml and MainPage.xaml.cs files (except for the constructor) and write the new code.
.NET MAUI provides many page objects that you can use to set up the user interfaces of your applications. Pages are root elements in the visual hierarchy, and each page allows you to add only one visual element, typically a root layout with other layouts and visual elements nested inside the root.
From a technical point of view, all the page objects in .NET MAUI derive from the abstract Page class, which provides the basic infrastructure of each page, including common properties such as Content. This is the most important property that you assign with the root visual element. Table 6 describes the available pages.
Table 6: Pages in .NET MAUI
Page Type | Description |
|---|---|
ContentPage | Displays a single view object. |
TabbedPage | Facilitates navigating among child pages using tabs. |
FlyoutPage | Manages two separate panes and includes a flyout control. |
NavigationPage | Provides the infrastructure for navigating among pages. |
The next sections describe the available pages in more detail. Remember that Visual Studio provides item templates for different page types, so you can right-click the project in Solution Explorer, select Add New Item, and in the Add New Item dialog box, you can expand the .NET MAUI node to see templates for each page described in Table 6.
The ContentPage object is the simplest page possible and allows for displaying a single visual element. You already looked at some examples of the ContentPage previously, but it is worth mentioning its Title property. This property is particularly useful when the ContentPage is used in pages with built-in navigation, such as TabbedPage and FlyoutPage, because it helps identify the active page.
The core of the ContentPage is the Content property, which you assign with the visual element you want to display. The visual element can be either a single control or a layout; the latter allows you to create complex visual hierarchies and real-world user interfaces. In XAML, the tag for the Content property can be omitted, which is a common practice (also notice Title):
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
Title="Main page"
x:Class="App1.MainPage">
<Label Text="A content page"/>
</ContentPage>
The ContentPage can be used individually or as the content of other pages discussed in the next sections.
The FlyoutPage is a very important page since it allows you to split contents into two categories: generic and detail. The user interface provided by the FlyoutPage is very common in Android and iOS apps. It offers a flyout on the left (the generic part) that you can swipe to show and hide, and a second area on the right that displays more information (the detail part).
For example, a very common scenario for this kind of page is displaying a list of topics or settings in the flyout and the content for the selected topic or setting in the detail part. Both the flyout and the detail parts are represented by ContentPage objects. A typical declaration for a FlyoutPage looks like Code Listing 12.
Code Listing 12
<?xml version="1.0" encoding="utf-8" ?> <FlyoutPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" Title="Main page" x:Class="App1.MainPage">
<FlyoutPage.Flyout> <ContentPage Title="Main page"> <Label Text="This is the Master" HorizontalOptions="Center" VerticalOptions="Center"/> </ContentPage> </FlyoutPage.Flyout> <FlyoutPage.Detail> <ContentPage> <Label Text="This is the Detail" HorizontalOptions="Center" VerticalOptions="Center"/> </ContentPage> </FlyoutPage.Detail> </FlyoutPage> |
As you can see, you populate the Flyout and Detail properties with the appropriate ContentPage objects. In real-world apps, you might have a list of topics in the Flyout and then show details for a topic in the Detail when the user taps one in the Flyout’s content.
Remember that assigning the Title property on the ContentPage that acts as the master is mandatory; otherwise, the runtime will throw an exception. The FlyoutPage object cannot work together with the Shell; this means that you need to change the following line in the App.xaml.cs file:
MainPage = new AppShell();
to the following:
MainPage = new MainPage();
This ensures that the FlyoutPage is used as the startup object instead of the Shell. You will need to make this change when using the TabbedPage as well, which is discussed in the next section.
Note: Every time you change the root page from ContentPage to another kind of page, such as FlyoutPage, you also need to change the inheritance in the code-behind. For example, if you open the C# MainPage.xaml.cs file, you will see that MainPage inherits from ContentPage, but in XAML you replaced this object with FlyoutPage. So, you also need to make MainPage inherit from FlyoutPage. If you forget this, the compiler will report an error. This note is valid for the pages discussed in the next sections as well.
Figures 43 and 44 show the flyout and detail parts, respectively. You can swipe from the left to display the master flyout, and then swipe back to hide it. You can also control the flyout appearance programmatically by assigning the FlyoutLayoutBehavior with one of the following values:
· Default: The pages are displayed using each platform’s default layout.
· Popover: The detail page covers the flyout page.
· Split: This equally splits the flyout page and the detail. The flyout is displayed on the left and the detail on the right.
· SplitOnLandscape: Similar to Split, but only applies when the device is in landscape orientation; otherwise, Default is assumed.
· SplitOnPortrait: Similar to Split, but only applies when the device is in portrait orientation; otherwise, Default is assumed.
|
|
Figure 43: The Flyout of FlyoutPage | Figure 44: The Detail of FlyoutPage |
Sometimes you might need to categorize multiple pages by topic or by activity type. When you have a small amount of content, you can take advantage of the TabbedPage, which can group multiple ContentPage objects into tabs for easy navigation. The TabbedPage can be declared as shown in Code Listing 13.
Code Listing 13
<?xml version="1.0" encoding="utf-8" ?> <TabbedPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" Title="Main page" x:Class="App1.MainPage">
<TabbedPage.Children> <ContentPage Title="First"> <Label Text="This is the first page" HorizontalOptions="Center" VerticalOptions="Center"/> </ContentPage> <ContentPage Title="Second"> <Label Text="This is the second page" HorizontalOptions="Center" VerticalOptions="Center"/> </ContentPage> <ContentPage Title="Third"> <Label Text="This is the third page" HorizontalOptions="Center" VerticalOptions="Center"/> </ContentPage> </TabbedPage.Children> </TabbedPage> |
As you can see, you populate the Children collection with multiple ContentPage objects. Providing a Title to each ContentPage is of primary importance since the title’s text is displayed in each tab, as demonstrated in Figure 45.

Figure 45: Displaying grouped contents with the TabbedPage
Of course, the TabbedPage works well with a small number of child pages, typically between three and four pages.
Note: In the spirit of the Succinctly series, this section explains the most important concepts and topics of page navigation. However, there are tips and considerations that are specific to each platform that you must know when dealing with navigation in .NET MAUI. To learn more about them, see the official documentation.
Most applications offer their content through multiple pages. In .NET MAUI, navigating among pages is very simple because of a built-in navigation framework. First, you leverage navigation features through the NavigationPage object. This kind of page must be instantiated, passing an instance of the first page in the stack of navigation to its constructor. This is typically done in the App.xaml.cs file, where you replace the assignment of the MainPage property with the following code:
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new MainPage());
}
Wrapping a root page in a NavigationPage will not only enable the navigation stack, but will also enable the navigation bar on Android, iOS, and Windows. The navigation bar’s text will be the value of the Title property of the current page object, represented by the CurrentPage read-only property.
Now suppose you added another page called SecondaryPage.xaml of type ContentPage to the project. The content of this page is not important at this point; just set its Title property with some text. If you want to navigate from the first page to the second page, use the PushAsync method as follows:
await Navigation.PushAsync(new SecondaryPage());
The Navigation property, exposed by each Page object, represents the navigation stack at the application level and provides methods for navigating between pages in a last-in, first-out (LIFO) approach. PushAsync navigates to the specified page instance. PopAsync, invoked from the current page, removes the current page from the stack and goes back to the previous page.
Similarly, PushModalAsync and PopModalAsync allow you to navigate between pages modally. The following lines of code demonstrate this:
// removes SecondaryPage from the stack and goes back to the previous page
await Navigation.PopAsync();
// displays the specified page as a modal page
await Navigation.PushModalAsync(new SecondaryPage());
await Navigation.PopModalAsync();
Figure 46 shows how the navigation bar appears on Android and iOS when navigating to another page.

Figure 46: The navigation bar offered by the NavigationPage object
Users can just tap the Back button on the navigation bar to go back to the previous page. However, when you implement modal navigation, you cannot take advantage of the built-in navigation mechanism offered by the navigation bar, so it is your responsibility to implement code that allows going back to the previous page.
Modal navigation can be useful if you must be able to intercept a tap on the Back button on each platform. In fact, Android devices have a built-in hardware Back button that you can manage with events, but iOS does not. In iOS, you only have the Back button provided by the navigation bar, but this cannot be accessed by any events. So, in this case, modal navigation can be a good option to intercept user actions.
The need to exchange data between pages is not uncommon. You can change or overload a Page’s constructor and require a parameter of the desired type. Then, when you call PushAsync and pass the instance of the new page, you will be able to supply the argument that is necessary to the new page’s constructor.
By default, the navigation includes an animation that makes the transition from one page to another nicer. However, you can disable animations by passing false as the argument of PushAsync and PushModalAsync.
Every Page object exposes the OnAppearing and OnDisappearing events, which are raised right before the page is rendered and right before the page is removed from the stack, respectively. Their code looks like the following:
protected override void OnAppearing()
{
// Replace with your code…
base.OnAppearing();
}
protected override void OnDisappearing()
{
// Replace with your code…
base.OnDisappearing();
}
Actually, these events are not strictly related to navigation since they are available to any page, including individual pages. However, it is with navigation that they become very important, especially when you need to execute some code at specific moments in the page lifecycle.
For a better understanding of the flow, think of the page constructor: this is invoked the very first time a page is created. Then, OnAppearing is raised right before the page is rendered on screen. When the app navigates to another page, OnDisappearing is invoked, but this does not destroy the current page instance (this makes perfect sense).
When the app navigates back from the second page to the first page, the first page is not created again because it is still in the navigation stack, so its constructor will not be invoked, while OnAppearing will. So, within the OnAppearing method body, you can write code that will be executed every time the page is shown, while in the constructor, you can write code that will be executed only once.
Android devices have a built-in hardware Back button for users instead of a Back button in the navigation bar. You can detect if the user presses the hardware Back button by handling the OnBackButtonPressed event as follows:
protected override bool OnBackButtonPressed()
{
return base.OnBackButtonPressed(); // replace with your logic here
}
Simply put your logic in the method body. The default behavior is to suspend the app, so you might want to override this with PopAsync to return to the previous page. This event does not intercept pressing the Back button in the navigation bar, which implies it has no effect on iOS devices.
More often than not, mobile apps share several features like a navigation bar, a search bar, and a flyout menu that users can open by sliding from the left side of the screen. Implementing these features by yourself is not complex at all, but it requires a certain amount of time. Over the course of multiple projects, it can be a repetitive task.
In .NET MAUI, developers can leverage a root layout called Shell that is implemented by default when you create a new project inside the AppShell.xaml file. With the Shell, you can define the whole app architecture and hierarchy in one place and make use of a built-in navigation mechanism, search bar, and flyout menu.
In this chapter, you will learn the most common features of the Shell. However, this is a very sophisticated layout, so I suggest you bookmark the link to the official documentation page to keep as a reference for further study.
You will now see how to create an app with basic functionalities based on the sample project called Shell, located under the Chapter6 folder of the companion repository. As you will see, the Shell definition resides in the AppShell.xaml file, which is the place where all the code discussed shortly needs to be placed.
At its core, the Shell is a container of the following visual elements:
· Flyout menu: This is a side menu that can be shown or hidden with a swipe gesture and can contain other visual elements or, commonly, shortcuts to other pages. This is what Android users call a “hamburger” menu.
· Tab bar: This is usually placed at the bottom of the Shell and includes buttons with icons and text that users can tap to navigate to different pages.
· Search bar: This is used to search items in a list, typically by filtering a data-bound collection.
Visual elements in the Shell, such as flyout items and bar buttons, can be styled through resources. The flyout, tab bar, and search bar are independent from one another, so you could implement just one of them.
To implement a flyout, you just need to add FlyoutItem objects to the Shell. If you only want a tab bar instead of the flyout menu, you add a TabBar object to the Shell and then add a Tab object for each button you want to include. Then, you assign the FlyoutBehavior property of the Shell with Disabled. Both FlyoutItem and Tab need to be populated with an object of type ShellContent, which points to the page you want to open.
If you want to have both the flyout and the tab bar, you will nest a Tab object in a FlyoutItem object. If this all seems confusing, don’t worry. You will see proper examples in the next sections.
Suppose you want to create a flyout menu with three items, each pointing to a specific page. The code looks like the following:
<Shell … >
<FlyoutItem Title="Home" Icon="home.png">
<ShellContent ContentTemplate="{DataTemplate pages:HomePage}"/>
</FlyoutItem>
<FlyoutItem Title="About" Icon="about.png">
<ShellContent ContentTemplate="{DataTemplate pages:AboutPage}"/>
</FlyoutItem>
<FlyoutItem Title="Contact" Icon="contact.png">
<ShellContent ContentTemplate="{DataTemplate pages:ContactPage}"/>
</FlyoutItem>
</Shell>
For each FlyoutItem you can specify the Title, which is the text you see in the menu, and an icon (icon files must follow the rules of each operating system). The ShellContent object represents the page that is the target of the navigation.
The syntax based on the ContentTemplate, which receives a DataTemplate and the name of the target page as an argument, makes sure the instance of the target page is created only when required. You could also use the Content property instead of ContentTemplate, passing the name of the page directly, but in this case each page would be instantiated directly with potential performance overhead, so this is not recommended.
Data templates will be discussed in more detail in the next chapter as part of the data-binding topic. The result of this code is shown in Figure 47.

Figure 47: Flyout menu with the Shell
The flyout can be further customized via the following properties:
· FlyoutIcon: Sets the icon for the flyout.
· FlyoutHeader: Assigned with a view that appears at the top of the flyout. You can alternatively use the FlyoutHeaderTemplate property, which is populated with a DataTemplate.
· FlyoutBackgroundImage: Sets a background image for the flyout.
· FlyoutBackgroundImageAspect: Sets the aspect for the background image with the same values used for regular images (Fill, AspectFill, AspectFit).
· FlyoutIsPresented: Gets or sets the appearance status of the flyout. This can also be used in C# code to programmatically control the flyout appearance.
For a full list and description of the flyout properties, refer to the documentation.
Implementing the tab bar is accomplished by adding a TabBar object to the Shell, and then adding a Tab item for each navigation button you want. Continuing the previous example of three items, the code would look like this:
<Shell … >
<TabBar>
<Tab Title="Home" Icon="home.png">
<ShellContent ContentTemplate="{DataTemplate pages:HomePage}"/>
</Tab>
<Tab Title="About" Icon="about.png">
<ShellContent ContentTemplate="{DataTemplate pages:AboutPage}"/>
</Tab>
<Tab Title="Contact" Icon="contact.png">
<ShellContent
ContentTemplate="{DataTemplate pages:ContactPage}"/>
</Tab>
</TabBar>
</Shell>
As you can see, each Tab has a title and icon, like the FlyoutItem. Navigation is again performed via the ShellContent object. Notice how you do not need to handle any events to perform navigation; everything is handled by the Shell. Figure 48 shows how the tab bar looks.

Figure 48: Tab bar with the Shell
Note: The Shell takes care of navigation between pages with its built-in mechanism, but you have deep control over it, and you can even manage navigation at runtime. This is the topic of so-called routing, and I recommend you have a look at the documentation for further study.
If you want to have the same elements in both the flyout and the tab bar, you can declare one FlyoutItem and add Tab objects inside it, like in the following code:
<Shell … >
<FlyoutItem FlyoutDisplayOptions="AsMultipleItems">
<Tab Title="Home" Icon="home.png">
<ShellContent ContentTemplate="{DataTemplate pages:HomePage}"/>
</Tab>
<Tab Title="About" Icon="library.png">
<ShellContent ContentTemplate="{DataTemplate pages:AboutPage}"/>
</Tab>
<Tab Title="Contact" Icon="contact.png">
<ShellContent
ContentTemplate="{DataTemplate pages:ContactPage}"/>
</Tab>
</FlyoutItem>
</Shell>
In this case, you set the FlyoutDisplayOptions property with AsMultipleItems, which allows you to display several elements in a single flyout item. You can run the code and see how the app will show both the flyout with its items and the navigation bar.
Another piece of the Shell is the integrated search tool. The search tool is simply made of a class that derives from SearchHandler. Before implementing search, suppose your app displays a list of products that you want to filter as the user types. A product could be represented by the following Product class:
public class Product
{
public string ProductName { get; set; }
public int ProductQuantity { get; set; }
}
You need to then implement a collection of products. As you will learn in Chapter 7, XAML-based platforms work with the ObservableCollection<T> class.
You could then write the following code:
public class ProductCollection
{
public ObservableCollection<Product> Products { get; set; }
public ProductCollection()
{
var product1 = new Product { ProductName = "Bread", Quantity = 10 };
var product2 = new Product { ProductName = "Wearables",
Quantity = 15 };
var product3 = new Product { ProductName = "Wine", Quantity = 5 };
var product4 = new Product { ProductName = "Tomatoes",
Quantity = 100 };
Products = new ObservableCollection<Product>()
{ product1, product2, product3, product4 };
}
}
The Products property, of type ObservableCollection<Product>, will be the data source for both the list of products and the search UI. The next step is writing the search handler. Code Listing 14 shows an example.
Code Listing 14
public class ItemsSearchHandler : SearchHandler { public ObservableCollection<Product> Products { get; set; } public ItemsSearchHandler() { Products = new ProductCollection().Products; ItemsSource = Products; } protected override void OnQueryChanged(string oldValue, string newValue) { base.OnQueryChanged(oldValue, newValue); if (!string.IsNullOrWhiteSpace(newValue)) { ItemsSource = Products.Where(p => p.ProductName .ToLower().Contains(newValue.ToLower())) .ToList(); } } protected override async void OnItemSelected(object item) { base.OnItemSelected(item); await (App.Current.MainPage as Shell). GoToAsync($"ObjectDetails?name={((Product)item).ProductName}"); } } |
The ItemsSource property of the search handler contains the filtered list of items and starts with the full data of the source collection. In the OnQueryChanged event handler, which is invoked when the user types in the search box, you will filter the collection that is bound to your page.
The oldValue and newValue string arguments of OnQueryChanged allow you to get the value in the search box before and after typing, respectively. In this example, a LINQ query filters the list of product names based on the user input.
Tip: The OnQueryChanged method is one of two key points for search implementation. This is the exact place where you implement your search logic, beyond a simple example like the previous one. The second key point is how you assign the ItemsSource property of the search handler. In this example, it has been done by assigning it with a sample list of products. In real-world scenarios, you might consider data binding or the assignment of lists populated with data coming from databases or view models.
The OnItemSelected event is raised when the user selects one of the items displayed in the search tool and allows for navigating to a specific page. The example assumes there is a page called ObjectDetails, to which the instance of the selected object is passed via query string as an argument, after conversion.
From the XAML point of view, you would add the following code inside a ContentPage definition (the companion solution does this in the HomePage.xaml page):
<Shell.SearchHandler>
<local:ItemsSearchHandler Placeholder="Enter search term"
ShowsResults="true"
DisplayMemberName="ProductName" />
</Shell.SearchHandler>
The SearchHandler property is assigned with the SearchHandler implementation and allows you to specify placeholder text, whether the search box shows a preview of the results, and the name of the property from the bound collection that will be displayed (DisplayMemberName).
For a complete sample, you can add the following XAML code to display a list of products inside a CollectionView control:
<Grid Margin="20">
<CollectionView ItemsSource="{Binding Products}"
x:Name="ProductsCollectionView">
<CollectionView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding ProductName}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
The CollectionView will be discussed in detail in the next chapter. For now, you just need to know that it is populated via data binding by assigning its ItemsSource property with an instance of the Products collection, and that the way it displays data is established through an ItemTemplate object.
The very last step for the example is creating an instance of the ProductCollection class and assigning it to the data source of the page:
public HomePage()
{
InitializeComponent();
var data = new ProductCollection();
BindingContext = data;
}
Tip: As you will learn in the next chapter, the BindingContext property represents a page’s data source.
Figure 49 shows what the search implementation looks like.

Figure 49: The search tool within the Shell
Elements in the Shell, such as the flyout and tab bar, can be customized with different colors and fonts. This is accomplished by defining specific resources. Resources in MAUI are discussed in the next chapter, so here you get a preview. For example, you could apply custom colors to the Shell and to tab bar elements with the following code:
<Shell.Resources>
<ResourceDictionary>
<Style x:Key="BaseStyle" TargetType="Element">
<Setter Property="Shell.BackgroundColor" Value="LightGreen" />
<Setter Property="Shell.ForegroundColor" Value="White" />
<Setter Property="Shell.TitleColor" Value="White" />
<Setter Property="Shell.DisabledColor" Value="#B4FFFFFF" />
<Setter Property="Shell.UnselectedColor" Value="#95FFFFFF" />
<Setter Property="Shell.TabBarBackgroundColor"
Value="LightBlue"/>
<Setter Property="Shell.TabBarForegroundColor" Value="White"/>
<Setter Property="Shell.TabBarUnselectedColor"
Value="#95FFFFFF"/>
<Setter Property="Shell.TabBarTitleColor" Value="White"/>
</Style>
</ResourceDictionary>
</Shell.Resources>
Names for properties that accept styling are self-explanatory. You can also customize the appearance of the flyout and its elements. There are several additional ways to customize and configure the Shell, which are discussed in the documentation; I recommend you take a look for further study.
Note: There is much more to say about the Shell because it is a very sophisticated tool. This book could not cover topics such as custom navigation settings, custom renderers, or programmatic access to the Shell. If you consider implementing the Shell in your apps, I strongly recommend you bookmark the root of the official documentation, which contains everything you need to create full Shell experiences.
This chapter has introduced the pages available in .NET MAUI, explaining how you can display single-view content with the ContentPage object, group content into tabs with the TabbedPage, and group content into two categories with the FlyoutPage object.
You have looked at how the NavigationPage object provides a built-in navigation framework that not only displays a navigation bar, but also allows for navigating between pages programmatically. Finally, you have been introducted to the Shell, an object that simplifies app infrastructure by including a flyout menu, a tab bar, and a search box in one place.
In the next chapter, you will look at information about two important and powerful features of .NET MAUI: resources and data binding.