So I have created a custom control in .net MAUI XAML with Bindable Properties.
For one, I wish to display a %
as a suffix after the text on a specific view, but the StringFormat doesn't display it.
My View element
<Entry
x:Name="PercentEntry"
Margin="10,0,10,0"
HorizontalOptions="Start"
IsReadOnly="True"
Text="{Binding Percent, Mode=OneWay, StringFormat='{0} %'}"
VerticalOptions="Center" />
My code
public static readonly BindableProperty PercentProperty = BindableProperty.Create(nameof(Percent), typeof(string), typeof(StatsControl),
propertyChanged: (bindable, oldValue, newValue) => {
var control = (StatsControl)bindable;
control.PercentEntry.Text = newValue as string;
});
public string Percent
{
get => GetValue(PercentProperty) as string;
set => SetValue(PercentProperty, value);
}
The actual value is correctly presented and works OK. But no other text I place in the StringFormat either before or after the value is shown!
Edit:
After much research I still can't really get this working with StringFormat
, so the only way around is really hardcoding the percent symbol where I need it.
My Custom control in full
<ContentView
x:Class="MathsForFlorence.Controls.StatsControl"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MathsForFlorence.Controls">
<Border
BackgroundColor="AliceBlue"
HorizontalOptions="Center"
Stroke="Black"
StrokeShape="RoundRectangle 10"
StrokeThickness="0"
WidthRequest="360">
<VerticalStackLayout HorizontalOptions="Center">
<Label
x:Name="TitleLabel"
Margin="5,5,0,0"
FontAttributes="Bold"
FontSize="14"
Text="{Binding Title, FallbackValue='Title'}" />
<FlexLayout
AlignContent="Stretch"
AlignItems="Stretch"
Direction="Row"
HorizontalOptions="CenterAndExpand"
JustifyContent="SpaceEvenly"
Wrap="Wrap">
<HorizontalStackLayout HorizontalOptions="Center">
<Label
Margin="10,0,10,0"
HorizontalOptions="Start"
Text="Correct:"
VerticalOptions="Center" />
<Entry
x:Name="CorrectEntry"
Margin="10,0,10,0"
HorizontalOptions="Start"
IsReadOnly="True"
Text="{Binding Correct, Source={RelativeSource AncestorType={x:Type local:StatsControl}}}"
VerticalOptions="Center" />
</HorizontalStackLayout>
<HorizontalStackLayout HorizontalOptions="Center">
<Label
Margin="10,0,10,0"
HorizontalOptions="Start"
Text="Wrong:"
VerticalOptions="Center" />
<Entry
x:Name="WrongEntry"
Margin="10,0,10,0"
HorizontalOptions="Start"
IsReadOnly="True"
Text="{Binding Wrong, FallbackValue='0'}"
VerticalOptions="Center" />
</HorizontalStackLayout>
<HorizontalStackLayout HorizontalOptions="Center">
<Label
Margin="10,0,10,0"
HorizontalOptions="Start"
Text="Percentage correct:"
VerticalOptions="Center" />
<Entry
x:Name="PercentEntry"
Margin="10,0,10,0"
HorizontalOptions="Start"
IsReadOnly="True"
Text="{Binding Percent, Source={x:Reference this}, FallbackValue='0%'}"
VerticalOptions="Center" />
</HorizontalStackLayout>
</FlexLayout>
</VerticalStackLayout>
</Border>
</ContentView>
Code for the control
public partial class StatsControl : ContentView
{
public static readonly BindableProperty CorrectProperty = BindableProperty.Create(nameof(Correct), typeof(int), typeof(StatsControl),
propertyChanged: (bindable, oldValue, newValue) => {
var control = (StatsControl)bindable;
control.CorrectEntry.Text = newValue.ToString();
});
public int Correct
{
get => (int)GetValue(CorrectProperty);
set
{
SetValue(CorrectProperty, value);
//OnPropertyChanged(nameof(Correct));
}
}
public static readonly BindableProperty WrongProperty = BindableProperty.Create(nameof(Wrong), typeof(int), typeof(StatsControl),
propertyChanged: (bindable, oldValue, newValue) => {
var control = (StatsControl)bindable;
control.WrongEntry.Text = newValue.ToString();
//control.WrongEntry.Text = String.Format(newValue as string, "{0}");
});
public int Wrong
{
get => (int)GetValue(WrongProperty);
set => SetValue(WrongProperty, value);
}
public static readonly BindableProperty PercentProperty = BindableProperty.Create(nameof(Percent), typeof(int), typeof(StatsControl),
propertyChanged: (bindable, oldValue, newValue) => {
var control = (StatsControl)bindable;
//control.PercentEntry.Text = (newValue as string) + "%";
control.PercentEntry.Text = String.Format("{0}%", newValue);
});
public int Percent
{
get => (int)GetValue(PercentProperty);
set => SetValue(PercentProperty, value);
}
public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string), typeof(StatsControl),
propertyChanged: (bindable, oldValue, newValue) => {
var control = (StatsControl)bindable;
control.TitleLabel.Text = newValue as string;
});
public string Title
{
get => GetValue(TitleProperty) as string;
set
{
SetValue(TitleProperty, value);
}
}
public StatsControl()
{
InitializeComponent();
}
}
How I use the control
<controls:StatsControl
x:Name="AdditionStatsControl"
Title="Addition"
Margin="0,5,0,0"
Correct="{Binding AdditionCorrect}"
Percent="{Binding AdditionPercentage}"
Wrong="{Binding AdditionWrong}" />
How I set the values in the ViewModel of the page
#region Properties - Addition
[ObservableProperty]
int additionCorrect;
[ObservableProperty]
int additionWrong;
[ObservableProperty]
int additionPercentage;
#endregion
//...
AdditionCorrect = await SumCorrect(MathOperator.Add);
AdditionWrong = await SumWrong(MathOperator.Add);
AdditionPercentage = Helpers.CalculatePercent(AdditionCorrect, AdditionWrong);
OnPropertyChanged(nameof(AdditionCorrect));
OnPropertyChanged(nameof(AdditionWrong));
OnPropertyChanged(nameof(AdditionPercentage));
The problem is that you're overwriting the binding expression by setting the Text
property in the code-behind of your control:
control.PercentEntry.Text = newValue as string;
You can either use a binding or you set the Text
property in the code-behind, you cannot do both.
What you're trying to do can be achieved in a much easier way than setting values in the code-behind of a custom control. You don't need the property change handlers for this, at all.
You can simplify your control's code-behind as follows:
public partial class StatsControl : ContentView
{
public static readonly BindableProperty CorrectProperty = BindableProperty.Create(nameof(Correct), typeof(int), typeof(StatsControl));
public static readonly BindableProperty WrongProperty = BindableProperty.Create(nameof(Wrong), typeof(int), typeof(StatsControl));
public static readonly BindableProperty PercentProperty = BindableProperty.Create(nameof(Percent), typeof(int), typeof(StatsControl));
public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string), typeof(StatsControl));
public int Correct
{
get => (int)GetValue(CorrectProperty);
set => SetValue(CorrectProperty, value);
}
public int Wrong
{
get => (int)GetValue(WrongProperty);
set => SetValue(WrongProperty, value);
}
public int Percent
{
get => (int)GetValue(PercentProperty);
set => SetValue(PercentProperty, value);
}
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public StatsControl()
{
InitializeComponent();
}
}
Then change your XAML to this:
<ContentView
x:Class="MathsForFlorence.Controls.StatsControl"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MathsForFlorence.Controls"
x:Name="MyStatsControl">
<Border
BackgroundColor="AliceBlue"
HorizontalOptions="Center"
Stroke="Black"
StrokeShape="RoundRectangle 10"
StrokeThickness="0"
WidthRequest="360">
<VerticalStackLayout HorizontalOptions="Center">
<Label
x:Name="TitleLabel"
Margin="5,5,0,0"
FontAttributes="Bold"
FontSize="14"
Text="{Binding Title, Source={x:Reference MyStatsControl}}" />
<FlexLayout
AlignContent="Stretch"
AlignItems="Stretch"
Direction="Row"
HorizontalOptions="CenterAndExpand"
JustifyContent="SpaceEvenly"
Wrap="Wrap">
<HorizontalStackLayout HorizontalOptions="Center">
<Label
Margin="10,0,10,0"
HorizontalOptions="Start"
Text="Correct:"
VerticalOptions="Center" />
<Entry
x:Name="CorrectEntry"
Margin="10,0,10,0"
HorizontalOptions="Start"
IsReadOnly="True"
Text="{Binding Correct, Source={x:Reference MyStatsControl}}"
VerticalOptions="Center" />
</HorizontalStackLayout>
<HorizontalStackLayout HorizontalOptions="Center">
<Label
Margin="10,0,10,0"
HorizontalOptions="Start"
Text="Wrong:"
VerticalOptions="Center" />
<Entry
x:Name="WrongEntry"
Margin="10,0,10,0"
HorizontalOptions="Start"
IsReadOnly="True"
Text="{Binding Wrong, Source={x:Reference MyStatsControl}}"
VerticalOptions="Center" />
</HorizontalStackLayout>
<HorizontalStackLayout HorizontalOptions="Center">
<Label
Margin="10,0,10,0"
HorizontalOptions="Start"
Text="Percentage correct:"
VerticalOptions="Center" />
<Entry
x:Name="PercentEntry"
Margin="10,0,10,0"
HorizontalOptions="Start"
IsReadOnly="True"
Text="{Binding Percent, Source={x:Reference MyStatsControl}, StringFormat='{}{0}%'}"
VerticalOptions="Center" />
</HorizontalStackLayout>
</FlexLayout>
</VerticalStackLayout>
</Border>
</ContentView>
What I've done here is I gave the entire control a name by assigning x:Name="MyStatsControl"
, that way, it can be used as a the source for be binding expression using x:Reference
, e.g.:
<Entry
x:Name="PercentEntry"
Margin="10,0,10,0"
HorizontalOptions="Start"
IsReadOnly="True"
Text="{Binding Percent, Source={x:Reference MyStatsControl}, StringFormat='{}{0}%'}"
VerticalOptions="Center" />
Now, the Text
property of the Entry doesn't get overwritten in the code-behind anymore, but it should still receive updates, when the bound Percent
property changes, while always displaying it in this format: 42%
.
You also don't need these calls:
OnPropertyChanged(nameof(AdditionCorrect));
OnPropertyChanged(nameof(AdditionWrong));
OnPropertyChanged(nameof(AdditionPercentage));
This is because those properties are already observable, so any change to them will trigger a notification.
If you need all properties to issue a PropertyChanged
notification when any one of them changes, then you can update your properties as follows:
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(AdditionWrong))]
[NotifyPropertyChangedFor(nameof(AdditionPercentage))]
int additionCorrect;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(AdditionCorrect))]
[NotifyPropertyChangedFor(nameof(AdditionPercentage))]
int additionWrong;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(AdditionCorrect))]
[NotifyPropertyChangedFor(nameof(AdditionWrong))]
int additionPercentage;