I am having trouble getting data binding to work with custom components.
I have created an IncrementValue property that gets incremented with every button click.
The changes are reflected when bound to a Label. However they do not work when I bind it to a Bindable property in a custom component.
In the example, I have built a custom component called Card
which has two bindable properties CardTitle
and CardIncrement
Is there something I'm missing as I'm new to MAUI and even Xamarin.
Github link of code snippets below: https://github.com/814k31/DataBindingExample
Card.xaml.cs
namespace DataBindingExample;
public partial class Card : VerticalStackLayout
{
public static readonly BindableProperty CardTitleProperty = BindableProperty.Create(nameof(CardTitle), typeof(string), typeof(Card), string.Empty);
public static readonly BindableProperty CardIncrementProperty = BindableProperty.Create(nameof(CardIncrement), typeof(int), typeof(Card), 0);
public string CardTitle
{
get => (string)GetValue(CardTitleProperty);
set => SetValue(CardTitleProperty, value);
}
public int CardIncrement
{
get => (int)GetValue(CardIncrementProperty);
set => SetValue(CardIncrementProperty, value);
}
public Card()
{
InitializeComponent();
BindingContext = this;
}
}
Card.xaml
<?xml version="1.0" encoding="utf-8" ?>
<VerticalStackLayout
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:databindingexample="clr-namespace:DataBindingExample"
x:DataType="databindingexample:Card"
x:Class="DataBindingExample.Card"
Spacing="25"
Padding="30,0"
VerticalOptions="Center"
BackgroundColor="red"
>
<Label
Text="{Binding CardTitle}"
SemanticProperties.HeadingLevel="Level1"
FontSize="32"
HorizontalOptions="Center"
/>
<Label
Text="{Binding CardIncrement}"
SemanticProperties.HeadingLevel="Level1"
FontSize="32"
HorizontalOptions="Center"
/>
</VerticalStackLayout>
MainPage.xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="DataBindingExample.MainPage"
xmlns:DataBindingExample="clr-namespace:DataBindingExample"
xmlns:ViewModels="clr-namespace:DataBindingExample.ViewModels"
x:DataType="ViewModels:MainPageViewModel"
>
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center"
>
<Label
Text="{Binding IncrementedValue}"
SemanticProperties.HeadingLevel="Level2"
FontSize="18"
HorizontalOptions="Center"
/>
<!-- Why doesnt this work? -->
<DataBindingExample:Card CardIncrement="{Binding IncrementedValue}" />
<Button
x:Name="CounterBtn"
Text="Click Me"
SemanticProperties.Hint="Counts the number of times you click"
Command="{Binding IncrementValueCommand}"
HorizontalOptions="Center"
/>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
When making a custom component (that includes XAML), DO NOT set BindingContext = this;
.
REASON: You want the component to use the SAME BindingContext as the page it is placed in. This happens automatically, if you do NOT set a BindingContext in the custom component.
HOWEVER, removing this line breaks all your component's xaml Bindings; you'll need to add something to the xaml, to fix this.
Or to put it another way: How refer to the card's Properties from its XAML? See the next section.
ACCESS COMPONENT PROPERTIES VIA x:Name
Solution: Give the custom component (card) an x:Name
, and make that the "Source" of those bindings:
<VerticalStackLayout
...
x:Name="me" <-- IMPORTANT! Change name as desired.
x:Class="DataBindingExample.Card"
>
...
<Label Text="{Binding CardIncrement, Source={x:Reference me}}"
...
Notice the two parts to this solution:
x:Name="mynamehere"
.Binding
, say that the component is the source:
, Source={x:Reference mynamehere}
.Source
before Path
: "{Binding Source={x:Reference me}, CardIncrement}"
Path
: "{Binding Source={x:Reference me}, Path=CardIncrement}"
OPTIONAL (RARE): If custom component has a "ViewModel":
IMPORTANT: Even if you are doing MVVM, a custom component usually DOES NOT have a viewmodel. Rather, its behavior is controlled by the containing page, using BindableProperties. As seen in code in question.
The technique shown here is a convenience, for those who have factored their component's data and data-manipulating methods into a separate ("viewmodel") class.
For BindableProperties to work, recall from above:
DO NOT set a BindingContext (so that component has easy access to the page's BindingContext).
So in this technique, we don't set the ViewModel as the BindingContext.
How access this ViewModel?
By saving it as a property of the component; e.g.:
public partial class MyComponent : ContentView
{
private MyViewModel VM;
public void MyComponent()
{
InitializeComponent();
VM = new MyViewModel();
//DO NOT DO "BindingContext = VM;"
}
public class MyViewModel : ObservableObject
{
[ObservableProperty]
SomeType someProperty; // This is field. Property "SomeProperty" is generated.
}
IMPORTANT: requires x:Name
defined on component. See earlier section of this answer.
Then in xaml, we access properties of VM, using .
notation:
<Label Text="{Binding VM.SomeProperty, Source={x:Reference me}}"
An alternative approach is Source={RelativeSource Self}
<SomeElement SomeAttribute="{Binding MyProperty, Source={RelativeSource Self}}" />
<-- OPTIONAL -->
<SomeElement SomeAttribute="{Binding VM.SomeProperty, Source={RelativeSource Self}}" />
I personally don’t use this syntax, because someone reading may not be sure what "Self" refers to. Is it "SomeElement" (the element containing the "Binding" expression)? Is it a surrounding "DataTemplate" (if any)? Is it the root of the XAML file? I’m not going to answer that; use x:Name instead.