Marek
Marek

Reputation: 1788

Binding issue in custom Xamarin.Forms control

I have a strange problem with bindings on a custom control. I created a custom toolbar:

public partial class TopToolbar
{
    public static readonly BindableProperty BackCommandProperty =
        BindableProperty.Create(nameof(BackCommand), typeof(ICommand), typeof(TopToolbar), propertyChanged: BackCommandChanged);

    public ICommand BackCommand
    {
        get => (ICommand) GetValue(BackCommandProperty);
        set => SetValue(BackCommandProperty, value);
    }

    public TopToolbar()
    {
        InitializeComponent();
    }

    // for debug purposes only
    protected override void OnBindingContextChanged()
    {
        base.OnBindingContextChanged();
        Debug.WriteLine(BindingContext);
    }

    // for debug purposes only
    private static void BackCommandChanged(BindableObject bindable, object oldvalue, object newvalue)
    {
        Debug.WriteLine($"old: {oldvalue}, new: {newvalue}");
    }
}


<?xml version="1.0" encoding="UTF-8"?>
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Core.Controls.TopToolbar"
             x:Name="TopToolbarView"
             BindingContext="{x:Reference TopToolbarView}"
             Orientation="Vertical">
            <StackLayout Orientation="Horizontal"
                         HorizontalOptions="FillAndExpand"
                <Image Source="{StaticResource Image.Toolbar.LeftArrow}">
                    <Image.GestureRecognizers>
                        <TapGestureRecognizer Command="{Binding BackCommand}" />
                    </Image.GestureRecognizers>
                </Image>
            </StackLayout>

</StackLayout>

I use it on a page in this way:

<pages:ContentPage.Content>
        <StackLayout BackgroundColor="{StaticResource LightGrayColor}"
                     Spacing="0"
                     Padding="0">
            <controls:TopToolbar Title="Master Data" BackCommand="{Binding MyBackCommand}" />

BindingContext of the page is a view model:

public class MyCustomersPageModel
{
    public RelayCommand MyBackCommand { get; set; }

    public MyCustomersPageModel()
    {
        MyBackCommand = // command creation;
    }
}

From the debugging I know that BindingContext of a control is set (OnBindingContextChanged called) properly to itself (TopToolbar object) twice - first time when there's no child views and second time after they are added. I've checked that BindingContext is correctly propagated in all child controls.

Unfortunately the BackCommand is not bind at all. The setter of the TopToolbar.BackCommand is not called even once.

Interestingly when I replace setting the BindingContext on a control to setting the Souce directly in bindings everything works fine:

<?xml version="1.0" encoding="UTF-8"?>
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Core.Controls.TopToolbar"
             x:Name="TopToolbarView"
             Orientation="Vertical">
            <StackLayout Orientation="Horizontal"
                         HorizontalOptions="FillAndExpand"
                <Image Source="{StaticResource Image.Toolbar.LeftArrow}">
                    <Image.GestureRecognizers>
                        <TapGestureRecognizer Command="{Binding Source={x:Reference TopToolbarView}, Path=BackCommand}" />
                    </Image.GestureRecognizers>
                </Image>
            </StackLayout>

</StackLayout>

Any clue what I do wrong?

Upvotes: 0

Views: 1086

Answers (1)

Sharada
Sharada

Reputation: 13591

It is working as expected. I would recommend using Source.

In first case, when you set BindingContext on TopToolbar to itself, then I would imagine this would be the sequence of events:

  1. Custom control is constructed, BindingContext is assigned to self through reference.

  2. Page instance is created, control is added to it.

  3. When page's BindingContext is set, through power of property inheritance, all it's child controls' BindingContext is updated.

  4. At this point your custom control's BindingContext is still referencing itself as value-propagation doesn't override manually set context.

  5. Therefore, binding <controls:TopToolbar BackCommand="{Binding MyBackCommand}" fails, as this binding will try to look for MyBackCommand on it's binding-context which is TopToolbar.

But, in second case, when you specify binding Source as TopToolbar on Tapped command, then this should be the sequence of events:

  1. Custom control is constructed, BindingContext is null.

  2. Page instance is created, control is added to it.

  3. When page's BindingContext is set, through power of property inheritance, all it's child controls' BindingContext is updated, including your custom control.

  4. At this point your custom control's BindingContext is now referencing MyCustomersPageModel. So binding in <controls:TopToolbar BackCommand="{Binding MyBackCommand}" is appropriately set.

  5. Now the Tapped binding doesn't care about BindingContext as it's source is explicitly specified, which is parent control TopToolbar - whose BackCommand in turn is bound to the view model's command. Hence, the view-model command is now bound to gesture-recognizer's Tapped command. And it works!

Upvotes: 3

Related Questions