0

I'm having problems binding text to a label inside a custom control.

I have made the following control:

<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Frida3.Components.TestView">
  <ContentView.Content>
      <StackLayout>
          <Label Text="{Binding Text}" />
      </StackLayout>
  </ContentView.Content>
</ContentView>

With the following code-behind:

[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class TestView : ContentView
{
    public TestView ()
    {
        InitializeComponent ();
        BindingContext = this;
    }

    public static readonly BindableProperty TextProperty =
        BindableProperty.Create("Text", typeof(string), typeof(TestView), default(string));

    public string Text
    {
        get => (string) GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }
}

When using the control and a binding to set the Text property, nothing is shown. Below is some sample code of showing the results:

<!-- Shows that the LoginText binding contains value -->
<Label Text="{Binding LoginText}" BackgroundColor="BurlyWood"/>
<!-- Nothing shown with same binding -->
<components:TestView Text="{Binding LoginText}" BackgroundColor="Aqua" />
<!-- Works without binding -->
<components:TestView Text="This is showing" BackgroundColor="Yellow" />

And here is the result of that:

Screenshot

Inrego
  • 1,524
  • 1
  • 15
  • 25
  • You're setting the binding context here to the code behind file. So if my understanding is correct, it's looking at the CodeBehind for a property LoginText and failing because it doesn't exist. In the xaml, on the middle one, try setting BindingContext='{Binding ViewModel}' or alternatively, get rid of BindingContext assignment in the constructor. – Max Hampton Nov 29 '18 at 00:01
  • @MaxHampton I tried removing the BindingContext assignment in the constructor, but the result was both Aqua and Yellow views were empty. I think that BindingContext is isolated inside that element, and is required for the binding on the Label. – Inrego Nov 29 '18 at 00:05
  • Adding bindings for a custom control creates unnecessarily complex code and potentially affects performance. You're much better off using the `HandlePropertyChanged` event. – Tom Nov 29 '18 at 13:36
  • @Tom I'm not sure I exactly understand what you mean. Can you provide an example? How else to make reusable controls, if not for binding data to it? – Inrego Nov 30 '18 at 12:40

2 Answers2

1

I found a solution. Basically I give my control a name (x:Name="this"), and I add a source to the data binding, like so: Source={x:Reference this}. With these changes, I can remove BindingContext = this;

So the final xaml will be like so:

<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Frida3.Components.TestView"
             x:Name="this">
  <ContentView.Content>
      <StackLayout>
          <Label Text="{Binding Path=Text, Source={x:Reference this}}" />
      </StackLayout>
  </ContentView.Content>
</ContentView>

I would like to know if there's a simpler way, so I don't have to add source to every single child that uses a BindableProperty..

Inrego
  • 1,524
  • 1
  • 15
  • 25
1

XAML

Let's take the XAML of your custom control as below:

<ContentView 
    xmlns="http://xamarin.com/schemas/2014/forms" 
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="Frida3.Components.TestView">
    <ContentView.Content>
        <StackLayout>
            <Label Text="{Binding Text}" />
        </StackLayout>
    </ContentView.Content>
</ContentView>

Adding bindings to your custom control adds another level of complexity to your app, which may hinder the app's performance if used excessively. You are better off rewriting the Label as:

<Label x:Name="MyLabel"/>

Now, the Label is accessible in the code-behind.

Code-Behind

You've set up the property correctly in your question as below:

public static readonly BindableProperty TextProperty =
    BindableProperty.Create(
        "Text", 
        typeof(string), 
        typeof(TestView), 
        default(string));

public string Text
{
    get => (string) GetValue(TextProperty);
    set => SetValue(TextProperty, value);
}

You can remove the BindingContext = this; from the constructor, because you won't be using any bindings.

In your TextProperty, we're going to add an event for the propertyChanged parameter, and define the event:

public static readonly BindableProperty TextProperty =
    BindableProperty.Create(
        propertyName: nameof(Text), 
        returnType: typeof(string), 
        declaringType: typeof(TestView), 
        defaultValue: default(string),
        propertyChanged: HandleTextPropertyChanged); // Property-changed handler!!

public string Text
{
    get => (string) GetValue(TextProperty);
    set => SetValue(TextProperty, value);
}

// Handler for when the Text property changes.
private static void HandleTextPropertyChanged(
    BindableObject bindable, object oldValue, object newValue)
{
    var control = (TestView)bindable;
    if (control != null)
    {
        control.MyLabel.Text = (string)newValue;
    }
}

What's happening here? Essentially, you've told the app

"When the Text property of TestView changes, set the Text property of MyLabel to the new string value".

Your code should now look like:

// BindableProperty for your Text property
public static readonly BindableProperty TextProperty =
    BindableProperty.Create(
        propertyName: nameof(Text), 
        returnType: typeof(string), 
        declaringType: typeof(TestView), 
        defaultValue: default(string),
        propertyChanged: HandleTextPropertyChanged); // Property-changed handler!!

// Text property of you TestView
public string Text
{
    get => (string) GetValue(TextProperty);
    set => SetValue(TextProperty, value);
}

// Constructor
public TestView ()
{
    InitializeComponent ();
}

// Handler for when the Text property changes.
private static void HandleTextPropertyChanged(
    BindableObject bindable, object oldValue, object newValue)
{
    var control = (TestView)bindable;
    if (control != null)
    {
        control.MyLabel.Text = (string)newValue;
    }
}

You can now call your custom view like so:

<components:TestView 
    Text="This is showing" />

<components:TestView 
    Text="{Binding SomeOtherTextProperty}" />

There you have it! No bindings, just good old-fashioned events.

Tom
  • 1,739
  • 15
  • 24
  • Aha! It just hit me that you probably meant like this. And then I came here to check if you replied, and you did - and confirmed my thoughts. Thank you for the in-depth answer! – Inrego Nov 30 '18 at 17:41
  • I ran into another issue with this approach. Maybe it's worth posting another question about. But when the Text property has been set through an app-wide style, the Text property is being set on TestView before MyLabel has been instantiated, which results in a null reference exception on the line setting control.MyLabel.Text = (string)newValue; Adding a null check, would probably mean the text property of the label will not end up with the value that I set in my style. – Inrego Nov 30 '18 at 18:22
  • I thought I could cleverly change my component to be completely C#-based, but ran into the same issue. See more about the same issue here: https://stackoverflow.com/questions/46523431/xamarin-forms-bindableproperty-changed-before-constructor Where a suggested workaround is using bindings. I'm confused :D – Inrego Nov 30 '18 at 19:31
  • I'm assuming as you've marked this as the correct answer you're no longer confused? – Tom Dec 03 '18 at 09:26
  • I guess I was a bit too quick on the correct answer button. In reality I am so far using the solution I posted on my own answer below. If you have a solution to the problems I posted regarding your answer, then I would love to see it. – Inrego Dec 03 '18 at 13:27