1

Here's my code:

using System.Windows.Controls;

namespace MyTest.validations
{
    public class DecimalValidationRule : ValidationRule
    {
        public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
        {
            decimal valueParsed;
            string? valueString = value.ToString();
            if (decimal.TryParse(valueString, out valueParsed))
            {
                return new ValidationResult(true, null);
            }

            return new ValidationResult(false, "Inserisci un valore Decimale corretto");
        }
    }
}

if object's value (filled by a WPF's TextBox) its 1.0.1 (checked with debugger), the function TryParse return true (which is not a decimal).

Why? How can I correctly check if the value is decimal?

EDIT: using @Panagiotis Kanavos suggestion, I have problem than writing 1,0 (for example), which become 10, and not 1,0.

Here's my ViewModelData class:

using System.ComponentModel;

namespace MyTest.models
{
    public class ViewModelData : INotifyPropertyChanged
    {
        private decimal? valoreDecimal;
        public decimal? ValoreDecimal
        {
            get { return valoreDecimal; }
            set
            {
                valoreDecimal = value + 0.0m;
                OnPropertyChanged(nameof(ValoreDecimal));
            }
        }

        public event PropertyChangedEventHandler? PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

And that's the XAML:

<TextBox 
    x:Name="ValoreDecimal"
    Width="130" 
    Height="40"
    HorizontalAlignment="Left" 
    VerticalAlignment="Top" 
    Margin="22,70,0,0" 
    Grid.ColumnSpan="2">
    <TextBox.Text>
        <Binding Path="ValoreDecimal">
            <Binding.ValidationRules>
                <validators:DecimalValidationRule ValidationStep="RawProposedValue"/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
    <Validation.ErrorTemplate>
        <ControlTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>
                <AdornedElementPlaceholder x:Name="textBox" Grid.Column="0"/>
                <TextBlock Text="{Binding [0].ErrorContent}" Foreground="Red" Grid.Column="1" Margin="5,0,0,0"/>
            </Grid>
        </ControlTemplate>
    </Validation.ErrorTemplate>
</TextBox>

Once writing 1,0, it becomes now 10. Why?

9
  • 1
    I bet . is the grouping separator in your culture. In desktop applications use a numeric or masked control and prevent the use of grouping separators. You can control how numbers are parsed through the NumberStyles parameter up to a point. If you want stricter validation, use TryParseExact Apr 12 at 13:56
  • The default is Number which allows both thousand and decimal separators. If you use NumberStyles.Float the thousand separator fails, but exponents are allowed. Apr 12 at 14:00
  • You may perhaps also use TryParse(text, CultureInfo.InvariantCulture, out Decimal dec)
    – Clemens
    Apr 12 at 14:01
  • 1
    @Clemens that won't work except in the US. In which case it will treat , as the thousand separator, allowing 1,0,0. Apr 12 at 14:01
  • 1
    @Clemens try 1,0,1. The issue isn't the culture, it's the thousand separators Apr 12 at 14:02

2 Answers 2

3

Your problem consists of two different but related problems.

Problem 1: the explicit conversion using e.g. decimal.Parse (takes place in your ValidationRule and while this particular conversion is correct, it doesn't seem to satisfy your special requirements).
Problem 2: the implicit type conversion between XAML and C#, that is performed by the XAML engine (takes place while binding TextBox.Text to ViewModelData.ValoreDecimal).

Both problems are related in that they misinterpret the group separator for a decimal separator. The difference is that one is produced by an explicit client code conversion while the other is performed by an "implicit" framework conversion.

As has already been pointed out by other users, you are observing a culture issue where in some cultures the decimal separator is the . symbol while in others it is the , symbol. A decimal symbol, of course, is of numeric relevance.

A group separator on the other hand is of no numerical relevance as it is simply a notational convention that is supposed to improve the readability of huge numbers.

Now, the compiler itself does not store numbers using group separators. For example the following code won't compile:

// Does not compile
decimal value = 100,000.58;

Group separators are only possible on UI level number presentation i.e. as string representation of a number.
And because group separators are not relevant and not presentable as language primitive, they simply get stripped when a numeric string is converted to a numeric primitive like double or decimal.

For example: when . is the group separator and , the decimal separator then "12.500" is equal to 12500 or 12500,0

when , is the group separator and . the decimal separator then "1,0" is equal to 10 or 10.0

It's just a group separator which is of no numerical relevance and simply not represented by a computer (why would a processor require group separators to understand a number --> waste of register capacity), so it gets stripped.

I will fix problem #2 first as the fix will also impact the solution of problem #1.

Fix the implicit XAML type conversion (problem #2)

The XAML engine will use a CultureInfo object to perform the implicit type conversions. The default XAML culture is "en-US".
Now here is the caveat: the implicit XAML conversions will fail to produce correct results when the user's system culture is not "en-US".

Let's assume your numeric value is the input from the UI and the user's current system culture is not en-US (which is not very unlikely), then you are running into issues.

For example, when binding a TextBox.Text of type string to a NumerValue property of type decimal

<TextBox Text="{Binding NumberValue}" />
private decimal? numberValue;
public decimal? NumberValue
{
  get => this.numberValue; 
  set
  {
    this.numberValue = value;
    OnPropertyChanged(nameof(this.NumberValue));
  }
}

Then the XAML engine performs the implicit conversion from string to decimal. Now, when the user enters "1,0" into the TextBox then the XAML engine will convert the string to 10 (just dropping the group separators) - which is not the correct value.

This is because the XAML engine will use the FrameworkElement.Language property value to perform all implicit type conversions e.g., from double to string (for example when binding a string property to a double or like in your case a decimal property).
The default of the FrameworkElement.Language property is the "en-US" language.
That's also the reason why the CultureInfo parameter that is passed by the XAML engine to the IValueConverter or ValidationRule is always the "en-US" culture.
However, this little detail will break the numeric conversion (or alphanumeric in general as text e.g. string comparison is also affected) when the application runs on systems that are not using "en-US" as culture.

This means that the XAML engine uses the wrong (for non-US cultures) CultureInfo to convert the input. The XAML engine essentially is always calling Convert.ToDecimal with the "en-US" culture info:

// Returns 10
string value = Convert.ToDecimal("1,0", CultureInfo.GetCultureInfo("en-US"));

Again, the decimal separator is interpreted as group separator due to the used "en-US" culture. And therefore, the group separator gets stripped from the input, which results in an integer.

You are currently trying to fix it by adding 0.0m to the value with the goal of converting the integer value back to a decimal value. It won't work as it would simply convert 10 to 10.0 which is still not 1.0 and therefore wrong.

Suggested Fix

The most convenient solution is to configure the XAML engine to always use the correct default CultureInfo.
We can achieve this by overriding the default value of the FrameworkElement.Language dependency property. We can change default values of dependency properties by overriding the default property metadata.

A good place to override the property metadata is the entry point of the WPF application, the App class:

App.xaml.cs

public partial class App : Application
{
  static App()
  {
    // Set the WPF default CultureInfo to the system's current culture
    FrameworkElement.LanguageProperty.OverrideMetadata(
      typeof(FrameworkElement),
      new FrameworkPropertyMetadata(
        defaultValue: XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));
  }
}

Now, the framework will always use the correct CultureInfo. You can now rely on the CultureInfo parameters of methods like IValueConverter.Convert or ValidationRule.Validate.

Finally, remove the value + 0.0m from the property setter of the ViewModelData.ValoreDecimal property:

ViewModelData.cs

private decimal? valoreDecimal;
public decimal? ValoreDecimal
{
  get => this.valoreDecimal; 
  set
  {
    this.valoreDecimal = value;
    OnPropertyChanged(nameof(this.ValoreDecimal));
  }
}

Fix the input validation (problem #1)

By default, decimal.Parse and similar converters will correctly use the current culture for the conversion. decimal.Parse will internally use NumberFormatInfo.CurrentInfo, which is equivalent to CultureInfo.CurrentCulture.NumberFormatInfo. So, the parsing itself is correct. As explained before, the parser correctly identifies the . as group separator for your culture and converts 1.0.1 to 101.

"if object's value (filled by a WPF's TextBox) its 1.0.1 (checked with debugger), the function TryParse return true (which is not a decimal). Why? How can I correctly check if the value is decimal?"

However, you are obviously not satisfied that 101 passes as decimal number (although it does so for the compiler and in terms of numerology in general as 1 = 1.0). At this point your requirement is not clear as you have not clearly explained why the input is not considered valid in your case.

Suggested Fix

If you require the user to input a decimal separator then

  • consider to append it implicitly to the TextBox.Text value e.g., by using Binding.StringFormat (recommended)
  • or manually parse the string to identify the absence (and force the user to append e.g. .0)

The following example does not allow inputs like "1.0.1"´ or "1,0,1". On the other hand, "1.0.1,0"` would be valid:

public class DecimalValidationRule : ValidationRule
{
  public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
  {
    // Because ValoreDecimal is explicitly defined as nullable 
    // I assume that NULL is treated as an anticipated and valid value
    if (value is null)
    {
      return ValidationResult.ValidResult;
    }

    string valueString = value.ToString();

    // Because we have previously configured the framework's CultureInfo to be 
    // CultureInfo.CurrentCulture we can safely use 
    // the 'cultureInfo' parameter of the current method
    string[] valueSegments = valueString.Split(cultureInfo.NumberFormat.NumberDecimalSeparator, StringSplitOptions.RemoveEmptyEntries);
    bool hasZeroOrMultipleDeciamlSeparators = 
      valueSegments.Length == 1 || valueSegments.Length > 2;
    if (hasZeroOrMultipleDeciamlSeparators)
    {
      return new ValidationResult(false, "Inserisci un valore Decimale corretto");
    }

    // TryParse will naturally use the correct CultureInfo.CurrentCulture to parse the string. 
    // Nothing wrong here.
    return double.TryParse(valueString, out _)
      ? ValidationResult.ValidResult
      : new ValidationResult(false, "Inserisci un valore Decimale corretto");
  }
}

If you want to prevent the user from using group separators (e.g., to avoid decimal separator confusion) then manually parse the string

bool hasGroupSeparators = valueString.Contains(cultureInfo.NumberFormat.NumberGroupSeparator);

or configure the value parser by using an overload that allows to pass in NumberStyles flags.

The following example does not allow grouped numeric inputs like `"1.0.1"´:

public class DecimalValidationRule : ValidationRule
{
  public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
  {
    // Because ValoreDecimal is explicitly defined as nullable 
    // I assume that NULL is treated as an anticipated and valid value
    if (value is null)
    {
      return ValidationResult.ValidResult;
    }

    string valueString = value.ToString();

    // TryParse will naturally use the correct CultureInfo.CurrentCulture to parse the string.
    // However, because of the previous fix of the framework language
    // we can safely use the cultureInfo parameter.
    return double.TryParse(valueString, NumberStyles.Float, cultureInfo, out _)
      ? ValidationResult.ValidResult
      : new ValidationResult(false, "Inserisci un valore Decimale corretto");
  }
}

Or maybe you want to combine both conditions.

Remarks

The valoreDecimal = value + 0.0m; in the ViewModelData.ValoreDecimal property is not very nice. It's programmatically not relevant. The operation is only to format the value for the presentation in the view.
For this reason, I would format the value in the view e.g., by using Binding.StringFormat.

Applying the custom format specifier, for example "0.0######", will append a decimal place of value 0 if the provided value has no decimal place. You can use other specifiers like rounding to e.g. three decimal places:

<TextBox x:Name="ValoreDecimal">
  <TextBox.Text>
    <Binding Path="ValoreDecimal" 
             StringFormat="{}{0:0.0##########}">
      <Binding.ValidationRules>
        <validators:DecimalValidationRule ValidationStep="RawProposedValue"/>
      </Binding.ValidationRules>
    </Binding>
  </TextBox.Text>

  <Validation.ErrorTemplate>
    ...     
  </Validation.ErrorTemplate>
</TextBox>

You can also bind the TextBox to a string property. Then validate this property by implementing INotifyDataErrorInfo and if valid convert the value to a decimal and store in a decimal property or field that you use internally (outside the view).
How to add validation to view model properties or how to implement INotifyDataErrorInfo
This way you would eliminate 1) the binding validation and 2) the implicit XAML to C# conversion. You would have a clean separation between view presentation and data logic.

Another very clean solution is to create a custom Numeric TextBox class that extends TextBox and can gracefully implement the presentation rules and numeric input rules (without business logic related validation, of course). Like INotifyDataErrorInfo, this solution would clean up your XAML and significantly improves reusability and maintainability.

4
  • Thanks for the detailed answer. First question: why remove the value + 0.0m? It helps if ones write just 1 (so it completes with 1,0).
    – markzzz
    Apr 16 at 13:52
  • I have updated the answer to address your requirement. In general, this is a pure view related formatting problem. Programmatically there is no difference between 1 and 1,0. Therefore; I would not pollute my properties with such redundancies. Instead, I would use string formatting in the view (see my updated answer, at the end).
    – BionicCode
    Apr 16 at 20:01
  • You can also use an IValueConverter. Or you leave your property the way it was. The property itself is not the problem. The implicit conversion between the property and the XAML is the problem. Removing the +0.0m from the property is more a design issue but not necessary to fix your original problem. Setting FrameworkElement.Language in the App class constructor is the solution to focus on. It solves all your problems.
    – BionicCode
    Apr 17 at 5:13
  • Seems perfect! Thanks!
    – markzzz
    Apr 17 at 6:41
2

. is the thousand separator in half of the world by population, including Europe, so 1.0.1 is parsed as 101. The equivalent in the US would be 1,0,1. When you call TryParse(valueString, out valueParsed) the current user's Culture is used to parse the string.

Perhaps the safest way that isn't affected by the culture is to prevent the use of thousand separators. You can control what is allowed in a numeric string through the NumberStyles parameter. The default is Number which allows both thousand and decimal separators. You can use Float that only allows the decimal separator instead :

if( decimal.TryParse("1.0.1",NumberStyles.Float, CultureInfo.GetCultureInfo("it-IT"),out var d))
{
    Console.WriteLine(d);
}

Passing null instead of a specific CultureInfo uses the current culture:

if( decimal.TryParse("1.0.1",NumberStyles.Float, null,out var d))
{
    Console.WriteLine(d);
}
4
  • Still return "true" with "1.0.1" :O
    – markzzz
    Apr 12 at 15:11
  • Ooops, it should be Float. Number is the default Apr 12 at 15:12
  • I see. Now I don't understand why if I have a decimal 1,0, and I do +0.0m, it become 10.0? It seems that on set, value is 10, not 1,0...
    – markzzz
    Apr 12 at 15:21
  • I mean, on setter (set) 1,0 become 10. Why this?
    – markzzz
    Apr 13 at 18:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.