ValidationRule的Validate中decimal.TryParse对1.0.1返回true?及输入异常问题
问题1:decimal.TryParse为何对“1.0.1”返回true?
当WPF的TextBox传入值“1.0.1”时,decimal.TryParse(valueString, out valueParsed)返回true,但该字符串显然不是合法的decimal格式。
相关代码
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"); } } }
原因分析
decimal.TryParse默认使用NumberStyles.Number规则,该规则允许解析包含千位分隔符的字符串。如果你的系统文化(比如意大利语it-IT)使用点(.)作为千位分隔符、逗号(,)作为小数点,那么“1.0.1”会被TryParse的容错机制误解析:第一个点被识别为千位分隔符,“1.0”被解析为10,后续的“.1”被忽略或错误处理,最终返回true。
解决方法
使用更严格的NumberStyles限制解析规则,只允许正负号、数字和小数点,不允许千位分隔符:
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo) { decimal valueParsed; string? valueString = value.ToString(); // 定义严格的解析规则:允许正负号、小数点、前后空白 var strictStyles = System.Globalization.NumberStyles.AllowLeadingSign | System.Globalization.NumberStyles.AllowDecimalPoint | System.Globalization.NumberStyles.AllowLeadingWhite | System.Globalization.NumberStyles.AllowTrailingWhite; if (decimal.TryParse(valueString, strictStyles, cultureInfo, out valueParsed)) { return new ValidationResult(true, null); } return new ValidationResult(false, "Inserisci un valore Decimale corretto"); }
如果需要更极致的格式检查,可以结合正则表达式:
using System.Text.RegularExpressions; // ... // 匹配正负号开头,整数部分+可选小数部分的格式 if (Regex.IsMatch(valueString.Trim(), @"^[-+]?\d+(\.\d+)?$") && decimal.TryParse(valueString, strictStyles, cultureInfo, out valueParsed)) { return new ValidationResult(true, null); }
问题2:输入“1,0”后值变为10
采纳建议修改验证规则后,输入“1,0”(意大利语文化下的1.0)时,ViewModel中的值变成了10而非预期的1.0。
相关代码
ViewModelData类
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)); } } }
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>
原因分析
WPF绑定默认使用当前线程的UI文化,如果线程文化是InvariantCulture(或与输入格式不匹配的文化),就会出现解析错误:
- 意大利语文化(
it-IT)中,逗号(,)是小数点分隔符; - 而
InvariantCulture中,逗号是千位分隔符,因此“1,0”会被解析为10(把逗号当成千位分隔符,1,0即10)。
解决方法
在绑定中指定正确的ConverterCulture
修改XAML中的Binding,明确使用当前系统文化:<!-- 添加命名空间 --> xmlns:SystemGlobalization="clr-namespace:System.Globalization;assembly=mscorlib" <!-- 修改Binding --> <Binding Path="ValoreDecimal" ConverterCulture="{x:Static SystemGlobalization:CultureInfo.CurrentCulture}"> <Binding.ValidationRules> <validators:DecimalValidationRule ValidationStep="RawProposedValue"/> </Binding.ValidationRules> </Binding>简化ViewModel中的赋值逻辑
原代码中valoreDecimal = value + 0.0m;是多余的,直接赋值即可,该操作不会导致数值变化,但没必要保留:public decimal? ValoreDecimal { get { return valoreDecimal; } set { valoreDecimal = value; OnPropertyChanged(nameof(ValoreDecimal)); } }全局设置应用文化
在App.xaml.cs的OnStartup方法中统一设置应用的文化,确保数值解析和格式化一致:using System.Globalization; using System.Threading; using System.Windows; namespace MyTest { public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { var currentCulture = CultureInfo.CurrentCulture; Thread.CurrentThread.CurrentCulture = currentCulture; Thread.CurrentThread.CurrentUICulture = currentCulture; CultureInfo.DefaultThreadCurrentCulture = currentCulture; CultureInfo.DefaultThreadCurrentUICulture = currentCulture; base.OnStartup(e); } } }
内容的提问来源于stack exchange,提问作者markzzz

