.NET MAUI Numeric Entry with a decimal enforcing Behavior containing a property that dynamically determines the number of decimal points not working

694 views Asked by At

I am following the MVVM design pattern for my .NET MAUI app, and have a DecimalDigitsBehavior behavior that is applied to my Entries:

                <Label Text="UNITS" StyleClass="InputHeader" />
                <Frame StyleClass="InputFrame">
                    <Entry Placeholder="0.0" Keyboard="Numeric" Text="{Binding My_Unit}">
                        <Entry.Behaviors>
                            <behaviors:DecimalDigitsBehavior MaxDecimalDigits="{Binding MaxUnitDecimalDigits}" />
                        </Entry.Behaviors>
                    </Entry>
                </Frame>

                <Label Text="DISCOUNT %" StyleClass="InputHeader" />
                <Frame StyleClass="InputFrame">
                    <Entry Placeholder="0.00" Keyboard="Numeric" Text="{Binding My_Discount}">
                        <Entry.Behaviors>
                            <behaviors:DecimalDigitsBehavior MaxDecimalDigits="2" />
                        </Entry.Behaviors>
                    </Entry>
                </Frame>

For clarity, I've included my second Entry using a static prop value "2" for MaxDecimalDigits, which works fine. But it does not work for my case of the first Entry, when I am using a Binding to determine that value.

This binding source is from an ObservableProperty, and is set on my page load. The below is from my corresponding ViewModel associated to the .xaml page:

    [ObservableProperty]
    private int maxUnitDecimalDigits;

    ...

    public void ApplyQueryAttributes(IDictionary<string, object> query)
    {
      ....
      MaxUnitDecimalDigits = myService.UnitSetting == "0.1" ? 1 : 0; // HERE, it should cause it to trigger, but it doesn't!
    }

Below is my custom decimal digits behavior for reference (just a typical behavior class using regex to validate):

using System.Text.RegularExpressions;

namespace MCoreMatter.Behaviors
{
    public class DecimalDigitsBehavior : Behavior<Entry>
    {
        // Create a BindableProperty for MaxDecimalDigits
        public static readonly BindableProperty MaxDecimalDigitsProperty =
            BindableProperty.Create(nameof(MaxDecimalDigits), typeof(int), typeof(DecimalDigitsBehavior), 2);

        public int MaxDecimalDigits
        {
            get => (int)GetValue(MaxDecimalDigitsProperty);
            set => SetValue(MaxDecimalDigitsProperty, value);
        }

        protected override void OnAttachedTo(Entry entry)
        {
            entry.TextChanged += OnEntryTextChanged;
            entry.Unfocused += OnEntryUnfocused;

            base.OnAttachedTo(entry);
        }

        protected override void OnDetachingFrom(Entry entry)
        {
            entry.TextChanged -= OnEntryTextChanged;
            entry.Unfocused -= OnEntryUnfocused;

            base.OnDetachingFrom(entry);
        }

        private void OnEntryTextChanged(object sender, TextChangedEventArgs args)
        {
            if (sender is Entry entry)
            {
                string pattern = $@"^-?\d*(\.\d{{0,{MaxDecimalDigits}}})?$";
                var isValid = Regex.IsMatch(args.NewTextValue, pattern);

                if (!isValid)
                {
                    entry.Text = args.OldTextValue;
                    entry.CursorPosition = entry.Text.Length;
                }
            }
        }

        private void OnEntryUnfocused(object sender, EventArgs args)
        {
            if (sender is Entry entry)
            {
                string text = entry.Text.TrimEnd('.');
                if (double.TryParse(text, out double value))
                {
                    entry.Text = string.Format("{0:0." + new string('0', MaxDecimalDigits) + "}", value);
                }
                else
                {
                    entry.Text = string.Format("{0:0." + new string('0', MaxDecimalDigits) + "}", 0);
                }
            }
        }
    }
}

I have added debugging statements to verify that the behaviors are being attached to the entries upon page load, but when my ObservableProperty MaxUnitDecimalDigits is updated, no changes to the behavior took effect (the field remains using 2 as per the default MaxUnitDecimalDigits prop value).

Appreciate the help!

2

There are 2 answers

3
Stephen Quan On BEST ANSWER

You could have multiple Entry inputs with differing MaxDecimalDigits applied. When your UnitSetting changes, it will update IsVisible to always ensure that the right Entry is shown. This works even better if you made a TwoWay binding to a string in your ViewModel so that a change in one Entry will be reflected in the others.

<Frame StyleClass="InputFrame">
    <Entry Placeholder="0.0" Keyboard="Numeric" Text="{Binding EntryText, Mode=TwoWay}" IsVisible="{Binding MaxDigits0}">
        <Entry.Behaviors>
            <behaviors:DecimalDigitsBehavior MaxDecimalDigits="0" />
        </Entry.Behaviors>
    </Entry>
</Frame>
<Frame StyleClass="InputFrame">
    <Entry Placeholder="0.0" Keyboard="Numeric" Text="{Binding EntryText, Mode=TwoWay}" IsVisible="{Binding MaxDigits1}">
        <Entry.Behaviors>
            <behaviors:DecimalDigitsBehavior MaxDecimalDigits="1" />
        </Entry.Behaviors>
    </Entry>
</Frame>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(MaxDigit0))]
[NotifyPropertyChangedFor(nameof(MaxDigit1))]
private int maxDigits = 1;

public bool MaxDigits0 => MaxDigits == 0;
public bool MaxDigits1 => MaxDigits == 1;

[ObservableProperty]
string entryText;
0
Liqun Shen-MSFT On

I suppose that may relate to Behavior binding context is always null. That means a null value may be passed to the MaxDecimalDigits since the BindingContext is null.

Please try the following code,

<Entry Placeholder="0.0" Keyboard="Numeric" Text="{Binding My_Unit}">
    <Entry.Behaviors>
        <behaviors:DecimalDigitsBehavior MaxDecimalDigits="{Binding BindingContext.MaxUnitDecimalDigits,Source={x:Reference this}}" />
    </Entry.Behaviors>
</Entry>

this is the name of the ContentPage,

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
         ...
         x:Name="this">

Now you could get the correct value from ViewModel.