Search code examples
c#xamlmauibindableproperty

StringFormat on bindable property not displaying additional text


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

Solution

  • 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;