mauiculture

Decimal separator depending on the current Culture


In my .NET MAUI App, I can switch Culture internally. Depending on that, I wanted to change the decimal separator. In the Entrys I used a routine to only allow the right decimal separator on the TextChanged-Event so that the user just can insert the right Format, I support. This works finde so far:

public class DecimalSeparatorValidator
{
    private CultureInfo culture;

    public DecimalSeparatorValidator(CultureInfo culture)
    {
        this.culture = culture ?? throw new ArgumentNullException(nameof(culture));
    }

    public string ValidateAndReplaceDecimalSeparator(string input)
    {
        if (input == null)
        {
            throw new ArgumentNullException(nameof(input));
        }

        // Replace the wrong decimal separator
        var numberDecimalSeparator = culture.NumberFormat.NumberDecimalSeparator;
        var correctedInput = input.Replace(numberDecimalSeparator == "." ? "," : ".", numberDecimalSeparator);

        // Validate the input format with regex
        var match = Regex.Match(correctedInput, "^(0|0[.,][0-9]*|[1-9]+[.,]*[0-9]*)$");
        if (!match.Success)
        {
            // If the input does not match the desired format, return null
            return null;
        }

        return correctedInput;
    }
}

In XAML, I attached the following StringToDoubleConverter to parse the double:

Text="{Binding Item.Value, Converter={StaticResource StringToDoubleConverter}, Mode=TwoWay}"
public class StringToDoubleConverter : IValueConverter
{
    // Used when the Double Value is converted to the String
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value;
    }

    // Used when the String value from the Entry is converted back to the Double
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is string stringValue)
        {
            char decimalSeparator = GetDecimalSeparator(stringValue);
            stringValue = stringValue.Replace(decimalSeparator, CultureInfo.CurrentCulture.NumberFormat.CurrencyDecimalSeparator[0]);

            if (double.TryParse(stringValue, out double result))
            {
                return result;
            }
        }

        return 0.0; // Default value or handle error accordingly
    }

    private static char GetDecimalSeparator(string input)
    {
        foreach (char character in input)
        {
            if (!char.IsDigit(character))
            {
                return character;
            }
        }

        return '.';
    }
}

Unfortunately, when I switch my Culture internally this is not called anymore and the decimal separators in the View stay the same.

What I wanted to achieve is, that in the corresponding culture where a "." is used as decimal separator this is used when a user types "3.14" and when a "," is used it shows up as "3,14".

Is there a way, I can trigger that, when I change my Culture internally?


Solution

  • I want to point out one of the TryParse overloads takes in an IFormatProvider (see https://learn.microsoft.com/en-us/dotnet/api/system.double.tryparse?view=net-8.0#system-double-tryparse(system-string-system-iformatprovider-system-double@)):

    public static bool TryParse (string? s,
                                IFormatProvider? provider,
                                out double result);
    

    You should use this overload and remove your string.Replace logic:

    double.TryParse(stringValue, CultureInfo.CurrentCulture, out double result)
    

    As to your Binding issue, one of the tricks you can use for reacting to culture changes is to use MultiBinding and have culture as one of the dependent bindings. This is a dummy in your MultiBinding since we're using it as a trigger, we're not using its value:

    <Label.Text>
        <MultiBinding Converter="{StaticResource StringToDoubleConverter}">
            <Binding Path="Item.Value"/>
            <Binding Path="NumberCulture"/>
        <MultiBinding>
    </LabelText>
    

    That would require you to change the StringToDoubleConverter from an IValueConverter to an IMultiValueConverter, but, you would just take in the first value of the array given and convert as you are currently doing.

    As for the second parameter, its a way for you to use the OnPropertyChanged system to broadcast to all of the components that are number aware that you've changed CultureInfo.CurrentCulture. e.g.

    public CultureInfo NumberCulture => CultureInfo.CurrentCulture;
    
    public void ChangeNumberCulture(CultureInfo NewCultureInfo)
    {
        CultureInfo.CurrentCulture = NewCultureInfo;
        OnPropertyChanged(nameof(NumberCulture)); // broadcast to all MultiBindings a change in number culture has occurred.
    }
    

    I would recommend showing a Label all the time, and, having a gesture, say a click to change the visibility to an Entry field, so, most of the time, it's only necessary to have a OneWay binding to reflect the number format change.