StringFormat on bindable property not displaying additional text

72 Views Asked by At

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));
1

There are 1 best solutions below

1
Julian On BEST ANSWER

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;