There's really no good title to describe what this article is about. (Sometimes naming posts is almost as difficult as naming variables and classes in code.)

But have you noticed when using a MasterDetailPage in Xamarin.Forms that the upper left navigation button ... the one that controls revealing the Master page (which I'll call the reveal button from here on out) ... will either display an icon or text - but not both?

99% of the time you can put a hamburger icon there and things will be fine ... but it's that 1% of the time that your boss, or customer, or worse, graphic designer requests that they want to see both an icon and text in the upper left corner.

Initially I thought this would be super easy - just need to implement an Effect and away I would go. Nope - not so fast.

What to do then?

Well - I hope to have you covered with this article! It will show you how to get the Xamarin.Forms MasterDetailPage to work on iOS so it displays text and images in the left nav bar.

And it's quite a journey ... we're going to hit up Custom Renderers, Attached Properties, and Bindable Properties - all in order to get this to work.

Right now be aware that this technique is targeted only towards iOS - not Android. Kinda defeats the purpose of Xamarin.Forms, right? Well, Android still works as normal for the MasterDetailPage there's just a bit of redundancy in setting properties.

All of the same code for this article can be found on GitHub here.

The Custom Renderer

The dreaded words ... custom renderer.

With the advent of effects and bindable native views and platform specifics - I thought the days of having to write a custom renderer was behind me.

But after trying to implement this as an Effect, I found I had no other choice...

So that's where I started ... I subclassed the MasterDetailPage in the core project and then went into the iOS project and created a renderer for it.

The first thing that one needs to understand is that the MasterDetailPage in Xamarin.Forms really is just a construct over 2 distinct "real" concepts.

In other words a MasterDetailPage holds a Master page and a Detail page ... duh, right?

Well, that fact starts to present some issues when you get into rendering it.

Rendering A MasterDetailPage

First off there several classes you can pick from when creating a custom renderer for a MasterDetailPage on iOS: PhoneMasterDetailRenderer and TabletMasterDetailRenderer.

I will be working with PhoneMasterDetailRenderer as that's the one which displays the button for the reveal.

Normally when you create a custom renderer - you do all the "customization" from the OnElementChanged function.

But that's not the case here ...

There are 2 properties which can be used to gain access to the "native" portion of the master-detail NativeView and ViewController. But I found that neither of those can access the UINavigationBar which will be the host for the reveal button.

Not a problem I thought - there's an Element property that will be a subclass of MasterDetailPage and I can use that to get the Detail page - grab it's UINavigationBar and pop the reveal button onto that.

No dice.

The Detail property is null during the OnElementChanged function - even when the "NewElement" is being set.

Luckily the PhoneMasterDetailRenderer inherits from UIViewController - which means there is a ViewDidLoad function to override.

Inside that function, I was able to get at the Detail property and then convert it to its native representation of UINavigationController. (Because the Detail will need to be in a navigation controller in order for the navigation bar to be visible.)

if (!(Element is MasterDetailLeftIconTextPage mdp)) return;
if (!(Platform.GetRenderer(mdp.Detail) is UINavigationController nc)) return;

This piece of code is doing the following:

  1. Taking the Element property of the custom renderer and casting it to the Xamarin.Forms MasterDetailPage subclass I'm using for this renderer.
  2. Then using the Detail property - getting the native representation of it via the Platform.GetRenderer.

Great - now I have the UINavigationController that I need to add the reveal button to!

Adding the button to the navigation bar is the standard run of the mill iOS code, and a function that only adds the button (and gets called from ViewDidLoad) looks like the following:

void SetupLeftButton(string buttonText = "", string buttonIcon = "")
{
    if (!(Element is MasterDetailLeftIconTextPage mdp)) return;
    if (!(Platform.GetRenderer(mdp.Detail) is UINavigationController nc)) return;

    UIButton btn = new UIButton(UIButtonType.Custom);
    btn.SetTitleColor(btn.TintColor, UIControlState.Normal);

    btn.Frame = new CGRect(0, 0, 100, 44);

    var title = string.IsNullOrEmpty(buttonText) ? mdp.Master.Title : buttonText;
    var icon = string.IsNullOrEmpty(buttonIcon) ? mdp.Master.Icon.File : buttonIcon;

    var img = UIImage.FromFile(icon);
    btn.SetTitle(title, UIControlState.Normal);
    btn.SetImage(img, UIControlState.Normal);
    btn.ImageEdgeInsets = new UIEdgeInsets(0, -15, 0, 0);
    btn.TouchUpInside += (sender, e) => mdp.IsPresented = true;

    var lbbi = new UIBarButtonItem(btn);

    nc.NavigationBar.TopItem.LeftBarButtonItem = lbbi;
}

The big thing to notice is that the function gets passed a string for the button's text and image.

If either of those are null - the code uses the Title or Icon property set on the Xamarin.Forms Master page.

It also makes sure the master either shows itself or disappears on the TouchUpInside event.

Making the Text and Icon Change

Well, the hard part's over now, right?

I found a way to get at the navigation bar, customize the reveal button to show both the text & icon, now the only thing that's left to do is monitor for changes on either the Master page's Title and Icon properties.

Super easy!

Not so fast.

I had figured I could use the Element_PropertyChanged function within the custom renderer and I would get notified whenever something on the Xamarin.Forms MasterDetailPage gets changed. And the Title and Icon properties of the Master property surely are one of those, right? Wrong.

Turns out the Element_PropertyChanged only responds to changes on properties on the MasterDetailPage itself - not to any of its children. That makes sense in hindsight, but I had hoped for an easier solution.

What to do?

Attached Properties to the Rescue!!

A while back I wrote a post about attached properties, finally figuring out what they're useful for.

And they sure seem like they would be useful in this case!

So - I put two attached properties into the subclass of the MasterDetailPage. This will allow the Master page to bind to those - thus causing the ElementProperty_Changed function to get fired!

But ... that didn't work either.

Turns out that attached properties will fire the ElementProperty_Changed of the control's renderer they are attached to - but not the one they are defined to be apart of.

The control they are defined to be apart of does have reference to them though. And you can setup a handler for the PropertyChanged delegate.

Hooray!! Now I know what I need to do!

Bindable Properties to the Rescue!!

All that needs to be done now is to create some bindable properties on the MasterDetailPage.

So - the bindable property definitions are super simple - can't get any more basic than this:

public static readonly BindableProperty InternalLeftTitleProperty = BindableProperty.Create("InternalLeftTitle", typeof(string), typeof(MasterDetailLeftIconTextPage), 
                                                                                            string.Empty, BindingMode.Default);

string InternalLeftTitle
{
    get => (string)GetValue(InternalLeftTitleProperty);
    set => SetValue(InternalLeftTitleProperty, value);
}

public static readonly BindableProperty InternalLeftIconProperty = BindableProperty.Create("InternalLeftIcon", typeof(string), typeof(MasterDetailLeftIconTextPage), 
                                                                                           string.Empty, BindingMode.Default);

public string InternalLeftIcon
{
    get => (string)GetValue(InternalLeftIconProperty);
    set => SetValue(InternalLeftIconProperty, value);
}

I'm calling them "Internal" because they shouldn't actually be bound to anything.

The attached properties that do get attached and bound, look like this:

public static readonly BindableProperty LeftTitleProperty = BindableProperty.CreateAttached("LeftTitle", typeof(string), typeof(MasterDetailLeftIconTextPage),
                                                                                            string.Empty, BindingMode.Default, null, LeftTitleChanged);

public static string GetLeftTitle(BindableObject bindable)
{
    return (string)bindable.GetValue(LeftTitleProperty);
}

public static void SetLeftTitle(BindableObject bindable, string value)
{
    bindable.SetValue(LeftTitleProperty, value);
}

protected static void LeftTitleChanged(BindableObject bindable, object oldValue, object newValue)
{
    if (!(((Page)bindable)?.Parent is MasterDetailLeftIconTextPage parent)) return;

    parent.InternalLeftTitle = newValue.ToString();
}

public static readonly BindableProperty LeftIconProperty = BindableProperty.CreateAttached("LeftIcon", typeof(string), typeof(MasterDetailLeftIconTextPage),
                                                                                           string.Empty, BindingMode.Default, null, LeftIconChanged);

public static string GetLeftIcon(BindableObject bindable)
{
    return (string)bindable.GetValue(LeftIconProperty);
}

public static void SetLeftIcon(BindableObject bindable, string value)
{
    bindable.SetValue(LeftIconProperty, value);
}

protected static void LeftIconChanged(BindableObject bindable, object oldValue, object newValue)
{
    if (!(((Page)bindable)?.Parent is MasterDetailLeftIconTextPage parent)) return;

    parent.InternalLeftIcon = newValue.ToString();
}

Notice how in the PropertyChanged delegate the "Internal" bindable property gets called.

Back to the Renderer!

Almost done now!

The custom renderer will listen for those properties to change within its ElementProperty_Changed function - the code in the renderer will look like this:

void Element_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    if (!(Element is MasterDetailLeftIconTextPage mdp)) return;

    if (e.PropertyName == MasterDetailLeftIconTextPage.InternalLeftTitleProperty.PropertyName || e.PropertyName == MasterDetailLeftIconTextPage.InternalLeftIconProperty.PropertyName)
    {
        var title = (string)mdp.GetValue(MasterDetailLeftIconTextPage.InternalLeftTitleProperty);
        var icon = (string)mdp.GetValue(MasterDetailLeftIconTextPage.InternalLeftIconProperty);

        SetupLeftButton(title, icon);
        mdp.IsPresented = false;
    }
}

Just checks to see if the property being changed is the correct one - and then sets the reveal button up properly.

It does suffer from the misfortune of being called two times in a row when the title and icon change at the same time. I still need to fix that.

Consuming This Thing

The very last thing to do is to setup the ContentPage that serves as the MasterDetail.Master page.

One needs to use both the existing Title and Icon properties as well as the new attached properties. (Duplicates code, I know.)

And this code example is assuming there's a view model bound to the page - with the properties MasterTitle and MasterIcon being bound to the Title, LeftTitle and Icon and LeftIcon respectively.

<ContentPage.Title>
    <Binding Path="MasterTitle"></Binding>
</ContentPage.Title>
<ContentPage.Icon>
    <Binding Path="MasterIcon"></Binding>
</ContentPage.Icon>

<local:MasterDetailLeftIconTextPage.LeftTitle>
    <Binding Path="MasterTitle"></Binding>
</local:MasterDetailLeftIconTextPage.LeftTitle>

<local:MasterDetailLeftIconTextPage.LeftIcon>
    <Binding Path="MasterIcon"></Binding>
</local:MasterDetailLeftIconTextPage.LeftIcon>

And there we go!! It works!!

Download the sample code & see for yourself.

What About MessagingCenter?

Yeah, yeah, yeah ... Instead of creating the bindable properties - and then using the ElementProperty_Changed function - I could have just used the attached properties - and then the MessagingCenter to broadcast a message. If the renderer was subscribed, it could then automatically do the update that way.

I suppose that would work (I didn't test it) ... but for some reason I don't like using the MessagingCenter if I can find a way around it.

However - I may end up using it for the Android version of all this ...

Summary

So it was a big rigamarole to get this all working properly. (And to be honest, I don't know if it's the best way of doing it either.)

The first issue was that the PhoneMasterDetailRenderer doesn't give access to the Master or Detail native controls in the OnElementChanged function.

Secondly, there's no good way to monitor for changes on any of the child properties from the custom renderer of a MasterDetailPage.

That necessitated creating an attached property on the MasterDetailPage's subclass ... but that doesn't fire the ElementProperty_Changed function in the custom renderer either.

Finally, by creating new bindable properties - and having the attached properties change those when the attached properties' value changes - the custom renderer can be notified of changes on one of the child controls!

Whew!