DataBinding for custom ContentView works in Code behind, but not Xaml

187 Views Asked by At

I am trying to make a custom ContentView for a control that works similar to Bootstrap's Navigation Pills.

Here is the code for my custom view.

PillToggle.xaml.cs

public partial class PillToggle : ContentView
{

    public PillToggle()
    {
        InitializeComponent();
        this.BindingContext = this;
        // BindableLayout.SetItemsSource(this.PillsContainer, this.Pills);
    }

    public ObservableCollection<Pill> Pills
    {
        get { return (ObservableCollection<Pill>)GetValue(PillsProperty); }
        set { SetValue(PillsProperty, value); }
    }

    public static readonly BindableProperty PillsProperty =
        BindableProperty.Create("Pills", typeof(ObservableCollection<Pill>), typeof(PillToggle), defaultValue: new ObservableCollection<Pill>());
}

PillToggle.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:REDACTED"
             x:Class="REDACTED.PillToggle">
    <Frame BackgroundColor="#eeeeee"  CornerRadius="24" Padding="4" MinimumHeightRequest="48">
        <HorizontalStackLayout x:Name="PillsContainer" BindableLayout.ItemsSource="{Binding Pills}"  HorizontalOptions="Start" >
            <BindableLayout.ItemTemplate>
                <DataTemplate x:DataType="controls:Pill">
                   ... snip
                </DataTemplate>
            </BindableLayout.ItemTemplate>
        </HorizontalStackLayout>
    </Frame>
</ContentView>

Pill.cs

public partial class Pill : ObservableObject
{
    [ObservableProperty]
    public string? label;

    [ObservableProperty]
    private bool selected = false;
}

When I try to use this View in a ContentPage, I am unable to Bind the Pills Collection via XAML.

MyPageViewModel.cs

public partial class MyViewModel : ObservableObject
{
    //Hard coded for now, but may become dynamic
    public IEnumerable<Pill> MyPills { get; } = new List<Pill>()
    {
        new Pill() { Label = "Test 1" },
        new Pill() { Label = "Test 2" }
    };
}

MyPage.xaml (Usage)

<controls:PillToggle x:Name="MyToggle" Pills="{Binding MyPills}" >

MyPage.xaml.cs (Constructor)

public MyPage(MyViewModel viewModel)
{
    this.BindingContext = viewModel;
    InitializeComponent();
}

When I run my app, I don't see any pills. I can see the Views background color, but the HorizontalStackView appears to not be bound to any items.

If I remove the Binding from Xaml, and manually bind in the code behind constructor, it works.

MyToggle.Pills = new ObservableCollection<Controls.Pill>(viewModel.MyPills);

It also works if I manually add the pills like this.

<controls:PillToggle x:Name="MyToggle">
    <controls:PillToggle.Pills>
        <controls: Pill Label="Test 3" Selected="True" />
        <controls: Pill Label="Test 4" />
    </controls:PillToggle.Pills>
</controls:PillToggle>

How can I get Binding to work in Xaml for my custom ContentView?

2

There are 2 best solutions below

2
On BEST ANSWER

First of all, and this is my opinion, you should avoid have a bindingcontext in your control. It will make your life so much simpler. The viewmodel you have its bindingcontext inheriting from the ContentPage (View). Also you have mixed a lot of thing and made it much more harder than it is. So let us see how we can make this work.

First we have the ContentView logic, Code behind. You had your BindableProperty a bit wrong. It need to have the actual propertyname as it first parameter. Also the second pararmeter, the return type was wrong, much because you had mixed in ObservableCollection in all this. It just need a List. So the the code is down to:

public partial class PillToggle : ContentView
{
    public static readonly BindableProperty PillsProperty =
        BindableProperty.Create(nameof(PillsProperty), typeof(List<Pill>), typeof(PillToggle));

    public List<Pill> Pills
    {
        get => (List<Pill>) GetValue(PillsProperty);
        set => SetValue(PillsProperty, value);
    }

    public PillToggle()
    {
        InitializeComponent();
    }

    private void OnPillTapped(object sender, TappedEventArgs e)
    {
        var label = sender as Label;
        var selectedPill = label?.BindingContext as Pill;

        if (selectedPill != null)
        {
            foreach (var child in PillsContainer.Children)
            {
                if (child is Label pillLabel)
                {
                    pillLabel.BackgroundColor = Colors.White;
                }
            }

            label.BackgroundColor = Colors.LightGray;

        }
    }
}

If we go to the xaml page I have just changed the bindings and added a TapGestureRecognizer because you talked about be able to click on it.

<Frame
        Padding="4"
        BackgroundColor="#eeeeee"
        CornerRadius="24"
        MinimumHeightRequest="48">
    <HorizontalStackLayout
            x:Name="PillsContainer"
            BindableLayout.ItemsSource="{Binding Source={RelativeSource AncestorType={x:Type local:PillToggle}}, Path=Pills}"
            HorizontalOptions="Start">
        <BindableLayout.ItemTemplate>
            <DataTemplate x:DataType="models:Pill">
                <Label Text="{Binding Label}" >
                    <Label.GestureRecognizers>
                        <TapGestureRecognizer Tapped="OnPillTapped" NumberOfTapsRequired="1" />
                    </Label.GestureRecognizers>
                </Label>
            </DataTemplate>
        </BindableLayout.ItemTemplate>
    </HorizontalStackLayout>
</Frame>

I have made your model much simpler too.:

public partial class Pill
{
    public string Label { get; set; }

    public bool Selected { get; set; } = false;
}

If we now look at the implementation you have your View with the control

<controls:PillToggle x:Name="MyToggle" Pills="{Binding MyPills}" />

Which binds to MyPills that is in your ViewModel

0
On

I modify two things then your code works on my end,

1. Because Pills is typeof(ObservableCollection<Pill>. So in MyViewModel:

Please change

public IEnumerable<Pill> MyPills { get; } = new List<Pill>()

to:

public IEnumerable<Pill> MyPills { get; } = new ObservableCollection<Pill>()

2. In PillToggle.xaml.cs

change

this.BindingContext = this;

to

Content.BindingContext = this;

Hope it helps!