Replacing the Xamarin Header/Navigation bar with a custom view
Intro:
I think I've started countless apps and said to myself, hey you really need to sit down and take the time to explain to yourself how exactly these Xamarin Forms custom header bars are actually working to get an understanding on how to maintain existing ones and to be able to develop better ones in the future. So here goes, my explanation of how to build a Xamarin Forms custom header/navigation bar with back button that'll work on any device past and present.
The code:
First things first. In a typical Xamarin Forms project, you'll have an App.cs in the shared/portable project, this is great but we need a .xaml file to go with it, so right click the project, click 'Add' and then click 'New Item...' When the window opens, choose 'Forms Xaml Page' then name it App.xaml and click 'Add'. This'll add the xaml file to your project, but there's already an issue. Yay! Visual Studio has created another cs file for this page called App.xaml.cs, we don't want this as it'll conflict with our existing file so delete the cs file and we're ready to make some changes to the xaml file. Open the xaml file and delete everything in it, then paste in the following code. I'll explain what it does once you've done it. ;)
<?xml version="1.0" encoding="utf-8"?> <Application xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyAppNamespace.App"> <Application.Resources> <!-- Application resource dictionary --> <ResourceDictionary> <ControlTemplate x:Key="MainPageTemplate"> <Grid VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" ColumnSpacing="0" RowSpacing="0"> <Grid.RowDefinitions> <RowDefinition Height="110"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!-- Begin Header --> <StackLayout Orientation="Vertical" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" Spacing="0" BackgroundColor="#2B2B2B" HeightRequest="100" Grid.Column="0" Grid.Row="0"> <StackLayout.Padding> <OnPlatform x:TypeArguments="Thickness" iOS="10, 10, 10, 10" Android="10, 10, 10, 10" WinPhone="10, 10, 10, 10" /> </StackLayout.Padding> <!-- My header content to make my app the prettiest. -->
<Image Source="back128.png" WidthRequest="30" HeightRequest="30">
<Image.GestureRecognizers>
<TapGestureRecognizer Tapped="OnBackButtonPressed" />
</Image.GestureRecognizers>
</Image> </StackLayout> <!-- End Header --> <!-- Begin Content --> <ScrollView Grid.Column="0" Grid.Row="1"> <ContentPresenter VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" /> </ScrollView> <!-- End Content --> </Grid> </ControlTemplate> </ResourceDictionary> </Application.Resources> </Application>
As you can see above, App.xaml is a sort of hybrid between an app configuration and a layout template, it allows you to define a set of application resources and within that, a set of control templates, these templates are what we need to set up our custom header/navigation bar because once you define a ControlTempate with a key, you're then free to write the layout code for that template directly within the control template. You might wonder why I use an Image and then assign a GestureRecognizer instead of just having an image button... Well my method has a little more code but will allow you to rezise images that you assign unlike the image button which will just crop the image if it's too large.
Before we move on, I'll save you the trouble of the 'Application.Resources StaticResource not found for key' error which you would later run into if you do not follow the next step.
Open the original App.cs file and within the constructor for that class, before everything else, add 'InitializeComponent();' as when we get round to testing, this will solve the error I've spoken about above which will save you a bit of head scratching.
Thinking back to when we added App.xaml and we spoke about GestureRecognizers for the back button, well now is when you want to add a method for that button to call. As you've already seen, we called a method named 'OnBackButtonPressed'. App.xaml is the file that will handle this method so paste the following code into it and your back button should work.
public async void OnBackButtonPressed(object sender, EventArgs e) { await Navigation.PopAsync(); }
So that you can confirm you've got yours set up correctly, here's what your App.cs should look like now:
using System; using Xamarin.Forms; namespace MyAppNamespace { public partial class App : Application { public static NavigationPage Navigation = null; public App() {
// We have to have this here to stop 'Application.Resources StaticResource not found for key error' InitializeComponent(); Navigation = new NavigationPage(new MainPage()); Application.Current.MainPage = Navigation; } protected override void OnStart() { // Handle when your app starts } protected override void OnSleep() { // Handle when your app sleeps } protected override void OnResume() { // Handle when your app resumes }
// Called by the back button in our header/navigation bar. public async void OnBackButtonPressed(object sender, EventArgs e) { await Navigation.PopAsync(); } } }
Now if your app doesn't yet have a main page to load when it starts, now is when you'll want to add it, use the same method we've already covered above for adding a new xaml page to your project, you can even add it under a folder if you want to keep the project tidy (which you should). Notice that in the example above, I create a new MainPage and wrap it in a NavigationPage then set the applications main page to it. This loads the class within the MainPage.xaml file and also allows my app to control navigation using the navigation stack. In case you didn't already know, the navigation stack is used to push and pop pages to and from the users view.
Another thing to look at in the example above is how I set the NavigationPage to a variable in App.cs, some would frown upon this so do it at your own discretion, all this does is allow me to push an pop pages from my navigation stack by calling 'App.Navigation.PopAsync();' instead of '((NavigationPage)Application.Current.MainPage).PopAsync();'
All that's left to do is to go through all the page cs files that this affects and turn off the default header/navigation bar by adding the following line to the page constructor:
NavigationPage.SetHasNavigationBar(this, false);
And add this line the ContentPage element within the xaml page files:
ControlTemplate="{StaticResource MainPageTemplate}"
Your content pages shuld look something ike this
<!--?xml version="1.0" encoding="utf-8" ?-->
<contentpage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:class="MyAppNamespace.MainPage"
controltemplate="{StaticResource MainPageTemplate}"
backgroundcolor="#FFFFFF"
title="My App">
<stacklayout orientation="Vertical" horizontaloptions="Fill" verticaloptions="FillAndExpand" x:name="mainContainer" spacing="10">
<stacklayout.padding>
<onplatform x:typearguments="Thickness" ios="10, 10, 10, 10" android="10, 10, 10, 10" winphone="10, 10, 10, 10"></onplatform>
</stacklayout.padding>
<label x:name="Title" text="My page title" fontsize="28" textcolor="#000" fontattributes="Bold"></label>
<label x:name="Summary" text="Content here..." fontsize="16"></label>
</stacklayout>
</contentpage>
Conclusion:
So turns out that this is a very simple and neat way to get what a web developer might call a partial view setup in Xamarin, almost like how masterpages used to work where you define the template and then specify where you want the content to load.
Other Resources:
Xamarin forums post that helps with a few tricky bits:
https://forums.xamarin.com/discussion/31872/application-resources-staticresource-not-found-for-key
Brilliant blog post by wolfeprogrammer that helped my understanding of this a lot:
https://wolfprogrammer.com/2016/07/07/custom-app-header-in-forms/
Published at 10 Jan 2017, 14:15 PM
Tags: Xamarin,C#,Xaml,Android,iOS,Windows Phone