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.
.
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, useTryParseExact
Number
which allows both thousand and decimal separators. If you useNumberStyles.Float
the thousand separator fails, but exponents are allowed.TryParse(text, CultureInfo.InvariantCulture, out Decimal dec)
,
as the thousand separator, allowing1,0,0
.1,0,1
. The issue isn't the culture, it's the thousand separators