14
votes

I have a problem binding data in a custom view in Xamarin forms to the view model of the containing page.

My Custom View is very simple, a pair of labels representing a key value pair:

<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="KeyValueView">
    <Grid VerticalOptions="Start">
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <Label x:Name="KeyLabel" Text="{Binding Key}"  Grid.Column="0" HorizontalOptions="Start" />
        <Label x:Name="ValueLabel" Text="{Binding Value}" Grid.Column="1" HorizontalOptions="EndAndExpand" />
    </Grid>
</ContentView>

with the code behind:

public partial class KeyValueView : ContentView
{
    public KeyValueView()
    {
        InitializeComponent();
        this.VerticalOptions = LayoutOptions.Start;
        this.BindingContext = this;
    }

    public static readonly BindableProperty ValueProperty =
                BindableProperty.Create<KeyValueView, string>(w => w.Value, default(string));

    public string Value
    {
        get {return (string)GetValue(ValueProperty);}
        set {SetValue(ValueProperty, value);}
    }

    public static readonly BindableProperty KeyProperty =
        BindableProperty.Create<KeyValueView, string>(w => w.Key, default(string));

    public string Key
    {
        get {return (string)GetValue(KeyProperty);}
        set {SetValue(KeyProperty, value);}
    }
}

This is consumed in a page as follows:

<views:KeyValueView Key="Order Number" Value="{Binding document_number}" />

The problem is that the Key string is displayed as expected, but the value string is not. I have tried forcing a PropertyChanged event on the document_number property, this did not help. I have also tried explicitly setting the Text property on the Labels during the setter of the Key/Value properties of the custom view:

public string Key
    {
        get {return (string)GetValue(KeyProperty);}
        set {
            SetValue(KeyProperty, value);
            KeyLabel.Text = value;
        }
    }

Again this did not help, the setter code never seems to get executed (I placed a breakpoint on it and it was not hit)

If I add an out of the box control such as a Label bound directly to the property in the page, this displays correctly:

<Label Text="{Binding document_number}"/>
<views:KeyValueView Key="Order Number" Value="{Binding document_number}" />

Can anyone explain why this is happening (or rather not happening)?

2

2 Answers

27
votes

Don't assign bindings internally inside custom controls. Use this:

public partial class KeyValueView : ContentView
{
    public KeyValueView()
    {
        InitializeComponent();
        this.VerticalOptions = LayoutOptions.Start;
    }

    public static readonly BindableProperty ValueProperty =
        BindableProperty.Create<KeyValueView, string>(w => w.Value, default(string));

    public string Value
    {
        get {return (string)GetValue(ValueProperty);}
        set {SetValue(ValueProperty, value);}
    }

    public static readonly BindableProperty KeyProperty =
        BindableProperty.Create<KeyValueView, string>(w => w.Key, default(string));

    public string Key
    {
        get {return (string)GetValue(KeyProperty);}
        set {SetValue(KeyProperty, value);}
    }
    protected override void OnPropertyChanged(string propertyName)
    {
        base.OnPropertyChanged(propertyName);

        if (propertyName == ValueProperty.PropertyName)
        {
            ValueLabel.Text = Value;
        }
        if (propertyName == KeyProperty.PropertyName)
        {
            KeyLabel.Text = Key;
        }
    }
}

<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="KeyValueView">
    <Grid VerticalOptions="Start">
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <Label x:Name="KeyLabel" Grid.Column="0" HorizontalOptions="Start" />
        <Label x:Name="ValueLabel" Grid.Column="1" HorizontalOptions="EndAndExpand" />
    </Grid>
</ContentView>

Treat properties (eg. Value) as references to BindableProperty (ValueProperty). If you do something inside Value setter BindableProperty won't be notified about it AND opposite if you do something with BindableProperty (assign/change ValueProperty) - property Value setter won't be called.

If you want to handle PropertyChanged you have an override for that (OnPropertyChanged) or Actions (as additional parameters when creating BindableProperty).

1
votes

I was able to get this to work by doing

public KeyValueView()
{
    InitializeComponent();
    this.VerticalOptions = LayoutOptions.Start;
    this.Content.BindingContext = this;
}

and then doing this in the xaml.cs for the view you're using the custom view in:

public ParentView()
{
    InitializeComponent();
    BindingContext = this;
}

For an Entry field, which gave me problems, I had to make sure the defaultBindingMode parameter was set to TwoWay.