Xamarin.Forms - using Custom Renderer to customize the back button icon and text in Navigation Bar

Background

In this blog, I will introduce how to use custom renderer to customize the back button icon and text in app’s navigation bar.

There is reason that why I have to log this blog. Let’s say, if you want to customize some control in Xamarin.Forms using custom render, usually you will do this: First, learn what is the native control that is wrapped in Xamarin.Forms, then you create a new class which inherits from that control’s class. After that, you will create a renderer class for that control in the platform-specific project which is Android, iOS or UWP. You will use the normal format how custom renderer does on that class and implement the OnElementChanged method for the customization logic, the OnElementChanged method was called when the control was generated. At the end, you will be able to use that new control in your XAML page file.

The official document of how to use custom renderer is here: Xamarin.Forms Custom Renderers

Difficulties

It’s not that easy / standard to implement the custom renderer for navigation bar’s back button icon and text as we mentioned in the title. The reason for that is as below:

Let’s use Android as example. As I mentioned above, the Back button is shown on the Toolbar, and Toolbar is shown only when the Page is in a Navigation Stack. Therefore, the first thing comes to mind should be that we need to rewrite the control for NavigationPage. Based on that thought, the implementation should be as below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[assembly: ExportRenderer(typeof(ContentPage), typeof(NavigtaionPageRendererDroid))]
// ...
public class NavigationPageRendererDroid : PageRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Page> e)
{
base.OnElementChanged(e);

var context = (Activity)Xamarin.Forms.Forms.Context;
var toolbar = context.FindViewById<AppCompToolbar>(Droid.Resource.Id.toolbar);
if (toolbar != null)
{
if (toolbar.NavigationIcon != null)
{
toolbar.NavigationIcon = Android.Support.V7.Content.Res.AppCompatResources.GetDrawable(context, Resource.Drawable.back);
toolbar.Title = "Custom Back";
}
}
}
}

In that way, we should have changed the back button’s icon and text. Actually we did, but there will be issue, I will explain later in this blog.

The usage of that control is easy, just open the App.xaml.cs file, make the MainPage a navigation page and then add a button click event as below:

1
2
3
4
5
6
7
MainPage = new NavigationPage(new MainPage);
// ...
// When click a button in MainPage, navigate to another page.
private async void Button_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new SecondPage());
}

What’s the problem here?

First, in my demo/sample for implementing this, I will use this structure to demo: I have a page called MainPage which will be the first page to be loaded. Then I will have another 2 pages which called SecondPage and ThirdPage. In the MainPage and SecondPage, there will be a Label showing what page this is and a button that will do the navigation to next page. In the ThirdPage, there is only one Label showing this is the third page.

Ok, back to the problem. If we implement the custom renderer that way, you will find that the back button icon and text will show as the default effect about 1 seoncd sometimes especially on the emulators whose RAM are less or some slow real devices. After showing the default icon and text, it will show the custom icon and text as we expected. Of course this cannot be accpeted by the users.

Root cause

If you check on the source code for that part in Xamarin.Android, if we search for OnPushAsync method, you will find the implementaion as below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
protected virtual Task<bool> OnPushAsync(Page view, bool animated)
{
return SwitchContentAsync(view, animated);
}

Task<bool> SwitchContentAsync(Page page, bool animated, bool removed = false, bool popToRoot = false)
{
// Other implementation
if (animated)
{
if (!removed)
{
UpdateToolbar();
// Other implementation
}
}
// Other implementation
}

void UpdateToolbar()
{
// Other implementation
if (NavigationPage.GetHasBackButton(currentPage))
{
var icon = new DrawerArrowDrawable(activity.SupportActionBar.ThemedContext);
icon.Progress = 1;
bar.NavigationIcon = icon;

// Other implementation
}
// Other implementation
}

From the above, we can understand that whenever the navigation is done, Xamarin.Android will set the NavigationIcon every time. Since we are calling base.OnElementChanged(e), that’s why it will be set back to default at first then the custom renderer will take effect, which casued this issue.

Solution

So basically what we need to do is just as Xamarin does, we are not going to change the behavior when generating the control ( which means implementing in the OnElementChanged method ), but to override the OnPushAsync method.

But here we will have another difficulty, which is the implementation between Android and iOS cannot be exactly the same. If you read further, you will find Android is easier. The reason is that we only have to check whether in current page, Android will show the navigation bar items or not, if not, we just leave it. Otherwise we will modify the toolbar items, that’s all. But iOS will be a little more complicated. Let’s see how it’s done.

For Android

  1. Create a class named CustomNavigationPage in the .NET Standard project, and this class inherits from NavigationPage to make our custom renderer applied to that class.

    Code as below:

    1
    2
    3
    4
    5
    6
    7
    public class CustomNavigationPage : NavigationPage
    {
    public CustomNavigationPage (Page startupPage) : base(startupPage)
    {

    }
    }
Notice that there is no implementation at all for that class. It's just a target to which we will apply the custom renderer implementation.
  1. In Android project, create class named NavigationPageRenderer, override the OnPushAsync method.

    Code as below:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    using AppCompToolbar = Android.Support.V7.Widget.Toolbar;
    // ...
    [assembly: ExportRenderer(typeof(CustomNavigationPage), typeof(NavigationPageRenderer))]
    namespace NavBarBackImage.Droid.CustomRenderers
    {
    public class NavigationPageRenderer : Xamarin.Forms.Platform.Android.AppCompat.NavigationPageRenderer
    {
    public AppCompToolbar toolbar;
    public Activity context;

    protected override Task<bool> OnPushAsync(Page view, bool animated)
    {
    var retVal = base.OnPushAsync(view, animated);

    context = (Activity)Forms.Context;
    toolbar = context.FindViewById<AppCompToolbar>(Droid.Resource.Id.toolbar);

    if (toolbar != null)
    {
    if (toolbar.NavigationIcon != null)
    {
    toolbar.NavigationIcon = Android.Support.V7.Content.Res.AppCompatResources.GetDrawable(context, Resource.Drawable.back);
    toolbar.Title = "返回";
    }
    }

    return retVal;
    }
    }
    }

    Here is some explaination for Xamarin.Forms custom renderer.

    First is a necessary attribute, which is the first line of code: [assembly: ExportRenderer(typeof(CustomNavigationPage), typeof(NavigationPageRenderer))]. This line of code means: apply the implementation of NavigationPageRenderer to CustomNavigationPage class which is the newly created class in .NET standard project.

    The other key point is, why this renderer inherits from Xamarin.Forms.Platform.Android.AppCompat.NavigationPageRenderer instead of Xamarin.Forms.Platform.Android.NavigationRenderer. The reason is for compatibility for older Android versions which AppCompat is for.

    The last is the logic here, quite straightforward, check whether toolbar exists, whether icon is null or not. If both exists, then set the icon using Android resource, and set the text using your own title text.

  2. Usage:

    InApp.xaml.csfile, set the MainPage to NavigtaionPage, code as below:

    1
    2
    3
    4
    5
    6
    public App()
    {
    InitializeComponent();

    MainPage = new CustomNavigationPage(new MainPage());
    }

Then in the MainPage’s button click event, call the normal PushAsync method, code as below:

1
2
3
4
private async void Button_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new SecondPage());
}

That’s how it’s done for Android part.

For iOS

iOS is a little more complicated than Android, the reason is due to the below 2 difficulties.

Difficulty 1

The Back button for iOS is just a UIButton. And when it’s needed to be shown, there is a default effect for that button implemented by Xamarin.iOS. If we are going to customize this button, we are not rewriting the control implementation but to set the TopViewController.NavigationItem.LeftBarButtonItems. When the value is null, iOS will apply the default back button, but if we set the value for that LeftBarButtonItems, iOS will show what we set for the button items.

You will notice there is also a RightBarButtonItems in the TopViewController.NavigationItem, the reason we are setting the left one is that by default page is LTR (left to right), so the back button should be on the left top corner.

Below is the difference of navigation bar between the 3 platforms in Xamarin:

So for the nav bar in iOS, we can try to understand it as this way: the left part is LeftBarButtonItems, in the middle it’s the PageTitle, and on the right it’s the RightBarButtonItems. By default, button items for both left and right are null.

So the first difficulty for us is to understand that we are not modifying but set the property to meet the requirement.

Difficulty 2

It’s easier for Android part is that whether we modify or not modify the back button’s icon and text is determined easily. We simply check whether the navbar is null or not and also the same for navbar’s icon. If both are not null, we modify the icon and title, that’s all.

But for iOS part, we can always set the LeftBarButtonItems, but this will lead us to a problem. You will find the first loaded page, aka the MainPage will also have a back button in the nav bar which is obviously not correct. When you click on the back button on that page, definitely there can be an exception if we don’t handle it correctly.

Therefore, we have to implement the logic around Navigation Stack to figure out whether we really need to set the LeftBarButtonItems, or we use the default behavior.

To achieve the above, in our custom renderer class for iOS, not only we need to implement the OnPushAsync method, but also the OnPopViewAsync method.

Implementation

  1. Create a new class NavigationPageRendereriOS under iOS project which inhertis from Xamarin.Forms.Platform.iOS.NavigationRenderer. We need to declare 2 private fields in it, which are navigationStack and CurrentNavigationPage. Check below code structure:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    [assembly: ExportRenderer(typeof(CustomNavigationPage), typeof(NavigationPageRendereriOS))]
    // Change the namespace to yours
    namespace NavBarBackImage.iOS
    {
    public class NavigationPageRenderer : Xamarin.Forms.Platform.iOS.NavigationRenderer
    {
    // Private fields
    private readonly Stack<NavigationPage> _navigationPageStack = new Stack<NavigationPage>();
    private NavigationPage CurrentNavigationPage => _navigationPage.Peek();

    // Constructor
    public NavigationPageRendereriOS() : base()
    {

    }

    // Below should be the override and custom methods which will be explained later in this blog.

    // ...
    }
    }
  2. Let’s implement 2 methods, one is SetImageTitleBackButton, which is to set the LeftBarButtonItems. The other is the opposite which is to set the default effect. This method will have 2 effects, if there is no page to navigate back, there will be no back button at all. But if for some specifc page, you do not want to use the custom renderer back button, you call also call this method to show the default one.

    Check code for SetImageTitleBackButton as below:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    void SetImageTitleBackButton(string imageBundleName, string buttonTitle, int horizontalOffset)
    {
    var topVC = this.TopViewController;

    // Create the image back button
    var backButtonImage = new UIBarButtonItem(
    UIImage.FromBundle(imageBundleName),
    UIBarButtonItemStyle.Plain,
    (sender, args) =>
    {
    topVC.NavigationController.PopViewController(true);
    });

    // Create the Text Back Button
    var backButtonText = new UIBarButtonItem(
    buttonTitle,
    UIBarButtonItemStyle.Plain,
    (sender, args) =>
    {
    topVC.NavigationController.PopViewController(true);
    });

    backButtonText.SetTitlePositionAdjustment(new UIOffset(horizontalOffset, 0), UIBarMetrics.Default);

    // Add buttons to the Top Bar
    UIBarButtonItem[] buttons = new UIBarButtonItem[2];
    buttons[0] = backButtonImage;
    buttons[1] = backButtonText;

    topVC.NavigationItem.LeftBarButtonItems = buttons;
    }

    From the above code, we can see that TopViewController.NavigationItem.LeftBarButtonItems is actually an array which can contain 2 elements. Both elements are for type UIBarButtonItem. In my code, I put the bundle image as the first element which is an down arrow, and the second element is a string which to be displayed after the image.

    Let’s check the meaning for the 3 parameters for that method.

    • imageBundleName: this parameter indicates the name of the bundle image, which in my case is the down arrow icon. Type of the parameter is string. About how to set a bundle image in Xamarin.iOS, check here: Displaying an image in Xamarin.iOS
    • buttonTile: this is a single string used to set the text after the arrow icon. We can set it to anything we want to show, such as back, return, etc.
    • horizontalOffset:as the name, it’s the horizontal offset to set the offset between bundle image and text.

      Now let’s take a look at the implementation of method SetDefaultBackButton:

      1
      2
      3
      4
      void SetDefaultBackButton()
      {
      this.TopViewController.NavigationItem.LeftBarButtonItems = null;
      }

      Quite staightforward, just set the LeftBarButtonItems to null.

  3. Now let’s define the logic when to set the custom button and when to set the default. In my sample project, it’s quite simple logic: if the page is the first loaded page, which is MainPage, then we set to default back button. Otherwise, we set our custom back button.

    Code as below:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void SetBackButtonOnPage(Page page)
    {
    if (page.GetType() == typeof(MainPage))
    {
    SetDefaultBackButton();
    }
    else
    {
    SetImageTitleBackButton("Down", "返回", -15);
    }
    }

    Notice the first parameter “Down”, it’s the bundle image name that I set for the down arrow.

  4. Now let’s implement the OnPushAsync method which is the easier one. Code as below:

    1
    2
    3
    4
    5
    6
    7
    8
    protected override Task<bool> OnPushAsync(Page page, bool animated)
    {
    var retVal = base.OnPushAsync(page, animated);

    SetBackButtonOnPage(page, animated);

    return retVal;
    }

    Nothing much to explain here. It first calls the basic implementation of the OnPushAsync, after that, set the back button following the logic that we defined.

  5. At the end, it’s the OnPopViewAsync method, code as below:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    protected override Task<bool> OnPopViewAsync(Page page, bool animated)
    {
    var retVal = base.OnPopViewAsync(page, animated);

    var stack = page.Navigation.NavigationStack;

    var returnPage = stack[stack.Count - 2];

    if (returnPage != null)
    {
    SetBackButtonOnPage(returnPage);
    }

    return retVal;
    }

    The main code here is: var returnPage = stack[stack.Count - 2];, which gives us the next returned page. And then pass this page to the SetBackButtonPage method, so that we can set the correct back button.

    The reason it’s minus 2 (-2) is because array starts from 0. Assume that we navigated twice and the current page is ThirdPage, at the moment, the stack.Count returns 3. That’s because there are totally 3 elements in the navigation stack. If you click on back button, what we return to is the SecondPage, which is stack[1] obviously. So, 3-2=1, explained why we are minusing 2.

Ending

At this moment, this demo is done. Actually we can do lots of other customizations regarding this feature. But to demonstrate, or to meet the common usage, I just set all the back buttons to a unified style. You can also check the whole sample here.

If you got any confusion or difficullties, please leave comments or go to the About Me and follow the instructs to create Xamarin case to me. I will help you on that.

Thanks for reading and happy coding!

How to implement Java Callback in constructor using C#
You need to set install_url to use ShareThis. Please set it in _config.yml.

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×